I've a serializer. I want to restrict updating a field. How would I do that?
class ABCSerializer(serializers.ModelSerializer):
class Meta:
"""Meta."""
model = ModelA
fields = ('colA', 'colB', 'colC',)
colA is a required field while creating the object. However, it should not be allowed to update. How can I do that??
Sounds like you need different serializers for PUT and POST methods. In the serializer for the PUT method you can set the colA field to readonly
class ABCViewSet(ModelViewSet):
serializer_class = ABCSerializer
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method == 'PUT':
serializer_class = SerializerWithReadOnlyColA
return serializer_class
You can use Django REST Frameworks field-level validation by validating that field has not changed on update like so:
from rest_framework.exceptions import ValidationError
class ABCSerializer(serializers.ModelSerializer):
colA = serializers.CharField(max_length=100)
def validate_colA(self, value):
if self.instance and self.instance.colA != value:
raise ValidationError("You may not edit colA")
return value
class Meta:
"""Meta."""
model = ModelA
fields = ('colA', 'colB', 'colC',)
This will check whether or not this is an update (via checking if an instance is populated on the serializer) and if so it will then check to see if you have made a change to the field and if you have it will throw a ValidationError. The benefit of this approach is that you can keep your view code the same as before and continue to keep your validation behaviour in your serializer.
You can override the serializer's update method to only update fields that you want.
class ABCSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
instance.colB = validated_data.get('colB', instance.colB)
instance.colC = validated_data.get('colC', instance.colC)
# do nothing to instance.colA
instance.save()
return instance
class Meta:
model = ModelA
fields = ('colA', 'colB', 'colC',)
Or if you have many fields, and just want to omit updating colA, you could write your update method like this:
def update(self, instance, validated_data):
validated_data.pop('colA') # validated_data no longer has colA
return super().update(instance, validated_data)
You can read more about overriding update here: https://www.django-rest-framework.org/api-guide/serializers/#saving-instances
I think it's too late to answer but this may be useful for others:)
you can solve your problem this way:
class ABCSerializer(serializers.ModelSerializer):
class Meta:
model = ModelA
fields = ('colA', 'colB', 'colC',)
def get_fields(self):
fields = super().get_fields()
if self.instance:
fields["colA"].read_only = True
return fields
When you want to create, the self.instance is None, it will pass the if clause, and in case of updating the if clause will make the field read only and non-editable.
You can do this with the read_only_fieldsoption
class ABCSerializer(serializers.ModelSerializer):
class Meta:
"""Meta."""
model = ModelA
fields = ('colB', 'colC',)
read_only_fields = ('colA',)
Related
I need to pass fields that are present in serializer, but not present in model to model save method (I have complicated saving logic and I want to make some decisions in object creation based on these fields). How can I do that? I tried to add
non_db_field = property to model, but I still get error MyModel() got an unexpected keyword argument 'negative_amount'
Let's say my model is
class MyModel(AbstractModel):
field1 = models.DateTimeField()
field2 = models.BigIntegerField()
My serializer is
class MyModelSerializer(AbstractSerializer):
field3 = serializers.BooleanField(required=False)
class Meta(AbstractSerializer.Meta):
model = MyModel
fields = '__all__'
And my viewset is
class MyModelViewSet(AbstractViewSet):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
You should handle this behavior in serializer.save method, for example, you can pop it from validated_data like that:
def save(self, **kwargs):
self.validated_data.pop("negative_amount")
return super().save(**kwargs)
You can use fields=['field1', 'field2', 'field3'] in serializer instead of fields='__all__'.
I found a solution based partly on Sharpek's answer and partly based on this answer:
In serializer I override save method:
def save(self, **kwargs):
if 'field3' in self.validated_data:
kwargs['field3'] = self.validated_data.pop('field3')
return super().save(**kwargs)
In models I override init method and define field:
field3 = None
def __init__(self, *args, **kwargs):
if 'field3' in kwargs:
self.field3 = kwargs.pop('field3')
super(Reading, self).__init__(*args, **kwargs)
I'm working with a finance application, and due to the way that floating point math works, have decided to store all values in the database as cents (so dollar amount * 100). I have been banging my head against a wall to get the serializer to perform two calculations for me. On create/update accept a float value but then before saving to the database do value*100. Then on get, do value/100.
I got it half working using a SerializerMethodField, but that seemed to remove my ability to do create/update actions. I also at one point had something that kind of worked for create/update by changing the serializer.save() method in the view and adding an IntegerField validator on the field, but then that broke the SerializerMethodField.
In short, I'm stuck. lol
Here is my very simple model:
class Items(models.Model):
user = models.ForeignKey(
'CustomUser',
on_delete=models.CASCADE,
)
name = models.CharField(max_length=60)
total = models.IntegerField()
My views for this item:
class GetItems(generics.ListCreateAPIView):
serializer_class = ItemsSerializer
permission_classes = [permissions.IsAuthenticated, IsAuthorOrDenied]
user = serializers.HiddenField(default=serializers.CurrentUserDefault(), )
def get_queryset(self):
user = self.request.user
return Items.objects.filter(user=user)
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class SingleItem(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ItemsSerializer
permission_classes = [permissions.IsAuthenticated, IsAuthorOrDenied]
user = serializers.HiddenField(default=serializers.CurrentUserDefault(), )
def get_queryset(self):
user = self.request.user
return Items.objects.filter(user=user)
def perform_update(self, serializer):
serializer.save(user=self.request.user)
And my serializer
class ItemsSerializer(serializers.ModelSerializer):
class Meta:
fields = ('id', 'name', 'budget_total')
model = models.Items
I feel like I should be doing more in my Serializer and less in my views, but that may be a completely different question all together.
Thanks in advance for the help!
Custom Serializer Field
You could write a custom field to handle the data:
class BudgetField(serializers.Field):
def to_representation(self, value):
# You can decide here how you want to return your data back
return value / 100
def to_internal_value(self, data):
# this will be passed to validated_data, so will be used to create/update instances
# you could do some validation here to make sure it is a float
# https://www.django-rest-framework.org/api-guide/fields/#raising-validation-errors
return int(data * 100)
Then use the custom field on your serializer.
class ItemsSerializer(serializers.ModelSerializer):
total = BudgetField()
class Meta:
fields = ('id', 'name', 'total')
model = models.Items
Override .update() and .create()
You could also choose to override these methods on the serializer.
class ItemsSerializer(serializers.ModelSerializer):
class Meta:
fields = ('id', 'name', 'budget_total')
model = models.Items
def create(self, validated_data):
# Modify validated_data with the value you need
return super().create(validated_data)
def update(self, instance, validated_data):
# Modify validated_data with the value you need
return super().update(instance, validated_data)
It's possible to change a serializer depth with a GET parameter? For example calling http://localhost:8000/api-auth/?depth=1
The proper way to pass some extra data from view to serializer is the serializer context.
In DRF class-based view, you can (and, actually, should for such purposes) override get_serializer_context() method. In the overridden method you just add to the context, which is just a dictionary, whatever you want.
Simple example, how to do this in view:
class YourView(generics.RetrieveAPIView):
def get_serializer_context(self):
context = super().get_serializer_context()
context['depth'] = self.request.query_params.get('depth', 1)
return context
And the to access it in serializer:
class YourSerializer(serializers.ModelSerializer):
the_depth_from_get_param = serializers.SerializerMethodField()
class Meta:
model = YourModel
fields = [
'the_depth_from_get_param'
]
def get_the_depth_from_get_param(self, obj):
return self.context['depth']
I've solved it in a really simple way, assuming that views are made with class based views or viewsets:
The serializer_class propriety it's actually the serializer, it's a class, so you can access to the depth value with serializer_class.Meta.depth, and assign it a value from the self.request.GET array
Example serializer:
class ItemSerializer(ModelSerializer):
class Meta:
model = Item
fields = '__all__'
Example view:
class ItemList(generics.ListCreateAPIView):
queryset = Item.objects.all()
serializer_class = serializers.ItemSerializer
def get_serializer_class(self):
if(self.request.GET.get('depth')):
if (10 >= int(self.request.GET.get('depth')) >= 0):
self.serializer_class.Meta.depth = int(
self.request.GET.get('depth'))
return self.serializer_class
My aim is to build endpoint which will surve to create objects of model with GenericForeignKey. Since model also includes ContentType, the actual type of model which we will reference is not known before object creation.
I will provide an example:
I have a 'Like' model which can reference a set of other models like 'Book', 'Author'.
class Like(models.Model):
created = models.DateTimeField()
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
Serializer may look like this:
class LikeSerializer(serializers.ModelSerializer):
class Meta:
model = models.Like
fields = ('id', 'created', )
What I want to achieve is to determine type of Like based on keys passed in request. The problem is that DRF do not pass those keys from request if they were not expilictly specified in Serializer fields. For example, POST request body contains:
{
"book":2
}
I want to do next
def restore_object(self, attrs, instance=None)
if attrs.get('book', None) is not None:
# create Like instance with Book contenttype
elif attrs.get('author', None) is not None:
# create Like instance with Author contenttype
In this case first if clause will be executed.
As you can see, The type determined based on key passed in request, without specifying special Field.
Is there any way to achieve this?
Thanks
You might try instantiating your serializer whenever your view is called by wrapping it in a function (you make a serializer factory):
def like_serializer_factory(type_of_like):
if type_of_like == 'book':
class LikeSerializer(serializers.ModelSerializer):
class Meta:
model = models.Like
fields = ('id', 'created', )
def restore_object(self, attrs, instance=None):
# create Like instance with Book contenttype
elif type_of_like == 'author':
class LikeSerializer(serializers.ModelSerializer):
class Meta:
model = models.Like
fields = ('id', 'created', )
def restore_object(self, attrs, instance=None):
# create Like instance with Author contenttype
return LikeSerializer
Then override this method in your view:
def get_serializer_class(self):
return like_serializer_factory(type_of_like)
Solution 1
Basically there is a method you can add on GenericAPIView class called get_context_serializer
By default your view, request and format class are passed to your serializer
DRF code for get_context_serializer
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}
you can override that on your view like this
def get_serializer_context(self):
data = super().get_serializer_context()
# Get the book from post and add to context
data['book'] = self.request.POST.get('book')
return data
And use this on your serializer class
def restore_object(self, attrs, instance=None):
# Get book from context to use
book = self.context.get('book', None)
author = attrs.get('author', None)
if book is not None:
# create Like instance with Book contenttype
pass
elif author is not None:
# create Like instance with Author contenttype
pass
Solution 2
Add a field on your serializer
class LikeSerializer(serializers.ModelSerializer):
# New field and should be write only, else it will be
# return as a serializer data
book = serializers.IntegerField(write_only=True)
class Meta:
model = models.Like
fields = ('id', 'created', )
def save(self, **kwargs):
# Remove book from validated data, so the serializer does
# not try to save it
self.validated_data.pop('book', None)
# Call model serializer save method
return super().save(**kwargs)
Here are the models. I need to avoid the reference of Filter objects in the FilterValue model which are being already referenced in the FilterValue model.
class Filter(models.Model):
name = models.CharField('Name', max_length=255)
class FilterValue(models.Model):
name = models.CharField('Name', max_length=255)
filter = models.ForeignKey(Filter, limit_choices_to=Q(***?***))
I'm looking for what could be the possible in place of ?.
As I understood from the OP's comment, the idea is to forbid adding duplicate entries.
But there is a more secure way of doing that:
class FilterValue(models.Model):
name = models.CharField('Name', max_length=255)
filter = models.ForeignKey(Filter)
class Meta:
unique_together = (("name", "filter"),)
The original solution will just display a list of Filters in the Admin, of in a Form, but will not actually forbid to add a duplicate programatically.
You can't do it in that way, but you can do it as part of a form. Specifically, during the __init__ method of a form, you can alter the queryset of a related field.
I wrote about how to do this in the admin at Filtering querysets in django.contrib.admin forms
I did it in another way by making FilterValueAdmin edit-only in admin and adding the same as inline in FilterAdmin model.
class FilterValueInline(admin.StackedInline):
formset = FilterValueInlineFormset
model = FilterValue
max_num = 1
can_delete = False
class FilterAdmin(admin.ModelAdmin):
list_display = ('id', 'name')
inlines = [FilterValueInline]
class FilterValueAdmin(admin.ModelAdmin):
"""Filter value has to be added via the filter table"""
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
actions = None
list_display = ('id', 'name', 'filter')