From 500f3f59d9d60a36dc1274e8194e2ff49709ae9c Mon Sep 17 00:00:00 2001 From: lukasbuenger Date: Tue, 2 Apr 2013 19:00:09 +0200 Subject: [PATCH 1/3] RecursiveRelatedField: First draft of implementation, tests and docs --- docs/api-guide/relations.md | 27 +++++ rest_framework/relations.py | 35 ++++++ rest_framework/tests/relations_recursive.py | 117 ++++++++++++++++++++ 3 files changed, 179 insertions(+) create mode 100644 rest_framework/tests/relations_recursive.py diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 623fe1a90..05b081e22 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -394,6 +394,33 @@ Note that reverse generic keys, expressed using the `GenericRelation` field, can For more information see [the Django documentation on generic relations][generic-relations]. + +## Recursive relationships + +If you want to serialize recursive relationships, you can use the `RecursiveRelatedField`. + +For example, given the following model that has a self-referencing foreign key to establish a tree-like structure: + + class TreeModel(models.Model): + + name = models.CharField(max_length=127) + parent = models.ForeignKey('self', null=True, related_name='children') + + def __unicode__(self): + return self.name + +You could have the child objects nested recursively with the following serializer: + + class TreeSerializer(serializers.ModelSerializer): + + children = RecursiveRelatedField(many=True) + + class Meta: + model = TreeModel + exclude = ('id', ) + +Note that as for now the the `RecursiveRelatedField` is read only. + --- ## Deprecated APIs diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 2a10e9af5..f982ad217 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -478,6 +478,41 @@ class HyperlinkedIdentityField(Field): raise Exception('Could not resolve URL for field using view name "%s"' % view_name) +### Recursive relationships + +class RecursiveRelatedField(RelatedField): + + def __init__(self, *args, **kwargs): + super(RecursiveRelatedField, self).__init__(*args, **kwargs) + # Forced read only. + self.read_only = True + + def field_to_native(self, obj, field_name): + + serializer_class = self.parent.__class__ + serializer = serializer_class() + serializer.initialize(self.parent, field_name) + + if self.many: + related_manager = getattr(obj, self.source or field_name) + if not obj.__class__ == related_manager.model: + raise Exception('`RecursiveRelatedField` must point at a self-referencing relation.') + queryset = related_manager.all() + return [serializer.to_native(item) for item in queryset] + + try: + queryset = getattr(obj, self.source or field_name) + if not obj.__class__ == queryset.__class__: + raise Exception('`RecursiveRelatedField` must point at a self-referencing relation.') + except ObjectDoesNotExist: + return None + return serializer.to_native(queryset) + + def to_native(self, value): + # Override to prevent simplifying process as present in `WritableField`. + return value + + ### Old-style many classes for backwards compat class ManyRelatedField(RelatedField): diff --git a/rest_framework/tests/relations_recursive.py b/rest_framework/tests/relations_recursive.py new file mode 100644 index 000000000..585ef8ab9 --- /dev/null +++ b/rest_framework/tests/relations_recursive.py @@ -0,0 +1,117 @@ +from __future__ import unicode_literals +from django.test import TestCase +from django.db import models +from rest_framework import serializers +from rest_framework.relations import RecursiveRelatedField + + +class TreeModel(models.Model): + + name = models.CharField(max_length=127) + parent = models.ForeignKey('self', null=True, related_name='children') + + def __unicode__(self): + return self.name + + +class TreeSerializer(serializers.ModelSerializer): + + children = RecursiveRelatedField(many=True) + + class Meta: + model = TreeModel + exclude = ('id', ) + + +class ChainModel(models.Model): + + name = models.CharField(max_length=127) + previous = models.OneToOneField('self', null=True, related_name='next') + + def __unicode__(self): + return self.name + + +class ChainSerializer(serializers.ModelSerializer): + + next = RecursiveRelatedField(many=False) + + class Meta: + model = ChainModel + exclude = ('id', ) + + +class TestRecursiveRelatedField(TestCase): + + def setUp(self): + self.tree_root = TreeModel.objects.create(name='Tree Root') + tree_depth_1_children = [] + + for x in range(0, 3): + tree_depth_1_children.append(TreeModel.objects.create(name='Child 1:%d' % x, parent=self.tree_root)) + + for x in range(0, 2): + TreeModel.objects.create(name='Child 2:%d' % x, parent=tree_depth_1_children[1]) + + self.chain_root = ChainModel.objects.create(name='Chain Root') + current = self.chain_root + for x in range(0, 3): + chain_link = ChainModel.objects.create(name='Chain link %d' % x, previous=current) + current = chain_link + + + def test_many(self): + serializer = TreeSerializer(self.tree_root) + expected = { + 'children': [ + { + 'children': [], + 'name': u'Child 1:0', + 'parent': 1 + }, + { + 'children': [ + { + 'children': [], + 'name': u'Child 2:0', + 'parent': 3 + }, + { + 'children': [], + 'name': u'Child 2:1', + 'parent': 3 + } + ], + 'name': u'Child 1:1', + 'parent': 1 + }, + { + 'children': [], + 'name': u'Child 1:2', + 'parent': 1 + } + ], + 'name': u'Tree Root', + 'parent': None + } + self.assertEqual(serializer.data, expected) + + def test_one(self): + serializer = ChainSerializer(self.chain_root) + expected = { + 'next': + { + 'next': + {'next': + {'next': None, + 'name': u'Chain link 2', + 'previous': 3}, + 'name': u'Chain link 1', + 'previous': 2}, + 'name': u'Chain link 0', + 'previous': 1}, + 'name': u'Chain Root', + 'previous': None + } + self.assertEqual(serializer.data, expected) + From 82f936b7ecc5cd8cd6a2e4daeee7e075d247cfc8 Mon Sep 17 00:00:00 2001 From: lukasbuenger Date: Tue, 2 Apr 2013 19:10:16 +0200 Subject: [PATCH 2/3] unicode errors fixed --- rest_framework/tests/relations_recursive.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/rest_framework/tests/relations_recursive.py b/rest_framework/tests/relations_recursive.py index 585ef8ab9..4e7854327 100644 --- a/rest_framework/tests/relations_recursive.py +++ b/rest_framework/tests/relations_recursive.py @@ -66,32 +66,32 @@ class TestRecursiveRelatedField(TestCase): 'children': [ { 'children': [], - 'name': u'Child 1:0', + 'name': 'Child 1:0', 'parent': 1 }, { 'children': [ { 'children': [], - 'name': u'Child 2:0', + 'name': 'Child 2:0', 'parent': 3 }, { 'children': [], - 'name': u'Child 2:1', + 'name': 'Child 2:1', 'parent': 3 } ], - 'name': u'Child 1:1', + 'name': 'Child 1:1', 'parent': 1 }, { 'children': [], - 'name': u'Child 1:2', + 'name': 'Child 1:2', 'parent': 1 } ], - 'name': u'Tree Root', + 'name': 'Tree Root', 'parent': None } self.assertEqual(serializer.data, expected) @@ -104,13 +104,13 @@ class TestRecursiveRelatedField(TestCase): 'next': {'next': {'next': None, - 'name': u'Chain link 2', + 'name': 'Chain link 2', 'previous': 3}, - 'name': u'Chain link 1', + 'name': 'Chain link 1', 'previous': 2}, - 'name': u'Chain link 0', + 'name': 'Chain link 0', 'previous': 1}, - 'name': u'Chain Root', + 'name': 'Chain Root', 'previous': None } self.assertEqual(serializer.data, expected) From 9c95d15cf24d19510fc5c074e9ddbfaeb557d77e Mon Sep 17 00:00:00 2001 From: lukasbuenger Date: Tue, 2 Apr 2013 19:27:06 +0200 Subject: [PATCH 3/3] Introducing max_depth argument --- docs/api-guide/relations.md | 6 ++++ rest_framework/relations.py | 9 +++++- rest_framework/tests/relations_recursive.py | 34 +++++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 05b081e22..ca7b58132 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -419,6 +419,12 @@ You could have the child objects nested recursively with the following serialize model = TreeModel exclude = ('id', ) +By default, the number of recursions is made until no further objects are found (`max_depth=-1`). + +However, you can restrict the number of recursions by passing the number of levels as `max_depth` argument: + + children = RecursiveRelatedField(many=True, max_depth=1) + Note that as for now the the `RecursiveRelatedField` is read only. --- diff --git a/rest_framework/relations.py b/rest_framework/relations.py index f982ad217..dc90c84cc 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -482,9 +482,10 @@ class HyperlinkedIdentityField(Field): class RecursiveRelatedField(RelatedField): - def __init__(self, *args, **kwargs): + def __init__(self, max_depth=-1, *args, **kwargs): super(RecursiveRelatedField, self).__init__(*args, **kwargs) # Forced read only. + self.max_depth = max_depth self.read_only = True def field_to_native(self, obj, field_name): @@ -493,6 +494,12 @@ class RecursiveRelatedField(RelatedField): serializer = serializer_class() serializer.initialize(self.parent, field_name) + if self.max_depth > -1: + if self.max_depth > 0: + serializer.fields[field_name].max_depth = self.max_depth - 1 + else: + return [] if self.many else None + if self.many: related_manager = getattr(obj, self.source or field_name) if not obj.__class__ == related_manager.model: diff --git a/rest_framework/tests/relations_recursive.py b/rest_framework/tests/relations_recursive.py index 4e7854327..fc21c3f08 100644 --- a/rest_framework/tests/relations_recursive.py +++ b/rest_framework/tests/relations_recursive.py @@ -23,6 +23,15 @@ class TreeSerializer(serializers.ModelSerializer): exclude = ('id', ) +class MaxDepthTreeSerializer(serializers.ModelSerializer): + + children = RecursiveRelatedField(many=True, max_depth=1) + + class Meta: + model = TreeModel + exclude = ('id', ) + + class ChainModel(models.Model): name = models.CharField(max_length=127) @@ -115,3 +124,28 @@ class TestRecursiveRelatedField(TestCase): } self.assertEqual(serializer.data, expected) + def test_max_depth(self): + serializer = MaxDepthTreeSerializer(self.tree_root) + expected = { + 'children': [ + { + 'children': [], + 'name': 'Child 1:0', + 'parent': 1 + }, + { + 'children': [], + 'name': 'Child 1:1', + 'parent': 1 + }, + { + 'children': [], + 'name': 'Child 1:2', + 'parent': 1 + } + ], + 'name': 'Tree Root', + 'parent': None + } + self.assertEqual(serializer.data, expected) +