Django Tastypie throws a 'maximum recursion depth exceeded' when full=True on reverse relation.
Asked Answered
M

3

7

I get a maximum recursion depth exceeded if a run the code below:

from tastypie import fields, utils
from tastypie.resources import ModelResource
from core.models import Project, Client


class ClientResource(ModelResource):
    projects = fields.ToManyField(
        'api.resources.ProjectResource', 'project_set', full=True
    )
    class Meta:
        queryset = Client.objects.all()
        resource_name = 'client'


class ProjectResource(ModelResource):
    client = fields.ForeignKey(ClientResource, 'client', full=True)
    class Meta:
        queryset = Project.objects.all()
        resource_name = 'project'

# curl http://localhost:8000/api/client/?format=json
# or
# curl http://localhost:8000/api/project/?format=json

If a set full=False on one of the relations it works. I do understand why this is happening but I need both relations to bring data, not just the "resource_uri". Is there a Tastypie way to do it? I managed to solve the problem creating a serialization method on my Project Model, but it is far from elegant. Thanks.

Mcreynolds answered 19/7, 2012 at 22:47 Comment(0)
P
13

You would have to override full_dehydrate method on at least one resource to skip dehydrating related resource that is causing the recursion.

Alternatively you can define two types of resources that use the same model one with full=Trueand another with full=False.

Proud answered 19/7, 2012 at 23:16 Comment(3)
Thanks for the two-resources tip, this helped me out a lot... :)Athirst
@MarkShust: I am facing similar kind of problem, can you please elaborate more on any of the method that worked for you ?Poppycock
@NikhilAgrawal if i remember right, i just made two resources, one with full=True, one with full=False, suffixed the full=False with Simple so i know which resource is what. then reference the appropriate resource.Athirst
F
3

Thanks @astevanovic pointing the right direction.

I found that overriding dehydrate method to process only some specified fields is a bit less tedious than overriding full_hydrate method to skip fields.

In the pursuit of reusability, I came up with the following code snippets. Hope it would be useful to some:

class BeeModelResource(ModelResource):

    def dehydrate(self, bundle):
        bundle = super(BeeModelResource, self).dehydrate(bundle)
        bundle = self.dehydrate_partial(bundle)        
        return bundle

    def dehydrate_partial(self, bundle):
        for field_name, resource_field in self.fields.items():
            if not isinstance(resource_field, RelatedField):
                continue

            if resource_field.full: # already dehydrated
                continue

            if not field_name in self._meta.partial_fields:
                continue

            if isinstance(resource_field, ToOneField):
                fk_object = getattr(bundle.obj, resource_field.attribute)
                fk_bundle = Bundle(obj=fk_object, request=bundle.request)
                fk_resource = resource_field.get_related_resource(fk_object)

                bundle.data[field_name] = fk_resource.dehydrate_selected( 
                        fk_bundle, self._meta.partial_fields[field_name]).data
            elif isinstance(resource_field, ToManyField):
                data = []

                fk_objects = getattr(bundle.obj, resource_field.attribute)
                for fk_object in fk_objects.all():
                    fk_bundle = Bundle(obj=fk_object, request=bundle.request)
                    fk_resource = resource_field.get_related_resource(fk_object)
                    fk_bundle = fk_resource.dehydrate_selected_fields( 
                            fk_bundle, self._meta.partial_fields[field_name])
                    data.append(fk_bundle.data)
                bundle.data[field_name] = data

        return bundle

    def dehydrate_selected_fields(self, bundle, selected_field_names):
        # Dehydrate each field.
        for field_name, field_object in self.fields.items():
            # A touch leaky but it makes URI resolution work. 
            # (borrowed from tastypie.resources.full_dehydrate)
            if field_name in selected_field_names and not self.is_special_fields(field_name):
                if getattr(field_object, 'dehydrated_type', None) == 'related':
                    field_object.api_name = self._meta.api_name
                    field_object.resource_name = self._meta.resource_name

                bundle.data[field_name] = field_object.dehydrate(bundle)

        bundle.data['resource_uri'] = self.get_resource_uri(bundle.obj)
        bundle.data['id'] = bundle.obj.pk

       return bundle

    @staticmethod
    def is_special_fields(field_name):
        return field_name in ['resource_uri']

With @sigmus' example, the resources will need 3 modifications:

  1. both resource will use BeeModuleResource as its super class (or, add dehydrate_partial to one resource and dehydrate_selected to the other.)
  2. unset full=True on either of the resource
  3. add partial_fields into the resource Meta the unset resource

```

class ClientResource(BeeModelResource): # make BeeModelResource a super class
    projects = fields.ToManyField(
        'api.resources.ProjectResource', 'project_set'
    ) # remove full=True
    class Meta:
        queryset = Client.objects.all()
        resource_name = 'client'
        partial_fields = {'projects': ['memo', 'title']} # add partial_fields

class ProjectResource(BeeModelResource): # make BeeModelResource a super class
    client = fields.ForeignKey(ClientResource, 'client', full=True)
    class Meta:
        queryset = Project.objects.all()
        resource_name = 'project'
Fourteenth answered 18/7, 2013 at 7:9 Comment(0)
C
3

Dead simple solution: set the use_in = 'list' kwarg on both relationship fields!

The docs: http://django-tastypie.readthedocs.org/en/latest/fields.html#use-in

Cupric answered 5/6, 2014 at 12:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.