Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 455
sigmus Avatar asked Jul 19 '12 22:07

sigmus


2 Answers

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.

like image 128
enticedwanderer Avatar answered Oct 04 '22 07:10

enticedwanderer


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'
like image 32
Thomas - BeeDesk Avatar answered Oct 04 '22 06:10

Thomas - BeeDesk