Switch over to django-rest-framework from tastypie. Less black magic, seems to just work :)

This commit is contained in:
Michael DeHaan
2013-03-19 22:26:35 -04:00
parent b95f4e0e13
commit d6aea8edcb
14 changed files with 257 additions and 780 deletions

15
TODO.md
View File

@@ -1,21 +1,16 @@
TODO items for ansible commander TODO items for ansible commander
================================ ================================
* tastypie subresources? Maybe not. Are they needed? * finish shift to DJANGO REST FRAMEWORK from tastypie, rework client lib code
* write custom rbac & validation classes
* tastypie authz (various subclasses) using RBAC permissions model * determine how to auto-add hrefs to serializers (including sub-resources)
** for editing, is user able to edit the resource
** if they can, did they remove anything they should not remove or add anything they cannot add?
** did they set any properites on any resources beyond just creating them?
* tastypie tests using various users
* CLI client * CLI client
* business logic * business logic
* celery integration / job status API * celery integration / job status API
* UI layer * UI layer
* clean up initial migrations * clean up initial migrations
* inventory plugin
* reporting plugin
NEXT STEPS NEXT STEPS

View File

View File

@@ -1,20 +0,0 @@
from tastypie.authentication import Authentication
from tastypie.authorization import Authorization
# FIXME: this is completely stubbed out at this point!
# INTENTIONALLY NOT IMPLEMENTED CORRECTLY :)
class AcomAuthorization(Authorization):
def is_authorized(self, request, object=None):
if request.user.username == 'admin':
return True
else:
return False
# Optional but useful for advanced limiting, such as per user.
def apply_limits(self, request, object_list):
#if request and hasattr(request, 'user'):
# return object_list.filter(author__username=request.user.username)
#return object_list.none()
return object_list.all()

View File

@@ -1 +0,0 @@
# Empty models file.

View File

@@ -1,77 +0,0 @@
from tastypie.resources import Resource, ModelResource, ALL
from tastypie.authentication import BasicAuthentication
from tastypie import fields, utils
from lib.api.auth import AcomAuthorization
#from django.conf.urls import url
import lib.main.models as models
from lib.vendor.extendedmodelresource import ExtendedModelResource
from tastypie.authorization import Authorization
class OrganizationAuthorization(Authorization):
"""
Our Authorization class for UserResource and its nested.
"""
def is_authorized(self, request, object=None):
if request.user.username == 'admin':
return True
else:
return False
def is_authorized(self, request, object=None):
# HACK
if 'admin' in request.user.username:
return True
return False
def apply_limits(self, request, object_list):
return object_list.all()
def is_authorized_nested_projects(self, request, parent_object, object=None):
# Is request.user authorized to access the EntryResource as # nested?
return True
def apply_limits_nested_projects(self, request, parent_object, object_list):
# Advanced filtering.
# Note that object_list already only contains the objects that
# are associated to parent_object.
return object_list.all()
class Organizations(ExtendedModelResource):
class Meta:
# related fields...
queryset = models.Organization.objects.all()
resource_name = 'organizations'
authentication = BasicAuthentication()
#authorization = AcomAuthorization()
authorization = OrganizationAuthorization()
class Nested:
#users = fields.ToManyField('lib.api.resources.Users', 'users', related_name='organizations', blank=True, help_text='list of all organization users')
#admins = fields.ToManyField('lib.api.resources.Users', 'admins', related_name='admin_of_organizations', blank=True, help_text='list of administrator users')
projects = fields.ToManyField('lib.api.resources.Projects', 'projects') # blank=True, help_text='list of projects')
def is_authorized(self, request, object=None):
return True
class Users(ExtendedModelResource):
class Meta:
queryset = models.User.objects.all()
resource_name = 'users'
authorization = AcomAuthorization()
class Projects(ExtendedModelResource):
class Meta:
queryset = models.Project.objects.all()
resource_name = 'projects'
authorization = AcomAuthorization()
#organizations = fields.ToManyField('lib.api.resources.Organizations', 'organizations', help_text='which organizations is this project in?')

View File

@@ -1,13 +0,0 @@
from django.conf import settings
from django.conf.urls import *
from tastypie.api import Api
from lib.api.resources import *
v1_api = Api(api_name='v1')
v1_api.register(Organizations())
v1_api.register(Projects())
v1_api.register(Users())
urlpatterns = patterns('',
(r'', include(v1_api.urls)),
)

View File

@@ -1,3 +1,5 @@
# FIXME: as we've switched things over to django-rest-framework from tastypie, this will need some revision
import os import os
import requests import requests
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth

