120 votes

Cadre de repos Django : objets autoréférentiels imbriqués

J'ai un modèle qui ressemble à ceci :

class Category(models.Model):
    parentCategory = models.ForeignKey('self', blank=True, null=True, related_name='subcategories')
    name = models.CharField(max_length=200)
    description = models.CharField(max_length=500)

J'ai réussi à obtenir une représentation json plate de toutes les catégories avec le sérialiseur :

class CategorySerializer(serializers.HyperlinkedModelSerializer):
    parentCategory = serializers.PrimaryKeyRelatedField()
    subcategories = serializers.ManyRelatedField()

    class Meta:
        model = Category
        fields = ('parentCategory', 'name', 'description', 'subcategories')

Maintenant, ce que je veux faire, c'est que la liste des sous-catégories ait une représentation json en ligne des sous-catégories au lieu de leurs ids. Comment puis-je faire cela avec django-rest-framework ? J'ai essayé de le trouver dans la documentation, mais elle semble incomplète.

11voto

Stefan Reinhard Points 241

Une autre option serait de faire une récursion dans la vue qui sérialise votre modèle. Voici un exemple :

class DepartmentSerializer(ModelSerializer):
    class Meta:
        model = models.Department

class DepartmentViewSet(ModelViewSet):
    model = models.Department
    serializer_class = DepartmentSerializer

    def serialize_tree(self, queryset):
        for obj in queryset:
            data = self.get_serializer(obj).data
            data['children'] = self.serialize_tree(obj.children.all())
            yield data

    def list(self, request):
        queryset = self.get_queryset().filter(level=0)
        data = self.serialize_tree(queryset)
        return Response(data)

    def retrieve(self, request, pk=None):
        self.object = self.get_object()
        data = self.serialize_tree([self.object])
        return Response(data)

9voto

caipirginka Points 246

J'ai récemment rencontré le même problème et j'ai trouvé une solution qui semble fonctionner jusqu'à présent, même pour une profondeur arbitraire. Cette solution est une petite modification de celle de Tom Christie :

class CategorySerializer(serializers.ModelSerializer):
    parentCategory = serializers.PrimaryKeyRelatedField()

    def convert_object(self, obj):
        #Add any self-referencing fields here (if not already done)
        if not self.fields.has_key('subcategories'):
            self.fields['subcategories'] = CategorySerializer()      
        return super(CategorySerializer,self).convert_object(obj) 

    class Meta:
        model = Category
        #do NOT include self-referencing fields here
        #fields = ('parentCategory', 'name', 'description', 'subcategories')
        fields = ('parentCategory', 'name', 'description')
#This is not needed
#CategorySerializer.base_fields['subcategories'] = CategorySerializer()

Je ne suis pas sûr qu'il puisse fonctionner de manière fiable dans tout situation, cependant...

9voto

RTan Points 870

Cette solution est presque similaire aux autres solutions postées ici mais présente une légère différence en termes de problème de répétition d'enfants au niveau de la racine (si vous pensez que c'est un problème). Par exemple

class RecursiveSerializer(serializers.Serializer):
    def to_representation(self, value):
        serializer = self.parent.parent.__class__(value, context=self.context)
        return serializer.data

class CategoryListSerializer(ModelSerializer):
    sub_category = RecursiveSerializer(many=True, read_only=True)

    class Meta:
        model = Category
        fields = (
            'name',
            'slug',
            'parent', 
            'sub_category'
    )

et si vous avez cette vue

class CategoryListAPIView(ListAPIView):
    queryset = Category.objects.all()
    serializer_class = CategoryListSerializer

Cela donnera le résultat suivant,

[
{
    "name": "parent category",
    "slug": "parent-category",
    "parent": null,
    "sub_category": [
        {
            "name": "child category",
            "slug": "child-category",
            "parent": 20,  
            "sub_category": []
        }
    ]
},
{
    "name": "child category",
    "slug": "child-category",
    "parent": 20,
    "sub_category": []
}
]

Ici, le parent category a un child category et la représentation json est exactement ce que nous voulons qu'elle représente.

