Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Compatible with TastyPie 0.9.12-alpha (dropped support for earlier

versions!)
  • Loading branch information...
commit 9b1ba600d560dc05f8c0ea34c5bafaa3f61655b9 1 parent 66136a7
Alan Descoins dekked authored
47 README.rst
Source Rendered
@@ -5,7 +5,7 @@
5 5 The ``ExtendedModelResource`` is an extension for TastyPie's ``ModelResource`` that adds some interesting features:
6 6
7 7 * Supports easily using resources as *nested* of another resource, with proper authorization checks for each case.
8   -* Supports using a custom identifier attribute for resources in uris (not only the object's pk!)
  8 +* [This feature has already been included in the official TastyPie] Supports using a custom identifier attribute for resources in uris (not only the object's pk!)
9 9
10 10
11 11 Requirements
@@ -13,11 +13,11 @@ Requirements
13 13
14 14 Required
15 15 --------
16   -* django-tastypie 0.9.11 and its requirements.
  16 +* Latest django-tastypie from repository (0.9.12-alpha or hopefully greater) and its requirements.
17 17
18 18 Optional
19 19 --------
20   -* Django 1.4 for the sample project.
  20 +* Django 1.4.1 for the sample project.
21 21
22 22
23 23 Installation
@@ -154,51 +154,24 @@ Caveats
154 154 Changing object's identifier attribute in urls
155 155 ==============================================
156 156
157   -With TastyPie's ``ModelResource`` you can override a method to change the identifier attribute used for objects in the URLs (see `Using Non-PK Data For Your URLs <http://django-tastypie.readthedocs.org/en/latest/cookbook.html#using-non-pk-data-for-your-urls>`_) ::
158   -
159   - class UserResource(ModelResource):
160   - class Meta:
161   - queryset = User.objects.all()
162   -
163   - def override_urls(self):
164   - return [
165   - url(r"^(?P<resource_name>%s)/(?P<username>[\w\d_.-]+)/$" % self._meta.resource_name, self.wrap_view('dispatch_detail'), name="api_dispatch_detail"),
166   - ]
167   -
168   -This adds a new URL using ``username`` and ignores the old URL using ``pk`` ::
169   -
170   - ^api/ ^(?P<resource_name>user)/(?P<username>[\w\d_.-]+)/$ [name='api_dispatch_detail']
171   - ^api/ ^(?P<resource_name>user)/$ [name='api_dispatch_list']
172   - ^api/ ^(?P<resource_name>user)/schema/$ [name='api_get_schema']
173   - ^api/ ^(?P<resource_name>user)/set/(?P<pk_list>\w[\w/;-]*)/$ [name='api_get_multiple']
174   - ^api/ ^(?P<resource_name>user)/(?P<pk>\w[\w/-]*)/$ [name='api_dispatch_detail']
175   -
176   -But the old URL is still there, and this can be a bit confusing when you have an error with the URLs.
177   -
178   -Using ``ExtendedModelResource`` it is as easy as adding a new entry in the ``Meta`` class ::
  157 +Using the latest TastyPie you can define a ``detail_uri_name`` attribute
  158 +in the ``Meta`` class, to use a different attribute than the object's ``pk`` ::
179 159
180 160 class UserResource(ExtendedModelResource):
181 161 class Meta:
182 162 queryset = User.objects.all()
183   - url_id_attribute = 'username'
184   -
185   -And you will get this list of urls ::
186   -
187   - ^api/ ^(?P<resource_name>user)/$ [name='api_dispatch_list']
188   - ^api/ ^(?P<resource_name>user)/schema/$ [name='api_get_schema']
189   - ^api/ ^(?P<resource_name>user)/set/(?P<username_list>(\w[\w-]*;?)*)/$ [name='api_get_multiple']
190   - ^api/ ^(?P<resource_name>user)/(?P<username>\w[\w-]*)/$ [name='api_dispatch_detail']
  163 + detail_uri_name = 'username'
191 164
192   -If you need to change the regular expression used for your identifier attribute in the urls, you can override the method ``get_url_id_attribute_regex`` and return it, like the following example ::
  165 +With ``ExtendedModelResource`` you can change the regular expression used for your identifier attribute in the urls, you can override the method ``get_url_id_attribute_regex`` and return it, like the following example ::
193 166
194   - def get_url_id_attribute_regex(self):
  167 + def get_detail_uri_name_regex(self):
195 168 return r'[aA-zZ][\w-]*'
196 169
197 170 More information
198 171 ================
199 172
200   -:Date: 04-19-2012
201   -:Version: 0.1
  173 +:Date: 08-20-2012
  174 +:Version: 0.2
202 175 :Authors:
203 176 - Alan Descoins - Tryolabs <alan@tryolabs.com>
204 177 - Martín Santos - Tryolabs <santos@tryolabs.com>
2  example/api/resources.py
@@ -38,7 +38,7 @@ class UserByNameResource(ExtendedModelResource):
38 38 class Meta:
39 39 queryset = User.objects.all()
40 40 resource_name = 'userbyname'
41   - url_id_attribute = 'username'
  41 + detail_uri_name = 'username'
42 42
43 43 def get_url_id_attribute_regex(self):
44 44 # The id attribute respects this regex.
203 extendedmodelresource/extendedmodelresource.py
@@ -4,48 +4,12 @@
4 4 from django.conf.urls.defaults import patterns, url, include
5 5
6 6 from tastypie import fields, http
7   -from tastypie.bundle import Bundle
8 7 from tastypie.exceptions import NotFound, ImmediateHttpResponse
9   -from tastypie.resources import ModelResource, ModelDeclarativeMetaclass, \
10   - ResourceOptions, convert_post_to_put
  8 +from tastypie.resources import ResourceOptions, ModelDeclarativeMetaclass, \
  9 + ModelResource, convert_post_to_put
11 10 from tastypie.utils import trailing_slash
12 11
13 12
14   -class ExtendedResourceOptions(ResourceOptions):
15   - """
16   - A configuration class for ``ExtendedModelResource``.
17   -
18   - Adds the ability to use an attribute in the URLs of the resources different
19   - than the primary key of the objects.
20   -
21   - Useful for the case in which you want to hide the primary key of the
22   - objects in the database. For example, you may want to use an UUID to
23   - identify resources in a URL so the user cannot simply increment an integer
24   - key and attempt to gain access to another object.
25   -
26   - Any field you with to use as identifier for the objects in the URIs must
27   - have unique=True constraint.
28   -
29   - To use this you must declare an ``url_id_attribute`` in the resource with
30   - the name of the attribute that will identify the objects in the URI.
31   - If you have a 'uuid' attribute in the model, you then should declare
32   -
33   - url_id_attribute = 'uuid'
34   -
35   - in the corresponding resource.
36   -
37   - If the ``url_id_attribute`` field is not found, the object's pkey will be
38   - used as default.
39   - """
40   - def __new__(cls, meta=None):
41   - new_class = super(ExtendedResourceOptions, cls).__new__(cls, meta)
42   -
43   - new_class.url_id_attribute = getattr(new_class,
44   - 'url_id_attribute',
45   - 'pk') # Defaults to pkey
46   - return new_class
47   -
48   -
49 13 class ExtendedDeclarativeMetaclass(ModelDeclarativeMetaclass):
50 14 """
51 15 Same as ``DeclarativeMetaclass`` but uses ``AnyIdAttributeResourceOptions``
@@ -58,7 +22,7 @@ def __new__(cls, name, bases, attrs):
58 22 name, bases, attrs)
59 23
60 24 opts = getattr(new_class, 'Meta', None)
61   - new_class._meta = ExtendedResourceOptions(opts)
  25 + new_class._meta = ResourceOptions(opts)
62 26
63 27 # Will map nested fields names to the actual fields
64 28 nested_fields = {}
@@ -83,7 +47,29 @@ class ExtendedModelResource(ModelResource):
83 47
84 48 __metaclass__ = ExtendedDeclarativeMetaclass
85 49
86   - def get_url_id_attribute_regex(self):
  50 + def remove_api_resource_names(self, url_dict):
  51 + """
  52 + Given a dictionary of regex matches from a URLconf, removes
  53 + ``api_name`` and/or ``resource_name`` if found.
  54 +
  55 + This is useful for converting URLconf matches into something suitable
  56 + for data lookup. For example::
  57 +
  58 + Model.objects.filter(**self.remove_api_resource_names(matches))
  59 + """
  60 + kwargs_subset = url_dict.copy()
  61 +
  62 + for key in ['api_name', 'resource_name', 'related_manager',
  63 + 'child_object', 'parent_resource', 'nested_name',
  64 + 'parent_object']:
  65 + try:
  66 + del(kwargs_subset[key])
  67 + except KeyError:
  68 + pass
  69 +
  70 + return kwargs_subset
  71 +
  72 + def get_detail_uri_name_regex(self):
87 73 """
88 74 Return the regular expression to which the id attribute used in
89 75 resource URLs should match.
@@ -96,7 +82,7 @@ def get_url_id_attribute_regex(self):
96 82 def base_urls(self):
97 83 """
98 84 Same as the original ``base_urls`` but supports using the custom
99   - url_id_attribute instead of the pk of the objects.
  85 + regex for the ``detail_uri_name`` attribute of the objects.
100 86 """
101 87 # Due to the way Django parses URLs, ``get_multiple``
102 88 # won't work without a trailing slash.
@@ -111,14 +97,14 @@ def base_urls(self):
111 97 name="api_get_schema"),
112 98 url(r"^(?P<resource_name>%s)/set/(?P<%s_list>(%s;?)*)/$" %
113 99 (self._meta.resource_name,
114   - self._meta.url_id_attribute,
115   - self.get_url_id_attribute_regex()),
  100 + self._meta.detail_uri_name,
  101 + self.get_detail_uri_name_regex()),
116 102 self.wrap_view('get_multiple'),
117 103 name="api_get_multiple"),
118 104 url(r"^(?P<resource_name>%s)/(?P<%s>%s)%s$" %
119 105 (self._meta.resource_name,
120   - self._meta.url_id_attribute,
121   - self.get_url_id_attribute_regex(),
  106 + self._meta.detail_uri_name,
  107 + self.get_detail_uri_name_regex(),
122 108 trailing_slash()),
123 109 self.wrap_view('dispatch_detail'),
124 110 name="api_dispatch_detail"),
@@ -134,8 +120,8 @@ def get_nested_url(nested_name):
134 120 return url(r"^(?P<resource_name>%s)/(?P<%s>%s)/"
135 121 r"(?P<nested_name>%s)%s$" %
136 122 (self._meta.resource_name,
137   - self._meta.url_id_attribute,
138   - self.get_url_id_attribute_regex(),
  123 + self._meta.detail_uri_name,
  124 + self.get_detail_uri_name_regex(),
139 125 nested_name,
140 126 trailing_slash()),
141 127 self.wrap_view('dispatch_nested'),
@@ -174,8 +160,8 @@ def detail_actions_urlpatterns(self):
174 160 if self.detail_actions():
175 161 detail_url = "^(?P<resource_name>%s)/(?P<%s>%s)/" % (
176 162 self._meta.resource_name,
177   - self._meta.url_id_attribute,
178   - self.get_url_id_attribute_regex()
  163 + self._meta.detail_uri_name,
  164 + self.get_detail_uri_name_regex()
179 165 )
180 166 return patterns('', (detail_url, include(self.detail_actions())))
181 167
@@ -192,61 +178,6 @@ def urls(self):
192 178 urls = self.override_urls() + self.base_urls() + self.nested_urls()
193 179 return patterns('', *urls) + self.detail_actions_urlpatterns()
194 180
195   - def get_multiple(self, request, **kwargs):
196   - """
197   - Same as the original ``get_multiple`` but supports using the custom
198   - ``url_id_attribute`` instead of the pk of the objects.
199   - """
200   - self.method_check(request, allowed=['get'])
201   - self.is_authenticated(request)
202   - self.throttle_check(request)
203   -
204   - # Rip apart the list then iterate.
205   - list_name = '%s_list' % self._meta.url_id_attribute
206   - obj_attributes = kwargs.get(list_name, '').split(';')
207   - objects = []
208   - not_found = []
209   -
210   - for att_value in obj_attributes:
211   - try:
212   - # Get the object by our attribute
213   - obj = self.obj_get(request,
214   - **{self._meta.url_id_attribute: att_value})
215   - bundle = self.build_bundle(obj=obj, request=request)
216   - bundle = self.full_dehydrate(bundle)
217   - objects.append(bundle)
218   - except ObjectDoesNotExist:
219   - not_found.append(att_value)
220   -
221   - object_list = {'objects': objects}
222   -
223   - if len(not_found):
224   - object_list['not_found'] = not_found
225   -
226   - self.log_throttled_access(request)
227   - return self.create_response(request, object_list)
228   -
229   - def get_resource_uri(self, bundle_or_obj):
230   - """
231   - Same as the original ``get_resource_uri`` but supports using the custom
232   - ``url_id_attribute`` instead of the pk of the objects.
233   - """
234   - kwargs = {'resource_name': self._meta.resource_name}
235   -
236   - # If url_id_attribute was not declared it has already been set to 'pk'
237   - # by the metaclass.
238   - id_attr_name = self._meta.url_id_attribute
239   -
240   - if isinstance(bundle_or_obj, Bundle):
241   - kwargs[id_attr_name] = getattr(bundle_or_obj.obj, id_attr_name)
242   - else:
243   - kwargs[id_attr_name] = getattr(bundle_or_obj, id_attr_name)
244   -
245   - if self._meta.api_name is not None:
246   - kwargs['api_name'] = self._meta.api_name
247   -
248   - return self._build_reverse_url("api_dispatch_detail", kwargs=kwargs)
249   -
250 181 def is_authorized_over_parent(self, request, parent_object):
251 182 """
252 183 Allows the ``Authorization`` class to check if a request to a nested
@@ -269,23 +200,17 @@ def parent_obj_get(self, request=None, **kwargs):
269 200 Will check authorization to see if the request is allowed to act on
270 201 the parent resource.
271 202 """
272   - try:
273   - parent_object = self.get_object_list(request).get(**kwargs)
  203 + parent_object = self.get_object_list(request).get(**kwargs)
274 204
275   - # If I am not authorized for the parent
276   - if not self.is_authorized_over_parent(request, parent_object):
277   - stringified_kwargs = ', '.join(["%s=%s" % (k, v)
278   - for k, v in kwargs.items()])
279   - raise self._meta.object_class.DoesNotExist("Couldn't find an "
280   - "instance of '%s' which matched '%s'." %
281   - (self._meta.object_class.__name__, stringified_kwargs))
  205 + # If I am not authorized for the parent
  206 + if not self.is_authorized_over_parent(request, parent_object):
  207 + stringified_kwargs = ', '.join(["%s=%s" % (k, v)
  208 + for k, v in kwargs.items()])
  209 + raise self._meta.object_class.DoesNotExist("Couldn't find an "
  210 + "instance of '%s' which matched '%s'." %
  211 + (self._meta.object_class.__name__, stringified_kwargs))
282 212
283   - return parent_object
284   - except ObjectDoesNotExist:
285   - return http.HttpNotFound()
286   - except MultipleObjectsReturned:
287   - return http.HttpMultipleChoices("More than one parent resource is "
288   - "found at this URI.")
  213 + return parent_object
289 214
290 215 def parent_cached_obj_get(self, request=None, **kwargs):
291 216 """
@@ -456,11 +381,8 @@ def dispatch_nested(self, request, **kwargs):
456 381 return http.HttpMultipleChoices("More than one parent resource is "
457 382 "found at this URI.")
458 383
459   - kwargs.pop(self._meta.url_id_attribute)
460   -
461 384 # TODO: comment further to make sense of this block
462 385 manager = None
463   -
464 386 if isinstance(nested_field.attribute, basestring):
465 387 name = nested_field.attribute
466 388 manager = getattr(obj, name, None)
@@ -481,17 +403,20 @@ def dispatch_nested(self, request, **kwargs):
481 403 nested_resource = nested_field.to_class()
482 404 nested_resource._meta.api_name = self._meta.api_name
483 405
484   - dispatch_type = 'list'
  406 + kwargs['nested_name'] = nested_name
  407 + kwargs['parent_resource'] = self
  408 + kwargs['parent_object'] = obj
  409 +
485 410 if manager is None or not hasattr(manager, 'all'):
486 411 dispatch_type = 'detail'
  412 + kwargs['child_object'] = manager
  413 + else:
  414 + dispatch_type = 'list'
  415 + kwargs['related_manager'] = manager
487 416
488 417 return nested_resource.dispatch(
489 418 dispatch_type,
490 419 request,
491   - nested_name=nested_name,
492   - parent_resource=self,
493   - parent_object=obj,
494   - related_manager=manager,
495 420 **kwargs
496 421 )
497 422
@@ -615,11 +540,13 @@ def get_list(self, request, **kwargs):
615 540 sorted_objects = self.apply_sorting(objects, options=request.GET)
616 541
617 542 paginator = self._meta.paginator_class(
618   - request.GET,
619   - sorted_objects,
620   - resource_uri=self.get_resource_list_uri(),
621   - limit=self._meta.limit
622   - )
  543 + request.GET, sorted_objects,
  544 + resource_uri=self.get_resource_uri(),
  545 + limit=self._meta.limit,
  546 + max_limit=self._meta.max_limit,
  547 + collection_name=self._meta.collection_name
  548 + )
  549 +
623 550 to_be_serialized = paginator.page()
624 551
625 552 # Dehydrate the bundles in preparation for serialization.
@@ -646,13 +573,10 @@ def get_detail(self, request, **kwargs):
646 573 Should return a HttpResponse (200 OK).
647 574 """
648 575 try:
649   - # If call was made through Nested, remove parameters
650   - if 'related_manager' in kwargs:
651   - kwargs.pop('related_manager', None)
652   - kwargs.pop('parent_resource', None)
653   - nested_name = kwargs.pop('nested_name', None)
654   - parent_object = kwargs.pop('parent_object', None)
655   - obj = getattr(parent_object, nested_name)
  576 + # If call was made through Nested we should already have the
  577 + # child object.
  578 + if 'child_object' in kwargs:
  579 + obj = kwargs.pop('child_object', None)
656 580 if obj is None:
657 581 return http.HttpNotFound()
658 582 else:
@@ -670,3 +594,4 @@ def get_detail(self, request, **kwargs):
670 594 bundle = self.full_dehydrate(bundle)
671 595 bundle = self.alter_detail_data_to_serialize(request, bundle)
672 596 return self.create_response(request, bundle)
  597 +
4 requirements.txt
... ... @@ -1,2 +1,2 @@
1   -Django==1.4
2   -django-tastypie==0.9.11
  1 +Django==1.4.1
  2 +-e git+https://github.com/toastdriven/django-tastypie@c38a667d335d0fa75a2befb1818bb643f911d111#egg=django_tastypie-dev
2  setup.py
@@ -7,7 +7,7 @@
7 7 use_setuptools()
8 8 from setuptools import setup
9 9
10   -VERSION = '0.1'
  10 +VERSION = '0.2'
11 11
12 12 if __name__ == '__main__':
13 13 setup(

0 comments on commit 9b1ba60

Please sign in to comment.
Something went wrong with that request. Please try again.