Permalink
Browse files

WIP: Making non-PK based URI possible again.

  • Loading branch information...
1 parent f078126 commit d867310f9c15a917f9709f5fc7ade82b731ec068 @toastdriven toastdriven committed Jun 3, 2012
Showing with 89 additions and 38 deletions.
  1. +0 −7 docs/resources.rst
  2. +41 −28 tastypie/resources.py
  3. +24 −0 tests/basic/api/resources.py
  4. +2 −1 tests/basic/api/urls.py
  5. +21 −1 tests/basic/tests/views.py
  6. +1 −1 tests/core/models.py
View
@@ -1015,13 +1015,6 @@ kwargs.
``ModelResource`` includes a full working version specific to Django's
``Models``.
-``get_resource_list_uri``
--------------------------
-
-.. method:: Resource.get_resource_list_uri(self)
-
-Returns a URL specific to this resource's list endpoint.
-
``get_via_uri``
---------------
View
@@ -602,32 +602,28 @@ def apply_sorting(self, obj_list, options=None):
# URL-related methods.
- def get_resource_uri(self, bundle_or_obj):
+ def detail_uri_kwargs(self, bundle_or_obj):
"""
This needs to be implemented at the user level.
- A call to ``reverse()`` should be all that would be needed::
-
- from django.core.urlresolvers import reverse
-
- def get_resource_uri(self, bundle):
- return reverse("api_dispatch_detail", kwargs={
- 'resource_name': self._meta.resource_name,
- 'pk': bundle.data['id'],
- })
-
- If you're using the :class:`~tastypie.api.Api` class to group your
- URLs, you also need to pass the ``api_name`` together with the other
- kwargs.
+ Given a ``Bundle`` or an object, it returns the extra kwargs needed to
+ generate a detail URI.
``ModelResource`` includes a full working version specific to Django's
``Models``.
"""
raise NotImplementedError()
- def get_resource_list_uri(self):
+ def resource_uri_kwargs(self, bundle_or_obj=None):
"""
- Returns a URL specific to this resource's list endpoint.
+ Builds a dictionary of kwargs to help generate URIs.
+
+ Automatically provides the ``Resource.Meta.resource_name`` (and
+ optionally the ``Resource.Meta.api_name`` if populated by an ``Api``
+ object).
+
+ If the ``bundle_or_obj`` argument is provided, it calls
+ ``Resource.detail_uri_kwargs`` for additional bits to create
"""
kwargs = {
'resource_name': self._meta.resource_name,
@@ -636,8 +632,29 @@ def get_resource_list_uri(self):
if self._meta.api_name is not None:
kwargs['api_name'] = self._meta.api_name
+ if bundle_or_obj is not None:
+ kwargs.update(self.detail_uri_kwargs(bundle_or_obj))
+
+ return kwargs
+
+ def get_resource_uri(self, bundle_or_obj=None, url_name='api_dispatch_list'):
+ """
+ Handles generating a resource URI.
+
+ If the ``bundle_or_obj`` argument is not provided, it builds the URI
+ for the list endpoint.
+
+ If the ``bundle_or_obj`` argument is provided, it builds the URI for
+ the detail endpoint.
+
+ Return the generated URI. If that URI can not be reversed (not found
+ in the URLconf), it will return ``None``.
+ """
+ if bundle_or_obj is not None:
+ url_name = 'api_dispatch_detail'
+
try:
- return self._build_reverse_url("api_dispatch_list", kwargs=kwargs)
+ return self._build_reverse_url(url_name, kwargs=self.resource_uri_kwargs(bundle_or_obj))
except NoReverseMatch:
return None
@@ -1069,7 +1086,7 @@ def get_list(self, request, **kwargs):
objects = self.obj_get_list(request=request, **self.remove_api_resource_names(kwargs))
sorted_objects = self.apply_sorting(objects, options=request.GET)
- paginator = self._meta.paginator_class(request.GET, sorted_objects, resource_uri=self.get_resource_list_uri(), limit=self._meta.limit, max_limit=self._meta.max_limit, collection_name=self._meta.collection_name)
+ paginator = self._meta.paginator_class(request.GET, sorted_objects, resource_uri=self.get_resource_uri(), limit=self._meta.limit, max_limit=self._meta.max_limit, collection_name=self._meta.collection_name)
to_be_serialized = paginator.page()
# Dehydrate the bundles in preparation for serialization.
@@ -2037,25 +2054,21 @@ def save_m2m(self, bundle):
related_mngr.add(*related_objs)
- def get_resource_uri(self, bundle_or_obj):
+ def detail_uri_kwargs(self, bundle_or_obj):
"""
- Handles generating a resource URI for a single resource.
+ Given a ``Bundle`` or an object (typically a ``Model`` instance),
+ it returns the extra kwargs needed to generate a detail URI.
- Uses the model's ``pk`` in order to create the URI.
+ By default, it uses the model's ``pk`` in order to create the URI.
"""
- kwargs = {
- 'resource_name': self._meta.resource_name,
- }
+ kwargs = {}
if isinstance(bundle_or_obj, Bundle):
kwargs['pk'] = bundle_or_obj.obj.pk
else:
kwargs['pk'] = bundle_or_obj.id
- 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)
+ return kwargs
class NamespacedModelResource(ModelResource):
@@ -1,4 +1,6 @@
+from django.conf.urls.defaults import url
from django.contrib.auth.models import User
+from tastypie.bundle import Bundle
from tastypie import fields
from tastypie.resources import ModelResource
from tastypie.authorization import Authorization
@@ -40,3 +42,25 @@ class Meta:
def get_list(self, *args, **kwargs):
raise Exception("It's broke.")
+
+
+class SlugBasedNoteResource(ModelResource):
+ class Meta:
+ queryset = Note.objects.all()
+ resource_name = 'slugbased'
+
+ def prepend_urls(self):
+ return [
+ url(r"^(?P<resource_name>%s)/set/(?P<slug_list>\w[\w/;-]*)/$" % self._meta.resource_name, self.wrap_view('get_multiple'), name="api_get_multiple"),
+ url(r"^(?P<resource_name>%s)/(?P<slug>\w[\w/-]*)/$" % self._meta.resource_name, self.wrap_view('dispatch_detail'), name="api_dispatch_detail"),
+ ]
+
+ def detail_uri_kwargs(self, bundle_or_obj):
+ kwargs = {}
+
+ if isinstance(bundle_or_obj, Bundle):
+ kwargs['slug'] = bundle_or_obj.obj.slug
+ else:
+ kwargs['slug'] = bundle_or_obj.slug
+
+ return kwargs
View
@@ -1,6 +1,6 @@
from django.conf.urls.defaults import *
from tastypie.api import Api
-from basic.api.resources import NoteResource, UserResource, BustedResource, CachedUserResource
+from basic.api.resources import NoteResource, UserResource, BustedResource, CachedUserResource, SlugBasedNoteResource
api = Api(api_name='v1')
api.register(NoteResource(), canonical=True)
@@ -9,5 +9,6 @@
v2_api = Api(api_name='v2')
v2_api.register(BustedResource(), canonical=True)
+v2_api.register(SlugBasedNoteResource())
urlpatterns = v2_api.urls + api.urls
View
@@ -79,7 +79,6 @@ def test_api_field_error(self):
self.assertEqual(resp.status_code, 400)
self.assertEqual(resp.content, "Could not find the provided object via resource URI '/api/v1/users/9001/'.")
-
def test_options(self):
resp = self.client.options('/api/v1/notes/')
self.assertEqual(resp.status_code, 200)
@@ -105,3 +104,24 @@ def test_options(self):
self.assertEqual(resp['Allow'], allows)
self.assertEqual(resp.content, allows)
+ def test_slugbased(self):
+ resp = self.client.get('/api/v2/slugbased/', data={'format': 'json'})
+ self.assertEqual(resp.status_code, 200)
+ deserialized = json.loads(resp.content)
+ self.assertEqual(len(deserialized), 2)
+ self.assertEqual(deserialized['meta']['limit'], 20)
+ self.assertEqual(len(deserialized['objects']), 2)
+ self.assertEqual([obj['title'] for obj in deserialized['objects']], [u'First Post!', u'Another Post'])
+
+ resp = self.client.get('/api/v2/slugbased/first-post/', data={'format': 'json'})
+ self.assertEqual(resp.status_code, 200)
+ deserialized = json.loads(resp.content)
+ self.assertEqual(len(deserialized), 8)
+ self.assertEqual(deserialized['title'], u'First Post!')
+
+ resp = self.client.get('/api/v2/slugbased/set/another-post;first-post/', data={'format': 'json'})
+ self.assertEqual(resp.status_code, 200)
+ deserialized = json.loads(resp.content)
+ self.assertEqual(len(deserialized), 1)
+ self.assertEqual(len(deserialized['objects']), 2)
+ self.assertEqual([obj['title'] for obj in deserialized['objects']], [u'Another Post', u'First Post!'])
View
@@ -7,7 +7,7 @@
class Note(models.Model):
author = models.ForeignKey(User, related_name='notes', blank=True, null=True)
title = models.CharField(max_length=100)
- slug = models.SlugField()
+ slug = models.SlugField(unique=True)
content = models.TextField(blank=True)
is_active = models.BooleanField(default=True)
created = models.DateTimeField(default=now)

0 comments on commit d867310

Please sign in to comment.