A few days ago I found a bug in our API that allowed it to change a field that should be read only after create. After I fixed it I looked for similar bugs in our API. During this search I found four different implementations for write-once fields.
The examples assume that models named Collection
and Document
exist.
Document
has a ForeignKey
to Collection
.
Raise a validation error in the serializer
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)
Use a second serializer
DOCUMENT_READ_ONLY_FIELDS = ('id', 'whatever')
class DocumentView(viewsets.ModelViewSet):
serializer_class = DocumentSerializer
def get_serializer_class(self):
if self.request.method in ['PUT', 'PATCH']:
return DocumentUpdateSerializer
return self.serializer_class
class DocumentSerializer(serializers.ModelSerializer):
class Meta:
model = Document
fields = '__all__'
read_only_fields = DOCUMENT_READ_ONLY_FIELDS
class DocumentUpdateSerializer(DocumentSerializer):
class Meta(DocumentSerializer.Meta):
read_only_fields = DOCUMENT_READ_ONLY_FIELDS + ('collection', )
Override the value
class DocumentView(viewsets.ModelViewSet):
def update(self, request, *args, **kwargs):
document = self.get_object()
collection_pk = document.collection_id
request.data['collection'] = collection_pk
return super().update(request, *args, **kwargs)
Use a custom mixin
class DocumentSerializer(WriteOnceMixin, serializers.ModelSerializer):
class Meta:
model = Document
fields = '__all__'
read_only_fields = ('id', 'whatever')
write_once_fields = ('collection', )
Here's the implementation of the mixin:
class WriteOnceMixin:
"""Adds support for write once fields to serializers.
To use it, specify a list of fields as `write_once_fields` on the
serializer's Meta:
```
class Meta:
model = SomeModel
fields = '__all__'
write_once_fields = ('collection', )
```
Now the fields in `write_once_fields` can be set during POST (create),
but cannot be changed afterwards via PUT or PATCH (update).
Inspired by http://stackoverflow.com/a/37487134/627411.
"""
def get_extra_kwargs(self):
extra_kwargs = super().get_extra_kwargs()
# We're only interested in PATCH/PUT.
if 'update' in getattr(self.context.get('view'), 'action', ''):
return self._set_write_once_fields(extra_kwargs)
return extra_kwargs
def _set_write_once_fields(self, extra_kwargs):
"""Set all fields in `Meta.write_once_fields` to read_only."""
write_once_fields = getattr(self.Meta, 'write_once_fields', None)
if not write_once_fields:
return extra_kwargs
if not isinstance(write_once_fields, (list, tuple)):
raise TypeError(
'The `write_once_fields` option must be a list or tuple. '
'Got {}.'.format(type(write_once_fields).__name__)
)
for field_name in write_once_fields:
kwargs = extra_kwargs.get(field_name, {})
kwargs['read_only'] = True
extra_kwargs[field_name] = kwargs
return extra_kwargs
Note: The listed code examples are only extracts and do not represent the complete implementations of the methods. Except for the mixin.
Tell us what you think about this. Is something unclear? Do you have questions or ideas? Leave your comments below.
comments powered by Disqus