From 4ec326a50507646aa3f1207b7a1f8c04fe6bc5a0 Mon Sep 17 00:00:00 2001 From: Henrik Levkowetz Date: Sun, 26 Apr 2015 19:24:02 +0000 Subject: [PATCH] Removed local copy of tastypie - Legacy-Id: 9562 --- tastypie/__init__.py | 5 - tastypie/admin.py | 20 - tastypie/api.py | 184 -- tastypie/authentication.py | 523 ---- tastypie/authorization.py | 245 -- tastypie/bundle.py | 33 - tastypie/cache.py | 98 - tastypie/compat.py | 25 - tastypie/constants.py | 6 - tastypie/contrib/__init__.py | 0 tastypie/contrib/contenttypes/__init__.py | 0 tastypie/contrib/contenttypes/fields.py | 55 - tastypie/contrib/contenttypes/resources.py | 42 - tastypie/contrib/gis/__init__.py | 0 tastypie/contrib/gis/resources.py | 77 - tastypie/exceptions.py | 101 - tastypie/fields.py | 907 ------ tastypie/http.py | 80 - tastypie/management/__init__.py | 0 tastypie/management/commands/__init__.py | 0 .../management/commands/backfill_api_keys.py | 30 - tastypie/migrations/0001_initial.py | 42 - tastypie/migrations/__init__.py | 0 tastypie/models.py | 63 - tastypie/paginator.py | 209 -- tastypie/resources.py | 2449 ----------------- tastypie/serializers.py | 519 ---- tastypie/south_migrations/0001_initial.py | 97 - .../south_migrations/0002_add_apikey_index.py | 76 - tastypie/south_migrations/__init__.py | 0 tastypie/templates/tastypie/basic.html | 29 - tastypie/templates/tastypie/detail.html | 4 - tastypie/templates/tastypie/list.html | 4 - tastypie/test.py | 526 ---- tastypie/throttle.py | 130 - tastypie/utils/__init__.py | 5 - tastypie/utils/dict.py | 19 - tastypie/utils/formatting.py | 37 - tastypie/utils/mime.py | 63 - tastypie/utils/timezone.py | 37 - tastypie/utils/urls.py | 9 - tastypie/utils/validate_jsonp.py | 211 -- tastypie/validation.py | 110 - 43 files changed, 7070 deletions(-) delete mode 100644 tastypie/__init__.py delete mode 100644 tastypie/admin.py delete mode 100644 tastypie/api.py delete mode 100644 tastypie/authentication.py delete mode 100644 tastypie/authorization.py delete mode 100644 tastypie/bundle.py delete mode 100644 tastypie/cache.py delete mode 100644 tastypie/compat.py delete mode 100644 tastypie/constants.py delete mode 100644 tastypie/contrib/__init__.py delete mode 100644 tastypie/contrib/contenttypes/__init__.py delete mode 100644 tastypie/contrib/contenttypes/fields.py delete mode 100644 tastypie/contrib/contenttypes/resources.py delete mode 100644 tastypie/contrib/gis/__init__.py delete mode 100644 tastypie/contrib/gis/resources.py delete mode 100644 tastypie/exceptions.py delete mode 100644 tastypie/fields.py delete mode 100644 tastypie/http.py delete mode 100644 tastypie/management/__init__.py delete mode 100644 tastypie/management/commands/__init__.py delete mode 100644 tastypie/management/commands/backfill_api_keys.py delete mode 100644 tastypie/migrations/0001_initial.py delete mode 100644 tastypie/migrations/__init__.py delete mode 100644 tastypie/models.py delete mode 100644 tastypie/paginator.py delete mode 100644 tastypie/resources.py delete mode 100644 tastypie/serializers.py delete mode 100644 tastypie/south_migrations/0001_initial.py delete mode 100644 tastypie/south_migrations/0002_add_apikey_index.py delete mode 100644 tastypie/south_migrations/__init__.py delete mode 100644 tastypie/templates/tastypie/basic.html delete mode 100644 tastypie/templates/tastypie/detail.html delete mode 100644 tastypie/templates/tastypie/list.html delete mode 100644 tastypie/test.py delete mode 100644 tastypie/throttle.py delete mode 100644 tastypie/utils/__init__.py delete mode 100644 tastypie/utils/dict.py delete mode 100644 tastypie/utils/formatting.py delete mode 100644 tastypie/utils/mime.py delete mode 100644 tastypie/utils/timezone.py delete mode 100644 tastypie/utils/urls.py delete mode 100644 tastypie/utils/validate_jsonp.py delete mode 100644 tastypie/validation.py diff --git a/tastypie/__init__.py b/tastypie/__init__.py deleted file mode 100644 index 370ca7714..000000000 --- a/tastypie/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from __future__ import unicode_literals - - -__author__ = 'Daniel Lindsley & the Tastypie core team' -__version__ = (0, 12, 1) diff --git a/tastypie/admin.py b/tastypie/admin.py deleted file mode 100644 index 87677051d..000000000 --- a/tastypie/admin.py +++ /dev/null @@ -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) diff --git a/tastypie/api.py b/tastypie/api.py deleted file mode 100644 index d8788afb9..000000000 --- a/tastypie/api.py +++ /dev/null @@ -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%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%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) diff --git a/tastypie/authentication.py b/tastypie/authentication.py deleted file mode 100644 index cd529d2e9..000000000 --- a/tastypie/authentication.py +++ /dev/null @@ -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' diff --git a/tastypie/authorization.py b/tastypie/authorization.py deleted file mode 100644 index 7a4c647ec..000000000 --- a/tastypie/authorization.py +++ /dev/null @@ -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 diff --git a/tastypie/bundle.py b/tastypie/bundle.py deleted file mode 100644 index 6adc8fef1..000000000 --- a/tastypie/bundle.py +++ /dev/null @@ -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 "" % (self.obj, self.data) diff --git a/tastypie/cache.py b/tastypie/cache.py deleted file mode 100644 index 22bfd4397..000000000 --- a/tastypie/cache.py +++ /dev/null @@ -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 diff --git a/tastypie/compat.py b/tastypie/compat.py deleted file mode 100644 index 4191bc545..000000000 --- a/tastypie/compat.py +++ /dev/null @@ -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' diff --git a/tastypie/constants.py b/tastypie/constants.py deleted file mode 100644 index 89f7fb79d..000000000 --- a/tastypie/constants.py +++ /dev/null @@ -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 diff --git a/tastypie/contrib/__init__.py b/tastypie/contrib/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tastypie/contrib/contenttypes/__init__.py b/tastypie/contrib/contenttypes/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tastypie/contrib/contenttypes/fields.py b/tastypie/contrib/contenttypes/fields.py deleted file mode 100644 index c27419024..000000000 --- a/tastypie/contrib/contenttypes/fields.py +++ /dev/null @@ -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) diff --git a/tastypie/contrib/contenttypes/resources.py b/tastypie/contrib/contenttypes/resources.py deleted file mode 100644 index aa70ca6da..000000000 --- a/tastypie/contrib/contenttypes/resources.py +++ /dev/null @@ -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) diff --git a/tastypie/contrib/gis/__init__.py b/tastypie/contrib/gis/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tastypie/contrib/gis/resources.py b/tastypie/contrib/gis/resources.py deleted file mode 100644 index da96969e7..000000000 --- a/tastypie/contrib/gis/resources.py +++ /dev/null @@ -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 diff --git a/tastypie/exceptions.py b/tastypie/exceptions.py deleted file mode 100644 index fdd90cc8b..000000000 --- a/tastypie/exceptions.py +++ /dev/null @@ -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 diff --git a/tastypie/fields.py b/tastypie/fields.py deleted file mode 100644 index 7ae752e51..000000000 --- a/tastypie/fields.py +++ /dev/null @@ -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\d{4})-(?P\d{2})-(?P\d{2}).*?$') -DATETIME_REGEX = re.compile('^(?P\d{4})-(?P\d{2})-(?P\d{2})(T|\s+)(?P\d{2}):(?P\d{2}):(?P\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 () 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 diff --git a/tastypie/http.py b/tastypie/http.py deleted file mode 100644 index 3486c14b2..000000000 --- a/tastypie/http.py +++ /dev/null @@ -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 - diff --git a/tastypie/management/__init__.py b/tastypie/management/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tastypie/management/commands/__init__.py b/tastypie/management/commands/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tastypie/management/commands/backfill_api_keys.py b/tastypie/management/commands/backfill_api_keys.py deleted file mode 100644 index 633c4a410..000000000 --- a/tastypie/management/commands/backfill_api_keys.py +++ /dev/null @@ -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) diff --git a/tastypie/migrations/0001_initial.py b/tastypie/migrations/0001_initial.py deleted file mode 100644 index d06753162..000000000 --- a/tastypie/migrations/0001_initial.py +++ /dev/null @@ -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,), - ), - ] diff --git a/tastypie/migrations/__init__.py b/tastypie/migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tastypie/models.py b/tastypie/models.py deleted file mode 100644 index d8ceec808..000000000 --- a/tastypie/models.py +++ /dev/null @@ -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')) diff --git a/tastypie/paginator.py b/tastypie/paginator.py deleted file mode 100644 index 09fc5937e..000000000 --- a/tastypie/paginator.py +++ /dev/null @@ -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, - } diff --git a/tastypie/resources.py b/tastypie/resources.py deleted file mode 100644 index fbdfea200..000000000 --- a/tastypie/resources.py +++ /dev/null @@ -1,2449 +0,0 @@ -from __future__ import unicode_literals -from __future__ import with_statement -from copy import deepcopy -import logging -import warnings - -from django.conf import settings -from django.conf.urls import patterns, url -from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned, ValidationError -from django.core.urlresolvers import NoReverseMatch, reverse, resolve, Resolver404, get_script_prefix -from django.core.signals import got_request_exception -from django.db import transaction -from django.db.models.constants import LOOKUP_SEP -from django.db.models.sql.constants import QUERY_TERMS -from django.http import HttpResponse, HttpResponseNotFound, Http404 -from django.utils.cache import patch_cache_control, patch_vary_headers -from django.utils.html import escape -from django.utils import six - -from tastypie.authentication import Authentication -from tastypie.authorization import ReadOnlyAuthorization -from tastypie.bundle import Bundle -from tastypie.cache import NoCache -from tastypie.constants import ALL, ALL_WITH_RELATIONS -from tastypie.exceptions import NotFound, BadRequest, InvalidFilterError, HydrationError, InvalidSortError, ImmediateHttpResponse, Unauthorized -from tastypie import fields -from tastypie import http -from tastypie.paginator import Paginator -from tastypie.serializers import Serializer -from tastypie.throttle import BaseThrottle -from tastypie.utils import is_valid_jsonp_callback_value, dict_strip_unicode_keys, trailing_slash -from tastypie.utils.mime import determine_format, build_content_type -from tastypie.validation import Validation - -# If ``csrf_exempt`` isn't present, stub it. -try: - from django.views.decorators.csrf import csrf_exempt -except ImportError: - def csrf_exempt(func): - return func - - -def sanitize(text): - # We put the single quotes back, due to their frequent usage in exception - # messages. - return escape(text).replace(''', "'").replace('"', '"') - - -class NOT_AVAILABLE: - def __str__(self): - return 'No such data is available.' - - -class ResourceOptions(object): - """ - A configuration class for ``Resource``. - - Provides sane defaults and the logic needed to augment these settings with - the internal ``class Meta`` used on ``Resource`` subclasses. - """ - serializer = Serializer() - authentication = Authentication() - authorization = ReadOnlyAuthorization() - cache = NoCache() - throttle = BaseThrottle() - validation = Validation() - paginator_class = Paginator - allowed_methods = ['get', 'post', 'put', 'delete', 'patch'] - list_allowed_methods = None - detail_allowed_methods = None - limit = getattr(settings, 'API_LIMIT_PER_PAGE', 20) - max_limit = 1000 - api_name = None - resource_name = None - urlconf_namespace = None - default_format = 'application/json' - filtering = {} - ordering = [] - object_class = None - queryset = None - fields = [] - excludes = [] - include_resource_uri = True - include_absolute_url = False - always_return_data = False - collection_name = 'objects' - detail_uri_name = 'pk' - - def __new__(cls, meta=None): - overrides = {} - - # Handle overrides. - if meta: - for override_name in dir(meta): - # No internals please. - if not override_name.startswith('_'): - overrides[override_name] = getattr(meta, override_name) - - allowed_methods = overrides.get('allowed_methods', ['get', 'post', 'put', 'delete', 'patch']) - - if overrides.get('list_allowed_methods', None) is None: - overrides['list_allowed_methods'] = allowed_methods - - if overrides.get('detail_allowed_methods', None) is None: - overrides['detail_allowed_methods'] = allowed_methods - - if six.PY3: - return object.__new__(type('ResourceOptions', (cls,), overrides)) - else: - return object.__new__(type(b'ResourceOptions', (cls,), overrides)) - - -class DeclarativeMetaclass(type): - def __new__(cls, name, bases, attrs): - attrs['base_fields'] = {} - declared_fields = {} - - # Inherit any fields from parent(s). - try: - parents = [b for b in bases if issubclass(b, Resource)] - # Simulate the MRO. - parents.reverse() - - for p in parents: - parent_fields = getattr(p, 'base_fields', {}) - - for field_name, field_object in parent_fields.items(): - attrs['base_fields'][field_name] = deepcopy(field_object) - except NameError: - pass - - for field_name, obj in attrs.copy().items(): - # Look for ``dehydrated_type`` instead of doing ``isinstance``, - # which can break down if Tastypie is re-namespaced as something - # else. - if hasattr(obj, 'dehydrated_type'): - field = attrs.pop(field_name) - declared_fields[field_name] = field - - attrs['base_fields'].update(declared_fields) - attrs['declared_fields'] = declared_fields - new_class = super(DeclarativeMetaclass, cls).__new__(cls, name, bases, attrs) - opts = getattr(new_class, 'Meta', None) - new_class._meta = ResourceOptions(opts) - - if not getattr(new_class._meta, 'resource_name', None): - # No ``resource_name`` provided. Attempt to auto-name the resource. - class_name = new_class.__name__ - name_bits = [bit for bit in class_name.split('Resource') if bit] - resource_name = ''.join(name_bits).lower() - new_class._meta.resource_name = resource_name - - if getattr(new_class._meta, 'include_resource_uri', True): - if not 'resource_uri' in new_class.base_fields: - new_class.base_fields['resource_uri'] = fields.CharField(readonly=True) - elif 'resource_uri' in new_class.base_fields and not 'resource_uri' in attrs: - del(new_class.base_fields['resource_uri']) - - for field_name, field_object in new_class.base_fields.items(): - if hasattr(field_object, 'contribute_to_class'): - field_object.contribute_to_class(new_class, field_name) - - return new_class - - -class Resource(six.with_metaclass(DeclarativeMetaclass)): - """ - Handles the data, request dispatch and responding to requests. - - Serialization/deserialization is handled "at the edges" (i.e. at the - beginning/end of the request/response cycle) so that everything internally - is Python data structures. - - This class tries to be non-model specific, so it can be hooked up to other - data sources, such as search results, files, other data, etc. - """ - def __init__(self, api_name=None): - self.fields = deepcopy(self.base_fields) - - if not api_name is None: - self._meta.api_name = api_name - - def __getattr__(self, name): - if name in self.fields: - return self.fields[name] - raise AttributeError(name) - - def wrap_view(self, view): - """ - Wraps methods so they can be called in a more functional way as well - as handling exceptions better. - - Note that if ``BadRequest`` or an exception with a ``response`` attr - are seen, there is special handling to either present a message back - to the user or return the response traveling with the exception. - """ - @csrf_exempt - def wrapper(request, *args, **kwargs): - try: - callback = getattr(self, view) - response = callback(request, *args, **kwargs) - - # Our response can vary based on a number of factors, use - # the cache class to determine what we should ``Vary`` on so - # caches won't return the wrong (cached) version. - varies = getattr(self._meta.cache, "varies", []) - - if varies: - patch_vary_headers(response, varies) - - if self._meta.cache.cacheable(request, response): - if self._meta.cache.cache_control(): - # If the request is cacheable and we have a - # ``Cache-Control`` available then patch the header. - patch_cache_control(response, **self._meta.cache.cache_control()) - - if request.is_ajax() and not response.has_header("Cache-Control"): - # IE excessively caches XMLHttpRequests, so we're disabling - # the browser cache here. - # See http://www.enhanceie.com/ie/bugs.asp for details. - patch_cache_control(response, no_cache=True) - - return response - except (BadRequest, fields.ApiFieldError) as e: - data = {"error": sanitize(e.args[0]) if getattr(e, 'args') else ''} - return self.error_response(request, data, response_class=http.HttpBadRequest) - except ValidationError as e: - data = {"error": sanitize(e.messages)} - return self.error_response(request, data, response_class=http.HttpBadRequest) - except Exception as e: - if hasattr(e, 'response'): - return e.response - - # A real, non-expected exception. - # Handle the case where the full traceback is more helpful - # than the serialized error. - if settings.DEBUG and getattr(settings, 'TASTYPIE_FULL_DEBUG', False): - raise - - # Re-raise the error to get a proper traceback when the error - # happend during a test case - if request.META.get('SERVER_NAME') == 'testserver': - raise - - # Rather than re-raising, we're going to things similar to - # what Django does. The difference is returning a serialized - # error message. - return self._handle_500(request, e) - - return wrapper - - def _handle_500(self, request, exception): - import traceback - import sys - the_trace = '\n'.join(traceback.format_exception(*(sys.exc_info()))) - response_class = http.HttpApplicationError - response_code = 500 - - NOT_FOUND_EXCEPTIONS = (NotFound, ObjectDoesNotExist, Http404) - - if isinstance(exception, NOT_FOUND_EXCEPTIONS): - response_class = HttpResponseNotFound - response_code = 404 - - if settings.DEBUG: - data = { - "error_message": sanitize(six.text_type(exception)), - "traceback": the_trace, - } - return self.error_response(request, data, response_class=response_class) - - # When DEBUG is False, send an error message to the admins (unless it's - # a 404, in which case we check the setting). - send_broken_links = getattr(settings, 'SEND_BROKEN_LINK_EMAILS', False) - - if not response_code == 404 or send_broken_links: - log = logging.getLogger('django.request.tastypie') - log.error('Internal Server Error: %s' % request.path, exc_info=True, - extra={'status_code': response_code, 'request': request}) - - # Send the signal so other apps are aware of the exception. - got_request_exception.send(self.__class__, request=request) - - # Prep the data going out. - data = { - "error_message": getattr(settings, 'TASTYPIE_CANNED_ERROR', "Sorry, this request could not be processed. Please try again later."), - } - return self.error_response(request, data, response_class=response_class) - - def _build_reverse_url(self, name, args=None, kwargs=None): - """ - A convenience hook for overriding how URLs are built. - - See ``NamespacedModelResource._build_reverse_url`` for an example. - """ - return reverse(name, args=args, kwargs=kwargs) - - def base_urls(self): - """ - The standard URLs this ``Resource`` should respond to. - """ - return [ - url(r"^(?P%s)%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('dispatch_list'), name="api_dispatch_list"), - url(r"^(?P%s)/schema%s$" % (self._meta.resource_name, trailing_slash()), self.wrap_view('get_schema'), name="api_get_schema"), - url(r"^(?P%s)/set/(?P<%s_list>.*?)%s$" % (self._meta.resource_name, self._meta.detail_uri_name, trailing_slash()), self.wrap_view('get_multiple'), name="api_get_multiple"), - url(r"^(?P%s)/(?P<%s>.*?)%s$" % (self._meta.resource_name, self._meta.detail_uri_name, trailing_slash()), self.wrap_view('dispatch_detail'), name="api_dispatch_detail"), - ] - - 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): - """ - The endpoints this ``Resource`` responds to. - - Mostly a standard URLconf, this is suitable for either automatic use - when registered with an ``Api`` class or for including directly in - a URLconf should you choose to. - """ - urls = 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``.") - urls += overridden_urls - - urls += self.base_urls() - urlpatterns = patterns('', - *urls - ) - return urlpatterns - - def determine_format(self, request): - """ - Used to determine the desired format. - - Largely relies on ``tastypie.utils.mime.determine_format`` but here - as a point of extension. - """ - return determine_format(request, self._meta.serializer, default_format=self._meta.default_format) - - def serialize(self, request, data, format, options=None): - """ - Given a request, data and a desired format, produces a serialized - version suitable for transfer over the wire. - - Mostly a hook, this uses the ``Serializer`` from ``Resource._meta``. - """ - options = options or {} - - if 'text/javascript' in format: - # get JSONP callback name. default to "callback" - callback = request.GET.get('callback', 'callback') - - if not is_valid_jsonp_callback_value(callback): - raise BadRequest('JSONP callback name is invalid.') - - options['callback'] = callback - - return self._meta.serializer.serialize(data, format, options) - - def deserialize(self, request, data, format='application/json'): - """ - Given a request, data and a format, deserializes the given data. - - It relies on the request properly sending a ``CONTENT_TYPE`` header, - falling back to ``application/json`` if not provided. - - Mostly a hook, this uses the ``Serializer`` from ``Resource._meta``. - """ - deserialized = self._meta.serializer.deserialize(data, format=request.META.get('CONTENT_TYPE', 'application/json')) - return deserialized - - def alter_list_data_to_serialize(self, request, data): - """ - A hook to alter list data just before it gets serialized & sent to the user. - - Useful for restructuring/renaming aspects of the what's going to be - sent. - - Should accommodate for a list of objects, generally also including - meta data. - """ - return data - - def alter_detail_data_to_serialize(self, request, data): - """ - A hook to alter detail data just before it gets serialized & sent to the user. - - Useful for restructuring/renaming aspects of the what's going to be - sent. - - Should accommodate for receiving a single bundle of data. - """ - return data - - def alter_deserialized_list_data(self, request, data): - """ - A hook to alter list data just after it has been received from the user & - gets deserialized. - - Useful for altering the user data before any hydration is applied. - """ - return data - - def alter_deserialized_detail_data(self, request, data): - """ - A hook to alter detail data just after it has been received from the user & - gets deserialized. - - Useful for altering the user data before any hydration is applied. - """ - return data - - def dispatch_list(self, request, **kwargs): - """ - A view for handling the various HTTP methods (GET/POST/PUT/DELETE) over - the entire list of resources. - - Relies on ``Resource.dispatch`` for the heavy-lifting. - """ - return self.dispatch('list', request, **kwargs) - - def dispatch_detail(self, request, **kwargs): - """ - A view for handling the various HTTP methods (GET/POST/PUT/DELETE) on - a single resource. - - Relies on ``Resource.dispatch`` for the heavy-lifting. - """ - return self.dispatch('detail', request, **kwargs) - - def dispatch(self, request_type, request, **kwargs): - """ - Handles the common operations (allowed HTTP method, authentication, - throttling, method lookup) surrounding most CRUD interactions. - """ - allowed_methods = getattr(self._meta, "%s_allowed_methods" % request_type, None) - - if 'HTTP_X_HTTP_METHOD_OVERRIDE' in request.META: - request.method = request.META['HTTP_X_HTTP_METHOD_OVERRIDE'] - - request_method = self.method_check(request, allowed=allowed_methods) - method = getattr(self, "%s_%s" % (request_method, request_type), None) - - if method is None: - raise ImmediateHttpResponse(response=http.HttpNotImplemented()) - - self.is_authenticated(request) - self.throttle_check(request) - - # All clear. Process the request. - request = convert_post_to_put(request) - response = method(request, **kwargs) - - # Add the throttled request. - self.log_throttled_access(request) - - # If what comes back isn't a ``HttpResponse``, assume that the - # request was accepted and that some action occurred. This also - # prevents Django from freaking out. - if not isinstance(response, HttpResponse): - return http.HttpNoContent() - - return response - - def remove_api_resource_names(self, url_dict): - """ - Given a dictionary of regex matches from a URLconf, removes - ``api_name`` and/or ``resource_name`` if found. - - This is useful for converting URLconf matches into something suitable - for data lookup. For example:: - - Model.objects.filter(**self.remove_api_resource_names(matches)) - """ - kwargs_subset = url_dict.copy() - - for key in ['api_name', 'resource_name']: - try: - del(kwargs_subset[key]) - except KeyError: - pass - - return kwargs_subset - - def method_check(self, request, allowed=None): - """ - Ensures that the HTTP method used on the request is allowed to be - handled by the resource. - - Takes an ``allowed`` parameter, which should be a list of lowercase - HTTP methods to check against. Usually, this looks like:: - - # The most generic lookup. - self.method_check(request, self._meta.allowed_methods) - - # A lookup against what's allowed for list-type methods. - self.method_check(request, self._meta.list_allowed_methods) - - # A useful check when creating a new endpoint that only handles - # GET. - self.method_check(request, ['get']) - """ - if allowed is None: - allowed = [] - - request_method = request.method.lower() - allows = ','.join([meth.upper() for meth in allowed]) - - if request_method == "options": - response = HttpResponse(allows) - response['Allow'] = allows - raise ImmediateHttpResponse(response=response) - - if not request_method in allowed: - response = http.HttpMethodNotAllowed(allows) - response['Allow'] = allows - raise ImmediateHttpResponse(response=response) - - return request_method - - def is_authenticated(self, request): - """ - Handles checking if the user is authenticated and dealing with - unauthenticated users. - - Mostly a hook, this uses class assigned to ``authentication`` from - ``Resource._meta``. - """ - # Authenticate the request as needed. - auth_result = self._meta.authentication.is_authenticated(request) - - if isinstance(auth_result, HttpResponse): - raise ImmediateHttpResponse(response=auth_result) - - if not auth_result is True: - raise ImmediateHttpResponse(response=http.HttpUnauthorized()) - - def throttle_check(self, request): - """ - Handles checking if the user should be throttled. - - Mostly a hook, this uses class assigned to ``throttle`` from - ``Resource._meta``. - """ - identifier = self._meta.authentication.get_identifier(request) - - # Check to see if they should be throttled. - if self._meta.throttle.should_be_throttled(identifier): - # Throttle limit exceeded. - raise ImmediateHttpResponse(response=http.HttpTooManyRequests()) - - def log_throttled_access(self, request): - """ - Handles the recording of the user's access for throttling purposes. - - Mostly a hook, this uses class assigned to ``throttle`` from - ``Resource._meta``. - """ - request_method = request.method.lower() - self._meta.throttle.accessed(self._meta.authentication.get_identifier(request), url=request.get_full_path(), request_method=request_method) - - def unauthorized_result(self, exception): - raise ImmediateHttpResponse(response=http.HttpUnauthorized()) - - def authorized_read_list(self, object_list, bundle): - """ - Handles checking of permissions to see if the user has authorization - to GET this resource. - """ - try: - auth_result = self._meta.authorization.read_list(object_list, bundle) - except Unauthorized as e: - self.unauthorized_result(e) - - return auth_result - - def authorized_read_detail(self, object_list, bundle): - """ - Handles checking of permissions to see if the user has authorization - to GET this resource. - """ - try: - auth_result = self._meta.authorization.read_detail(object_list, bundle) - if not auth_result is True: - raise Unauthorized() - except Unauthorized as e: - self.unauthorized_result(e) - - return auth_result - - def authorized_create_list(self, object_list, bundle): - """ - Handles checking of permissions to see if the user has authorization - to POST this resource. - """ - try: - auth_result = self._meta.authorization.create_list(object_list, bundle) - except Unauthorized as e: - self.unauthorized_result(e) - - return auth_result - - def authorized_create_detail(self, object_list, bundle): - """ - Handles checking of permissions to see if the user has authorization - to POST this resource. - """ - try: - auth_result = self._meta.authorization.create_detail(object_list, bundle) - if not auth_result is True: - raise Unauthorized() - except Unauthorized as e: - self.unauthorized_result(e) - - return auth_result - - def authorized_update_list(self, object_list, bundle): - """ - Handles checking of permissions to see if the user has authorization - to PUT this resource. - """ - try: - auth_result = self._meta.authorization.update_list(object_list, bundle) - except Unauthorized as e: - self.unauthorized_result(e) - - return auth_result - - def authorized_update_detail(self, object_list, bundle): - """ - Handles checking of permissions to see if the user has authorization - to PUT this resource. - """ - try: - auth_result = self._meta.authorization.update_detail(object_list, bundle) - if not auth_result is True: - raise Unauthorized() - except Unauthorized as e: - self.unauthorized_result(e) - - return auth_result - - def authorized_delete_list(self, object_list, bundle): - """ - Handles checking of permissions to see if the user has authorization - to DELETE this resource. - """ - try: - auth_result = self._meta.authorization.delete_list(object_list, bundle) - except Unauthorized as e: - self.unauthorized_result(e) - - return auth_result - - def authorized_delete_detail(self, object_list, bundle): - """ - Handles checking of permissions to see if the user has authorization - to DELETE this resource. - """ - try: - auth_result = self._meta.authorization.delete_detail(object_list, bundle) - if not auth_result: - raise Unauthorized() - except Unauthorized as e: - self.unauthorized_result(e) - - return auth_result - - def build_bundle(self, obj=None, data=None, request=None, objects_saved=None): - """ - Given either an object, a data dictionary or both, builds a ``Bundle`` - for use throughout the ``dehydrate/hydrate`` cycle. - - If no object is provided, an empty object from - ``Resource._meta.object_class`` is created so that attempts to access - ``bundle.obj`` do not fail. - """ - if obj is None and self._meta.object_class: - obj = self._meta.object_class() - - return Bundle( - obj=obj, - data=data, - request=request, - objects_saved=objects_saved - ) - - def build_filters(self, filters=None): - """ - Allows for the filtering of applicable objects. - - This needs to be implemented at the user level.' - - ``ModelResource`` includes a full working version specific to Django's - ``Models``. - """ - return filters - - def apply_sorting(self, obj_list, options=None): - """ - Allows for the sorting of objects being returned. - - This needs to be implemented at the user level. - - ``ModelResource`` includes a full working version specific to Django's - ``Models``. - """ - return obj_list - - def get_bundle_detail_data(self, bundle): - """ - Convenience method to return the ``detail_uri_name`` attribute off - ``bundle.obj``. - - Usually just accesses ``bundle.obj.pk`` by default. - """ - return getattr(bundle.obj, self._meta.detail_uri_name) - - # URL-related methods. - - def detail_uri_kwargs(self, bundle_or_obj): - """ - This needs to be implemented at the user level. - - Given a ``Bundle`` or an object, it returns the extra kwargs needed to - generate a detail URI. - - ``ModelResource`` includes a full working version specific to Django's - ``Models``. - """ - raise NotImplementedError() - - def resource_uri_kwargs(self, bundle_or_obj=None): - """ - Builds a dictionary of kwargs to help generate URIs. - - Automatically provides the ``Resource.Meta.resource_name`` (and - optionally the ``Resource.Meta.api_name`` if populated by an ``Api`` - object). - - If the ``bundle_or_obj`` argument is provided, it calls - ``Resource.detail_uri_kwargs`` for additional bits to create - """ - kwargs = { - 'resource_name': self._meta.resource_name, - } - - if self._meta.api_name is not None: - kwargs['api_name'] = self._meta.api_name - - if bundle_or_obj is not None: - kwargs.update(self.detail_uri_kwargs(bundle_or_obj)) - - return kwargs - - def get_resource_uri(self, bundle_or_obj=None, url_name='api_dispatch_list'): - """ - Handles generating a resource URI. - - If the ``bundle_or_obj`` argument is not provided, it builds the URI - for the list endpoint. - - If the ``bundle_or_obj`` argument is provided, it builds the URI for - the detail endpoint. - - Return the generated URI. If that URI can not be reversed (not found - in the URLconf), it will return an empty string. - """ - if bundle_or_obj is not None: - url_name = 'api_dispatch_detail' - - try: - return self._build_reverse_url(url_name, kwargs=self.resource_uri_kwargs(bundle_or_obj)) - except NoReverseMatch: - return '' - - 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:] - - # We mangle the path a bit further & run URL resolution against *only* - # the current class. This ought to prevent bad URLs from resolving to - # incorrect data. - found_at = chomped_uri.rfind(self._meta.resource_name) - if found_at == -1: - raise NotFound("An incorrect URL was provided '%s' for the '%s' resource." % (uri, self.__class__.__name__)) - chomped_uri = chomped_uri[found_at:] - try: - for url_resolver in getattr(self, 'urls', []): - result = url_resolver.resolve(chomped_uri) - - if result is not None: - view, args, kwargs = result - break - else: - raise Resolver404("URI not found in 'self.urls'.") - except Resolver404: - raise NotFound("The URL provided '%s' was not a link to a valid resource." % uri) - - bundle = self.build_bundle(request=request) - return self.obj_get(bundle=bundle, **self.remove_api_resource_names(kwargs)) - - # Data preparation. - - def full_dehydrate(self, bundle, for_list=False): - """ - Given a bundle with an object instance, extract the information from it - to populate the resource. - """ - use_in = ['all', 'list' if for_list else 'detail'] - - # Dehydrate each field. - for field_name, field_object in self.fields.items(): - # If it's not for use in this mode, skip - field_use_in = getattr(field_object, 'use_in', 'all') - if callable(field_use_in): - if not field_use_in(bundle): - continue - else: - if field_use_in not in use_in: - continue - - # A touch leaky but it makes URI resolution work. - if getattr(field_object, 'dehydrated_type', None) == 'related': - field_object.api_name = self._meta.api_name - field_object.resource_name = self._meta.resource_name - - bundle.data[field_name] = field_object.dehydrate(bundle, for_list=for_list) - - # Check for an optional method to do further dehydration. - method = getattr(self, "dehydrate_%s" % field_name, None) - - if method: - bundle.data[field_name] = method(bundle) - - bundle = self.dehydrate(bundle) - return bundle - - def dehydrate(self, bundle): - """ - A hook to allow a final manipulation of data once all fields/methods - have built out the dehydrated data. - - Useful if you need to access more than one dehydrated field or want - to annotate on additional data. - - Must return the modified bundle. - """ - return bundle - - def full_hydrate(self, bundle): - """ - Given a populated bundle, distill it and turn it back into - a full-fledged object instance. - """ - if bundle.obj is None: - bundle.obj = self._meta.object_class() - - bundle = self.hydrate(bundle) - - for field_name, field_object in self.fields.items(): - if field_object.readonly is True: - continue - - # Check for an optional method to do further hydration. - method = getattr(self, "hydrate_%s" % field_name, None) - - if method: - bundle = method(bundle) - - if field_object.attribute: - value = field_object.hydrate(bundle) - - # NOTE: We only get back a bundle when it is related field. - if isinstance(value, Bundle) and value.errors.get(field_name): - bundle.errors[field_name] = value.errors[field_name] - - if value is not None or field_object.null: - # We need to avoid populating M2M data here as that will - # cause things to blow up. - if not getattr(field_object, 'is_related', False): - setattr(bundle.obj, field_object.attribute, value) - elif not getattr(field_object, 'is_m2m', False): - if value is not None: - # NOTE: A bug fix in Django (ticket #18153) fixes incorrect behavior - # which Tastypie was relying on. To fix this, we store value.obj to - # be saved later in save_related. - try: - setattr(bundle.obj, field_object.attribute, value.obj) - except (ValueError, ObjectDoesNotExist): - bundle.related_objects_to_save[field_object.attribute] = value.obj - elif field_object.blank: - continue - elif field_object.null: - setattr(bundle.obj, field_object.attribute, value) - - return bundle - - def hydrate(self, bundle): - """ - A hook to allow an initial manipulation of data before all methods/fields - have built out the hydrated data. - - Useful if you need to access more than one hydrated field or want - to annotate on additional data. - - Must return the modified bundle. - """ - return bundle - - def hydrate_m2m(self, bundle): - """ - Populate the ManyToMany data on the instance. - """ - if bundle.obj is None: - raise HydrationError("You must call 'full_hydrate' before attempting to run 'hydrate_m2m' on %r." % self) - - for field_name, field_object in self.fields.items(): - if not getattr(field_object, 'is_m2m', False): - continue - - if field_object.attribute: - # Note that we only hydrate the data, leaving the instance - # unmodified. It's up to the user's code to handle this. - # The ``ModelResource`` provides a working baseline - # in this regard. - bundle.data[field_name] = field_object.hydrate_m2m(bundle) - - for field_name, field_object in self.fields.items(): - if not getattr(field_object, 'is_m2m', False): - continue - - method = getattr(self, "hydrate_%s" % field_name, None) - - if method: - method(bundle) - - return bundle - - def build_schema(self): - """ - Returns a dictionary of all the fields on the resource and some - properties about those fields. - - Used by the ``schema/`` endpoint to describe what will be available. - """ - data = { - 'fields': {}, - 'default_format': self._meta.default_format, - 'allowed_list_http_methods': self._meta.list_allowed_methods, - 'allowed_detail_http_methods': self._meta.detail_allowed_methods, - 'default_limit': self._meta.limit, - } - - if self._meta.ordering: - data['ordering'] = self._meta.ordering - - if self._meta.filtering: - data['filtering'] = self._meta.filtering - - for field_name, field_object in self.fields.items(): - data['fields'][field_name] = { - 'default': field_object.default, - 'type': field_object.dehydrated_type, - 'nullable': field_object.null, - 'blank': field_object.blank, - 'readonly': field_object.readonly, - 'help_text': field_object.help_text, - 'unique': field_object.unique, - } - if field_object.dehydrated_type == 'related': - if getattr(field_object, 'is_m2m', False): - related_type = 'to_many' - else: - related_type = 'to_one' - data['fields'][field_name]['related_type'] = related_type - - return data - - def dehydrate_resource_uri(self, bundle): - """ - For the automatically included ``resource_uri`` field, dehydrate - the URI for the given bundle. - - Returns empty string if no URI can be generated. - """ - try: - return self.get_resource_uri(bundle) - except NotImplementedError: - return '' - except NoReverseMatch: - return '' - - def generate_cache_key(self, *args, **kwargs): - """ - Creates a unique-enough cache key. - - This is based off the current api_name/resource_name/args/kwargs. - """ - smooshed = [] - - for key, value in kwargs.items(): - smooshed.append("%s=%s" % (key, value)) - - # Use a list plus a ``.join()`` because it's faster than concatenation. - return "%s:%s:%s:%s" % (self._meta.api_name, self._meta.resource_name, ':'.join(args), ':'.join(sorted(smooshed))) - - # Data access methods. - - def get_object_list(self, request): - """ - A hook to allow making returning the list of available objects. - - This needs to be implemented at the user level. - - ``ModelResource`` includes a full working version specific to Django's - ``Models``. - """ - raise NotImplementedError() - - def apply_authorization_limits(self, request, object_list): - """ - Deprecated. - - FIXME: REMOVE BEFORE 1.0 - """ - return self._meta.authorization.apply_limits(request, object_list) - - def can_create(self): - """ - Checks to ensure ``post`` is within ``allowed_methods``. - """ - allowed = set(self._meta.list_allowed_methods + self._meta.detail_allowed_methods) - return 'post' in allowed - - def can_update(self): - """ - Checks to ensure ``put`` is within ``allowed_methods``. - - Used when hydrating related data. - """ - allowed = set(self._meta.list_allowed_methods + self._meta.detail_allowed_methods) - return 'put' in allowed - - def can_delete(self): - """ - Checks to ensure ``delete`` is within ``allowed_methods``. - """ - allowed = set(self._meta.list_allowed_methods + self._meta.detail_allowed_methods) - return 'delete' in allowed - - def apply_filters(self, request, applicable_filters): - """ - A hook to alter how the filters are applied to the object list. - - This needs to be implemented at the user level. - - ``ModelResource`` includes a full working version specific to Django's - ``Models``. - """ - raise NotImplementedError() - - def obj_get_list(self, bundle, **kwargs): - """ - Fetches the list of objects available on the resource. - - This needs to be implemented at the user level. - - ``ModelResource`` includes a full working version specific to Django's - ``Models``. - """ - raise NotImplementedError() - - def cached_obj_get_list(self, bundle, **kwargs): - """ - A version of ``obj_get_list`` that uses the cache as a means to get - commonly-accessed data faster. - """ - cache_key = self.generate_cache_key('list', **kwargs) - obj_list = self._meta.cache.get(cache_key) - - if obj_list is None: - obj_list = self.obj_get_list(bundle=bundle, **kwargs) - self._meta.cache.set(cache_key, obj_list) - - return obj_list - - def obj_get(self, bundle, **kwargs): - """ - Fetches an individual object on the resource. - - This needs to be implemented at the user level. If the object can not - be found, this should raise a ``NotFound`` exception. - - ``ModelResource`` includes a full working version specific to Django's - ``Models``. - """ - raise NotImplementedError() - - def cached_obj_get(self, bundle, **kwargs): - """ - A version of ``obj_get`` that uses the cache as a means to get - commonly-accessed data faster. - """ - cache_key = self.generate_cache_key('detail', **kwargs) - cached_bundle = self._meta.cache.get(cache_key) - - if cached_bundle is None: - cached_bundle = self.obj_get(bundle=bundle, **kwargs) - self._meta.cache.set(cache_key, cached_bundle) - - return cached_bundle - - def obj_create(self, bundle, **kwargs): - """ - Creates a new object based on the provided data. - - This needs to be implemented at the user level. - - ``ModelResource`` includes a full working version specific to Django's - ``Models``. - """ - raise NotImplementedError() - - def obj_update(self, bundle, **kwargs): - """ - Updates an existing object (or creates a new object) based on the - provided data. - - This needs to be implemented at the user level. - - ``ModelResource`` includes a full working version specific to Django's - ``Models``. - """ - raise NotImplementedError() - - def obj_delete_list(self, bundle, **kwargs): - """ - Deletes an entire list of objects. - - This needs to be implemented at the user level. - - ``ModelResource`` includes a full working version specific to Django's - ``Models``. - """ - raise NotImplementedError() - - def obj_delete_list_for_update(self, bundle, **kwargs): - """ - Deletes an entire list of objects, specific to PUT list. - - This needs to be implemented at the user level. - - ``ModelResource`` includes a full working version specific to Django's - ``Models``. - """ - raise NotImplementedError() - - def obj_delete(self, bundle, **kwargs): - """ - Deletes a single object. - - This needs to be implemented at the user level. - - ``ModelResource`` includes a full working version specific to Django's - ``Models``. - """ - raise NotImplementedError() - - def create_response(self, request, data, response_class=HttpResponse, **response_kwargs): - """ - Extracts the common "which-format/serialize/return-response" cycle. - - Mostly a useful shortcut/hook. - """ - desired_format = self.determine_format(request) - serialized = self.serialize(request, data, desired_format) - return response_class(content=serialized, content_type=build_content_type(desired_format), **response_kwargs) - - def error_response(self, request, errors, response_class=None): - """ - Extracts the common "which-format/serialize/return-error-response" - cycle. - - Should be used as much as possible to return errors. - """ - if response_class is None: - response_class = http.HttpBadRequest - - desired_format = None - - if request: - if request.GET.get('callback', None) is None: - try: - desired_format = self.determine_format(request) - except BadRequest: - pass # Fall through to default handler below - else: - # JSONP can cause extra breakage. - desired_format = 'application/json' - - if not desired_format: - desired_format = self._meta.default_format - - try: - serialized = self.serialize(request, errors, desired_format) - except BadRequest as e: - error = "Additional errors occurred, but serialization of those errors failed." - - if settings.DEBUG: - error += " %s" % e - - return response_class(content=error, content_type='text/plain') - - return response_class(content=serialized, content_type=build_content_type(desired_format)) - - def is_valid(self, bundle): - """ - Handles checking if the data provided by the user is valid. - - Mostly a hook, this uses class assigned to ``validation`` from - ``Resource._meta``. - - If validation fails, an error is raised with the error messages - serialized inside it. - """ - errors = self._meta.validation.is_valid(bundle, bundle.request) - - if errors: - bundle.errors[self._meta.resource_name] = errors - return False - - return True - - def rollback(self, bundles): - """ - Given the list of bundles, delete all objects pertaining to those - bundles. - - This needs to be implemented at the user level. No exceptions should - be raised if possible. - - ``ModelResource`` includes a full working version specific to Django's - ``Models``. - """ - raise NotImplementedError() - - # Views. - - def get_list(self, request, **kwargs): - """ - Returns a serialized list of resources. - - Calls ``obj_get_list`` to provide the data, then handles that result - set and serializes it. - - Should return a HttpResponse (200 OK). - """ - # TODO: Uncached for now. Invalidation that works for everyone may be - # impossible. - base_bundle = self.build_bundle(request=request) - objects = self.obj_get_list(bundle=base_bundle, **self.remove_api_resource_names(kwargs)) - sorted_objects = self.apply_sorting(objects, options=request.GET) - - paginator = self._meta.paginator_class(request.GET, sorted_objects, resource_uri=self.get_resource_uri(), limit=self._meta.limit, max_limit=self._meta.max_limit, collection_name=self._meta.collection_name) - to_be_serialized = paginator.page() - - # Dehydrate the bundles in preparation for serialization. - bundles = [] - - for obj in to_be_serialized[self._meta.collection_name]: - bundle = self.build_bundle(obj=obj, request=request) - bundles.append(self.full_dehydrate(bundle, for_list=True)) - - to_be_serialized[self._meta.collection_name] = bundles - to_be_serialized = self.alter_list_data_to_serialize(request, to_be_serialized) - return self.create_response(request, to_be_serialized) - - def get_detail(self, request, **kwargs): - """ - Returns a single serialized resource. - - Calls ``cached_obj_get/obj_get`` to provide the data, then handles that result - set and serializes it. - - Should return a HttpResponse (200 OK). - """ - basic_bundle = self.build_bundle(request=request) - - try: - obj = self.cached_obj_get(bundle=basic_bundle, **self.remove_api_resource_names(kwargs)) - except ObjectDoesNotExist: - return http.HttpNotFound() - except MultipleObjectsReturned: - return http.HttpMultipleChoices("More than one resource is found at this URI.") - - bundle = self.build_bundle(obj=obj, request=request) - bundle = self.full_dehydrate(bundle) - bundle = self.alter_detail_data_to_serialize(request, bundle) - return self.create_response(request, bundle) - - def post_list(self, request, **kwargs): - """ - Creates a new resource/object with the provided data. - - Calls ``obj_create`` with the provided data and returns a response - with the new resource's location. - - If a new resource is created, return ``HttpCreated`` (201 Created). - If ``Meta.always_return_data = True``, there will be a populated body - of serialized data. - """ - deserialized = self.deserialize(request, request.body, format=request.META.get('CONTENT_TYPE', 'application/json')) - deserialized = self.alter_deserialized_detail_data(request, deserialized) - bundle = self.build_bundle(data=dict_strip_unicode_keys(deserialized), request=request) - updated_bundle = self.obj_create(bundle, **self.remove_api_resource_names(kwargs)) - location = self.get_resource_uri(updated_bundle) - - if not self._meta.always_return_data: - return http.HttpCreated(location=location) - else: - updated_bundle = self.full_dehydrate(updated_bundle) - updated_bundle = self.alter_detail_data_to_serialize(request, updated_bundle) - return self.create_response(request, updated_bundle, response_class=http.HttpCreated, location=location) - - def post_detail(self, request, **kwargs): - """ - Creates a new subcollection of the resource under a resource. - - This is not implemented by default because most people's data models - aren't self-referential. - - If a new resource is created, return ``HttpCreated`` (201 Created). - """ - return http.HttpNotImplemented() - - def put_list(self, request, **kwargs): - """ - Replaces a collection of resources with another collection. - - Calls ``delete_list`` to clear out the collection then ``obj_create`` - with the provided the data to create the new collection. - - Return ``HttpNoContent`` (204 No Content) if - ``Meta.always_return_data = False`` (default). - - Return ``HttpAccepted`` (200 OK) if - ``Meta.always_return_data = True``. - """ - deserialized = self.deserialize(request, request.body, format=request.META.get('CONTENT_TYPE', 'application/json')) - deserialized = self.alter_deserialized_list_data(request, deserialized) - - if not self._meta.collection_name in deserialized: - raise BadRequest("Invalid data sent.") - - basic_bundle = self.build_bundle(request=request) - self.obj_delete_list_for_update(bundle=basic_bundle, **self.remove_api_resource_names(kwargs)) - bundles_seen = [] - - for object_data in deserialized[self._meta.collection_name]: - bundle = self.build_bundle(data=dict_strip_unicode_keys(object_data), request=request) - - # Attempt to be transactional, deleting any previously created - # objects if validation fails. - try: - self.obj_create(bundle=bundle, **self.remove_api_resource_names(kwargs)) - bundles_seen.append(bundle) - except ImmediateHttpResponse: - self.rollback(bundles_seen) - raise - - if not self._meta.always_return_data: - return http.HttpNoContent() - else: - to_be_serialized = {} - to_be_serialized[self._meta.collection_name] = [self.full_dehydrate(bundle, for_list=True) for bundle in bundles_seen] - to_be_serialized = self.alter_list_data_to_serialize(request, to_be_serialized) - return self.create_response(request, to_be_serialized) - - def put_detail(self, request, **kwargs): - """ - Either updates an existing resource or creates a new one with the - provided data. - - Calls ``obj_update`` with the provided data first, but falls back to - ``obj_create`` if the object does not already exist. - - If a new resource is created, return ``HttpCreated`` (201 Created). - If ``Meta.always_return_data = True``, there will be a populated body - of serialized data. - - If an existing resource is modified and - ``Meta.always_return_data = False`` (default), return ``HttpNoContent`` - (204 No Content). - If an existing resource is modified and - ``Meta.always_return_data = True``, return ``HttpAccepted`` (200 - OK). - """ - deserialized = self.deserialize(request, request.body, format=request.META.get('CONTENT_TYPE', 'application/json')) - deserialized = self.alter_deserialized_detail_data(request, deserialized) - bundle = self.build_bundle(data=dict_strip_unicode_keys(deserialized), request=request) - - try: - updated_bundle = self.obj_update(bundle=bundle, **self.remove_api_resource_names(kwargs)) - - if not self._meta.always_return_data: - return http.HttpNoContent() - else: - updated_bundle = self.full_dehydrate(updated_bundle) - updated_bundle = self.alter_detail_data_to_serialize(request, updated_bundle) - return self.create_response(request, updated_bundle) - except (NotFound, MultipleObjectsReturned): - updated_bundle = self.obj_create(bundle=bundle, **self.remove_api_resource_names(kwargs)) - location = self.get_resource_uri(updated_bundle) - - if not self._meta.always_return_data: - return http.HttpCreated(location=location) - else: - updated_bundle = self.full_dehydrate(updated_bundle) - updated_bundle = self.alter_detail_data_to_serialize(request, updated_bundle) - return self.create_response(request, updated_bundle, response_class=http.HttpCreated, location=location) - - def delete_list(self, request, **kwargs): - """ - Destroys a collection of resources/objects. - - Calls ``obj_delete_list``. - - If the resources are deleted, return ``HttpNoContent`` (204 No Content). - """ - bundle = self.build_bundle(request=request) - self.obj_delete_list(bundle=bundle, request=request, **self.remove_api_resource_names(kwargs)) - return http.HttpNoContent() - - def delete_detail(self, request, **kwargs): - """ - Destroys a single resource/object. - - Calls ``obj_delete``. - - If the resource is deleted, return ``HttpNoContent`` (204 No Content). - If the resource did not exist, return ``Http404`` (404 Not Found). - """ - # Manually construct the bundle here, since we don't want to try to - # delete an empty instance. - bundle = Bundle(request=request) - - try: - self.obj_delete(bundle=bundle, **self.remove_api_resource_names(kwargs)) - return http.HttpNoContent() - except NotFound: - return http.HttpNotFound() - - def patch_list(self, request, **kwargs): - """ - Updates a collection in-place. - - The exact behavior of ``PATCH`` to a list resource is still the matter of - some debate in REST circles, and the ``PATCH`` RFC isn't standard. So the - behavior this method implements (described below) is something of a - stab in the dark. It's mostly cribbed from GData, with a smattering - of ActiveResource-isms and maybe even an original idea or two. - - The ``PATCH`` format is one that's similar to the response returned from - a ``GET`` on a list resource:: - - { - "objects": [{object}, {object}, ...], - "deleted_objects": ["URI", "URI", "URI", ...], - } - - For each object in ``objects``: - - * If the dict does not have a ``resource_uri`` key then the item is - considered "new" and is handled like a ``POST`` to the resource list. - - * If the dict has a ``resource_uri`` key and the ``resource_uri`` refers - to an existing resource then the item is a update; it's treated - like a ``PATCH`` to the corresponding resource detail. - - * If the dict has a ``resource_uri`` but the resource *doesn't* exist, - then this is considered to be a create-via-``PUT``. - - Each entry in ``deleted_objects`` referes to a resource URI of an existing - resource to be deleted; each is handled like a ``DELETE`` to the relevent - resource. - - In any case: - - * If there's a resource URI it *must* refer to a resource of this - type. It's an error to include a URI of a different resource. - - * ``PATCH`` is all or nothing. If a single sub-operation fails, the - entire request will fail and all resources will be rolled back. - - * For ``PATCH`` to work, you **must** have ``put`` in your - :ref:`detail-allowed-methods` setting. - - * To delete objects via ``deleted_objects`` in a ``PATCH`` request you - **must** have ``delete`` in your :ref:`detail-allowed-methods` - setting. - - Substitute appropriate names for ``objects`` and - ``deleted_objects`` if ``Meta.collection_name`` is set to something - other than ``objects`` (default). - """ - request = convert_post_to_patch(request) - deserialized = self.deserialize(request, request.body, format=request.META.get('CONTENT_TYPE', 'application/json')) - - collection_name = self._meta.collection_name - deleted_collection_name = 'deleted_%s' % collection_name - if collection_name not in deserialized: - raise BadRequest("Invalid data sent: missing '%s'" % collection_name) - - if len(deserialized[collection_name]) and 'put' not in self._meta.detail_allowed_methods: - raise ImmediateHttpResponse(response=http.HttpMethodNotAllowed()) - - bundles_seen = [] - - for data in deserialized[collection_name]: - # If there's a resource_uri then this is either an - # update-in-place or a create-via-PUT. - if "resource_uri" in data: - uri = data.pop('resource_uri') - - try: - obj = self.get_via_uri(uri, request=request) - - # The object does exist, so this is an update-in-place. - bundle = self.build_bundle(obj=obj, request=request) - bundle = self.full_dehydrate(bundle, for_list=True) - bundle = self.alter_detail_data_to_serialize(request, bundle) - self.update_in_place(request, bundle, data) - except (ObjectDoesNotExist, MultipleObjectsReturned): - # The object referenced by resource_uri doesn't exist, - # so this is a create-by-PUT equivalent. - data = self.alter_deserialized_detail_data(request, data) - bundle = self.build_bundle(data=dict_strip_unicode_keys(data), request=request) - self.obj_create(bundle=bundle) - else: - # There's no resource URI, so this is a create call just - # like a POST to the list resource. - data = self.alter_deserialized_detail_data(request, data) - bundle = self.build_bundle(data=dict_strip_unicode_keys(data), request=request) - self.obj_create(bundle=bundle) - - bundles_seen.append(bundle) - - deleted_collection = deserialized.get(deleted_collection_name, []) - - if deleted_collection: - if 'delete' not in self._meta.detail_allowed_methods: - raise ImmediateHttpResponse(response=http.HttpMethodNotAllowed()) - - for uri in deleted_collection: - obj = self.get_via_uri(uri, request=request) - bundle = self.build_bundle(obj=obj, request=request) - self.obj_delete(bundle=bundle) - - if not self._meta.always_return_data: - return http.HttpAccepted() - else: - to_be_serialized = {} - to_be_serialized['objects'] = [self.full_dehydrate(bundle, for_list=True) for bundle in bundles_seen] - to_be_serialized = self.alter_list_data_to_serialize(request, to_be_serialized) - return self.create_response(request, to_be_serialized, response_class=http.HttpAccepted) - - def patch_detail(self, request, **kwargs): - """ - Updates a resource in-place. - - Calls ``obj_update``. - - If the resource is updated, return ``HttpAccepted`` (202 Accepted). - If the resource did not exist, return ``HttpNotFound`` (404 Not Found). - """ - request = convert_post_to_patch(request) - basic_bundle = self.build_bundle(request=request) - - # We want to be able to validate the update, but we can't just pass - # the partial data into the validator since all data needs to be - # present. Instead, we basically simulate a PUT by pulling out the - # original data and updating it in-place. - # So first pull out the original object. This is essentially - # ``get_detail``. - try: - obj = self.cached_obj_get(bundle=basic_bundle, **self.remove_api_resource_names(kwargs)) - except ObjectDoesNotExist: - return http.HttpNotFound() - except MultipleObjectsReturned: - return http.HttpMultipleChoices("More than one resource is found at this URI.") - - bundle = self.build_bundle(obj=obj, request=request) - bundle = self.full_dehydrate(bundle) - bundle = self.alter_detail_data_to_serialize(request, bundle) - - # Now update the bundle in-place. - deserialized = self.deserialize(request, request.body, format=request.META.get('CONTENT_TYPE', 'application/json')) - self.update_in_place(request, bundle, deserialized) - - if not self._meta.always_return_data: - return http.HttpAccepted() - else: - bundle = self.full_dehydrate(bundle) - bundle = self.alter_detail_data_to_serialize(request, bundle) - return self.create_response(request, bundle, response_class=http.HttpAccepted) - - def update_in_place(self, request, original_bundle, new_data): - """ - Update the object in original_bundle in-place using new_data. - """ - original_bundle.data.update(**dict_strip_unicode_keys(new_data)) - - # Now we've got a bundle with the new data sitting in it and we're - # we're basically in the same spot as a PUT request. SO the rest of this - # function is cribbed from put_detail. - self.alter_deserialized_detail_data(request, original_bundle.data) - kwargs = { - self._meta.detail_uri_name: self.get_bundle_detail_data(original_bundle), - 'request': request, - } - return self.obj_update(bundle=original_bundle, **kwargs) - - def get_schema(self, request, **kwargs): - """ - Returns a serialized form of the schema of the resource. - - Calls ``build_schema`` to generate the data. This method only responds - to HTTP GET. - - Should return a HttpResponse (200 OK). - """ - self.method_check(request, allowed=['get']) - self.is_authenticated(request) - self.throttle_check(request) - self.log_throttled_access(request) - bundle = self.build_bundle(request=request) - self.authorized_read_detail(self.get_object_list(bundle.request), bundle) - return self.create_response(request, self.build_schema()) - - def get_multiple(self, request, **kwargs): - """ - Returns a serialized list of resources based on the identifiers - from the URL. - - Calls ``obj_get`` to fetch only the objects requested. This method - only responds to HTTP GET. - - Should return a HttpResponse (200 OK). - """ - self.method_check(request, allowed=['get']) - self.is_authenticated(request) - self.throttle_check(request) - - # Rip apart the list then iterate. - kwarg_name = '%s_list' % self._meta.detail_uri_name - obj_identifiers = kwargs.get(kwarg_name, '').split(';') - objects = [] - not_found = [] - base_bundle = self.build_bundle(request=request) - - for identifier in obj_identifiers: - try: - obj = self.obj_get(bundle=base_bundle, **{self._meta.detail_uri_name: identifier}) - bundle = self.build_bundle(obj=obj, request=request) - bundle = self.full_dehydrate(bundle, for_list=True) - objects.append(bundle) - except (ObjectDoesNotExist, Unauthorized): - not_found.append(identifier) - - object_list = { - self._meta.collection_name: objects, - } - - if len(not_found): - object_list['not_found'] = not_found - - self.log_throttled_access(request) - return self.create_response(request, object_list) - - -class ModelDeclarativeMetaclass(DeclarativeMetaclass): - def __new__(cls, name, bases, attrs): - meta = attrs.get('Meta') - - if meta and hasattr(meta, 'queryset'): - setattr(meta, 'object_class', meta.queryset.model) - - new_class = super(ModelDeclarativeMetaclass, cls).__new__(cls, name, bases, attrs) - include_fields = getattr(new_class._meta, 'fields', []) - excludes = getattr(new_class._meta, 'excludes', []) - field_names = list(new_class.base_fields.keys()) - - for field_name in field_names: - if field_name == 'resource_uri': - continue - if field_name in new_class.declared_fields: - continue - if len(include_fields) and not field_name in include_fields: - del(new_class.base_fields[field_name]) - if len(excludes) and field_name in excludes: - del(new_class.base_fields[field_name]) - - # Add in the new fields. - new_class.base_fields.update(new_class.get_fields(include_fields, excludes)) - - if getattr(new_class._meta, 'include_absolute_url', True): - if not 'absolute_url' in new_class.base_fields: - new_class.base_fields['absolute_url'] = fields.CharField(attribute='get_absolute_url', readonly=True) - elif 'absolute_url' in new_class.base_fields and not 'absolute_url' in attrs: - del(new_class.base_fields['absolute_url']) - - return new_class - - -class BaseModelResource(Resource): - """ - A subclass of ``Resource`` designed to work with Django's ``Models``. - - This class will introspect a given ``Model`` and build a field list based - on the fields found on the model (excluding relational fields). - - Given that it is aware of Django's ORM, it also handles the CRUD data - operations of the resource. - """ - @classmethod - def should_skip_field(cls, field): - """ - Given a Django model field, return if it should be included in the - contributed ApiFields. - """ - # Ignore certain fields (related fields). - if getattr(field, 'rel'): - return True - - return False - - @classmethod - def api_field_from_django_field(cls, f, default=fields.CharField): - """ - Returns the field type that would likely be associated with each - Django type. - """ - result = default - internal_type = f.get_internal_type() - - if internal_type in ('DateField', 'DateTimeField'): - result = fields.DateTimeField - elif internal_type in ('BooleanField', 'NullBooleanField'): - result = fields.BooleanField - elif internal_type in ('FloatField',): - result = fields.FloatField - elif internal_type in ('DecimalField',): - result = fields.DecimalField - elif internal_type in ('IntegerField', 'PositiveIntegerField', 'PositiveSmallIntegerField', 'SmallIntegerField', 'AutoField'): - result = fields.IntegerField - elif internal_type in ('FileField', 'ImageField'): - result = fields.FileField - elif internal_type == 'TimeField': - result = fields.TimeField - # TODO: Perhaps enable these via introspection. The reason they're not enabled - # by default is the very different ``__init__`` they have over - # the other fields. - # elif internal_type == 'ForeignKey': - # result = ForeignKey - # elif internal_type == 'ManyToManyField': - # result = ManyToManyField - - return result - - @classmethod - def get_fields(cls, fields=None, excludes=None): - """ - Given any explicit fields to include and fields to exclude, add - additional fields based on the associated model. - """ - final_fields = {} - fields = fields or [] - excludes = excludes or [] - - if not cls._meta.object_class: - return final_fields - - for f in cls._meta.object_class._meta.fields: - # If the field name is already present, skip - if f.name in cls.base_fields: - continue - - # If field is not present in explicit field listing, skip - if fields and f.name not in fields: - continue - - # If field is in exclude list, skip - if excludes and f.name in excludes: - continue - - if cls.should_skip_field(f): - continue - - api_field_class = cls.api_field_from_django_field(f) - - kwargs = { - 'attribute': f.name, - 'help_text': f.help_text, - } - - if f.null is True: - kwargs['null'] = True - - kwargs['unique'] = f.unique - - if not f.null and f.blank is True: - kwargs['default'] = '' - kwargs['blank'] = True - - if f.get_internal_type() == 'TextField': - kwargs['default'] = '' - - if f.has_default(): - kwargs['default'] = f.default - - if getattr(f, 'auto_now', False): - kwargs['default'] = f.auto_now - - if getattr(f, 'auto_now_add', False): - kwargs['default'] = f.auto_now_add - - final_fields[f.name] = api_field_class(**kwargs) - final_fields[f.name].instance_name = f.name - - return final_fields - - def check_filtering(self, field_name, filter_type='exact', filter_bits=None): - """ - Given a field name, a optional filter type and an optional list of - additional relations, determine if a field can be filtered on. - - If a filter does not meet the needed conditions, it should raise an - ``InvalidFilterError``. - - If the filter meets the conditions, a list of attribute names (not - field names) will be returned. - """ - if filter_bits is None: - filter_bits = [] - - if not field_name in self._meta.filtering: - raise InvalidFilterError("The '%s' field does not allow filtering." % field_name) - - # Check to see if it's an allowed lookup type. - if not self._meta.filtering[field_name] in (ALL, ALL_WITH_RELATIONS): - # Must be an explicit whitelist. - if not filter_type in self._meta.filtering[field_name]: - raise InvalidFilterError("'%s' is not an allowed filter on the '%s' field." % (filter_type, field_name)) - - if self.fields[field_name].attribute is None: - raise InvalidFilterError("The '%s' field has no 'attribute' for searching with." % field_name) - - # Check to see if it's a relational lookup and if that's allowed. - if len(filter_bits): - if not getattr(self.fields[field_name], 'is_related', False): - raise InvalidFilterError("The '%s' field does not support relations." % field_name) - - if not self._meta.filtering[field_name] == ALL_WITH_RELATIONS: - raise InvalidFilterError("Lookups are not allowed more than one level deep on the '%s' field." % field_name) - - # Recursively descend through the remaining lookups in the filter, - # if any. We should ensure that all along the way, we're allowed - # to filter on that field by the related resource. - related_resource = self.fields[field_name].get_related_resource(None) - return [self.fields[field_name].attribute] + related_resource.check_filtering(filter_bits[0], filter_type, filter_bits[1:]) - - return [self.fields[field_name].attribute] - - def filter_value_to_python(self, value, field_name, filters, filter_expr, - filter_type): - """ - Turn the string ``value`` into a python object. - """ - # Simple values - if value in ['true', 'True', True]: - value = True - elif value in ['false', 'False', False]: - value = False - elif value in ('nil', 'none', 'None', None): - value = None - - # Split on ',' if not empty string and either an in or range filter. - if filter_type in ('in', 'range') and len(value): - if hasattr(filters, 'getlist'): - value = [] - - for part in filters.getlist(filter_expr): - value.extend(part.split(',')) - else: - value = value.split(',') - - return value - - def build_filters(self, filters=None): - """ - Given a dictionary of filters, create the necessary ORM-level filters. - - Keys should be resource fields, **NOT** model fields. - - Valid values are either a list of Django filter types (i.e. - ``['startswith', 'exact', 'lte']``), the ``ALL`` constant or the - ``ALL_WITH_RELATIONS`` constant. - """ - # At the declarative level: - # filtering = { - # 'resource_field_name': ['exact', 'startswith', 'endswith', 'contains'], - # 'resource_field_name_2': ['exact', 'gt', 'gte', 'lt', 'lte', 'range'], - # 'resource_field_name_3': ALL, - # 'resource_field_name_4': ALL_WITH_RELATIONS, - # ... - # } - # Accepts the filters as a dict. None by default, meaning no filters. - if filters is None: - filters = {} - - qs_filters = {} - - if getattr(self._meta, 'queryset', None) is not None: - # Get the possible query terms from the current QuerySet. - query_terms = self._meta.queryset.query.query_terms - else: - query_terms = QUERY_TERMS - - for filter_expr, value in filters.items(): - filter_bits = filter_expr.split(LOOKUP_SEP) - field_name = filter_bits.pop(0) - filter_type = 'exact' - - if not field_name in self.fields: - # It's not a field we know about. Move along citizen. - continue - - if len(filter_bits) and filter_bits[-1] in query_terms: - filter_type = filter_bits.pop() - - lookup_bits = self.check_filtering(field_name, filter_type, filter_bits) - value = self.filter_value_to_python(value, field_name, filters, filter_expr, filter_type) - - db_field_name = LOOKUP_SEP.join(lookup_bits) - qs_filter = "%s%s%s" % (db_field_name, LOOKUP_SEP, filter_type) - qs_filters[qs_filter] = value - - return dict_strip_unicode_keys(qs_filters) - - def apply_sorting(self, obj_list, options=None): - """ - Given a dictionary of options, apply some ORM-level sorting to the - provided ``QuerySet``. - - Looks for the ``order_by`` key and handles either ascending (just the - field name) or descending (the field name with a ``-`` in front). - - The field name should be the resource field, **NOT** model field. - """ - if options is None: - options = {} - - parameter_name = 'order_by' - - if not 'order_by' in options: - if not 'sort_by' in options: - # Nothing to alter the order. Return what we've got. - return obj_list - else: - warnings.warn("'sort_by' is a deprecated parameter. Please use 'order_by' instead.") - parameter_name = 'sort_by' - - order_by_args = [] - - if hasattr(options, 'getlist'): - order_bits = options.getlist(parameter_name) - else: - order_bits = options.get(parameter_name) - - if not isinstance(order_bits, (list, tuple)): - order_bits = [order_bits] - - for order_by in order_bits: - order_by_bits = order_by.split(LOOKUP_SEP) - - field_name = order_by_bits[0] - order = '' - - if order_by_bits[0].startswith('-'): - field_name = order_by_bits[0][1:] - order = '-' - - if not field_name in self.fields: - # It's not a field we know about. Move along citizen. - raise InvalidSortError("No matching '%s' field for ordering on." % field_name) - - if not field_name in self._meta.ordering: - raise InvalidSortError("The '%s' field does not allow ordering." % field_name) - - if self.fields[field_name].attribute is None: - raise InvalidSortError("The '%s' field has no 'attribute' for ordering with." % field_name) - - order_by_args.append("%s%s" % (order, LOOKUP_SEP.join([self.fields[field_name].attribute] + order_by_bits[1:]))) - - return obj_list.order_by(*order_by_args) - - def apply_filters(self, request, applicable_filters): - """ - An ORM-specific implementation of ``apply_filters``. - - The default simply applies the ``applicable_filters`` as ``**kwargs``, - but should make it possible to do more advanced things. - """ - return self.get_object_list(request).filter(**applicable_filters) - - def get_object_list(self, request): - """ - An ORM-specific implementation of ``get_object_list``. - - Returns a queryset that may have been limited by other overrides. - """ - return self._meta.queryset._clone() - - def obj_get_list(self, bundle, **kwargs): - """ - A ORM-specific implementation of ``obj_get_list``. - - Takes an optional ``request`` object, whose ``GET`` dictionary can be - used to narrow the query. - """ - filters = {} - - if hasattr(bundle.request, 'GET'): - # Grab a mutable copy. - filters = bundle.request.GET.copy() - - # Update with the provided kwargs. - filters.update(kwargs) - applicable_filters = self.build_filters(filters=filters) - - try: - objects = self.apply_filters(bundle.request, applicable_filters) - return self.authorized_read_list(objects, bundle) - except ValueError: - raise BadRequest("Invalid resource lookup data provided (mismatched type).") - except TypeError as e: - raise BadRequest("Invalid resource lookup data provided (%s)." % e) - - def obj_get(self, bundle, **kwargs): - """ - A ORM-specific implementation of ``obj_get``. - - Takes optional ``kwargs``, which are used to narrow the query to find - the instance. - """ - try: - object_list = self.get_object_list(bundle.request).filter(**kwargs) - stringified_kwargs = ', '.join(["%s=%s" % (k, v) for k, v in kwargs.items()]) - - if len(object_list) <= 0: - raise self._meta.object_class.DoesNotExist("Couldn't find an instance of '%s' which matched '%s'." % (self._meta.object_class.__name__, stringified_kwargs)) - elif len(object_list) > 1: - raise MultipleObjectsReturned("More than '%s' matched '%s'." % (self._meta.object_class.__name__, stringified_kwargs)) - - bundle.obj = object_list[0] - self.authorized_read_detail(object_list, bundle) - return bundle.obj - except ValueError: - raise NotFound("Invalid resource lookup data provided (mismatched type).") - - def obj_create(self, bundle, **kwargs): - """ - A ORM-specific implementation of ``obj_create``. - """ - bundle.obj = self._meta.object_class() - - for key, value in kwargs.items(): - setattr(bundle.obj, key, value) - - bundle = self.full_hydrate(bundle) - return self.save(bundle) - - def lookup_kwargs_with_identifiers(self, bundle, kwargs): - """ - Kwargs here represent uri identifiers Ex: /repos/// - We need to turn those identifiers into Python objects for generating - lookup parameters that can find them in the DB - """ - lookup_kwargs = {} - bundle.obj = self.get_object_list(bundle.request).model() - # Override data values, we rely on uri identifiers - bundle.data.update(kwargs) - # We're going to manually hydrate, as opposed to calling - # ``full_hydrate``, to ensure we don't try to flesh out related - # resources & keep things speedy. - bundle = self.hydrate(bundle) - - for identifier in kwargs: - if identifier == self._meta.detail_uri_name: - lookup_kwargs[identifier] = kwargs[identifier] - continue - - field_object = self.fields[identifier] - - # Skip readonly or related fields. - if field_object.readonly is True or getattr(field_object, 'is_related', False): - continue - - # Check for an optional method to do further hydration. - method = getattr(self, "hydrate_%s" % identifier, None) - - if method: - bundle = method(bundle) - - if field_object.attribute: - value = field_object.hydrate(bundle) - - lookup_kwargs[identifier] = value - - return lookup_kwargs - - def obj_update(self, bundle, skip_errors=False, **kwargs): - """ - A ORM-specific implementation of ``obj_update``. - """ - if not bundle.obj or not self.get_bundle_detail_data(bundle): - try: - lookup_kwargs = self.lookup_kwargs_with_identifiers(bundle, kwargs) - except: - # if there is trouble hydrating the data, fall back to just - # using kwargs by itself (usually it only contains a "pk" key - # and this will work fine. - lookup_kwargs = kwargs - - try: - bundle.obj = self.obj_get(bundle=bundle, **lookup_kwargs) - except ObjectDoesNotExist: - raise NotFound("A model instance matching the provided arguments could not be found.") - - bundle = self.full_hydrate(bundle) - return self.save(bundle, skip_errors=skip_errors) - - def obj_delete_list(self, bundle, **kwargs): - """ - A ORM-specific implementation of ``obj_delete_list``. - """ - objects_to_delete = self.obj_get_list(bundle=bundle, **kwargs) - deletable_objects = self.authorized_delete_list(objects_to_delete, bundle) - - if hasattr(deletable_objects, 'delete'): - # It's likely a ``QuerySet``. Call ``.delete()`` for efficiency. - deletable_objects.delete() - else: - for authed_obj in deletable_objects: - authed_obj.delete() - - def obj_delete_list_for_update(self, bundle, **kwargs): - """ - A ORM-specific implementation of ``obj_delete_list_for_update``. - """ - objects_to_delete = self.obj_get_list(bundle=bundle, **kwargs) - deletable_objects = self.authorized_update_list(objects_to_delete, bundle) - - if hasattr(deletable_objects, 'delete'): - # It's likely a ``QuerySet``. Call ``.delete()`` for efficiency. - deletable_objects.delete() - else: - for authed_obj in deletable_objects: - authed_obj.delete() - - def obj_delete(self, bundle, **kwargs): - """ - A ORM-specific implementation of ``obj_delete``. - - Takes optional ``kwargs``, which are used to narrow the query to find - the instance. - """ - if not hasattr(bundle.obj, 'delete'): - try: - bundle.obj = self.obj_get(bundle=bundle, **kwargs) - except ObjectDoesNotExist: - raise NotFound("A model instance matching the provided arguments could not be found.") - - self.authorized_delete_detail(self.get_object_list(bundle.request), bundle) - bundle.obj.delete() - - @transaction.commit_on_success() - def patch_list(self, request, **kwargs): - """ - An ORM-specific implementation of ``patch_list``. - - Necessary because PATCH should be atomic (all-success or all-fail) - and the only way to do this neatly is at the database level. - """ - return super(BaseModelResource, self).patch_list(request, **kwargs) - - def rollback(self, bundles): - """ - A ORM-specific implementation of ``rollback``. - - Given the list of bundles, delete all models pertaining to those - bundles. - """ - for bundle in bundles: - if bundle.obj and self.get_bundle_detail_data(bundle): - bundle.obj.delete() - - def create_identifier(self, obj): - return u"%s.%s.%s" % (obj._meta.app_label, obj._meta.module_name, obj.pk) - - def save(self, bundle, skip_errors=False): - self.is_valid(bundle) - - if bundle.errors and not skip_errors: - raise ImmediateHttpResponse(response=self.error_response(bundle.request, bundle.errors)) - - # Check if they're authorized. - if bundle.obj.pk: - self.authorized_update_detail(self.get_object_list(bundle.request), bundle) - else: - self.authorized_create_detail(self.get_object_list(bundle.request), bundle) - - # Save FKs just in case. - self.save_related(bundle) - - # Save the main object. - bundle.obj.save() - bundle.objects_saved.add(self.create_identifier(bundle.obj)) - - # Now pick up the M2M bits. - m2m_bundle = self.hydrate_m2m(bundle) - self.save_m2m(m2m_bundle) - return bundle - - def save_related(self, bundle): - """ - Handles the saving of related non-M2M data. - - Calling assigning ``child.parent = parent`` & then calling - ``Child.save`` isn't good enough to make sure the ``parent`` - is saved. - - To get around this, we go through all our related fields & - call ``save`` on them if they have related, non-M2M data. - M2M data is handled by the ``ModelResource.save_m2m`` method. - """ - for field_name, field_object in self.fields.items(): - if not getattr(field_object, 'is_related', False): - continue - - if getattr(field_object, 'is_m2m', False): - continue - - if not field_object.attribute: - continue - - if field_object.readonly: - continue - - if field_object.blank and not field_name in bundle.data: - continue - - # Get the object. - try: - related_obj = getattr(bundle.obj, field_object.attribute) - except ObjectDoesNotExist: - related_obj = bundle.related_objects_to_save.get(field_object.attribute, None) - - # Because sometimes it's ``None`` & that's OK. - if related_obj: - if field_object.related_name: - if not self.get_bundle_detail_data(bundle): - bundle.obj.save() - - setattr(related_obj, field_object.related_name, bundle.obj) - - related_resource = field_object.get_related_resource(related_obj) - - # Before we build the bundle & try saving it, let's make sure we - # haven't already saved it. - obj_id = self.create_identifier(related_obj) - - if obj_id in bundle.objects_saved: - # It's already been saved. We're done here. - continue - - if bundle.data.get(field_name) and hasattr(bundle.data[field_name], 'keys'): - # Only build & save if there's data, not just a URI. - related_bundle = related_resource.build_bundle( - obj=related_obj, - data=bundle.data.get(field_name), - request=bundle.request, - objects_saved=bundle.objects_saved - ) - related_resource.save(related_bundle) - - setattr(bundle.obj, field_object.attribute, related_obj) - - def save_m2m(self, bundle): - """ - Handles the saving of related M2M data. - - Due to the way Django works, the M2M data must be handled after the - main instance, which is why this isn't a part of the main ``save`` bits. - - Currently slightly inefficient in that it will clear out the whole - relation and recreate the related data as needed. - """ - for field_name, field_object in self.fields.items(): - if not getattr(field_object, 'is_m2m', False): - continue - - if not field_object.attribute: - continue - - if field_object.readonly: - continue - - # Get the manager. - related_mngr = None - - if isinstance(field_object.attribute, six.string_types): - related_mngr = getattr(bundle.obj, field_object.attribute) - elif callable(field_object.attribute): - related_mngr = field_object.attribute(bundle) - - if not related_mngr: - continue - - if hasattr(related_mngr, 'clear'): - # FIXME: Dupe the original bundle, copy in the new object & - # check the perms on that (using the related resource)? - - # Clear it out, just to be safe. - related_mngr.clear() - - related_objs = [] - - for related_bundle in bundle.data[field_name]: - related_resource = field_object.get_related_resource(bundle.obj) - - # Before we build the bundle & try saving it, let's make sure we - # haven't already saved it. - obj_id = self.create_identifier(related_bundle.obj) - - if obj_id in bundle.objects_saved: - # It's already been saved. We're done here. - continue - - # Only build & save if there's data, not just a URI. - updated_related_bundle = related_resource.build_bundle( - obj=related_bundle.obj, - data=related_bundle.data, - request=bundle.request, - objects_saved=bundle.objects_saved - ) - - #Only save related models if they're newly added. - if updated_related_bundle.obj._state.adding: - related_resource.save(updated_related_bundle) - related_objs.append(updated_related_bundle.obj) - - related_mngr.add(*related_objs) - - def detail_uri_kwargs(self, bundle_or_obj): - """ - Given a ``Bundle`` or an object (typically a ``Model`` instance), - it returns the extra kwargs needed to generate a detail URI. - - By default, it uses the model's ``pk`` in order to create the URI. - """ - kwargs = {} - - if isinstance(bundle_or_obj, Bundle): - kwargs[self._meta.detail_uri_name] = getattr(bundle_or_obj.obj, self._meta.detail_uri_name) - else: - kwargs[self._meta.detail_uri_name] = getattr(bundle_or_obj, self._meta.detail_uri_name) - - return kwargs - - -class ModelResource(six.with_metaclass(ModelDeclarativeMetaclass, BaseModelResource)): - pass - - -class NamespacedModelResource(ModelResource): - """ - A ModelResource subclass that respects Django namespaces. - """ - def _build_reverse_url(self, name, args=None, kwargs=None): - namespaced = "%s:%s" % (self._meta.urlconf_namespace, name) - return reverse(namespaced, args=args, kwargs=kwargs) - - -# Based off of ``piston.utils.coerce_put_post``. Similarly BSD-licensed. -# And no, the irony is not lost on me. -def convert_post_to_VERB(request, verb): - """ - Force Django to process the VERB. - """ - if request.method == verb: - if hasattr(request, '_post'): - del(request._post) - del(request._files) - - try: - request.method = "POST" - request._load_post_and_files() - request.method = verb - except AttributeError: - request.META['REQUEST_METHOD'] = 'POST' - request._load_post_and_files() - request.META['REQUEST_METHOD'] = verb - setattr(request, verb, request.POST) - - return request - - -def convert_post_to_put(request): - return convert_post_to_VERB(request, verb='PUT') - - -def convert_post_to_patch(request): - return convert_post_to_VERB(request, verb='PATCH') diff --git a/tastypie/serializers.py b/tastypie/serializers.py deleted file mode 100644 index a5064a650..000000000 --- a/tastypie/serializers.py +++ /dev/null @@ -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' diff --git a/tastypie/south_migrations/0001_initial.py b/tastypie/south_migrations/0001_initial.py deleted file mode 100644 index ebd9e526c..000000000 --- a/tastypie/south_migrations/0001_initial.py +++ /dev/null @@ -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'] diff --git a/tastypie/south_migrations/0002_add_apikey_index.py b/tastypie/south_migrations/0002_add_apikey_index.py deleted file mode 100644 index a3d5d3955..000000000 --- a/tastypie/south_migrations/0002_add_apikey_index.py +++ /dev/null @@ -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'] \ No newline at end of file diff --git a/tastypie/south_migrations/__init__.py b/tastypie/south_migrations/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tastypie/templates/tastypie/basic.html b/tastypie/templates/tastypie/basic.html deleted file mode 100644 index f805b5017..000000000 --- a/tastypie/templates/tastypie/basic.html +++ /dev/null @@ -1,29 +0,0 @@ - - - {% block title %}API for example.com{% endblock %} - - - -
-

