From d6aea8edcb2ea78c5ceb3555d97cd40dd1805de7 Mon Sep 17 00:00:00 2001 From: Michael DeHaan Date: Tue, 19 Mar 2013 22:26:35 -0400 Subject: [PATCH] Switch over to django-rest-framework from tastypie. Less black magic, seems to just work :) --- TODO.md | 15 +- lib/api/__init__.py | 0 lib/api/auth.py | 20 - lib/api/models.py | 1 - lib/api/resources/__init__.py | 77 ---- lib/api/urls.py | 13 - lib/cli/main.py | 2 + lib/main/serializers.py | 10 + lib/main/tests.py | 145 +++++-- lib/main/views.py | 118 +++++- lib/settings/defaults.py | 8 +- lib/urls.py | 4 +- lib/vendor/extendedmodelresource.py | 620 ---------------------------- requirements.txt | 4 +- 14 files changed, 257 insertions(+), 780 deletions(-) delete mode 100644 lib/api/__init__.py delete mode 100644 lib/api/auth.py delete mode 100644 lib/api/models.py delete mode 100644 lib/api/resources/__init__.py delete mode 100644 lib/api/urls.py create mode 100644 lib/main/serializers.py delete mode 100644 lib/vendor/extendedmodelresource.py diff --git a/TODO.md b/TODO.md index e254781f60..e80aa35a8c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,21 +1,16 @@ TODO items for ansible commander ================================ -* tastypie subresources? Maybe not. Are they needed? - -* tastypie authz (various subclasses) using RBAC permissions model - - ** 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 - +* finish shift to DJANGO REST FRAMEWORK from tastypie, rework client lib code +* write custom rbac & validation classes +* determine how to auto-add hrefs to serializers (including sub-resources) * CLI client * business logic * celery integration / job status API * UI layer * clean up initial migrations +* inventory plugin +* reporting plugin NEXT STEPS diff --git a/lib/api/__init__.py b/lib/api/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lib/api/auth.py b/lib/api/auth.py deleted file mode 100644 index 53978a70d4..0000000000 --- a/lib/api/auth.py +++ /dev/null @@ -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() diff --git a/lib/api/models.py b/lib/api/models.py deleted file mode 100644 index 6b226388e6..0000000000 --- a/lib/api/models.py +++ /dev/null @@ -1 +0,0 @@ -# Empty models file. diff --git a/lib/api/resources/__init__.py b/lib/api/resources/__init__.py deleted file mode 100644 index 87a32456d7..0000000000 --- a/lib/api/resources/__init__.py +++ /dev/null @@ -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?') - - diff --git a/lib/api/urls.py b/lib/api/urls.py deleted file mode 100644 index dc727684d7..0000000000 --- a/lib/api/urls.py +++ /dev/null @@ -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)), -) diff --git a/lib/cli/main.py b/lib/cli/main.py index 91aabec703..ca549729f7 100644 --- a/lib/cli/main.py +++ b/lib/cli/main.py @@ -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 requests from requests.auth import HTTPBasicAuth diff --git a/lib/main/serializers.py b/lib/main/serializers.py new file mode 100644 index 0000000000..687900cfd2 --- /dev/null +++ b/lib/main/serializers.py @@ -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') + diff --git a/lib/main/tests.py b/lib/main/tests.py index d53e32c3bc..8b455d1a73 100644 --- a/lib/main/tests.py +++ b/lib/main/tests.py @@ -1,3 +1,5 @@ +# FIXME: do not use ResourceTestCase + """ This file demonstrates two different styles of tests (one doctest and one 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 tastypie.test import ResourceTestCase 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): @@ -32,24 +29,35 @@ class BaseResourceTest(ResourceTestCase): acom_user = User.objects.create(name=username, auth_user=django_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): # Create a user. self.super_username = 'admin' self.super_password = 'admin' - self.normal_username = '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.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) def get_normal_credentials(self): 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): return self.create_basic('random', 'combination') @@ -60,16 +68,25 @@ class OrganizationsResourceTest(BaseResourceTest): def setUp(self): 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. - # Note that we aren't using PKs because they can change depending - # on what other tests are running. - #self.entry_1 = Entry.objects.get(slug='first-post') - - # We also build a detail URI, since we will be using it all over. - # DRY, baby. DRY. - #self.detail_url = '/api/v1/entry/{0}/'.format(self.entry_1.pk) + for x in self.organizations: + # NOTE: superuser does not have to be explicitly added to admin group + # x.admins.add(self.super_acom_user) + x.users.add(self.super_acom_user) + x.users.add(self.other_acom_user) + + self.organizations[0].users.add(self.normal_acom_user) + 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 # frequently (enough). @@ -80,34 +97,94 @@ class OrganizationsResourceTest(BaseResourceTest): # 'created': '2012-05-01T22:05:12' #} - + # TODO: combine this triplet. def test_get_list_unauthorzied(self): + + # no credentials == 401 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())) - 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()) self.assertValidJSONResponse(resp) + self.assertEqual(len(self.deserialize(resp)['objects']), 2) -# # Scope out the data for correctness. -# self.assertEqual(len(self.deserialize(resp)['objects']), 12) -# # Here, we're checking an entire structure for the expected data. -# self.assertEqual(self.deserialize(resp)['objects'][0], { -# 'pk': str(self.entry_1.pk), -# 'user': '/api/v1/user/{0}/'.format(self.user.pk), -# 'title': 'First post', -# 'slug': 'first-post', -# 'created': '2012-05-01T19:13:42', -# 'resource_uri': '/api/v1/entry/{0}/'.format(self.entry_1.pk) -# }) -# -# def test_get_list_xml(self): -# self.assertValidXMLResponse(self.api_client.get('/api/v1/entries/', format='xml', authentication=self.get_credentials())) + # no admin rights? get empty list + resp = self.api_client.get(self.collection(), format='json', authentication=self.get_other_credentials()) + self.assertValidJSONResponse(resp) + self.assertEqual(len(self.deserialize(resp)['objects']), 0) + + def test_get_item(self): + + # no credentials == 401 + #self.assertHttpUnauthorized(self.api_client.get(self.a_detail_url, format='json')) + + # wrong crendentials == 401 + #self.assertHttpUnauthorized(self.api_client.get(self.c_detail_url, format='json', authentication=self.get_invalid_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): -# self.assertHttpUnauthorized(self.api_client.get(self.detail_url, format='json')) # # def test_get_detail_json(self): # resp = self.api_client.get(self.detail_url, format='json', authentication=self.get_credentials()) diff --git a/lib/main/views.py b/lib/main/views.py index 60f00ef0ef..4008a31557 100644 --- a/lib/main/views.py +++ b/lib/main/views.py @@ -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) + + diff --git a/lib/settings/defaults.py b/lib/settings/defaults.py index e51aeee835..ead8366758 100644 --- a/lib/settings/defaults.py +++ b/lib/settings/defaults.py @@ -22,6 +22,11 @@ ADMINS = ( MANAGERS = ADMINS +REST_FRAMEWORK = { + 'PAGINATE_BY': 10, + 'PAGINATE_BY_PARAM': 'page_size' +} + DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', @@ -116,12 +121,11 @@ INSTALLED_APPS = ( 'django.contrib.sites', 'django.contrib.staticfiles', 'lib.main', - 'lib.api', 'lib.web', 'south', # not yet compatible with Django 1.5 unless using version from github # 'devserver', - 'tastypie', + 'rest_framework', 'django_extensions', 'djcelery', 'kombu.transport.django', diff --git a/lib/urls.py b/lib/urls.py index c94486fe38..5b67f03d70 100644 --- a/lib/urls.py +++ b/lib/urls.py @@ -1,9 +1,11 @@ from django.conf import settings from django.conf.urls import * +import lib.main.views as views urlpatterns = patterns('', 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[0-9]+)/$', views.OrganizationsDetail.as_view()), ) if 'django.contrib.admin' in settings.INSTALLED_APPS: diff --git a/lib/vendor/extendedmodelresource.py b/lib/vendor/extendedmodelresource.py deleted file mode 100644 index 53f589eed2..0000000000 --- a/lib/vendor/extendedmodelresource.py +++ /dev/null @@ -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%s)%s$" % - (self._meta.resource_name, trailing_slash()), - self.wrap_view('dispatch_list'), - name="api_dispatch_list"), - url(r"^(?P%s)/schema%s$" % - (self._meta.resource_name, trailing_slash()), - self.wrap_view('get_schema'), - name="api_get_schema"), - url(r"^(?P%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%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%s)/(?P<%s>%s)/" - r"(?P%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%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) - diff --git a/requirements.txt b/requirements.txt index 626cd582d6..f25c85455a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,8 +3,10 @@ django-celery==3.0.11 django-devserver==0.4.0 django-extensions==1.1.1 django-jsonfield==0.9.2 -django-tastypie==0.9.12 ipython==0.13.1 South==0.7.6 python-dateutil==1.5 requests +djangorestframework +markdown +django-filter