diff options
| -rw-r--r-- | docs/api-guide/fields.md | 5 | ||||
| -rwxr-xr-x | docs/api-guide/generic-views.md | 14 | ||||
| -rw-r--r-- | docs/api-guide/serializers.md | 6 | ||||
| -rw-r--r-- | docs/img/sponsors/2-wusawork.png | bin | 0 -> 12067 bytes | |||
| -rw-r--r-- | docs/topics/2.4-accouncement.md | 4 | ||||
| -rw-r--r-- | docs/topics/kickstarter-announcement.md | 7 | ||||
| -rw-r--r-- | docs/topics/release-notes.md | 1 | ||||
| -rw-r--r-- | rest_framework/exceptions.py | 2 | ||||
| -rw-r--r-- | rest_framework/static/rest_framework/css/bootstrap-tweaks.css | 127 | ||||
| -rw-r--r-- | rest_framework/static/rest_framework/css/default.css | 40 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/base.html | 436 | ||||
| -rw-r--r-- | rest_framework/views.py | 1 | ||||
| -rw-r--r-- | tests/test_throttling.py | 4 |
13 files changed, 350 insertions, 297 deletions
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 95d9fad3..bfbff2ad 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -366,6 +366,9 @@ The [drf-extra-fields][drf-extra-fields] package provides extra serializer field The [django-rest-framework-gis][django-rest-framework-gis] package provides geographic addons for django rest framework like a `GeometryField` field and a GeoJSON serializer. +## django-rest-framework-hstore + +The [django-rest-framework-hstore][django-rest-framework-hstore] package provides an `HStoreField` to support [django-hstore][django-hstore] `DictionaryField` model field. [cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data [FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS @@ -376,3 +379,5 @@ The [django-rest-framework-gis][django-rest-framework-gis] package provides geog [drf-compound-fields]: http://drf-compound-fields.readthedocs.org [drf-extra-fields]: https://github.com/Hipo/drf-extra-fields [django-rest-framework-gis]: https://github.com/djangonauts/django-rest-framework-gis +[django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore +[django-hstore]: https://github.com/djangonauts/django-hstore diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index cab382fb..b1c4e65a 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -49,7 +49,7 @@ For more complex cases you might also want to override various methods on the vi serializer = UserSerializer(queryset, many=True) return Response(serializer.data) -For very simple cases you might want to pass through any class attributes using the `.as_view()` method. For example, your URLconf might include something the following entry. +For very simple cases you might want to pass through any class attributes using the `.as_view()` method. For example, your URLconf might include something like the following entry: url(r'^/users/', ListCreateAPIView.as_view(model=User), name='user-list') @@ -101,7 +101,7 @@ Returns the queryset that should be used for list views, and that should be used This method should always be used rather than accessing `self.queryset` directly, as `self.queryset` gets evaluated only once, and those results are cached for all subsequent requests. -May be overridden to provide dynamic behavior such as returning a queryset that is specific to the user making the request. +May be overridden to provide dynamic behavior, such as returning a queryset, that is specific to the user making the request. For example: @@ -113,7 +113,7 @@ For example: Returns an object instance that should be used for detail views. Defaults to using the `lookup_field` parameter to filter the base queryset. -May be overridden to provide more complex behavior such as object lookups based on more than one URL kwarg. +May be overridden to provide more complex behavior, such as object lookups based on more than one URL kwarg. For example: @@ -133,7 +133,7 @@ Note that if your API doesn't include any object level permissions, you may opti Returns the classes that should be used to filter the queryset. Defaults to returning the `filter_backends` attribute. -May be override to provide more complex behavior with filters, as using different (or even exlusive) lists of filter_backends depending on different criteria. +May be overridden to provide more complex behavior with filters, such as using different (or even exlusive) lists of filter_backends depending on different criteria. For example: @@ -149,7 +149,7 @@ For example: Returns the class that should be used for the serializer. Defaults to returning the `serializer_class` attribute, or dynamically generating a serializer class if the `model` shortcut is being used. -May be override to provide dynamic behavior such as using different serializers for read and write operations, or providing different serializers to different types of users. +May be overridden to provide dynamic behavior, such as using different serializers for read and write operations, or providing different serializers to different types of users. For example: @@ -162,7 +162,7 @@ For example: Returns the page size to use with pagination. By default this uses the `paginate_by` attribute, and may be overridden by the client if the `paginate_by_param` attribute is set. -You may want to override this method to provide more complex behavior such as modifying page sizes based on the media type of the response. +You may want to override this method to provide more complex behavior, such as modifying page sizes based on the media type of the response. For example: @@ -204,7 +204,7 @@ You won't typically need to override the following methods, although you might n # Mixins -The mixin classes provide the actions that are used to provide the basic view behavior. Note that the mixin classes provide action methods rather than defining the handler methods such as `.get()` and `.post()` directly. This allows for more flexible composition of behavior. +The mixin classes provide the actions that are used to provide the basic view behavior. Note that the mixin classes provide action methods rather than defining the handler methods, such as `.get()` and `.post()`, directly. This allows for more flexible composition of behavior. ## ListModelMixin diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 29b7851b..a3694510 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -594,7 +594,13 @@ The [django-rest-framework-mongoengine][mongoengine] package provides a `MongoEn The [django-rest-framework-gis][django-rest-framework-gis] package provides a `GeoFeatureModelSerializer` serializer class that supports GeoJSON both for read and write operations. +## HStoreSerializer + +The [django-rest-framework-hstore][django-rest-framework-hstore] package provides an `HStoreSerializer` to support [django-hstore][django-hstore] `DictionaryField` model field and its `schema-mode` feature. + [cite]: https://groups.google.com/d/topic/django-users/sVFaOfQi4wY/discussion [relations]: relations.md [mongoengine]: https://github.com/umutbozkurt/django-rest-framework-mongoengine [django-rest-framework-gis]: https://github.com/djangonauts/django-rest-framework-gis +[django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore +[django-hstore]: https://github.com/djangonauts/django-hstore diff --git a/docs/img/sponsors/2-wusawork.png b/docs/img/sponsors/2-wusawork.png Binary files differnew file mode 100644 index 00000000..5834729b --- /dev/null +++ b/docs/img/sponsors/2-wusawork.png diff --git a/docs/topics/2.4-accouncement.md b/docs/topics/2.4-accouncement.md index a68f25ed..b6936d2a 100644 --- a/docs/topics/2.4-accouncement.md +++ b/docs/topics/2.4-accouncement.md @@ -75,7 +75,7 @@ Note: The test case and test method matching is fuzzy and will sometimes run oth The `@action` and `@link` decorators were inflexible in that they only allowed additional routes to be added against instance style URLs, not against list style URLs. -The `@action` and `@link` decorators have now been moved to pending deprecation, and the `@list_route` and `@detail_route` decroators have been introduced. +The `@action` and `@link` decorators have now been moved to pending deprecation, and the `@list_route` and `@detail_route` decorators have been introduced. Here's an example of using the new decorators. Firstly we have a detail-type route named "set_password" that acts on a single instance, and takes a `pk` argument in the URL. Secondly we have a list-type route named "recent_users" that acts on a queryset, and does not take any arguments in the URL. @@ -126,6 +126,8 @@ There are also a number of other features and bugfixes as [listed in the release Smarter [client IP identification for throttling][client-ip-identification], with the addition of the `NUM_PROXIES` setting. +Added the standardized `Retry-After` header to throttled responses, as per [RFC 6585](http://tools.ietf.org/html/rfc6585). This should now be used in preference to the custom `X-Trottle-Wait-Seconds` header which will be fully deprecated in 3.0. + ## Deprecations All API changes in 2.3 that previously raised `PendingDeprecationWarning` will now raise a `DeprecationWarning`, which is loud by default. diff --git a/docs/topics/kickstarter-announcement.md b/docs/topics/kickstarter-announcement.md index 6d091064..7d1f6d0e 100644 --- a/docs/topics/kickstarter-announcement.md +++ b/docs/topics/kickstarter-announcement.md @@ -82,7 +82,7 @@ Our gold sponsors include companies large and small. Many thanks for their signi <li><a href="https://opbeat.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-opbeat.png);">Opbeat</a></li> <li><a href="https://koordinates.com" rel="nofollow" style="background-image:url(../img/sponsors/2-koordinates.png);">Koordinates</a></li> <li><a href="http://pulsecode.ca" rel="nofollow" style="background-image:url(../img/sponsors/2-pulsecode.png);">Pulsecode Inc.</a></li> -<li><a href="http://singinghorsestudio.com" rel="nofollow" style="background-image:url(../img/sponsors/2-singing-horse.png);">Singing Horse Studio. Ltd.</a></li> +<li><a href="http://singinghorsestudio.com" rel="nofollow" style="background-image:url(../img/sponsors/2-singing-horse.png);">Singing Horse Studio Ltd.</a></li> <li><a href="https://www.heroku.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-heroku.png);">Heroku</a></li> <li><a href="https://www.galileo-press.de/" rel="nofollow" style="background-image:url(../img/sponsors/2-galileo_press.png);">Galileo Press</a></li> <li><a href="http://www.securitycompass.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-security_compass.png);">Security Compass</a></li> @@ -92,13 +92,12 @@ Our gold sponsors include companies large and small. Many thanks for their signi <li><a href="http://crypticocorp.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-cryptico.png);">Cryptico Corp</a></li> <li><a href="http://www.nexthub.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-nexthub.png);">NextHub</a></li> <li><a href="https://www.compile.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-compile.png);">Compile</a></li> +<li><a href="http://wusawork.org" rel="nofollow" style="background-image:url(../img/sponsors/2-wusawork.png);">WusaWork</a></li> <li><a href="http://envisionlinux.org/blog" rel="nofollow">Envision Linux</a></li> </ul> <div style="clear: both; padding-bottom: 40px;"></div> -**Individual backers**: Simon Haugk. - --- ### Silver sponsors @@ -145,7 +144,7 @@ The serious financial contribution that our silver sponsors have made is very mu <div style="clear: both; padding-bottom: 40px;"></div> -**Individual backers**: Paul Hallet, <a href="http://www.paulwhippconsulting.com/">Paul Whipp</a>, Dylan Roy, Jannis Leidel, <a href="https://linovia.com/en/">Xavier Ordoquy</a>, <a href="http://spielmannsolutions.com/">Johannes Spielmann</a>, <a href="http://brooklynhacker.com/">Rob Spectre</a>, <a href="http://chrisheisel.com/">Chris Heisel</a>, Marwan Alsabbagh, Haris Ali, Tuomas Toivonen. +**Individual backers**: Paul Hallett, <a href="http://www.paulwhippconsulting.com/">Paul Whipp</a>, Dylan Roy, Jannis Leidel, <a href="https://linovia.com/en/">Xavier Ordoquy</a>, <a href="http://spielmannsolutions.com/">Johannes Spielmann</a>, <a href="http://brooklynhacker.com/">Rob Spectre</a>, <a href="http://chrisheisel.com/">Chris Heisel</a>, Marwan Alsabbagh, Haris Ali, Tuomas Toivonen. --- diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index badc28e9..c158c47b 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -53,6 +53,7 @@ You can determine your currently installed version using `pip freeze`: * Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings. * Added `NUM_PROXIES` setting for smarter client IP identification. * Added `MAX_PAGINATE_BY` setting and `max_paginate_by` generic view attribute. +* Added `Retry-After` header to throttled responses, as per [RFC 6585](http://tools.ietf.org/html/rfc6585). This should now be used in preference to the custom `X-Trottle-Wait-Seconds` header which will be fully deprecated in 3.0. * Added `cache` attribute to throttles to allow overriding of default cache. * Added `lookup_value_regex` attribute to routers, to allow the URL argument matching to be constrainted by the user. * Added `allow_none` option to `CharField`. diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index 97dab77e..ad52d172 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -72,7 +72,7 @@ class UnsupportedMediaType(APIException): class Throttled(APIException): status_code = status.HTTP_429_TOO_MANY_REQUESTS default_detail = 'Request was throttled.' - extra_detail = "Expected available in %d second%s." + extra_detail = " Expected available in %d second%s." def __init__(self, wait=None, detail=None): if wait is None: diff --git a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css index 6bfb778c..6fa1e6cb 100644 --- a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css +++ b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css @@ -6,30 +6,30 @@ a single block in the template. */ - .form-actions { - background: transparent; - border-top-color: transparent; - padding-top: 0; + background: transparent; + border-top-color: transparent; + padding-top: 0; } .navbar-inverse .brand a { - color: #999; + color: #999999; } .navbar-inverse .brand:hover a { - color: white; - text-decoration: none; + color: white; + text-decoration: none; } /* custom navigation styles */ -.wrapper .navbar{ +.navbar { width: 100%; - position: absolute; + position: fixed; left: 0; top: 0; + z-index: 3; } -.navbar .navbar-inner{ +.navbar .navbar-inner { background: #2C2C2C; color: white; border: none; @@ -37,25 +37,26 @@ a single block in the template. border-radius: 0px; } -.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand:hover{ +.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand:hover { color: white; } .nav-list > .active > a, .nav-list > .active > a:hover { - background: #2c2c2c; + background: #2C2C2C; } -.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{ - color: #A30000; +.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li { + color: #A30000; } -.navbar .navbar-inner .dropdown-menu li a:hover{ - background: #eeeeee; - color: #c20000; + +.navbar .navbar-inner .dropdown-menu li a:hover { + background: #EEEEEE; + color: #C20000; } /*=== dabapps bootstrap styles ====*/ -html{ +html { width:100%; background: none; } @@ -65,121 +66,127 @@ body, .navbar .navbar-inner .container-fluid { margin: 0 auto; } -body{ +body { background: url("../img/grid.png") repeat-x; background-attachment: fixed; } -#content{ - margin: 0; +#content { + margin: 0; + padding-bottom: 60px; } /* sticky footer and footer */ html, body { height: 100%; } + .wrapper { + position: relative; + top: 0; + left: 0; + padding-top: 60px; + margin: -60px 0; min-height: 100%; - height: auto !important; - height: 100%; - margin: 0 auto -60px; } .form-switcher { - margin-bottom: 0; + margin-bottom: 0; } .well { - -webkit-box-shadow: none; - -moz-box-shadow: none; - box-shadow: none; + -webkit-box-shadow: none; + -moz-box-shadow: none; + box-shadow: none; } .well .form-actions { - padding-bottom: 0; - margin-bottom: 0; + padding-bottom: 0; + margin-bottom: 0; } .well form { - margin-bottom: 0; + margin-bottom: 0; } .well form .help-block { - color: #999; + color: #999999; } .nav-tabs { - border: 0; + border: 0; } .nav-tabs > li { - float: right; + float: right; } .nav-tabs li a { - margin-right: 0; + margin-right: 0; } .nav-tabs > .active > a { - background: #f5f5f5; + background: #F5F5F5; } .nav-tabs > .active > a:hover { - background: #f5f5f5; -} - -.tabbable.first-tab-active .tab-content -{ - border-top-right-radius: 0; + background: #F5F5F5; } -#footer, #push { - height: 60px; /* .push must be the same height as .footer */ +.tabbable.first-tab-active .tab-content { + border-top-right-radius: 0; } -#footer{ - text-align: right; +footer { + position: absolute; + bottom: 0; + left: 0; + clear: both; + z-index: 10; + height: 60px; + width: 95%; + margin: 0 2.5%; } -#footer p { +footer p { text-align: center; color: gray; - border-top: 1px solid #DDD; + border-top: 1px solid #DDDDDD; padding-top: 10px; } -#footer a { - color: gray; +footer a { + color: gray !important; font-weight: bold; } -#footer a:hover { +footer a:hover { color: gray; } .page-header { - border-bottom: none; - padding-bottom: 0px; - margin-bottom: 20px; + border-bottom: none; + padding-bottom: 0px; + margin-bottom: 20px; } /* custom general page styles */ -.hero-unit h2, .hero-unit h1{ +.hero-unit h1, .hero-unit h2 { color: #A30000; } -body a, body a{ +body a { color: #A30000; } -body a:hover{ +body a:hover { color: #c20000; } -#content a span{ - text-decoration: underline; +#content a span { + text-decoration: underline; } .request-info { - clear:both; + clear:both; } diff --git a/rest_framework/static/rest_framework/css/default.css b/rest_framework/static/rest_framework/css/default.css index 0261a303..461cdfe5 100644 --- a/rest_framework/static/rest_framework/css/default.css +++ b/rest_framework/static/rest_framework/css/default.css @@ -3,20 +3,20 @@ content running up underneath it. */ h1 { - font-weight: 500; + font-weight: 500; } h2, h3 { - font-weight: 300; + font-weight: 300; } .resource-description, .response-info { - margin-bottom: 2em; + margin-bottom: 2em; } .version:before { - content: "v"; - opacity: 0.6; - padding-right: 0.25em; + content: "v"; + opacity: 0.6; + padding-right: 0.25em; } .version { @@ -24,16 +24,16 @@ h2, h3 { } .format-option { - font-family: Menlo, Consolas, "Andale Mono", "Lucida Console", monospace; + font-family: Menlo, Consolas, "Andale Mono", "Lucida Console", monospace; } .button-form { - float: right; - margin-right: 1em; + float: right; + margin-right: 1em; } ul.breadcrumb { - margin: 58px 0 0 0; + margin: 80px 0 0 0; } form select, form input, form textarea { @@ -43,17 +43,18 @@ form select, form input, form textarea { form select[multiple] { height: 150px; } + /* To allow tooltips to work on disabled elements */ .disabled-tooltip-shield { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; } .errorlist { - margin-top: 0.5em; + margin-top: 0.5em; } pre { @@ -64,8 +65,7 @@ pre { } .page-header { - border-bottom: none; - padding-bottom: 0px; - margin-bottom: 20px; + border-bottom: none; + padding-bottom: 0px; + margin-bottom: 20px; } - diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index b6e9ca5c..cee9724d 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -4,233 +4,261 @@ <!DOCTYPE html> <html> <head> - {% block head %} - - {% block meta %} - <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> - <meta name="robots" content="NONE,NOARCHIVE" /> + {% 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" %}"/> + <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/> + {% endblock %} + <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> - <title>{% block title %}Django REST framework{% endblock %}</title> + <body class="{% block bodyclass %}{% endblock %} container"> - {% block style %} - {% block bootstrap_theme %} - <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/> - <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/> - {% endblock %} - <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 %} + <div class="wrapper"> - {% endblock %} - </head> + {% block navbar %} + <div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}"> + <div class="navbar-inner"> + <div class="container-fluid"> + <span> + {% block branding %} + <a class='brand' rel="nofollow" href='http://www.django-rest-framework.org'> + Django REST framework <span class="version">{{ version }}</span> + </a> + {% endblock %} + </span> + <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> + {% else %} + <li>{% optional_login request %}</li> + {% endif %} + {% endblock %} + </ul> + </div> + </div> + </div> + {% endblock %} - {% block body %} - <body class="{% block bodyclass %}{% endblock %} container"> + {% block breadcrumbs %} + <ul class="breadcrumb"> + {% for breadcrumb_name, breadcrumb_url in breadcrumblist %} + <li> + <a href="{{ breadcrumb_url }}" {% if forloop.last %}class="active"{% endif %}> + {{ breadcrumb_name }} + </a> + {% if not forloop.last %}<span class="divider">›</span>{% endif %} + </li> + {% endfor %} + </ul> + {% endblock %} - <div class="wrapper"> + <!-- Content --> + <div id="content"> - {% block navbar %} - <div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}"> - <div class="navbar-inner"> - <div class="container-fluid"> - <span href="/"> - {% block branding %}<a class='brand' rel="nofollow" href='http://www.django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %} - </span> - <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> + {% if 'GET' in allowed_methods %} + <form id="get-form" class="pull-right"> + <fieldset> + <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> + </button> <ul class="dropdown-menu"> - <li>{% optional_logout request %}</li> + {% for format in available_formats %} + <li> + <a class="js-tooltip format-option" + href='{% add_query_param request api_settings.URL_FORMAT_OVERRIDE format %}' + rel="nofollow" + title="Make a GET request on the {{ name }} resource with the format set to `{{ format }}`"> + {{ format }} + </a> + </li> + {% endfor %} </ul> - </li> - {% else %} - <li>{% optional_login request %}</li> - {% endif %} - {% endblock %} - </ul> - </div> - </div> - </div> - {% endblock %} - - {% block breadcrumbs %} - <ul class="breadcrumb"> - {% for breadcrumb_name, breadcrumb_url in breadcrumblist %} - <li> - <a href="{{ breadcrumb_url }}" {% if forloop.last %}class="active"{% endif %}>{{ breadcrumb_name }}</a> {% if not forloop.last %}<span class="divider">›</span>{% endif %} - </li> - {% endfor %} - </ul> - {% endblock %} + </div> + </fieldset> + </form> + {% endif %} - <!-- Content --> - <div id="content"> + {% if options_form %} + <form class="button-form" action="{{ request.get_full_path }}" method="POST"> + {% csrf_token %} + <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" /> + <button class="btn btn-primary js-tooltip" + title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button> + </form> + {% endif %} - {% if 'GET' in allowed_methods %} - <form id="get-form" class="pull-right"> - <fieldset> - <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> + {% if delete_form %} + <form class="button-form" action="{{ request.get_full_path }}" method="POST"> + {% csrf_token %} + <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" /> + <button class="btn btn-danger js-tooltip" + title="Make a DELETE request on the {{ name }} resource">DELETE</button> + </form> + {% endif %} - <button class="btn btn-primary dropdown-toggle js-tooltip" data-toggle="dropdown" title="Specify a format for the GET request"> - <span class="caret"></span> - </button> - <ul class="dropdown-menu"> - {% for format in available_formats %} - <li> - <a class="js-tooltip format-option" href='{% add_query_param request api_settings.URL_FORMAT_OVERRIDE format %}' rel="nofollow" title="Make a GET request on the {{ name }} resource with the format set to `{{ format }}`">{{ format }}</a> - </li> - {% endfor %} - </ul> + <div class="content-main"> + <div class="page-header"> + <h1>{{ name }}</h1> </div> - - </fieldset> - </form> - {% endif %} - - {% if options_form %} - <form class="button-form" action="{{ request.get_full_path }}" method="POST"> - {% csrf_token %} - <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" /> - <button class="btn btn-primary js-tooltip" title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button> - </form> - {% endif %} - - {% if delete_form %} - <form class="button-form" action="{{ request.get_full_path }}" method="POST"> - {% csrf_token %} - <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" /> - <button class="btn btn-danger js-tooltip" title="Make a DELETE request on the {{ name }} resource">DELETE</button> - </form> - {% endif %} - - <div class="content-main"> - <div class="page-header"><h1>{{ name }}</h1></div> - {% block description %} - {{ description }} - {% endblock %} - <div class="request-info" style="clear: both" > - <pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre> - </div> - <div class="response-info"> - <pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %} + {% block description %} + {{ description }} + {% endblock %} + <div class="request-info" style="clear: both" > + <pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre> + </div> + <div class="response-info"> + <pre class="prettyprint"><span class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %} {% for key, val in response_headers.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span> {% endfor %} -</div>{{ content|urlize_quoted_links }}</pre>{% endautoescape %} - </div> - </div> - - {% if display_edit_forms %} - - {% if post_form or raw_data_post_form %} - <div {% if post_form %}class="tabbable"{% endif %}> - {% if post_form %} - <ul class="nav nav-tabs form-switcher"> - <li><a name='html-tab' href="#object-form" data-toggle="tab">HTML form</a></li> - <li><a name='raw-tab' href="#generic-content-form" data-toggle="tab">Raw data</a></li> - </ul> - {% endif %} - <div class="well tab-content"> - {% if post_form %} - <div class="tab-pane" id="object-form"> - {% with form=post_form %} - <form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal"> - <fieldset> - {{ post_form }} - <div class="form-actions"> - <button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button> - </div> - </fieldset> - </form> - {% endwith %} - </div> - {% endif %} - <div {% if post_form %}class="tab-pane"{% endif %} id="generic-content-form"> - {% with form=raw_data_post_form %} - <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal"> - <fieldset> - {% include "rest_framework/raw_data_form.html" %} - <div class="form-actions"> - <button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button> - </div> - </fieldset> - </form> - {% endwith %} - </div> +</span>{{ content|urlize_quoted_links }}</pre>{% endautoescape %} </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 %} - <ul class="nav nav-tabs form-switcher"> - <li><a name='html-tab' href="#object-form" data-toggle="tab">HTML form</a></li> - <li><a name='raw-tab' href="#generic-content-form" data-toggle="tab">Raw data</a></li> - </ul> - {% endif %} - <div class="well tab-content"> - {% if put_form %} - <div class="tab-pane" id="object-form"> - <form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal"> - <fieldset> - {{ put_form }} - <div class="form-actions"> - <button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button> + {% if display_edit_forms %} + + {% if post_form or raw_data_post_form %} + <div {% if post_form %}class="tabbable"{% endif %}> + {% if post_form %} + <ul class="nav nav-tabs form-switcher"> + <li> + <a name='html-tab' href="#object-form" data-toggle="tab">HTML form</a> + </li> + <li> + <a name='raw-tab' href="#generic-content-form" data-toggle="tab">Raw data</a> + </li> + </ul> + {% endif %} + <div class="well tab-content"> + {% if post_form %} + <div class="tab-pane" id="object-form"> + {% with form=post_form %} + <form action="{{ request.get_full_path }}" + method="POST" enctype="multipart/form-data" class="form-horizontal"> + <fieldset> + {{ post_form }} + <div class="form-actions"> + <button class="btn btn-primary" + title="Make a POST request on the {{ name }} resource">POST</button> + </div> + </fieldset> + </form> + {% endwith %} </div> - </fieldset> - </form> + {% endif %} + <div {% if post_form %}class="tab-pane"{% endif %} id="generic-content-form"> + {% with form=raw_data_post_form %} + <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal"> + <fieldset> + {% include "rest_framework/raw_data_form.html" %} + <div class="form-actions"> + <button class="btn btn-primary" + title="Make a POST request on the {{ name }} resource">POST</button> + </div> + </fieldset> + </form> + {% endwith %} + </div> + </div> </div> - {% endif %} - <div {% if put_form %}class="tab-pane"{% endif %} id="generic-content-form"> - {% with form=raw_data_put_or_patch_form %} - <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal"> - <fieldset> - {% include "rest_framework/raw_data_form.html" %} - <div class="form-actions"> - {% if raw_data_put_form %} - <button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button> - {% endif %} - {% if raw_data_patch_form %} - <button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PATCH" title="Make a PATCH request on the {{ name }} resource">PATCH</button> - {% endif %} + {% endif %} + + {% if put_form or raw_data_put_form or raw_data_patch_form %} + <div {% if put_form %}class="tabbable"{% endif %}> + {% if put_form %} + <ul class="nav nav-tabs form-switcher"> + <li> + <a name='html-tab' href="#object-form" data-toggle="tab">HTML form</a> + </li> + <li> + <a name='raw-tab' href="#generic-content-form" data-toggle="tab">Raw data</a> + </li> + </ul> + {% endif %} + <div class="well tab-content"> + {% if put_form %} + <div class="tab-pane" id="object-form"> + <form action="{{ request.get_full_path }}" + method="POST" enctype="multipart/form-data" class="form-horizontal"> + <fieldset> + {{ put_form }} + <div class="form-actions"> + <button class="btn btn-primary js-tooltip" + name="{{ api_settings.FORM_METHOD_OVERRIDE }}" + value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button> + </div> + </fieldset> + </form> </div> - </fieldset> - </form> - {% endwith %} + {% endif %} + <div {% if put_form %}class="tab-pane"{% endif %} id="generic-content-form"> + {% with form=raw_data_put_or_patch_form %} + <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal"> + <fieldset> + {% include "rest_framework/raw_data_form.html" %} + <div class="form-actions"> + {% if raw_data_put_form %} + <button class="btn btn-primary js-tooltip" + name="{{ api_settings.FORM_METHOD_OVERRIDE }}" + value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button> + {% endif %} + {% if raw_data_patch_form %} + <button class="btn btn-primary js-tooltip" + name="{{ api_settings.FORM_METHOD_OVERRIDE }}" + value="PATCH" title="Make a PATCH request on the {{ name }} resource">PATCH</button> + {% endif %} + </div> + </fieldset> + </form> + {% endwith %} + </div> + </div> </div> - </div> - </div> + {% endif %} {% endif %} - {% endif %} - - </div> - <!-- END content-main --> - - </div> - <!-- END Content --> - - <div id="push"></div> - - </div> - - </div><!-- ./wrapper --> + </div> + <!-- END Content --> + + <footer> + {% block footer %} + <p>Sponsored by <a href="http://dabapps.com/">DabApps</a>.</p> + {% endblock %} + </footer> - {% block footer %} - {% endblock %} + </div><!-- ./wrapper --> - {% block script %} - <script src="{% static "rest_framework/js/jquery-1.8.1-min.js" %}"></script> - <script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script> - <script src="{% static "rest_framework/js/prettify-min.js" %}"></script> - <script src="{% static "rest_framework/js/default.js" %}"></script> - {% endblock %} - </body> - {% endblock %} + {% block script %} + <script src="{% static "rest_framework/js/jquery-1.8.1-min.js" %}"></script> + <script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script> + <script src="{% static "rest_framework/js/prettify-min.js" %}"></script> + <script src="{% static "rest_framework/js/default.js" %}"></script> + {% endblock %} + </body> </html> diff --git a/rest_framework/views.py b/rest_framework/views.py index bca0aaef..23df3443 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -62,6 +62,7 @@ def exception_handler(exc): headers['WWW-Authenticate'] = exc.auth_header if getattr(exc, 'wait', None): headers['X-Throttle-Wait-Seconds'] = '%d' % exc.wait + headers['Retry-After'] = '%d' % exc.wait return Response({'detail': exc.detail}, status=exc.status_code, diff --git a/tests/test_throttling.py b/tests/test_throttling.py index b0cb2fe7..7b696f07 100644 --- a/tests/test_throttling.py +++ b/tests/test_throttling.py @@ -118,8 +118,10 @@ class ThrottlingTests(TestCase): response = view.as_view()(request) if expect is not None: self.assertEqual(response['X-Throttle-Wait-Seconds'], expect) + self.assertEqual(response['Retry-After'], expect) else: self.assertFalse('X-Throttle-Wait-Seconds' in response) + self.assertFalse('Retry-After' in response) def test_seconds_fields(self): """ @@ -172,11 +174,13 @@ class ThrottlingTests(TestCase): response = MockView_NonTimeThrottling.as_view()(request) self.assertFalse('X-Throttle-Wait-Seconds' in response) + self.assertFalse('Retry-After' in response) self.assertTrue(MockView_NonTimeThrottling.throttle_classes[0].called) response = MockView_NonTimeThrottling.as_view()(request) self.assertFalse('X-Throttle-Wait-Seconds' in response) + self.assertFalse('Retry-After' in response) class ScopedRateThrottleTests(TestCase): |
