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)
Related
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]
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')
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)
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()
So, I have a Django generic view:
class Foobaz(models.Model):
name = models.CharField(max_length=140)
organisation = models.ForeignKey(Organisation)
class FoobazForm(forms.ModelForm):
class Meta:
model = Foobaz
fields = ('name')
class FoobazCreate(CreateView):
form_class = FoobazForm
#login_required
def dispatch(self, *args, **kwargs):
return super(FoobazCreate, self).dispatch(*args, **kwargs)
What I'm trying to do is to take the organisation id from the URL:
/organisation/1/foobaz/create/
And add it back to the created object. I realise I can do this in CreateView.form_valid(), but from what I understand this is then completely unvalidated.
I've tried adding it to get_form_kwargs() but this does not expect the organisation kwarg as it is not in the included fields.
Ideally what I'd like to do is to add it to the instance of the form to validate it with the rest - ensuring it is a valid organisation, and that the user in question has the correct permissions to add a new foobaz to it.
I'm happy to just roll my own view if that is the best way of doing this, but I may just be simply missing a trick.
Thanks!
I thnik it would be better to include the organisation field and define it as hidden and readonly, this way django will validate it for you.
You can then override get_queryset method like this:
def get_queryset(self):
return Foobaz.objects.filter(
organisation__id=self.kwargs['organisation_id'])
where organisation_id is a keyword in url pattern.
You can do it overriding the View's get_kwargs() method and the Form's save() method. In the get_kwargs() I "inject" the organization_id into the initial data of the form, and in save() I retrieve the missing info with the supplied initial data:
In urls.py:
urlpatterns('',
#... Capture the organization_id
url(r'^/organisation/(?P<organization_id>\d+)/foobaz/create/',
FoobazCreate.as_view()),
#...
)
In views.py:
class FoobazCreate(CreateView):
# Override get_kwargs() so you can pass
# extra info to the form (via 'initial')
# ...(all your other code remains the same)
def get_form_kwargs(self):
# get CreateView kwargs
kw = super(CreateComment, self).get_form_kwargs()
# Add any kwargs you need:
kw['initial']['organiztion_id'] = self.kwargs['organization_id']
# Or, altenatively, pass any View kwarg to the Form:
# kw['initial'].update(self.kwargs)
return kw
In forms.py:
class FoobazForm(forms.ModelForm):
# Override save() so that you can add any
# missing field in the form to the model
# ...(Idem)
def save(self, commit=True):
org_id = self.initial['organization_id']
self.instance.organization = Organization.objects.get(pk=org_id)
return super(FoobazForm, self).save(commit=commit)