aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--djangorestframework/authentication.py50
-rw-r--r--djangorestframework/authtoken/__init__.py (renamed from djangorestframework/tokenauth/__init__.py)0
-rw-r--r--djangorestframework/authtoken/migrations/0001_initial.py72
-rw-r--r--djangorestframework/authtoken/migrations/__init__.py (renamed from djangorestframework/tokenauth/views.py)0
-rw-r--r--djangorestframework/authtoken/models.py23
-rw-r--r--djangorestframework/authtoken/views.py0
-rw-r--r--djangorestframework/permissions.py12
-rw-r--r--djangorestframework/renderers.py2
-rw-r--r--djangorestframework/request.py14
-rw-r--r--djangorestframework/response.py18
-rw-r--r--djangorestframework/runtests/settings.py2
-rw-r--r--djangorestframework/tests/authentication.py14
-rw-r--r--djangorestframework/tests/renderers.py14
-rw-r--r--djangorestframework/tests/request.py2
-rw-r--r--djangorestframework/tests/response.py34
-rw-r--r--djangorestframework/tests/throttling.py30
-rw-r--r--djangorestframework/throttling.py130
-rw-r--r--djangorestframework/tokenauth/models.py15
-rw-r--r--djangorestframework/views.py24
-rw-r--r--docs/api-guide/authentication.md36
-rw-r--r--docs/api-guide/content-negotiation.md7
-rw-r--r--docs/api-guide/contentnegotiation.md1
-rw-r--r--docs/api-guide/exceptions.md79
-rw-r--r--docs/api-guide/format-suffixes.md11
-rw-r--r--docs/api-guide/generic-views.md10
-rw-r--r--docs/api-guide/permissions.md99
-rw-r--r--docs/api-guide/reverse.md4
-rw-r--r--docs/api-guide/settings.md10
-rw-r--r--docs/api-guide/throttling.md148
-rw-r--r--docs/index.md10
-rw-r--r--docs/static/css/default.css (renamed from docs/static/css/drf-styles.css)44
-rw-r--r--docs/template.html12
-rw-r--r--docs/topics/changelog.md107
-rw-r--r--docs/tutorial/6-resource-orientated-projects.md37
34 files changed, 878 insertions, 193 deletions
diff --git a/djangorestframework/authentication.py b/djangorestframework/authentication.py
index 2446fbbd..a6634392 100644
--- a/djangorestframework/authentication.py
+++ b/djangorestframework/authentication.py
@@ -6,14 +6,9 @@ Authentication behavior is provided by mixing the :class:`mixins.RequestMixin` c
from django.contrib.auth import authenticate
from djangorestframework.compat import CsrfViewMiddleware
+from djangorestframework.authtoken.models import Token
import base64
-__all__ = (
- 'BaseAuthentication',
- 'BasicAuthentication',
- 'SessionAuthentication'
-)
-
class BaseAuthentication(object):
"""
@@ -105,36 +100,33 @@ class SessionAuthentication(BaseAuthentication):
class TokenAuthentication(BaseAuthentication):
"""
- Use a token model for authentication.
+ Simple token based authentication.
- A custom token model may be used here, but must have the following minimum
- properties:
+ Clients should authenticate by passing the token key in the "Authorization"
+ HTTP header, prepended with the string "Token ". For example:
- * 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: Token 401f7ac837da42b97f613d789819ff93537bee6a
+ """
- Authorization: 0123456789abcdef0123456789abcdef
+ model = Token
+ """
+ A custom token model may be used, but must have the following properties.
+ * key -- The string identifying the token
+ * user -- The user to which the token belongs
"""
- 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
+ auth = request.META.get('HTTP_AUTHORIZATION', '').split()
- try:
- token = self.model.objects.get(key=key)
- except self.model.DoesNotExist:
- return None
+ if len(auth) == 2 and auth[0].lower() == "token":
+ key = auth[1]
+ 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)
+ if token.user.is_active and not getattr(token, 'revoked', False):
+ return (token.user, token)
-# TODO: DigestAuthentication, OAuthAuthentication
+# TODO: OAuthAuthentication
diff --git a/djangorestframework/tokenauth/__init__.py b/djangorestframework/authtoken/__init__.py
index e69de29b..e69de29b 100644
--- a/djangorestframework/tokenauth/__init__.py
+++ b/djangorestframework/authtoken/__init__.py
diff --git a/djangorestframework/authtoken/migrations/0001_initial.py b/djangorestframework/authtoken/migrations/0001_initial.py
new file mode 100644
index 00000000..a91006b0
--- /dev/null
+++ b/djangorestframework/authtoken/migrations/0001_initial.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding model 'Token'
+ db.create_table('authtoken_token', (
+ ('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)),
+ ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
+ ('revoked', self.gf('django.db.models.fields.BooleanField')(default=False)),
+ ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+ ))
+ db.send_create_signal('authtoken', ['Token'])
+
+
+ def backwards(self, orm):
+ # Deleting model 'Token'
+ db.delete_table('authtoken_token')
+
+
+ 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': {
+ 'Meta': {'object_name': 'User'},
+ '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'}),
+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ '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'})
+ },
+ 'authtoken.token': {
+ 'Meta': {'object_name': 'Token'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}),
+ 'revoked': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+ 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
+ },
+ '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'})
+ }
+ }
+
+ complete_apps = ['authtoken'] \ No newline at end of file
diff --git a/djangorestframework/tokenauth/views.py b/djangorestframework/authtoken/migrations/__init__.py
index e69de29b..e69de29b 100644
--- a/djangorestframework/tokenauth/views.py
+++ b/djangorestframework/authtoken/migrations/__init__.py
diff --git a/djangorestframework/authtoken/models.py b/djangorestframework/authtoken/models.py
new file mode 100644
index 00000000..fd47e6c7
--- /dev/null
+++ b/djangorestframework/authtoken/models.py
@@ -0,0 +1,23 @@
+import uuid
+import hmac
+from hashlib import sha1
+from django.db import models
+
+
+class Token(models.Model):
+ """
+ The default authorization token model.
+ """
+ key = models.CharField(max_length=40, primary_key=True)
+ user = models.ForeignKey('auth.User')
+ revoked = models.BooleanField(default=False)
+ created = models.DateTimeField(auto_now_add=True)
+
+ def save(self, *args, **kwargs):
+ if not self.key:
+ self.key = self.generate_key()
+ return super(Token, self).save(*args, **kwargs)
+
+ def generate_key(self):
+ unique = str(uuid.uuid4())
+ return hmac.new(unique, digestmod=sha1).hexdigest()
diff --git a/djangorestframework/authtoken/views.py b/djangorestframework/authtoken/views.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/djangorestframework/authtoken/views.py
diff --git a/djangorestframework/permissions.py b/djangorestframework/permissions.py
index 64e455f5..3a669822 100644
--- a/djangorestframework/permissions.py
+++ b/djangorestframework/permissions.py
@@ -28,11 +28,11 @@ class BasePermission(object):
"""
self.view = view
- def check_permission(self, request, obj=None):
+ def has_permission(self, request, obj=None):
"""
Should simply return, or raise an :exc:`response.ImmediateResponse`.
"""
- raise NotImplementedError(".check_permission() must be overridden.")
+ raise NotImplementedError(".has_permission() must be overridden.")
class IsAuthenticated(BasePermission):
@@ -40,7 +40,7 @@ class IsAuthenticated(BasePermission):
Allows access only to authenticated users.
"""
- def check_permission(self, request, obj=None):
+ def has_permission(self, request, obj=None):
if request.user and request.user.is_authenticated():
return True
return False
@@ -51,7 +51,7 @@ class IsAdminUser(BasePermission):
Allows access only to admin users.
"""
- def check_permission(self, request, obj=None):
+ def has_permission(self, request, obj=None):
if request.user and request.user.is_staff:
return True
return False
@@ -62,7 +62,7 @@ class IsAuthenticatedOrReadOnly(BasePermission):
The request is authenticated as a user, or is a read-only request.
"""
- def check_permission(self, request, obj=None):
+ def has_permission(self, request, obj=None):
if (request.method in SAFE_METHODS or
request.user and
request.user.is_authenticated()):
@@ -105,7 +105,7 @@ class DjangoModelPermissions(BasePermission):
}
return [perm % kwargs for perm in self.perms_map[method]]
- def check_permission(self, request, obj=None):
+ def has_permission(self, request, obj=None):
model_cls = self.view.model
perms = self.get_required_permissions(request.method, model_cls)
diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py
index 45cdbbbb..26e8cba1 100644
--- a/djangorestframework/renderers.py
+++ b/djangorestframework/renderers.py
@@ -216,7 +216,7 @@ class DocumentingTemplateRenderer(BaseRenderer):
"""
# Find the first valid renderer and render the content. (Don't use another documenting renderer.)
- renderers = [renderer for renderer in view.renderers
+ renderers = [renderer for renderer in view.renderer_classes
if not issubclass(renderer, DocumentingTemplateRenderer)]
if not renderers:
return '[No renderers were found]'
diff --git a/djangorestframework/request.py b/djangorestframework/request.py
index 83ee47c6..450d2ac7 100644
--- a/djangorestframework/request.py
+++ b/djangorestframework/request.py
@@ -37,9 +37,9 @@ class Request(object):
Kwargs:
- request(HttpRequest). The original request instance.
- - parsers(list/tuple). The parsers to use for parsing the
+ - parsers_classes(list/tuple). The parsers to use for parsing the
request content.
- - authentications(list/tuple). The authentications used to try
+ - authentication_classes(list/tuple). The authentications used to try
authenticating the request's user.
"""
@@ -47,10 +47,10 @@ class Request(object):
_CONTENT_PARAM = api_settings.FORM_CONTENT_OVERRIDE
_CONTENTTYPE_PARAM = api_settings.FORM_CONTENTTYPE_OVERRIDE
- def __init__(self, request, parsers=None, authentication=None):
+ def __init__(self, request, parser_classes=None, authentication_classes=None):
self._request = request
- self.parsers = parsers or ()
- self.authentication = authentication or ()
+ self.parser_classes = parser_classes or ()
+ self.authentication_classes = authentication_classes or ()
self._data = Empty
self._files = Empty
self._method = Empty
@@ -61,13 +61,13 @@ class Request(object):
"""
Instantiates and returns the list of parsers the request will use.
"""
- return [parser() for parser in self.parsers]
+ return [parser() for parser in self.parser_classes]
def get_authentications(self):
"""
Instantiates and returns the list of parsers the request will use.
"""
- return [authentication() for authentication in self.authentication]
+ return [authentication() for authentication in self.authentication_classes]
@property
def method(self):
diff --git a/djangorestframework/response.py b/djangorestframework/response.py
index 08e14199..e1366bdb 100644
--- a/djangorestframework/response.py
+++ b/djangorestframework/response.py
@@ -35,14 +35,14 @@ class Response(SimpleTemplateResponse):
- content(object). The raw content, not yet serialized.
This must be native Python data that renderers can handle.
(e.g.: `dict`, `str`, ...)
- - renderers(list/tuple). The renderers to use for rendering the response content.
+ - renderer_classes(list/tuple). The renderers to use for rendering the response content.
"""
_ACCEPT_QUERY_PARAM = api_settings.URL_ACCEPT_OVERRIDE
_IGNORE_IE_ACCEPT_HEADER = True
def __init__(self, content=None, status=None, headers=None, view=None,
- request=None, renderers=None, format=None):
+ request=None, renderer_classes=None, format=None):
# First argument taken by `SimpleTemplateResponse.__init__` is template_name,
# which we don't need
super(Response, self).__init__(None, status=status)
@@ -52,17 +52,17 @@ class Response(SimpleTemplateResponse):
self.headers = headers and headers[:] or []
self.view = view
self.request = request
- self.renderers = renderers
+ self.renderer_classes = renderer_classes
self.format = format
def get_renderers(self):
"""
Instantiates and returns the list of renderers the response will use.
"""
- if self.renderers is None:
+ if self.renderer_classes is None:
renderer_classes = api_settings.DEFAULT_RENDERERS
else:
- renderer_classes = self.renderers
+ renderer_classes = self.renderer_classes
if self.format:
return [cls(self.view) for cls in renderer_classes
@@ -133,7 +133,7 @@ class Response(SimpleTemplateResponse):
def _determine_renderer(self):
"""
Determines the appropriate renderer for the output, given the list of
- accepted media types, and the :attr:`renderers` set on this class.
+ accepted media types, and the :attr:`renderer_classes` set on this class.
Returns a 2-tuple of `(renderer, media_type)`
@@ -162,12 +162,12 @@ class Response(SimpleTemplateResponse):
raise NotAcceptable
def _get_406_response(self):
- renderer = self.renderers[0]
+ renderer = self.renderer_classes[0]
return Response(
{
'detail': 'Could not satisfy the client\'s Accept header',
'available_types': [renderer.media_type
- for renderer in self.renderers]
+ for renderer in self.renderer_classes]
},
status=status.HTTP_406_NOT_ACCEPTABLE,
- view=self.view, request=self.request, renderers=[renderer])
+ view=self.view, request=self.request, renderer_classes=[renderer])
diff --git a/djangorestframework/runtests/settings.py b/djangorestframework/runtests/settings.py
index 1fc6b47b..da2ae5b8 100644
--- a/djangorestframework/runtests/settings.py
+++ b/djangorestframework/runtests/settings.py
@@ -90,7 +90,7 @@ INSTALLED_APPS = (
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
'djangorestframework',
- 'djangorestframework.tokenauth',
+ 'djangorestframework.authtoken',
)
STATIC_URL = '/static/'
diff --git a/djangorestframework/tests/authentication.py b/djangorestframework/tests/authentication.py
index fcc8f7ba..ddbee4b6 100644
--- a/djangorestframework/tests/authentication.py
+++ b/djangorestframework/tests/authentication.py
@@ -8,7 +8,7 @@ from django.http import HttpResponse
from djangorestframework.views import APIView
from djangorestframework import permissions
-from djangorestframework.tokenauth.models import BasicToken
+from djangorestframework.authtoken.models import Token
from djangorestframework.authentication import TokenAuthentication
import base64
@@ -23,7 +23,7 @@ class MockView(APIView):
def put(self, request):
return HttpResponse({'a': 1, 'b': 2, 'c': 3})
-MockView.authentication += (TokenAuthentication,)
+MockView.authentication_classes += (TokenAuthentication,)
urlpatterns = patterns('',
(r'^$', MockView.as_view()),
@@ -123,17 +123,17 @@ class TokenAuthTests(TestCase):
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)
+ self.token = Token.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
+ auth = "Token " + 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
+ auth = "Token " + self.key
response = self.csrf_client.post('/', json.dumps({'example': 'example'}), 'application/json', HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)
@@ -149,5 +149,5 @@ class TokenAuthTests(TestCase):
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)
+ token = Token.objects.create(user=self.user)
+ self.assertTrue(bool(token.key))
diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py
index 692243e6..718c903f 100644
--- a/djangorestframework/tests/renderers.py
+++ b/djangorestframework/tests/renderers.py
@@ -51,7 +51,7 @@ class RendererB(BaseRenderer):
class MockView(APIView):
- renderers = (RendererA, RendererB)
+ renderer_classes = (RendererA, RendererB)
def get(self, request, **kwargs):
response = Response(DUMMYCONTENT, status=DUMMYSTATUS)
@@ -65,23 +65,23 @@ class MockGETView(APIView):
class HTMLView(APIView):
- renderers = (DocumentingHTMLRenderer, )
+ renderer_classes = (DocumentingHTMLRenderer, )
def get(self, request, **kwargs):
return Response('text')
class HTMLView1(APIView):
- renderers = (DocumentingHTMLRenderer, JSONRenderer)
+ renderer_classes = (DocumentingHTMLRenderer, JSONRenderer)
def get(self, request, **kwargs):
return Response('text')
urlpatterns = patterns('',
- url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderers=[RendererA, RendererB])),
- url(r'^$', MockView.as_view(renderers=[RendererA, RendererB])),
- url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderers=[JSONRenderer, JSONPRenderer])),
- url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderers=[JSONPRenderer])),
+ url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
+ url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
+ url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])),
+ url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])),
url(r'^html$', HTMLView.as_view()),
url(r'^html1$', HTMLView1.as_view()),
url(r'^api', include('djangorestframework.urls', namespace='djangorestframework'))
diff --git a/djangorestframework/tests/request.py b/djangorestframework/tests/request.py
index 8b2f66ee..51e3660c 100644
--- a/djangorestframework/tests/request.py
+++ b/djangorestframework/tests/request.py
@@ -217,7 +217,7 @@ class TestContentParsing(TestCase):
class MockView(APIView):
- authentication = (SessionAuthentication,)
+ authentication_classes = (SessionAuthentication,)
def post(self, request):
if request.POST.get('example') is not None:
diff --git a/djangorestframework/tests/response.py b/djangorestframework/tests/response.py
index a0b76610..0483d826 100644
--- a/djangorestframework/tests/response.py
+++ b/djangorestframework/tests/response.py
@@ -26,12 +26,12 @@ class MockJsonRenderer(BaseRenderer):
class TestResponseDetermineRenderer(TestCase):
- def get_response(self, url='', accept_list=[], renderers=[]):
+ def get_response(self, url='', accept_list=[], renderer_classes=[]):
kwargs = {}
if accept_list is not None:
kwargs['HTTP_ACCEPT'] = ','.join(accept_list)
request = RequestFactory().get(url, **kwargs)
- return Response(request=request, renderers=renderers)
+ return Response(request=request, renderer_classes=renderer_classes)
def test_determine_accept_list_accept_header(self):
"""
@@ -62,14 +62,14 @@ class TestResponseDetermineRenderer(TestCase):
Test that right renderer is chosen, in the order of Accept list.
"""
accept_list = ['application/pickle', 'application/json']
- renderers = (MockPickleRenderer, MockJsonRenderer)
- response = self.get_response(accept_list=accept_list, renderers=renderers)
+ renderer_classes = (MockPickleRenderer, MockJsonRenderer)
+ response = self.get_response(accept_list=accept_list, renderer_classes=renderer_classes)
renderer, media_type = response._determine_renderer()
self.assertEqual(media_type, 'application/pickle')
self.assertTrue(isinstance(renderer, MockPickleRenderer))
- renderers = (MockJsonRenderer, )
- response = self.get_response(accept_list=accept_list, renderers=renderers)
+ renderer_classes = (MockJsonRenderer, )
+ response = self.get_response(accept_list=accept_list, renderer_classes=renderer_classes)
renderer, media_type = response._determine_renderer()
self.assertEqual(media_type, 'application/json')
self.assertTrue(isinstance(renderer, MockJsonRenderer))
@@ -78,8 +78,8 @@ class TestResponseDetermineRenderer(TestCase):
"""
Test determine renderer when Accept was not specified.
"""
- renderers = (MockPickleRenderer, )
- response = self.get_response(accept_list=None, renderers=renderers)
+ renderer_classes = (MockPickleRenderer, )
+ response = self.get_response(accept_list=None, renderer_classes=renderer_classes)
renderer, media_type = response._determine_renderer()
self.assertEqual(media_type, '*/*')
self.assertTrue(isinstance(renderer, MockPickleRenderer))
@@ -89,15 +89,15 @@ class TestResponseDetermineRenderer(TestCase):
Test determine renderer when no renderer can satisfy the Accept list.
"""
accept_list = ['application/json']
- renderers = (MockPickleRenderer, )
- response = self.get_response(accept_list=accept_list, renderers=renderers)
+ renderer_classes = (MockPickleRenderer, )
+ response = self.get_response(accept_list=accept_list, renderer_classes=renderer_classes)
self.assertRaises(NotAcceptable, response._determine_renderer)
class TestResponseRenderContent(TestCase):
- def get_response(self, url='', accept_list=[], content=None, renderers=None):
+ def get_response(self, url='', accept_list=[], content=None, renderer_classes=None):
request = RequestFactory().get(url, HTTP_ACCEPT=','.join(accept_list))
- return Response(request=request, content=content, renderers=renderers or DEFAULT_RENDERERS)
+ return Response(request=request, content=content, renderer_classes=renderer_classes or DEFAULT_RENDERERS)
def test_render(self):
"""
@@ -168,29 +168,29 @@ class RendererB(BaseRenderer):
class MockView(APIView):
- renderers = (RendererA, RendererB)
+ renderer_classes = (RendererA, RendererB)
def get(self, request, **kwargs):
return Response(DUMMYCONTENT, status=DUMMYSTATUS)
class HTMLView(APIView):
- renderers = (DocumentingHTMLRenderer, )
+ renderer_classes = (DocumentingHTMLRenderer, )
def get(self, request, **kwargs):
return Response('text')
class HTMLView1(APIView):
- renderers = (DocumentingHTMLRenderer, JSONRenderer)
+ renderer_classes = (DocumentingHTMLRenderer, JSONRenderer)
def get(self, request, **kwargs):
return Response('text')
urlpatterns = patterns('',
- url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderers=[RendererA, RendererB])),
- url(r'^$', MockView.as_view(renderers=[RendererA, RendererB])),
+ url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
+ url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
url(r'^html$', HTMLView.as_view()),
url(r'^html1$', HTMLView1.as_view()),
url(r'^restframework', include('djangorestframework.urls', namespace='djangorestframework'))
diff --git a/djangorestframework/tests/throttling.py b/djangorestframework/tests/throttling.py
index d144d956..3033614f 100644
--- a/djangorestframework/tests/throttling.py
+++ b/djangorestframework/tests/throttling.py
@@ -8,24 +8,32 @@ from django.core.cache import cache
from djangorestframework.compat import RequestFactory
from djangorestframework.views import APIView
-from djangorestframework.throttling import PerUserThrottling, PerViewThrottling
+from djangorestframework.throttling import UserRateThrottle
from djangorestframework.response import Response
-class MockView(APIView):
- throttle_classes = (PerUserThrottling,)
+class User3SecRateThrottle(UserRateThrottle):
rate = '3/sec'
+ scope = 'seconds'
+
+
+class User3MinRateThrottle(UserRateThrottle):
+ rate = '3/min'
+ scope = 'minutes'
+
+
+class MockView(APIView):
+ throttle_classes = (User3SecRateThrottle,)
def get(self, request):
return Response('foo')
-class MockView_PerViewThrottling(MockView):
- throttle_classes = (PerViewThrottling,)
+class MockView_MinuteThrottling(APIView):
+ throttle_classes = (User3MinRateThrottle,)
-
-class MockView_MinuteThrottling(MockView):
- rate = '3/min'
+ def get(self, request):
+ return Response('foo')
class ThrottlingTests(TestCase):
@@ -86,12 +94,6 @@ class ThrottlingTests(TestCase):
"""
self.ensure_is_throttled(MockView, 200)
- def test_request_throttling_is_per_view(self):
- """
- Ensure request rate is limited globally per View for PerViewThrottles
- """
- self.ensure_is_throttled(MockView_PerViewThrottling, 429)
-
def ensure_response_header_contains_proper_throttle_field(self, view, expected_headers):
"""
Ensure the response returns an X-Throttle field with status and next attributes
diff --git a/djangorestframework/throttling.py b/djangorestframework/throttling.py
index a096eab7..6249bd42 100644
--- a/djangorestframework/throttling.py
+++ b/djangorestframework/throttling.py
@@ -1,4 +1,5 @@
from django.core.cache import cache
+from djangorestframework.settings import api_settings
import time
@@ -13,11 +14,11 @@ class BaseThrottle(object):
"""
self.view = view
- def check_throttle(self, request):
+ def allow_request(self, request):
"""
Return `True` if the request should be allowed, `False` otherwise.
"""
- raise NotImplementedError('.check_throttle() must be overridden')
+ raise NotImplementedError('.allow_request() must be overridden')
def wait(self):
"""
@@ -27,7 +28,7 @@ class BaseThrottle(object):
return None
-class SimpleCachingThrottle(BaseThrottle):
+class SimpleRateThottle(BaseThrottle):
"""
A simple cache implementation, that only requires `.get_cache_key()`
to be overridden.
@@ -41,33 +42,51 @@ class SimpleCachingThrottle(BaseThrottle):
Previous request information used for throttling is stored in the cache.
"""
- attr_name = 'rate'
- rate = '1000/day'
timer = time.time
+ settings = api_settings
+ cache_format = 'throtte_%(scope)s_%(ident)s'
+ scope = None
def __init__(self, view):
- """
- Check the throttling.
- Return `None` or raise an :exc:`.ImmediateResponse`.
- """
- super(SimpleCachingThrottle, self).__init__(view)
- num, period = getattr(view, self.attr_name, self.rate).split('/')
- self.num_requests = int(num)
- self.duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
+ super(SimpleRateThottle, self).__init__(view)
+ rate = self.get_rate_description()
+ self.num_requests, self.duration = self.parse_rate_description(rate)
def get_cache_key(self, request):
"""
Should return a unique cache-key which can be used for throttling.
Must be overridden.
+
+ May return `None` if the request should not be throttled.
"""
raise NotImplementedError('.get_cache_key() must be overridden')
- def check_throttle(self, request):
+ def get_rate_description(self):
+ """
+ Determine the string representation of the allowed request rate.
+ """
+ try:
+ return self.rate
+ except AttributeError:
+ return self.settings.DEFAULT_THROTTLE_RATES.get(self.scope)
+
+ def parse_rate_description(self, rate):
+ """
+ Given the request rate string, return a two tuple of:
+ <allowed number of requests>, <period of time in seconds>
+ """
+ assert rate, "No throttle rate set for '%s'" % self.__class__.__name__
+ num, period = rate.split('/')
+ num_requests = int(num)
+ duration = {'s': 1, 'm': 60, 'h': 3600, 'd': 86400}[period[0]]
+ return (num_requests, duration)
+
+ def allow_request(self, request):
"""
Implement the check to see if the request should be throttled.
- On success calls :meth:`throttle_success`.
- On failure calls :meth:`throttle_failure`.
+ On success calls `throttle_success`.
+ On failure calls `throttle_failure`.
"""
self.key = self.get_cache_key(request)
self.history = cache.get(self.key, [])
@@ -110,30 +129,89 @@ class SimpleCachingThrottle(BaseThrottle):
return remaining_duration / float(available_requests)
-class PerUserThrottling(SimpleCachingThrottle):
+class AnonRateThrottle(SimpleRateThottle):
+ """
+ Limits the rate of API calls that may be made by a anonymous users.
+
+ The IP address of the request will be used as the unqiue cache key.
+ """
+ scope = 'anon'
+
+ def get_cache_key(self, request):
+ if request.user.is_authenticated():
+ return None # Only throttle unauthenticated requests.
+
+ ident = request.META.get('REMOTE_ADDR', None)
+
+ return self.cache_format % {
+ 'scope': self.scope,
+ 'ident': ident
+ }
+
+
+class UserRateThrottle(SimpleRateThottle):
"""
Limits the rate of API calls that may be made by a given user.
- The user id will be used as a unique identifier if the user is
- authenticated. For anonymous requests, the IP address of the client will
+ The user id will be used as a unique cache key if the user is
+ authenticated. For anonymous requests, the IP address of the request will
be used.
"""
+ scope = 'user'
def get_cache_key(self, request):
if request.user.is_authenticated():
ident = request.user.id
else:
ident = request.META.get('REMOTE_ADDR', None)
- return 'throttle_user_%s' % ident
+ return self.cache_format % {
+ 'scope': self.scope,
+ 'ident': ident
+ }
-class PerViewThrottling(SimpleCachingThrottle):
- """
- Limits the rate of API calls that may be used on a given view.
- The class name of the view is used as a unique identifier to
- throttle against.
+class ScopedRateThrottle(SimpleRateThottle):
+ """
+ Limits the rate of API calls by different amounts for various parts of
+ the API. Any view that has the `throttle_scope` property set will be
+ throttled. The unique cache key will be generated by concatenating the
+ user id of the request, and the scope of the view being accessed.
"""
+ scope_attr = 'throttle_scope'
+
+ def __init__(self, view):
+ """
+ Scope is determined from the view being accessed.
+ """
+ self.scope = getattr(self.view, self.scope_attr, None)
+ super(ScopedRateThrottle, self).__init__(view)
+
+ def parse_rate_description(self, rate):
+ """
+ Subclassed so that we don't fail if `view.throttle_scope` is not set.
+ """
+ if not rate:
+ return (None, None)
+ return super(ScopedRateThrottle, self).parse_rate_description(rate)
+
def get_cache_key(self, request):
- return 'throttle_view_%s' % self.view.__class__.__name__
+ """
+ If `view.throttle_scope` is not set, don't apply this throttle.
+
+ Otherwise generate the unique cache key by concatenating the user id
+ with the '.throttle_scope` property of the view.
+ """
+ if not self.scope:
+ return None # Only throttle views if `.throttle_scope` is set.
+
+ if request.user.is_authenticated():
+ ident = request.user.id
+ else:
+ ident = request.META.get('REMOTE_ADDR', None)
+
+ return self.cache_format % {
+ 'scope': self.scope,
+ 'ident': ident
+ }
diff --git a/djangorestframework/tokenauth/models.py b/djangorestframework/tokenauth/models.py
deleted file mode 100644
index f289b0fd..00000000
--- a/djangorestframework/tokenauth/models.py
+++ /dev/null
@@ -1,15 +0,0 @@
-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)
diff --git a/djangorestframework/views.py b/djangorestframework/views.py
index 1679f161..a309386b 100644
--- a/djangorestframework/views.py
+++ b/djangorestframework/views.py
@@ -54,9 +54,9 @@ def _camelcase_to_spaces(content):
class APIView(_View):
- renderers = api_settings.DEFAULT_RENDERERS
- parsers = api_settings.DEFAULT_PARSERS
- authentication = api_settings.DEFAULT_AUTHENTICATION
+ renderer_classes = api_settings.DEFAULT_RENDERERS
+ parser_classes = api_settings.DEFAULT_PARSERS
+ authentication_classes = api_settings.DEFAULT_AUTHENTICATION
throttle_classes = api_settings.DEFAULT_THROTTLES
permission_classes = api_settings.DEFAULT_PERMISSIONS
@@ -139,35 +139,35 @@ class APIView(_View):
"""
Return a list of all the media types that this view can parse.
"""
- return [parser.media_type for parser in self.parsers]
+ return [parser.media_type for parser in self.parser_classes]
@property
def _default_parser(self):
"""
Return the view's default parser class.
"""
- return self.parsers[0]
+ return self.parser_classes[0]
@property
def _rendered_media_types(self):
"""
Return an list of all the media types that this response can render.
"""
- return [renderer.media_type for renderer in self.renderers]
+ return [renderer.media_type for renderer in self.renderer_classes]
@property
def _rendered_formats(self):
"""
Return a list of all the formats that this response can render.
"""
- return [renderer.format for renderer in self.renderers]
+ return [renderer.format for renderer in self.renderer_classes]
@property
def _default_renderer(self):
"""
Return the response's default renderer class.
"""
- return self.renderers[0]
+ return self.renderer_classes[0]
def get_permissions(self):
"""
@@ -186,7 +186,7 @@ class APIView(_View):
Check if request should be permitted.
"""
for permission in self.get_permissions():
- if not permission.check_permission(request, obj):
+ if not permission.has_permission(request, obj):
self.permission_denied(request)
def check_throttles(self, request):
@@ -194,14 +194,14 @@ class APIView(_View):
Check if request should be throttled.
"""
for throttle in self.get_throttles():
- if not throttle.check_throttle(request):
+ if not throttle.allow_request(request):
self.throttled(request, throttle.wait())
def initialize_request(self, request, *args, **kargs):
"""
Returns the initial request object.
"""
- return Request(request, parsers=self.parsers, authentication=self.authentication)
+ return Request(request, parser_classes=self.parser_classes, authentication_classes=self.authentication_classes)
def finalize_response(self, request, response, *args, **kwargs):
"""
@@ -210,7 +210,7 @@ class APIView(_View):
if isinstance(response, Response):
response.view = self
response.request = request
- response.renderers = self.renderers
+ response.renderer_classes = self.renderer_classes
if api_settings.FORMAT_SUFFIX_KWARG:
response.format = kwargs.get(api_settings.FORMAT_SUFFIX_KWARG, None)
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index ca29bc4d..79950946 100644
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -2,6 +2,10 @@
# Authentication
+> Auth needs to be pluggable.
+>
+> &mdash; Jacob Kaplan-Moss, ["REST worst practices"][cite]
+
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.
REST framework provides a number of authentication policies out of the box, and also allows you to implement custom policies.
@@ -14,7 +18,7 @@ The `request.auth` property is used for any additional authentication informatio
## How authentication is determined
-Authentication is always set as a list of classes. REST framework will attempt to authenticate with each class in the list, and will set `request.user` and `request.auth` using the return value of the first class that successfully authenticates.
+The authentication policy is always defined as a list of classes. REST framework will attempt to authenticate with each class in the list, and will set `request.user` and `request.auth` using the return value of the first class that successfully authenticates.
If no class authenticates, `request.user` will be set to an instance of `django.contrib.auth.models.AnonymousUser`, and `request.auth` will be set to `None`.
@@ -56,31 +60,40 @@ Or, if you're using the `@api_view` decorator with function based views.
}
return Response(content)
-## UserBasicAuthentication
-
-This policy uses [HTTP Basic Authentication][basicauth], signed against a user's username and password. User basic authentication is generally only appropriate for testing.
+## BasicAuthentication
-**Note:** If you run `UserBasicAuthentication` in production your API must be `https` only, or it will be completely insecure. You should also ensure that your API clients will always re-request the username and password at login, and will never store those details to persistent storage.
+This policy uses [HTTP Basic Authentication][basicauth], signed against a user's username and password. Basic authentication is generally only appropriate for testing.
-If successfully authenticated, `UserBasicAuthentication` provides the following credentials.
+If successfully authenticated, `BasicAuthentication` provides the following credentials.
* `request.user` will be a `django.contrib.auth.models.User` instance.
* `request.auth` will be `None`.
+**Note:** If you use `BasicAuthentication` in production you must ensure that your API is only available over `https` only. You should also ensure that your API clients will always re-request the username and password at login, and will never store those details to persistent storage.
+
## 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 a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients.
+
+To use the `TokenAuthentication` policy, include `djangorestframework.authtoken` in your `INSTALLED_APPS` setting.
+
+You'll also need to create tokens for your users.
+
+ from djangorestframework.authtoken.models import Token
+
+ token = Token.objects.create(user=...)
+ print token.key
- curl http://my.api.org/ -X POST -H "Authorization: 0123456789abcdef0123456789abcdef"
+For clients to authenticate, the token key should be included in the `Authorization` HTTP header. The key should be prefixed by the string literal "Token", with whitespace seperating the two strings. For example:
-**Note:** If you run `TokenAuthentication` in production your API must be `https` only, or it will be completely insecure.
+ Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b
If successfully authenticated, `TokenAuthentication` provides the following credentials.
* `request.user` will be a `django.contrib.auth.models.User` 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.
+**Note:** If you use `TokenAuthentication` in production you must ensure that your API is only available over `https` only.
## OAuthAuthentication
@@ -102,8 +115,9 @@ If successfully authenticated, `SessionAuthentication` provides the following cr
## Custom authentication policies
-To implement a custom authentication policy, subclass `BaseAuthentication` and override the `authenticate(self, request)` method. The method should return a two-tuple of `(user, auth)` if authentication succeeds, or `None` otherwise.
+To implement a custom authentication policy, subclass `BaseAuthentication` and override the `.authenticate(self, request)` method. The method should return a two-tuple of `(user, auth)` if authentication succeeds, or `None` otherwise.
+[cite]: http://jacobian.org/writing/rest-worst-practices/
[basicauth]: http://tools.ietf.org/html/rfc2617
[oauth]: http://oauth.net/2/
[permission]: permissions.md
diff --git a/docs/api-guide/content-negotiation.md b/docs/api-guide/content-negotiation.md
new file mode 100644
index 00000000..01895a4b
--- /dev/null
+++ b/docs/api-guide/content-negotiation.md
@@ -0,0 +1,7 @@
+# Content negotiation
+
+> HTTP has provisions for several mechanisms for "content negotiation" - the process of selecting the best representation for a given response when there are multiple representations available.
+>
+> &mdash; [RFC 2616][cite], Fielding et al.
+
+[cite]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec12.html
diff --git a/docs/api-guide/contentnegotiation.md b/docs/api-guide/contentnegotiation.md
deleted file mode 100644
index f01627d8..00000000
--- a/docs/api-guide/contentnegotiation.md
+++ /dev/null
@@ -1 +0,0 @@
-> HTTP has provisions for several mechanisms for "content negotiation" -- the process of selecting the best representation for a given response when there are multiple representations available. -- RFC 2616, Fielding et al.
diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md
index bb3ed56e..c22d6d8b 100644
--- a/docs/api-guide/exceptions.md
+++ b/docs/api-guide/exceptions.md
@@ -2,4 +2,83 @@
# Exceptions
+> Exceptions… allow error handling to be organized cleanly in a central or high-level place within the program structure.
+>
+> &mdash; Doug Hellmann, [Python Exception Handling Techniques][cite]
+## Exception handling in REST framework views
+
+REST framework's views handle various exceptions, and deal with returning appropriate error responses.
+
+The handled exceptions are:
+
+* Subclasses of `APIException` raised inside REST framework.
+* Django's `Http404` exception.
+* Django's `PermissionDenied` exception.
+
+In each case, REST framework will return a response with an appropriate status code and content-type. The body of the response will include any additional details regarding the nature of the error.
+
+By default all error responses will include a key `details` in the body of the response, but other keys may also be included.
+
+For example, the following request:
+
+ DELETE http://api.example.com/foo/bar HTTP/1.1
+ Accept: application/json
+
+Might recieve an error response indicating that the `DELETE` method is not allowed on that resource:
+
+ HTTP/1.1 405 Method Not Allowed
+ Content-Type: application/json; charset=utf-8
+ Content-Length: 42
+
+ {"detail": "Method 'DELETE' not allowed."}
+
+## APIException
+
+**Signature:** `APIException(detail=None)`
+
+The base class for all exceptions raised inside REST framework.
+
+To provide a custom exception, subclass `APIException` and set the `.status_code` and `.detail` properties on the class.
+
+## ParseError
+
+**Signature:** `ParseError(detail=None)`
+
+Raised if the request contains malformed data when accessing `request.DATA` or `request.FILES`.
+
+By default this exception results in a response with the HTTP status code "400 Bad Request".
+
+## PermissionDenied
+
+**Signature:** `PermissionDenied(detail=None)`
+
+Raised when an incoming request fails the permission checks.
+
+By default this exception results in a response with the HTTP status code "403 Forbidden".
+
+## MethodNotAllowed
+
+**Signature:** `MethodNotAllowed(method, detail=None)`
+
+Raised when an incoming request occurs that does not map to a handler method on the view.
+
+By default this exception results in a response with the HTTP status code "405 Method Not Allowed".
+
+## UnsupportedMediaType
+
+**Signature:** `UnsupportedMediaType(media_type, detail=None)`
+
+Raised if there are no parsers that can handle the content type of the request data when accessing `request.DATA` or `request.FILES`.
+
+By default this exception results in a response with the HTTP status code "415 Unsupported Media Type".
+
+## Throttled
+
+**Signature:** `Throttled(wait=None, detail=None)`
+
+Raised when an incoming request fails the throttling checks.
+
+By default this exception results in a response with the HTTP status code "429 Too Many Requests".
+
+[cite]: http://www.doughellmann.com/articles/how-tos/python-exception-handling/index.html \ No newline at end of file
diff --git a/docs/api-guide/format-suffixes.md b/docs/api-guide/format-suffixes.md
new file mode 100644
index 00000000..7d72d9f8
--- /dev/null
+++ b/docs/api-guide/format-suffixes.md
@@ -0,0 +1,11 @@
+<a class="github" href="urlpatterns.py"></a>
+
+# Format suffixes
+
+> Section 6.2.1 does not say that content negotiation should be
+used all the time.
+>
+> &mdash; Roy Fielding, [REST discuss mailing list][cite]
+
+[cite]: http://tech.groups.yahoo.com/group/rest-discuss/message/5857
+
diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md
new file mode 100644
index 00000000..202875a4
--- /dev/null
+++ b/docs/api-guide/generic-views.md
@@ -0,0 +1,10 @@
+<a class="github" href="mixins.py"></a>
+<a class="github" href="generics.py"></a>
+
+# Generic views
+
+> Django’s generic views... were developed as a shortcut for common usage patterns... They take certain common idioms and patterns found in view development and abstract them so that you can quickly write common views of data without having to repeat yourself.
+>
+> &mdash; [Django Documentation][cite]
+
+[cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views
diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md
index 2e15107c..fafef305 100644
--- a/docs/api-guide/permissions.md
+++ b/docs/api-guide/permissions.md
@@ -1,3 +1,100 @@
<a class="github" href="permissions.py"></a>
-# Permissions \ No newline at end of file
+# Permissions
+
+> Authentication or identification by itself is not usually sufficient to gain access to information or code. For that, the entity requesting access must have authorization.
+>
+> &mdash; [Apple Developer Documentation][cite]
+
+Together with [authentication] and [throttling], permissions determine wheter a request should be granted or denied access.
+
+Permission checks are always run at the very start of the view, before any other code is allowed to proceed. Permission checks will typically use the authentication information in the `request.user` and `request.auth` properties to determine if the incoming request should be permitted.
+
+## How permissions are determined
+
+Permissions in REST framework are always defined as a list of permission classes.
+
+Before running the main body of the view each permission in the list is checked.
+If any permission check fails an `exceptions.PermissionDenied` exception will be raised, and the main body of the view will not run.
+
+## Object level permissions
+
+REST framework permissions also support object-level permissioning. Object level permissions are used to determine if a user should be allowed to act on a particular object, which will typically be a model instance.
+
+Object level permissions are run by REST framework's generic views when `.get_object()` is called. As with view level permissions, an `exceptions.PermissionDenied` exception will be raised if the user is not allowed to act on the given object.
+
+## Setting the permission policy
+
+The default permission policy may be set globally, using the `DEFAULT_PERMISSIONS` setting. For example.
+
+ API_SETTINGS = {
+ 'DEFAULT_PERMISSIONS': (
+ 'djangorestframework.permissions.IsAuthenticated',
+ )
+ }
+
+You can also set the authentication policy on a per-view basis, using the `APIView` class based views.
+
+ class ExampleView(APIView):
+ permission_classes = (IsAuthenticated,)
+
+ def get(self, request, format=None):
+ content = {
+ 'status': 'request was permitted'
+ }
+ return Response(content)
+
+Or, if you're using the `@api_view` decorator with function based views.
+
+ @api_view('GET')
+ @permission_classes(IsAuthenticated)
+ def example_view(request, format=None):
+ content = {
+ 'status': 'request was permitted'
+ }
+ return Response(content)
+
+## IsAuthenticated
+
+The `IsAuthenticated` permission class will deny permission to any unauthenticated user, and allow permission otherwise.
+
+This permission is suitable if you want your API to only be accessible to registered users.
+
+## IsAdminUser
+
+The `IsAdminUser` permission class will deny permission to any user, unless `user.is_staff`is `True` in which case permission will be allowed.
+
+This permission is suitable is you want your API to only be accessible to a subset of trusted administrators.
+
+## IsAuthenticatedOrReadOnly
+
+The `IsAuthenticatedOrReadOnly` will allow authenticated users to perform any request. Requests for unauthorised users will only be permitted if the request method is one of the "safe" methods; `GET`, `HEAD` or `OPTIONS`.
+
+This permission is suitable if you want to your API to allow read permissions to anonymous users, and only allow write permissions to authenticated users.
+
+## DjangoModelPermissions
+
+This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. When applied to a view that has a `.model` property, authorization will only be granted if the user has the relevant model permissions assigned.
+
+* `POST` requests require the user to have the `add` permission on the model.
+* `PUT` and `PATCH` requests require the user to have the `change` permission on the model.
+* `DELETE` requests require the user to have the `delete` permission on the model.
+
+The default behaviour can also be overridden to support custom model permissions. For example, you might want to include a `view` model permission for `GET` requests.
+
+To use custom model permissions, override `DjangoModelPermissions` and set the `.perms_map` property. Refer to the source code for details.
+
+The `DjangoModelPermissions` class also supports object-level permissions. Third-party authorization backends such as [django-guardian][guardian] that provide object-level permissions should work just fine with `DjangoModelPermissions` without any custom configuration required.
+
+## Custom permissions
+
+To implement a custom permission, override `BasePermission` and implement the `.has_permission(self, request, obj=None)` method.
+
+The method should return `True` if the request should be granted access, and `False` otherwise.
+
+
+[cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html
+[authentication]: authentication.md
+[throttling]: throttling.md
+[contribauth]: https://docs.djangoproject.com/en/1.0/topics/auth/#permissions
+[guardian]: https://github.com/lukaszb/django-guardian \ No newline at end of file
diff --git a/docs/api-guide/reverse.md b/docs/api-guide/reverse.md
index 6e42b68e..f3cb0c64 100644
--- a/docs/api-guide/reverse.md
+++ b/docs/api-guide/reverse.md
@@ -1,12 +1,12 @@
<a class="github" href="reverse.py"></a>
-# Returning URIs from your Web APIs
+# Returning URLs
> The central feature that distinguishes the REST architectural style from other network-based styles is its emphasis on a uniform interface between components.
>
> &mdash; Roy Fielding, [Architectural Styles and the Design of Network-based Software Architectures][cite]
-As a rule, it's probably better practice to return absolute URIs from you web APIs, such as `http://example.com/foobar`, rather than returning relative URIs, such as `/foobar`.
+As a rule, it's probably better practice to return absolute URIs from you Web APIs, such as `http://example.com/foobar`, rather than returning relative URIs, such as `/foobar`.
The advantages of doing so are:
diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md
index ae8dce76..2513928c 100644
--- a/docs/api-guide/settings.md
+++ b/docs/api-guide/settings.md
@@ -2,9 +2,13 @@
# Settings
-Configuration for REST framework is all namespaced inside the `API_SETTINGS` setting.
+> Namespaces are one honking great idea - let's do more of those!
+>
+> &mdash; [The Zen of Python][cite]
-For example your project's `settings.py` file might look like this:
+Configuration for REST framework is all namespaced inside a single Django setting, named `API_SETTINGS`.
+
+For example your project's `settings.py` file might include something like this:
API_SETTINGS = {
'DEFAULT_RENDERERS': (
@@ -133,3 +137,5 @@ The name of a URL parameter that may be used to override the HTTP `Accept` heade
If the value of this setting is `None` then URL accept overloading will be disabled.
Default: `'_accept'`
+
+[cite]: http://www.python.org/dev/peps/pep-0020/
diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md
index e3a66c83..10997801 100644
--- a/docs/api-guide/throttling.md
+++ b/docs/api-guide/throttling.md
@@ -1,3 +1,151 @@
<a class="github" href="throttling.py"></a>
# Throttling
+
+> HTTP/1.1 420 Enhance Your Calm
+>
+> [Twitter API rate limiting response][cite]
+
+[cite]: https://dev.twitter.com/docs/error-codes-responses
+
+Throttling is similar to [permissions], in that it determines if a request should be authorized. Throttles indicate a temporary state, and are used to control the rate of requests that clients can make to an API.
+
+As with permissions, multiple throttles may be used. Your API might have a restrictive throttle for unauthenticated requests, and a less restrictive throttle for authenticated requests.
+
+Another scenario where you might want to use multiple throttles would be if you need to impose different constraints on different parts of the API, due to some services being particularly resource-intensive.
+
+Multiple throttles can also be used if you want to impose both burst throttling rates, and sustained throttling rates. For example, you might want to limit a user to a maximum of 60 requests per minute, and 1000 requests per day.
+
+Throttles do not necessarily only refer to rate-limiting requests. For example a storage service might also need to throttle against bandwidth, and a paid data service might want to throttle against a certain number of a records being accessed.
+
+## How throttling is determined
+
+As with permissions and authentication, throttling in REST framework is always defined as a list of classes.
+
+Before running the main body of the view each throttle in the list is checked.
+If any throttle check fails an `exceptions.Throttled` exception will be raised, and the main body of the view will not run.
+
+## Setting the throttling policy
+
+The default throttling policy may be set globally, using the `DEFAULT_THROTTLES` and `DEFAULT_THROTTLE_RATES` settings. For example.
+
+ API_SETTINGS = {
+ 'DEFAULT_THROTTLES': (
+ 'djangorestframework.throttles.AnonThrottle',
+ 'djangorestframework.throttles.UserThrottle',
+ )
+ 'DEFAULT_THROTTLE_RATES': {
+ 'anon': '100/day',
+ 'user': '1000/day'
+ }
+ }
+
+The rate descriptions used in `DEFAULT_THROTTLE_RATES` may include `second`, `minute`, `hour` or `day` as the throttle period.
+
+You can also set the throttling policy on a per-view basis, using the `APIView` class based views.
+
+ class ExampleView(APIView):
+ throttle_classes = (UserThrottle,)
+
+ def get(self, request, format=None):
+ content = {
+ 'status': 'request was permitted'
+ }
+ return Response(content)
+
+Or, if you're using the `@api_view` decorator with function based views.
+
+ @api_view('GET')
+ @throttle_classes(UserThrottle)
+ def example_view(request, format=None):
+ content = {
+ 'status': 'request was permitted'
+ }
+ return Response(content)
+
+## AnonRateThrottle
+
+The `AnonThrottle` will only ever throttle unauthenticated users. The IP address of the incoming request is used to generate a unique key to throttle against.
+
+The allowed request rate is determined from one of the following (in order of preference).
+
+* The `rate` property on the class, which may be provided by overriding `AnonThrottle` and setting the property.
+* The `DEFAULT_THROTTLE_RATES['anon']` setting.
+
+`AnonThrottle` is suitable if you want to restrict the rate of requests from unknown sources.
+
+## UserRateThrottle
+
+The `UserThrottle` will throttle users to a given rate of requests across the API. The user id is used to generate a unique key to throttle against. Unauthenticted requests will fall back to using the IP address of the incoming request to generate a unique key to throttle against.
+
+The allowed request rate is determined from one of the following (in order of preference).
+
+* The `rate` property on the class, which may be provided by overriding `UserThrottle` and setting the property.
+* The `DEFAULT_THROTTLE_RATES['user']` setting.
+
+An API may have multiple `UserRateThrottles` in place at the same time. To do so, override `UserRateThrottle` and set a unique "scope" for each class.
+
+For example, multiple user throttle rates could be implemented by using the following classes...
+
+ class BurstRateThrottle(UserRateThrottle):
+ scope = 'burst'
+
+ class SustainedRateThrottle(UserRateThrottle):
+ scope = 'sustained'
+
+...and the following settings.
+
+ API_SETTINGS = {
+ 'DEFAULT_THROTTLES': (
+ 'example.throttles.BurstRateThrottle',
+ 'example.throttles.SustainedRateThrottle',
+ )
+ 'DEFAULT_THROTTLE_RATES': {
+ 'burst': '60/min',
+ 'sustained': '1000/day'
+ }
+ }
+
+`UserThrottle` is suitable if you want simple global rate restrictions per-user.
+
+## ScopedRateThrottle
+
+The `ScopedThrottle` class can be used to restrict access to specific parts of the API. This throttle will only be applied if the view that is being accessed includes a `.throttle_scope` property. The unique throttle key will then be formed by concatenating the "scope" of the request with the unqiue user id or IP address.
+
+The allowed request rate is determined by the `DEFAULT_THROTTLE_RATES` setting using a key from the request "scope".
+
+For example, given the following views...
+
+ class ContactListView(APIView):
+ throttle_scope = 'contacts'
+ ...
+
+ class ContactDetailView(ApiView):
+ throttle_scope = 'contacts'
+ ...
+
+ class UploadView(APIView):
+ throttle_scope = 'uploads'
+ ...
+
+...and the following settings.
+
+ API_SETTINGS = {
+ 'DEFAULT_THROTTLES': (
+ 'djangorestframework.throttles.ScopedRateThrottle',
+ )
+ 'DEFAULT_THROTTLE_RATES': {
+ 'contacts': '1000/day',
+ 'uploads': '20/day'
+ }
+ }
+
+User requests to either `ContactListView` or `ContactDetailView` would be restricted to a total of 1000 requests per-day. User requests to `UploadView` would be restricted to 20 requests per day.
+
+## Custom throttles
+
+To create a custom throttle, override `BaseThrottle` and implement `.allow_request(request)`. The method should return `True` if the request should be allowed, and `False` otherwise.
+
+Optionally you may also override the `.wait()` method. If implemented, `.wait()` should return a recomended number of seconds to wait before attempting the next request, or `None`. The `.wait()` method will only be called if `.check_throttle()` has previously returned `False`.
+
+[permissions]: permissions.md \ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
index 0072b56f..c2035a19 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -77,15 +77,18 @@ The API guide is your complete reference manual to all the functionality provide
* [Requests][request]
* [Responses][response]
* [Views][views]
+* [Generic views][generic-views]
* [Parsers][parsers]
* [Renderers][renderers]
* [Serializers][serializers]
* [Authentication][authentication]
* [Permissions][permissions]
* [Throttling][throttling]
+* [Content negotiation][contentnegotiation]
+* [Format suffixes][formatsuffixes]
+* [Returning URLs][reverse]
* [Exceptions][exceptions]
* [Status codes][status]
-* [Returning URLs][reverse]
* [Settings][settings]
## Topics
@@ -150,15 +153,18 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[request]: api-guide/requests.md
[response]: api-guide/responses.md
[views]: api-guide/views.md
+[generic-views]: api-guide/generic-views.md
[parsers]: api-guide/parsers.md
[renderers]: api-guide/renderers.md
[serializers]: api-guide/serializers.md
[authentication]: api-guide/authentication.md
[permissions]: api-guide/permissions.md
[throttling]: api-guide/throttling.md
+[contentnegotiation]: api-guide/content-negotiation.md
+[formatsuffixes]: api-guide/format-suffixes.md
+[reverse]: api-guide/reverse.md
[exceptions]: api-guide/exceptions.md
[status]: api-guide/status-codes.md
-[reverse]: api-guide/reverse.md
[settings]: api-guide/settings.md
[csrf]: topics/csrf.md
diff --git a/docs/static/css/drf-styles.css b/docs/static/css/default.css
index bb31b07c..213a700e 100644
--- a/docs/static/css/drf-styles.css
+++ b/docs/static/css/default.css
@@ -14,15 +14,6 @@ pre {
font-size: 12px;
}
-a.github {
- float: right;
- margin-top: -12px;
-}
-
-a.github:hover {
- text-decoration: none;
-}
-
.dropdown .dropdown-menu {
display: none;
}
@@ -31,25 +22,51 @@ a.github:hover {
display: block;
}
-body.index #main-content iframe {
+@media (max-width: 480px) {
+ .repo-link {
+ display: none;
+ }
+}
+
+/* Header link to GitHub */
+.repo-link {
float: right;
+ margin-right: 10px;
+ margin-top: 9px;
}
+/* GitHub 'Star' badge */
body.index #main-content iframe {
float: right;
+ margin-top: -12px;
margin-right: -15px;
}
+/* Travis CI badge */
body.index #main-content p:first-of-type {
float: right;
margin-right: 8px;
- margin-top: -1px;
+ margin-top: -14px;
+ margin-bottom: 0px;
}
+/* Github source file badges */
+a.github {
+ float: right;
+ margin-top: -12px;
+ margin-right: 12px;
+}
+
+a.github:hover {
+ text-decoration: none;
+}
+
+/* Force TOC text to not overrun */
#table-of-contents {
overflow: hidden;
}
+/* Code blocks should scroll horizontally */
pre {
overflow: auto;
word-wrap: normal;
@@ -71,13 +88,10 @@ pre {
}
}
-
.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) {
@@ -94,7 +108,7 @@ pre {
}
}
-
+/* Cutesy quote styling */
blockquote {
font-family: Georgia, serif;
font-size: 18px;
diff --git a/docs/template.html b/docs/template.html
index 819e3b92..127978d2 100644
--- a/docs/template.html
+++ b/docs/template.html
@@ -10,7 +10,7 @@
<link href="{{ base_url }}/css/prettify.css" rel="stylesheet">
<link href="{{ base_url }}/css/bootstrap.css" rel="stylesheet">
<link href="{{ base_url }}/css/bootstrap-responsive.css" rel="stylesheet">
- <link href="{{ base_url }}/css/drf-styles.css" rel="stylesheet">
+ <link href="{{ base_url }}/css/default.css" rel="stylesheet">
<!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
@@ -21,6 +21,7 @@
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner">
<div class="container-fluid">
+ <a class="repo-link btn btn-primary btn-small" href="https://github.com/tomchristie/django-rest-framework/tree/restframework2">GitHub</a>
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
@@ -47,15 +48,18 @@
<li><a href="{{ base_url }}/api-guide/requests{{ suffix }}">Requests</a></li>
<li><a href="{{ base_url }}/api-guide/responses{{ suffix }}">Responses</a></li>
<li><a href="{{ base_url }}/api-guide/views{{ suffix }}">Views</a></li>
+ <li><a href="{{ base_url }}/api-guide/generic-views{{ suffix }}">Generic views</a></li>
<li><a href="{{ base_url }}/api-guide/parsers{{ suffix }}">Parsers</a></li>
<li><a href="{{ base_url }}/api-guide/renderers{{ suffix }}">Renderers</a></li>
<li><a href="{{ base_url }}/api-guide/serializers{{ suffix }}">Serializers</a></li>
<li><a href="{{ base_url }}/api-guide/authentication{{ suffix }}">Authentication</a></li>
<li><a href="{{ base_url }}/api-guide/permissions{{ suffix }}">Permissions</a></li>
<li><a href="{{ base_url }}/api-guide/throttling{{ suffix }}">Throttling</a></li>
+ <li><a href="{{ base_url }}/api-guide/content-negotiation{{ suffix }}">Content negotiation</a></li>
+ <li><a href="{{ base_url }}/api-guide/format-suffixes{{ suffix }}">Format suffixes</a></li>
+ <li><a href="{{ base_url }}/api-guide/reverse{{ suffix }}">Returning URLs</a></li>
<li><a href="{{ base_url }}/api-guide/exceptions{{ suffix }}">Exceptions</a></li>
<li><a href="{{ base_url }}/api-guide/status-codes{{ suffix }}">Status codes</a></li>
- <li><a href="{{ base_url }}/api-guide/reverse{{ suffix }}">Returning URLs</a></li>
<li><a href="{{ base_url }}/api-guide/settings{{ suffix }}">Settings</a></li>
</ul>
</li>
@@ -113,5 +117,9 @@
var shiftWindow = function() { scrollBy(0, -50) };
if (location.hash) shiftWindow();
window.addEventListener("hashchange", shiftWindow);
+
+ $('.dropdown-menu').click(function(event) {
+ event.stopPropagation();
+ });
</script>
</body></html> \ No newline at end of file
diff --git a/docs/topics/changelog.md b/docs/topics/changelog.md
new file mode 100644
index 00000000..a4fd39e2
--- /dev/null
+++ b/docs/topics/changelog.md
@@ -0,0 +1,107 @@
+# Release Notes
+
+## 2.0.0
+
+**TODO:** Explain REST framework 2.0
+
+## 0.4.0
+
+* Supports Django 1.5.
+* Fixes issues with 'HEAD' method.
+* Allow views to specify template used by TemplateRenderer
+* More consistent error responses
+* Some serializer fixes
+* Fix internet explorer ajax behaviour
+* Minor xml and yaml fixes
+* Improve setup (eg use staticfiles, not the defunct ADMIN_MEDIA_PREFIX)
+* Sensible absolute URL generation, not using hacky set_script_prefix
+
+## 0.3.3
+
+* Added DjangoModelPermissions class to support `django.contrib.auth` style permissions.
+* Use `staticfiles` for css files.
+ - Easier to override. Won't conflict with customised admin styles (eg grappelli)
+* Templates are now nicely namespaced.
+ - Allows easier overriding.
+* Drop implied 'pk' filter if last arg in urlconf is unnamed.
+ - Too magical. Explict is better than implicit.
+* Saner template variable autoescaping.
+* Tider setup.py
+* Updated for URLObject 2.0
+* Bugfixes:
+ - Bug with PerUserThrottling when user contains unicode chars.
+
+## 0.3.2
+
+* Bugfixes:
+ * Fix 403 for POST and PUT from the UI with UserLoggedInAuthentication (#115)
+ * serialize_model method in serializer.py may cause wrong value (#73)
+ * Fix Error when clicking OPTIONS button (#146)
+ * And many other fixes
+* Remove short status codes
+ - Zen of Python: "There should be one-- and preferably only one --obvious way to do it."
+* get_name, get_description become methods on the view - makes them overridable.
+* Improved model mixin API - Hooks for build_query, get_instance_data, get_model, get_queryset, get_ordering
+
+## 0.3.1
+
+* [not documented]
+
+## 0.3.0
+
+* JSONP Support
+* Bugfixes, including support for latest markdown release
+
+## 0.2.4
+
+* Fix broken IsAdminUser permission.
+* OPTIONS support.
+* XMLParser.
+* Drop mentions of Blog, BitBucket.
+
+## 0.2.3
+
+* Fix some throttling bugs.
+* ``X-Throttle`` header on throttling.
+* Support for nesting resources on related models.
+
+## 0.2.2
+
+* Throttling support complete.
+
+## 0.2.1
+
+* Couple of simple bugfixes over 0.2.0
+
+## 0.2.0
+
+* Big refactoring changes since 0.1.0, ask on the discussion group if anything isn't clear.
+ The public API has been massively cleaned up. Expect it to be fairly stable from here on in.
+
+* ``Resource`` becomes decoupled into ``View`` and ``Resource``, your views should now inherit from ``View``, not ``Resource``.
+
+* The handler functions on views ``.get() .put() .post()`` etc, no longer have the ``content`` and ``auth`` args.
+ Use ``self.CONTENT`` inside a view to access the deserialized, validated content.
+ Use ``self.user`` inside a view to access the authenticated user.
+
+* ``allowed_methods`` and ``anon_allowed_methods`` are now defunct. if a method is defined, it's available.
+ The ``permissions`` attribute on a ``View`` is now used to provide generic permissions checking.
+ Use permission classes such as ``FullAnonAccess``, ``IsAuthenticated`` or ``IsUserOrIsAnonReadOnly`` to set the permissions.
+
+* The ``authenticators`` class becomes ``authentication``. Class names change to ``Authentication``.
+
+* The ``emitters`` class becomes ``renderers``. Class names change to ``Renderers``.
+
+* ``ResponseException`` becomes ``ErrorResponse``.
+
+* The mixin classes have been nicely refactored, the basic mixins are now ``RequestMixin``, ``ResponseMixin``, ``AuthMixin``, and ``ResourceMixin``
+ You can reuse these mixin classes individually without using the ``View`` class.
+
+## 0.1.1
+
+* Final build before pulling in all the refactoring changes for 0.2, in case anyone needs to hang on to 0.1.
+
+## 0.1.0
+
+* Initial release.
+
diff --git a/docs/tutorial/6-resource-orientated-projects.md b/docs/tutorial/6-resource-orientated-projects.md
index 4282c25d..0d0cfac5 100644
--- a/docs/tutorial/6-resource-orientated-projects.md
+++ b/docs/tutorial/6-resource-orientated-projects.md
@@ -1,3 +1,7 @@
+In REST framework Resources classes are just View classes that don't have any handler methods bound to them. This allows us to seperate out the behaviour of the classes from how that behaviour should be bound to a set of URLs.
+
+For instance, given our serializers
+
serializers.py
class BlogPostSerializer(URLModelSerializer):
@@ -8,21 +12,44 @@ serializers.py
class Meta:
model = Comment
+We can re-write our 4 sets of views into something more compact...
+
resources.py
class BlogPostResource(ModelResource):
serializer_class = BlogPostSerializer
model = BlogPost
- permissions = [AdminOrAnonReadonly()]
- throttles = [AnonThrottle(rate='5/min')]
+ permissions_classes = (permissions.IsAuthenticatedOrReadOnly,)
+ throttle_classes = (throttles.UserRateThrottle,)
class CommentResource(ModelResource):
serializer_class = CommentSerializer
model = Comment
- permissions = [AdminOrAnonReadonly()]
- throttles = [AnonThrottle(rate='5/min')]
+ permissions_classes = (permissions.IsAuthenticatedOrReadOnly,)
+ throttle_classes = (throttles.UserRateThrottle,)
+
+The handler methods only get bound to the actions when we define the URLConf. Here's our urls.py:
+
+ comment_root = CommentResource.as_view(actions={
+ 'get': 'list',
+ 'post': 'create'
+ })
+ comment_instance = CommentInstance.as_view(actions={
+ 'get': 'retrieve',
+ 'put': 'update',
+ 'delete': 'destroy'
+ })
+ ... # And for blog post
+
+ urlpatterns = patterns('blogpost.views',
+ url(r'^$', comment_root),
+ url(r'^(?P<pk>[0-9]+)$', comment_instance)
+ ... # And for blog post
+ )
+
+## Using Routers
-Now that we're using Resources rather than Views, we don't need to design the urlconf ourselves. The conventions for wiring up resources into views and urls are handled automatically. All we need to do is register the appropriate resources with a router, and let it do the rest. Here's our re-wired `urls.py` file.
+Right now that hasn't really saved us a lot of code. However, now that we're using Resources rather than Views, we actually don't need to design the urlconf ourselves. The conventions for wiring up resources into views and urls can be handled automatically, using `Router` classes. All we need to do is register the appropriate resources with a router, and let it do the rest. Here's our re-wired `urls.py` file.
from blog import resources
from djangorestframework.routers import DefaultRouter