AX Semantics Developer Blog
  • Home
  • Categories
  • Tags
  • Archives

Write-once fields with Django Rest Framework

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
comments powered by Disqus

  • « testing content generation in docker
  • Unescaped UTF-8 in Django's admin with JSONField »

Published

Nov 7, 2016

Category

Techstack

Tags

  • django rest framework 1
  • python 8
  • AX Semantics Developer Blog - Technical stuff and learnings we had while developing the AX environment
  • Powered by Pelican. Theme: Elegant by Talha Mansoor