API for example.com

- -
- {% block resources_nav %} -
    - {% for resource in resources_nav_items %} -
  • {{ resource }}
  • - {% endfor %} -
- {% endblock %} -
- -
- {% block content %}{% endblock %} -
-
- - \ No newline at end of file diff --git a/tastypie/templates/tastypie/detail.html b/tastypie/templates/tastypie/detail.html deleted file mode 100644 index 8bfda0d46..000000000 --- a/tastypie/templates/tastypie/detail.html +++ /dev/null @@ -1,4 +0,0 @@ -{% extends "tastypie/basic.html" %} - -{% block content %} -{% endblock %} \ No newline at end of file diff --git a/tastypie/templates/tastypie/list.html b/tastypie/templates/tastypie/list.html deleted file mode 100644 index 8bfda0d46..000000000 --- a/tastypie/templates/tastypie/list.html +++ /dev/null @@ -1,4 +0,0 @@ -{% extends "tastypie/basic.html" %} - -{% block content %} -{% endblock %} \ No newline at end of file diff --git a/tastypie/test.py b/tastypie/test.py deleted file mode 100644 index cfcbdfef6..000000000 --- a/tastypie/test.py +++ /dev/null @@ -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)) diff --git a/tastypie/throttle.py b/tastypie/throttle.py deleted file mode 100644 index e80157ced..000000000 --- a/tastypie/throttle.py +++ /dev/null @@ -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', '') - ) diff --git a/tastypie/utils/__init__.py b/tastypie/utils/__init__.py deleted file mode 100644 index 9c2de8fc8..000000000 --- a/tastypie/utils/__init__.py +++ /dev/null @@ -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 diff --git a/tastypie/utils/dict.py b/tastypie/utils/dict.py deleted file mode 100644 index e81e73f8d..000000000 --- a/tastypie/utils/dict.py +++ /dev/null @@ -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 diff --git a/tastypie/utils/formatting.py b/tastypie/utils/formatting.py deleted file mode 100644 index 1e4d77213..000000000 --- a/tastypie/utils/formatting.py +++ /dev/null @@ -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') diff --git a/tastypie/utils/mime.py b/tastypie/utils/mime.py deleted file mode 100644 index a2a77c9bb..000000000 --- a/tastypie/utils/mime.py +++ /dev/null @@ -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) diff --git a/tastypie/utils/timezone.py b/tastypie/utils/timezone.py deleted file mode 100644 index 2a3d523b7..000000000 --- a/tastypie/utils/timezone.py +++ /dev/null @@ -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)) diff --git a/tastypie/utils/urls.py b/tastypie/utils/urls.py deleted file mode 100644 index 961bedf1a..000000000 --- a/tastypie/utils/urls.py +++ /dev/null @@ -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 '/' diff --git a/tastypie/utils/validate_jsonp.py b/tastypie/utils/validate_jsonp.py deleted file mode 100644 index 7ad436e0c..000000000 --- a/tastypie/utils/validate_jsonp.py +++ /dev/null @@ -1,211 +0,0 @@ -# -*- coding: utf-8 -*- - -# Placed into the Public Domain by tav -# 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() diff --git a/tastypie/validation.py b/tastypie/validation.py deleted file mode 100644 index 6fd8a252c..000000000 --- a/tastypie/validation.py +++ /dev/null @@ -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