Snippets

Paul Atkin Django rest serializer for django-gm2m fields

Created by Paul Atkin last modified
from urllib.parse import urlparse
from django.core.urlresolvers import resolve
from django.utils import six
from rest_framework import serializers


class GenericRelatedField(serializers.StringRelatedField):
    """
    A custom field to use for serializing generic relationships.
    """
    def __init__(self, serializer_dict, *args, **kwargs):
        super(GenericRelatedField, self).__init__(*args, **kwargs)

        self.serializer_dict = serializer_dict
        for s in self.serializer_dict.values():
            s.bind('',self)

            
    def to_representation(self, instance):
        # find a serializer correspoding to the instance class
        for key in self.serializer_dict.keys():
            if isinstance(instance, key):
                # Return the result of the classes serializer
                return self.serializer_dict[key].to_representation(instance=instance)
        return '{}'.format(instance)

        
    def to_internal_value(self, data):
        # If provided as string, must be url to resource. Create dict containing just url
        if type(data) == str:
            data = {'url':data}
                
        # Existing resource can be specified as url
        if 'url' in data:
            # Extract details from the url and grab real object
            resolved_func, unused_args, resolved_kwargs = resolve(urlparse(data['url']).path)
            object=resolved_func.cls.queryset.get(pk=resolved_kwargs['pk'])
        else:
            # If url is not specified then object is new and must have a 'type' field to allow us to create correct object from list of serializers
            for key in self.serializer_dict.keys():
                if data['type'] == key.__name__:
                    object = key()
        
        # Deserialize data into attributes of object and apply
        obj_internal_value = self.serializer_dict[object.__class__].to_internal_value(data)
        for k,v in obj_internal_value.items():
            setattr(object,k,v)
        
        # Save object to store new or any updated attributes
        object.save()
        return object
        
        
        
class TypeField(serializers.Field):
    """
        Read only Field which displays the object type from the class name
    """
    def __init__(self, *args, **kwargs):
        
        kwargs['source'] = '__class__.__name__'
        kwargs['read_only'] = True
        super(TypeField, self).__init__(*args, **kwargs)
        
    def to_representation(self, value):
        return value
        
        
        
       


from urllib.parse import urlparse
from django.core.urlresolvers import resolve
from rest_framework import serializers
from rest-gm2m-field import TypeField,GenericRelatedField

class GenericHyperlinkedSerializer(serializers.HyperlinkedModelSerializer):
    """
        Serializer for models with generic relations.
        Includes a type field to allow GenericRelatedFields specified in the serializer to know how to serialize correctly.
        Derived from hyperlinked serializer and the url field must be present on the serializer
    """
    type = TypeField()

    def create(self, validated_data):        
        # Remove generic fields from validated data and add to separate dict
        related = {}
        for field in self._fields.keys():
            if type(self._fields[field]) == serializers.ManyRelatedField:
                if type( self._fields[field].child_relation) == GenericRelatedField:
                    related[field] = validated_data.pop(field)
                    
        # Create instance of serializers Meta.model
        instance = self.Meta.model.objects.create(**validated_data)
        
        # For all related fields attach the listed objects
        for field in related.keys():
            for object in related[field]:
                attr = getattr( instance, field )
                attr.add(object)

        return instance

        
    def update(self, instance, validated_data):
        
        # Create a dict of updatable fields 
        fields = {}
        generics = {}
        for field in self._fields.keys():
            # Exclude read only fields
            if not self._fields[field].read_only:
                if type(self._fields[field]) != serializers.ManyRelatedField:
                    fields[field] = self._fields[field]
                else:
                    # Exclude generics but add to separate dict
                    if type( self._fields[field].child_relation) != GenericRelatedField:
                        fields[field] = self._fields[field]
                    else:
                        generics[field] = self._fields[field]

        # Set all valid attributes of the instance to the validated data
        for field in fields.keys():
            setattr(instance, field, validated_data.get(field, getattr(instance,field))) 
        
        # Add any new generic relations
        for generic_attr in generics.keys():
            attr = getattr( instance, generic_attr )
            attr_objects = list(attr.all())
            for object in validated_data.get(generic_attr, getattr(instance,generic_attr)):
                if object not in attr_objects:
                    # If the object is not in the list of existing generic relations then add it
                    attr.add(object)
                else:
                    # If the object is already related then remove it from the list so we end up with a list of missing generic relations
                    attr_objects.remove(object)
            
            # Remove any missing generic relations if not a partial update (PUT but not PATCH)
            if not self.partial:
                for object in attr_objects:
                    attr.remove( object )
        
        instance.save()
        return instance   

Comments (0)

HTTPS SSH

You can clone a snippet to your computer for local editing. Learn more.