Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Infinite Recursion Fix #1

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions dynamic_rest/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@
# Enables caching of serializer fields to speed up serializer usage
# Needs to also be configured on a per-serializer basis
'ENABLE_FIELDS_CACHE': False,

# Controls maximum recursion depth to prevent infinite recursion
# in cases where two DynamicRelationFields interfere with each other
MAX_RECURSION_DEPTH: 5
}


Expand Down
39 changes: 22 additions & 17 deletions dynamic_rest/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,8 @@ def _build_requested_prefetches(
requirements,
model,
fields,
filters
filters,
recursion_depth=0
):
"""Build a prefetch dictionary based on request requirements."""

Expand Down Expand Up @@ -418,22 +419,24 @@ def _build_requested_prefetches(
# not conflict.
required = requirements.pop(source, None)

prefetch_queryset = self._build_queryset(
serializer=field,
filters=filters.get(name, {}),
queryset=related_queryset,
requirements=required
)
if recursion_depth < settings.MAX_RECURSION_DEPTH:
prefetch_queryset = self._build_queryset(
serializer=field,
filters=filters.get(name, {}),
queryset=related_queryset,
requirements=required,
recursion_depth=recursion_depth + 1
)

# Note: There can only be one prefetch per source, even
# though there can be multiple fields pointing to
# the same source. This could break in some cases,
# but is mostly an issue on writes when we use all
# fields by default.
prefetches[source] = self._create_prefetch(
source,
prefetch_queryset
)
# Note: There can only be one prefetch per source, even
# though there can be multiple fields pointing to
# the same source. This could break in some cases,
# but is mostly an issue on writes when we use all
# fields by default.
prefetches[source] = self._create_prefetch(
source,
prefetch_queryset
)

return prefetches

Expand Down Expand Up @@ -477,6 +480,7 @@ def _build_queryset(
requirements=None,
extra_filters=None,
disable_prefetches=False,
recursion_depth=0,
):
"""Build a queryset that pulls in all data required by this request.

Expand Down Expand Up @@ -539,7 +543,8 @@ def _build_queryset(
requirements,
model,
fields,
filters
filters,
recursion_depth=recursion_depth + 1
)

# build remaining prefetches out of internal requirements
Expand Down
18 changes: 18 additions & 0 deletions tests/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,24 @@ class Meta:
untrimmed_fields = ('name',)


class CatSerializer2(DynamicModelSerializer):
home = DynamicRelationField('LocationSerializer', link=None)
backup_home = DynamicRelationField(
'LocationSerializer', link=backup_home_link)
foobar = DynamicRelationField(
'LocationSerializer', source='hunting_grounds', many=True)
parent_id = DynamicRelationField('CatSerializer2', source='parent', read_only=True)
parent = DynamicRelationField('CatSerializer2', embed=True, deferred=True, read_only=True)

class Meta:
model = Cat
name = 'cat'
fields = ('id', 'name', 'home', 'backup_home', 'foobar', 'parent_id', 'parent')
deferred_fields = ('home', 'backup_home', 'foobar', 'parent')
immutable_fields = ('name',)
untrimmed_fields = ('name',)


class LocationSerializer(DynamicModelSerializer):

class Meta:
Expand Down
2 changes: 2 additions & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,5 @@
'ENABLE_LINKS': True,
'DEBUG': os.environ.get('DYNAMIC_REST_DEBUG', 'false').lower() == 'true'
}

MAX_RECURSION_DEPTH = 5
58 changes: 58 additions & 0 deletions tests/test_recurse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import datetime
import json

from django.db import connection
from django.test import override_settings
import six
from rest_framework.test import APITestCase

from tests.models import Cat, Group, Location, Permission, Profile, User
from tests.serializers import NestedEphemeralSerializer, PermissionSerializer
from tests.setup import create_fixture

UNICODE_STRING = six.unichr(9629) # unicode heart
# UNICODE_URL_STRING = urllib.quote(UNICODE_STRING.encode('utf-8'))
UNICODE_URL_STRING = '%E2%96%9D'


@override_settings(
DYNAMIC_REST={
'ENABLE_LINKS': False
}
)
class Tests(APITestCase):

def setUp(self):
self.fixture = create_fixture()
home_id = self.fixture.locations[0].id
backup_home_id = self.fixture.locations[1].id
parent = Cat.objects.create(
name='Parent 1',
home_id=home_id,
backup_home_id=backup_home_id
)
self.kitten = Cat.objects.create(
name='Kitten 1',
home_id=home_id,
backup_home_id=backup_home_id,
parent=parent
)

def test_put(self):
parent_id = self.kitten.parent_id
kitten_name = 'Kitten 1'
data = {
'name': kitten_name,
'home': self.kitten.home_id,
'backup_home': self.kitten.backup_home_id,
'parent': parent_id
}
response = self.client.put(
f'/cats2/{self.kitten.id}',
json.dumps(data),
content_type='application/json'
)
self.assertEqual(200, response.status_code)
data = json.loads(response.content.decode('utf-8'))
self.assertEqual(data['cat']['parent']['id'], parent_id)
self.assertEqual(data['cat']['name'], kitten_name)
1 change: 1 addition & 0 deletions tests/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

router.register(r'cars', viewsets.CarViewSet)
router.register(r'cats', viewsets.CatViewSet)
router.register(r'cats2', viewsets.CatViewSet2)
router.register_resource(viewsets.DogViewSet)
router.register_resource(viewsets.HorseViewSet)
router.register_resource(viewsets.PermissionViewSet)
Expand Down
6 changes: 6 additions & 0 deletions tests/viewsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
)
from tests.serializers import (
CarSerializer,
CatSerializer2,
CatSerializer,
DogSerializer,
GroupSerializer,
Expand Down Expand Up @@ -147,6 +148,11 @@ class CatViewSet(DynamicModelViewSet):
queryset = Cat.objects.all()


class CatViewSet2(DynamicModelViewSet):
serializer_class = CatSerializer2
queryset = Cat.objects.all()


class DogViewSet(DynamicModelViewSet):
model = Dog
serializer_class = DogSerializer
Expand Down