Commit 45001033 by Alec Perkins

Merge 'tomchristie/restframework2' into 'browsable-bootstrap'

parents 0ae5500f d4f8b4cf
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
**Author:** Tom Christie. [Follow me on Twitter][twitter] **Author:** Tom Christie. [Follow me on Twitter][twitter]
[![Build Status](https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=restframework2)][travis]
# Overview # Overview
This branch is the redesign of Django REST framework. It is a work in progress. This branch is the redesign of Django REST framework. It is a work in progress.
...@@ -79,7 +81,7 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, ...@@ -79,7 +81,7 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=restframework2
[twitter]: https://twitter.com/_tomchristie [twitter]: https://twitter.com/_tomchristie
[docs]: http://tomchristie.github.com/django-rest-framework/ [docs]: http://tomchristie.github.com/django-rest-framework/
[urlobject]: https://github.com/zacharyvoase/urlobject [urlobject]: https://github.com/zacharyvoase/urlobject
......
...@@ -103,4 +103,38 @@ class SessionAuthentication(BaseAuthentication): ...@@ -103,4 +103,38 @@ class SessionAuthentication(BaseAuthentication):
return (user, None) return (user, None)
# TODO: TokenAuthentication, DigestAuthentication, OAuthAuthentication class TokenAuthentication(BaseAuthentication):
"""
Use a token model for authentication.
A custom token model may be used here, but must have the following minimum
properties:
* key -- The string identifying the token
* user -- The user to which the token belongs
* revoked -- The status of the token
The token key should be passed in as a string to the "Authorization" HTTP
header. For example:
Authorization: 0123456789abcdef0123456789abcdef
"""
model = None
def authenticate(self, request):
key = request.META.get('HTTP_AUTHORIZATION', '').strip()
if self.model is None:
from djangorestframework.tokenauth.models import BasicToken
self.model = BasicToken
try:
token = self.model.objects.get(key=key)
except self.model.DoesNotExist:
return None
if token.user.is_active and not token.revoked:
return (token.user, token)
# TODO: DigestAuthentication, OAuthAuthentication
...@@ -366,6 +366,59 @@ else: ...@@ -366,6 +366,59 @@ else:
return self._accept(request) return self._accept(request)
# timezone support is new in Django 1.4
try:
from django.utils import timezone
except ImportError:
timezone = None
# dateparse is ALSO new in Django 1.4
try:
from django.utils.dateparse import parse_date, parse_datetime
except ImportError:
import datetime
import re
date_re = re.compile(
r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})$'
)
datetime_re = re.compile(
r'(?P<year>\d{4})-(?P<month>\d{1,2})-(?P<day>\d{1,2})'
r'[T ](?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?'
r'(?P<tzinfo>Z|[+-]\d{1,2}:\d{1,2})?$'
)
time_re = re.compile(
r'(?P<hour>\d{1,2}):(?P<minute>\d{1,2})'
r'(?::(?P<second>\d{1,2})(?:\.(?P<microsecond>\d{1,6})\d{0,6})?)?'
)
def parse_date(value):
match = date_re.match(value)
if match:
kw = dict((k, int(v)) for k, v in match.groupdict().iteritems())
return datetime.date(**kw)
def parse_time(value):
match = time_re.match(value)
if match:
kw = match.groupdict()
if kw['microsecond']:
kw['microsecond'] = kw['microsecond'].ljust(6, '0')
kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None)
return datetime.time(**kw)
def parse_datetime(value):
"""Parse datetime, but w/o the timezone awareness in 1.4"""
match = datetime_re.match(value)
if match:
kw = match.groupdict()
if kw['microsecond']:
kw['microsecond'] = kw['microsecond'].ljust(6, '0')
kw = dict((k, int(v)) for k, v in kw.iteritems() if v is not None)
return datetime.datetime(**kw)
# Markdown is optional # Markdown is optional
try: try:
......
...@@ -8,10 +8,10 @@ from django.core.exceptions import ValidationError ...@@ -8,10 +8,10 @@ from django.core.exceptions import ValidationError
from django.conf import settings from django.conf import settings
from django.db import DEFAULT_DB_ALIAS from django.db import DEFAULT_DB_ALIAS
from django.db.models.related import RelatedObject from django.db.models.related import RelatedObject
from django.utils import timezone
from django.utils.dateparse import parse_date, parse_datetime
from django.utils.encoding import is_protected_type, smart_unicode from django.utils.encoding import is_protected_type, smart_unicode
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from djangorestframework.compat import parse_date, parse_datetime
from djangorestframework.compat import timezone
def is_simple_callable(obj): def is_simple_callable(obj):
...@@ -317,7 +317,7 @@ class DateField(Field): ...@@ -317,7 +317,7 @@ class DateField(Field):
if value is None: if value is None:
return value return value
if isinstance(value, datetime.datetime): if isinstance(value, datetime.datetime):
if settings.USE_TZ and timezone.is_aware(value): if timezone and settings.USE_TZ and timezone.is_aware(value):
# Convert aware datetimes to the default time zone # Convert aware datetimes to the default time zone
# before casting them to dates (#17742). # before casting them to dates (#17742).
default_timezone = timezone.get_default_timezone() default_timezone = timezone.get_default_timezone()
......
...@@ -22,7 +22,7 @@ class CreateModelMixin(object): ...@@ -22,7 +22,7 @@ class CreateModelMixin(object):
self.object = serializer.object self.object = serializer.object
self.object.save() self.object.save()
return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.error_data, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class ListModelMixin(object): class ListModelMixin(object):
......
...@@ -16,6 +16,7 @@ from djangorestframework.utils import encoders ...@@ -16,6 +16,7 @@ from djangorestframework.utils import encoders
from djangorestframework.utils.breadcrumbs import get_breadcrumbs from djangorestframework.utils.breadcrumbs import get_breadcrumbs
from djangorestframework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches from djangorestframework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches
from djangorestframework import VERSION from djangorestframework import VERSION
from djangorestframework.fields import FloatField, IntegerField, DateTimeField, DateField, EmailField, CharField, BooleanField
import string import string
...@@ -233,33 +234,31 @@ class DocumentingTemplateRenderer(BaseRenderer): ...@@ -233,33 +234,31 @@ class DocumentingTemplateRenderer(BaseRenderer):
In the absence on of the Resource having an associated form then In the absence on of the Resource having an associated form then
provide a form that can be used to submit arbitrary content. provide a form that can be used to submit arbitrary content.
""" """
if not hasattr(self.view, 'get_serializer'): # No serializer, no form.
# Get the form instance if we have one bound to the input return
form_instance = None # We need to map our Fields to Django's Fields.
if method == getattr(view, 'method', view.request.method).lower(): field_mapping = dict([
form_instance = getattr(view, 'bound_form_instance', None) [FloatField.__name__, forms.FloatField],
[IntegerField.__name__, forms.IntegerField],
if not form_instance and hasattr(view, 'get_bound_form'): [DateTimeField.__name__, forms.DateTimeField],
# Otherwise if we have a response that is valid against the form then use that [DateField.__name__, forms.DateField],
if view.response.has_content_body: [EmailField.__name__, forms.EmailField],
try: [CharField.__name__, forms.CharField],
form_instance = view.get_bound_form(view.response.cleaned_content, method=method) [BooleanField.__name__, forms.BooleanField]
if form_instance and not form_instance.is_valid(): ])
form_instance = None
except Exception: # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python
form_instance = None fields = {}
object, data = None, None
# If we still don't have a form instance then try to get an unbound form if hasattr(self.view, 'object'):
if not form_instance: object = self.view.object
try: serializer = self.view.get_serializer(instance=object)
form_instance = view.get_bound_form(method=method) for k, v in serializer.fields.items():
except Exception: fields[k] = field_mapping[v.__class__.__name__]()
pass OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields)
if object and not self.view.request.method == 'DELETE': # Don't fill in the form when the object is deleted
# If we still don't have a form instance then try to get an unbound form which can tunnel arbitrary content types data = serializer.data
if not form_instance: form_instance = OnTheFlyForm(data)
form_instance = self._get_generic_content_form(view)
return form_instance return form_instance
def _get_generic_content_form(self, view): def _get_generic_content_form(self, view):
......
...@@ -90,6 +90,7 @@ INSTALLED_APPS = ( ...@@ -90,6 +90,7 @@ INSTALLED_APPS = (
# Uncomment the next line to enable admin documentation: # Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs', # 'django.contrib.admindocs',
'djangorestframework', 'djangorestframework',
'djangorestframework.tokenauth',
) )
STATIC_URL = '/static/' STATIC_URL = '/static/'
......
...@@ -88,7 +88,10 @@ def import_from_string(val, setting): ...@@ -88,7 +88,10 @@ def import_from_string(val, setting):
module_path, class_name = '.'.join(parts[:-1]), parts[-1] module_path, class_name = '.'.join(parts[:-1]), parts[-1]
module = importlib.import_module(module_path) module = importlib.import_module(module_path)
return getattr(module, class_name) return getattr(module, class_name)
except: except Exception, e:
import traceback
tb = traceback.format_exc()
import pdb; pdb.set_trace()
msg = "Could not import '%s' for API setting '%s'" % (val, setting) msg = "Could not import '%s' for API setting '%s'" % (val, setting)
raise ImportError(msg) raise ImportError(msg)
......
...@@ -8,6 +8,9 @@ from django.http import HttpResponse ...@@ -8,6 +8,9 @@ from django.http import HttpResponse
from djangorestframework.views import APIView from djangorestframework.views import APIView
from djangorestframework import permissions from djangorestframework import permissions
from djangorestframework.tokenauth.models import BasicToken
from djangorestframework.authentication import TokenAuthentication
import base64 import base64
...@@ -20,6 +23,8 @@ class MockView(APIView): ...@@ -20,6 +23,8 @@ class MockView(APIView):
def put(self, request): def put(self, request):
return HttpResponse({'a': 1, 'b': 2, 'c': 3}) return HttpResponse({'a': 1, 'b': 2, 'c': 3})
MockView.authentication += (TokenAuthentication,)
urlpatterns = patterns('', urlpatterns = patterns('',
(r'^$', MockView.as_view()), (r'^$', MockView.as_view()),
) )
...@@ -104,3 +109,45 @@ class SessionAuthTests(TestCase): ...@@ -104,3 +109,45 @@ class SessionAuthTests(TestCase):
""" """
response = self.csrf_client.post('/', {'example': 'example'}) response = self.csrf_client.post('/', {'example': 'example'})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
class TokenAuthTests(TestCase):
"""Token authentication"""
urls = 'djangorestframework.tests.authentication'
def setUp(self):
self.csrf_client = Client(enforce_csrf_checks=True)
self.username = 'john'
self.email = 'lennon@thebeatles.com'
self.password = 'password'
self.user = User.objects.create_user(self.username, self.email, self.password)
self.key = 'abcd1234'
self.token = BasicToken.objects.create(key=self.key, user=self.user)
def test_post_form_passing_token_auth(self):
"""Ensure POSTing json over token auth with correct credentials passes and does not require CSRF"""
auth = self.key
response = self.csrf_client.post('/', {'example': 'example'}, HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)
def test_post_json_passing_token_auth(self):
"""Ensure POSTing form over token auth with correct credentials passes and does not require CSRF"""
auth = self.key
response = self.csrf_client.post('/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)
def test_post_form_failing_token_auth(self):
"""Ensure POSTing form over token auth without correct credentials fails"""
response = self.csrf_client.post('/', {'example': 'example'})
self.assertEqual(response.status_code, 403)
def test_post_json_failing_token_auth(self):
"""Ensure POSTing json over token auth without correct credentials fails"""
response = self.csrf_client.post('/', json.dumps({'example': 'example'}), 'application/json')
self.assertEqual(response.status_code, 403)
def test_token_has_auto_assigned_key_if_none_provided(self):
"""Ensure creating a token with no key will auto-assign a key"""
token = BasicToken.objects.create(user=self.user)
self.assertEqual(len(token.key), 32)
...@@ -55,27 +55,27 @@ class MockView(APIView): ...@@ -55,27 +55,27 @@ class MockView(APIView):
def get(self, request, **kwargs): def get(self, request, **kwargs):
response = Response(DUMMYCONTENT, status=DUMMYSTATUS) response = Response(DUMMYCONTENT, status=DUMMYSTATUS)
return self.render(response) return response
class MockGETView(APIView): class MockGETView(APIView):
def get(self, request, **kwargs): def get(self, request, **kwargs):
return {'foo': ['bar', 'baz']} return Response({'foo': ['bar', 'baz']})
class HTMLView(APIView): class HTMLView(APIView):
renderers = (DocumentingHTMLRenderer, ) renderers = (DocumentingHTMLRenderer, )
def get(self, request, **kwargs): def get(self, request, **kwargs):
return 'text' return Response('text')
class HTMLView1(APIView): class HTMLView1(APIView):
renderers = (DocumentingHTMLRenderer, JSONRenderer) renderers = (DocumentingHTMLRenderer, JSONRenderer)
def get(self, request, **kwargs): def get(self, request, **kwargs):
return 'text' return Response('text')
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderers=[RendererA, RendererB])), url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderers=[RendererA, RendererB])),
...@@ -88,7 +88,7 @@ urlpatterns = patterns('', ...@@ -88,7 +88,7 @@ urlpatterns = patterns('',
) )
class RendererIntegrationTests(TestCase): class RendererEndToEndTests(TestCase):
""" """
End-to-end testing of renderers using an RendererMixin on a generic view. End-to-end testing of renderers using an RendererMixin on a generic view.
""" """
...@@ -216,18 +216,6 @@ class JSONRendererTests(TestCase): ...@@ -216,18 +216,6 @@ class JSONRendererTests(TestCase):
self.assertEquals(strip_trailing_whitespace(content), _indented_repr) self.assertEquals(strip_trailing_whitespace(content), _indented_repr)
class MockGETView(APIView):
def get(self, request, *args, **kwargs):
return Response({'foo': ['bar', 'baz']})
urlpatterns = patterns('',
url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderers=[JSONRenderer, JSONPRenderer])),
url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderers=[JSONPRenderer])),
)
class JSONPRendererTests(TestCase): class JSONPRendererTests(TestCase):
""" """
Tests specific to the JSONP Renderer Tests specific to the JSONP Renderer
......
...@@ -94,7 +94,16 @@ class TestContentParsing(TestCase): ...@@ -94,7 +94,16 @@ class TestContentParsing(TestCase):
""" """
data = {'qwerty': 'uiop'} data = {'qwerty': 'uiop'}
parsers = (FormParser, MultiPartParser) parsers = (FormParser, MultiPartParser)
request = factory.put('/', data, parsers=parsers)
from django import VERSION
if VERSION >= (1, 5):
from django.test.client import MULTIPART_CONTENT, BOUNDARY, encode_multipart
request = factory.put('/', encode_multipart(BOUNDARY, data), parsers=parsers,
content_type=MULTIPART_CONTENT)
else:
request = factory.put('/', data, parsers=parsers)
self.assertEqual(request.DATA.items(), data.items()) self.assertEqual(request.DATA.items(), data.items())
def test_standard_behaviour_determines_non_form_content_PUT(self): def test_standard_behaviour_determines_non_form_content_PUT(self):
......
import uuid
from django.db import models
class BasicToken(models.Model):
"""
The default authorization token model class.
"""
key = models.CharField(max_length=32, primary_key=True, blank=True)
user = models.ForeignKey('auth.User')
revoked = models.BooleanField(default=False)
def save(self, *args, **kwargs):
if not self.key:
self.key = uuid.uuid4().hex
return super(BasicToken, self).save(*args, **kwargs)
...@@ -3,8 +3,8 @@ Helper classes for parsers. ...@@ -3,8 +3,8 @@ Helper classes for parsers.
""" """
import datetime import datetime
import decimal import decimal
from django.utils import timezone
from django.utils import simplejson as json from django.utils import simplejson as json
from djangorestframework.compat import timezone
class JSONEncoder(json.JSONEncoder): class JSONEncoder(json.JSONEncoder):
...@@ -25,7 +25,7 @@ class JSONEncoder(json.JSONEncoder): ...@@ -25,7 +25,7 @@ class JSONEncoder(json.JSONEncoder):
elif isinstance(o, datetime.date): elif isinstance(o, datetime.date):
return o.isoformat() return o.isoformat()
elif isinstance(o, datetime.time): elif isinstance(o, datetime.time):
if timezone.is_aware(o): if timezone and timezone.is_aware(o):
raise ValueError("JSON can't represent timezone-aware times.") raise ValueError("JSON can't represent timezone-aware times.")
r = o.isoformat() r = o.isoformat()
if o.microsecond: if o.microsecond:
......
<a class="github" href="authentication.py"></a>
# Authentication # Authentication
Authentication is the mechanism of associating an incoming request with a set of identifying credentials, such as the user the request came from, or the token that it was signed with. The [permission] and [throttling] policies can then use those credentials to determine if the request should be permitted. Authentication is the mechanism of associating an incoming request with a set of identifying credentials, such as the user the request came from, or the token that it was signed with. The [permission] and [throttling] policies can then use those credentials to determine if the request should be permitted.
...@@ -8,7 +10,7 @@ Authentication will run the first time either the `request.user` or `request.aut ...@@ -8,7 +10,7 @@ Authentication will run the first time either the `request.user` or `request.aut
The `request.user` property will typically be set to an instance of the `contrib.auth` package's `User` class. The `request.user` property will typically be set to an instance of the `contrib.auth` package's `User` class.
The `request.auth` property is used for any additional authentication information, for example, it may be used to represent an authentication token that the request was signed with. The `request.auth` property is used for any additional authentication information, for example, it may be used to represent an authentication token that the request was signed with.
## How authentication is determined ## How authentication is determined
...@@ -36,7 +38,7 @@ You can also set the authentication policy on a per-view basis, using the `APIVi ...@@ -36,7 +38,7 @@ You can also set the authentication policy on a per-view basis, using the `APIVi
def get(self, request, format=None): def get(self, request, format=None):
content = { content = {
'user': unicode(request.user), # `django.contrib.auth.User` instance. 'user': unicode(request.user), # `django.contrib.auth.User` instance.
'auth': unicode(request.auth), # None 'auth': unicode(request.auth), # None
} }
return Response(content) return Response(content)
...@@ -49,7 +51,7 @@ Or, if you're using the `@api_view` decorator with function based views. ...@@ -49,7 +51,7 @@ Or, if you're using the `@api_view` decorator with function based views.
) )
def example_view(request, format=None): def example_view(request, format=None):
content = { content = {
'user': unicode(request.user), # `django.contrib.auth.User` instance. 'user': unicode(request.user), # `django.contrib.auth.User` instance.
'auth': unicode(request.auth), # None 'auth': unicode(request.auth), # None
} }
return Response(content) return Response(content)
...@@ -65,16 +67,20 @@ If successfully authenticated, `UserBasicAuthentication` provides the following ...@@ -65,16 +67,20 @@ If successfully authenticated, `UserBasicAuthentication` provides the following
* `request.user` will be a `django.contrib.auth.models.User` instance. * `request.user` will be a `django.contrib.auth.models.User` instance.
* `request.auth` will be `None`. * `request.auth` will be `None`.
## TokenBasicAuthentication ## TokenAuthentication
This policy uses [HTTP Authentication][basicauth] with no authentication scheme. Token basic authentication is appropriate for client-server setups, such as native desktop and mobile clients. The token key should be passed in as a string to the "Authorization" HTTP header. For example:
This policy uses [HTTP Basic Authentication][basicauth], signed against a token key and secret. Token basic authentication is appropriate for client-server setups, such as native desktop and mobile clients. curl http://my.api.org/ -X POST -H "Authorization: 0123456789abcdef0123456789abcdef"
**Note:** If you run `TokenBasicAuthentication` in production your API must be `https` only, or it will be completely insecure. **Note:** If you run `TokenAuthentication` in production your API must be `https` only, or it will be completely insecure.
If successfully authenticated, `TokenBasicAuthentication` provides the following credentials. If successfully authenticated, `TokenAuthentication` provides the following credentials.
* `request.user` will be a `django.contrib.auth.models.User` instance. * `request.user` will be a `django.contrib.auth.models.User` instance.
* `request.auth` will be a `djangorestframework.models.BasicToken` instance. * `request.auth` will be a `djangorestframework.tokenauth.models.BasicToken` instance.
To use the `TokenAuthentication` policy, you must have a token model. Django REST Framework comes with a minimal default token model. To use it, include `djangorestframework.tokenauth` in your installed applications and sync your database. To use your own token model, subclass the `djangorestframework.tokenauth.TokenAuthentication` class and specify a `model` attribute that references your custom token model. The token model must provide `user`, `key`, and `revoked` attributes. Refer to the `djangorestframework.tokenauth.models.BasicToken` model as an example.
## OAuthAuthentication ## OAuthAuthentication
......
<a class="github" href="exceptions.py"></a>
# Exceptions # Exceptions
<a class="github" href="parsers.py"></a>
# Parsers # Parsers
## .parse(request) ## .parse(request)
<a class="github" href="permissions.py"></a>
# Permissions
\ No newline at end of file
<a class="github" href="renderers.py"></a>
# Renderers # Renderers
## .render(response) ## .render(response)
......
<a class="github" href="request.py"></a>
# Requests # Requests
> If you're doing REST-based web service stuff ... you should ignore request.POST. > If you're doing REST-based web service stuff ... you should ignore request.POST.
......
<a class="github" href="response.py"></a>
# Responses # Responses
> Unlike basic HttpResponse objects, TemplateResponse objects retain the details of the context that was provided by the view to compute the response. The final output of the response is not computed until it is needed, later in the response process. > Unlike basic HttpResponse objects, TemplateResponse objects retain the details of the context that was provided by the view to compute the response. The final output of the response is not computed until it is needed, later in the response process.
......
<a class="github" href="reverse.py"></a>
# Returning URIs from your Web APIs # Returning URIs from your Web APIs
> The central feature that distinguishes the REST architectural style from other network-based styles is its emphasis on a uniform interface between components. > The central feature that distinguishes the REST architectural style from other network-based styles is its emphasis on a uniform interface between components.
......
<a class="github" href="serializers.py"></a>
# Serializers # Serializers
> Expanding the usefulness of the serializers is something that we would > Expanding the usefulness of the serializers is something that we would
......
<a class="github" href="settings.py"></a>
# Settings # Settings
Configuration for REST framework is all namespaced inside the `API_SETTINGS` setting. Configuration for REST framework is all namespaced inside the `API_SETTINGS` setting.
......
<a class="github" href="status.py"></a>
# Status Codes # Status Codes
> 418 I'm a teapot - Any attempt to brew coffee with a teapot should result in the error code "418 I'm a teapot". The resulting entity body MAY be short and stout. > 418 I'm a teapot - Any attempt to brew coffee with a teapot should result in the error code "418 I'm a teapot". The resulting entity body MAY be short and stout.
......
<a class="github" href="throttling.py"></a>
# Throttling
<a class="github" href="views.py"></a>
# Views
> Django's class based views are a welcome departure from the old-style views. > Django's class based views are a welcome departure from the old-style views.
> >
> &mdash; [Reinout van Rees][cite] > &mdash; [Reinout van Rees][cite]
# Views
REST framework provides a simple `APIView` class, built on Django's `django.generics.views.View`. The `APIView` class ensures five main things: REST framework provides a simple `APIView` class, built on Django's `django.generics.views.View`. The `APIView` class ensures five main things:
1. Any requests inside the view will become `Request` instances. 1. Any requests inside the view will become `Request` instances.
......
<iframe src="http://ghbtns.com/github-btn.html?user=tomchristie&amp;repo=django-rest-framework&amp;type=watch&amp;count=true" allowtransparency="true" frameborder="0" scrolling="0" width="110px" height="20px"></iframe>
[![Build Status](https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=restframework2)][travis]
# Django REST framework # Django REST framework
**A toolkit for building well-connected, self-describing Web APIs.** **A toolkit for building well-connected, self-describing Web APIs.**
...@@ -131,6 +134,7 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, ...@@ -131,6 +134,7 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=restframework2
[urlobject]: https://github.com/zacharyvoase/urlobject [urlobject]: https://github.com/zacharyvoase/urlobject
[markdown]: http://pypi.python.org/pypi/Markdown/ [markdown]: http://pypi.python.org/pypi/Markdown/
[yaml]: http://pypi.python.org/pypi/PyYAML [yaml]: http://pypi.python.org/pypi/PyYAML
......
/*! /*!
* Bootstrap Responsive v2.1.0 * Bootstrap Responsive v2.1.1
* *
* Copyright 2012 Twitter, Inc * Copyright 2012 Twitter, Inc
* Licensed under the Apache License v2.0 * Licensed under the Apache License v2.0
...@@ -107,6 +107,7 @@ ...@@ -107,6 +107,7 @@
} }
[class*="span"] { [class*="span"] {
float: left; float: left;
min-height: 1px;
margin-left: 30px; margin-left: 30px;
} }
.container, .container,
...@@ -453,6 +454,7 @@ ...@@ -453,6 +454,7 @@
} }
[class*="span"] { [class*="span"] {
float: left; float: left;
min-height: 1px;
margin-left: 20px; margin-left: 20px;
} }
.container, .container,
...@@ -780,7 +782,8 @@ ...@@ -780,7 +782,8 @@
padding-left: 20px; padding-left: 20px;
} }
.navbar-fixed-top, .navbar-fixed-top,
.navbar-fixed-bottom { .navbar-fixed-bottom,
.navbar-static-top {
margin-right: -20px; margin-right: -20px;
margin-left: -20px; margin-left: -20px;
} }
...@@ -814,8 +817,11 @@ ...@@ -814,8 +817,11 @@
.row-fluid [class*="span"] { .row-fluid [class*="span"] {
display: block; display: block;
float: none; float: none;
width: auto; width: 100%;
margin-left: 0; margin-left: 0;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
} }
.span12, .span12,
.row-fluid .span12 { .row-fluid .span12 {
...@@ -845,6 +851,9 @@ ...@@ -845,6 +851,9 @@
display: inline-block; display: inline-block;
width: auto; width: auto;
} }
.controls-row [class*="span"] + [class*="span"] {
margin-left: 0;
}
.modal { .modal {
position: fixed; position: fixed;
top: 20px; top: 20px;
...@@ -870,7 +879,7 @@ ...@@ -870,7 +879,7 @@
input[type="radio"] { input[type="radio"] {
border: 1px solid #ccc; border: 1px solid #ccc;
} }
.form-horizontal .control-group > label { .form-horizontal .control-label {
float: none; float: none;
width: auto; width: auto;
padding-top: 0; padding-top: 0;
...@@ -944,14 +953,14 @@ ...@@ -944,14 +953,14 @@
display: none; display: none;
} }
.nav-collapse .nav .nav-header { .nav-collapse .nav .nav-header {
color: #555555; color: #777777;
text-shadow: none; text-shadow: none;
} }
.nav-collapse .nav > li > a, .nav-collapse .nav > li > a,
.nav-collapse .dropdown-menu a { .nav-collapse .dropdown-menu a {
padding: 9px 15px; padding: 9px 15px;
font-weight: bold; font-weight: bold;
color: #555555; color: #777777;
-webkit-border-radius: 3px; -webkit-border-radius: 3px;
-moz-border-radius: 3px; -moz-border-radius: 3px;
border-radius: 3px; border-radius: 3px;
...@@ -1003,6 +1012,10 @@ ...@@ -1003,6 +1012,10 @@
.nav-collapse .dropdown-menu .divider { .nav-collapse .dropdown-menu .divider {
display: none; display: none;
} }
.nav-collapse .nav > li > .dropdown-menu:before,
.nav-collapse .nav > li > .dropdown-menu:after {
display: none;
}
.nav-collapse .navbar-form, .nav-collapse .navbar-form,
.nav-collapse .navbar-search { .nav-collapse .navbar-search {
float: none; float: none;
...@@ -1014,6 +1027,11 @@ ...@@ -1014,6 +1027,11 @@
-moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); -moz-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
} }
.navbar-inverse .nav-collapse .navbar-form,
.navbar-inverse .nav-collapse .navbar-search {
border-top-color: #111111;
border-bottom-color: #111111;
}
.navbar .nav-collapse .nav.pull-right { .navbar .nav-collapse .nav.pull-right {
float: none; float: none;
margin-left: 0; margin-left: 0;
......
/* Set the body padding-top when above 980px to push the content down from
below the navbar, which is fixed at >980px screen widths. */
@media (min-width: 980px) {
body {
padding-top: 71px;
}
}
body {
padding-bottom: 40px;
}
pre {
font-size: 12px;
}
a.github {
float: right;
margin-top: -12px;
}
a.github:hover {
text-decoration: none;
}
.dropdown .dropdown-menu {
display: none;
}
.dropdown.open .dropdown-menu {
display: block;
}
body.index #main-content iframe {
float: right;
}
body.index #main-content iframe {
float: right;
margin-right: -15px;
}
body.index #main-content p:first-of-type {
float: right;
margin-right: 8px;
margin-top: -1px;
}
#table-of-contents {
overflow: hidden;
}
pre {
overflow: auto;
word-wrap: normal;
white-space: pre;
}
/* Preserve the spacing of the navbar across different screen sizes. */
.navbar-inner {
padding: 5px 0;
}
@media (max-width: 979px) {
.navbar .brand {
margin-left: 0;
padding-left: 0;
}
.navbar-inner .container-fluid {
padding-left: 15px;
}
}
.nav-list li.main {
font-weight: bold;
}
/* Set the table of contents to static so it flows back into the content when
viewed on tablets and smaller. */
@media (max-width: 767px) {
#table-of-contents {
position: static;
}
}
/* When the page is in two-column layout, give the main content some room
to breath on the left. */
@media (min-width: 768px) {
#main-content {
padding-left: 1em;
}
}
blockquote {
font-family: Georgia, serif;
font-size: 18px;
font-style: italic;
margin: 0.25em 0;
padding: 0.25em 40px;
line-height: 1.45;
position: relative;
color: #383838;
border-left: none;
}
blockquote:before {
display: block;
content: "\201C";
font-size: 80px;
position: absolute;
left: -10px;
top: -20px;
color: #7a7a7a;
}
blockquote p:last-child {
color: #999999;
font-size: 14px;
display: block;
margin-top: 5px;
}
.com { color: #93a1a1; }
.lit { color: #195f91; }
.pun, .opn, .clo { color: #93a1a1; }
.fun { color: #dc322f; }
.str, .atv { color: #D14; }
.kwd, .prettyprint .tag { color: #1e347b; }
.typ, .atn, .dec, .var { color: teal; }
.pln { color: #48484c; }
.prettyprint {
padding: 8px;
background-color: #f7f7f9;
border: 1px solid #e1e1e8;
}
.prettyprint.linenums {
-webkit-box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0;
-moz-box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0;
box-shadow: inset 40px 0 0 #fbfbfc, inset 41px 0 0 #ececf0;
}
/* Specify class=linenums on a pre to get line numbering */
ol.linenums {
margin: 0 0 0 33px; /* IE indents via margin-left */
}
ol.linenums li {
padding-left: 12px;
color: #bebec5;
line-height: 20px;
text-shadow: 0 1px 0 #fff;
}
\ No newline at end of file
/* ==========================================================
* bootstrap-affix.js v2.1.1
* http://twitter.github.com/bootstrap/javascript.html#affix
* ==========================================================
* Copyright 2012 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================== */
!function ($) {
"use strict"; // jshint ;_;
/* AFFIX CLASS DEFINITION
* ====================== */
var Affix = function (element, options) {
this.options = $.extend({}, $.fn.affix.defaults, options)
this.$window = $(window).on('scroll.affix.data-api', $.proxy(this.checkPosition, this))
this.$element = $(element)
this.checkPosition()
}
Affix.prototype.checkPosition = function () {
if (!this.$element.is(':visible')) return
var scrollHeight = $(document).height()
, scrollTop = this.$window.scrollTop()
, position = this.$element.offset()
, offset = this.options.offset
, offsetBottom = offset.bottom
, offsetTop = offset.top
, reset = 'affix affix-top affix-bottom'
, affix
if (typeof offset != 'object') offsetBottom = offsetTop = offset
if (typeof offsetTop == 'function') offsetTop = offset.top()
if (typeof offsetBottom == 'function') offsetBottom = offset.bottom()
affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ?
false : offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ?
'bottom' : offsetTop != null && scrollTop <= offsetTop ?
'top' : false
if (this.affixed === affix) return
this.affixed = affix
this.unpin = affix == 'bottom' ? position.top - scrollTop : null
this.$element.removeClass(reset).addClass('affix' + (affix ? '-' + affix : ''))
}
/* AFFIX PLUGIN DEFINITION
* ======================= */
$.fn.affix = function (option) {
return this.each(function () {
var $this = $(this)
, data = $this.data('affix')
, options = typeof option == 'object' && option
if (!data) $this.data('affix', (data = new Affix(this, options)))
if (typeof option == 'string') data[option]()
})
}
$.fn.affix.Constructor = Affix
$.fn.affix.defaults = {
offset: 0
}
/* AFFIX DATA-API
* ============== */
$(window).on('load', function () {
$('[data-spy="affix"]').each(function () {
var $spy = $(this)
, data = $spy.data()
data.offset = data.offset || {}
data.offsetBottom && (data.offset.bottom = data.offsetBottom)
data.offsetTop && (data.offset.top = data.offsetTop)
$spy.affix(data)
})
})
}(window.jQuery);
\ No newline at end of file
/* ========================================================== /* ==========================================================
* bootstrap-alert.js v2.1.0 * bootstrap-alert.js v2.1.1
* http://twitter.github.com/bootstrap/javascript.html#alerts * http://twitter.github.com/bootstrap/javascript.html#alerts
* ========================================================== * ==========================================================
* Copyright 2012 Twitter, Inc. * Copyright 2012 Twitter, Inc.
......
/* ============================================================ /* ============================================================
* bootstrap-button.js v2.1.0 * bootstrap-button.js v2.1.1
* http://twitter.github.com/bootstrap/javascript.html#buttons * http://twitter.github.com/bootstrap/javascript.html#buttons
* ============================================================ * ============================================================
* Copyright 2012 Twitter, Inc. * Copyright 2012 Twitter, Inc.
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
} }
Button.prototype.toggle = function () { Button.prototype.toggle = function () {
var $parent = this.$element.parent('[data-toggle="buttons-radio"]') var $parent = this.$element.closest('[data-toggle="buttons-radio"]')
$parent && $parent $parent && $parent
.find('.active') .find('.active')
......
/* ========================================================== /* ==========================================================
* bootstrap-carousel.js v2.1.0 * bootstrap-carousel.js v2.1.1
* http://twitter.github.com/bootstrap/javascript.html#carousel * http://twitter.github.com/bootstrap/javascript.html#carousel
* ========================================================== * ==========================================================
* Copyright 2012 Twitter, Inc. * Copyright 2012 Twitter, Inc.
......
/* ============================================================= /* =============================================================
* bootstrap-collapse.js v2.1.0 * bootstrap-collapse.js v2.1.1
* http://twitter.github.com/bootstrap/javascript.html#collapse * http://twitter.github.com/bootstrap/javascript.html#collapse
* ============================================================= * =============================================================
* Copyright 2012 Twitter, Inc. * Copyright 2012 Twitter, Inc.
......
/* ============================================================ /* ============================================================
* bootstrap-dropdown.js v2.1.0 * bootstrap-dropdown.js v2.1.1
* http://twitter.github.com/bootstrap/javascript.html#dropdowns * http://twitter.github.com/bootstrap/javascript.html#dropdowns
* ============================================================ * ============================================================
* Copyright 2012 Twitter, Inc. * Copyright 2012 Twitter, Inc.
...@@ -110,7 +110,7 @@ ...@@ -110,7 +110,7 @@
if (!selector) { if (!selector) {
selector = $this.attr('href') selector = $this.attr('href')
selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7 selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
} }
$parent = $(selector) $parent = $(selector)
...@@ -142,7 +142,7 @@ ...@@ -142,7 +142,7 @@
$('html') $('html')
.on('click.dropdown.data-api touchstart.dropdown.data-api', clearMenus) .on('click.dropdown.data-api touchstart.dropdown.data-api', clearMenus)
$('body') $('body')
.on('click.dropdown touchstart.dropdown.data-api', '.dropdown', function (e) { e.stopPropagation() }) .on('click.dropdown touchstart.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
.on('click.dropdown.data-api touchstart.dropdown.data-api' , toggle, Dropdown.prototype.toggle) .on('click.dropdown.data-api touchstart.dropdown.data-api' , toggle, Dropdown.prototype.toggle)
.on('keydown.dropdown.data-api touchstart.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) .on('keydown.dropdown.data-api touchstart.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown)
}) })
......
/* ========================================================= /* =========================================================
* bootstrap-modal.js v2.1.0 * bootstrap-modal.js v2.1.1
* http://twitter.github.com/bootstrap/javascript.html#modals * http://twitter.github.com/bootstrap/javascript.html#modals
* ========================================================= * =========================================================
* Copyright 2012 Twitter, Inc. * Copyright 2012 Twitter, Inc.
......
/* =========================================================== /* ===========================================================
* bootstrap-popover.js v2.1.0 * bootstrap-popover.js v2.1.1
* http://twitter.github.com/bootstrap/javascript.html#popovers * http://twitter.github.com/bootstrap/javascript.html#popovers
* =========================================================== * ===========================================================
* Copyright 2012 Twitter, Inc. * Copyright 2012 Twitter, Inc.
......
/* ============================================================= /* =============================================================
* bootstrap-scrollspy.js v2.1.0 * bootstrap-scrollspy.js v2.1.1
* http://twitter.github.com/bootstrap/javascript.html#scrollspy * http://twitter.github.com/bootstrap/javascript.html#scrollspy
* ============================================================= * =============================================================
* Copyright 2012 Twitter, Inc. * Copyright 2012 Twitter, Inc.
......
/* ======================================================== /* ========================================================
* bootstrap-tab.js v2.1.0 * bootstrap-tab.js v2.1.1
* http://twitter.github.com/bootstrap/javascript.html#tabs * http://twitter.github.com/bootstrap/javascript.html#tabs
* ======================================================== * ========================================================
* Copyright 2012 Twitter, Inc. * Copyright 2012 Twitter, Inc.
......
/* =========================================================== /* ===========================================================
* bootstrap-tooltip.js v2.1.0 * bootstrap-tooltip.js v2.1.1
* http://twitter.github.com/bootstrap/javascript.html#tooltips * http://twitter.github.com/bootstrap/javascript.html#tooltips
* Inspired by the original jQuery.tipsy by Jason Frame * Inspired by the original jQuery.tipsy by Jason Frame
* =========================================================== * ===========================================================
......
/* =================================================== /* ===================================================
* bootstrap-transition.js v2.1.0 * bootstrap-transition.js v2.1.1
* http://twitter.github.com/bootstrap/javascript.html#transitions * http://twitter.github.com/bootstrap/javascript.html#transitions
* =================================================== * ===================================================
* Copyright 2012 Twitter, Inc. * Copyright 2012 Twitter, Inc.
......
/* ============================================================= /* =============================================================
* bootstrap-typeahead.js v2.1.0 * bootstrap-typeahead.js v2.1.1
* http://twitter.github.com/bootstrap/javascript.html#typeahead * http://twitter.github.com/bootstrap/javascript.html#typeahead
* ============================================================= * =============================================================
* Copyright 2012 Twitter, Inc. * Copyright 2012 Twitter, Inc.
...@@ -174,7 +174,7 @@ ...@@ -174,7 +174,7 @@
.on('keypress', $.proxy(this.keypress, this)) .on('keypress', $.proxy(this.keypress, this))
.on('keyup', $.proxy(this.keyup, this)) .on('keyup', $.proxy(this.keyup, this))
if ($.browser.webkit || $.browser.msie) { if ($.browser.chrome || $.browser.webkit || $.browser.msie) {
this.$element.on('keydown', $.proxy(this.keydown, this)) this.$element.on('keydown', $.proxy(this.keydown, this))
} }
......
...@@ -7,54 +7,16 @@ ...@@ -7,54 +7,16 @@
<meta name="author" content=""> <meta name="author" content="">
<!-- Le styles --> <!-- Le styles -->
<link href="{{ base_url }}/css/prettify.css" rel="stylesheet">
<link href="{{ base_url }}/css/bootstrap.css" rel="stylesheet"> <link href="{{ base_url }}/css/bootstrap.css" rel="stylesheet">
<style type="text/css">
body {
padding-top: 60px;
padding-bottom: 40px;
}
.sidebar-nav {
padding: 9px 0;
}
.nav-list li.main {
font-weight: bold;
}
blockquote {
font-family: Georgia, serif;
font-size: 18px;
font-style: italic;
margin: 0.25em 0;
padding: 0.25em 40px;
line-height: 1.45;
position: relative;
color: #383838;
border-left: none;
}
blockquote:before {
display: block;
content: "\201C";
font-size: 80px;
position: absolute;
left: -10px;
top: -20px;
color: #7a7a7a;
}
blockquote p:last-child {
color: #999999;
font-size: 14px;
display: block;
margin-top: 5px;
}
</style>
<link href="{{ base_url }}/css/bootstrap-responsive.css" rel="stylesheet"> <link href="{{ base_url }}/css/bootstrap-responsive.css" rel="stylesheet">
<link href="{{ base_url }}/css/drf-styles.css" rel="stylesheet">
<!-- Le HTML5 shim, for IE6-8 support of HTML5 elements --> <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script> <script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]--> <![endif]-->
<body> <body onload="prettyPrint()" class="{{ page_id }}">
<div class="navbar navbar-inverse navbar-fixed-top"> <div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner"> <div class="navbar-inner">
...@@ -108,14 +70,14 @@ margin-top: 5px; ...@@ -108,14 +70,14 @@ margin-top: 5px;
</li> </li>
</ul> </ul>
<ul class="nav pull-right"> <ul class="nav pull-right">
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Version: 2.0.0 <b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown">Version: 2.0.0 <b class="caret"></b></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="#">Trunk</a></li> <li><a href="#">Trunk</a></li>
<li><a href="#">2.0.0</a></li> <li><a href="#">2.0.0</a></li>
</ul> </ul>
</li> </li>
</ul> </ul>
</div><!--/.nav-collapse --> </div><!--/.nav-collapse -->
</div> </div>
</div> </div>
...@@ -123,16 +85,16 @@ margin-top: 5px; ...@@ -123,16 +85,16 @@ margin-top: 5px;
<div class="container-fluid"> <div class="container-fluid">
<div class="row-fluid"> <div class="row-fluid">
<div class="span3"> <div class="span3">
<div class="well affix span3"> <div id="table-of-contents" class="well affix span3">
<ul class="nav nav-list side-nav"> <ul class="nav nav-list side-nav">
{{ toc }} {{ toc }}
</ul> </ul>
</div> </div>
</div> </div>
<div class="span9"> <div id="main-content" class="span9">
{{ content }} {{ content }}
</div><!--/span--> </div><!--/span-->
</div><!--/row--> </div><!--/row-->
</div><!--/.fluid-container--> </div><!--/.fluid-container-->
...@@ -140,13 +102,15 @@ margin-top: 5px; ...@@ -140,13 +102,15 @@ margin-top: 5px;
<!-- Le javascript <!-- Le javascript
================================================== --> ================================================== -->
<!-- Placed at the end of the document so the pages load faster --> <!-- Placed at the end of the document so the pages load faster -->
<script src="{{ base_url }}/js/jquery.js"></script> <script src="{{ base_url }}/js/jquery-1.8.1-min.js"></script>
<script src="{{ base_url }}/js/prettify.js"></script>
<script src="{{ base_url }}/js/bootstrap-dropdown.js"></script> <script src="{{ base_url }}/js/bootstrap-dropdown.js"></script>
<script src="{{ base_url }}/js/bootstrap-scrollspy.js"></script> <script src="{{ base_url }}/js/bootstrap-scrollspy.js"></script>
<script src="{{ base_url }}/js/bootstrap-collapse.js"></script>
<script> <script>
//$('.side-nav').scrollspy() //$('.side-nav').scrollspy()
var shiftWindow = function() { scrollBy(0, -50) }; var shiftWindow = function() { scrollBy(0, -50) };
if (location.hash) shiftWindow(); if (location.hash) shiftWindow();
window.addEventListener("hashchange", shiftWindow); window.addEventListener("hashchange", shiftWindow);
</script> </script>
</body></html> </body></html>
\ No newline at end of file
...@@ -2,43 +2,44 @@ ...@@ -2,43 +2,44 @@
The following people have helped make REST framework great. The following people have helped make REST framework great.
* Tom Christie <tomchristie> * Tom Christie - [tomchristie]
* Marko Tibold <markotibold> * Marko Tibold - [markotibold]
* Paul Bagwell <pbgwl> * Paul Bagwell - [pbgwl]
* Sébastien Piquemal <sebpiq> * Sébastien Piquemal - [sebpiq]
* Carmen Wick <cwick> * Carmen Wick - [cwick]
* Alex Ehlke <aehlke> * Alex Ehlke - [aehlke]
* Alen Mujezinovic <flashingpumpkin> * Alen Mujezinovic - [flashingpumpkin]
* Carles Barrobés <txels> * Carles Barrobés - [txels]
* Michael Fötsch <mfoetsch> * Michael Fötsch - [mfoetsch]
* David Larlet <david> * David Larlet - [david]
* Andrew Straw <astraw> * Andrew Straw - [astraw]
* Zeth <zeth> * Zeth - [zeth]
* Fernando Zunino <fzunino> * Fernando Zunino - [fzunino]
* Jens Alm <ulmus> * Jens Alm - [ulmus]
* Craig Blaszczyk <jakul> * Craig Blaszczyk - [jakul]
* Garcia Solero <garciasolero> * Garcia Solero - [garciasolero]
* Tom Drummond <devioustree> * Tom Drummond - [devioustree]
* Danilo Bargen <gwrtheyrn> * Danilo Bargen - [gwrtheyrn]
* Andrew McCloud <amccloud> * Andrew McCloud - [amccloud]
* Thomas Steinacher <thomasst> * Thomas Steinacher - [thomasst]
* Meurig Freeman <meurig> * Meurig Freeman - [meurig]
* Anthony Nemitz <anemitz> * Anthony Nemitz - [anemitz]
* Ewoud Kohl van Wijngaarden <ekohl> * Ewoud Kohl van Wijngaarden - [ekohl]
* Michael Ding <yandy> * Michael Ding - [yandy]
* Mjumbe Poe <mjumbewu> * Mjumbe Poe - [mjumbewu]
* Natim <natim> * Natim - [natim]
* Sebastian Żurek <sebzur> * Sebastian Żurek - [sebzur]
* Benoit C <dzen> * Benoit C - [dzen]
* Chris Pickett <bunchesofdonald> * Chris Pickett - [bunchesofdonald]
* Ben Timby <btimby> * Ben Timby - [btimby]
* Michele Lazzeri <michelelazzeri-nextage> * Michele Lazzeri - [michelelazzeri-nextage]
* Camille Harang <mammique> * Camille Harang - [mammique]
* Paul Oswald <poswald> * Paul Oswald - [poswald]
* Sean C. Farley <scfarley> * Sean C. Farley - [scfarley]
* Daniel Izquierdo <izquierdo> * Daniel Izquierdo - [izquierdo]
* Can Yavuz <tschan> * Can Yavuz - [tschan]
* Shawn Lewis <shawnlewis> * Shawn Lewis - [shawnlewis]
* Alec Perkins - [alecperkins]
Many thanks to everyone who's contributed to the project. Many thanks to everyone who's contributed to the project.
...@@ -55,11 +56,49 @@ Continuous integration testing is managed with [Travis CI][travis-ci]. ...@@ -55,11 +56,49 @@ Continuous integration testing is managed with [Travis CI][travis-ci].
To contact the author directly: To contact the author directly:
* twitter: [@_tomchristie][twitter] * twitter: [@_tomchristie][twitter]
* mail: [tom@tomchristie.com][email] * email: [tom@tomchristie.com][email]
[email]: mailto:tom@tomchristie.com [email]: mailto:tom@tomchristie.com
[twitter]: http://twitter.com/_tomchristie [twitter]: http://twitter.com/_tomchristie
[bootstrap]: http://twitter.github.com/bootstrap/ [bootstrap]: http://twitter.github.com/bootstrap/
[markdown]: http://daringfireball.net/projects/markdown/ [markdown]: http://daringfireball.net/projects/markdown/
[github]: github.com/tomchristie/django-rest-framework [github]: github.com/tomchristie/django-rest-framework
[travis-ci]: https://secure.travis-ci.org/tomchristie/django-rest-framework [travis-ci]: https://secure.travis-ci.org/tomchristie/django-rest-framework
\ No newline at end of file [tomchristie]: https://github.com/tomchristie
[markotibold]: https://github.com/markotibold
[pbgwl]: https://github.com/pbgwl
[sebpiq]: https://github.com/sebpiq
[cwick]: https://github.com/cwick
[aehlke]: https://github.com/aehlke
[flashingpumpkin]: https://github.com/flashingpumpkin
[txels]: https://github.com/txels
[mfoetsch]: https://github.com/mfoetsch
[david]: https://github.com/david
[astraw]: https://github.com/astraw
[zeth]: https://github.com/zeth
[fzunino]: https://github.com/fzunino
[ulmus]: https://github.com/ulmus
[jakul]: https://github.com/jakul
[garciasolero]: https://github.com/garciasolero
[devioustree]: https://github.com/devioustree
[gwrtheyrn]: https://github.com/gwrtheyrn
[amccloud]: https://github.com/amccloud
[thomasst]: https://github.com/thomasst
[meurig]: https://github.com/meurig
[anemitz]: https://github.com/anemitz
[ekohl]: https://github.com/ekohl
[yandy]: https://github.com/yandy
[mjumbewu]: https://github.com/mjumbewu
[natim]: https://github.com/natim
[sebzur]: https://github.com/sebzur
[dzen]: https://github.com/dzen
[bunchesofdonald]: https://github.com/bunchesofdonald
[btimby]: https://github.com/btimby
[michelelazzeri-nextage]: https://github.com/michelelazzeri-nextage
[mammique]: https://github.com/mammique
[poswald]: https://github.com/poswald
[scfarley]: https://github.com/scfarley
[izquierdo]: https://github.com/izquierdo
[tschan]: https://github.com/tschan
[shawnlewis]: https://github.com/shawnlewis
[alecperkins]: https://github.com/alecperkins
...@@ -8,6 +8,7 @@ This tutorial will walk you through the building blocks that make up REST framew ...@@ -8,6 +8,7 @@ This tutorial will walk you through the building blocks that make up REST framew
Before we do anything else we'll create a new virtual environment, using [virtualenv]. This will make sure our package configuration is keep nicely isolated from any other projects we're working on. Before we do anything else we'll create a new virtual environment, using [virtualenv]. This will make sure our package configuration is keep nicely isolated from any other projects we're working on.
:::bash
mkdir ~/env mkdir ~/env
virtualenv --no-site-packages ~/env/tutorial virtualenv --no-site-packages ~/env/tutorial
source ~/env/tutorial/bin/activate source ~/env/tutorial/bin/activate
......
...@@ -7,30 +7,31 @@ We can also write our API views using class based views, rather than function ba ...@@ -7,30 +7,31 @@ We can also write our API views using class based views, rather than function ba
We'll start by rewriting the root view as a class based view. All this involves is a little bit of refactoring. We'll start by rewriting the root view as a class based view. All this involves is a little bit of refactoring.
from blog.models import Comment from blog.models import Comment
from blog.serializers import ComentSerializer from blog.serializers import CommentSerializer
from django.http import Http404 from django.http import Http404
from djangorestframework.views import APIView from djangorestframework.views import APIView
from djangorestframework.response import Response from djangorestframework.response import Response
from djangorestframework.status import status from djangorestframework import status
class CommentRoot(APIView): class CommentRoot(APIView):
""" """
List all comments, or create a new comment. List all comments, or create a new comment.
""" """
def get(self, request, format=None): def get(self, request, format=None):
comments = Comment.objects.all() comments = Comment.objects.all()
serializer = ComentSerializer(instance=comments) serializer = CommentSerializer(instance=comments)
return Response(serializer.data) return Response(serializer.data)
def post(self, request, format=None) def post(self, request, format=None):
serializer = ComentSerializer(request.DATA) serializer = CommentSerializer(request.DATA)
if serializer.is_valid(): if serializer.is_valid():
comment = serializer.object comment = serializer.object
comment.save() comment.save()
return Response(serializer.serialized, status=HTTP_201_CREATED) return Response(serializer.serialized, status=status.HTTP_201_CREATED)
return Response(serializer.serialized_errors, status=HTTP_400_BAD_REQUEST) return Response(serializer.serialized_errors, status=status.HTTP_400_BAD_REQUEST)
comment_root = CommentRoot.as_view() comment_root = CommentRoot.as_view()
So far, so good. It looks pretty similar to the previous case, but we've got better seperation between the different HTTP methods. We'll also need to update the instance view. So far, so good. It looks pretty similar to the previous case, but we've got better seperation between the different HTTP methods. We'll also need to update the instance view.
...@@ -38,18 +39,18 @@ So far, so good. It looks pretty similar to the previous case, but we've got be ...@@ -38,18 +39,18 @@ So far, so good. It looks pretty similar to the previous case, but we've got be
""" """
Retrieve, update or delete a comment instance. Retrieve, update or delete a comment instance.
""" """
def get_object(self, pk): def get_object(self, pk):
try: try:
return Comment.objects.get(pk=pk) return Comment.objects.get(pk=pk)
except Comment.DoesNotExist: except Comment.DoesNotExist:
raise Http404 raise Http404
def get(self, request, pk, format=None): def get(self, request, pk, format=None):
comment = self.get_object(pk) comment = self.get_object(pk)
serializer = CommentSerializer(instance=comment) serializer = CommentSerializer(instance=comment)
return Response(serializer.data) return Response(serializer.data)
def put(self, request, pk, format=None): def put(self, request, pk, format=None):
comment = self.get_object(pk) comment = self.get_object(pk)
serializer = CommentSerializer(request.DATA, instance=comment) serializer = CommentSerializer(request.DATA, instance=comment)
...@@ -64,7 +65,7 @@ So far, so good. It looks pretty similar to the previous case, but we've got be ...@@ -64,7 +65,7 @@ So far, so good. It looks pretty similar to the previous case, but we've got be
comment.delete() comment.delete()
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
comment_instance = CommentInstance.as_view() comment_instance = CommentInstance.as_view()
That's looking good. Again, it's still pretty similar to the function based view right now. That's looking good. Again, it's still pretty similar to the function based view right now.
Okay, we're done. If you run the development server everything should be working just as before. Okay, we're done. If you run the development server everything should be working just as before.
......
...@@ -44,6 +44,6 @@ We've reached the end of our tutorial. If you want to get more involved in the ...@@ -44,6 +44,6 @@ We've reached the end of our tutorial. If you want to get more involved in the
* Contribute on GitHub by reviewing issues, and submitting issues or pull requests. * Contribute on GitHub by reviewing issues, and submitting issues or pull requests.
* Join the REST framework group, and help build the community. * Join the REST framework group, and help build the community.
* Follow me on Twitter and say hi. * Follow me [on Twitter](https://twitter.com/_tomchristie) and say hi.
Now go build something great. Now go build something great.
\ No newline at end of file
...@@ -24,11 +24,12 @@ else: ...@@ -24,11 +24,12 @@ else:
main_header = '<li class="main"><a href="#{{ anchor }}">{{ title }}</a></li>' main_header = '<li class="main"><a href="#{{ anchor }}">{{ title }}</a></li>'
sub_header = '<li><a href="#{{ anchor }}">{{ title }}</a></li>' sub_header = '<li><a href="#{{ anchor }}">{{ title }}</a></li>'
code_label = r'<a class="github" href="https://github.com/tomchristie/django-rest-framework/blob/restframework2/djangorestframework/\1"><span class="label label-info">\1</span></a>'
page = open(os.path.join(docs_dir, 'template.html'), 'r').read() page = open(os.path.join(docs_dir, 'template.html'), 'r').read()
# Copy static files # Copy static files
for static in ['css', 'js']: for static in ['css', 'js', 'img']:
source = os.path.join(docs_dir, 'static', static) source = os.path.join(docs_dir, 'static', static)
target = os.path.join(html_dir, static) target = os.path.join(html_dir, static)
if os.path.exists(target): if os.path.exists(target):
...@@ -65,5 +66,9 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir): ...@@ -65,5 +66,9 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
if not os.path.exists(build_dir): if not os.path.exists(build_dir):
os.makedirs(build_dir) os.makedirs(build_dir)
output = page.replace('{{ content }}', content).replace('{{ toc }}', toc).replace('{{ base_url }}', base_url).replace('{{ suffix }}', suffix).replace('{{ index }}', index) output = page.replace('{{ content }}', content).replace('{{ toc }}', toc).replace('{{ base_url }}', base_url).replace('{{ suffix }}', suffix).replace('{{ index }}', index)
output = output.replace('{{ page_id }}', filename[:-3])
output = re.sub(r'a href="([^"]*)\.md"', r'a href="\1%s"' % suffix, output) output = re.sub(r'a href="([^"]*)\.md"', r'a href="\1%s"' % suffix, output)
output = re.sub(r'<pre><code>:::bash', r'<pre class="prettyprint lang-bsh">', output)
output = re.sub(r'<pre>', r'<pre class="prettyprint lang-py">', output)
output = re.sub(r'<a class="github" href="([^"]*)"></a>', code_label, output)
open(build_file, 'w').write(output.encode('utf-8')) open(build_file, 'w').write(output.encode('utf-8'))
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment