diff options
| author | Alec Perkins | 2012-09-09 13:23:07 -0400 | 
|---|---|---|
| committer | Alec Perkins | 2012-09-09 13:23:07 -0400 | 
| commit | 45001033378a49986d4cd7f5bdf4673b083cdbd0 (patch) | |
| tree | e5eb2cd49d122ba56d63058413cb3d4d138dae7a /djangorestframework | |
| parent | 0ae5500f34a81005ba0161dacb280a94f768a885 (diff) | |
| parent | d4f8b4cf0683923fe85652f8fd572d2931eb3074 (diff) | |
| download | django-rest-framework-45001033378a49986d4cd7f5bdf4673b083cdbd0.tar.bz2 | |
Merge 'tomchristie/restframework2' into 'browsable-bootstrap'
Diffstat (limited to 'djangorestframework')
| -rw-r--r-- | djangorestframework/authentication.py | 36 | ||||
| -rw-r--r-- | djangorestframework/compat.py | 53 | ||||
| -rw-r--r-- | djangorestframework/fields.py | 6 | ||||
| -rw-r--r-- | djangorestframework/mixins.py | 2 | ||||
| -rw-r--r-- | djangorestframework/renderers.py | 53 | ||||
| -rw-r--r-- | djangorestframework/runtests/settings.py | 1 | ||||
| -rw-r--r-- | djangorestframework/settings.py | 5 | ||||
| -rw-r--r-- | djangorestframework/tests/authentication.py | 47 | ||||
| -rw-r--r-- | djangorestframework/tests/renderers.py | 22 | ||||
| -rw-r--r-- | djangorestframework/tests/request.py | 11 | ||||
| -rw-r--r-- | djangorestframework/tokenauth/__init__.py | 0 | ||||
| -rw-r--r-- | djangorestframework/tokenauth/models.py | 15 | ||||
| -rw-r--r-- | djangorestframework/tokenauth/views.py | 0 | ||||
| -rw-r--r-- | djangorestframework/utils/encoders.py | 4 | 
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:  | 
