aboutsummaryrefslogtreecommitdiffstats
path: root/djangorestframework
diff options
context:
space:
mode:
authorAlec Perkins2012-09-09 13:23:07 -0400
committerAlec Perkins2012-09-09 13:23:07 -0400
commit45001033378a49986d4cd7f5bdf4673b083cdbd0 (patch)
treee5eb2cd49d122ba56d63058413cb3d4d138dae7a /djangorestframework
parent0ae5500f34a81005ba0161dacb280a94f768a885 (diff)
parentd4f8b4cf0683923fe85652f8fd572d2931eb3074 (diff)
downloaddjango-rest-framework-45001033378a49986d4cd7f5bdf4673b083cdbd0.tar.bz2
Merge 'tomchristie/restframework2' into 'browsable-bootstrap'
Diffstat (limited to 'djangorestframework')
-rw-r--r--djangorestframework/authentication.py36
-rw-r--r--djangorestframework/compat.py53
-rw-r--r--djangorestframework/fields.py6
-rw-r--r--djangorestframework/mixins.py2
-rw-r--r--djangorestframework/renderers.py53
-rw-r--r--djangorestframework/runtests/settings.py1
-rw-r--r--djangorestframework/settings.py5
-rw-r--r--djangorestframework/tests/authentication.py47
-rw-r--r--djangorestframework/tests/renderers.py22
-rw-r--r--djangorestframework/tests/request.py11
-rw-r--r--djangorestframework/tokenauth/__init__.py0
-rw-r--r--djangorestframework/tokenauth/models.py15
-rw-r--r--djangorestframework/tokenauth/views.py0
-rw-r--r--djangorestframework/utils/encoders.py4
14 files changed, 202 insertions, 53 deletions
diff --git a/djangorestframework/authentication.py b/djangorestframework/authentication.py
index 4d5a7e86..2446fbbd 100644
--- a/djangorestframework/authentication.py
+++ b/djangorestframework/authentication.py
@@ -103,4 +103,38 @@ class SessionAuthentication(BaseAuthentication):
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
diff --git a/djangorestframework/compat.py b/djangorestframework/compat.py
index 7ced70c5..f21dc4ef 100644
--- a/djangorestframework/compat.py
+++ b/djangorestframework/compat.py
@@ -366,6 +366,59 @@ else:
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
try:
diff --git a/djangorestframework/fields.py b/djangorestframework/fields.py
index a44eb417..13b0e37d 100644
--- a/djangorestframework/fields.py
+++ b/djangorestframework/fields.py
@@ -8,10 +8,10 @@ from django.core.exceptions import ValidationError
from django.conf import settings
from django.db import DEFAULT_DB_ALIAS
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.translation import ugettext_lazy as _
+from djangorestframework.compat import parse_date, parse_datetime
+from djangorestframework.compat import timezone
def is_simple_callable(obj):
@@ -317,7 +317,7 @@ class DateField(Field):
if value is None:
return value
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
# before casting them to dates (#17742).
default_timezone = timezone.get_default_timezone()
diff --git a/djangorestframework/mixins.py b/djangorestframework/mixins.py
index 6ab7ab6e..1f06dd34 100644
--- a/djangorestframework/mixins.py
+++ b/djangorestframework/mixins.py
@@ -22,7 +22,7 @@ class CreateModelMixin(object):
self.object = serializer.object
self.object.save()
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):
diff --git a/djangorestframework/renderers.py b/djangorestframework/renderers.py
index 4f8225b1..45cdbbbb 100644
--- a/djangorestframework/renderers.py
+++ b/djangorestframework/renderers.py
@@ -16,6 +16,7 @@ from djangorestframework.utils import encoders
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 import VERSION
+from djangorestframework.fields import FloatField, IntegerField, DateTimeField, DateField, EmailField, CharField, BooleanField
import string
@@ -233,33 +234,31 @@ class DocumentingTemplateRenderer(BaseRenderer):
In the absence on of the Resource having an associated form then
provide a form that can be used to submit arbitrary content.
"""
-
- # Get the form instance if we have one bound to the input
- form_instance = None
- if method == getattr(view, 'method', view.request.method).lower():
- form_instance = getattr(view, 'bound_form_instance', None)
-
- if not form_instance and hasattr(view, 'get_bound_form'):
- # Otherwise if we have a response that is valid against the form then use that
- if view.response.has_content_body:
- try:
- form_instance = view.get_bound_form(view.response.cleaned_content, method=method)
- if form_instance and not form_instance.is_valid():
- form_instance = None
- except Exception:
- form_instance = None
-
- # If we still don't have a form instance then try to get an unbound form
- if not form_instance:
- try:
- form_instance = view.get_bound_form(method=method)
- except Exception:
- pass
-
- # If we still don't have a form instance then try to get an unbound form which can tunnel arbitrary content types
- if not form_instance:
- form_instance = self._get_generic_content_form(view)
-
+ if not hasattr(self.view, 'get_serializer'): # No serializer, no form.
+ return
+ # We need to map our Fields to Django's Fields.
+ field_mapping = dict([
+ [FloatField.__name__, forms.FloatField],
+ [IntegerField.__name__, forms.IntegerField],
+ [DateTimeField.__name__, forms.DateTimeField],
+ [DateField.__name__, forms.DateField],
+ [EmailField.__name__, forms.EmailField],
+ [CharField.__name__, forms.CharField],
+ [BooleanField.__name__, forms.BooleanField]
+ ])
+
+ # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python
+ fields = {}
+ object, data = None, None
+ if hasattr(self.view, 'object'):
+ object = self.view.object
+ serializer = self.view.get_serializer(instance=object)
+ for k, v in serializer.fields.items():
+ fields[k] = field_mapping[v.__class__.__name__]()
+ 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
+ data = serializer.data
+ form_instance = OnTheFlyForm(data)
return form_instance
def _get_generic_content_form(self, view):
diff --git a/djangorestframework/runtests/settings.py b/djangorestframework/runtests/settings.py
index 7cb3e27b..1fc6b47b 100644
--- a/djangorestframework/runtests/settings.py
+++ b/djangorestframework/runtests/settings.py
@@ -90,6 +90,7 @@ INSTALLED_APPS = (
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
'djangorestframework',
+ 'djangorestframework.tokenauth',
)
STATIC_URL = '/static/'
diff --git a/djangorestframework/settings.py b/djangorestframework/settings.py
index 8bb03555..e5181f4b 100644
--- a/djangorestframework/settings.py
+++ b/djangorestframework/settings.py
@@ -88,7 +88,10 @@ def import_from_string(val, setting):
module_path, class_name = '.'.join(parts[:-1]), parts[-1]
module = importlib.import_module(module_path)
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)
raise ImportError(msg)
diff --git a/djangorestframework/tests/authentication.py b/djangorestframework/tests/authentication.py
index 79194718..fcc8f7ba 100644
--- a/djangorestframework/tests/authentication.py
+++ b/djangorestframework/tests/authentication.py
@@ -8,6 +8,9 @@ from django.http import HttpResponse
from djangorestframework.views import APIView
from djangorestframework import permissions
+from djangorestframework.tokenauth.models import BasicToken
+from djangorestframework.authentication import TokenAuthentication
+
import base64
@@ -20,6 +23,8 @@ class MockView(APIView):
def put(self, request):
return HttpResponse({'a': 1, 'b': 2, 'c': 3})
+MockView.authentication += (TokenAuthentication,)
+
urlpatterns = patterns('',
(r'^$', MockView.as_view()),
)
@@ -104,3 +109,45 @@ class SessionAuthTests(TestCase):
"""
response = self.csrf_client.post('/', {'example': 'example'})
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)
diff --git a/djangorestframework/tests/renderers.py b/djangorestframework/tests/renderers.py
index 0a1cd9c7..692243e6 100644
--- a/djangorestframework/tests/renderers.py
+++ b/djangorestframework/tests/renderers.py
@@ -55,27 +55,27 @@ class MockView(APIView):
def get(self, request, **kwargs):
response = Response(DUMMYCONTENT, status=DUMMYSTATUS)
- return self.render(response)
+ return response
class MockGETView(APIView):
def get(self, request, **kwargs):
- return {'foo': ['bar', 'baz']}
+ return Response({'foo': ['bar', 'baz']})
class HTMLView(APIView):
renderers = (DocumentingHTMLRenderer, )
def get(self, request, **kwargs):
- return 'text'
+ return Response('text')
class HTMLView1(APIView):
renderers = (DocumentingHTMLRenderer, JSONRenderer)
def get(self, request, **kwargs):
- return 'text'
+ return Response('text')
urlpatterns = patterns('',
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderers=[RendererA, RendererB])),
@@ -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.
"""
@@ -216,18 +216,6 @@ class JSONRendererTests(TestCase):
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):
"""
Tests specific to the JSONP Renderer
diff --git a/djangorestframework/tests/request.py b/djangorestframework/tests/request.py
index 2bb90c0a..8b2f66ee 100644
--- a/djangorestframework/tests/request.py
+++ b/djangorestframework/tests/request.py
@@ -94,7 +94,16 @@ class TestContentParsing(TestCase):
"""
data = {'qwerty': 'uiop'}
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())
def test_standard_behaviour_determines_non_form_content_PUT(self):
diff --git a/djangorestframework/tokenauth/__init__.py b/djangorestframework/tokenauth/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/djangorestframework/tokenauth/__init__.py
diff --git a/djangorestframework/tokenauth/models.py b/djangorestframework/tokenauth/models.py
new file mode 100644
index 00000000..f289b0fd
--- /dev/null
+++ b/djangorestframework/tokenauth/models.py
@@ -0,0 +1,15 @@
+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/tokenauth/views.py b/djangorestframework/tokenauth/views.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/djangorestframework/tokenauth/views.py
diff --git a/djangorestframework/utils/encoders.py b/djangorestframework/utils/encoders.py
index ba7c8553..74876017 100644
--- a/djangorestframework/utils/encoders.py
+++ b/djangorestframework/utils/encoders.py
@@ -3,8 +3,8 @@ Helper classes for parsers.
"""
import datetime
import decimal
-from django.utils import timezone
from django.utils import simplejson as json
+from djangorestframework.compat import timezone
class JSONEncoder(json.JSONEncoder):
@@ -25,7 +25,7 @@ class JSONEncoder(json.JSONEncoder):
elif isinstance(o, datetime.date):
return o.isoformat()
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.")
r = o.isoformat()
if o.microsecond: