diff options
| author | Tom Christie | 2014-09-11 13:20:44 +0100 |
|---|---|---|
| committer | Tom Christie | 2014-09-11 13:20:44 +0100 |
| commit | de301f3b6647e1c79a506405a88071ef977418d1 (patch) | |
| tree | 407f3497b422f334b47088b0bb35d39a8a3a520a /rest_framework | |
| parent | 80ba0473473501968154c5cc5dd5922e53d96a70 (diff) | |
| parent | 015a8122c7738dd8913939b42d3f0ec932d88711 (diff) | |
| download | django-rest-framework-de301f3b6647e1c79a506405a88071ef977418d1.tar.bz2 | |
Merge master
Diffstat (limited to 'rest_framework')
| -rw-r--r-- | rest_framework/__init__.py | 2 | ||||
| -rw-r--r-- | rest_framework/authentication.py | 2 | ||||
| -rw-r--r-- | rest_framework/fields.py | 7 | ||||
| -rw-r--r-- | rest_framework/filters.py | 1 | ||||
| -rw-r--r-- | rest_framework/pagination.py | 8 | ||||
| -rw-r--r-- | rest_framework/parsers.py | 20 | ||||
| -rw-r--r-- | rest_framework/routers.py | 4 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 28 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/base.html | 30 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/login_base.html | 35 | ||||
| -rw-r--r-- | rest_framework/templatetags/rest_framework.py | 21 | ||||
| -rw-r--r-- | rest_framework/test.py | 2 | ||||
| -rw-r--r-- | rest_framework/urls.py | 2 | ||||
| -rw-r--r-- | rest_framework/utils/formatting.py | 6 | ||||
| -rw-r--r-- | rest_framework/views.py | 10 |
15 files changed, 124 insertions, 54 deletions
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index f95bdc22..8d82a4b9 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '2.4.0' +__version__ = '2.4.2' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2014 Tom Christie' diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 5721a869..f3fec05e 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -344,7 +344,7 @@ class OAuth2Authentication(BaseAuthentication): user = token.user if not user.is_active: - msg = 'User inactive or deleted: %s' % user.username + msg = 'User inactive or deleted: %s' % user.get_username() raise exceptions.AuthenticationFailed(msg) return (user, token) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 3ec28908..7496a629 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -51,8 +51,11 @@ def get_attribute(instance, attrs): for attr in attrs: try: instance = getattr(instance, attr) - except AttributeError: - return instance[attr] + except AttributeError as exc: + try: + return instance[attr] + except (KeyError, TypeError): + raise exc return instance diff --git a/rest_framework/filters.py b/rest_framework/filters.py index e2080013..c580f935 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -56,7 +56,6 @@ class DjangoFilterBackend(BaseFilterBackend): class Meta: model = queryset.model fields = filter_fields - order_by = True return AutoFilterSet return None diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 478d32b4..9cf31629 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -43,8 +43,9 @@ class DefaultObjectSerializer(serializers.Field): as the default. """ - def __init__(self, source=None, context=None): - # Note: Swallow context kwarg - only required for eg. ModelSerializer. + def __init__(self, source=None, many=None, context=None): + # Note: Swallow context and many kwargs - only required for + # eg. ModelSerializer. super(DefaultObjectSerializer, self).__init__(source=source) @@ -61,6 +62,7 @@ class BasePaginationSerializer(serializers.Serializer): """ super(BasePaginationSerializer, self).__init__(*args, **kwargs) results_field = self.results_field + try: object_serializer = self.Meta.object_serializer_class except AttributeError: @@ -70,7 +72,7 @@ class BasePaginationSerializer(serializers.Serializer): child=object_serializer(), source='object_list' ) - self.fields[results_field].bind(results_field, self, self) # TODO: Support automatic binding + self.fields[results_field].bind(results_field, self, self) class PaginationSerializer(BasePaginationSerializer): diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index aa4fd3f1..c287908d 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -11,7 +11,7 @@ from django.http import QueryDict from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter from django.utils import six -from rest_framework.compat import etree, yaml, force_text +from rest_framework.compat import etree, yaml, force_text, urlparse from rest_framework.exceptions import ParseError from rest_framework import renderers import json @@ -290,6 +290,22 @@ class FileUploadParser(BaseParser): try: meta = parser_context['request'].META disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode('utf-8')) - return force_text(disposition[1]['filename']) + filename_parm = disposition[1] + if 'filename*' in filename_parm: + return self.get_encoded_filename(filename_parm) + return force_text(filename_parm['filename']) except (AttributeError, KeyError): pass + + def get_encoded_filename(self, filename_parm): + """ + Handle encoded filenames per RFC6266. See also: + http://tools.ietf.org/html/rfc2231#section-4 + """ + encoded_filename = force_text(filename_parm['filename*']) + try: + charset, lang, filename = encoded_filename.split('\'', 2) + filename = urlparse.unquote(filename) + except (ValueError, LookupError): + filename = force_text(filename_parm['filename']) + return filename diff --git a/rest_framework/routers.py b/rest_framework/routers.py index ae56673d..8f1ab6fa 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -284,10 +284,10 @@ class DefaultRouter(SimpleRouter): class APIRoot(views.APIView): _ignore_model_permissions = True - def get(self, request, format=None): + def get(self, request, *args, **kwargs): ret = {} for key, url_name in api_root_dict.items(): - ret[key] = reverse(url_name, request=request, format=format) + ret[key] = reverse(url_name, request=request, format=kwargs.get('format', None)) return Response(ret) return APIRoot.as_view() diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 13e57939..8fe999ae 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -317,6 +317,19 @@ class ModelSerializerOptions(object): self.depth = getattr(meta, 'depth', 0) +def lookup_class(mapping, obj): + """ + Takes a dictionary with classes as keys, and an object. + Traverses the object's inheritance hierarchy in method + resolution order, and returns the first matching value + from the dictionary or None. + """ + return next( + (mapping[cls] for cls in inspect.getmro(obj.__class__) if cls in mapping), + None + ) + + class ModelSerializer(Serializer): field_mapping = { models.AutoField: IntegerField, @@ -580,13 +593,20 @@ class ModelSerializer(Serializer): if decimal_places is not None: kwargs['decimal_places'] = decimal_places + if isinstance(model_field, models.BooleanField): + # models.BooleanField has `blank=True`, but *is* actually + # required *unless* a default is provided. + # Also note that <1.6 `default=False`, >=1.6 `default=None`. + kwargs.pop('required', None) + if validator_kwarg: kwargs['validators'] = validator_kwarg - try: - return self.field_mapping[model_field.__class__](**kwargs) - except KeyError: - return ModelField(model_field=model_field, **kwargs) + cls = lookup_class(self.field_mapping, model_field) + if cls is None: + cls = ModelField + kwargs['model_field'] = model_field + return cls(**kwargs) class HyperlinkedModelSerializerOptions(ModelSerializerOptions): diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index cee9724d..a84ccf26 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -5,14 +5,14 @@ <html> <head> {% block head %} - + {% block meta %} <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <meta name="robots" content="NONE,NOARCHIVE" /> {% endblock %} - + <title>{% block title %}Django REST framework{% endblock %}</title> - + {% block style %} {% block bootstrap_theme %} <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/> @@ -21,10 +21,11 @@ <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/> <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/> {% endblock %} - + {% endblock %} </head> + {% block body %} <body class="{% block bodyclass %}{% endblock %} container"> <div class="wrapper"> @@ -43,17 +44,9 @@ <ul class="nav pull-right"> {% block userlinks %} {% if user.is_authenticated %} - <li class="dropdown"> - <a href="#" class="dropdown-toggle" data-toggle="dropdown"> - {{ user }} - <b class="caret"></b> - </a> - <ul class="dropdown-menu"> - <li>{% optional_logout request %}</li> - </ul> - </li> + {% optional_logout request user %} {% else %} - <li>{% optional_login request %}</li> + {% optional_login request %} {% endif %} {% endblock %} </ul> @@ -84,7 +77,7 @@ <div class="btn-group format-selection"> <a class="btn btn-primary js-tooltip" href='{{ request.get_full_path }}' rel="nofollow" title="Make a GET request on the {{ name }} resource">GET</a> - + <button class="btn btn-primary dropdown-toggle js-tooltip" data-toggle="dropdown" title="Specify a format for the GET request"> <span class="caret"></span> @@ -143,7 +136,7 @@ </div> {% if display_edit_forms %} - + {% if post_form or raw_data_post_form %} <div {% if post_form %}class="tabbable"{% endif %}> {% if post_form %} @@ -189,7 +182,7 @@ </div> </div> {% endif %} - + {% if put_form or raw_data_put_form or raw_data_patch_form %} <div {% if put_form %}class="tabbable"{% endif %}> {% if put_form %} @@ -245,7 +238,7 @@ {% endif %} </div> <!-- END Content --> - + <footer> {% block footer %} <p>Sponsored by <a href="http://dabapps.com/">DabApps</a>.</p> @@ -261,4 +254,5 @@ <script src="{% static "rest_framework/js/default.js" %}"></script> {% endblock %} </body> + {% endblock %} </html> diff --git a/rest_framework/templates/rest_framework/login_base.html b/rest_framework/templates/rest_framework/login_base.html index 43860e53..8ab682ac 100644 --- a/rest_framework/templates/rest_framework/login_base.html +++ b/rest_framework/templates/rest_framework/login_base.html @@ -17,21 +17,44 @@ <div class="row-fluid"> <div> - <form action="{% url 'rest_framework:login' %}" class=" form-inline" method="post"> + <form action="{% url 'rest_framework:login' %}" role="form" method="post"> {% csrf_token %} - <div id="div_id_username" class="clearfix control-group"> + <div id="div_id_username" + class="clearfix control-group {% if form.username.errors %}error{% endif %}"> <div class="controls"> <Label class="span4">Username:</label> - <input style="height: 25px" type="text" name="username" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_username"> + <input style="height: 25px" type="text" name="username" maxlength="100" + autocapitalize="off" + autocorrect="off" class="span12 textinput textInput" + id="id_username" required + {% if form.username.value %}value="{{ form.username.value }}"{% endif %}> + {% if form.username.errors %} + <p class="text-error"> + {{ form.username.errors|striptags }} + </p> + {% endif %} </div> </div> - <div id="div_id_password" class="clearfix control-group"> - <div class="controls"> + <div id="div_id_password" + class="clearfix control-group {% if form.password.errors %}error{% endif %}"> + <div class="controls"> <Label class="span4">Password:</label> - <input style="height: 25px" type="password" name="password" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_password"> + <input style="height: 25px" type="password" name="password" maxlength="100" + autocapitalize="off" autocorrect="off" class="span12 textinput textInput" + id="id_password" required> + {% if form.password.errors %} + <p class="text-error"> + {{ form.password.errors|striptags }} + </p> + {% endif %} </div> </div> <input type="hidden" name="next" value="{{ next }}" /> + {% if form.non_field_errors %} + {% for error in form.non_field_errors %} + <div class="well well-small text-error" style="border: none">{{ error }}</div> + {% endfor %} + {% endif %} <div class="form-actions-no-box"> <input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit"> </div> diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index b80a7d77..864d64dd 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -41,22 +41,31 @@ def optional_login(request): except NoReverseMatch: return '' - snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, escape(request.path)) + snippet = "<li><a href='{href}?next={next}'>Log in</a></li>".format(href=login_url, next=escape(request.path)) return snippet @register.simple_tag -def optional_logout(request): +def optional_logout(request, user): """ Include a logout snippet if REST framework's logout view is in the URLconf. """ try: logout_url = reverse('rest_framework:logout') except NoReverseMatch: - return '' - - snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, escape(request.path)) - return snippet + return '<li class="navbar-text">{user}</li>'.format(user=user) + + snippet = """<li class="dropdown"> + <a href="#" class="dropdown-toggle" data-toggle="dropdown"> + {user} + <b class="caret"></b> + </a> + <ul class="dropdown-menu"> + <li><a href='{href}?next={next}'>Log out</a></li> + </ul> + </li>""" + + return snippet.format(user=user, href=logout_url, next=escape(request.path)) @register.simple_tag diff --git a/rest_framework/test.py b/rest_framework/test.py index f89a6dcd..9b40353a 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -36,7 +36,7 @@ class APIRequestFactory(DjangoRequestFactory): Encode the data returning a two tuple of (bytes, content_type) """ - if not data: + if data is None: return ('', content_type) assert format is None or content_type is None, ( diff --git a/rest_framework/urls.py b/rest_framework/urls.py index 8fa3073e..cfcee534 100644 --- a/rest_framework/urls.py +++ b/rest_framework/urls.py @@ -6,7 +6,7 @@ your API requires authentication: urlpatterns = patterns('', ... - url(r'^auth', include('rest_framework.urls', namespace='rest_framework')) + url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')) ) The urls must be namespaced as 'rest_framework', and you should make sure diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index 6d53aed1..470af51b 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -2,11 +2,12 @@ Utility functions to return a formatted name and description for a given view. """ from __future__ import unicode_literals +import re from django.utils.html import escape from django.utils.safestring import mark_safe -from rest_framework.compat import apply_markdown -import re + +from rest_framework.compat import apply_markdown, force_text def remove_trailing_string(content, trailing): @@ -28,6 +29,7 @@ def dedent(content): as it fails to dedent multiline docstrings that include unindented text on the initial line. """ + content = force_text(content) whitespace_counts = [len(line) - len(line.lstrip(' ')) for line in content.splitlines()[1:] if line.lstrip()] diff --git a/rest_framework/views.py b/rest_framework/views.py index 079e9285..3b7b1c16 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -108,7 +108,9 @@ class APIView(View): """ view = super(APIView, cls).as_view(**initkwargs) view.cls = cls - return view + # Note: session based authentication is explicitly CSRF validated, + # all other authentication is CSRF exempt. + return csrf_exempt(view) @property def allowed_methods(self): @@ -376,9 +378,9 @@ class APIView(View): response.exception = True return response - # Note: session based authentication is explicitly CSRF validated, - # all other authentication is CSRF exempt. - @csrf_exempt + # Note: Views are made CSRF exempt from within `as_view` as to prevent + # accidental removal of this exemption in cases where `dispatch` needs to + # be overridden. def dispatch(self, request, *args, **kwargs): """ `.dispatch()` is pretty much the same as Django's regular dispatch, |