10
lib/main/serializers.py Normal file
View File

@@ -0,0 +1,10 @@
from django.contrib.auth.models import User as DjangoUser
from lib.main.models import User, Organization, Project
from rest_framework import serializers, pagination
class OrganizationSerializer(serializers.ModelSerializer):
class Meta:
model = Organization
fields = ('name', 'description')

View File

@@ -1,3 +1,5 @@
# FIXME: do not use ResourceTestCase
""" """
This file demonstrates two different styles of tests (one doctest and one This file demonstrates two different styles of tests (one doctest and one
unittest). These will both pass when you run "manage.py test". unittest). These will both pass when you run "manage.py test".
@@ -11,11 +13,6 @@ import datetime
from django.contrib.auth.models import User as DjangoUser from django.contrib.auth.models import User as DjangoUser
from tastypie.test import ResourceTestCase from tastypie.test import ResourceTestCase
from lib.main.models import User, Organization, Project from lib.main.models import User, Organization, Project
# from entries.models import Entry
#class SimpleTest(TestCase):
# def test_basic_addition(self):
# self.failUnlessEqual(1 + 1, 2)
class BaseResourceTest(ResourceTestCase): class BaseResourceTest(ResourceTestCase):
@@ -32,24 +29,35 @@ class BaseResourceTest(ResourceTestCase):
acom_user = User.objects.create(name=username, auth_user=django_user) acom_user = User.objects.create(name=username, auth_user=django_user)
return (django_user, acom_user) return (django_user, acom_user)
def make_organizations(self, count=1):
results = []
for x in range(0, count):
results.append(Organization.objects.create(name="org%s" % x, description="org%s" % x))
return results
def setup_users(self): def setup_users(self):
# Create a user. # Create a user.
self.super_username = 'admin' self.super_username = 'admin'
self.super_password = 'admin' self.super_password = 'admin'
self.normal_username = 'normal' self.normal_username = 'normal'
self.normal_password = 'normal' self.normal_password = 'normal'
self.other_username = 'other'
self.other_password = 'other'
(self.super_django_user, self.super_acom_user) = self.make_user(self.super_username, self.super_password, super_user=True) (self.super_django_user, self.super_acom_user) = self.make_user(self.super_username, self.super_password, super_user=True)
(self.normal_django_user, self.normal_acom_user) = self.make_user(self.normal_username, self.normal_password, super_user=False) (self.normal_django_user, self.normal_acom_user) = self.make_user(self.normal_username, self.normal_password, super_user=False)
(self.other_django_user, self.other_acom_user) = self.make_user(self.other_username, self.other_password, super_user=False)
def get_super_credentials(): def get_super_credentials(self):
return self.create_basic(self.super_username, self.super_password) return self.create_basic(self.super_username, self.super_password)
def get_normal_credentials(self): def get_normal_credentials(self):
return self.create_basic(self.normal_username, self.normal_password) return self.create_basic(self.normal_username, self.normal_password)
def get_other_credentials(self):
return self.create_basic(self.other_username, self.other_password)
def get_invalid_credentials(self): def get_invalid_credentials(self):
return self.create_basic('random', 'combination') return self.create_basic('random', 'combination')
@@ -60,16 +68,25 @@ class OrganizationsResourceTest(BaseResourceTest):
def setUp(self): def setUp(self):
super(OrganizationsResourceTest, self).setUp() super(OrganizationsResourceTest, self).setUp()
self.organizations = self.make_organizations(10)
self.a_detail_url = "%s%s" % (self.collection(), self.organizations[0].pk)
self.b_detail_url = "%s%s" % (self.collection(), self.organizations[1].pk)
self.c_detail_url = "%s%s" % (self.collection(), self.organizations[2].pk)
# configuration:
# admin_user is an admin and regular user in all organizations
# other_user is all organizations
# normal_user is a user in organization 0, and an admin of organization 1
# Fetch the ``Entry`` object we'll use in testing. for x in self.organizations:
# Note that we aren't using PKs because they can change depending # NOTE: superuser does not have to be explicitly added to admin group
# on what other tests are running. # x.admins.add(self.super_acom_user)
#self.entry_1 = Entry.objects.get(slug='first-post') x.users.add(self.super_acom_user)
x.users.add(self.other_acom_user)
# We also build a detail URI, since we will be using it all over.
# DRY, baby. DRY. self.organizations[0].users.add(self.normal_acom_user)
#self.detail_url = '/api/v1/entry/{0}/'.format(self.entry_1.pk) self.organizations[0].users.add(self.normal_acom_user)
self.organizations[1].admins.add(self.normal_acom_user)
# The data we'll send on POST requests. Again, because we'll use it # The data we'll send on POST requests. Again, because we'll use it
# frequently (enough). # frequently (enough).
@@ -80,34 +97,94 @@ class OrganizationsResourceTest(BaseResourceTest):
# 'created': '2012-05-01T22:05:12' # 'created': '2012-05-01T22:05:12'
#} #}
# TODO: combine this triplet.
def test_get_list_unauthorzied(self): def test_get_list_unauthorzied(self):
# no credentials == 401
self.assertHttpUnauthorized(self.api_client.get(self.collection(), format='json')) self.assertHttpUnauthorized(self.api_client.get(self.collection(), format='json'))
def test_get_list_invalid_authorization(self): # wrong credentials == 401
self.assertHttpUnauthorized(self.api_client.get(self.collection(), format='json', authentication=self.get_invalid_credentials())) self.assertHttpUnauthorized(self.api_client.get(self.collection(), format='json', authentication=self.get_invalid_credentials()))
def test_get_list_json(self): # superuser credentials == 200, full list
resp = self.api_client.get(self.collection(), format='json', authentication=self.get_super_credentials())
self.assertValidJSONResponse(resp)
self.assertEqual(len(self.deserialize(resp)['objects']), 10)
# check member data
first = self.deserialize(resp)['objects'][0]
self.assertEqual(first['name'], 'org0')
# normal credentials == 200, get only organizations that I am actually added to (there are 2)
resp = self.api_client.get(self.collection(), format='json', authentication=self.get_normal_credentials()) resp = self.api_client.get(self.collection(), format='json', authentication=self.get_normal_credentials())
self.assertValidJSONResponse(resp) self.assertValidJSONResponse(resp)
self.assertEqual(len(self.deserialize(resp)['objects']), 2)
# # Scope out the data for correctness. # no admin rights? get empty list
# self.assertEqual(len(self.deserialize(resp)['objects']), 12) resp = self.api_client.get(self.collection(), format='json', authentication=self.get_other_credentials())
# # Here, we're checking an entire structure for the expected data. self.assertValidJSONResponse(resp)
# self.assertEqual(self.deserialize(resp)['objects'][0], { self.assertEqual(len(self.deserialize(resp)['objects']), 0)
# 'pk': str(self.entry_1.pk),
# 'user': '/api/v1/user/{0}/'.format(self.user.pk), def test_get_item(self):
# 'title': 'First post',
# 'slug': 'first-post', # no credentials == 401
# 'created': '2012-05-01T19:13:42', #self.assertHttpUnauthorized(self.api_client.get(self.a_detail_url, format='json'))
# 'resource_uri': '/api/v1/entry/{0}/'.format(self.entry_1.pk)
# }) # wrong crendentials == 401
# #self.assertHttpUnauthorized(self.api_client.get(self.c_detail_url, format='json', authentication=self.get_invalid_credentials())
# def test_get_list_xml(self):
# self.assertValidXMLResponse(self.api_client.get('/api/v1/entries/', format='xml', authentication=self.get_credentials())) # superuser credentials ==
pass
def test_get_item_subobjects_projects(self):
pass
def test_get_item_subobjects_users(self):
pass
def test_get_item_subobjects_admins(self):
pass
def test_post_item(self):
pass
def test_post_item_subobjects_projects(self):
pass
def test_post_item_subobjects_users(self):
pass
def test_post_item_subobjects_admins(self):
pass
def test_put_item(self):
pass
def test_put_item_subobjects_projects(self):
pass
def test_put_item_subobjects_users(self):
pass
def test_put_item_subobjects_admins(self):
pass
def test_delete_item(self):
pass
def test_delete_item_subobjects_projects(self):
pass
def test_delete_item_subobjects_users(self):
pass
def test_delete_item_subobjects_admins(self):
pass
def test_get_list_xml(self):
self.assertValidXMLResponse(self.api_client.get(self.collection(), format='xml', authentication=self.get_normal_credentials()))
# #
# def test_get_detail_unauthenticated(self): # def test_get_detail_unauthenticated(self):
# self.assertHttpUnauthorized(self.api_client.get(self.detail_url, format='json'))
# #
# def test_get_detail_json(self): # def test_get_detail_json(self):
# resp = self.api_client.get(self.detail_url, format='json', authentication=self.get_credentials()) # resp = self.api_client.get(self.detail_url, format='json', authentication=self.get_credentials())

View File

@@ -1 +1,117 @@
# Create your views here. from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
#from rest_framework.renderers import JSONRenderer
#from rest_framework.parsers import JSONParser
from lib.main.models import *
from lib.main.serializers import *
from rest_framework import mixins
from rest_framework import generics
from rest_framework import permissions
from rest_framework import permissions
# TODO: verify pagination
# TODO: how to add relative resources
# TODO:
class CustomRbac(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS: # GET, HEAD, OPTIONS
return True
# Write permissions are only allowed to the owner of the snippet
return obj.owner == request.user
class OrganizationsList(generics.ListCreateAPIView):
model = Organization
serializer_class = OrganizationSerializer
permission_classes = (CustomRbac,)
#def pre_save(self, obj):
# obj.owner = self.request.user
class OrganizationsDetail(generics.RetrieveUpdateDestroyAPIView):
model = Organization
serializer_class = OrganizationSerializer
permission_classes = (CustomRbac,)
#def pre_save(self, obj):
# obj.owner = self.request.user
#class OrganizationsList(mixins.ListModelMixin, mixins.CreateModelMixin, generics.MultipleObjectAPIView):
#
# model = Organization
# serializer_class = OrganizationSerializer
#
# def get(self, request, *args, **kwargs):
# return self.list(request, *args, **kwargs)
#
# def post(self, request, *args, **kwargs):
# return self.create(request, *args, **kwargs)
#class JSONResponse(HttpResponse):
# """
# An HttpResponse that renders it's content into JSON.
# """
# def __init__(self, data, **kwargs):
# content = JSONRenderer().render(data)
# kwargs['content_type'] = 'application/json'
# super(JSONResponse, self).__init__(content, **kwargs)
#@csrf_exempt
#def organizations_list(request):
# """
# List all code snippets, or create a new snippet.
# """
# if request.method == 'GET':
# # TODO: FILTER
# organizations = Organization.objects.all()
# serializer = OrganizationSerializer(organizations, many=True)
# return JSONResponse(serializer.data)
#
# elif request.method == 'POST':
# data = JSONParser().parse(request)
# # TODO: DATA AUDIT
# serializer = OrganizationSerializer(data=data)
# if serializer.is_valid():
# serializer.save()
# return JSONResponse(serializer.data, status=201)
# else:
# return JSONResponse(serializer.errors, status=400)
#@csrf_exempt
#def snippet_detail(request, pk):
# """
# Retrieve, update or delete a code snippet.
# """
# try:
# snippet = Snippet.objects.get(pk=pk)
# except Snippet.DoesNotExist:
# return HttpResponse(status=404)
#
# if request.method == 'GET':
# serializer = SnippetSerializer(snippet)
# return JSONResponse(serializer.data)
#
# elif request.method == 'PUT':
# data = JSONParser().parse(request)
# serializer = SnippetSerializer(snippet, data=data)
# if serializer.is_valid():
# serializer.save()
# return JSONResponse(serializer.data)
# else:
# return JSONResponse(serializer.errors, status=400)
#
# elif request.method == 'DELETE':
# snippet.delete()
# return HttpResponse(status=204)

View File

@@ -22,6 +22,11 @@ ADMINS = (
MANAGERS = ADMINS MANAGERS = ADMINS
REST_FRAMEWORK = {
'PAGINATE_BY': 10,
'PAGINATE_BY_PARAM': 'page_size'
}
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.sqlite3',
@@ -116,12 +121,11 @@ INSTALLED_APPS = (
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'lib.main', 'lib.main',
'lib.api',
'lib.web', 'lib.web',
'south', 'south',
# not yet compatible with Django 1.5 unless using version from github # not yet compatible with Django 1.5 unless using version from github
# 'devserver', # 'devserver',
'tastypie', 'rest_framework',
'django_extensions', 'django_extensions',
'djcelery', 'djcelery',
'kombu.transport.django', 'kombu.transport.django',

View File

@@ -1,9 +1,11 @@
from django.conf import settings from django.conf import settings
from django.conf.urls import * from django.conf.urls import *
import lib.main.views as views
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'', include('lib.web.urls')), url(r'', include('lib.web.urls')),
url(r'^api/', include('lib.api.urls')), url(r'^api/v1/organizations/$', views.OrganizationsList.as_view()),
url(r'^api/v1/organizations/(?P<pk>[0-9]+)/$', views.OrganizationsDetail.as_view()),
) )
if 'django.contrib.admin' in settings.INSTALLED_APPS: if 'django.contrib.admin' in settings.INSTALLED_APPS:

