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)
Related
I am having an issue when using the API to send an update to an existing record.
When I send the API for a new record, it works perfectly. But when I send it for an existing record, I would like it to update the current record, but it just gives me an integrity error instead.
My Serializers.py looks like this:
class PartSerializer(serializers.ModelSerializer):
part = serializers.CharField()
class Meta:
model = DocumentRef
fields = ('part', 'field1', 'field2', 'field3')
def create(self, validated_data):
part = Part.objects.get(part_number=validated_data['part'])
validated_data['part'] = part
return DocumentRef.objects.update_or_create(**validated_data)
I have tried changing update_or_create to just create or just update but it will still only work if the record does not exist yet.
The model it should be referencing is DocumentRef, which looks like this:
class DocumentRef(models.Model):
part = models.OneToOneField(Part, on_delete=models.CASCADE)
field1 = models.FileField(upload_to='mcp/')
field2 = models.FileField(upload_to='qcp/')
field3 = models.FileField(upload_to='cus/')
The API View I am using is this:
class APIDetailTest(APIView):
def get_object(self, pk):
try:
return DocumentRef.objects.get(pk=pk)
except DocumentRef.DoesNotExist:
return HttpResponse(status=status.HTTP_404_NOT_FOUND)
def get(self, request, pk):
part = self.get_object(pk)
serializer = PartSerializer(part)
return Response(serializer.data)
def put(self, request, pk):
part = self.get_object(pk)
serializer = PartSerializer(part, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Edit: Changed create_or_update to update_or_create -- Just made this error in this post, in my code it was correct from the beginning.
Edit2: Have also tried changing the return value to:
return DocumentRef.objects.update_or_create(defaults={'part_id': part.id}, field1=validated_data['field1'], field2=validated_data['field2'], field3=validated_data['field3']) but that still gives the unique constraint failed error.
This is more of a workaround than an answer, but you could try catching the error and treating the request differently.
something like this:
def create(self, validated_data):
...
try:
return DocumentRef.objects.create(**validated_data)
except IntegrityError:
DocumentRef.objects.filter(part=validated_data['part']).delete()
return DocumentRef.objects.create(**validated_data)
obviously, this is not updating the record. Just deleting the existing one and making a new one.
Try using it with defaults and kwargs please read here: django docs
The update_or_create method tries to fetch an object from database
based on the given kwargs. If a match is found, it updates the fields
passed in the defaults dictionary.
You need to update your query like this
def create(self, validated_data):
part = Part.objects.get(part_number=validated_data['part'])
return DocumentRef.objects.update_or_create(defaults={'part': part}, **validated_data)
My SerializerMethodField method is only printing HERE when I have a breakpoint at the return in the get method, and open the serializer variable after it has triggered OR when serializer.data is called (in which case it prints the expected data, but validated_data is still empty).
View:
class EventAddPeople(generics.GenericAPIView):
serializer_class = EventAddPeopleSerializer_Read
def get(self, request, *args, **kwargs):
serializer = EventAddPeopleSerializer_Read(data=request.GET)
serializer.is_valid(raise_exception=True)
print(serializer.validated_data)
return HttpResponse(serializer.validated_data)
Serializer:
class EventAddPeopleSerializer_Read(serializers.Serializer):
event_id = serializers.SerializerMethodField(method_name='get_event_id')
person_ids = serializers.SerializerMethodField()
def get_event_id(self, obj):
print("HERE")
return "TEST00"
def get_person_ids(self, obj):
print("HERE")
return "TEST00"
class Meta:
fields = ('event_id', 'person_ids')
Your get method is not called (may be).
check by just printing('anything') in your get method
check Methos for genericApiViews
Thanks
First thing, request.data is applicable for non-GET requests. You are not supposed to send data in the payload section with HTTP GET. If you want to send data with GET method, pass it through URL query parameters
So, the url will become, /api/my/end-point/?event_id=1&person_ids=3
and you need to pass this query param to serializer as,
serializer = EventAddPeopleSerializer_Read(data=request.GET)
Second thing, you've missed to add the Meta class in the serializer
class EventAddPeopleSerializer_Read(serializers.Serializer):
# other code
class Meta:
fields = ('event_id', 'person_ids')
Is there anyway to make the list_editable optional on a per object bases? For example the readonly fields attribute has this option, which doesn't affect the changelist_view.
class MyAdmin(admin.ModelAdmin):
readonly_fields = ('foo',)
def get_readonly_fields(self, request, obj=None):
fields = super(MyAdmin, self).get_readonly_fields(request, obj=obj)
if obj.status == 'CLOSED':
return fields + ('bar',)
return fields
The same can be achieved for list_display and some other attributes. It seems there isn't a method 'get_list_editable_fields'.
I want some of the rows to be immutable obviously, but other than raising a vulgar error doesn't seem to work. I didn't find any documentation about the attribute either
Would it somehow be possible to render the widget via a list_display getter?
class MyAdmin(admin.ModelAdmin):
list_display = ('get_bar',)
list_editable = ('get_bar',)
def get_bar(self, obj):
return widget or str(obj.bar) # ???
get_bar.allow_tags = True
update using Alasdair's feedback:
def get_changelist_formset(self, request, **kwargs):
"""
Returns a FormSet class for use on the changelist page if list_editable
is used.
"""
# I run through this code for each row in the changelist, but there's nothing in kwargs, so I don't know how to use the instance as a guide to which fields should be in list_editable?
defaults = {
"formfield_callback": partial(self.formfield_for_dbfield, request=request),
}
defaults.update(kwargs)
return modelformset_factory(
self.model, self.get_changelist_form(request), extra=0,
fields=self.list_editable, **defaults
)
As you say, there is no get_list_editable method.
Try overriding the get_changelist_formset method. I think you'll need to duplicate the entire method, and change the list of fields passed to modelformset_factory.
As said, there is not get_list_editable method in the ModelAdmin class, but it is possible to implement it easily (tested on django==2.1):
class MyAdminClass(admin.ModelAdmin):
def get_list_editable(self, request):
"""
get_list_editable method implementation,
django ModelAdmin doesn't provide it.
"""
dynamically_editable_fields = ('name', 'published', )
return dynamically_editable_fields
def get_changelist_instance(self, request):
"""
override admin method and list_editable property value
with values returned by our custom method implementation.
"""
self.list_editable = self.get_list_editable(request)
return super(MyAdminClass, self).get_changelist_instance(request)
Also, you could override the changelist_view and do something like that:
def changelist_view(self, request, extra_context=None):
resp = super(CustomModelAdmin, self).changelist_view(request, extra_context)
if something:
resp.context_data['cl'].formset = None
return resp
A little late but I found a way.
Override the get_changelist_instance()
def get_changelist_instance(self, request):
if request.user.is_superuser:
self.list_editable = ('state',) # replace state with list of fields you wish to be editable
else:
self.list_editable = ()
return super().get_changelist_instance(request)
It is better than overriding "get_changelist_formset" because get_changelist_formset() only runs if you have set list_editable to atleast one field. Link to Documentation
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)
I'm trying to make my User model RESTful via Django Rest Framework API calls, so that I can create users as well as update their profiles.
However, as I go through a particular verification process with my users, I do not want the users to have the ability to update the username after their account is created. I attempted to use read_only_fields, but that seemed to disable that field in POST operations, so I was unable to specify a username when creating the user object.
How can I go about implementing this? Relevant code for the API as it exists now is below.
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('url', 'username', 'password', 'email')
write_only_fields = ('password',)
def restore_object(self, attrs, instance=None):
user = super(UserSerializer, self).restore_object(attrs, instance)
user.set_password(attrs['password'])
return user
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
serializer_class = UserSerializer
model = User
def get_permissions(self):
if self.request.method == 'DELETE':
return [IsAdminUser()]
elif self.request.method == 'POST':
return [AllowAny()]
else:
return [IsStaffOrTargetUser()]
Thanks!
It seems that you need different serializers for POST and PUT methods. In the serializer for PUT method you are able to just except the username field (or set the username field as read only).
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
serializer_class = UserSerializer
model = User
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method == 'PUT':
serializer_class = SerializerWithoutUsernameField
return serializer_class
def get_permissions(self):
if self.request.method == 'DELETE':
return [IsAdminUser()]
elif self.request.method == 'POST':
return [AllowAny()]
else:
return [IsStaffOrTargetUser()]
Check this question django-rest-framework: independent GET and PUT in same URL but different generics view
Another option (DRF3 only)
class MySerializer(serializers.ModelSerializer):
...
def get_extra_kwargs(self):
extra_kwargs = super(MySerializer, self).get_extra_kwargs()
action = self.context['view'].action
if action in ['create']:
kwargs = extra_kwargs.get('ro_oncreate_field', {})
kwargs['read_only'] = True
extra_kwargs['ro_oncreate_field'] = kwargs
elif action in ['update', 'partial_update']:
kwargs = extra_kwargs.get('ro_onupdate_field', {})
kwargs['read_only'] = True
extra_kwargs['ro_onupdate_field'] = kwargs
return extra_kwargs
Another method would be to add a validation method, but throw a validation error if the instance already exists and the value has changed:
def validate_foo(self, value):
if self.instance and value != self.instance.foo:
raise serializers.ValidationError("foo is immutable once set.")
return value
In my case, I wanted a foreign key to never be updated:
def validate_foo_id(self, value):
if self.instance and value.id != self.instance.foo_id:
raise serializers.ValidationError("foo_id is immutable once set.")
return value
See also: Level-field validation in django rest framework 3.1 - access to the old value
My approach is to modify the perform_update method when using generics view classes. I remove the field when update is performed.
class UpdateView(generics.UpdateAPIView):
...
def perform_update(self, serializer):
#remove some field
rem_field = serializer.validated_data.pop('some_field', None)
serializer.save()
I used this approach:
def get_serializer_class(self):
if getattr(self, 'object', None) is None:
return super(UserViewSet, self).get_serializer_class()
else:
return SerializerWithoutUsernameField
UPDATE:
Turns out Rest Framework already comes equipped with this functionality. The correct way of having a "create-only" field is by using the CreateOnlyDefault() option.
I guess the only thing left to say is Read the Docs!!!
http://www.django-rest-framework.org/api-guide/validators/#createonlydefault
Old Answer:
Looks I'm quite late to the party but here are my two cents anyway.
To me it doesn't make sense to have two different serializers just because you want to prevent a field from being updated. I had this exact same issue and the approach I used was to implement my own validate method in the Serializer class. In my case, the field I don't want updated is called owner. Here is the relevant code:
class BusinessSerializer(serializers.ModelSerializer):
class Meta:
model = Business
pass
def validate(self, data):
instance = self.instance
# this means it's an update
# see also: http://www.django-rest-framework.org/api-guide/serializers/#accessing-the-initial-data-and-instance
if instance is not None:
originalOwner = instance.owner
# if 'dataOwner' is not None it means they're trying to update the owner field
dataOwner = data.get('owner')
if dataOwner is not None and (originalOwner != dataOwner):
raise ValidationError('Cannot update owner')
return data
pass
pass
And here is a unit test to validate it:
def test_owner_cant_be_updated(self):
harry = User.objects.get(username='harry')
jack = User.objects.get(username='jack')
# create object
serializer = BusinessSerializer(data={'name': 'My Company', 'owner': harry.id})
self.assertTrue(serializer.is_valid())
serializer.save()
# retrieve object
business = Business.objects.get(name='My Company')
self.assertIsNotNone(business)
# update object
serializer = BusinessSerializer(business, data={'owner': jack.id}, partial=True)
# this will be False! owners cannot be updated!
self.assertFalse(serializer.is_valid())
pass
I raise a ValidationError because I don't want to hide the fact that someone tried to perform an invalid operation. If you don't want to do this and you want to allow the operation to be completed without updating the field instead, do the following:
remove the line:
raise ValidationError('Cannot update owner')
and replace it with:
data.update({'owner': originalOwner})
Hope this helps!
More universal way to "Disable field update after object is created"
- adjust read_only_fields per View.action
1) add method to Serializer (better to use your own base cls)
def get_extra_kwargs(self):
extra_kwargs = super(BasePerTeamSerializer, self).get_extra_kwargs()
action = self.context['view'].action
actions_readonly_fields = getattr(self.Meta, 'actions_readonly_fields', None)
if actions_readonly_fields:
for actions, fields in actions_readonly_fields.items():
if action in actions:
for field in fields:
if extra_kwargs.get(field):
extra_kwargs[field]['read_only'] = True
else:
extra_kwargs[field] = {'read_only': True}
return extra_kwargs
2) Add to Meta of serializer dict named actions_readonly_fields
class Meta:
model = YourModel
fields = '__all__'
actions_readonly_fields = {
('update', 'partial_update'): ('client', )
}
In the example above client field will become read-only for actions: 'update', 'partial_update' (ie for PUT, PATCH methods)
This post mentions four different ways to achieve this goal.
This was the cleanest way I think: [collection must not be edited]
class DocumentSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
if 'collection' in validated_data:
raise serializers.ValidationError({
'collection': 'You must not change this field.',
})
return super().update(instance, validated_data)
Another solution (apart from creating a separate serializer) would be to pop the username from attrs in the restore_object method if the instance is set (which means it's a PATCH / PUT method):
def restore_object(self, attrs, instance=None):
if instance is not None:
attrs.pop('username', None)
user = super(UserSerializer, self).restore_object(attrs, instance)
user.set_password(attrs['password'])
return user
If you don't want to create another serializer, you may want to try customizing get_serializer_class() inside MyViewSet. This has been useful to me for simple projects.
# Your clean serializer
class MySerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = '__all__'
# Your hardworking viewset
class MyViewSet(MyParentViewSet):
serializer_class = MySerializer
model = MyModel
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method in ['PUT', 'PATCH']:
# setting `exclude` while having `fields` raises an error
# so set `read_only_fields` if request is PUT/PATCH
setattr(serializer_class.Meta, 'read_only_fields', ('non_updatable_field',))
# set serializer_class here instead if you have another serializer for finer control
return serializer_class
setattr(object, name, value)
This is the counterpart of getattr(). The
arguments are an object, a string and an arbitrary value. The string
may name an existing attribute or a new attribute. The function
assigns the value to the attribute, provided the object allows it. For
example, setattr(x, 'foobar', 123) is equivalent to x.foobar = 123.
class UserUpdateSerializer(UserSerializer):
class Meta(UserSerializer.Meta):
fields = ('username', 'email')
class UserViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
return UserUpdateSerializer if self.action == 'update' else super().get_serializer_class()
djangorestframework==3.8.2
I would suggest also looking at Django pgtrigger
This allows you to install triggers for validation. I started using it and was very pleased with its simplicity:
Here's one of their examples that prevents a published post from being updated:
import pgtrigger
from django.db import models
#pgtrigger.register(
pgtrigger.Protect(
operation=pgtrigger.Update,
condition=pgtrigger.Q(old__status='published')
)
)
class Post(models.Model):
status = models.CharField(default='unpublished')
content = models.TextField()
The advantage of this approach is it also protects you from .update() calls that bypass .save()