Skip to content

Commit

Permalink
More work on related fields. URL reversal now works correctly for rel…
Browse files Browse the repository at this point in the history
…ated representations.
  • Loading branch information
toastdriven committed Apr 13, 2010
1 parent dc43b6c commit 72fcf11
Show file tree
Hide file tree
Showing 9 changed files with 280 additions and 28 deletions.
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

setup(
name='django-tastypie',
version='0.3.0',
version='0.4.0',
description='A flexible & capable API layer for Django.',
author='Daniel Lindsley',
author_email='daniel@toastdriven.com',
Expand Down
105 changes: 104 additions & 1 deletion tastypie/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,105 @@
import inspect
from tastypie.exceptions import URLReverseError


__author__ = 'Daniel Lindsley, Cody Soyland, Matt Croydon'
__version__ = (0, 3, 0)
__version__ = (0, 4, 0)


# This is a global place where ``Api`` instances register themselves.
# Kinda sucks, but necessary with the current architecture to do url
# resolution at the ``Representation`` level.
# I don't feel totally bad about this, because the admin does similar things
# for ``formfield_overrides``, but would welcome a better idea.
#
# Structure (when built) should look like::
# available_apis = {
# 'v1': {
# 'class': <Api object>,
# 'resources': [
# 'notes',
# ],
# 'representations': {
# # Note - ``NoteRepresentation.__name__``, NOT ``NoteRepresentation`` the class.
# 'NoteRepresentation': 'notes',
# }
# },
# 'v2': {
# 'class': <Api object>,
# 'resources': [
# 'notes',
# 'users',
# ],
# 'representations': {
# # Note - ``CustomNoteRepresentation.__name__``, NOT ``CustomNoteRepresentation`` the class.
# 'CustomNoteRepresentation': 'notes',
# 'UserRepresentation': 'users',
# }
# },
# }
available_apis = {}


def _add_resource(api, resource, canonical=True):
if not api.api_name in available_apis:
available_apis[api.api_name] = {
'class': api,
'resources': [],
'representations': {},
}

if not resource.resource_name in available_apis[api.api_name]['resources']:
available_apis[api.api_name]['resources'].append(resource.resource_name)

if canonical is True:
repr_name = resource.detail_representation.__name__
available_apis[api.api_name]['representations'][repr_name] = resource.resource_name


def _remove_resource(api, resource):
if not api.api_name in available_apis:
return False

try:
resource_offset = available_apis[api.api_name]['resources'].index(resource.resource_name)
del(available_apis[api.api_name]['resources'][resource_offset])
except (ValueError, IndexError):
return False

if inspect.isclass(resource.detail_representation):
representation_name = resource.detail_representation.__name__
else:
representation_name = resource.detail_representation.__class__.__name__

if representation_name in available_apis[api.api_name]['representations']:
if available_apis[api.api_name]['representations'][representation_name] == resource.resource_name:
del(available_apis[api.api_name]['representations'][representation_name])

return True


def _get_canonical_resource_name(api_name, representation):
if inspect.isclass(representation) and getattr(representation, '__name__', None):
representation_name = representation.__name__
else:
representation_name = representation.__class__.__name__

if not api_name in available_apis:
raise URLReverseError("The api_name '%s' does not appear to have been instantiated." % api_name)

if not 'representations' in available_apis[api_name]:
raise URLReverseError("The api_name '%s' does not appear to have any representations registered." % api_name)

if not 'resources' in available_apis[api_name]:
raise URLReverseError("The api_name '%s' does not appear to have any resources registered." % api_name)

if not representation_name in available_apis[api_name]['representations']:
raise URLReverseError("The api '%s' does not have a '%s' representation registered." % (api_name, representation_name))

desired_resource_name = available_apis[api_name]['representations'][representation_name]

# Now verify the resource is in the list.
if not desired_resource_name in available_apis[api_name]['resources']:
raise URLReverseError("The api '%s' does not have a canonical resource named '%s' registered." % (api_name, desired_resource_name))

return desired_resource_name
11 changes: 9 additions & 2 deletions tastypie/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse
from django.http import HttpResponse
from tastypie.exceptions import TastyPieError, NotRegistered
from tastypie import _add_resource, _remove_resource
from tastypie.exceptions import NotRegistered
from tastypie.serializers import Serializer


Expand All @@ -19,19 +20,25 @@ def __init__(self, api_name="v1"):
self._registry = {}
self._canonicals = {}

def register(self, resource, canonical=False):
def register(self, resource, canonical=True):
resource_name = getattr(resource, 'resource_name', None)

if resource_name is None:
raise ImproperlyConfigured("Resource %r must define a 'resource_name'." % resource)

self._registry[resource_name] = resource

# TODO: Silent replacement. Not sure if that's good or bad. Seems
# like it should at least emit a warning...
if canonical is True:
self._canonicals[resource_name] = resource

# Register it globally so we can build URIs.
_add_resource(self, resource, canonical)

def unregister(self, resource_name):
if resource_name in self._registry:
_remove_resource(self, self._registry[resource_name])
del(self._registry[resource_name])

if resource_name in self._canonicals:
Expand Down
4 changes: 4 additions & 0 deletions tastypie/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ class NotRegistered(TastyPieError):
pass


class URLReverseError(TastyPieError):
pass


class NotFound(TastyPieError):
pass

Expand Down
5 changes: 4 additions & 1 deletion tastypie/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ def __init__(self, to, attribute, related_name=None, null=False, full_repr=False
self.null = null
self.full_repr = full_repr
self.value = None
self.api_name = None
self.resource_name = None

def has_default(self):
return False
Expand All @@ -204,7 +206,8 @@ def default(self):

def get_related_representation(self, related_instance):
# TODO: More leakage.
related_repr = self.to()
# FIXME: Wrong ``resource_name``. Need to lookup in the ``Api``?
related_repr = self.to(api_name=self.api_name, resource_name=self.resource_name)
# Try to be efficient about DB queries.
related_repr.instance = related_instance
return related_repr
Expand Down
8 changes: 7 additions & 1 deletion tastypie/representations/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
from django.core.urlresolvers import reverse, resolve, NoReverseMatch, Resolver404
from django.utils.copycompat import deepcopy
from tastypie.exceptions import NotFound
from tastypie import _get_canonical_resource_name
from tastypie.exceptions import NotFound, URLReverseError
from tastypie.fields import *
from tastypie.representations.simple import Representation

Expand Down Expand Up @@ -188,6 +189,11 @@ def get_resource_uri(self):
}

if self.api_name is not None:
try:
kwargs['resource_name'] = _get_canonical_resource_name(self.api_name, self)
except URLReverseError:
pass

kwargs['api_name'] = self.api_name

return reverse("api_dispatch_detail", kwargs=kwargs)
Expand Down
7 changes: 6 additions & 1 deletion tastypie/representations/simple.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from django.core.exceptions import ImproperlyConfigured
from django.utils.copycompat import deepcopy
from tastypie.fields import ApiField
from tastypie.fields import ApiField, RelatedField


class DeclarativeMetaclass(type):
Expand Down Expand Up @@ -115,6 +115,11 @@ def full_dehydrate(self, obj):
"""
# Dehydrate each field.
for field_name, field_object in self.fields.items():
# A touch leaky but it makes URI resolution work.
if isinstance(field_object, RelatedField):
field_object.api_name = self.api_name
field_object.resource_name = self.resource_name

field_object.value = field_object.dehydrate(obj)

# Run through optional overrides.
Expand Down
91 changes: 80 additions & 11 deletions tests/core/tests/api.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from django.contrib.auth.models import User
from django.http import HttpRequest
from django.test import TestCase
import tastypie
from tastypie.api import Api
from tastypie.exceptions import NotRegistered
from tastypie.exceptions import NotRegistered, URLReverseError
from tastypie.resources import Resource
from tastypie.representations.models import ModelRepresentation
from core.models import Note
Expand Down Expand Up @@ -47,40 +48,108 @@ def test_register(self):
self.assertEqual(len(api._registry), 2)
self.assertEqual(sorted(api._registry.keys()), ['notes', 'users'])

self.assertEqual(len(api._canonicals), 0)
api.register(UserResource(), canonical=True)
self.assertEqual(len(api._canonicals), 2)
api.register(UserResource(), canonical=False)
self.assertEqual(len(api._registry), 2)
self.assertEqual(sorted(api._registry.keys()), ['notes', 'users'])
self.assertEqual(len(api._canonicals), 1)
self.assertEqual(len(api._canonicals), 2)

def test_global_registry(self):
tastypie.available_apis = {}
api = Api()
self.assertEqual(len(api._registry), 0)
self.assertEqual(len(tastypie.available_apis), 0)

api.register(NoteResource())
self.assertEqual(len(api._registry), 1)
self.assertEqual(sorted(api._registry.keys()), ['notes'])
self.assertEqual(len(tastypie.available_apis), 1)
self.assertEqual(tastypie.available_apis['v1']['class'], api)
self.assertEqual(tastypie.available_apis['v1']['resources'], ['notes'])
self.assertEqual(tastypie.available_apis['v1']['representations'], {'NoteRepresentation': 'notes'})

api.register(UserResource())
self.assertEqual(len(api._registry), 2)
self.assertEqual(sorted(api._registry.keys()), ['notes', 'users'])
self.assertEqual(len(tastypie.available_apis), 1)
self.assertEqual(tastypie.available_apis['v1']['class'], api)
self.assertEqual(tastypie.available_apis['v1']['resources'], ['notes', 'users'])
self.assertEqual(tastypie.available_apis['v1']['representations'], {'UserRepresentation': 'users', 'NoteRepresentation': 'notes'})

api.register(UserResource())
self.assertEqual(len(api._registry), 2)
self.assertEqual(sorted(api._registry.keys()), ['notes', 'users'])
self.assertEqual(len(tastypie.available_apis), 1)
self.assertEqual(tastypie.available_apis['v1']['class'], api)
self.assertEqual(tastypie.available_apis['v1']['resources'], ['notes', 'users'])
self.assertEqual(tastypie.available_apis['v1']['representations'], {'UserRepresentation': 'users', 'NoteRepresentation': 'notes'})

self.assertEqual(len(api._canonicals), 2)
api.register(UserResource(), canonical=False)
self.assertEqual(len(api._registry), 2)
self.assertEqual(sorted(api._registry.keys()), ['notes', 'users'])
self.assertEqual(len(api._canonicals), 2)
self.assertEqual(len(tastypie.available_apis), 1)
self.assertEqual(tastypie.available_apis['v1']['class'], api)
self.assertEqual(tastypie.available_apis['v1']['resources'], ['notes', 'users'])
self.assertEqual(tastypie.available_apis['v1']['representations'], {'UserRepresentation': 'users', 'NoteRepresentation': 'notes'})

def test_unregister(self):
tastypie.available_apis = {}
api = Api()
api.register(NoteResource())
api.register(UserResource(), canonical=True)
api.register(UserResource(), canonical=False)
self.assertEqual(sorted(api._registry.keys()), ['notes', 'users'])
self.assertEqual(len(tastypie.available_apis), 1)
self.assertEqual(tastypie.available_apis['v1']['class'], api)
self.assertEqual(tastypie.available_apis['v1']['resources'], ['notes', 'users'])
self.assertEqual(tastypie.available_apis['v1']['representations'], {'NoteRepresentation': 'notes'})

self.assertEqual(len(api._canonicals), 1)
api.unregister('users')
self.assertEqual(len(api._registry), 1)
self.assertEqual(sorted(api._registry.keys()), ['notes'])
self.assertEqual(len(api._canonicals), 0)
self.assertEqual(len(api._canonicals), 1)
self.assertEqual(tastypie.available_apis['v1']['class'], api)
self.assertEqual(tastypie.available_apis['v1']['resources'], ['notes'])
self.assertEqual(tastypie.available_apis['v1']['representations'], {'NoteRepresentation': 'notes'})

api.unregister('notes')
self.assertEqual(len(api._registry), 0)
self.assertEqual(sorted(api._registry.keys()), [])
self.assertEqual(tastypie.available_apis['v1']['class'], api)
self.assertEqual(tastypie.available_apis['v1']['resources'], [])
self.assertEqual(tastypie.available_apis['v1']['representations'], {})

api.unregister('users')
self.assertEqual(len(api._registry), 0)
self.assertEqual(sorted(api._registry.keys()), [])
self.assertEqual(tastypie.available_apis['v1']['class'], api)
self.assertEqual(tastypie.available_apis['v1']['resources'], [])
self.assertEqual(tastypie.available_apis['v1']['representations'], {})

def test_canonical_resource_for(self):
tastypie.available_apis = {}
api = Api()
api.register(NoteResource())
api.register(UserResource(), canonical=True)
self.assertEqual(len(api._canonicals), 1)
note_resource = NoteResource()
user_resource = UserResource()
api.register(note_resource)
api.register(user_resource)
self.assertEqual(len(api._canonicals), 2)

self.assertEqual(isinstance(api.canonical_resource_for('notes'), NoteResource), True)

api_2 = Api()
self.assertRaises(URLReverseError, tastypie._get_canonical_resource_name, api_2, NoteRepresentation)
self.assertEqual(tastypie._get_canonical_resource_name(api.api_name, NoteRepresentation), 'notes')
self.assertEqual(tastypie._get_canonical_resource_name(api.api_name, NoteRepresentation()), 'notes')
self.assertEqual(tastypie._get_canonical_resource_name(api.api_name, note_resource.detail_representation), 'notes')
self.assertEqual(tastypie._get_canonical_resource_name(api.api_name, UserRepresentation), 'users')
self.assertEqual(tastypie._get_canonical_resource_name(api.api_name, UserRepresentation()), 'users')
self.assertEqual(tastypie._get_canonical_resource_name(api.api_name, user_resource.detail_representation), 'users')

self.assertRaises(NotRegistered, api.canonical_resource_for, 'notes')
self.assertEqual(isinstance(api.canonical_resource_for('users'), UserResource), True)
api.unregister(user_resource.resource_name)
self.assertRaises(NotRegistered, api.canonical_resource_for, 'users')

def test_urls(self):
api = Api()
Expand Down
Loading

0 comments on commit 72fcf11

Please sign in to comment.