Removed local copy of tastypie
- Legacy-Id: 9562
This commit is contained in:
parent
e6518affe4
commit
4ec326a505
|
@ -1,5 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
|
||||
__author__ = 'Daniel Lindsley & the Tastypie core team'
|
||||
__version__ = (0, 12, 1)
|
|
@ -1,20 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
|
||||
|
||||
if 'django.contrib.auth' in settings.INSTALLED_APPS:
|
||||
from tastypie.models import ApiKey
|
||||
|
||||
class ApiKeyInline(admin.StackedInline):
|
||||
model = ApiKey
|
||||
extra = 0
|
||||
|
||||
ABSTRACT_APIKEY = getattr(settings, 'TASTYPIE_ABSTRACT_APIKEY', False)
|
||||
|
||||
if ABSTRACT_APIKEY and not isinstance(ABSTRACT_APIKEY, bool):
|
||||
raise TypeError("'TASTYPIE_ABSTRACT_APIKEY' must be either 'True' "
|
||||
"or 'False'.")
|
||||
|
||||
if not ABSTRACT_APIKEY:
|
||||
admin.site.register(ApiKey)
|
184
tastypie/api.py
184
tastypie/api.py
|
@ -1,184 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
import warnings
|
||||
from django.conf.urls import url, patterns, include
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from tastypie.exceptions import NotRegistered, BadRequest
|
||||
from tastypie.serializers import Serializer
|
||||
from tastypie.utils import trailing_slash, is_valid_jsonp_callback_value
|
||||
from tastypie.utils.mime import determine_format, build_content_type
|
||||
|
||||
|
||||
class Api(object):
|
||||
"""
|
||||
Implements a registry to tie together the various resources that make up
|
||||
an API.
|
||||
|
||||
Especially useful for navigation, HATEOAS and for providing multiple
|
||||
versions of your API.
|
||||
|
||||
Optionally supplying ``api_name`` allows you to name the API. Generally,
|
||||
this is done with version numbers (i.e. ``v1``, ``v2``, etc.) but can
|
||||
be named any string.
|
||||
"""
|
||||
def __init__(self, api_name="v1", serializer_class=Serializer):
|
||||
self.api_name = api_name
|
||||
self._registry = {}
|
||||
self._canonicals = {}
|
||||
self.serializer = serializer_class()
|
||||
|
||||
def register(self, resource, canonical=True):
|
||||
"""
|
||||
Registers an instance of a ``Resource`` subclass with the API.
|
||||
|
||||
Optionally accept a ``canonical`` argument, which indicates that the
|
||||
resource being registered is the canonical variant. Defaults to
|
||||
``True``.
|
||||
"""
|
||||
resource_name = getattr(resource._meta, 'resource_name', None)
|
||||
|
||||
if resource_name is None:
|
||||
raise ImproperlyConfigured("Resource %r must define a 'resource_name'." % resource)
|
||||
|
||||
self._registry[resource_name] = resource
|
||||
|
||||
if canonical is True:
|
||||
if resource_name in self._canonicals:
|
||||
warnings.warn("A new resource '%r' is replacing the existing canonical URL for '%s'." % (resource, resource_name), Warning, stacklevel=2)
|
||||
|
||||
self._canonicals[resource_name] = resource
|
||||
# TODO: This is messy, but makes URI resolution on FK/M2M fields
|
||||
# work consistently.
|
||||
resource._meta.api_name = self.api_name
|
||||
resource.__class__.Meta.api_name = self.api_name
|
||||
|
||||
def unregister(self, resource_name):
|
||||
"""
|
||||
If present, unregisters a resource from the API.
|
||||
"""
|
||||
if resource_name in self._registry:
|
||||
del(self._registry[resource_name])
|
||||
|
||||
if resource_name in self._canonicals:
|
||||
del(self._canonicals[resource_name])
|
||||
|
||||
def canonical_resource_for(self, resource_name):
|
||||
"""
|
||||
Returns the canonical resource for a given ``resource_name``.
|
||||
"""
|
||||
if resource_name in self._canonicals:
|
||||
return self._canonicals[resource_name]
|
||||
|
||||
raise NotRegistered("No resource was registered as canonical for '%s'." % resource_name)
|
||||
|
||||
def wrap_view(self, view):
|
||||
def wrapper(request, *args, **kwargs):
|
||||
try:
|
||||
return getattr(self, view)(request, *args, **kwargs)
|
||||
except BadRequest:
|
||||
return HttpResponseBadRequest()
|
||||
return wrapper
|
||||
|
||||
def override_urls(self):
|
||||
"""
|
||||
Deprecated. Will be removed by v1.0.0. Please use ``prepend_urls`` instead.
|
||||
"""
|
||||
return []
|
||||
|
||||
def prepend_urls(self):
|
||||
"""
|
||||
A hook for adding your own URLs or matching before the default URLs.
|
||||
"""
|
||||
return []
|
||||
|
||||
@property
|
||||
def urls(self):
|
||||
"""
|
||||
Provides URLconf details for the ``Api`` and all registered
|
||||
``Resources`` beneath it.
|
||||
"""
|
||||
pattern_list = [
|
||||
url(r"^(?P<api_name>%s)%s$" % (self.api_name, trailing_slash()), self.wrap_view('top_level'), name="api_%s_top_level" % self.api_name),
|
||||
]
|
||||
|
||||
for name in sorted(self._registry.keys()):
|
||||
self._registry[name].api_name = self.api_name
|
||||
pattern_list.append((r"^(?P<api_name>%s)/" % self.api_name, include(self._registry[name].urls)))
|
||||
|
||||
urlpatterns = self.prepend_urls()
|
||||
|
||||
overridden_urls = self.override_urls()
|
||||
if overridden_urls:
|
||||
warnings.warn("'override_urls' is a deprecated method & will be removed by v1.0.0. Please rename your method to ``prepend_urls``.")
|
||||
urlpatterns += overridden_urls
|
||||
|
||||
urlpatterns += patterns('',
|
||||
*pattern_list
|
||||
)
|
||||
return urlpatterns
|
||||
|
||||
def top_level(self, request, api_name=None):
|
||||
"""
|
||||
A view that returns a serialized list of all resources registers
|
||||
to the ``Api``. Useful for discovery.
|
||||
"""
|
||||
available_resources = {}
|
||||
|
||||
if api_name is None:
|
||||
api_name = self.api_name
|
||||
|
||||
for name in sorted(self._registry.keys()):
|
||||
available_resources[name] = {
|
||||
'list_endpoint': self._build_reverse_url("api_dispatch_list", kwargs={
|
||||
'api_name': api_name,
|
||||
'resource_name': name,
|
||||
}),
|
||||
'schema': self._build_reverse_url("api_get_schema", kwargs={
|
||||
'api_name': api_name,
|
||||
'resource_name': name,
|
||||
}),
|
||||
}
|
||||
|
||||
desired_format = determine_format(request, self.serializer)
|
||||
|
||||
options = {}
|
||||
|
||||
if 'text/javascript' in desired_format:
|
||||
callback = request.GET.get('callback', 'callback')
|
||||
|
||||
if not is_valid_jsonp_callback_value(callback):
|
||||
raise BadRequest('JSONP callback name is invalid.')
|
||||
|
||||
options['callback'] = callback
|
||||
|
||||
serialized = self.serializer.serialize(available_resources, desired_format, options)
|
||||
return HttpResponse(content=serialized, content_type=build_content_type(desired_format))
|
||||
|
||||
def _build_reverse_url(self, name, args=None, kwargs=None):
|
||||
"""
|
||||
A convenience hook for overriding how URLs are built.
|
||||
|
||||
See ``NamespacedApi._build_reverse_url`` for an example.
|
||||
"""
|
||||
return reverse(name, args=args, kwargs=kwargs)
|
||||
|
||||
|
||||
class NamespacedApi(Api):
|
||||
"""
|
||||
An API subclass that respects Django namespaces.
|
||||
"""
|
||||
def __init__(self, api_name="v1", urlconf_namespace=None, **kwargs):
|
||||
super(NamespacedApi, self).__init__(api_name=api_name, **kwargs)
|
||||
self.urlconf_namespace = urlconf_namespace
|
||||
|
||||
def register(self, resource, canonical=True):
|
||||
super(NamespacedApi, self).register(resource, canonical=canonical)
|
||||
|
||||
if canonical is True:
|
||||
# Plop in the namespace here as well.
|
||||
resource._meta.urlconf_namespace = self.urlconf_namespace
|
||||
|
||||
def _build_reverse_url(self, name, args=None, kwargs=None):
|
||||
namespaced = "%s:%s" % (self.urlconf_namespace, name)
|
||||
return reverse(namespaced, args=args, kwargs=kwargs)
|
|
@ -1,523 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
import base64
|
||||
import hmac
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.middleware.csrf import _sanitize_token, constant_time_compare
|
||||
from django.utils.http import same_origin
|
||||
from django.utils.translation import ugettext as _
|
||||
from tastypie.http import HttpUnauthorized
|
||||
from tastypie.compat import get_user_model, get_username_field
|
||||
|
||||
try:
|
||||
from hashlib import sha1
|
||||
except ImportError:
|
||||
import sha
|
||||
sha1 = sha.sha
|
||||
|
||||
try:
|
||||
import python_digest
|
||||
except ImportError:
|
||||
python_digest = None
|
||||
|
||||
try:
|
||||
import oauth2
|
||||
except ImportError:
|
||||
oauth2 = None
|
||||
|
||||
try:
|
||||
import oauth_provider
|
||||
except ImportError:
|
||||
oauth_provider = None
|
||||
|
||||
|
||||
class Authentication(object):
|
||||
"""
|
||||
A simple base class to establish the protocol for auth.
|
||||
|
||||
By default, this indicates the user is always authenticated.
|
||||
"""
|
||||
def __init__(self, require_active=True):
|
||||
self.require_active = require_active
|
||||
|
||||
def is_authenticated(self, request, **kwargs):
|
||||
"""
|
||||
Identifies if the user is authenticated to continue or not.
|
||||
|
||||
Should return either ``True`` if allowed, ``False`` if not or an
|
||||
``HttpResponse`` if you need something custom.
|
||||
"""
|
||||
return True
|
||||
|
||||
def get_identifier(self, request):
|
||||
"""
|
||||
Provides a unique string identifier for the requestor.
|
||||
|
||||
This implementation returns a combination of IP address and hostname.
|
||||
"""
|
||||
return "%s_%s" % (request.META.get('REMOTE_ADDR', 'noaddr'), request.META.get('REMOTE_HOST', 'nohost'))
|
||||
|
||||
def check_active(self, user):
|
||||
"""
|
||||
Ensures the user has an active account.
|
||||
|
||||
Optimized for the ``django.contrib.auth.models.User`` case.
|
||||
"""
|
||||
if not self.require_active:
|
||||
# Ignore & move on.
|
||||
return True
|
||||
|
||||
return user.is_active
|
||||
|
||||
|
||||
class BasicAuthentication(Authentication):
|
||||
"""
|
||||
Handles HTTP Basic auth against a specific auth backend if provided,
|
||||
or against all configured authentication backends using the
|
||||
``authenticate`` method from ``django.contrib.auth``.
|
||||
|
||||
Optional keyword arguments:
|
||||
|
||||
``backend``
|
||||
If specified, use a specific ``django.contrib.auth`` backend instead
|
||||
of checking all backends specified in the ``AUTHENTICATION_BACKENDS``
|
||||
setting.
|
||||
``realm``
|
||||
The realm to use in the ``HttpUnauthorized`` response. Default:
|
||||
``django-tastypie``.
|
||||
"""
|
||||
def __init__(self, backend=None, realm='django-tastypie', **kwargs):
|
||||
super(BasicAuthentication, self).__init__(**kwargs)
|
||||
self.backend = backend
|
||||
self.realm = realm
|
||||
|
||||
def _unauthorized(self):
|
||||
response = HttpUnauthorized()
|
||||
# FIXME: Sanitize realm.
|
||||
response['WWW-Authenticate'] = 'Basic Realm="%s"' % self.realm
|
||||
return response
|
||||
|
||||
def is_authenticated(self, request, **kwargs):
|
||||
"""
|
||||
Checks a user's basic auth credentials against the current
|
||||
Django auth backend.
|
||||
|
||||
Should return either ``True`` if allowed, ``False`` if not or an
|
||||
``HttpResponse`` if you need something custom.
|
||||
"""
|
||||
if not request.META.get('HTTP_AUTHORIZATION'):
|
||||
return self._unauthorized()
|
||||
|
||||
try:
|
||||
(auth_type, data) = request.META['HTTP_AUTHORIZATION'].split()
|
||||
if auth_type.lower() != 'basic':
|
||||
return self._unauthorized()
|
||||
user_pass = base64.b64decode(data).decode('utf-8')
|
||||
except:
|
||||
return self._unauthorized()
|
||||
|
||||
bits = user_pass.split(':', 1)
|
||||
|
||||
if len(bits) != 2:
|
||||
return self._unauthorized()
|
||||
|
||||
if self.backend:
|
||||
user = self.backend.authenticate(username=bits[0], password=bits[1])
|
||||
else:
|
||||
user = authenticate(username=bits[0], password=bits[1])
|
||||
|
||||
if user is None:
|
||||
return self._unauthorized()
|
||||
|
||||
if not self.check_active(user):
|
||||
return False
|
||||
|
||||
request.user = user
|
||||
return True
|
||||
|
||||
def get_identifier(self, request):
|
||||
"""
|
||||
Provides a unique string identifier for the requestor.
|
||||
|
||||
This implementation returns the user's basic auth username.
|
||||
"""
|
||||
return request.META.get('REMOTE_USER', 'nouser')
|
||||
|
||||
|
||||
class ApiKeyAuthentication(Authentication):
|
||||
"""
|
||||
Handles API key auth, in which a user provides a username & API key.
|
||||
|
||||
Uses the ``ApiKey`` model that ships with tastypie. If you wish to use
|
||||
a different model, override the ``get_key`` method to perform the key check
|
||||
as suits your needs.
|
||||
"""
|
||||
def _unauthorized(self):
|
||||
return HttpUnauthorized()
|
||||
|
||||
def extract_credentials(self, request):
|
||||
if request.META.get('HTTP_AUTHORIZATION') and request.META['HTTP_AUTHORIZATION'].lower().startswith('apikey '):
|
||||
(auth_type, data) = request.META['HTTP_AUTHORIZATION'].split()
|
||||
|
||||
if auth_type.lower() != 'apikey':
|
||||
raise ValueError("Incorrect authorization header.")
|
||||
|
||||
username, api_key = data.split(':', 1)
|
||||
else:
|
||||
username = request.GET.get('username') or request.POST.get('username')
|
||||
api_key = request.GET.get('api_key') or request.POST.get('api_key')
|
||||
|
||||
return username, api_key
|
||||
|
||||
def is_authenticated(self, request, **kwargs):
|
||||
"""
|
||||
Finds the user and checks their API key.
|
||||
|
||||
Should return either ``True`` if allowed, ``False`` if not or an
|
||||
``HttpResponse`` if you need something custom.
|
||||
"""
|
||||
|
||||
try:
|
||||
username, api_key = self.extract_credentials(request)
|
||||
except ValueError:
|
||||
return self._unauthorized()
|
||||
|
||||
if not username or not api_key:
|
||||
return self._unauthorized()
|
||||
|
||||
username_field = get_username_field()
|
||||
User = get_user_model()
|
||||
|
||||
try:
|
||||
lookup_kwargs = {username_field: username}
|
||||
user = User.objects.get(**lookup_kwargs)
|
||||
except (User.DoesNotExist, User.MultipleObjectsReturned):
|
||||
return self._unauthorized()
|
||||
|
||||
if not self.check_active(user):
|
||||
return False
|
||||
|
||||
key_auth_check = self.get_key(user, api_key)
|
||||
if key_auth_check and not isinstance(key_auth_check, HttpUnauthorized):
|
||||
request.user = user
|
||||
|
||||
return key_auth_check
|
||||
|
||||
def get_key(self, user, api_key):
|
||||
"""
|
||||
Attempts to find the API key for the user. Uses ``ApiKey`` by default
|
||||
but can be overridden.
|
||||
"""
|
||||
from tastypie.models import ApiKey
|
||||
|
||||
try:
|
||||
ApiKey.objects.get(user=user, key=api_key)
|
||||
except ApiKey.DoesNotExist:
|
||||
return self._unauthorized()
|
||||
|
||||
return True
|
||||
|
||||
def get_identifier(self, request):
|
||||
"""
|
||||
Provides a unique string identifier for the requestor.
|
||||
|
||||
This implementation returns the user's username.
|
||||
"""
|
||||
username, api_key = self.extract_credentials(request)
|
||||
return username or 'nouser'
|
||||
|
||||
|
||||
class SessionAuthentication(Authentication):
|
||||
"""
|
||||
An authentication mechanism that piggy-backs on Django sessions.
|
||||
|
||||
This is useful when the API is talking to Javascript on the same site.
|
||||
Relies on the user being logged in through the standard Django login
|
||||
setup.
|
||||
|
||||
Requires a valid CSRF token.
|
||||
"""
|
||||
def is_authenticated(self, request, **kwargs):
|
||||
"""
|
||||
Checks to make sure the user is logged in & has a Django session.
|
||||
"""
|
||||
# Cargo-culted from Django 1.3/1.4's ``django/middleware/csrf.py``.
|
||||
# We can't just use what's there, since the return values will be
|
||||
# wrong.
|
||||
# We also can't risk accessing ``request.POST``, which will break with
|
||||
# the serialized bodies.
|
||||
if request.method in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
|
||||
return request.user.is_authenticated()
|
||||
|
||||
if getattr(request, '_dont_enforce_csrf_checks', False):
|
||||
return request.user.is_authenticated()
|
||||
|
||||
csrf_token = _sanitize_token(request.COOKIES.get(settings.CSRF_COOKIE_NAME, ''))
|
||||
|
||||
if request.is_secure():
|
||||
referer = request.META.get('HTTP_REFERER')
|
||||
|
||||
if referer is None:
|
||||
return False
|
||||
|
||||
good_referer = 'https://%s/' % request.get_host()
|
||||
|
||||
if not same_origin(referer, good_referer):
|
||||
return False
|
||||
|
||||
request_csrf_token = request.META.get('HTTP_X_CSRFTOKEN', '')
|
||||
|
||||
if not constant_time_compare(request_csrf_token, csrf_token):
|
||||
return False
|
||||
|
||||
return request.user.is_authenticated()
|
||||
|
||||
def get_identifier(self, request):
|
||||
"""
|
||||
Provides a unique string identifier for the requestor.
|
||||
|
||||
This implementation returns the user's username.
|
||||
"""
|
||||
|
||||
return getattr(request.user, get_username_field())
|
||||
|
||||
|
||||
class DigestAuthentication(Authentication):
|
||||
"""
|
||||
Handles HTTP Digest auth against a specific auth backend if provided,
|
||||
or against all configured authentication backends using the
|
||||
``authenticate`` method from ``django.contrib.auth``. However, instead of
|
||||
the user's password, their API key should be used.
|
||||
|
||||
Optional keyword arguments:
|
||||
|
||||
``backend``
|
||||
If specified, use a specific ``django.contrib.auth`` backend instead
|
||||
of checking all backends specified in the ``AUTHENTICATION_BACKENDS``
|
||||
setting.
|
||||
``realm``
|
||||
The realm to use in the ``HttpUnauthorized`` response. Default:
|
||||
``django-tastypie``.
|
||||
"""
|
||||
def __init__(self, backend=None, realm='django-tastypie', **kwargs):
|
||||
super(DigestAuthentication, self).__init__(**kwargs)
|
||||
self.backend = backend
|
||||
self.realm = realm
|
||||
|
||||
if python_digest is None:
|
||||
raise ImproperlyConfigured("The 'python_digest' package could not be imported. It is required for use with the 'DigestAuthentication' class.")
|
||||
|
||||
def _unauthorized(self):
|
||||
response = HttpUnauthorized()
|
||||
new_uuid = uuid.uuid4()
|
||||
opaque = hmac.new(str(new_uuid).encode('utf-8'), digestmod=sha1).hexdigest()
|
||||
response['WWW-Authenticate'] = python_digest.build_digest_challenge(
|
||||
timestamp=time.time(),
|
||||
secret=getattr(settings, 'SECRET_KEY', ''),
|
||||
realm=self.realm,
|
||||
opaque=opaque,
|
||||
stale=False
|
||||
)
|
||||
return response
|
||||
|
||||
def is_authenticated(self, request, **kwargs):
|
||||
"""
|
||||
Finds the user and checks their API key.
|
||||
|
||||
Should return either ``True`` if allowed, ``False`` if not or an
|
||||
``HttpResponse`` if you need something custom.
|
||||
"""
|
||||
if not request.META.get('HTTP_AUTHORIZATION'):
|
||||
return self._unauthorized()
|
||||
|
||||
try:
|
||||
(auth_type, data) = request.META['HTTP_AUTHORIZATION'].split(' ', 1)
|
||||
|
||||
if auth_type.lower() != 'digest':
|
||||
return self._unauthorized()
|
||||
except:
|
||||
return self._unauthorized()
|
||||
|
||||
digest_response = python_digest.parse_digest_credentials(request.META['HTTP_AUTHORIZATION'])
|
||||
|
||||
# FIXME: Should the nonce be per-user?
|
||||
if not python_digest.validate_nonce(digest_response.nonce, getattr(settings, 'SECRET_KEY', '')):
|
||||
return self._unauthorized()
|
||||
|
||||
user = self.get_user(digest_response.username)
|
||||
api_key = self.get_key(user)
|
||||
|
||||
if user is False or api_key is False:
|
||||
return self._unauthorized()
|
||||
|
||||
expected = python_digest.calculate_request_digest(
|
||||
request.method,
|
||||
python_digest.calculate_partial_digest(digest_response.username, self.realm, api_key),
|
||||
digest_response)
|
||||
|
||||
if not digest_response.response == expected:
|
||||
return self._unauthorized()
|
||||
|
||||
if not self.check_active(user):
|
||||
return False
|
||||
|
||||
request.user = user
|
||||
return True
|
||||
|
||||
def get_user(self, username):
|
||||
username_field = get_username_field()
|
||||
User = get_user_model()
|
||||
|
||||
try:
|
||||
lookup_kwargs = {username_field: username}
|
||||
user = User.objects.get(**lookup_kwargs)
|
||||
except (User.DoesNotExist, User.MultipleObjectsReturned):
|
||||
return False
|
||||
|
||||
return user
|
||||
|
||||
def get_key(self, user):
|
||||
"""
|
||||
Attempts to find the API key for the user. Uses ``ApiKey`` by default
|
||||
but can be overridden.
|
||||
|
||||
Note that this behaves differently than the ``ApiKeyAuthentication``
|
||||
method of the same name.
|
||||
"""
|
||||
from tastypie.models import ApiKey
|
||||
|
||||
try:
|
||||
key = ApiKey.objects.get(user=user)
|
||||
except ApiKey.DoesNotExist:
|
||||
return False
|
||||
|
||||
return key.key
|
||||
|
||||
def get_identifier(self, request):
|
||||
"""
|
||||
Provides a unique string identifier for the requestor.
|
||||
|
||||
This implementation returns the user's username.
|
||||
"""
|
||||
if hasattr(request, 'user'):
|
||||
if hasattr(request.user, 'username'):
|
||||
return request.user.username
|
||||
|
||||
return 'nouser'
|
||||
|
||||
|
||||
class OAuthAuthentication(Authentication):
|
||||
"""
|
||||
Handles OAuth, which checks a user's credentials against a separate service.
|
||||
Currently verifies against OAuth 1.0a services.
|
||||
|
||||
This does *NOT* provide OAuth authentication in your API, strictly
|
||||
consumption.
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
super(OAuthAuthentication, self).__init__(**kwargs)
|
||||
|
||||
if oauth2 is None:
|
||||
raise ImproperlyConfigured("The 'python-oauth2' package could not be imported. It is required for use with the 'OAuthAuthentication' class.")
|
||||
|
||||
if oauth_provider is None:
|
||||
raise ImproperlyConfigured("The 'django-oauth-plus' package could not be imported. It is required for use with the 'OAuthAuthentication' class.")
|
||||
|
||||
def is_authenticated(self, request, **kwargs):
|
||||
from oauth_provider.store import store, InvalidTokenError
|
||||
|
||||
if self.is_valid_request(request):
|
||||
oauth_request = oauth_provider.utils.get_oauth_request(request)
|
||||
consumer = store.get_consumer(request, oauth_request, oauth_request.get_parameter('oauth_consumer_key'))
|
||||
|
||||
try:
|
||||
token = store.get_access_token(request, oauth_request, consumer, oauth_request.get_parameter('oauth_token'))
|
||||
except oauth_provider.store.InvalidTokenError:
|
||||
return oauth_provider.utils.send_oauth_error(oauth2.Error(_('Invalid access token: %s') % oauth_request.get_parameter('oauth_token')))
|
||||
|
||||
try:
|
||||
self.validate_token(request, consumer, token)
|
||||
except oauth2.Error as e:
|
||||
return oauth_provider.utils.send_oauth_error(e)
|
||||
|
||||
if consumer and token:
|
||||
if not self.check_active(token.user):
|
||||
return False
|
||||
|
||||
request.user = token.user
|
||||
return True
|
||||
|
||||
return oauth_provider.utils.send_oauth_error(oauth2.Error(_('You are not allowed to access this resource.')))
|
||||
|
||||
return oauth_provider.utils.send_oauth_error(oauth2.Error(_('Invalid request parameters.')))
|
||||
|
||||
def is_in(self, params):
|
||||
"""
|
||||
Checks to ensure that all the OAuth parameter names are in the
|
||||
provided ``params``.
|
||||
"""
|
||||
from oauth_provider.consts import OAUTH_PARAMETERS_NAMES
|
||||
|
||||
for param_name in OAUTH_PARAMETERS_NAMES:
|
||||
if param_name not in params:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_valid_request(self, request):
|
||||
"""
|
||||
Checks whether the required parameters are either in the HTTP
|
||||
``Authorization`` header sent by some clients (the preferred method
|
||||
according to OAuth spec) or fall back to ``GET/POST``.
|
||||
"""
|
||||
auth_params = request.META.get("HTTP_AUTHORIZATION", [])
|
||||
return self.is_in(auth_params) or self.is_in(request.REQUEST)
|
||||
|
||||
def validate_token(self, request, consumer, token):
|
||||
oauth_server, oauth_request = oauth_provider.utils.initialize_server_request(request)
|
||||
return oauth_server.verify_request(oauth_request, consumer, token)
|
||||
|
||||
|
||||
class MultiAuthentication(object):
|
||||
"""
|
||||
An authentication backend that tries a number of backends in order.
|
||||
"""
|
||||
def __init__(self, *backends, **kwargs):
|
||||
super(MultiAuthentication, self).__init__(**kwargs)
|
||||
self.backends = backends
|
||||
|
||||
def is_authenticated(self, request, **kwargs):
|
||||
"""
|
||||
Identifies if the user is authenticated to continue or not.
|
||||
|
||||
Should return either ``True`` if allowed, ``False`` if not or an
|
||||
``HttpResponse`` if you need something custom.
|
||||
"""
|
||||
unauthorized = False
|
||||
|
||||
for backend in self.backends:
|
||||
check = backend.is_authenticated(request, **kwargs)
|
||||
|
||||
if check:
|
||||
if isinstance(check, HttpUnauthorized):
|
||||
unauthorized = unauthorized or check
|
||||
else:
|
||||
request._authentication_backend = backend
|
||||
return check
|
||||
|
||||
return unauthorized
|
||||
|
||||
def get_identifier(self, request):
|
||||
"""
|
||||
Provides a unique string identifier for the requestor.
|
||||
|
||||
This implementation returns a combination of IP address and hostname.
|
||||
"""
|
||||
try:
|
||||
return request._authentication_backend.get_identifier(request)
|
||||
except AttributeError:
|
||||
return 'nouser'
|
|
@ -1,245 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from tastypie.exceptions import TastypieError, Unauthorized
|
||||
|
||||
|
||||
class Authorization(object):
|
||||
"""
|
||||
A base class that provides no permissions checking.
|
||||
"""
|
||||
def __get__(self, instance, owner):
|
||||
"""
|
||||
Makes ``Authorization`` a descriptor of ``ResourceOptions`` and creates
|
||||
a reference to the ``ResourceOptions`` object that may be used by
|
||||
methods of ``Authorization``.
|
||||
"""
|
||||
self.resource_meta = instance
|
||||
return self
|
||||
|
||||
def apply_limits(self, request, object_list):
|
||||
"""
|
||||
Deprecated.
|
||||
|
||||
FIXME: REMOVE BEFORE 1.0
|
||||
"""
|
||||
raise TastypieError("Authorization classes no longer support `apply_limits`. Please update to using `read_list`.")
|
||||
|
||||
def read_list(self, object_list, bundle):
|
||||
"""
|
||||
Returns a list of all the objects a user is allowed to read.
|
||||
|
||||
Should return an empty list if none are allowed.
|
||||
|
||||
Returns the entire list by default.
|
||||
"""
|
||||
return object_list
|
||||
|
||||
def read_detail(self, object_list, bundle):
|
||||
"""
|
||||
Returns either ``True`` if the user is allowed to read the object in
|
||||
question or throw ``Unauthorized`` if they are not.
|
||||
|
||||
Returns ``True`` by default.
|
||||
"""
|
||||
return True
|
||||
|
||||
def create_list(self, object_list, bundle):
|
||||
"""
|
||||
Unimplemented, as Tastypie never creates entire new lists, but
|
||||
present for consistency & possible extension.
|
||||
"""
|
||||
raise NotImplementedError("Tastypie has no way to determine if all objects should be allowed to be created.")
|
||||
|
||||
def create_detail(self, object_list, bundle):
|
||||
"""
|
||||
Returns either ``True`` if the user is allowed to create the object in
|
||||
question or throw ``Unauthorized`` if they are not.
|
||||
|
||||
Returns ``True`` by default.
|
||||
"""
|
||||
return True
|
||||
|
||||
def update_list(self, object_list, bundle):
|
||||
"""
|
||||
Returns a list of all the objects a user is allowed to update.
|
||||
|
||||
Should return an empty list if none are allowed.
|
||||
|
||||
Returns the entire list by default.
|
||||
"""
|
||||
return object_list
|
||||
|
||||
def update_detail(self, object_list, bundle):
|
||||
"""
|
||||
Returns either ``True`` if the user is allowed to update the object in
|
||||
question or throw ``Unauthorized`` if they are not.
|
||||
|
||||
Returns ``True`` by default.
|
||||
"""
|
||||
return True
|
||||
|
||||
def delete_list(self, object_list, bundle):
|
||||
"""
|
||||
Returns a list of all the objects a user is allowed to delete.
|
||||
|
||||
Should return an empty list if none are allowed.
|
||||
|
||||
Returns the entire list by default.
|
||||
"""
|
||||
return object_list
|
||||
|
||||
def delete_detail(self, object_list, bundle):
|
||||
"""
|
||||
Returns either ``True`` if the user is allowed to delete the object in
|
||||
question or throw ``Unauthorized`` if they are not.
|
||||
|
||||
Returns ``True`` by default.
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
class ReadOnlyAuthorization(Authorization):
|
||||
"""
|
||||
Default Authentication class for ``Resource`` objects.
|
||||
|
||||
Only allows ``GET`` requests.
|
||||
"""
|
||||
def read_list(self, object_list, bundle):
|
||||
return object_list
|
||||
|
||||
def read_detail(self, object_list, bundle):
|
||||
return True
|
||||
|
||||
def create_list(self, object_list, bundle):
|
||||
return []
|
||||
|
||||
def create_detail(self, object_list, bundle):
|
||||
raise Unauthorized("You are not allowed to access that resource.")
|
||||
|
||||
def update_list(self, object_list, bundle):
|
||||
return []
|
||||
|
||||
def update_detail(self, object_list, bundle):
|
||||
raise Unauthorized("You are not allowed to access that resource.")
|
||||
|
||||
def delete_list(self, object_list, bundle):
|
||||
return []
|
||||
|
||||
def delete_detail(self, object_list, bundle):
|
||||
raise Unauthorized("You are not allowed to access that resource.")
|
||||
|
||||
|
||||
class DjangoAuthorization(Authorization):
|
||||
"""
|
||||
Uses permission checking from ``django.contrib.auth`` to map
|
||||
``POST / PUT / DELETE / PATCH`` to their equivalent Django auth
|
||||
permissions.
|
||||
|
||||
Both the list & detail variants simply check the model they're based
|
||||
on, as that's all the more granular Django's permission setup gets.
|
||||
"""
|
||||
def base_checks(self, request, model_klass):
|
||||
# If it doesn't look like a model, we can't check permissions.
|
||||
if not model_klass or not getattr(model_klass, '_meta', None):
|
||||
return False
|
||||
|
||||
# User must be logged in to check permissions.
|
||||
if not hasattr(request, 'user'):
|
||||
return False
|
||||
|
||||
return model_klass
|
||||
|
||||
def read_list(self, object_list, bundle):
|
||||
klass = self.base_checks(bundle.request, object_list.model)
|
||||
|
||||
if klass is False:
|
||||
return []
|
||||
|
||||
# GET-style methods are always allowed.
|
||||
return object_list
|
||||
|
||||
def read_detail(self, object_list, bundle):
|
||||
klass = self.base_checks(bundle.request, bundle.obj.__class__)
|
||||
|
||||
if klass is False:
|
||||
raise Unauthorized("You are not allowed to access that resource.")
|
||||
|
||||
# GET-style methods are always allowed.
|
||||
return True
|
||||
|
||||
def create_list(self, object_list, bundle):
|
||||
klass = self.base_checks(bundle.request, object_list.model)
|
||||
|
||||
if klass is False:
|
||||
return []
|
||||
|
||||
permission = '%s.add_%s' % (klass._meta.app_label, klass._meta.module_name)
|
||||
|
||||
if not bundle.request.user.has_perm(permission):
|
||||
return []
|
||||
|
||||
return object_list
|
||||
|
||||
def create_detail(self, object_list, bundle):
|
||||
klass = self.base_checks(bundle.request, bundle.obj.__class__)
|
||||
|
||||
if klass is False:
|
||||
raise Unauthorized("You are not allowed to access that resource.")
|
||||
|
||||
permission = '%s.add_%s' % (klass._meta.app_label, klass._meta.module_name)
|
||||
|
||||
if not bundle.request.user.has_perm(permission):
|
||||
raise Unauthorized("You are not allowed to access that resource.")
|
||||
|
||||
return True
|
||||
|
||||
def update_list(self, object_list, bundle):
|
||||
klass = self.base_checks(bundle.request, object_list.model)
|
||||
|
||||
if klass is False:
|
||||
return []
|
||||
|
||||
permission = '%s.change_%s' % (klass._meta.app_label, klass._meta.module_name)
|
||||
|
||||
if not bundle.request.user.has_perm(permission):
|
||||
return []
|
||||
|
||||
return object_list
|
||||
|
||||
def update_detail(self, object_list, bundle):
|
||||
klass = self.base_checks(bundle.request, bundle.obj.__class__)
|
||||
|
||||
if klass is False:
|
||||
raise Unauthorized("You are not allowed to access that resource.")
|
||||
|
||||
permission = '%s.change_%s' % (klass._meta.app_label, klass._meta.module_name)
|
||||
|
||||
if not bundle.request.user.has_perm(permission):
|
||||
raise Unauthorized("You are not allowed to access that resource.")
|
||||
|
||||
return True
|
||||
|
||||
def delete_list(self, object_list, bundle):
|
||||
klass = self.base_checks(bundle.request, object_list.model)
|
||||
|
||||
if klass is False:
|
||||
return []
|
||||
|
||||
permission = '%s.delete_%s' % (klass._meta.app_label, klass._meta.module_name)
|
||||
|
||||
if not bundle.request.user.has_perm(permission):
|
||||
return []
|
||||
|
||||
return object_list
|
||||
|
||||
def delete_detail(self, object_list, bundle):
|
||||
klass = self.base_checks(bundle.request, bundle.obj.__class__)
|
||||
|
||||
if klass is False:
|
||||
raise Unauthorized("You are not allowed to access that resource.")
|
||||
|
||||
permission = '%s.delete_%s' % (klass._meta.app_label, klass._meta.module_name)
|
||||
|
||||
if not bundle.request.user.has_perm(permission):
|
||||
raise Unauthorized("You are not allowed to access that resource.")
|
||||
|
||||
return True
|
|
@ -1,33 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.http import HttpRequest
|
||||
|
||||
|
||||
# In a separate file to avoid circular imports...
|
||||
class Bundle(object):
|
||||
"""
|
||||
A small container for instances and converted data for the
|
||||
``dehydrate/hydrate`` cycle.
|
||||
|
||||
Necessary because the ``dehydrate/hydrate`` cycle needs to access data at
|
||||
different points.
|
||||
"""
|
||||
def __init__(self,
|
||||
obj=None,
|
||||
data=None,
|
||||
request=None,
|
||||
related_obj=None,
|
||||
related_name=None,
|
||||
objects_saved=None,
|
||||
related_objects_to_save=None,
|
||||
):
|
||||
self.obj = obj
|
||||
self.data = data or {}
|
||||
self.request = request or HttpRequest()
|
||||
self.related_obj = related_obj
|
||||
self.related_name = related_name
|
||||
self.errors = {}
|
||||
self.objects_saved = objects_saved or set()
|
||||
self.related_objects_to_save = related_objects_to_save or {}
|
||||
|
||||
def __repr__(self):
|
||||
return "<Bundle for obj: '%s' and with data: '%s'>" % (self.obj, self.data)
|
|
@ -1,98 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.core.cache import get_cache
|
||||
|
||||
|
||||
class NoCache(object):
|
||||
"""
|
||||
A simplified, swappable base class for caching.
|
||||
|
||||
Does nothing save for simulating the cache API.
|
||||
"""
|
||||
def __init__(self, varies=None, *args, **kwargs):
|
||||
"""
|
||||
Optionally accepts a ``varies`` list that will be used in the
|
||||
Vary header. Defaults to ["Accept"].
|
||||
"""
|
||||
super(NoCache, self).__init__(*args, **kwargs)
|
||||
self.varies = varies
|
||||
|
||||
if self.varies is None:
|
||||
self.varies = ["Accept"]
|
||||
|
||||
def get(self, key):
|
||||
"""
|
||||
Always returns ``None``.
|
||||
"""
|
||||
return None
|
||||
|
||||
def set(self, key, value, timeout=60):
|
||||
"""
|
||||
No-op for setting values in the cache.
|
||||
"""
|
||||
pass
|
||||
|
||||
def cacheable(self, request, response):
|
||||
"""
|
||||
Returns True or False if the request -> response is capable of being
|
||||
cached.
|
||||
"""
|
||||
return bool(request.method == "GET" and response.status_code == 200)
|
||||
|
||||
def cache_control(self):
|
||||
"""
|
||||
No-op for returning values for cache-control
|
||||
"""
|
||||
return {
|
||||
'no_cache': True,
|
||||
}
|
||||
|
||||
|
||||
class SimpleCache(NoCache):
|
||||
"""
|
||||
Uses Django's current ``CACHES`` configuration to store cached data.
|
||||
"""
|
||||
|
||||
def __init__(self, cache_name='default', timeout=None, public=None,
|
||||
private=None, *args, **kwargs):
|
||||
"""
|
||||
Optionally accepts a ``timeout`` in seconds for the resource's cache.
|
||||
Defaults to ``60`` seconds.
|
||||
"""
|
||||
super(SimpleCache, self).__init__(*args, **kwargs)
|
||||
self.cache = get_cache(cache_name)
|
||||
self.timeout = timeout or self.cache.default_timeout
|
||||
self.public = public
|
||||
self.private = private
|
||||
|
||||
def get(self, key, **kwargs):
|
||||
"""
|
||||
Gets a key from the cache. Returns ``None`` if the key is not found.
|
||||
"""
|
||||
return self.cache.get(key, **kwargs)
|
||||
|
||||
def set(self, key, value, timeout=None):
|
||||
"""
|
||||
Sets a key-value in the cache.
|
||||
|
||||
Optionally accepts a ``timeout`` in seconds. Defaults to ``None`` which
|
||||
uses the resource's default timeout.
|
||||
"""
|
||||
|
||||
if timeout is None:
|
||||
timeout = self.timeout
|
||||
|
||||
self.cache.set(key, value, timeout)
|
||||
|
||||
def cache_control(self):
|
||||
control = {
|
||||
'max_age': self.timeout,
|
||||
's_maxage': self.timeout,
|
||||
}
|
||||
|
||||
if self.public is not None:
|
||||
control["public"] = self.public
|
||||
|
||||
if self.private is not None:
|
||||
control["private"] = self.private
|
||||
|
||||
return control
|
|
@ -1,25 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.conf import settings
|
||||
import django
|
||||
|
||||
__all__ = ['get_user_model', 'get_username_field', 'AUTH_USER_MODEL']
|
||||
|
||||
AUTH_USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', 'auth.User')
|
||||
|
||||
# Django 1.5+ compatibility
|
||||
if django.VERSION >= (1, 5):
|
||||
def get_user_model():
|
||||
from django.contrib.auth import get_user_model as django_get_user_model
|
||||
|
||||
return django_get_user_model()
|
||||
|
||||
def get_username_field():
|
||||
return get_user_model().USERNAME_FIELD
|
||||
else:
|
||||
def get_user_model():
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
return User
|
||||
|
||||
def get_username_field():
|
||||
return 'username'
|
|
@ -1,6 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
# Enable all basic ORM filters but do not allow filtering across relationships.
|
||||
ALL = 1
|
||||
# Enable all ORM filters, including across relationships
|
||||
ALL_WITH_RELATIONS = 2
|
|
@ -1,55 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from functools import partial
|
||||
from tastypie import fields
|
||||
from tastypie.resources import Resource
|
||||
from tastypie.exceptions import ApiFieldError
|
||||
from django.db import models
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from .resources import GenericResource
|
||||
|
||||
|
||||
class GenericForeignKeyField(fields.ToOneField):
|
||||
"""
|
||||
Provides access to GenericForeignKey objects from the django content_types
|
||||
framework.
|
||||
"""
|
||||
|
||||
def __init__(self, to, attribute, **kwargs):
|
||||
if not isinstance(to, dict):
|
||||
raise ValueError('to field must be a dictionary in GenericForeignKeyField')
|
||||
|
||||
if len(to) <= 0:
|
||||
raise ValueError('to field must have some values')
|
||||
|
||||
for k, v in to.items():
|
||||
if not issubclass(k, models.Model) or not issubclass(v, Resource):
|
||||
raise ValueError('to field must map django models to tastypie resources')
|
||||
|
||||
super(GenericForeignKeyField, self).__init__(to, attribute, **kwargs)
|
||||
|
||||
def get_related_resource(self, related_instance):
|
||||
self._to_class = self.to.get(type(related_instance), None)
|
||||
|
||||
if self._to_class is None:
|
||||
raise TypeError('no resource for model %s' % type(related_instance))
|
||||
|
||||
return super(GenericForeignKeyField, self).get_related_resource(related_instance)
|
||||
|
||||
@property
|
||||
def to_class(self):
|
||||
if self._to_class and not issubclass(GenericResource, self._to_class):
|
||||
return self._to_class
|
||||
|
||||
return partial(GenericResource, resources=self.to.values())
|
||||
|
||||
def resource_from_uri(self, fk_resource, uri, request=None, related_obj=None, related_name=None):
|
||||
try:
|
||||
obj = fk_resource.get_via_uri(uri, request=request)
|
||||
fk_resource = self.get_related_resource(obj)
|
||||
return super(GenericForeignKeyField, self).resource_from_uri(fk_resource, uri, request, related_obj, related_name)
|
||||
except ObjectDoesNotExist:
|
||||
raise ApiFieldError("Could not find the provided object via resource URI '%s'." % uri)
|
||||
|
||||
def build_related_resource(self, *args, **kwargs):
|
||||
self._to_class = None
|
||||
return super(GenericForeignKeyField, self).build_related_resource(*args, **kwargs)
|
|
@ -1,42 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from tastypie.bundle import Bundle
|
||||
from tastypie.resources import ModelResource
|
||||
from tastypie.exceptions import NotFound
|
||||
from django.core.urlresolvers import resolve, Resolver404, get_script_prefix
|
||||
|
||||
|
||||
class GenericResource(ModelResource):
|
||||
"""
|
||||
Provides a stand-in resource for GFK relations.
|
||||
"""
|
||||
def __init__(self, resources, *args, **kwargs):
|
||||
self.resource_mapping = dict((r._meta.resource_name, r) for r in resources)
|
||||
return super(GenericResource, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_via_uri(self, uri, request=None):
|
||||
"""
|
||||
This pulls apart the salient bits of the URI and populates the
|
||||
resource via a ``obj_get``.
|
||||
|
||||
Optionally accepts a ``request``.
|
||||
|
||||
If you need custom behavior based on other portions of the URI,
|
||||
simply override this method.
|
||||
"""
|
||||
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)
|
||||
resource_name = kwargs['resource_name']
|
||||
resource_class = self.resource_mapping[resource_name]
|
||||
except (Resolver404, KeyError):
|
||||
raise NotFound("The URL provided '%s' was not a link to a valid resource." % uri)
|
||||
|
||||
parent_resource = resource_class(api_name=self._meta.api_name)
|
||||
kwargs = parent_resource.remove_api_resource_names(kwargs)
|
||||
bundle = Bundle(request=request)
|
||||
return parent_resource.obj_get(bundle, **kwargs)
|
|
@ -1,77 +0,0 @@
|
|||
# See COPYING file in this directory.
|
||||
# Some code originally from django-boundaryservice
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
|
||||
try:
|
||||
from urllib.parse import unquote
|
||||
except ImportError:
|
||||
from urllib import unquote
|
||||
|
||||
from django.contrib.gis.db.models import GeometryField
|
||||
from django.contrib.gis.geos import GEOSGeometry
|
||||
|
||||
from tastypie.fields import ApiField, CharField
|
||||
from tastypie import resources
|
||||
|
||||
|
||||
class GeometryApiField(ApiField):
|
||||
"""
|
||||
Custom ApiField for dealing with data from GeometryFields (by serializing
|
||||
them as GeoJSON).
|
||||
"""
|
||||
dehydrated_type = 'geometry'
|
||||
help_text = 'Geometry data.'
|
||||
|
||||
def hydrate(self, bundle):
|
||||
value = super(GeometryApiField, self).hydrate(bundle)
|
||||
if value is None:
|
||||
return value
|
||||
return json.dumps(value)
|
||||
|
||||
def dehydrate(self, obj, for_list=False):
|
||||
return self.convert(super(GeometryApiField, self).dehydrate(obj))
|
||||
|
||||
def convert(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
|
||||
# Get ready-made geojson serialization and then convert it _back_ to
|
||||
# a Python object so that tastypie can serialize it as part of the
|
||||
# bundle.
|
||||
return json.loads(value.geojson)
|
||||
|
||||
|
||||
class ModelResource(resources.ModelResource):
|
||||
"""
|
||||
ModelResource subclass that handles geometry fields as GeoJSON.
|
||||
"""
|
||||
@classmethod
|
||||
def api_field_from_django_field(cls, f, default=CharField):
|
||||
"""
|
||||
Overrides default field handling to support custom GeometryApiField.
|
||||
"""
|
||||
if isinstance(f, GeometryField):
|
||||
return GeometryApiField
|
||||
|
||||
return super(ModelResource, cls).api_field_from_django_field(f, default)
|
||||
|
||||
def filter_value_to_python(self, value, field_name, filters, filter_expr,
|
||||
filter_type):
|
||||
value = super(ModelResource, self).filter_value_to_python(
|
||||
value, field_name, filters, filter_expr, filter_type)
|
||||
|
||||
# If we are filtering on a GeometryApiField then we should try
|
||||
# and convert this to a GEOSGeometry object. The conversion
|
||||
# will fail if we don't have value JSON, so in that case we'll
|
||||
# just return ``value`` as normal.
|
||||
if isinstance(self.fields[field_name], GeometryApiField):
|
||||
try:
|
||||
value = GEOSGeometry(unquote(value))
|
||||
except ValueError:
|
||||
pass
|
||||
return value
|
|
@ -1,101 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.http import HttpResponse
|
||||
|
||||
|
||||
class TastypieError(Exception):
|
||||
"""A base exception for other tastypie-related errors."""
|
||||
pass
|
||||
|
||||
|
||||
class HydrationError(TastypieError):
|
||||
"""Raised when there is an error hydrating data."""
|
||||
pass
|
||||
|
||||
|
||||
class NotRegistered(TastypieError):
|
||||
"""
|
||||
Raised when the requested resource isn't registered with the ``Api`` class.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class NotFound(TastypieError):
|
||||
"""
|
||||
Raised when the resource/object in question can't be found.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Unauthorized(TastypieError):
|
||||
"""
|
||||
Raised when the request object is not accessible to the user.
|
||||
|
||||
This is different than the ``tastypie.http.HttpUnauthorized`` & is handled
|
||||
differently internally.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ApiFieldError(TastypieError):
|
||||
"""
|
||||
Raised when there is a configuration error with a ``ApiField``.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedFormat(TastypieError):
|
||||
"""
|
||||
Raised when an unsupported serialization format is requested.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class BadRequest(TastypieError):
|
||||
"""
|
||||
A generalized exception for indicating incorrect request parameters.
|
||||
|
||||
Handled specially in that the message tossed by this exception will be
|
||||
presented to the end user.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class BlueberryFillingFound(TastypieError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidFilterError(BadRequest):
|
||||
"""
|
||||
Raised when the end user attempts to use a filter that has not be
|
||||
explicitly allowed.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidSortError(BadRequest):
|
||||
"""
|
||||
Raised when the end user attempts to sort on a field that has not be
|
||||
explicitly allowed.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ImmediateHttpResponse(TastypieError):
|
||||
"""
|
||||
This exception is used to interrupt the flow of processing to immediately
|
||||
return a custom HttpResponse.
|
||||
|
||||
Common uses include::
|
||||
|
||||
* for authentication (like digest/OAuth)
|
||||
* for throttling
|
||||
|
||||
"""
|
||||
_response = HttpResponse("Nothing provided.")
|
||||
|
||||
def __init__(self, response):
|
||||
self._response = response
|
||||
|
||||
@property
|
||||
def response(self):
|
||||
return self._response
|
|
@ -1,907 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
import datetime
|
||||
from dateutil.parser import parse
|
||||
from decimal import Decimal
|
||||
import re
|
||||
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
|
||||
from django.utils import datetime_safe, importlib
|
||||
from django.utils import six
|
||||
from tastypie.bundle import Bundle
|
||||
from tastypie.exceptions import ApiFieldError, NotFound
|
||||
from tastypie.utils import dict_strip_unicode_keys, make_aware
|
||||
|
||||
|
||||
class NOT_PROVIDED:
|
||||
def __str__(self):
|
||||
return 'No default provided.'
|
||||
|
||||
|
||||
DATE_REGEX = re.compile('^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2}).*?$')
|
||||
DATETIME_REGEX = re.compile('^(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})(T|\s+)(?P<hour>\d{2}):(?P<minute>\d{2}):(?P<second>\d{2}).*?$')
|
||||
|
||||
|
||||
# All the ApiField variants.
|
||||
|
||||
class ApiField(object):
|
||||
"""The base implementation of a field used by the resources."""
|
||||
dehydrated_type = 'string'
|
||||
help_text = ''
|
||||
|
||||
def __init__(self, attribute=None, default=NOT_PROVIDED, null=False, blank=False, readonly=False, unique=False, help_text=None, use_in='all'):
|
||||
"""
|
||||
Sets up the field. This is generally called when the containing
|
||||
``Resource`` is initialized.
|
||||
|
||||
Optionally accepts an ``attribute``, which should be a string of
|
||||
either an instance attribute or callable off the object during the
|
||||
``dehydrate`` or push data onto an object during the ``hydrate``.
|
||||
Defaults to ``None``, meaning data will be manually accessed.
|
||||
|
||||
Optionally accepts a ``default``, which provides default data when the
|
||||
object being ``dehydrated``/``hydrated`` has no data on the field.
|
||||
Defaults to ``NOT_PROVIDED``.
|
||||
|
||||
Optionally accepts a ``null``, which indicated whether or not a
|
||||
``None`` is allowable data on the field. Defaults to ``False``.
|
||||
|
||||
Optionally accepts a ``blank``, which indicated whether or not
|
||||
data may be omitted on the field. Defaults to ``False``.
|
||||
|
||||
Optionally accepts a ``readonly``, which indicates whether the field
|
||||
is used during the ``hydrate`` or not. Defaults to ``False``.
|
||||
|
||||
Optionally accepts a ``unique``, which indicates if the field is a
|
||||
unique identifier for the object.
|
||||
|
||||
Optionally accepts ``help_text``, which lets you provide a
|
||||
human-readable description of the field exposed at the schema level.
|
||||
Defaults to the per-Field definition.
|
||||
|
||||
Optionally accepts ``use_in``. This may be one of ``list``, ``detail``
|
||||
``all`` or a callable which accepts a ``bundle`` and returns
|
||||
``True`` or ``False``. Indicates wheather this field will be included
|
||||
during dehydration of a list of objects or a single object. If ``use_in``
|
||||
is a callable, and returns ``True``, the field will be included during
|
||||
dehydration.
|
||||
Defaults to ``all``.
|
||||
"""
|
||||
# Track what the index thinks this field is called.
|
||||
self.instance_name = None
|
||||
self._resource = None
|
||||
self.attribute = attribute
|
||||
self._default = default
|
||||
self.null = null
|
||||
self.blank = blank
|
||||
self.readonly = readonly
|
||||
self.value = None
|
||||
self.unique = unique
|
||||
self.use_in = 'all'
|
||||
|
||||
if use_in in ['all', 'detail', 'list'] or callable(use_in):
|
||||
self.use_in = use_in
|
||||
|
||||
if help_text:
|
||||
self.help_text = help_text
|
||||
|
||||
def contribute_to_class(self, cls, name):
|
||||
# Do the least we can here so that we don't hate ourselves in the
|
||||
# morning.
|
||||
self.instance_name = name
|
||||
self._resource = cls
|
||||
|
||||
def has_default(self):
|
||||
"""Returns a boolean of whether this field has a default value."""
|
||||
return self._default is not NOT_PROVIDED
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
"""Returns the default value for the field."""
|
||||
if callable(self._default):
|
||||
return self._default()
|
||||
|
||||
return self._default
|
||||
|
||||
def dehydrate(self, bundle, for_list=True):
|
||||
"""
|
||||
Takes data from the provided object and prepares it for the
|
||||
resource.
|
||||
"""
|
||||
if self.attribute is not None:
|
||||
# Check for `__` in the field for looking through the relation.
|
||||
attrs = self.attribute.split('__')
|
||||
current_object = bundle.obj
|
||||
|
||||
for attr in attrs:
|
||||
previous_object = current_object
|
||||
current_object = getattr(current_object, attr, None)
|
||||
|
||||
if current_object is None:
|
||||
if self.has_default():
|
||||
current_object = self._default
|
||||
# Fall out of the loop, given any further attempts at
|
||||
# accesses will fail miserably.
|
||||
break
|
||||
elif self.null:
|
||||
current_object = None
|
||||
# Fall out of the loop, given any further attempts at
|
||||
# accesses will fail miserably.
|
||||
break
|
||||
else:
|
||||
raise ApiFieldError("The object '%r' has an empty attribute '%s' and doesn't allow a default or null value." % (previous_object, attr))
|
||||
|
||||
if callable(current_object):
|
||||
current_object = current_object()
|
||||
|
||||
return self.convert(current_object)
|
||||
|
||||
if self.has_default():
|
||||
return self.convert(self.default)
|
||||
else:
|
||||
return None
|
||||
|
||||
def convert(self, value):
|
||||
"""
|
||||
Handles conversion between the data found and the type of the field.
|
||||
|
||||
Extending classes should override this method and provide correct
|
||||
data coercion.
|
||||
"""
|
||||
return value
|
||||
|
||||
def hydrate(self, bundle):
|
||||
"""
|
||||
Takes data stored in the bundle for the field and returns it. Used for
|
||||
taking simple data and building a instance object.
|
||||
"""
|
||||
if self.readonly:
|
||||
return None
|
||||
if not self.instance_name in bundle.data:
|
||||
if getattr(self, 'is_related', False) and not getattr(self, 'is_m2m', False):
|
||||
# We've got an FK (or alike field) & a possible parent object.
|
||||
# Check for it.
|
||||
if bundle.related_obj and bundle.related_name in (self.attribute, self.instance_name):
|
||||
return bundle.related_obj
|
||||
if self.blank:
|
||||
return None
|
||||
elif self.attribute and getattr(bundle.obj, self.attribute, None):
|
||||
return getattr(bundle.obj, self.attribute)
|
||||
elif self.instance_name and hasattr(bundle.obj, self.instance_name):
|
||||
return getattr(bundle.obj, self.instance_name)
|
||||
elif self.has_default():
|
||||
if callable(self._default):
|
||||
return self._default()
|
||||
|
||||
return self._default
|
||||
elif self.null:
|
||||
return None
|
||||
else:
|
||||
raise ApiFieldError("The '%s' field has no data and doesn't allow a default or null value." % self.instance_name)
|
||||
|
||||
return bundle.data[self.instance_name]
|
||||
|
||||
|
||||
class CharField(ApiField):
|
||||
"""
|
||||
A text field of arbitrary length.
|
||||
|
||||
Covers both ``models.CharField`` and ``models.TextField``.
|
||||
"""
|
||||
dehydrated_type = 'string'
|
||||
help_text = 'Unicode string data. Ex: "Hello World"'
|
||||
|
||||
def convert(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
return six.text_type(value)
|
||||
|
||||
|
||||
class FileField(ApiField):
|
||||
"""
|
||||
A file-related field.
|
||||
|
||||
Covers both ``models.FileField`` and ``models.ImageField``.
|
||||
"""
|
||||
dehydrated_type = 'string'
|
||||
help_text = 'A file URL as a string. Ex: "http://media.example.com/media/photos/my_photo.jpg"'
|
||||
|
||||
def convert(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Try to return the URL if it's a ``File``, falling back to the string
|
||||
# itself if it's been overridden or is a default.
|
||||
return getattr(value, 'url', value)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
class IntegerField(ApiField):
|
||||
"""
|
||||
An integer field.
|
||||
|
||||
Covers ``models.IntegerField``, ``models.PositiveIntegerField``,
|
||||
``models.PositiveSmallIntegerField`` and ``models.SmallIntegerField``.
|
||||
"""
|
||||
dehydrated_type = 'integer'
|
||||
help_text = 'Integer data. Ex: 2673'
|
||||
|
||||
def convert(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
return int(value)
|
||||
|
||||
|
||||
class FloatField(ApiField):
|
||||
"""
|
||||
A floating point field.
|
||||
"""
|
||||
dehydrated_type = 'float'
|
||||
help_text = 'Floating point numeric data. Ex: 26.73'
|
||||
|
||||
def convert(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
return float(value)
|
||||
|
||||
|
||||
class DecimalField(ApiField):
|
||||
"""
|
||||
A decimal field.
|
||||
"""
|
||||
dehydrated_type = 'decimal'
|
||||
help_text = 'Fixed precision numeric data. Ex: 26.73'
|
||||
|
||||
def convert(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
return Decimal(value)
|
||||
|
||||
def hydrate(self, bundle):
|
||||
value = super(DecimalField, self).hydrate(bundle)
|
||||
|
||||
if value and not isinstance(value, Decimal):
|
||||
value = Decimal(value)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class BooleanField(ApiField):
|
||||
"""
|
||||
A boolean field.
|
||||
|
||||
Covers both ``models.BooleanField`` and ``models.NullBooleanField``.
|
||||
"""
|
||||
dehydrated_type = 'boolean'
|
||||
help_text = 'Boolean data. Ex: True'
|
||||
|
||||
def convert(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
return bool(value)
|
||||
|
||||
|
||||
class ListField(ApiField):
|
||||
"""
|
||||
A list field.
|
||||
"""
|
||||
dehydrated_type = 'list'
|
||||
help_text = "A list of data. Ex: ['abc', 26.73, 8]"
|
||||
|
||||
def convert(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
return list(value)
|
||||
|
||||
|
||||
class DictField(ApiField):
|
||||
"""
|
||||
A dictionary field.
|
||||
"""
|
||||
dehydrated_type = 'dict'
|
||||
help_text = "A dictionary of data. Ex: {'price': 26.73, 'name': 'Daniel'}"
|
||||
|
||||
def convert(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
return dict(value)
|
||||
|
||||
|
||||
class DateField(ApiField):
|
||||
"""
|
||||
A date field.
|
||||
"""
|
||||
dehydrated_type = 'date'
|
||||
help_text = 'A date as a string. Ex: "2010-11-10"'
|
||||
|
||||
def convert(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
if isinstance(value, six.string_types):
|
||||
match = DATE_REGEX.search(value)
|
||||
|
||||
if match:
|
||||
data = match.groupdict()
|
||||
return datetime_safe.date(int(data['year']), int(data['month']), int(data['day']))
|
||||
else:
|
||||
raise ApiFieldError("Date provided to '%s' field doesn't appear to be a valid date string: '%s'" % (self.instance_name, value))
|
||||
|
||||
return value
|
||||
|
||||
def hydrate(self, bundle):
|
||||
value = super(DateField, self).hydrate(bundle)
|
||||
|
||||
if value and not hasattr(value, 'year'):
|
||||
try:
|
||||
# Try to rip a date/datetime out of it.
|
||||
value = make_aware(parse(value))
|
||||
|
||||
if hasattr(value, 'hour'):
|
||||
value = value.date()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class DateTimeField(ApiField):
|
||||
"""
|
||||
A datetime field.
|
||||
"""
|
||||
dehydrated_type = 'datetime'
|
||||
help_text = 'A date & time as a string. Ex: "2010-11-10T03:07:43"'
|
||||
|
||||
def convert(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
if isinstance(value, six.string_types):
|
||||
match = DATETIME_REGEX.search(value)
|
||||
|
||||
if match:
|
||||
data = match.groupdict()
|
||||
return make_aware(datetime_safe.datetime(int(data['year']), int(data['month']), int(data['day']), int(data['hour']), int(data['minute']), int(data['second'])))
|
||||
else:
|
||||
raise ApiFieldError("Datetime provided to '%s' field doesn't appear to be a valid datetime string: '%s'" % (self.instance_name, value))
|
||||
|
||||
return value
|
||||
|
||||
def hydrate(self, bundle):
|
||||
value = super(DateTimeField, self).hydrate(bundle)
|
||||
|
||||
if value and not hasattr(value, 'year'):
|
||||
if isinstance(value, six.string_types):
|
||||
try:
|
||||
# Try to rip a date/datetime out of it.
|
||||
value = make_aware(parse(value))
|
||||
except (ValueError, TypeError):
|
||||
raise ApiFieldError("Datetime provided to '%s' field doesn't appear to be a valid datetime string: '%s'" % (self.instance_name, value))
|
||||
|
||||
else:
|
||||
raise ApiFieldError("Datetime provided to '%s' field must be a string: %s" % (self.instance_name, value))
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class RelatedField(ApiField):
|
||||
"""
|
||||
Provides access to data that is related within the database.
|
||||
|
||||
The ``RelatedField`` base class is not intended for direct use but provides
|
||||
functionality that ``ToOneField`` and ``ToManyField`` build upon.
|
||||
|
||||
The contents of this field actually point to another ``Resource``,
|
||||
rather than the related object. This allows the field to represent its data
|
||||
in different ways.
|
||||
|
||||
The abstractions based around this are "leaky" in that, unlike the other
|
||||
fields provided by ``tastypie``, these fields don't handle arbitrary objects
|
||||
very well. The subclasses use Django's ORM layer to make things go, though
|
||||
there is no ORM-specific code at this level.
|
||||
"""
|
||||
dehydrated_type = 'related'
|
||||
is_related = True
|
||||
self_referential = False
|
||||
help_text = 'A related resource. Can be either a URI or set of nested resource data.'
|
||||
|
||||
def __init__(self, to, attribute, related_name=None, default=NOT_PROVIDED, null=False, blank=False, readonly=False, full=False, unique=False, help_text=None, use_in='all', full_list=True, full_detail=True):
|
||||
"""
|
||||
Builds the field and prepares it to access to related data.
|
||||
|
||||
The ``to`` argument should point to a ``Resource`` class, NOT
|
||||
to a ``Model``. Required.
|
||||
|
||||
The ``attribute`` argument should specify what field/callable points to
|
||||
the related data on the instance object. Required.
|
||||
|
||||
Optionally accepts a ``related_name`` argument. Currently unused, as
|
||||
unlike Django's ORM layer, reverse relations between ``Resource``
|
||||
classes are not automatically created. Defaults to ``None``.
|
||||
|
||||
Optionally accepts a ``null``, which indicated whether or not a
|
||||
``None`` is allowable data on the field. Defaults to ``False``.
|
||||
|
||||
Optionally accepts a ``blank``, which indicated whether or not
|
||||
data may be omitted on the field. Defaults to ``False``.
|
||||
|
||||
Optionally accepts a ``readonly``, which indicates whether the field
|
||||
is used during the ``hydrate`` or not. Defaults to ``False``.
|
||||
|
||||
Optionally accepts a ``full``, which indicates how the related
|
||||
``Resource`` will appear post-``dehydrate``. If ``False``, the
|
||||
related ``Resource`` will appear as a URL to the endpoint of that
|
||||
resource. If ``True``, the result of the sub-resource's
|
||||
``dehydrate`` will be included in full.
|
||||
|
||||
Optionally accepts a ``unique``, which indicates if the field is a
|
||||
unique identifier for the object.
|
||||
|
||||
Optionally accepts ``help_text``, which lets you provide a
|
||||
human-readable description of the field exposed at the schema level.
|
||||
Defaults to the per-Field definition.
|
||||
|
||||
Optionally accepts ``use_in``. This may be one of ``list``, ``detail``
|
||||
``all`` or a callable which accepts a ``bundle`` and returns
|
||||
``True`` or ``False``. Indicates wheather this field will be included
|
||||
during dehydration of a list of objects or a single object. If ``use_in``
|
||||
is a callable, and returns ``True``, the field will be included during
|
||||
dehydration.
|
||||
Defaults to ``all``.
|
||||
|
||||
Optionally accepts a ``full_list``, which indicated whether or not
|
||||
data should be fully dehydrated when the request is for a list of
|
||||
resources. Accepts ``True``, ``False`` or a callable that accepts
|
||||
a bundle and returns ``True`` or ``False``. Depends on ``full``
|
||||
being ``True``. Defaults to ``True``.
|
||||
|
||||
Optionally accepts a ``full_detail``, which indicated whether or not
|
||||
data should be fully dehydrated when then request is for a single
|
||||
resource. Accepts ``True``, ``False`` or a callable that accepts a
|
||||
bundle and returns ``True`` or ``False``.Depends on ``full``
|
||||
being ``True``. Defaults to ``True``.
|
||||
"""
|
||||
self.instance_name = None
|
||||
self._resource = None
|
||||
self.to = to
|
||||
self.attribute = attribute
|
||||
self.related_name = related_name
|
||||
self._default = default
|
||||
self.null = null
|
||||
self.blank = blank
|
||||
self.readonly = readonly
|
||||
self.full = full
|
||||
self.api_name = None
|
||||
self.resource_name = None
|
||||
self.unique = unique
|
||||
self._to_class = None
|
||||
self.use_in = 'all'
|
||||
self.full_list = full_list
|
||||
self.full_detail = full_detail
|
||||
|
||||
if use_in in ['all', 'detail', 'list'] or callable(use_in):
|
||||
self.use_in = use_in
|
||||
|
||||
if self.to == 'self':
|
||||
self.self_referential = True
|
||||
self._to_class = self.__class__
|
||||
|
||||
if help_text:
|
||||
self.help_text = help_text
|
||||
|
||||
def contribute_to_class(self, cls, name):
|
||||
super(RelatedField, self).contribute_to_class(cls, name)
|
||||
|
||||
# Check if we're self-referential and hook it up.
|
||||
# We can't do this quite like Django because there's no ``AppCache``
|
||||
# here (which I think we should avoid as long as possible).
|
||||
if self.self_referential or self.to == 'self':
|
||||
self._to_class = cls
|
||||
|
||||
def get_related_resource(self, related_instance):
|
||||
"""
|
||||
Instaniates the related resource.
|
||||
"""
|
||||
related_resource = self.to_class()
|
||||
|
||||
# Fix the ``api_name`` if it's not present.
|
||||
if related_resource._meta.api_name is None:
|
||||
if self._resource and not self._resource._meta.api_name is None:
|
||||
related_resource._meta.api_name = self._resource._meta.api_name
|
||||
|
||||
# Try to be efficient about DB queries.
|
||||
related_resource.instance = related_instance
|
||||
return related_resource
|
||||
|
||||
@property
|
||||
def to_class(self):
|
||||
# We need to be lazy here, because when the metaclass constructs the
|
||||
# Resources, other classes may not exist yet.
|
||||
# That said, memoize this so we never have to relookup/reimport.
|
||||
if self._to_class:
|
||||
return self._to_class
|
||||
|
||||
if not isinstance(self.to, six.string_types):
|
||||
self._to_class = self.to
|
||||
return self._to_class
|
||||
|
||||
# It's a string. Let's figure it out.
|
||||
if '.' in self.to:
|
||||
# Try to import.
|
||||
module_bits = self.to.split('.')
|
||||
module_path, class_name = '.'.join(module_bits[:-1]), module_bits[-1]
|
||||
module = importlib.import_module(module_path)
|
||||
else:
|
||||
# We've got a bare class name here, which won't work (No AppCache
|
||||
# to rely on). Try to throw a useful error.
|
||||
raise ImportError("Tastypie requires a Python-style path (<module.module.Class>) to lazy load related resources. Only given '%s'." % self.to)
|
||||
|
||||
self._to_class = getattr(module, class_name, None)
|
||||
|
||||
if self._to_class is None:
|
||||
raise ImportError("Module '%s' does not appear to have a class called '%s'." % (module_path, class_name))
|
||||
|
||||
return self._to_class
|
||||
|
||||
def dehydrate_related(self, bundle, related_resource, for_list=True):
|
||||
"""
|
||||
Based on the ``full_resource``, returns either the endpoint or the data
|
||||
from ``full_dehydrate`` for the related resource.
|
||||
"""
|
||||
should_dehydrate_full_resource = self.should_full_dehydrate(bundle, for_list=for_list)
|
||||
|
||||
if not should_dehydrate_full_resource:
|
||||
# Be a good netizen.
|
||||
return related_resource.get_resource_uri(bundle)
|
||||
else:
|
||||
# ZOMG extra data and big payloads.
|
||||
bundle = related_resource.build_bundle(
|
||||
obj=related_resource.instance,
|
||||
request=bundle.request,
|
||||
objects_saved=bundle.objects_saved
|
||||
)
|
||||
return related_resource.full_dehydrate(bundle)
|
||||
|
||||
def resource_from_uri(self, fk_resource, uri, request=None, related_obj=None, related_name=None):
|
||||
"""
|
||||
Given a URI is provided, the related resource is attempted to be
|
||||
loaded based on the identifiers in the URI.
|
||||
"""
|
||||
try:
|
||||
obj = fk_resource.get_via_uri(uri, request=request)
|
||||
bundle = fk_resource.build_bundle(
|
||||
obj=obj,
|
||||
request=request
|
||||
)
|
||||
return fk_resource.full_dehydrate(bundle)
|
||||
except ObjectDoesNotExist:
|
||||
raise ApiFieldError("Could not find the provided object via resource URI '%s'." % uri)
|
||||
|
||||
def resource_from_data(self, fk_resource, data, request=None, related_obj=None, related_name=None):
|
||||
"""
|
||||
Given a dictionary-like structure is provided, a fresh related
|
||||
resource is created using that data.
|
||||
"""
|
||||
# Try to hydrate the data provided.
|
||||
data = dict_strip_unicode_keys(data)
|
||||
fk_bundle = fk_resource.build_bundle(
|
||||
data=data,
|
||||
request=request
|
||||
)
|
||||
|
||||
if related_obj:
|
||||
fk_bundle.related_obj = related_obj
|
||||
fk_bundle.related_name = related_name
|
||||
|
||||
unique_keys = dict((k, v) for k, v in data.items() if k == 'pk' or (hasattr(fk_resource, k) and getattr(fk_resource, k).unique))
|
||||
|
||||
# If we have no unique keys, we shouldn't go look for some resource that
|
||||
# happens to match other kwargs. In the case of a create, it might be the
|
||||
# completely wrong resource.
|
||||
# We also need to check to see if updates are allowed on the FK resource.
|
||||
if unique_keys and fk_resource.can_update():
|
||||
try:
|
||||
return fk_resource.obj_update(fk_bundle, skip_errors=True, **data)
|
||||
except (NotFound, TypeError):
|
||||
try:
|
||||
# Attempt lookup by primary key
|
||||
return fk_resource.obj_update(fk_bundle, skip_errors=True, **unique_keys)
|
||||
except NotFound:
|
||||
pass
|
||||
except MultipleObjectsReturned:
|
||||
pass
|
||||
|
||||
# If we shouldn't update a resource, or we couldn't find a matching
|
||||
# resource we'll just return a populated bundle instead
|
||||
# of mistakenly updating something that should be read-only.
|
||||
fk_bundle = fk_resource.full_hydrate(fk_bundle)
|
||||
fk_resource.is_valid(fk_bundle)
|
||||
return fk_bundle
|
||||
|
||||
def resource_from_pk(self, fk_resource, obj, request=None, related_obj=None, related_name=None):
|
||||
"""
|
||||
Given an object with a ``pk`` attribute, the related resource
|
||||
is attempted to be loaded via that PK.
|
||||
"""
|
||||
bundle = fk_resource.build_bundle(
|
||||
obj=obj,
|
||||
request=request
|
||||
)
|
||||
return fk_resource.full_dehydrate(bundle)
|
||||
|
||||
def build_related_resource(self, value, request=None, related_obj=None, related_name=None):
|
||||
"""
|
||||
Returns a bundle of data built by the related resource, usually via
|
||||
``hydrate`` with the data provided.
|
||||
|
||||
Accepts either a URI, a data dictionary (or dictionary-like structure)
|
||||
or an object with a ``pk``.
|
||||
"""
|
||||
self.fk_resource = self.to_class()
|
||||
kwargs = {
|
||||
'request': request,
|
||||
'related_obj': related_obj,
|
||||
'related_name': related_name,
|
||||
}
|
||||
|
||||
if isinstance(value, Bundle):
|
||||
# Already hydrated, probably nested bundles. Just return.
|
||||
return value
|
||||
elif isinstance(value, six.string_types):
|
||||
# We got a URI. Load the object and assign it.
|
||||
return self.resource_from_uri(self.fk_resource, value, **kwargs)
|
||||
elif hasattr(value, 'items'):
|
||||
# We've got a data dictionary.
|
||||
# Since this leads to creation, this is the only one of these
|
||||
# methods that might care about "parent" data.
|
||||
return self.resource_from_data(self.fk_resource, value, **kwargs)
|
||||
elif hasattr(value, 'pk'):
|
||||
# We've got an object with a primary key.
|
||||
return self.resource_from_pk(self.fk_resource, value, **kwargs)
|
||||
else:
|
||||
raise ApiFieldError("The '%s' field was given data that was not a URI, not a dictionary-alike and does not have a 'pk' attribute: %s." % (self.instance_name, value))
|
||||
|
||||
def should_full_dehydrate(self, bundle, for_list):
|
||||
"""
|
||||
Based on the ``full``, ``list_full`` and ``detail_full`` returns ``True`` or ``False``
|
||||
indicating weather the resource should be fully dehydrated.
|
||||
"""
|
||||
should_dehydrate_full_resource = False
|
||||
if self.full:
|
||||
is_details_view = not for_list
|
||||
if is_details_view:
|
||||
if (not callable(self.full_detail) and self.full_detail) or (callable(self.full_detail) and self.full_detail(bundle)):
|
||||
should_dehydrate_full_resource = True
|
||||
else:
|
||||
if (not callable(self.full_list) and self.full_list) or (callable(self.full_list) and self.full_list(bundle)):
|
||||
should_dehydrate_full_resource = True
|
||||
|
||||
return should_dehydrate_full_resource
|
||||
|
||||
|
||||
class ToOneField(RelatedField):
|
||||
"""
|
||||
Provides access to related data via foreign key.
|
||||
|
||||
This subclass requires Django's ORM layer to work properly.
|
||||
"""
|
||||
help_text = 'A single related resource. Can be either a URI or set of nested resource data.'
|
||||
|
||||
def __init__(self, to, attribute, related_name=None, default=NOT_PROVIDED,
|
||||
null=False, blank=False, readonly=False, full=False,
|
||||
unique=False, help_text=None, use_in='all', full_list=True, full_detail=True):
|
||||
super(ToOneField, self).__init__(
|
||||
to, attribute, related_name=related_name, default=default,
|
||||
null=null, blank=blank, readonly=readonly, full=full,
|
||||
unique=unique, help_text=help_text, use_in=use_in,
|
||||
full_list=full_list, full_detail=full_detail
|
||||
)
|
||||
self.fk_resource = None
|
||||
|
||||
def dehydrate(self, bundle, for_list=True):
|
||||
foreign_obj = None
|
||||
error_to_raise = None
|
||||
|
||||
if isinstance(self.attribute, six.string_types):
|
||||
attrs = self.attribute.split('__')
|
||||
foreign_obj = bundle.obj
|
||||
|
||||
for attr in attrs:
|
||||
previous_obj = foreign_obj
|
||||
try:
|
||||
foreign_obj = getattr(foreign_obj, attr, None)
|
||||
except ObjectDoesNotExist:
|
||||
foreign_obj = None
|
||||
|
||||
elif callable(self.attribute):
|
||||
previous_obj = bundle.obj
|
||||
foreign_obj = self.attribute(bundle)
|
||||
|
||||
if not foreign_obj:
|
||||
if not self.null:
|
||||
if callable(self.attribute):
|
||||
raise ApiFieldError("The related resource for resource %s could not be found." % (previous_obj))
|
||||
else:
|
||||
raise ApiFieldError("The model '%r' has an empty attribute '%s' and doesn't allow a null value." % (previous_obj, attr))
|
||||
|
||||
return None
|
||||
|
||||
self.fk_resource = self.get_related_resource(foreign_obj)
|
||||
fk_bundle = Bundle(obj=foreign_obj, request=bundle.request)
|
||||
return self.dehydrate_related(fk_bundle, self.fk_resource, for_list=for_list)
|
||||
|
||||
def hydrate(self, bundle):
|
||||
value = super(ToOneField, self).hydrate(bundle)
|
||||
|
||||
if value is None:
|
||||
return value
|
||||
|
||||
return self.build_related_resource(value, request=bundle.request)
|
||||
|
||||
class ForeignKey(ToOneField):
|
||||
"""
|
||||
A convenience subclass for those who prefer to mirror ``django.db.models``.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class OneToOneField(ToOneField):
|
||||
"""
|
||||
A convenience subclass for those who prefer to mirror ``django.db.models``.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ToManyField(RelatedField):
|
||||
"""
|
||||
Provides access to related data via a join table.
|
||||
|
||||
This subclass requires Django's ORM layer to work properly.
|
||||
|
||||
Note that the ``hydrate`` portions of this field are quite different than
|
||||
any other field. ``hydrate_m2m`` actually handles the data and relations.
|
||||
This is due to the way Django implements M2M relationships.
|
||||
"""
|
||||
is_m2m = True
|
||||
help_text = 'Many related resources. Can be either a list of URIs or list of individually nested resource data.'
|
||||
|
||||
def __init__(self, to, attribute, related_name=None, default=NOT_PROVIDED,
|
||||
null=False, blank=False, readonly=False, full=False,
|
||||
unique=False, help_text=None, use_in='all', full_list=True, full_detail=True):
|
||||
super(ToManyField, self).__init__(
|
||||
to, attribute, related_name=related_name, default=default,
|
||||
null=null, blank=blank, readonly=readonly, full=full,
|
||||
unique=unique, help_text=help_text, use_in=use_in,
|
||||
full_list=full_list, full_detail=full_detail
|
||||
)
|
||||
self.m2m_bundles = []
|
||||
|
||||
def dehydrate(self, bundle, for_list=True):
|
||||
if not bundle.obj or not bundle.obj.pk:
|
||||
if not self.null:
|
||||
raise ApiFieldError("The model '%r' does not have a primary key and can not be used in a ToMany context." % bundle.obj)
|
||||
|
||||
return []
|
||||
|
||||
the_m2ms = None
|
||||
previous_obj = bundle.obj
|
||||
attr = self.attribute
|
||||
|
||||
if isinstance(self.attribute, six.string_types):
|
||||
attrs = self.attribute.split('__')
|
||||
the_m2ms = bundle.obj
|
||||
|
||||
for attr in attrs:
|
||||
previous_obj = the_m2ms
|
||||
try:
|
||||
the_m2ms = getattr(the_m2ms, attr, None)
|
||||
except ObjectDoesNotExist:
|
||||
the_m2ms = None
|
||||
|
||||
if not the_m2ms:
|
||||
break
|
||||
|
||||
elif callable(self.attribute):
|
||||
the_m2ms = self.attribute(bundle)
|
||||
|
||||
if not the_m2ms:
|
||||
if not self.null:
|
||||
raise ApiFieldError("The model '%r' has an empty attribute '%s' and doesn't allow a null value." % (previous_obj, attr))
|
||||
|
||||
return []
|
||||
|
||||
self.m2m_resources = []
|
||||
m2m_dehydrated = []
|
||||
|
||||
# TODO: Also model-specific and leaky. Relies on there being a
|
||||
# ``Manager`` there.
|
||||
for m2m in the_m2ms.all():
|
||||
m2m_resource = self.get_related_resource(m2m)
|
||||
m2m_bundle = Bundle(obj=m2m, request=bundle.request)
|
||||
self.m2m_resources.append(m2m_resource)
|
||||
m2m_dehydrated.append(self.dehydrate_related(m2m_bundle, m2m_resource, for_list=for_list))
|
||||
|
||||
return m2m_dehydrated
|
||||
|
||||
def hydrate(self, bundle):
|
||||
pass
|
||||
|
||||
def hydrate_m2m(self, bundle):
|
||||
if self.readonly:
|
||||
return None
|
||||
|
||||
if bundle.data.get(self.instance_name) is None:
|
||||
if self.blank:
|
||||
return []
|
||||
elif self.null:
|
||||
return []
|
||||
else:
|
||||
raise ApiFieldError("The '%s' field has no data and doesn't allow a null value." % self.instance_name)
|
||||
|
||||
m2m_hydrated = []
|
||||
|
||||
for value in bundle.data.get(self.instance_name):
|
||||
if value is None:
|
||||
continue
|
||||
|
||||
kwargs = {
|
||||
'request': bundle.request,
|
||||
}
|
||||
|
||||
if self.related_name:
|
||||
kwargs['related_obj'] = bundle.obj
|
||||
kwargs['related_name'] = self.related_name
|
||||
|
||||
m2m_hydrated.append(self.build_related_resource(value, **kwargs))
|
||||
|
||||
return m2m_hydrated
|
||||
|
||||
|
||||
class ManyToManyField(ToManyField):
|
||||
"""
|
||||
A convenience subclass for those who prefer to mirror ``django.db.models``.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class OneToManyField(ToManyField):
|
||||
"""
|
||||
A convenience subclass for those who prefer to mirror ``django.db.models``.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class TimeField(ApiField):
|
||||
dehydrated_type = 'time'
|
||||
help_text = 'A time as string. Ex: "20:05:23"'
|
||||
|
||||
def dehydrate(self, obj, for_list=True):
|
||||
return self.convert(super(TimeField, self).dehydrate(obj))
|
||||
|
||||
def convert(self, value):
|
||||
if isinstance(value, six.string_types):
|
||||
return self.to_time(value)
|
||||
return value
|
||||
|
||||
def to_time(self, s):
|
||||
try:
|
||||
dt = parse(s)
|
||||
except (ValueError, TypeError) as e:
|
||||
raise ApiFieldError(str(e))
|
||||
else:
|
||||
return datetime.time(dt.hour, dt.minute, dt.second)
|
||||
|
||||
def hydrate(self, bundle):
|
||||
value = super(TimeField, self).hydrate(bundle)
|
||||
|
||||
if value and not isinstance(value, datetime.time):
|
||||
value = self.to_time(value)
|
||||
|
||||
return value
|
|
@ -1,80 +0,0 @@
|
|||
"""
|
||||
The various HTTP responses for use in returning proper HTTP codes.
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
from django.http import HttpResponse
|
||||
|
||||
|
||||
class HttpCreated(HttpResponse):
|
||||
status_code = 201
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
location = kwargs.pop('location', '')
|
||||
|
||||
super(HttpCreated, self).__init__(*args, **kwargs)
|
||||
self['Location'] = location
|
||||
|
||||
|
||||
class HttpAccepted(HttpResponse):
|
||||
status_code = 202
|
||||
|
||||
|
||||
class HttpNoContent(HttpResponse):
|
||||
status_code = 204
|
||||
|
||||
|
||||
class HttpMultipleChoices(HttpResponse):
|
||||
status_code = 300
|
||||
|
||||
|
||||
class HttpSeeOther(HttpResponse):
|
||||
status_code = 303
|
||||
|
||||
|
||||
class HttpNotModified(HttpResponse):
|
||||
status_code = 304
|
||||
|
||||
|
||||
class HttpBadRequest(HttpResponse):
|
||||
status_code = 400
|
||||
|
||||
|
||||
class HttpUnauthorized(HttpResponse):
|
||||
status_code = 401
|
||||
|
||||
|
||||
class HttpForbidden(HttpResponse):
|
||||
status_code = 403
|
||||
|
||||
|
||||
class HttpNotFound(HttpResponse):
|
||||
status_code = 404
|
||||
|
||||
|
||||
class HttpMethodNotAllowed(HttpResponse):
|
||||
status_code = 405
|
||||
|
||||
|
||||
class HttpConflict(HttpResponse):
|
||||
status_code = 409
|
||||
|
||||
|
||||
class HttpGone(HttpResponse):
|
||||
status_code = 410
|
||||
|
||||
|
||||
class HttpUnprocessableEntity(HttpResponse):
|
||||
status_code = 422
|
||||
|
||||
|
||||
class HttpTooManyRequests(HttpResponse):
|
||||
status_code = 429
|
||||
|
||||
|
||||
class HttpApplicationError(HttpResponse):
|
||||
status_code = 500
|
||||
|
||||
|
||||
class HttpNotImplemented(HttpResponse):
|
||||
status_code = 501
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
from django.core.management.base import NoArgsCommand
|
||||
from tastypie.compat import get_user_model
|
||||
from tastypie.models import ApiKey
|
||||
|
||||
|
||||
class Command(NoArgsCommand):
|
||||
help = "Goes through all users and adds API keys for any that don't have one."
|
||||
|
||||
def handle_noargs(self, **options):
|
||||
"""Goes through all users and adds API keys for any that don't have one."""
|
||||
self.verbosity = int(options.get('verbosity', 1))
|
||||
|
||||
User = get_user_model()
|
||||
for user in User.objects.all().iterator():
|
||||
try:
|
||||
api_key = ApiKey.objects.get(user=user)
|
||||
|
||||
if not api_key.key:
|
||||
# Autogenerate the key.
|
||||
api_key.save()
|
||||
|
||||
if self.verbosity >= 1:
|
||||
print(u"Generated a new key for '%s'" % user.username)
|
||||
except ApiKey.DoesNotExist:
|
||||
api_key = ApiKey.objects.create(user=user)
|
||||
|
||||
if self.verbosity >= 1:
|
||||
print(u"Created a new key for '%s'" % user.username)
|
|
@ -1,42 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.conf import settings
|
||||
import tastypie.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ApiAccess',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('identifier', models.CharField(max_length=255)),
|
||||
('url', models.CharField(default='', max_length=255, blank=True)),
|
||||
('request_method', models.CharField(default='', max_length=10, blank=True)),
|
||||
('accessed', models.PositiveIntegerField()),
|
||||
],
|
||||
options={
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ApiKey',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('key', models.CharField(default='', max_length=128, db_index=True, blank=True)),
|
||||
('created', models.DateTimeField(default=tastypie.utils.timezone.now)),
|
||||
('user', models.OneToOneField(related_name='api_key', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
|
@ -1,63 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
import hmac
|
||||
import time
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from tastypie.utils import now
|
||||
|
||||
try:
|
||||
from hashlib import sha1
|
||||
except ImportError:
|
||||
import sha
|
||||
sha1 = sha.sha
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ApiAccess(models.Model):
|
||||
"""A simple model for use with the ``CacheDBThrottle`` behaviors."""
|
||||
identifier = models.CharField(max_length=255)
|
||||
url = models.CharField(max_length=255, blank=True, default='')
|
||||
request_method = models.CharField(max_length=10, blank=True, default='')
|
||||
accessed = models.PositiveIntegerField()
|
||||
|
||||
def __str__(self):
|
||||
return "%s @ %s" % (self.identifier, self.accessed)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.accessed = int(time.time())
|
||||
return super(ApiAccess, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
if 'django.contrib.auth' in settings.INSTALLED_APPS:
|
||||
import uuid
|
||||
from tastypie.compat import AUTH_USER_MODEL
|
||||
class ApiKey(models.Model):
|
||||
user = models.OneToOneField(AUTH_USER_MODEL, related_name='api_key')
|
||||
key = models.CharField(max_length=128, blank=True, default='', db_index=True)
|
||||
created = models.DateTimeField(default=now)
|
||||
|
||||
def __unicode__(self):
|
||||
return u"%s for %s" % (self.key, self.user)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.key:
|
||||
self.key = self.generate_key()
|
||||
|
||||
return super(ApiKey, self).save(*args, **kwargs)
|
||||
|
||||
def generate_key(self):
|
||||
# Get a random UUID.
|
||||
new_uuid = uuid.uuid4()
|
||||
# Hmac that beast.
|
||||
return hmac.new(new_uuid.bytes, digestmod=sha1).hexdigest()
|
||||
|
||||
class Meta:
|
||||
abstract = getattr(settings, 'TASTYPIE_ABSTRACT_APIKEY', False)
|
||||
|
||||
|
||||
def create_api_key(sender, **kwargs):
|
||||
"""
|
||||
A signal for hooking up automatic ``ApiKey`` creation.
|
||||
"""
|
||||
if kwargs.get('created') is True:
|
||||
ApiKey.objects.create(user=kwargs.get('instance'))
|
|
@ -1,209 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import six
|
||||
|
||||
from tastypie.exceptions import BadRequest
|
||||
|
||||
try:
|
||||
from urllib.parse import urlencode
|
||||
except ImportError:
|
||||
from urllib import urlencode
|
||||
|
||||
|
||||
class Paginator(object):
|
||||
"""
|
||||
Limits result sets down to sane amounts for passing to the client.
|
||||
|
||||
This is used in place of Django's ``Paginator`` due to the way pagination
|
||||
works. ``limit`` & ``offset`` (tastypie) are used in place of ``page``
|
||||
(Django) so none of the page-related calculations are necessary.
|
||||
|
||||
This implementation also provides additional details like the
|
||||
``total_count`` of resources seen and convenience links to the
|
||||
``previous``/``next`` pages of data as available.
|
||||
"""
|
||||
def __init__(self, request_data, objects, resource_uri=None, limit=None, offset=0, max_limit=1000, collection_name='objects'):
|
||||
"""
|
||||
Instantiates the ``Paginator`` and allows for some configuration.
|
||||
|
||||
The ``request_data`` argument ought to be a dictionary-like object.
|
||||
May provide ``limit`` and/or ``offset`` to override the defaults.
|
||||
Commonly provided ``request.GET``. Required.
|
||||
|
||||
The ``objects`` should be a list-like object of ``Resources``.
|
||||
This is typically a ``QuerySet`` but can be anything that
|
||||
implements slicing. Required.
|
||||
|
||||
Optionally accepts a ``limit`` argument, which specifies how many
|
||||
items to show at a time. Defaults to ``None``, which is no limit.
|
||||
|
||||
Optionally accepts an ``offset`` argument, which specifies where in
|
||||
the ``objects`` to start displaying results from. Defaults to 0.
|
||||
|
||||
Optionally accepts a ``max_limit`` argument, which the upper bound
|
||||
limit. Defaults to ``1000``. If you set it to 0 or ``None``, no upper
|
||||
bound will be enforced.
|
||||
"""
|
||||
self.request_data = request_data
|
||||
self.objects = objects
|
||||
self.limit = limit
|
||||
self.max_limit = max_limit
|
||||
self.offset = offset
|
||||
self.resource_uri = resource_uri
|
||||
self.collection_name = collection_name
|
||||
|
||||
def get_limit(self):
|
||||
"""
|
||||
Determines the proper maximum number of results to return.
|
||||
|
||||
In order of importance, it will use:
|
||||
|
||||
* The user-requested ``limit`` from the GET parameters, if specified.
|
||||
* The object-level ``limit`` if specified.
|
||||
* ``settings.API_LIMIT_PER_PAGE`` if specified.
|
||||
|
||||
Default is 20 per page.
|
||||
"""
|
||||
|
||||
limit = self.request_data.get('limit', self.limit)
|
||||
if limit is None:
|
||||
limit = getattr(settings, 'API_LIMIT_PER_PAGE', 20)
|
||||
|
||||
try:
|
||||
limit = int(limit)
|
||||
except ValueError:
|
||||
raise BadRequest("Invalid limit '%s' provided. Please provide a positive integer." % limit)
|
||||
|
||||
if limit < 0:
|
||||
raise BadRequest("Invalid limit '%s' provided. Please provide a positive integer >= 0." % limit)
|
||||
|
||||
if self.max_limit and (not limit or limit > self.max_limit):
|
||||
# If it's more than the max, we're only going to return the max.
|
||||
# This is to prevent excessive DB (or other) load.
|
||||
return self.max_limit
|
||||
|
||||
return limit
|
||||
|
||||
def get_offset(self):
|
||||
"""
|
||||
Determines the proper starting offset of results to return.
|
||||
|
||||
It attempts to use the user-provided ``offset`` from the GET parameters,
|
||||
if specified. Otherwise, it falls back to the object-level ``offset``.
|
||||
|
||||
Default is 0.
|
||||
"""
|
||||
offset = self.offset
|
||||
|
||||
if 'offset' in self.request_data:
|
||||
offset = self.request_data['offset']
|
||||
|
||||
try:
|
||||
offset = int(offset)
|
||||
except ValueError:
|
||||
raise BadRequest("Invalid offset '%s' provided. Please provide an integer." % offset)
|
||||
|
||||
if offset < 0:
|
||||
raise BadRequest("Invalid offset '%s' provided. Please provide a positive integer >= 0." % offset)
|
||||
|
||||
return offset
|
||||
|
||||
def get_slice(self, limit, offset):
|
||||
"""
|
||||
Slices the result set to the specified ``limit`` & ``offset``.
|
||||
"""
|
||||
if limit == 0:
|
||||
return self.objects[offset:]
|
||||
|
||||
return self.objects[offset:offset + limit]
|
||||
|
||||
def get_count(self):
|
||||
"""
|
||||
Returns a count of the total number of objects seen.
|
||||
"""
|
||||
try:
|
||||
return self.objects.count()
|
||||
except (AttributeError, TypeError):
|
||||
# If it's not a QuerySet (or it's ilk), fallback to ``len``.
|
||||
return len(self.objects)
|
||||
|
||||
def get_previous(self, limit, offset):
|
||||
"""
|
||||
If a previous page is available, will generate a URL to request that
|
||||
page. If not available, this returns ``None``.
|
||||
"""
|
||||
if offset - limit < 0:
|
||||
return None
|
||||
|
||||
return self._generate_uri(limit, offset-limit)
|
||||
|
||||
def get_next(self, limit, offset, count):
|
||||
"""
|
||||
If a next page is available, will generate a URL to request that
|
||||
page. If not available, this returns ``None``.
|
||||
"""
|
||||
if offset + limit >= count:
|
||||
return None
|
||||
|
||||
return self._generate_uri(limit, offset+limit)
|
||||
|
||||
def _generate_uri(self, limit, offset):
|
||||
if self.resource_uri is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
# QueryDict has a urlencode method that can handle multiple values for the same key
|
||||
request_params = self.request_data.copy()
|
||||
if 'limit' in request_params:
|
||||
del request_params['limit']
|
||||
if 'offset' in request_params:
|
||||
del request_params['offset']
|
||||
request_params.update({'limit': limit, 'offset': offset})
|
||||
encoded_params = request_params.urlencode()
|
||||
except AttributeError:
|
||||
request_params = {}
|
||||
|
||||
for k, v in self.request_data.items():
|
||||
if isinstance(v, six.text_type):
|
||||
request_params[k] = v.encode('utf-8')
|
||||
else:
|
||||
request_params[k] = v
|
||||
|
||||
if 'limit' in request_params:
|
||||
del request_params['limit']
|
||||
if 'offset' in request_params:
|
||||
del request_params['offset']
|
||||
request_params.update({'limit': limit, 'offset': offset})
|
||||
encoded_params = urlencode(request_params)
|
||||
|
||||
return '%s?%s' % (
|
||||
self.resource_uri,
|
||||
encoded_params
|
||||
)
|
||||
|
||||
def page(self):
|
||||
"""
|
||||
Generates all pertinent data about the requested page.
|
||||
|
||||
Handles getting the correct ``limit`` & ``offset``, then slices off
|
||||
the correct set of results and returns all pertinent metadata.
|
||||
"""
|
||||
limit = self.get_limit()
|
||||
offset = self.get_offset()
|
||||
count = self.get_count()
|
||||
objects = self.get_slice(limit, offset)
|
||||
meta = {
|
||||
'offset': offset,
|
||||
'limit': limit,
|
||||
'total_count': count,
|
||||
}
|
||||
|
||||
if limit:
|
||||
meta['previous'] = self.get_previous(limit, offset)
|
||||
meta['next'] = self.get_next(limit, offset, count)
|
||||
|
||||
return {
|
||||
self.collection_name: objects,
|
||||
'meta': meta,
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,519 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
import datetime
|
||||
import re
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils import six
|
||||
from django.utils.encoding import force_text, smart_bytes
|
||||
from django.core.serializers import json as djangojson
|
||||
|
||||
from tastypie.bundle import Bundle
|
||||
from tastypie.exceptions import BadRequest, UnsupportedFormat
|
||||
from tastypie.utils import format_datetime, format_date, format_time, make_naive
|
||||
|
||||
try:
|
||||
import defusedxml.lxml as lxml
|
||||
from defusedxml.common import DefusedXmlException
|
||||
from defusedxml.lxml import parse as parse_xml
|
||||
from lxml.etree import Element, tostring, LxmlError, XMLParser
|
||||
except ImportError:
|
||||
lxml = None
|
||||
|
||||
try:
|
||||
import yaml
|
||||
from django.core.serializers import pyyaml
|
||||
except ImportError:
|
||||
yaml = None
|
||||
|
||||
try:
|
||||
import biplist
|
||||
except ImportError:
|
||||
biplist = None
|
||||
|
||||
import json
|
||||
|
||||
|
||||
XML_ENCODING = re.compile('<\?xml.*?\?>', re.IGNORECASE)
|
||||
|
||||
|
||||
# Ugh & blah.
|
||||
# So doing a regular dump is generally fine, since Tastypie doesn't usually
|
||||
# serialize advanced types. *HOWEVER*, it will dump out Python Unicode strings
|
||||
# as a custom YAML tag, which of course ``yaml.safe_load`` can't handle.
|
||||
if yaml is not None:
|
||||
from yaml.constructor import SafeConstructor
|
||||
from yaml.loader import Reader, Scanner, Parser, Composer, Resolver
|
||||
|
||||
class TastypieConstructor(SafeConstructor):
|
||||
def construct_yaml_unicode_dammit(self, node):
|
||||
value = self.construct_scalar(node)
|
||||
try:
|
||||
return value.encode('ascii')
|
||||
except UnicodeEncodeError:
|
||||
return value
|
||||
|
||||
TastypieConstructor.add_constructor(u'tag:yaml.org,2002:python/unicode', TastypieConstructor.construct_yaml_unicode_dammit)
|
||||
|
||||
class TastypieLoader(Reader, Scanner, Parser, Composer, TastypieConstructor, Resolver):
|
||||
def __init__(self, stream):
|
||||
Reader.__init__(self, stream)
|
||||
Scanner.__init__(self)
|
||||
Parser.__init__(self)
|
||||
Composer.__init__(self)
|
||||
TastypieConstructor.__init__(self)
|
||||
Resolver.__init__(self)
|
||||
|
||||
|
||||
class Serializer(object):
|
||||
"""
|
||||
A swappable class for serialization.
|
||||
|
||||
This handles most types of data as well as the following output formats::
|
||||
|
||||
* json
|
||||
* jsonp (Disabled by default)
|
||||
* xml
|
||||
* yaml
|
||||
* html
|
||||
* plist (see http://explorapp.com/biplist/)
|
||||
|
||||
It was designed to make changing behavior easy, either by overridding the
|
||||
various format methods (i.e. ``to_json``), by changing the
|
||||
``formats/content_types`` options or by altering the other hook methods.
|
||||
"""
|
||||
|
||||
formats = ['json', 'xml', 'yaml', 'html', 'plist']
|
||||
|
||||
content_types = {'json': 'application/json',
|
||||
'jsonp': 'text/javascript',
|
||||
'xml': 'application/xml',
|
||||
'yaml': 'text/yaml',
|
||||
'html': 'text/html',
|
||||
'plist': 'application/x-plist'}
|
||||
|
||||
def __init__(self, formats=None, content_types=None, datetime_formatting=None):
|
||||
if datetime_formatting is not None:
|
||||
self.datetime_formatting = datetime_formatting
|
||||
else:
|
||||
self.datetime_formatting = getattr(settings, 'TASTYPIE_DATETIME_FORMATTING', 'iso-8601')
|
||||
|
||||
self.supported_formats = []
|
||||
|
||||
if content_types is not None:
|
||||
self.content_types = content_types
|
||||
|
||||
if formats is not None:
|
||||
self.formats = formats
|
||||
|
||||
if self.formats is Serializer.formats and hasattr(settings, 'TASTYPIE_DEFAULT_FORMATS'):
|
||||
# We want TASTYPIE_DEFAULT_FORMATS to override unmodified defaults but not intentational changes
|
||||
# on Serializer subclasses:
|
||||
self.formats = settings.TASTYPIE_DEFAULT_FORMATS
|
||||
|
||||
if not isinstance(self.formats, (list, tuple)):
|
||||
raise ImproperlyConfigured('Formats should be a list or tuple, not %r' % self.formats)
|
||||
|
||||
for format in self.formats:
|
||||
try:
|
||||
self.supported_formats.append(self.content_types[format])
|
||||
except KeyError:
|
||||
raise ImproperlyConfigured("Content type for specified type '%s' not found. Please provide it at either the class level or via the arguments." % format)
|
||||
|
||||
def get_mime_for_format(self, format):
|
||||
"""
|
||||
Given a format, attempts to determine the correct MIME type.
|
||||
|
||||
If not available on the current ``Serializer``, returns
|
||||
``application/json`` by default.
|
||||
"""
|
||||
try:
|
||||
return self.content_types[format]
|
||||
except KeyError:
|
||||
return 'application/json'
|
||||
|
||||
def format_datetime(self, data):
|
||||
"""
|
||||
A hook to control how datetimes are formatted.
|
||||
|
||||
Can be overridden at the ``Serializer`` level (``datetime_formatting``)
|
||||
or globally (via ``settings.TASTYPIE_DATETIME_FORMATTING``).
|
||||
|
||||
Default is ``iso-8601``, which looks like "2010-12-16T03:02:14".
|
||||
"""
|
||||
data = make_naive(data)
|
||||
if self.datetime_formatting == 'rfc-2822':
|
||||
return format_datetime(data)
|
||||
if self.datetime_formatting == 'iso-8601-strict':
|
||||
# Remove microseconds to strictly adhere to iso-8601
|
||||
data = data - datetime.timedelta(microseconds = data.microsecond)
|
||||
|
||||
return data.isoformat()
|
||||
|
||||
def format_date(self, data):
|
||||
"""
|
||||
A hook to control how dates are formatted.
|
||||
|
||||
Can be overridden at the ``Serializer`` level (``datetime_formatting``)
|
||||
or globally (via ``settings.TASTYPIE_DATETIME_FORMATTING``).
|
||||
|
||||
Default is ``iso-8601``, which looks like "2010-12-16".
|
||||
"""
|
||||
if self.datetime_formatting == 'rfc-2822':
|
||||
return format_date(data)
|
||||
|
||||
return data.isoformat()
|
||||
|
||||
def format_time(self, data):
|
||||
"""
|
||||
A hook to control how times are formatted.
|
||||
|
||||
Can be overridden at the ``Serializer`` level (``datetime_formatting``)
|
||||
or globally (via ``settings.TASTYPIE_DATETIME_FORMATTING``).
|
||||
|
||||
Default is ``iso-8601``, which looks like "03:02:14".
|
||||
"""
|
||||
if self.datetime_formatting == 'rfc-2822':
|
||||
return format_time(data)
|
||||
if self.datetime_formatting == 'iso-8601-strict':
|
||||
# Remove microseconds to strictly adhere to iso-8601
|
||||
data = (datetime.datetime.combine(datetime.date(1,1,1),data) - datetime.timedelta(microseconds = data.microsecond)).time()
|
||||
|
||||
return data.isoformat()
|
||||
|
||||
def serialize(self, bundle, format='application/json', options=None):
|
||||
"""
|
||||
Given some data and a format, calls the correct method to serialize
|
||||
the data and returns the result.
|
||||
"""
|
||||
desired_format = None
|
||||
if options is None:
|
||||
options = {}
|
||||
|
||||
for short_format, long_format in self.content_types.items():
|
||||
if format == long_format:
|
||||
if hasattr(self, "to_%s" % short_format):
|
||||
desired_format = short_format
|
||||
break
|
||||
|
||||
if desired_format is None:
|
||||
raise UnsupportedFormat("The format indicated '%s' had no available serialization method. Please check your ``formats`` and ``content_types`` on your Serializer." % format)
|
||||
|
||||
serialized = getattr(self, "to_%s" % desired_format)(bundle, options)
|
||||
return serialized
|
||||
|
||||
def deserialize(self, content, format='application/json'):
|
||||
"""
|
||||
Given some data and a format, calls the correct method to deserialize
|
||||
the data and returns the result.
|
||||
"""
|
||||
desired_format = None
|
||||
|
||||
format = format.split(';')[0]
|
||||
|
||||
for short_format, long_format in self.content_types.items():
|
||||
if format == long_format:
|
||||
if hasattr(self, "from_%s" % short_format):
|
||||
desired_format = short_format
|
||||
break
|
||||
|
||||
if desired_format is None:
|
||||
raise UnsupportedFormat("The format indicated '%s' had no available deserialization method. Please check your ``formats`` and ``content_types`` on your Serializer." % format)
|
||||
|
||||
if isinstance(content, six.binary_type):
|
||||
content = force_text(content)
|
||||
|
||||
deserialized = getattr(self, "from_%s" % desired_format)(content)
|
||||
return deserialized
|
||||
|
||||
def to_simple(self, data, options):
|
||||
"""
|
||||
For a piece of data, attempts to recognize it and provide a simplified
|
||||
form of something complex.
|
||||
|
||||
This brings complex Python data structures down to native types of the
|
||||
serialization format(s).
|
||||
"""
|
||||
if isinstance(data, (list, tuple)):
|
||||
return [self.to_simple(item, options) for item in data]
|
||||
if isinstance(data, dict):
|
||||
return dict((key, self.to_simple(val, options)) for (key, val) in data.items())
|
||||
elif isinstance(data, Bundle):
|
||||
return dict((key, self.to_simple(val, options)) for (key, val) in data.data.items())
|
||||
elif hasattr(data, 'dehydrated_type'):
|
||||
if getattr(data, 'dehydrated_type', None) == 'related' and data.is_m2m == False:
|
||||
if data.full:
|
||||
return self.to_simple(data.fk_resource, options)
|
||||
else:
|
||||
return self.to_simple(data.value, options)
|
||||
elif getattr(data, 'dehydrated_type', None) == 'related' and data.is_m2m == True:
|
||||
if data.full:
|
||||
return [self.to_simple(bundle, options) for bundle in data.m2m_bundles]
|
||||
else:
|
||||
return [self.to_simple(val, options) for val in data.value]
|
||||
else:
|
||||
return self.to_simple(data.value, options)
|
||||
elif isinstance(data, datetime.datetime):
|
||||
return self.format_datetime(data)
|
||||
elif isinstance(data, datetime.date):
|
||||
return self.format_date(data)
|
||||
elif isinstance(data, datetime.time):
|
||||
return self.format_time(data)
|
||||
elif isinstance(data, bool):
|
||||
return data
|
||||
elif isinstance(data, (six.integer_types, float)):
|
||||
return data
|
||||
elif data is None:
|
||||
return None
|
||||
else:
|
||||
return force_text(data)
|
||||
|
||||
def to_etree(self, data, options=None, name=None, depth=0):
|
||||
"""
|
||||
Given some data, converts that data to an ``etree.Element`` suitable
|
||||
for use in the XML output.
|
||||
"""
|
||||
if isinstance(data, (list, tuple)):
|
||||
element = Element(name or 'objects')
|
||||
if name:
|
||||
element = Element(name)
|
||||
element.set('type', 'list')
|
||||
else:
|
||||
element = Element('objects')
|
||||
for item in data:
|
||||
element.append(self.to_etree(item, options, depth=depth+1))
|
||||
element[:] = sorted(element, key=lambda x: x.tag)
|
||||
elif isinstance(data, dict):
|
||||
if depth == 0:
|
||||
element = Element(name or 'response')
|
||||
else:
|
||||
element = Element(name or 'object')
|
||||
element.set('type', 'hash')
|
||||
for (key, value) in data.items():
|
||||
element.append(self.to_etree(value, options, name=key, depth=depth+1))
|
||||
element[:] = sorted(element, key=lambda x: x.tag)
|
||||
elif isinstance(data, Bundle):
|
||||
element = Element(name or 'object')
|
||||
for field_name, field_object in data.data.items():
|
||||
element.append(self.to_etree(field_object, options, name=field_name, depth=depth+1))
|
||||
element[:] = sorted(element, key=lambda x: x.tag)
|
||||
elif hasattr(data, 'dehydrated_type'):
|
||||
if getattr(data, 'dehydrated_type', None) == 'related' and data.is_m2m == False:
|
||||
if data.full:
|
||||
return self.to_etree(data.fk_resource, options, name, depth+1)
|
||||
else:
|
||||
return self.to_etree(data.value, options, name, depth+1)
|
||||
elif getattr(data, 'dehydrated_type', None) == 'related' and data.is_m2m == True:
|
||||
if data.full:
|
||||
element = Element(name or 'objects')
|
||||
for bundle in data.m2m_bundles:
|
||||
element.append(self.to_etree(bundle, options, bundle.resource_name, depth+1))
|
||||
else:
|
||||
element = Element(name or 'objects')
|
||||
for value in data.value:
|
||||
element.append(self.to_etree(value, options, name, depth=depth+1))
|
||||
else:
|
||||
return self.to_etree(data.value, options, name)
|
||||
else:
|
||||
element = Element(name or 'value')
|
||||
simple_data = self.to_simple(data, options)
|
||||
data_type = get_type_string(simple_data)
|
||||
|
||||
if data_type != 'string':
|
||||
element.set('type', get_type_string(simple_data))
|
||||
|
||||
if data_type != 'null':
|
||||
if isinstance(simple_data, six.text_type):
|
||||
element.text = simple_data
|
||||
else:
|
||||
element.text = force_text(simple_data)
|
||||
|
||||
return element
|
||||
|
||||
def from_etree(self, data):
|
||||
"""
|
||||
Not the smartest deserializer on the planet. At the request level,
|
||||
it first tries to output the deserialized subelement called "object"
|
||||
or "objects" and falls back to deserializing based on hinted types in
|
||||
the XML element attribute "type".
|
||||
"""
|
||||
if data.tag == 'request':
|
||||
# if "object" or "objects" exists, return deserialized forms.
|
||||
elements = data.getchildren()
|
||||
for element in elements:
|
||||
if element.tag in ('object', 'objects'):
|
||||
return self.from_etree(element)
|
||||
return dict((element.tag, self.from_etree(element)) for element in elements)
|
||||
elif data.tag == 'object' or data.get('type') == 'hash':
|
||||
return dict((element.tag, self.from_etree(element)) for element in data.getchildren())
|
||||
elif data.tag == 'objects' or data.get('type') == 'list':
|
||||
return [self.from_etree(element) for element in data.getchildren()]
|
||||
else:
|
||||
type_string = data.get('type')
|
||||
if type_string in ('string', None):
|
||||
return data.text
|
||||
elif type_string == 'integer':
|
||||
return int(data.text)
|
||||
elif type_string == 'float':
|
||||
return float(data.text)
|
||||
elif type_string == 'boolean':
|
||||
if data.text == 'True':
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return None
|
||||
|
||||
def to_json(self, data, options=None):
|
||||
"""
|
||||
Given some Python data, produces JSON output.
|
||||
"""
|
||||
options = options or {}
|
||||
data = self.to_simple(data, options)
|
||||
|
||||
return djangojson.json.dumps(data, cls=djangojson.DjangoJSONEncoder, sort_keys=True, ensure_ascii=False, indent=2)
|
||||
|
||||
def from_json(self, content):
|
||||
"""
|
||||
Given some JSON data, returns a Python dictionary of the decoded data.
|
||||
"""
|
||||
try:
|
||||
return json.loads(content)
|
||||
except ValueError:
|
||||
raise BadRequest
|
||||
|
||||
def to_jsonp(self, data, options=None):
|
||||
"""
|
||||
Given some Python data, produces JSON output wrapped in the provided
|
||||
callback.
|
||||
|
||||
Due to a difference between JSON and Javascript, two
|
||||
newline characters, \u2028 and \u2029, need to be escaped.
|
||||
See http://timelessrepo.com/json-isnt-a-javascript-subset for
|
||||
details.
|
||||
"""
|
||||
options = options or {}
|
||||
json = self.to_json(data, options)
|
||||
json = json.replace(u'\u2028', u'\\u2028').replace(u'\u2029', u'\\u2029')
|
||||
return u'%s(%s)' % (options['callback'], json)
|
||||
|
||||
def to_xml(self, data, options=None):
|
||||
"""
|
||||
Given some Python data, produces XML output.
|
||||
"""
|
||||
options = options or {}
|
||||
|
||||
if lxml is None:
|
||||
raise ImproperlyConfigured("Usage of the XML aspects requires lxml and defusedxml.")
|
||||
|
||||
return tostring(self.to_etree(data, options), xml_declaration=True, encoding='utf-8')
|
||||
|
||||
def from_xml(self, content, forbid_dtd=True, forbid_entities=True):
|
||||
"""
|
||||
Given some XML data, returns a Python dictionary of the decoded data.
|
||||
|
||||
By default XML entity declarations and DTDs will raise a BadRequest
|
||||
exception content but subclasses may choose to override this if
|
||||
necessary.
|
||||
"""
|
||||
if lxml is None:
|
||||
raise ImproperlyConfigured("Usage of the XML aspects requires lxml and defusedxml.")
|
||||
|
||||
try:
|
||||
# Stripping the encoding declaration. Because lxml.
|
||||
# See http://lxml.de/parsing.html, "Python unicode strings".
|
||||
content = XML_ENCODING.sub('', content)
|
||||
parsed = parse_xml(
|
||||
six.StringIO(content),
|
||||
forbid_dtd=forbid_dtd,
|
||||
forbid_entities=forbid_entities
|
||||
)
|
||||
except (LxmlError, DefusedXmlException):
|
||||
raise BadRequest()
|
||||
|
||||
return self.from_etree(parsed.getroot())
|
||||
|
||||
def to_yaml(self, data, options=None):
|
||||
"""
|
||||
Given some Python data, produces YAML output.
|
||||
"""
|
||||
options = options or {}
|
||||
|
||||
if yaml is None:
|
||||
raise ImproperlyConfigured("Usage of the YAML aspects requires yaml.")
|
||||
|
||||
return yaml.dump(self.to_simple(data, options))
|
||||
|
||||
def from_yaml(self, content):
|
||||
"""
|
||||
Given some YAML data, returns a Python dictionary of the decoded data.
|
||||
"""
|
||||
if yaml is None:
|
||||
raise ImproperlyConfigured("Usage of the YAML aspects requires yaml.")
|
||||
|
||||
return yaml.load(content, Loader=TastypieLoader)
|
||||
|
||||
def to_plist(self, data, options=None):
|
||||
"""
|
||||
Given some Python data, produces binary plist output.
|
||||
"""
|
||||
options = options or {}
|
||||
|
||||
if biplist is None:
|
||||
raise ImproperlyConfigured("Usage of the plist aspects requires biplist.")
|
||||
|
||||
return biplist.writePlistToString(self.to_simple(data, options))
|
||||
|
||||
def from_plist(self, content):
|
||||
"""
|
||||
Given some binary plist data, returns a Python dictionary of the decoded data.
|
||||
"""
|
||||
if biplist is None:
|
||||
raise ImproperlyConfigured("Usage of the plist aspects requires biplist.")
|
||||
|
||||
if isinstance(content, six.text_type):
|
||||
content = smart_bytes(content)
|
||||
|
||||
return biplist.readPlistFromString(content)
|
||||
|
||||
def to_html(self, data, options=None):
|
||||
"""
|
||||
Reserved for future usage.
|
||||
|
||||
The desire is to provide HTML output of a resource, making an API
|
||||
available to a browser. This is on the TODO list but not currently
|
||||
implemented.
|
||||
"""
|
||||
options = options or {}
|
||||
return 'Sorry, not implemented yet. Please append "?format=json" to your URL.'
|
||||
|
||||
def from_html(self, content):
|
||||
"""
|
||||
Reserved for future usage.
|
||||
|
||||
The desire is to handle form-based (maybe Javascript?) input, making an
|
||||
API available to a browser. This is on the TODO list but not currently
|
||||
implemented.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_type_string(data):
|
||||
"""
|
||||
Translates a Python data type into a string format.
|
||||
"""
|
||||
data_type = type(data)
|
||||
|
||||
if data_type in six.integer_types:
|
||||
return 'integer'
|
||||
elif data_type == float:
|
||||
return 'float'
|
||||
elif data_type == bool:
|
||||
return 'boolean'
|
||||
elif data_type in (list, tuple):
|
||||
return 'list'
|
||||
elif data_type == dict:
|
||||
return 'hash'
|
||||
elif data is None:
|
||||
return 'null'
|
||||
elif isinstance(data, six.string_types):
|
||||
return 'string'
|
|
@ -1,97 +0,0 @@
|
|||
# encoding: utf-8
|
||||
from __future__ import unicode_literals
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
from tastypie.compat import AUTH_USER_MODEL
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
|
||||
# Adding model 'ApiAccess'
|
||||
db.create_table('tastypie_apiaccess', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('identifier', self.gf('django.db.models.fields.CharField')(max_length=255)),
|
||||
('url', self.gf('django.db.models.fields.CharField')(default='', max_length=255, blank=True)),
|
||||
('request_method', self.gf('django.db.models.fields.CharField')(default='', max_length=10, blank=True)),
|
||||
('accessed', self.gf('django.db.models.fields.PositiveIntegerField')()),
|
||||
))
|
||||
db.send_create_signal('tastypie', ['ApiAccess'])
|
||||
|
||||
# Adding model 'ApiKey'
|
||||
db.create_table('tastypie_apikey', (
|
||||
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
|
||||
('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='api_key', unique=True, to=orm[AUTH_USER_MODEL])),
|
||||
('key', self.gf('django.db.models.fields.CharField')(default='', max_length=256, blank=True)),
|
||||
('created', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.now)),
|
||||
))
|
||||
db.send_create_signal('tastypie', ['ApiKey'])
|
||||
|
||||
|
||||
def backwards(self, orm):
|
||||
|
||||
# Deleting model 'ApiAccess'
|
||||
db.delete_table('tastypie_apiaccess')
|
||||
|
||||
# Deleting model 'ApiKey'
|
||||
db.delete_table('tastypie_apikey')
|
||||
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
AUTH_USER_MODEL: {
|
||||
'Meta': {'object_name': AUTH_USER_MODEL.split('.')[-1]},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'tastypie.apiaccess': {
|
||||
'Meta': {'object_name': 'ApiAccess'},
|
||||
'accessed': ('django.db.models.fields.PositiveIntegerField', [], {}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'request_method': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': 'True'}),
|
||||
'url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'})
|
||||
},
|
||||
'tastypie.apikey': {
|
||||
'Meta': {'object_name': 'ApiKey'},
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'api_key'", 'unique': 'True', 'to': "orm['%s']" % AUTH_USER_MODEL})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['tastypie']
|
|
@ -1,76 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
import datetime
|
||||
from south.db import db
|
||||
from south.v2 import SchemaMigration
|
||||
from django.db import models
|
||||
from tastypie.compat import AUTH_USER_MODEL
|
||||
|
||||
|
||||
class Migration(SchemaMigration):
|
||||
|
||||
def forwards(self, orm):
|
||||
if not db.backend_name in ('mysql', 'sqlite'):
|
||||
# Adding index on 'ApiKey', fields ['key']
|
||||
db.create_index('tastypie_apikey', ['key'])
|
||||
|
||||
def backwards(self, orm):
|
||||
if not db.backend_name in ('mysql', 'sqlite'):
|
||||
# Removing index on 'ApiKey', fields ['key']
|
||||
db.delete_index('tastypie_apikey', ['key'])
|
||||
|
||||
models = {
|
||||
'auth.group': {
|
||||
'Meta': {'object_name': 'Group'},
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
|
||||
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
|
||||
},
|
||||
'auth.permission': {
|
||||
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
|
||||
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
|
||||
},
|
||||
AUTH_USER_MODEL: {
|
||||
'Meta': {'object_name': AUTH_USER_MODEL.split('.')[-1]},
|
||||
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
|
||||
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True', 'blank': 'True'}),
|
||||
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
|
||||
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'blank': 'True'}),
|
||||
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
|
||||
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
|
||||
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
|
||||
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
|
||||
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
|
||||
},
|
||||
'contenttypes.contenttype': {
|
||||
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
|
||||
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
|
||||
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
|
||||
},
|
||||
'tastypie.apiaccess': {
|
||||
'Meta': {'object_name': 'ApiAccess'},
|
||||
'accessed': ('django.db.models.fields.PositiveIntegerField', [], {}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'identifier': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
|
||||
'request_method': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': 'True'}),
|
||||
'url': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '255', 'blank': 'True'})
|
||||
},
|
||||
'tastypie.apikey': {
|
||||
'Meta': {'object_name': 'ApiKey'},
|
||||
'created': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(2012, 11, 5, 0, 0)'}),
|
||||
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
|
||||
'key': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '256', 'db_index': 'True', 'blank': 'True'}),
|
||||
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'api_key'", 'unique': 'True', 'to': "orm['%s']" % AUTH_USER_MODEL})
|
||||
}
|
||||
}
|
||||
|
||||
complete_apps = ['tastypie']
|
|
@ -1,29 +0,0 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>{% block title %}API for example.com{% endblock %}</title>
|
||||
<style type="text/css">
|
||||
* { margin: 0; padding: 0; }
|
||||
body { background-color: #DDDDDD; font-family: Georgia, Times New Roman, sans-serif; text-align: center; }
|
||||
#page_wrapper { margin: 0 auto; text-align: left; width: 800px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="page_wrapper">
|
||||
<h1>API for example.com</h1>
|
||||
|
||||
<section class="resources_nav">
|
||||
{% block resources_nav %}
|
||||
<ul>
|
||||
{% for resource in resources_nav_items %}
|
||||
<li><a href="{# % url api_resource_list % #}">{{ resource }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
||||
</section>
|
||||
|
||||
<section class="content">
|
||||
{% block content %}{% endblock %}
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,4 +0,0 @@
|
|||
{% extends "tastypie/basic.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
|
@ -1,4 +0,0 @@
|
|||
{% extends "tastypie/basic.html" %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
526
tastypie/test.py
526
tastypie/test.py
|
@ -1,526 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.test.client import FakePayload, Client
|
||||
from django.utils.encoding import force_text
|
||||
|
||||
from tastypie.serializers import Serializer
|
||||
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
except ImportError:
|
||||
from urlparse import urlparse
|
||||
|
||||
|
||||
class TestApiClient(object):
|
||||
def __init__(self, serializer=None):
|
||||
"""
|
||||
Sets up a fresh ``TestApiClient`` instance.
|
||||
|
||||
If you are employing a custom serializer, you can pass the class to the
|
||||
``serializer=`` kwarg.
|
||||
"""
|
||||
self.client = Client()
|
||||
self.serializer = serializer
|
||||
|
||||
if not self.serializer:
|
||||
self.serializer = Serializer()
|
||||
|
||||
def get_content_type(self, short_format):
|
||||
"""
|
||||
Given a short name (such as ``json`` or ``xml``), returns the full content-type
|
||||
for it (``application/json`` or ``application/xml`` in this case).
|
||||
"""
|
||||
return self.serializer.content_types.get(short_format, 'json')
|
||||
|
||||
def get(self, uri, format='json', data=None, authentication=None, **kwargs):
|
||||
"""
|
||||
Performs a simulated ``GET`` request to the provided URI.
|
||||
|
||||
Optionally accepts a ``data`` kwarg, which in the case of ``GET``, lets you
|
||||
send along ``GET`` parameters. This is useful when testing filtering or other
|
||||
things that read off the ``GET`` params. Example::
|
||||
|
||||
from tastypie.test import TestApiClient
|
||||
client = TestApiClient()
|
||||
|
||||
response = client.get('/api/v1/entry/1/', data={'format': 'json', 'title__startswith': 'a', 'limit': 20, 'offset': 60})
|
||||
|
||||
Optionally accepts an ``authentication`` kwarg, which should be an HTTP header
|
||||
with the correct authentication data already setup.
|
||||
|
||||
All other ``**kwargs`` passed in get passed through to the Django
|
||||
``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client
|
||||
for details.
|
||||
"""
|
||||
content_type = self.get_content_type(format)
|
||||
kwargs['HTTP_ACCEPT'] = content_type
|
||||
|
||||
# GET & DELETE are the only times we don't serialize the data.
|
||||
if data is not None:
|
||||
kwargs['data'] = data
|
||||
|
||||
if authentication is not None:
|
||||
kwargs['HTTP_AUTHORIZATION'] = authentication
|
||||
|
||||
return self.client.get(uri, **kwargs)
|
||||
|
||||
def post(self, uri, format='json', data=None, authentication=None, **kwargs):
|
||||
"""
|
||||
Performs a simulated ``POST`` request to the provided URI.
|
||||
|
||||
Optionally accepts a ``data`` kwarg. **Unlike** ``GET``, in ``POST`` the
|
||||
``data`` gets serialized & sent as the body instead of becoming part of the URI.
|
||||
Example::
|
||||
|
||||
from tastypie.test import TestApiClient
|
||||
client = TestApiClient()
|
||||
|
||||
response = client.post('/api/v1/entry/', data={
|
||||
'created': '2012-05-01T20:02:36',
|
||||
'slug': 'another-post',
|
||||
'title': 'Another Post',
|
||||
'user': '/api/v1/user/1/',
|
||||
})
|
||||
|
||||
Optionally accepts an ``authentication`` kwarg, which should be an HTTP header
|
||||
with the correct authentication data already setup.
|
||||
|
||||
All other ``**kwargs`` passed in get passed through to the Django
|
||||
``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client
|
||||
for details.
|
||||
"""
|
||||
content_type = self.get_content_type(format)
|
||||
kwargs['content_type'] = content_type
|
||||
|
||||
if data is not None:
|
||||
kwargs['data'] = self.serializer.serialize(data, format=content_type)
|
||||
|
||||
if authentication is not None:
|
||||
kwargs['HTTP_AUTHORIZATION'] = authentication
|
||||
|
||||
return self.client.post(uri, **kwargs)
|
||||
|
||||
def put(self, uri, format='json', data=None, authentication=None, **kwargs):
|
||||
"""
|
||||
Performs a simulated ``PUT`` request to the provided URI.
|
||||
|
||||
Optionally accepts a ``data`` kwarg. **Unlike** ``GET``, in ``PUT`` the
|
||||
``data`` gets serialized & sent as the body instead of becoming part of the URI.
|
||||
Example::
|
||||
|
||||
from tastypie.test import TestApiClient
|
||||
client = TestApiClient()
|
||||
|
||||
response = client.put('/api/v1/entry/1/', data={
|
||||
'created': '2012-05-01T20:02:36',
|
||||
'slug': 'another-post',
|
||||
'title': 'Another Post',
|
||||
'user': '/api/v1/user/1/',
|
||||
})
|
||||
|
||||
Optionally accepts an ``authentication`` kwarg, which should be an HTTP header
|
||||
with the correct authentication data already setup.
|
||||
|
||||
All other ``**kwargs`` passed in get passed through to the Django
|
||||
``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client
|
||||
for details.
|
||||
"""
|
||||
content_type = self.get_content_type(format)
|
||||
kwargs['content_type'] = content_type
|
||||
|
||||
if data is not None:
|
||||
kwargs['data'] = self.serializer.serialize(data, format=content_type)
|
||||
|
||||
if authentication is not None:
|
||||
kwargs['HTTP_AUTHORIZATION'] = authentication
|
||||
|
||||
return self.client.put(uri, **kwargs)
|
||||
|
||||
def patch(self, uri, format='json', data=None, authentication=None, **kwargs):
|
||||
"""
|
||||
Performs a simulated ``PATCH`` request to the provided URI.
|
||||
|
||||
Optionally accepts a ``data`` kwarg. **Unlike** ``GET``, in ``PATCH`` the
|
||||
``data`` gets serialized & sent as the body instead of becoming part of the URI.
|
||||
Example::
|
||||
|
||||
from tastypie.test import TestApiClient
|
||||
client = TestApiClient()
|
||||
|
||||
response = client.patch('/api/v1/entry/1/', data={
|
||||
'created': '2012-05-01T20:02:36',
|
||||
'slug': 'another-post',
|
||||
'title': 'Another Post',
|
||||
'user': '/api/v1/user/1/',
|
||||
})
|
||||
|
||||
Optionally accepts an ``authentication`` kwarg, which should be an HTTP header
|
||||
with the correct authentication data already setup.
|
||||
|
||||
All other ``**kwargs`` passed in get passed through to the Django
|
||||
``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client
|
||||
for details.
|
||||
"""
|
||||
content_type = self.get_content_type(format)
|
||||
kwargs['content_type'] = content_type
|
||||
|
||||
if data is not None:
|
||||
kwargs['data'] = self.serializer.serialize(data, format=content_type)
|
||||
|
||||
if authentication is not None:
|
||||
kwargs['HTTP_AUTHORIZATION'] = authentication
|
||||
|
||||
# This hurts because Django doesn't support PATCH natively.
|
||||
parsed = urlparse(uri)
|
||||
r = {
|
||||
'CONTENT_LENGTH': len(kwargs['data']),
|
||||
'CONTENT_TYPE': content_type,
|
||||
'PATH_INFO': self.client._get_path(parsed),
|
||||
'QUERY_STRING': parsed[4],
|
||||
'REQUEST_METHOD': 'PATCH',
|
||||
'wsgi.input': FakePayload(kwargs['data']),
|
||||
}
|
||||
r.update(kwargs)
|
||||
return self.client.request(**r)
|
||||
|
||||
def delete(self, uri, format='json', data=None, authentication=None, **kwargs):
|
||||
"""
|
||||
Performs a simulated ``DELETE`` request to the provided URI.
|
||||
|
||||
Optionally accepts a ``data`` kwarg, which in the case of ``DELETE``, lets you
|
||||
send along ``DELETE`` parameters. This is useful when testing filtering or other
|
||||
things that read off the ``DELETE`` params. Example::
|
||||
|
||||
from tastypie.test import TestApiClient
|
||||
client = TestApiClient()
|
||||
|
||||
response = client.delete('/api/v1/entry/1/', data={'format': 'json'})
|
||||
|
||||
Optionally accepts an ``authentication`` kwarg, which should be an HTTP header
|
||||
with the correct authentication data already setup.
|
||||
|
||||
All other ``**kwargs`` passed in get passed through to the Django
|
||||
``TestClient``. See https://docs.djangoproject.com/en/dev/topics/testing/#module-django.test.client
|
||||
for details.
|
||||
"""
|
||||
content_type = self.get_content_type(format)
|
||||
kwargs['content_type'] = content_type
|
||||
|
||||
# GET & DELETE are the only times we don't serialize the data.
|
||||
if data is not None:
|
||||
kwargs['data'] = data
|
||||
|
||||
if authentication is not None:
|
||||
kwargs['HTTP_AUTHORIZATION'] = authentication
|
||||
|
||||
return self.client.delete(uri, **kwargs)
|
||||
|
||||
|
||||
class ResourceTestCase(TestCase):
|
||||
"""
|
||||
A useful base class for the start of testing Tastypie APIs.
|
||||
"""
|
||||
def setUp(self):
|
||||
super(ResourceTestCase, self).setUp()
|
||||
self.serializer = Serializer()
|
||||
self.api_client = TestApiClient()
|
||||
|
||||
def get_credentials(self):
|
||||
"""
|
||||
A convenience method for the user as a way to shorten up the
|
||||
often repetitious calls to create the same authentication.
|
||||
|
||||
Raises ``NotImplementedError`` by default.
|
||||
|
||||
Usage::
|
||||
|
||||
class MyResourceTestCase(ResourceTestCase):
|
||||
def get_credentials(self):
|
||||
return self.create_basic('daniel', 'pass')
|
||||
|
||||
# Then the usual tests...
|
||||
|
||||
"""
|
||||
raise NotImplementedError("You must return the class for your Resource to test.")
|
||||
|
||||
def create_basic(self, username, password):
|
||||
"""
|
||||
Creates & returns the HTTP ``Authorization`` header for use with BASIC
|
||||
Auth.
|
||||
"""
|
||||
import base64
|
||||
return 'Basic %s' % base64.b64encode(':'.join([username, password]).encode('utf-8')).decode('utf-8')
|
||||
|
||||
def create_apikey(self, username, api_key):
|
||||
"""
|
||||
Creates & returns the HTTP ``Authorization`` header for use with
|
||||
``ApiKeyAuthentication``.
|
||||
"""
|
||||
return 'ApiKey %s:%s' % (username, api_key)
|
||||
|
||||
def create_digest(self, username, api_key, method, uri):
|
||||
"""
|
||||
Creates & returns the HTTP ``Authorization`` header for use with Digest
|
||||
Auth.
|
||||
"""
|
||||
from tastypie.authentication import hmac, sha1, uuid, python_digest
|
||||
|
||||
new_uuid = uuid.uuid4()
|
||||
opaque = hmac.new(str(new_uuid).encode('utf-8'), digestmod=sha1).hexdigest().decode('utf-8')
|
||||
return python_digest.build_authorization_request(
|
||||
username,
|
||||
method.upper(),
|
||||
uri,
|
||||
1, # nonce_count
|
||||
digest_challenge=python_digest.build_digest_challenge(time.time(), getattr(settings, 'SECRET_KEY', ''), 'django-tastypie', opaque, False),
|
||||
password=api_key
|
||||
)
|
||||
|
||||
def create_oauth(self, user):
|
||||
"""
|
||||
Creates & returns the HTTP ``Authorization`` header for use with Oauth.
|
||||
"""
|
||||
from oauth_provider.models import Consumer, Token, Resource
|
||||
|
||||
# Necessary setup for ``oauth_provider``.
|
||||
resource, _ = Resource.objects.get_or_create(url='test', defaults={
|
||||
'name': 'Test Resource'
|
||||
})
|
||||
consumer, _ = Consumer.objects.get_or_create(key='123', defaults={
|
||||
'name': 'Test',
|
||||
'description': 'Testing...'
|
||||
})
|
||||
token, _ = Token.objects.get_or_create(key='foo', token_type=Token.ACCESS, defaults={
|
||||
'consumer': consumer,
|
||||
'resource': resource,
|
||||
'secret': '',
|
||||
'user': user,
|
||||
})
|
||||
|
||||
# Then generate the header.
|
||||
oauth_data = {
|
||||
'oauth_consumer_key': '123',
|
||||
'oauth_nonce': 'abc',
|
||||
'oauth_signature': '&',
|
||||
'oauth_signature_method': 'PLAINTEXT',
|
||||
'oauth_timestamp': str(int(time.time())),
|
||||
'oauth_token': 'foo',
|
||||
}
|
||||
return 'OAuth %s' % ','.join([key+'='+value for key, value in oauth_data.items()])
|
||||
|
||||
def assertHttpOK(self, resp):
|
||||
"""
|
||||
Ensures the response is returning a HTTP 200.
|
||||
"""
|
||||
return self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def assertHttpCreated(self, resp):
|
||||
"""
|
||||
Ensures the response is returning a HTTP 201.
|
||||
"""
|
||||
return self.assertEqual(resp.status_code, 201)
|
||||
|
||||
def assertHttpAccepted(self, resp):
|
||||
"""
|
||||
Ensures the response is returning either a HTTP 202 or a HTTP 204.
|
||||
"""
|
||||
return self.assertIn(resp.status_code, [202, 204])
|
||||
|
||||
def assertHttpMultipleChoices(self, resp):
|
||||
"""
|
||||
Ensures the response is returning a HTTP 300.
|
||||
"""
|
||||
return self.assertEqual(resp.status_code, 300)
|
||||
|
||||
def assertHttpSeeOther(self, resp):
|
||||
"""
|
||||
Ensures the response is returning a HTTP 303.
|
||||
"""
|
||||
return self.assertEqual(resp.status_code, 303)
|
||||
|
||||
def assertHttpNotModified(self, resp):
|
||||
"""
|
||||
Ensures the response is returning a HTTP 304.
|
||||
"""
|
||||
return self.assertEqual(resp.status_code, 304)
|
||||
|
||||
def assertHttpBadRequest(self, resp):
|
||||
"""
|
||||
Ensures the response is returning a HTTP 400.
|
||||
"""
|
||||
return self.assertEqual(resp.status_code, 400)
|
||||
|
||||
def assertHttpUnauthorized(self, resp):
|
||||
"""
|
||||
Ensures the response is returning a HTTP 401.
|
||||
"""
|
||||
return self.assertEqual(resp.status_code, 401)
|
||||
|
||||
def assertHttpForbidden(self, resp):
|
||||
"""
|
||||
Ensures the response is returning a HTTP 403.
|
||||
"""
|
||||
return self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def assertHttpNotFound(self, resp):
|
||||
"""
|
||||
Ensures the response is returning a HTTP 404.
|
||||
"""
|
||||
return self.assertEqual(resp.status_code, 404)
|
||||
|
||||
def assertHttpMethodNotAllowed(self, resp):
|
||||
"""
|
||||
Ensures the response is returning a HTTP 405.
|
||||
"""
|
||||
return self.assertEqual(resp.status_code, 405)
|
||||
|
||||
def assertHttpConflict(self, resp):
|
||||
"""
|
||||
Ensures the response is returning a HTTP 409.
|
||||
"""
|
||||
return self.assertEqual(resp.status_code, 409)
|
||||
|
||||
def assertHttpGone(self, resp):
|
||||
"""
|
||||
Ensures the response is returning a HTTP 410.
|
||||
"""
|
||||
return self.assertEqual(resp.status_code, 410)
|
||||
|
||||
def assertHttpUnprocessableEntity(self, resp):
|
||||
"""
|
||||
Ensures the response is returning a HTTP 422.
|
||||
"""
|
||||
return self.assertEqual(resp.status_code, 422)
|
||||
|
||||
def assertHttpTooManyRequests(self, resp):
|
||||
"""
|
||||
Ensures the response is returning a HTTP 429.
|
||||
"""
|
||||
return self.assertEqual(resp.status_code, 429)
|
||||
|
||||
def assertHttpApplicationError(self, resp):
|
||||
"""
|
||||
Ensures the response is returning a HTTP 500.
|
||||
"""
|
||||
return self.assertEqual(resp.status_code, 500)
|
||||
|
||||
def assertHttpNotImplemented(self, resp):
|
||||
"""
|
||||
Ensures the response is returning a HTTP 501.
|
||||
"""
|
||||
return self.assertEqual(resp.status_code, 501)
|
||||
|
||||
def assertValidJSON(self, data):
|
||||
"""
|
||||
Given the provided ``data`` as a string, ensures that it is valid JSON &
|
||||
can be loaded properly.
|
||||
"""
|
||||
# Just try the load. If it throws an exception, the test case will fail.
|
||||
self.serializer.from_json(data)
|
||||
|
||||
def assertValidXML(self, data):
|
||||
"""
|
||||
Given the provided ``data`` as a string, ensures that it is valid XML &
|
||||
can be loaded properly.
|
||||
"""
|
||||
# Just try the load. If it throws an exception, the test case will fail.
|
||||
self.serializer.from_xml(data)
|
||||
|
||||
def assertValidYAML(self, data):
|
||||
"""
|
||||
Given the provided ``data`` as a string, ensures that it is valid YAML &
|
||||
can be loaded properly.
|
||||
"""
|
||||
# Just try the load. If it throws an exception, the test case will fail.
|
||||
self.serializer.from_yaml(data)
|
||||
|
||||
def assertValidPlist(self, data):
|
||||
"""
|
||||
Given the provided ``data`` as a string, ensures that it is valid
|
||||
binary plist & can be loaded properly.
|
||||
"""
|
||||
# Just try the load. If it throws an exception, the test case will fail.
|
||||
self.serializer.from_plist(data)
|
||||
|
||||
def assertValidJSONResponse(self, resp):
|
||||
"""
|
||||
Given a ``HttpResponse`` coming back from using the ``client``, assert that
|
||||
you get back:
|
||||
|
||||
* An HTTP 200
|
||||
* The correct content-type (``application/json``)
|
||||
* The content is valid JSON
|
||||
"""
|
||||
self.assertHttpOK(resp)
|
||||
self.assertTrue(resp['Content-Type'].startswith('application/json'))
|
||||
self.assertValidJSON(force_text(resp.content))
|
||||
|
||||
def assertValidXMLResponse(self, resp):
|
||||
"""
|
||||
Given a ``HttpResponse`` coming back from using the ``client``, assert that
|
||||
you get back:
|
||||
|
||||
* An HTTP 200
|
||||
* The correct content-type (``application/xml``)
|
||||
* The content is valid XML
|
||||
"""
|
||||
self.assertHttpOK(resp)
|
||||
self.assertTrue(resp['Content-Type'].startswith('application/xml'))
|
||||
self.assertValidXML(force_text(resp.content))
|
||||
|
||||
def assertValidYAMLResponse(self, resp):
|
||||
"""
|
||||
Given a ``HttpResponse`` coming back from using the ``client``, assert that
|
||||
you get back:
|
||||
|
||||
* An HTTP 200
|
||||
* The correct content-type (``text/yaml``)
|
||||
* The content is valid YAML
|
||||
"""
|
||||
self.assertHttpOK(resp)
|
||||
self.assertTrue(resp['Content-Type'].startswith('text/yaml'))
|
||||
self.assertValidYAML(force_text(resp.content))
|
||||
|
||||
def assertValidPlistResponse(self, resp):
|
||||
"""
|
||||
Given a ``HttpResponse`` coming back from using the ``client``, assert that
|
||||
you get back:
|
||||
|
||||
* An HTTP 200
|
||||
* The correct content-type (``application/x-plist``)
|
||||
* The content is valid binary plist data
|
||||
"""
|
||||
self.assertHttpOK(resp)
|
||||
self.assertTrue(resp['Content-Type'].startswith('application/x-plist'))
|
||||
self.assertValidPlist(force_text(resp.content))
|
||||
|
||||
def deserialize(self, resp):
|
||||
"""
|
||||
Given a ``HttpResponse`` coming back from using the ``client``, this method
|
||||
checks the ``Content-Type`` header & attempts to deserialize the data based on
|
||||
that.
|
||||
|
||||
It returns a Python datastructure (typically a ``dict``) of the serialized data.
|
||||
"""
|
||||
return self.serializer.deserialize(resp.content, format=resp['Content-Type'])
|
||||
|
||||
def serialize(self, data, format='application/json'):
|
||||
"""
|
||||
Given a Python datastructure (typically a ``dict``) & a desired content-type,
|
||||
this method will return a serialized string of that data.
|
||||
"""
|
||||
return self.serializer.serialize(data, format=format)
|
||||
|
||||
def assertKeys(self, data, expected):
|
||||
"""
|
||||
This method ensures that the keys of the ``data`` match up to the keys of
|
||||
``expected``.
|
||||
|
||||
It covers the (extremely) common case where you want to make sure the keys of
|
||||
a response match up to what is expected. This is typically less fragile than
|
||||
testing the full structure, which can be prone to data changes.
|
||||
"""
|
||||
self.assertEqual(sorted(data.keys()), sorted(expected))
|
|
@ -1,130 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
import time
|
||||
from django.core.cache import cache
|
||||
|
||||
|
||||
class BaseThrottle(object):
|
||||
"""
|
||||
A simplified, swappable base class for throttling.
|
||||
|
||||
Does nothing save for simulating the throttling API and implementing
|
||||
some common bits for the subclasses.
|
||||
|
||||
Accepts a number of optional kwargs::
|
||||
|
||||
* ``throttle_at`` - the number of requests at which the user should
|
||||
be throttled. Default is 150 requests.
|
||||
* ``timeframe`` - the length of time (in seconds) in which the user
|
||||
make up to the ``throttle_at`` requests. Default is 3600 seconds (
|
||||
1 hour).
|
||||
* ``expiration`` - the length of time to retain the times the user
|
||||
has accessed the api in the cache. Default is 604800 (1 week).
|
||||
"""
|
||||
def __init__(self, throttle_at=150, timeframe=3600, expiration=None):
|
||||
self.throttle_at = throttle_at
|
||||
# In seconds, please.
|
||||
self.timeframe = timeframe
|
||||
|
||||
if expiration is None:
|
||||
# Expire in a week.
|
||||
expiration = 604800
|
||||
|
||||
self.expiration = int(expiration)
|
||||
|
||||
def convert_identifier_to_key(self, identifier):
|
||||
"""
|
||||
Takes an identifier (like a username or IP address) and converts it
|
||||
into a key usable by the cache system.
|
||||
"""
|
||||
bits = []
|
||||
|
||||
for char in identifier:
|
||||
if char.isalnum() or char in ['_', '.', '-']:
|
||||
bits.append(char)
|
||||
|
||||
safe_string = ''.join(bits)
|
||||
return "%s_accesses" % safe_string
|
||||
|
||||
def should_be_throttled(self, identifier, **kwargs):
|
||||
"""
|
||||
Returns whether or not the user has exceeded their throttle limit.
|
||||
|
||||
Always returns ``False``, as this implementation does not actually
|
||||
throttle the user.
|
||||
"""
|
||||
return False
|
||||
|
||||
def accessed(self, identifier, **kwargs):
|
||||
"""
|
||||
Handles recording the user's access.
|
||||
|
||||
Does nothing in this implementation.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class CacheThrottle(BaseThrottle):
|
||||
"""
|
||||
A throttling mechanism that uses just the cache.
|
||||
"""
|
||||
def should_be_throttled(self, identifier, **kwargs):
|
||||
"""
|
||||
Returns whether or not the user has exceeded their throttle limit.
|
||||
|
||||
Maintains a list of timestamps when the user accessed the api within
|
||||
the cache.
|
||||
|
||||
Returns ``False`` if the user should NOT be throttled or ``True`` if
|
||||
the user should be throttled.
|
||||
"""
|
||||
key = self.convert_identifier_to_key(identifier)
|
||||
|
||||
# Weed out anything older than the timeframe.
|
||||
minimum_time = int(time.time()) - int(self.timeframe)
|
||||
times_accessed = [access for access in cache.get(key, []) if access >= minimum_time]
|
||||
cache.set(key, times_accessed, self.expiration)
|
||||
|
||||
if len(times_accessed) >= int(self.throttle_at):
|
||||
# Throttle them.
|
||||
return True
|
||||
|
||||
# Let them through.
|
||||
return False
|
||||
|
||||
def accessed(self, identifier, **kwargs):
|
||||
"""
|
||||
Handles recording the user's access.
|
||||
|
||||
Stores the current timestamp in the "accesses" list within the cache.
|
||||
"""
|
||||
key = self.convert_identifier_to_key(identifier)
|
||||
times_accessed = cache.get(key, [])
|
||||
times_accessed.append(int(time.time()))
|
||||
cache.set(key, times_accessed, self.expiration)
|
||||
|
||||
|
||||
class CacheDBThrottle(CacheThrottle):
|
||||
"""
|
||||
A throttling mechanism that uses the cache for actual throttling but
|
||||
writes-through to the database.
|
||||
|
||||
This is useful for tracking/aggregating usage through time, to possibly
|
||||
build a statistics interface or a billing mechanism.
|
||||
"""
|
||||
def accessed(self, identifier, **kwargs):
|
||||
"""
|
||||
Handles recording the user's access.
|
||||
|
||||
Does everything the ``CacheThrottle`` class does, plus logs the
|
||||
access within the database using the ``ApiAccess`` model.
|
||||
"""
|
||||
# Do the import here, instead of top-level, so that the model is
|
||||
# only required when using this throttling mechanism.
|
||||
from tastypie.models import ApiAccess
|
||||
super(CacheDBThrottle, self).accessed(identifier, **kwargs)
|
||||
# Write out the access to the DB for logging purposes.
|
||||
ApiAccess.objects.create(
|
||||
identifier=identifier,
|
||||
url=kwargs.get('url', ''),
|
||||
request_method=kwargs.get('request_method', '')
|
||||
)
|
|
@ -1,5 +0,0 @@
|
|||
from tastypie.utils.dict import dict_strip_unicode_keys
|
||||
from tastypie.utils.formatting import mk_datetime, format_datetime, format_date, format_time
|
||||
from tastypie.utils.urls import trailing_slash
|
||||
from tastypie.utils.validate_jsonp import is_valid_jsonp_callback_value
|
||||
from tastypie.utils.timezone import now, make_aware, make_naive, aware_date, aware_datetime
|
|
@ -1,19 +0,0 @@
|
|||
from django.utils.encoding import smart_bytes
|
||||
from django.utils import six
|
||||
|
||||
|
||||
def dict_strip_unicode_keys(uni_dict):
|
||||
"""
|
||||
Converts a dict of unicode keys into a dict of ascii keys.
|
||||
|
||||
Useful for converting a dict to a kwarg-able format.
|
||||
"""
|
||||
if six.PY3:
|
||||
return uni_dict
|
||||
|
||||
data = {}
|
||||
|
||||
for key, value in uni_dict.items():
|
||||
data[smart_bytes(key)] = value
|
||||
|
||||
return data
|
|
@ -1,37 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
import email
|
||||
import datetime
|
||||
import time
|
||||
from django.utils import dateformat
|
||||
from tastypie.utils.timezone import make_aware, make_naive, aware_datetime
|
||||
|
||||
# Try to use dateutil for maximum date-parsing niceness. Fall back to
|
||||
# hard-coded RFC2822 parsing if that's not possible.
|
||||
try:
|
||||
from dateutil.parser import parse as mk_datetime
|
||||
except ImportError:
|
||||
def mk_datetime(string):
|
||||
return make_aware(datetime.datetime.fromtimestamp(time.mktime(email.utils.parsedate(string))))
|
||||
|
||||
def format_datetime(dt):
|
||||
"""
|
||||
RFC 2822 datetime formatter
|
||||
"""
|
||||
return dateformat.format(make_naive(dt), 'r')
|
||||
|
||||
def format_date(d):
|
||||
"""
|
||||
RFC 2822 date formatter
|
||||
"""
|
||||
# workaround because Django's dateformat utility requires a datetime
|
||||
# object (not just date)
|
||||
dt = aware_datetime(d.year, d.month, d.day, 0, 0, 0)
|
||||
return dateformat.format(dt, 'j M Y')
|
||||
|
||||
def format_time(t):
|
||||
"""
|
||||
RFC 2822 time formatter
|
||||
"""
|
||||
# again, workaround dateformat input requirement
|
||||
dt = aware_datetime(2000, 1, 1, t.hour, t.minute, t.second)
|
||||
return dateformat.format(dt, 'H:i:s O')
|
|
@ -1,63 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import mimeparse
|
||||
|
||||
from tastypie.exceptions import BadRequest
|
||||
|
||||
|
||||
def determine_format(request, serializer, default_format='application/json'):
|
||||
"""
|
||||
Tries to "smartly" determine which output format is desired.
|
||||
|
||||
First attempts to find a ``format`` override from the request and supplies
|
||||
that if found.
|
||||
|
||||
If no request format was demanded, it falls back to ``mimeparse`` and the
|
||||
``Accepts`` header, allowing specification that way.
|
||||
|
||||
If still no format is found, returns the ``default_format`` (which defaults
|
||||
to ``application/json`` if not provided).
|
||||
|
||||
NOTE: callers *must* be prepared to handle BadRequest exceptions due to
|
||||
malformed HTTP request headers!
|
||||
"""
|
||||
# First, check if they forced the format.
|
||||
if request.GET.get('format'):
|
||||
if request.GET['format'] in serializer.formats:
|
||||
return serializer.get_mime_for_format(request.GET['format'])
|
||||
|
||||
# If callback parameter is present, use JSONP.
|
||||
if 'callback' in request.GET:
|
||||
return serializer.get_mime_for_format('jsonp')
|
||||
|
||||
# Try to fallback on the Accepts header.
|
||||
if request.META.get('HTTP_ACCEPT', '*/*') != '*/*':
|
||||
formats = list(serializer.supported_formats) or []
|
||||
# Reverse the list, because mimeparse is weird like that. See also
|
||||
# https://github.com/toastdriven/django-tastypie/issues#issue/12 for
|
||||
# more information.
|
||||
formats.reverse()
|
||||
|
||||
try:
|
||||
best_format = mimeparse.best_match(formats, request.META['HTTP_ACCEPT'])
|
||||
except ValueError:
|
||||
raise BadRequest('Invalid Accept header')
|
||||
|
||||
if best_format:
|
||||
return best_format
|
||||
|
||||
# No valid 'Accept' header/formats. Sane default.
|
||||
return default_format
|
||||
|
||||
|
||||
def build_content_type(format, encoding='utf-8'):
|
||||
"""
|
||||
Appends character encoding to the provided format if not already present.
|
||||
"""
|
||||
if 'charset' in format:
|
||||
return format
|
||||
|
||||
if format in ('application/json', 'text/javascript'):
|
||||
return format
|
||||
|
||||
return "%s; charset=%s" % (format, encoding)
|
|
@ -1,37 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
import datetime
|
||||
from django.conf import settings
|
||||
|
||||
try:
|
||||
from django.utils import timezone
|
||||
|
||||
def make_aware(value):
|
||||
if getattr(settings, "USE_TZ", False) and timezone.is_naive(value):
|
||||
default_tz = timezone.get_default_timezone()
|
||||
value = timezone.make_aware(value, default_tz)
|
||||
return value
|
||||
|
||||
def make_naive(value):
|
||||
if getattr(settings, "USE_TZ", False) and timezone.is_aware(value):
|
||||
default_tz = timezone.get_default_timezone()
|
||||
value = timezone.make_naive(value, default_tz)
|
||||
return value
|
||||
|
||||
def now():
|
||||
d = timezone.now()
|
||||
|
||||
if d.tzinfo:
|
||||
return timezone.localtime(timezone.now())
|
||||
|
||||
return d
|
||||
except ImportError:
|
||||
now = datetime.datetime.now
|
||||
make_aware = make_naive = lambda x: x
|
||||
|
||||
|
||||
def aware_date(*args, **kwargs):
|
||||
return make_aware(datetime.date(*args, **kwargs))
|
||||
|
||||
|
||||
def aware_datetime(*args, **kwargs):
|
||||
return make_aware(datetime.datetime(*args, **kwargs))
|
|
@ -1,9 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def trailing_slash():
|
||||
if getattr(settings, 'TASTYPIE_ALLOW_MISSING_SLASH', False):
|
||||
return '/?'
|
||||
|
||||
return '/'
|
|
@ -1,211 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Placed into the Public Domain by tav <tav@espians.com>
|
||||
# Modified for Python 3 compatibility.
|
||||
|
||||
"""Validate Javascript Identifiers for use as JSON-P callback parameters."""
|
||||
from __future__ import unicode_literals
|
||||
import re
|
||||
|
||||
from unicodedata import category
|
||||
|
||||
from django.utils import six
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# javascript identifier unicode categories and "exceptional" chars
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
valid_jsid_categories_start = frozenset([
|
||||
'Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nl'
|
||||
])
|
||||
|
||||
valid_jsid_categories = frozenset([
|
||||
'Lu', 'Ll', 'Lt', 'Lm', 'Lo', 'Nl', 'Mn', 'Mc', 'Nd', 'Pc'
|
||||
])
|
||||
|
||||
valid_jsid_chars = ('$', '_')
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# regex to find array[index] patterns
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
array_index_regex = re.compile(r'\[[0-9]+\]$')
|
||||
|
||||
has_valid_array_index = array_index_regex.search
|
||||
replace_array_index = array_index_regex.sub
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# javascript reserved words -- including keywords and null/boolean literals
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
is_reserved_js_word = frozenset([
|
||||
|
||||
'abstract', 'boolean', 'break', 'byte', 'case', 'catch', 'char', 'class',
|
||||
'const', 'continue', 'debugger', 'default', 'delete', 'do', 'double',
|
||||
'else', 'enum', 'export', 'extends', 'false', 'final', 'finally', 'float',
|
||||
'for', 'function', 'goto', 'if', 'implements', 'import', 'in', 'instanceof',
|
||||
'int', 'interface', 'long', 'native', 'new', 'null', 'package', 'private',
|
||||
'protected', 'public', 'return', 'short', 'static', 'super', 'switch',
|
||||
'synchronized', 'this', 'throw', 'throws', 'transient', 'true', 'try',
|
||||
'typeof', 'var', 'void', 'volatile', 'while', 'with',
|
||||
|
||||
# potentially reserved in a future version of the ES5 standard
|
||||
# 'let', 'yield'
|
||||
|
||||
]).__contains__
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# the core validation functions
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
def is_valid_javascript_identifier(identifier, escape=r'\\u', ucd_cat=category):
|
||||
"""Return whether the given ``id`` is a valid Javascript identifier."""
|
||||
|
||||
if not identifier:
|
||||
return False
|
||||
|
||||
if not isinstance(identifier, six.text_type):
|
||||
try:
|
||||
identifier = six.text_type(identifier, 'utf-8')
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
|
||||
if escape in identifier:
|
||||
|
||||
new = []; add_char = new.append
|
||||
split_id = identifier.split(escape)
|
||||
add_char(split_id.pop(0))
|
||||
|
||||
for segment in split_id:
|
||||
if len(segment) < 4:
|
||||
return False
|
||||
try:
|
||||
add_char(unichr(int('0x' + segment[:4], 16)))
|
||||
except Exception:
|
||||
return False
|
||||
add_char(segment[4:])
|
||||
|
||||
identifier = u''.join(new)
|
||||
|
||||
if is_reserved_js_word(identifier):
|
||||
return False
|
||||
|
||||
first_char = identifier[0]
|
||||
|
||||
if not ((first_char in valid_jsid_chars) or
|
||||
(ucd_cat(first_char) in valid_jsid_categories_start)):
|
||||
return False
|
||||
|
||||
for char in identifier[1:]:
|
||||
if not ((char in valid_jsid_chars) or
|
||||
(ucd_cat(char) in valid_jsid_categories)):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def is_valid_jsonp_callback_value(value):
|
||||
"""Return whether the given ``value`` can be used as a JSON-P callback."""
|
||||
|
||||
for identifier in value.split(u'.'):
|
||||
while '[' in identifier:
|
||||
if not has_valid_array_index(identifier):
|
||||
return False
|
||||
identifier = replace_array_index(u'', identifier)
|
||||
if not is_valid_javascript_identifier(identifier):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# test
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
def test():
|
||||
"""
|
||||
The function ``is_valid_javascript_identifier`` validates a given identifier
|
||||
according to the latest draft of the ECMAScript 5 Specification:
|
||||
|
||||
>>> is_valid_javascript_identifier('hello')
|
||||
True
|
||||
|
||||
>>> is_valid_javascript_identifier('alert()')
|
||||
False
|
||||
|
||||
>>> is_valid_javascript_identifier('a-b')
|
||||
False
|
||||
|
||||
>>> is_valid_javascript_identifier('23foo')
|
||||
False
|
||||
|
||||
>>> is_valid_javascript_identifier('foo23')
|
||||
True
|
||||
|
||||
>>> is_valid_javascript_identifier('$210')
|
||||
True
|
||||
|
||||
>>> is_valid_javascript_identifier(u'Stra\u00dfe')
|
||||
True
|
||||
|
||||
>>> is_valid_javascript_identifier(r'\u0062') # u'b'
|
||||
True
|
||||
|
||||
>>> is_valid_javascript_identifier(r'\u0020')
|
||||
False
|
||||
|
||||
>>> is_valid_javascript_identifier('_bar')
|
||||
True
|
||||
|
||||
>>> is_valid_javascript_identifier('some_var')
|
||||
True
|
||||
|
||||
>>> is_valid_javascript_identifier('$')
|
||||
True
|
||||
|
||||
But ``is_valid_jsonp_callback_value`` is the function you want to use for
|
||||
validating JSON-P callback parameter values:
|
||||
|
||||
>>> is_valid_jsonp_callback_value('somevar')
|
||||
True
|
||||
|
||||
>>> is_valid_jsonp_callback_value('function')
|
||||
False
|
||||
|
||||
>>> is_valid_jsonp_callback_value(' somevar')
|
||||
False
|
||||
|
||||
It supports the possibility of '.' being present in the callback name, e.g.
|
||||
|
||||
>>> is_valid_jsonp_callback_value('$.ajaxHandler')
|
||||
True
|
||||
|
||||
>>> is_valid_jsonp_callback_value('$.23')
|
||||
False
|
||||
|
||||
As well as the pattern of providing an array index lookup, e.g.
|
||||
|
||||
>>> is_valid_jsonp_callback_value('array_of_functions[42]')
|
||||
True
|
||||
|
||||
>>> is_valid_jsonp_callback_value('array_of_functions[42][1]')
|
||||
True
|
||||
|
||||
>>> is_valid_jsonp_callback_value('$.ajaxHandler[42][1].foo')
|
||||
True
|
||||
|
||||
>>> is_valid_jsonp_callback_value('array_of_functions[42]foo[1]')
|
||||
False
|
||||
|
||||
>>> is_valid_jsonp_callback_value('array_of_functions[]')
|
||||
False
|
||||
|
||||
>>> is_valid_jsonp_callback_value('array_of_functions["key"]')
|
||||
False
|
||||
|
||||
Enjoy!
|
||||
|
||||
"""
|
||||
|
||||
if __name__ == '__main__':
|
||||
import doctest
|
||||
doctest.testmod()
|
|
@ -1,110 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.forms import ModelForm
|
||||
from django.forms.models import model_to_dict
|
||||
|
||||
|
||||
class Validation(object):
|
||||
"""
|
||||
A basic validation stub that does no validation.
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
pass
|
||||
|
||||
def is_valid(self, bundle, request=None):
|
||||
"""
|
||||
Performs a check on the data within the bundle (and optionally the
|
||||
request) to ensure it is valid.
|
||||
|
||||
Should return a dictionary of error messages. If the dictionary has
|
||||
zero items, the data is considered valid. If there are errors, keys
|
||||
in the dictionary should be field names and the values should be a list
|
||||
of errors, even if there is only one.
|
||||
"""
|
||||
return {}
|
||||
|
||||
|
||||
class FormValidation(Validation):
|
||||
"""
|
||||
A validation class that uses a Django ``Form`` to validate the data.
|
||||
|
||||
This class **DOES NOT** alter the data sent, only verifies it. If you
|
||||
want to alter the data, please use the ``CleanedDataFormValidation`` class
|
||||
instead.
|
||||
|
||||
This class requires a ``form_class`` argument, which should be a Django
|
||||
``Form`` (or ``ModelForm``, though ``save`` will never be called) class.
|
||||
This form will be used to validate the data in ``bundle.data``.
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
if not 'form_class' in kwargs:
|
||||
raise ImproperlyConfigured("You must provide a 'form_class' to 'FormValidation' classes.")
|
||||
|
||||
self.form_class = kwargs.pop('form_class')
|
||||
super(FormValidation, self).__init__(**kwargs)
|
||||
|
||||
def form_args(self, bundle):
|
||||
data = bundle.data
|
||||
|
||||
# Ensure we get a bound Form, regardless of the state of the bundle.
|
||||
if data is None:
|
||||
data = {}
|
||||
|
||||
kwargs = {'data': {}}
|
||||
|
||||
if hasattr(bundle.obj, 'pk'):
|
||||
if issubclass(self.form_class, ModelForm):
|
||||
kwargs['instance'] = bundle.obj
|
||||
|
||||
kwargs['data'] = model_to_dict(bundle.obj)
|
||||
|
||||
kwargs['data'].update(data)
|
||||
return kwargs
|
||||
|
||||
def is_valid(self, bundle, request=None):
|
||||
"""
|
||||
Performs a check on ``bundle.data``to ensure it is valid.
|
||||
|
||||
If the form is valid, an empty list (all valid) will be returned. If
|
||||
not, a list of errors will be returned.
|
||||
"""
|
||||
|
||||
form = self.form_class(**self.form_args(bundle))
|
||||
|
||||
if form.is_valid():
|
||||
return {}
|
||||
|
||||
# The data is invalid. Let's collect all the error messages & return
|
||||
# them.
|
||||
return form.errors
|
||||
|
||||
|
||||
class CleanedDataFormValidation(FormValidation):
|
||||
"""
|
||||
A validation class that uses a Django ``Form`` to validate the data.
|
||||
|
||||
This class **ALTERS** data sent by the user!!!
|
||||
|
||||
This class requires a ``form_class`` argument, which should be a Django
|
||||
``Form`` (or ``ModelForm``, though ``save`` will never be called) class.
|
||||
This form will be used to validate the data in ``bundle.data``.
|
||||
"""
|
||||
def is_valid(self, bundle, request=None):
|
||||
"""
|
||||
Checks ``bundle.data``to ensure it is valid & replaces it with the
|
||||
cleaned results.
|
||||
|
||||
If the form is valid, an empty list (all valid) will be returned. If
|
||||
not, a list of errors will be returned.
|
||||
"""
|
||||
form = self.form_class(**self.form_args(bundle))
|
||||
|
||||
if form.is_valid():
|
||||
# We're different here & relying on having a reference to the same
|
||||
# bundle the rest of the process is using.
|
||||
bundle.data = form.cleaned_data
|
||||
return {}
|
||||
|
||||
# The data is invalid. Let's collect all the error messages & return
|
||||
# them.
|
||||
return form.errors
|
Loading…
Reference in a new issue