View File

@@ -1,620 +0,0 @@
# modified version of https://github.com/tryolabs/django-tastypie-extendedmodelresource
# from PyPi, tweaked to make it work with latest tastypie
from django.http import HttpResponse
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.core.urlresolvers import get_script_prefix, resolve, Resolver404
from django.conf.urls.defaults import patterns, url, include
from tastypie import fields, http
from tastypie.exceptions import NotFound, ImmediateHttpResponse
from tastypie.resources import ResourceOptions, ModelDeclarativeMetaclass, \
ModelResource, convert_post_to_put
from tastypie.utils import trailing_slash
class ExtendedDeclarativeMetaclass(ModelDeclarativeMetaclass):
"""
Same as ``DeclarativeMetaclass`` but uses ``AnyIdAttributeResourceOptions``
instead of ``ResourceOptions`` and adds support for multiple nested fields
defined in a "Nested" class (the same way as "Meta") inside the resources.
"""
def __new__(cls, name, bases, attrs):
new_class = super(ExtendedDeclarativeMetaclass, cls).__new__(cls,
name, bases, attrs)
opts = getattr(new_class, 'Meta', None)
new_class._meta = ResourceOptions(opts)
# Will map nested fields names to the actual fields
nested_fields = {}
nested_class = getattr(new_class, 'Nested', None)
if nested_class is not None:
for field_name in dir(nested_class):
if not field_name.startswith('_'): # No internals
field_object = getattr(nested_class, field_name)
nested_fields[field_name] = field_object
if hasattr(field_object, 'contribute_to_class'):
field_object.contribute_to_class(new_class,
field_name)
new_class._nested = nested_fields
return new_class
class ExtendedModelResource(ModelResource):
__metaclass__ = ExtendedDeclarativeMetaclass
def remove_api_resource_names(self, url_dict):
"""
Given a dictionary of regex matches from a URLconf, removes
``api_name`` and/or ``resource_name`` if found.
This is useful for converting URLconf matches into something suitable
for data lookup. For example::
Model.objects.filter(**self.remove_api_resource_names(matches))
"""
kwargs_subset = url_dict.copy()
for key in ['api_name', 'resource_name', 'related_manager',
'child_object', 'parent_resource', 'nested_name',
'parent_object']:
try:
del(kwargs_subset[key])
except KeyError:
pass
return kwargs_subset
def get_detail_uri_name_regex(self):
"""
Return the regular expression to which the id attribute used in
resource URLs should match.
By default we admit any alphanumeric value and "-", but you may
override this function and provide your own.
"""
return r'\w[\w-]*'
def base_urls(self):
"""
Same as the original ``base_urls`` but supports using the custom
regex for the ``detail_uri_name`` attribute of the objects.
"""
# Due to the way Django parses URLs, ``get_multiple``
# won't work without a trailing slash.
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<%s_list>(%s;?)*)/$" %
(self._meta.resource_name,
self._meta.detail_uri_name,
self.get_detail_uri_name_regex()),
self.wrap_view('get_multiple'),
name="api_get_multiple"),
url(r"^(?P<resource_name>%s)/(?P<%s>%s)%s$" %
(self._meta.resource_name,
self._meta.detail_uri_name,
self.get_detail_uri_name_regex(),
trailing_slash()),
self.wrap_view('dispatch_detail'),
name="api_dispatch_detail"),
]
def nested_urls(self):
"""
Return the list of all urls nested under the detail view of a resource.
Each resource listed as Nested will generate one url.
"""
def get_nested_url(nested_name):
return url(r"^(?P<resource_name>%s)/(?P<%s>%s)/"
r"(?P<nested_name>%s)%s$" %
(self._meta.resource_name,
self._meta.detail_uri_name,
self.get_detail_uri_name_regex(),
nested_name,
trailing_slash()),
self.wrap_view('dispatch_nested'),
name='api_dispatch_nested')
return [get_nested_url(nested_name)
for nested_name in self._nested.keys()]
def detail_actions(self):
"""
Return urls of custom actions to be performed on the detail view of a
resource. These urls will be appended to the url of the detail view.
This allows a finer control by providing a custom view for each of
these actions in the resource.
A resource should override this method and provide its own list of
detail actions urls, if needed.
For example:
return [
url(r"^show_schema/$", self.wrap_view('get_schema'),
name="api_get_schema")
]
will add show schema capabilities to a detail resource URI (ie.
/api/user/3/show_schema/ will work just like /api/user/schema/).
"""
return []
def detail_actions_urlpatterns(self):
"""
Return the url patterns corresponding to the detail actions available
on this resource.
"""
if self.detail_actions():
detail_url = "^(?P<resource_name>%s)/(?P<%s>%s)/" % (
self._meta.resource_name,
self._meta.detail_uri_name,
self.get_detail_uri_name_regex()
)
return patterns('', (detail_url, include(self.detail_actions())))
return []
@property
def urls(self):
"""
The endpoints this ``Resource`` responds to.
Same as the original ``urls`` attribute but supports nested urls as
well as detail actions urls.
"""
urls = self.override_urls() + self.base_urls() + self.nested_urls()
return patterns('', *urls) + self.detail_actions_urlpatterns()
def is_authorized_over_parent(self, request, parent_object):
"""
Allows the ``Authorization`` class to check if a request to a nested
resource has permissions over the parent.
Will call the ``is_authorized_parent`` function of the
``Authorization`` class.
"""
if hasattr(self._meta.authorization, 'is_authorized_parent'):
return self._meta.authorization.is_authorized_parent(request,
parent_object)
return True
def parent_obj_get(self, request=None, **kwargs):
"""
Same as the original ``obj_get`` but called when a nested resource
wants to get its parent.
Will check authorization to see if the request is allowed to act on
the parent resource.
"""
parent_object = self.get_object_list(request).get(**kwargs)
# If I am not authorized for the parent
if not self.is_authorized_over_parent(request, parent_object):
stringified_kwargs = ', '.join(["%s=%s" % (k, v)
for k, v in kwargs.items()])
raise self._meta.object_class.DoesNotExist("Couldn't find an "
"instance of '%s' which matched '%s'." %
(self._meta.object_class.__name__, stringified_kwargs))
return parent_object
def parent_cached_obj_get(self, request=None, **kwargs):
"""
Same as the original ``cached_obj_get`` but called when a nested
resource wants to get its parent.
"""
cache_key = self.generate_cache_key('detail', **kwargs)
bundle = self._meta.cache.get(cache_key)
if bundle is None:
bundle = self.parent_obj_get(request=request, **kwargs)
self._meta.cache.set(cache_key, bundle)
return bundle
def get_via_uri_resolver(self, uri):
"""
Do the work of the original ``get_via_uri`` except calling ``obj_get``.
Use this as a helper function.
"""
prefix = get_script_prefix()
chomped_uri = uri
if prefix and chomped_uri.startswith(prefix):
chomped_uri = chomped_uri[len(prefix) - 1:]
try:
_view, _args, kwargs = resolve(chomped_uri)
except Resolver404:
raise NotFound("The URL provided '%s' was not a link to a valid "
"resource." % uri)
return kwargs
def get_nested_via_uri(self, uri, parent_resource,
parent_object, nested_name, request=None):
"""
Obtain a nested resource from an uri, a parent resource and a parent
object.
Calls ``obj_get`` which handles the authorization checks.
"""
# TODO: improve this to get parent resource & object from uri too?
kwargs = self.get_via_uri_resolver(uri)
return self.obj_get(nested_name=nested_name,
parent_resource=parent_resource,
parent_object=parent_object,
request=request,
**self.remove_api_resource_names(kwargs))
def get_via_uri_no_auth_check(self, uri, request=None):
"""
Obtain a nested resource from an uri, a parent resource and a
parent object.
Does *not* do authorization checks, those must be performed manually.
This function is useful be called from custom views over a resource
which need access to objects and can do the check of permissions
theirselves.
"""
kwargs = self.get_via_uri_resolver(uri)
return self.obj_get_no_auth_check(request=request,
**self.remove_api_resource_names(kwargs))
def obj_get(self, request=None, **kwargs):
"""
Same as the original ``obj_get`` but knows when it is being called to
get an object from a nested resource uri.
Performs authorization checks in every case.
"""
try:
nested_name = kwargs.pop('nested_name', None)
parent_resource = kwargs.pop('parent_resource', None)
parent_object = kwargs.pop('parent_object', None)
bundle = kwargs.pop('bundle', None) # MPD: fixup
base_object_list = self.get_object_list(request).filter(**kwargs)
if nested_name is not None:
# TODO: throw exception if parent_resource or parent_object are
# None
object_list = self.apply_nested_authorization_limits(request,
nested_name, parent_resource,
parent_object, base_object_list)
else:
object_list = self.apply_authorization_limits(request,
base_object_list)
stringified_kwargs = ', '.join(["%s=%s" % (k, v)
for k, v in kwargs.items()])
if len(object_list) <= 0:
raise self._meta.object_class.DoesNotExist("Couldn't find an "
"instance of '%s' which matched '%s'." %
(self._meta.object_class.__name__,
stringified_kwargs))
elif len(object_list) > 1:
raise MultipleObjectsReturned("More than '%s' matched '%s'." %
(self._meta.object_class.__name__, stringified_kwargs))
return object_list[0]
except ValueError:
raise NotFound("Invalid resource lookup data provided (mismatched "
"type).")
def obj_get_no_auth_check(self, request=None, **kwargs):
"""
Same as the original ``obj_get`` knows when it is being called to get
a nested resource.
Does *not* do authorization checks.
"""
# TODO: merge this and original obj_get and use another argument in
# kwargs to know if we should check for auth?
try:
object_list = self.get_object_list(request).filter(**kwargs)
stringified_kwargs = ', '.join(["%s=%s" % (k, v)
for k, v in kwargs.items()])
if len(object_list) <= 0:
raise self._meta.object_class.DoesNotExist("Couldn't find an "
"instance of '%s' which matched '%s'." %
(self._meta.object_class.__name__,
stringified_kwargs))
elif len(object_list) > 1:
raise MultipleObjectsReturned("More than '%s' matched '%s'." %
(self._meta.object_class.__name__, stringified_kwargs))
return object_list[0]
except ValueError:
raise NotFound("Invalid resource lookup data provided (mismatched "
"type).")
def apply_nested_authorization_limits(self, request, nested_name,
parent_resource, parent_object,
object_list):
"""
Allows the ``Authorization`` class to further limit the object list.
Also a hook to customize per ``Resource``.
"""
method_name = 'apply_limits_nested_%s' % nested_name
if hasattr(parent_resource._meta.authorization, method_name):
method = getattr(parent_resource._meta.authorization, method_name)
object_list = method(request, parent_object, object_list)
return object_list
def dispatch_nested(self, request, **kwargs):
"""
Dispatch a request to the nested resource.
"""
# We don't check for is_authorized here since it will be
# parent_cached_obj_get which will check that we have permissions
# over the parent.
self.is_authenticated(request)
self.throttle_check(request)
nested_name = kwargs.pop('nested_name')
nested_field = self._nested[nested_name]
try:
obj = self.parent_cached_obj_get(request=request,
**self.remove_api_resource_names(kwargs))
except ObjectDoesNotExist:
return http.HttpNotFound()
except MultipleObjectsReturned:
return http.HttpMultipleChoices("More than one parent resource is "
"found at this URI.")
# The nested resource needs to get the api_name from its parent because
# it is possible that the resource being used as nested is not
# registered in the API (ie. it can only be used as nested)
nested_resource = nested_field.to_class()
nested_resource._meta.api_name = self._meta.api_name
# TODO: comment further to make sense of this block
manager = None
try:
if isinstance(nested_field.attribute, basestring):
name = nested_field.attribute
manager = getattr(obj, name, None)
elif callable(nested_field.attribute):
manager = nested_field.attribute(obj)
else:
raise fields.ApiFieldError(
"The model '%r' has an empty attribute '%s' \
and doesn't allow a null value." % (
obj,
nested_field.attribute
)
)
except ObjectDoesNotExist:
pass
kwargs['nested_name'] = nested_name
kwargs['parent_resource'] = self
kwargs['parent_object'] = obj
if manager is None or not hasattr(manager, 'all'):
dispatch_type = 'detail'
kwargs['child_object'] = manager
else:
dispatch_type = 'list'
kwargs['related_manager'] = manager
return nested_resource.dispatch(
dispatch_type,
request,
**kwargs
)
# MPD: fixup upstream module
def is_authorized(self, request):
auth = getattr(self._meta, 'authorization')
if auth is not None:
return auth.is_authorized(request)
return False
def is_authorized_nested(self, request, nested_name,
parent_resource, parent_object, object=None):
"""
Handles checking of permissions to see if the user has authorization
to GET, POST, PUT, or DELETE this resource. If ``object`` is provided,
the authorization backend can apply additional row-level permissions
checking.
"""
# We use the authorization of the parent resource
method_name = 'is_authorized_nested_%s' % nested_name
if hasattr(parent_resource._meta.authorization, method_name):
method = getattr(parent_resource._meta.authorization, method_name)
auth_result = method(request, parent_object, object)
if isinstance(auth_result, HttpResponse):
raise ImmediateHttpResponse(response=auth_result)
if not auth_result is True:
raise ImmediateHttpResponse(response=http.HttpUnauthorized())
def dispatch(self, request_type, request, **kwargs):
"""
Same as the usual dispatch, but knows if its being called from a nested
resource.
"""
allowed_methods = getattr(self._meta,
"%s_allowed_methods" % request_type, None)
request_method = self.method_check(request, allowed=allowed_methods)
method = getattr(self, "%s_%s" % (request_method, request_type), None)
if method is None:
raise ImmediateHttpResponse(response=http.HttpNotImplemented())
self.is_authenticated(request)
self.throttle_check(request)
nested_name = kwargs.get('nested_name', None)
parent_resource = kwargs.get('parent_resource', None)
parent_object = kwargs.get('parent_object', None)
if nested_name is None:
self.is_authorized(request)
else:
self.is_authorized_nested(request, nested_name,
parent_resource,
parent_object)
# All clear. Process the request.
request = convert_post_to_put(request)
# MPD: fixup
response = method(request, **kwargs)
# Add the throttled request.
self.log_throttled_access(request)
# If what comes back isn't a ``HttpResponse``, assume that the
# request was accepted and that some action occurred. This also
# prevents Django from freaking out.
if not isinstance(response, HttpResponse):
return http.HttpNoContent()
return response
def obj_create(self, bundle, request=None, **kwargs):
related_manager = kwargs.pop('related_manager', None)
# Remove the other parameters used for the nested resources, if they
# are present.
kwargs.pop('nested_name', None)
kwargs.pop('parent_resource', None)
kwargs.pop('parent_object', None)
bundle.obj = self._meta.object_class()
for key, value in kwargs.items():
setattr(bundle.obj, key, value)
bundle = self.full_hydrate(bundle)
# Save FKs just in case.
self.save_related(bundle)
if related_manager is not None:
related_manager.add(bundle.obj)
# Save the main object.
bundle.obj.save()
# Now pick up the M2M bits.
m2m_bundle = self.hydrate_m2m(bundle)
self.save_m2m(m2m_bundle)
return bundle
def get_list(self, request, **kwargs):
"""
Returns a serialized list of resources.
Calls ``obj_get_list`` to provide the data, then handles that result
set and serializes it.
Should return a HttpResponse (200 OK).
"""
if 'related_manager' in kwargs:
manager = kwargs.pop('related_manager')
base_objects = manager.all()
nested_name = kwargs.pop('nested_name', None)
parent_resource = kwargs.pop('parent_resource', None)
parent_object = kwargs.pop('parent_object', None)
objects = self.apply_nested_authorization_limits(request,
nested_name, parent_resource, parent_object,
base_objects)
else:
# MPD: fixup compat with tastypie
basic_bundle = self.build_bundle(request=request)
objects = self.obj_get_list(
basic_bundle, # WAS: 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_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.
bundles = []
for obj in to_be_serialized['objects']:
bundles.append(
self.full_dehydrate(
self.build_bundle(obj=obj, request=request)
)
)
to_be_serialized['objects'] = bundles
to_be_serialized = self.alter_list_data_to_serialize(request,
to_be_serialized)
return self.create_response(request, to_be_serialized)
def get_detail(self, request, **kwargs):
"""
Returns a single serialized resource.
Calls ``cached_obj_get/obj_get`` to provide the data, then handles that
result set and serializes it.
Should return a HttpResponse (200 OK).
"""
try:
# If call was made through Nested we should already have the
# child object.
if 'child_object' in kwargs:
obj = kwargs.pop('child_object', None)
if obj is None:
return http.HttpNotFound()
else:
# MPD: fixed up
basic_bundle = self.build_bundle(request=request)
# MPD: fixup
if 'bundle' in kwargs:
kwargs.pop('bundle')
obj = self.cached_obj_get(basic_bundle, **self.remove_api_resource_names(kwargs))
except AttributeError:
return http.HttpNotFound()
except ObjectDoesNotExist:
return http.HttpNotFound()
except MultipleObjectsReturned:
return http.HttpMultipleChoices("More than one resource is found "
"at this URI.")
bundle = self.build_bundle(obj=obj, request=request)
bundle = self.full_dehydrate(bundle)
bundle = self.alter_detail_data_to_serialize(request, bundle)
return self.create_response(request, bundle)

View File

@@ -3,8 +3,10 @@ django-celery==3.0.11
django-devserver==0.4.0 django-devserver==0.4.0
django-extensions==1.1.1 django-extensions==1.1.1
django-jsonfield==0.9.2 django-jsonfield==0.9.2
django-tastypie==0.9.12
ipython==0.13.1 ipython==0.13.1
South==0.7.6 South==0.7.6
python-dateutil==1.5 python-dateutil==1.5
requests requests
djangorestframework
markdown
django-filter