Django rest framework - как вы вставляете вложенные данные?

У меня есть "сквозная" модель, регулирующая отношения многих и многих, и я хочу иметь возможность возвращать "сквозную" модель и целевую модель в виде плоских данных, в отличие от размещения целевой модели.

Таким образом, используя стандартный пример для многих и многих с сквозными, скажем, это модели,

class Person(models.Model):
    first_name = models.CharField(max_length=128)
    last_name = models.CharField(max_length=128)
    favourite_food = models.CharField(max_length=128)

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Membership')

class Membership(models.Model):
    person = models.ForeignKey(Person)
    group = models.ForeignKey(Group)
    date_joined = models.DateField()
    invite_reason = models.CharField(max_length=64)

Итак, у сериализаторов, которые есть на данный момент, чтобы вернуть элементы членства,

class MembershipSerializer(serializers.HyperlinkedModelSerializer):
    person = PersonSerializer()

    class Meta:
        model = Membership
        fields = ('id', 'url', 'group', 'date_joined', 'invite_reason', 'person')

class PersonSerializer(serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Person
        fields = ('first_name', 'last_name', 'favourite_food')

Итак, когда я получаю модель членства с помощью MembershipSerializer, я получаю этот json,

{
    'id':1,
    'url':'http://cheeselovers.com/api/member/1/'
    'group':'http://cheeselovers.com/api/group/1/'
    'date_joined': '2014-01-24T16:33:40.781Z',
    'invite_reason': 'loves cheese',
    'person':{
        'first_name':'Barry',
        'last_name':'CheeseLover',
        'favourite_food': 'cheese'
    }
}

но я бы хотел вернуть это,

{
    'id':1,
    'url':'http://cheeselovers.com/api/member/1/'
    'group':'http://cheeselovers.com/api/group/1/'
    'date_joined': '2014-01-24T16:33:40.781Z',
    'invite_reason': 'loves cheese',
    'first_name':'Barry',
    'last_name':'CheeseLover',
    'favourite_food': 'cheese'
}

Теперь я понимаю, что я мог бы просто выполнить это, изменив для этого членство в Сериализаторе,

class MembershipSerializer(serializers.HyperlinkedModelSerializer):
    first_name = serializers.Field(source='person.first_name')
    last_name = serializers.Field(source='person.last_name')
    favourite_food = serializers.Field(source='person.favourite_food')

    class Meta:
        model = Membership
        fields = ('id', 'url', 'group', 'date_joined', 'invite_reason', 'first_name', 'last_name', 'favourite_food')

НО, целевая модель я имеет 10 свойств, а промежуточная 'сквозная' модель имеет только реквизиты только для чтения, поэтому у меня уже есть функционирующий сериализатор для целевой модели, который использовался при создании промежуточной модели.

Чуть более СУХОЙ, чтобы иметь возможность повторно использовать это, так что, если что-либо изменится на целевую модель, я должен внести изменения в это сериализатор, чтобы эти изменения отражались в данных, возвращаемых промежуточным сериализатором.

Итак, есть ли способ получить данные из PersonSerializer и добавить их в данные членства, чтобы он был плоским, а не вложенным?

... надеюсь, что все имеет смысл.

Ответ 1

Я не уверен, что это самый простой способ, но решение, с которым я столкнулся, состояло в том, чтобы переопределить метод to_native MemberhipSerializer, а затем вручную создать и вызвать метод to_native PersonSerializer и объединить два результирующих словаря вместе

class MembershipSerializer(serializers.HyperlinkedModelSerializer):

    def to_native(self, obj):

        ret = super(MembershipSerializer, self).to_native(obj)
        p_serializer = PersonSerializer(obj.person, context=self.context)
        p_ret = p_serializer.to_native(obj.person)

        for key in p_ret:
            ret[key] = p_ret[key]

        return ret

    class Meta:
        model = Membership
        fields = ('id', 'url', 'group', 'date_joined', 'invite_reason', 'person')

Словарь является подклассом SortedDict. Я не уверен, есть ли явный метод для объединения двух, которые сохраняют порядок, поэтому я только использовал цикл.

Ответ 2

Здесь подход, основанный на ответе Джеймса, но для более новой версии Django Rest Framework и поддержки чтения и записи (обновление только вложенного поля, должно быть достаточно просто добавить создание, см. документацию по DRF для этого.)

class ProfileSerializer(serializers.ModelSerializer):
    class Meta:
        model = Profile
        fields = ('phone', 'some', 'other', 'fields')


class UserDetailsSerializer(serializers.ModelSerializer):
    """User model with Profile. Handled as a single object, profile is flattened."""
    profile = ProfileSerializer()

    class Meta:
        model = User
        fields = ('username', 'email', 'profile')
        read_only_fields = ('email', )

    def to_representation(self, obj):
        """Move fields from profile to user representation."""
        representation = super().to_representation(obj)
        profile_representation = representation.pop('profile')
        for key in profile_representation:
            representation[key] = profile_representation[key]

        return representation

    def to_internal_value(self, data):
        """Move fields related to profile to their own profile dictionary."""
        profile_internal = {}
        for key in ProfileSerializer.Meta.fields:
            if key in data:
                profile_internal[key] = data.pop(key)

        internal = super().to_internal_value(data)
        internal['profile'] = profile_internal
        return internal

    def update(self, instance, validated_data):
        """Update user and profile. Assumes there is a profile for every user."""
        profile_data = validated_data.pop('profile')
        super().update(instance, validated_data)

        profile = instance.profile
        for attr, value in profile_data.items():
            setattr(profile, attr, value)
        profile.save()

        return instance

Ответ 3

Ответ Джеймса - то, что я наконец использовал. Поскольку у меня было несколько сериализаторов, использующих этот метод, я преобразовал его в миксин:

class FlattenMixin(object):
    """Flatens the specified related objects in this representation"""
    def to_representation(self, obj):
        assert hasattr(self.Meta, 'flatten'), (
            'Class {serializer_class} missing "Meta.flatten" attribute'.format(
                serializer_class=self.__class__.__name__
            )
        )
        # Get the current object representation
        rep = super(FlattenMixin, self).to_representation(obj)
        # Iterate the specified related objects with their serializer
        for field, serializer_class in self.Meta.flatten:
            serializer = serializer_class(context = self.context)
            objrep = serializer.to_representation(getattr(obj, field))
            #Include their fields, prefixed, in the current   representation
            for key in objrep:
                rep[field + "__" + key] = objrep[key]
        return rep

Таким образом, вы можете сделать что-то вроде:

class MembershipSerializer(FlattenMixin, serializers.HyperlinkedModelSerializer):
    class Meta:
        model = Membership
        fields = ('id', 'url', 'group', 'date_joined', 'invite_reason')
        flatten = [ ('person', PersonSerializer) ]

Ответ 4

Я не пробовал это с помощью HyperlinkedModelSerializer, но с ModelSerializer вы можете создать собственный класс serializer, поддерживающий flatten.

class CustomModelSerializer(serializers.ModelSerializer):
    def __init__(self, *args, **kwargs):
        self.flatten = kwargs.pop('flatten', False)
        super(CustomModelSerializer, self).__init__(*args, **kwargs)

    def get_fields(self):
        fields = super(CustomModelSerializer, self).get_fields()
        for field_name, field in fields.items():
            if getattr(field, 'flatten', False):
                del fields[field_name]
                for nested_field_name, nested_field in field.fields.iteritems():
                    nested_field.source = (field_name + '.' +
                                           (nested_field.source or nested_field_name))
                    fields[nested_field_name] = nested_field
        return fields

Использование:

class MembershipSerializer(CustomModelSerializer):
    person = PersonSerializer(flatten=True)

    class Meta:
        model = Membership
        fields = ('person', ...)


class PersonSerializer(CustomModelSerializer):

    class Meta:
        model = Person
        fields = (...)