mais vous pouvez voir qu'il y a une répétition de la child category au niveau de la racine.

Comme certaines personnes le demandent dans les sections de commentaires des réponses postées ci-dessus que comment pouvons-nous arrêter cette répétition d'enfants au niveau de la racine ? il suffit de filtrer votre ensemble de requêtes avec parent=None comme le suivant

class CategoryListAPIView(ListAPIView):
    queryset = Category.objects.filter(parent=None)
    serializer_class = CategoryListSerializer

cela résoudra le problème.

REMARQUE : Cette réponse n'est peut-être pas directement liée à la question, mais le problème est en quelque sorte lié. De plus, cette approche consistant à utiliser RecursiveSerializer est coûteux. Il vaut mieux utiliser d'autres options qui sont sujettes aux performances.

6voto

Ceci est une adaptation de la solution caipirginka qui fonctionne sur drf 3.0.5 et django 2.7.4 :

class CategorySerializer(serializers.ModelSerializer):

    def to_representation(self, obj):
        #Add any self-referencing fields here (if not already done)
        if 'branches' not in self.fields:
            self.fields['subcategories'] = CategorySerializer(obj, many=True)      
        return super(CategorySerializer, self).to_representation(obj) 

    class Meta:
        model = Category
        fields = ('id', 'description', 'parentCategory')

Notez que le CategorySerializer de la 6e ligne est appelé avec l'objet et l'attribut many=True.

6voto

Will S Points 460

J'ai pensé que je pourrais participer à l'amusement !

Via wjin y Mark Chackerian J'ai créé une solution plus générale, qui fonctionne pour les modèles directs d'arbres et les structures d'arbres qui ont un modèle traversant. Je ne suis pas sûr que cela fasse partie de sa propre réponse, mais j'ai pensé que je pourrais le mettre quelque part. J'ai inclus une option max_depth qui empêchera la récursion infinie, au niveau le plus profond les enfants sont représentés comme des URL (c'est la clause finale else si vous préférez que ce ne soit pas une url).

from rest_framework.reverse import reverse
from rest_framework import serializers

class RecursiveField(serializers.Serializer):
    """
    Can be used as a field within another serializer,
    to produce nested-recursive relationships. Works with
    through models, and limited and/or arbitrarily deep trees.
    """
    def __init__(self, **kwargs):
        self._recurse_through = kwargs.pop('through_serializer', None)
        self._recurse_max = kwargs.pop('max_depth', None)
        self._recurse_view = kwargs.pop('reverse_name', None)
        self._recurse_attr = kwargs.pop('reverse_attr', None)
        self._recurse_many = kwargs.pop('many', False)

        super(RecursiveField, self).__init__(**kwargs)

    def to_representation(self, value):
        parent = self.parent
        if isinstance(parent, serializers.ListSerializer):
            parent = parent.parent

        lvl = getattr(parent, '_recurse_lvl', 1)
        max_lvl = self._recurse_max or getattr(parent, '_recurse_max', None)

        # Defined within RecursiveField(through_serializer=A)
        serializer_class = self._recurse_through
        is_through = has_through = True

        # Informed by previous serializer (for through m2m)
        if not serializer_class:
            is_through = False
            serializer_class = getattr(parent, '_recurse_next', None)

        # Introspected for cases without through models.
        if not serializer_class:
            has_through = False
            serializer_class = parent.__class__

        if is_through or not max_lvl or lvl <= max_lvl: 
            serializer = serializer_class(
                value, many=self._recurse_many, context=self.context)

            # Propagate hereditary attributes.
            serializer._recurse_lvl = lvl + is_through or not has_through
            serializer._recurse_max = max_lvl

            if is_through:
                # Delay using parent serializer till next lvl.
                serializer._recurse_next = parent.__class__

            return serializer.data
        else:
            view = self._recurse_view or self.context['request'].resolver_match.url_name
            attr = self._recurse_attr or 'id'
            return reverse(view, args=[getattr(value, attr)],
                           request=self.context['request'])

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X