Skip to content

Instantly share code, notes, and snippets.

@dhui
Created April 23, 2012 01:46
Show Gist options
  • Save dhui/2468156 to your computer and use it in GitHub Desktop.
Save dhui/2468156 to your computer and use it in GitHub Desktop.
Tastypie Hierarchical Resource
from tastypie.fields import ToOneField
from resources_base import CustomModelResource, ChildResource
from models import User, Post
class UserResource(CustomModelResource):
class Meta:
queryset = User.objects.all()
resource_name = "users"
fields = ["id", "name"]
class PostResource(ChildResource):
user = ToOneField(UserResource, "user")
class Meta:
queryset = Post.objects.all()
resource_name = "posts"
parent = "user"
fields = ["id", "content"]
import re
from django.conf.urls import patterns, include, url
from django.core.urlresolvers import RegexURLPattern, RegexURLResolver
from tastypie.bundle import Bundle
from tastypie.constants import ALL, ALL_WITH_RELATIONS
from tastypie.exceptions import TastypieError, BadRequest, ApiFieldError
from tastypie.fields import RelatedField
from tastypie.resources import ModelResource
from tastypie.utils import trailing_slash
def rreplace(string, old, new, count):
"""
Replaces a string starting from the right side
"""
return string[::-1].replace(old[::-1], new[::-1], count)[::-1]
class CustomModelResource(ModelResource):
def base_urls(self):
"""
Exactly the same as ModelResource.base_urls() but removes the '/' from primary key matching
Note: This is needed for nesting resources
"""
return [
url(r"^(?P<resource_name>%s)%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('dispatch_list'), name="api_dispatch_list"),
url(r"^(?P<resource_name>%s)/schema%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('get_schema'), name="api_get_schema"),
url(r"^(?P<resource_name>%s)/set/(?P<pk_list>\w[\w;-]*)/$" % self._meta.resource_name, self.wrap_view('get_multiple'), name="api_get_multiple"),
url(r"^(?P<resource_name>%s)/(?P<pk>\w[\w-]*)%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('dispatch_detail'), name="api_dispatch_detail"),
]
class ChildResource(CustomModelResource):
"""
A Tastypie resource that support hierarchy.
To use, just have your Resource inherit from ChildResource and set the parent attribute in the Meta class to a RelatedField in your Resource. The RelatedField resource must be a CustomModelResource.
"""
class Meta:
parent = "" # Should be overwritten with name of the RelatedField you want to be nested under
def __init__(self, *args, **kwargs):
super(ChildResource, self).__init__(*args, **kwargs)
# Set the parent resource and name
self.parent_field_name = self._meta.parent
self.parent_resource = None
for field, field_type in self.fields.iteritems():
if field == self.parent_field_name and isinstance(field_type, RelatedField):
self.parent_resource = field_type.to
if self.parent_resource is None:
raise ApiFieldError("Could not find parent resource: %s" % self._meta.parent)
self.parent_resource_instance = self.parent_resource() # cache an instance of the parent resource
# Add filtering for the parent
filters_for_parent = self._meta.filtering.setdefault(self.parent_field_name, [])
if filters_for_parent not in (ALL, ALL_WITH_RELATIONS) and "exact" not in filters_for_parent:
if isinstance(filters_for_parent, list):
filters_for_parent.append("exact")
elif isinstance(filters_for_parent, tuple):
filters_for_parent = list(filters_for_parent)
filters_for_parent.append("exact")
self._meta.filtering[self.parent_field_name] = tuple(filters_for_parent)
def base_urls(self):
# Overriding default base_urls() since we want this resource to only be accessed via the parent resource
return []
@property
def urls(self):
# need to override the urls() property b/c we need to include original urls under the parent's url
orig_urls = super(ChildResource, self).base_urls() + super(ChildResource, self).override_urls()
orig_urlpatterns = patterns("",
*orig_urls
)
# Get the parent's urlpatterns to build regex for this child's urlpatterns
# Need to munge regexes b/c using django's include with the same named parameter won't work
# Note: the type of each element in urlpatterns could be either a RegexURLPattern or RegexURLResolver
parent_urlpatterns = self.parent_resource_instance.urls
parent_regexes = [p for p in parent_urlpatterns if (isinstance(p, RegexURLPattern) and p.name == "api_dispatch_detail") or (isinstance(p, RegexURLResolver))]
if not parent_regexes:
raise TastypieError("No parent regexes found for resource: %s" % self.__class_.name)
if len(parent_regexes) > 1:
raise TastypieError("Too many parent regexes found for resource: %s" % self.__class__.name)
# Get the parent_regex based on the type
parent_regex = parent_regexes[0]
if isinstance(parent_regex, RegexURLPattern):
parent_regex = parent_regex.regex.pattern
elif isinstance(parent_regex, RegexURLResolver):
# See RegexURLResolver._populate()'s usage of reverse_dict
matches, pattern, defaults = parent_regex.reverse_dict["api_dispatch_detail"]
parent_regex = pattern
else:
raise TastypieError("Consistency error with hierarchical urls")
# Munge the parent regex so it correctly supports multiple levels of hierarchy
parent_regex = parent_regex.rstrip("$")
# Remove all of the parent resource names/pks since they could be at the beginning
parent_regex = parent_regex.replace("(?P<parent_resource_name>", "(?P<resource_name>").replace("(?P<parent_pk>", "(?P<pk>")
# Replace the resource name/pk with parent name/pk
parent_regex = rreplace(parent_regex, "(?P<resource_name>", "(?P<parent_resource_name>", 1)
parent_regex = rreplace(parent_regex, "(?P<pk>", "(?P<parent_pk>", 1)
# Remove the remaing resource name/pk regex groupings
# Excuse the running of regexes on regexes *sigh*
parent_regex = re.sub("\(\?\P\<resource_name\>(.*?)\)", "\g<1>", parent_regex)
parent_regex = re.sub("\(\?\P\<pk\>(.*?)\)", "\g<1>", parent_regex)
urlpatterns = patterns("",
(parent_regex, include(orig_urlpatterns))
)
return urlpatterns
def get_resource_uri(self, bundle_or_obj):
"""
Same as the default get_resource_uri() but sets the parent_resource_name and the parent_pk so reverse() can generate the url
"""
kwargs = {
'resource_name': self._meta.resource_name,
'parent_resource_name': self.parent_resource._meta.resource_name,
}
if isinstance(bundle_or_obj, Bundle):
kwargs['pk'] = bundle_or_obj.obj.pk
kwargs['parent_pk'] = getattr(bundle_or_obj.obj, self.parent_field_name).pk
else:
kwargs['pk'] = bundle_or_obj.id
kwargs['parent_pk'] = getattr(bundle_or_obj, self.parent_field_name).pk
if self._meta.api_name is not None:
kwargs['api_name'] = self._meta.api_name
return self._build_reverse_url("api_dispatch_detail", kwargs=kwargs)
def remove_api_resource_names(self, url_dict):
"""
Also need to remove parent_resource_name and parent_pk
"""
kwargs_subset = super(ChildResource, self).remove_api_resource_names(url_dict.copy())
# Added parent resource as a kwarg to filter and POST
if "parent_resource_name" in kwargs_subset and "parent_pk" in kwargs_subset:
kwargs_subset[self.parent_field_name] = self.parent_resource._meta.queryset.model.objects.get(pk=kwargs_subset["parent_pk"])
for key in ['parent_resource_name', 'parent_pk']:
try:
del(kwargs_subset[key])
except KeyError:
pass
return kwargs_subset
def alter_deserialized_list_data(self, request, data):
if self.parent_field_name in data:
raise BadRequest("Error: parent resource cannot be specified in the body")
return super(ChildResource, self).alter_deserialized_list_data(request, data)
def alter_deserialized_detail_data(self, request, data):
if self.parent_field_name in data:
raise BadRequest("Error: parent resource cannot be specified in the body")
return super(ChildResource, self).alter_deserialized_detail_data(request, data)
@dhui
Copy link
Author

dhui commented Sep 22, 2015

You can use CustomModelResource for your resource and not have any other resources nested under it.

Having the parent explicitly list it's children is a bad coding pattern. It makes code maintenance a nightmare. There's a good reason why object oriented programing languages don't do this for inheritance.

That being said, what's being done in this gist isn't great either. It relies too much on the internals of TastyPie. But that's not my fault... TastyPie wasn't designed with flexible customization in mind.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment