diff --git a/docs/code/authentication.py b/docs/code/authentication.py new file mode 100644 index 000000000..889b00b37 --- /dev/null +++ b/docs/code/authentication.py @@ -0,0 +1,4 @@ +import mock + + +OAuth20Authentication = mock.Mock() diff --git a/docs/code/manage.py b/docs/code/manage.py new file mode 100755 index 000000000..621838c79 --- /dev/null +++ b/docs/code/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings") + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/docs/code/myapp/__init__.py b/docs/code/myapp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/code/myapp/api/__init__.py b/docs/code/myapp/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/code/myapp/api/resources.py b/docs/code/myapp/api/resources.py new file mode 100644 index 000000000..45df77a9b --- /dev/null +++ b/docs/code/myapp/api/resources.py @@ -0,0 +1,8 @@ +from tastypie.resources import ModelResource + +from ..models import User + + +class UserResource(ModelResource): + class Meta: + object_class = User.objects.all() diff --git a/docs/code/myapp/models.py b/docs/code/myapp/models.py new file mode 100644 index 000000000..ce794bf45 --- /dev/null +++ b/docs/code/myapp/models.py @@ -0,0 +1,8 @@ +import mock + +from django.contrib.auth.models import User # flake8: noqa + + +Choice = mock.MagicMock() +Poll = mock.MagicMock() +MyModel = mock.MagicMock() diff --git a/docs/code/myapp/urls.py b/docs/code/myapp/urls.py new file mode 100644 index 000000000..74b13b097 --- /dev/null +++ b/docs/code/myapp/urls.py @@ -0,0 +1,4 @@ +from tastypie.api import Api + + +my_api = Api() diff --git a/docs/code/myproject/__init__.py b/docs/code/myproject/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/docs/code/myproject/settings.py b/docs/code/myproject/settings.py new file mode 100644 index 000000000..9f74992c6 --- /dev/null +++ b/docs/code/myproject/settings.py @@ -0,0 +1,85 @@ +""" +Django settings for myproject project. + +For more information on this file, see +https://docs.djangoproject.com/en/1.7/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.7/ref/settings/ +""" + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +import os +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '%ovvje%lh&k-%0v!@_c1gygt#aq-!o3*t$(hpee7@aj&35cr3a' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +TEMPLATE_DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'myapp', + 'tastypie', +) + +MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) + +ROOT_URLCONF = 'myproject.urls' + +WSGI_APPLICATION = 'myproject.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.7/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': ':memory:', + } +} + +# Internationalization +# https://docs.djangoproject.com/en/1.7/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.7/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/docs/code/myproject/urls.py b/docs/code/myproject/urls.py new file mode 100644 index 000000000..a10f3c4d7 --- /dev/null +++ b/docs/code/myproject/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import patterns, include, url +from django.contrib import admin + +urlpatterns = patterns('', + # Examples: + # url(r'^$', 'myproject.views.home', name='home'), + # url(r'^blog/', include('blog.urls')), + + url(r'^admin/', include(admin.site.urls)), +) diff --git a/docs/code/myproject/wsgi.py b/docs/code/myproject/wsgi.py new file mode 100644 index 000000000..938530883 --- /dev/null +++ b/docs/code/myproject/wsgi.py @@ -0,0 +1,14 @@ +""" +WSGI config for myproject project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ +""" + +import os +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings") + +from django.core.wsgi import get_wsgi_application # flake8: noqa +application = get_wsgi_application() diff --git a/docs/conf.py b/docs/conf.py index 94809085a..fd4c12c7d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,12 +18,16 @@ # sys.path.append(os.path.abspath('.')) import datetime +import os import sys sys.path.append('..') from tastypie import __short_version__, __version__ # flake8: noqa +docs_path = os.path.dirname(__file__) +doctest_path = [os.path.join(docs_path, 'code'), os.path.join(docs_path, '..')] + # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be diff --git a/docs/cookbook.rst b/docs/cookbook.rst index f4f24b60c..605dd46fe 100644 --- a/docs/cookbook.rst +++ b/docs/cookbook.rst @@ -9,15 +9,28 @@ Creating a Full OAuth 2.0 API It is common to use django to provision OAuth 2.0 tokens for users and then have Tasty Pie use these tokens to authenticate users to the API. `Follow this tutorial `_ and `use this custom authentication class `_ to enable -OAuth 2.0 authentication with Tasty Pie.:: +OAuth 2.0 authentication with Tasty Pie. + +.. testsetup:: + + import os + import django + from django.core.management import call_command + + os.environ['DJANGO_SETTINGS_MODULE'] = 'myproject.settings' + django.setup() + call_command('migrate', verbosity=0) + +.. testcode:: # api.py - from tastypie.resources import ModelResource - from tastypie.authorization import DjangoAuthorization - from polls.models import Poll, Choice from tastypie import fields + from tastypie.authorization import DjangoAuthorization + from tastypie.resources import ModelResource, Resource + from myapp.models import Poll, Choice from authentication import OAuth20Authentication + class ChoiceResource(ModelResource): class Meta: queryset = Choice.objects.all() @@ -25,14 +38,21 @@ OAuth 2.0 authentication with Tasty Pie.:: authorization = DjangoAuthorization() authentication = OAuth20Authentication() + class PollResource(ModelResource): choices = fields.ToManyField(ChoiceResource, 'choice_set', full=True) + class Meta: queryset = Poll.objects.all() resource_name = 'poll' authorization = DjangoAuthorization() authentication = OAuth20Authentication() +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... Adding Custom Values -------------------- @@ -40,32 +60,53 @@ Adding Custom Values You might encounter cases where you wish to include additional data in a response which is not obtained from a field or method on your model. You can easily extend the :meth:`~tastypie.resources.Resource.dehydrate` method to -provide additional values:: +provide additional values: + +.. testcode:: + + from myapp.models import MyModel + class MyModelResource(Resource): class Meta: - qs = MyModel.objects.all() + queryset = MyModel.objects.all() def dehydrate(self, bundle): bundle.data['custom_field'] = "Whatever you want" return bundle +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + Per-Request Alterations To The Queryset --------------------------------------- A common pattern is needing to limit a queryset by something that changes per-request, for instance the date/time. You can accomplish this by lightly -modifying ``get_object_list``:: +modifying ``get_object_list``: + +.. testcode:: from django.utils import timezone + from myapp.models import MyModel - class MyResource(ModelResource): + + class MyModelResource(ModelResource): class Meta: - queryset = MyObject.objects.all() + queryset = MyModel.objects.all() def get_object_list(self, request): - return super(MyResource, self).get_object_list(request).filter(start_date__gte=timezone.now()) + return super(MyModelResource, self).get_object_list(request).filter(start_date__gte=timezone.now()) + +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... Using Your ``Resource`` In Regular Views @@ -75,7 +116,9 @@ In addition to using your resource classes to power the API, you can also use them to write other parts of your application, such as your views. For instance, if you wanted to encode user information in the page for some Javascript's use, you could do the following. In this case, ``user_json`` will -not include a valid ``resource_uri``:: +not include a valid ``resource_uri``: + +.. testcode:: # views.py from django.shortcuts import render_to_response @@ -97,8 +140,16 @@ not include a valid ``resource_uri``:: "user_json": user_json, }) +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + To include a valid ``resource_uri``, the resource must be associated -with an ``tastypie.Api`` instance, as below:: +with an ``tastypie.Api`` instance, as below: + +.. testcode:: # urls.py from tastypie.api import Api @@ -108,8 +159,6 @@ with an ``tastypie.Api`` instance, as below:: my_api = Api(api_name='v1') my_api.register(UserResource()) -:: - # views.py from myapp.urls import my_api @@ -118,8 +167,16 @@ with an ``tastypie.Api`` instance, as below:: res = my_api.canonical_resource_for('user') # continue as above... +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + Alternatively, to get a valid ``resource_uri`` you may pass in the ``api_name`` -parameter directly to the Resource:: +parameter directly to the Resource: + +.. testcode:: # views.py from django.shortcuts import render_to_response @@ -130,7 +187,15 @@ parameter directly to the Resource:: res = UserResource(api_name='v1') # continue as above... -Example of getting a list of users:: +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + +Example of getting a list of users: + +.. testcode:: def user_list(request): res = UserResource() @@ -149,10 +214,16 @@ Example of getting a list of users:: "list_json": list_json, }) +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + Then in template you could convert JSON into JavaScript object:: @@ -166,9 +237,14 @@ requirement. Each URL can take other named URLconf parameters that can be used for the lookup. For example, if you want to expose ``User`` resources by username, you can do -something like the following:: +something like the following: + +.. testcode:: # myapp/api/resources.py + from django.contrib.auth.models import User + + class UserResource(ModelResource): class Meta: queryset = User.objects.all() @@ -179,42 +255,62 @@ something like the following:: url(r"^(?P%s)/(?P[\w\d_.-]+)/$" % self._meta.resource_name, self.wrap_view('dispatch_detail'), name="api_dispatch_detail"), ] +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + The added URLconf matches before the standard URLconf included by default & matches on the username provided in the URL. -Another alternative approach is to override the ``dispatch`` method:: +Another alternative approach is to override the ``dispatch`` method: + +.. testcode:: # myapp/api/resources.py - class EntryResource(ModelResource): + from myapp.models import MyModel + + class MyModelResource(ModelResource): user = fields.ForeignKey(UserResource, 'user') class Meta: - queryset = Entry.objects.all() - resource_name = 'entry' + queryset = MyModel.objects.all() + resource_name = 'mymodel' def dispatch(self, request_type, request, **kwargs): username = kwargs.pop('username') kwargs['user'] = get_object_or_404(User, username=username) - return super(EntryResource, self).dispatch(request_type, request, **kwargs) + return super(MyModelResource, self).dispatch(request_type, request, **kwargs) # urls.py from django.conf.urls import url, include - from myapp.api import EntryResource - entry_resource = EntryResource() + mymodel_resource = MyModelResource() urlpatterns = [ # The normal jazz here, then... - url(r'^api/(?P\w+)/', include(entry_resource.urls)), + url(r'^api/(?P\w+)/', include(mymodel_resource.urls)), ] +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + Nested Resources ---------------- You can also do "nested resources" (resources within another related resource) by lightly overriding the ``prepend_urls`` method & adding on a new method to -handle the children:: +handle the children: + +.. testcode:: + + class ChildResource(ModelResource): + pass class ParentResource(ModelResource): children = fields.ToManyField(ChildResource, 'children') @@ -236,6 +332,12 @@ handle the children:: child_resource = ChildResource() return child_resource.get_list(request, parent_id=obj.pk) +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + Adding Search Functionality --------------------------- @@ -307,37 +409,49 @@ For listing we want to list only objects for which 'user' field matches method of your resource. For creating we'd have to wrap ``obj_create`` method of ``ModelResource``. Then the -resulting code will look something like:: +resulting code will look something like: + +.. testcode:: # myapp/api/resources.py - class EnvironmentResource(ModelResource): + from tastypie.authentication import ApiKeyAuthentication + from tastypie.authorization import Authorization + + + class MyModelResource(ModelResource): class Meta: - queryset = Environment.objects.all() - resource_name = 'environment' + queryset = MyModel.objects.all() + resource_name = 'mymodel' list_allowed_methods = ['get', 'post'] authentication = ApiKeyAuthentication() authorization = Authorization() def obj_create(self, bundle, **kwargs): - return super(EnvironmentResource, self).obj_create(bundle, user=bundle.request.user) + return super(MyModelResource, self).obj_create(bundle, user=bundle.request.user) def apply_authorization_limits(self, request, object_list): return object_list.filter(user=request.user) +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + camelCase JSON Serialization ---------------------------- The convention in the world of Javascript has standardized on camelCase, where Tastypie uses underscore syntax, which can lead to "ugly" looking code in Javascript. You can create a custom serializer that emits -values in camelCase instead:: +values in camelCase instead: - import re - import simplejson - from tastypie.serializers import Serializer +.. testcode:: import re import json + from tastypie.serializers import Serializer + class CamelCaseJSONSerializer(Serializer): formats = ['json'] @@ -393,12 +507,20 @@ values in camelCase instead:: return underscored_data +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + Pretty-printed JSON Serialization --------------------------------- By default, Tastypie outputs JSON with no indentation or newlines (equivalent to calling :py:func:`json.dumps` with *indent* set to ``None``). You can override this -behavior in a custom serializer:: +behavior in a custom serializer: + +.. testcode:: import json from django.core.serializers.json import DjangoJSONEncoder @@ -413,16 +535,25 @@ behavior in a custom serializer:: return json.dumps(data, cls=DjangoJSONEncoder, sort_keys=True, ensure_ascii=False, indent=self.json_indent) +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + Determining format via URL -------------------------- Sometimes it's required to allow selecting the response format by specifying it in the API URL, for example ``/api/v1/users.json`` instead of ``/api/v1/users/?format=json``. The following snippet allows that kind -of syntax additional to the default URL scheme:: +of syntax additional to the default URL scheme: + +.. testcode:: # myapp/api/resources.py + from django.contrib.auth.models import User # Piggy-back on internal csrf_exempt existence handling from tastypie.resources import csrf_exempt @@ -460,6 +591,12 @@ of syntax additional to the default URL scheme:: return wrapped_view(request, *args, **kwargs) return wrapper +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + Adding to the Django Admin -------------------------- @@ -467,21 +604,29 @@ If you're using the django admin and ApiKeyAuthentication, you may want to see or edit ApiKeys next to users. To do this, you need to unregister the built-in UserAdmin, alter the inlines, and re-register it. This could go in any of your admin.py files. You may also want to register ApiAccess and ApiKey models on -their own.:: +their own.: - from tastypie.admin import ApiKeyInline - from tastypie.models import ApiAccess, ApiKey +.. testcode:: + + from django.contrib import admin from django.contrib.auth.admin import UserAdmin from django.contrib.auth.models import User - admin.site.register(ApiKey) - admin.site.register(ApiAccess) + from tastypie.admin import ApiKeyInline + class UserModelAdmin(UserAdmin): inlines = UserAdmin.inlines + [ApiKeyInline] + admin.site.unregister(User) - admin.site.register(User,UserModelAdmin) + admin.site.register(User, UserModelAdmin) + +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... Using ``SessionAuthentication`` @@ -489,7 +634,9 @@ Using ``SessionAuthentication`` If your users are logged into the site & you want Javascript to be able to access the API (assuming jQuery), the first thing to do is setup -``SessionAuthentication``:: +``SessionAuthentication``: + +.. testcode:: from django.contrib.auth.models import User from tastypie.authentication import SessionAuthentication @@ -502,6 +649,12 @@ access the API (assuming jQuery), the first thing to do is setup queryset = User.objects.all() authentication = SessionAuthentication() +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + :hide: + + ... + Then you'd build a template like:: diff --git a/tox.ini b/tox.ini index ef5125b26..6bda2883b 100644 --- a/tox.ini +++ b/tox.ini @@ -51,6 +51,8 @@ deps = dj{17,18,19,dev}: -r{toxinidir}/tests/requirements.txt docs: Sphinx + docs: Django>=1.9,<1.10 + docs: mock {flake8,flake8-strict}: flake8 changedir =