aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml26
-rw-r--r--README.md1
-rwxr-xr-xdocs/api-guide/authentication.md2
-rw-r--r--docs/api-guide/fields.md20
-rw-r--r--docs/api-guide/filtering.md10
-rwxr-xr-xdocs/api-guide/generic-views.md14
-rw-r--r--docs/api-guide/pagination.md3
-rw-r--r--docs/api-guide/permissions.md11
-rw-r--r--docs/api-guide/renderers.md20
-rw-r--r--docs/api-guide/routers.md9
-rw-r--r--docs/api-guide/serializers.md22
-rw-r--r--docs/api-guide/throttling.md2
-rw-r--r--docs/api-guide/viewsets.md4
-rw-r--r--docs/css/default.css73
-rw-r--r--docs/img/sponsors/0-eventbrite.pngbin0 -> 22429 bytes
-rw-r--r--docs/img/sponsors/1-cyan.pngbin0 -> 6121 bytes
-rw-r--r--docs/img/sponsors/1-divio.pngbin0 -> 4864 bytes
-rw-r--r--docs/img/sponsors/1-kuwaitnet.pngbin0 -> 15489 bytes
-rw-r--r--docs/img/sponsors/1-lulu.pngbin0 -> 18013 bytes
-rw-r--r--docs/img/sponsors/1-potato.pngbin0 -> 12190 bytes
-rw-r--r--docs/img/sponsors/1-purplebit.pngbin0 -> 9161 bytes
-rw-r--r--docs/img/sponsors/1-runscope.pngbin0 -> 10913 bytes
-rw-r--r--docs/img/sponsors/1-simple-energy.pngbin0 -> 54455 bytes
-rw-r--r--docs/img/sponsors/1-vokal_interactive.pngbin0 -> 22814 bytes
-rw-r--r--docs/img/sponsors/1-wiredrive.pngbin0 -> 8082 bytes
-rw-r--r--docs/img/sponsors/2-byte.pngbin0 -> 13690 bytes
-rw-r--r--docs/img/sponsors/2-compile.pngbin0 -> 3108 bytes
-rw-r--r--docs/img/sponsors/2-crate.pngbin0 -> 8257 bytes
-rw-r--r--docs/img/sponsors/2-cryptico.pngbin0 -> 9970 bytes
-rw-r--r--docs/img/sponsors/2-django.pngbin0 -> 5055 bytes
-rw-r--r--docs/img/sponsors/2-galileo_press.pngbin0 -> 11451 bytes
-rw-r--r--docs/img/sponsors/2-heroku.pngbin0 -> 7337 bytes
-rw-r--r--docs/img/sponsors/2-hipflask.pngbin0 -> 6016 bytes
-rw-r--r--docs/img/sponsors/2-hipo.pngbin0 -> 8111 bytes
-rw-r--r--docs/img/sponsors/2-koordinates.pngbin0 -> 1934 bytes
-rw-r--r--docs/img/sponsors/2-laterpay.pngbin0 -> 2003 bytes
-rw-r--r--docs/img/sponsors/2-lightning_kite.pngbin0 -> 6715 bytes
-rw-r--r--docs/img/sponsors/2-mirus_research.pngbin0 -> 12414 bytes
-rw-r--r--docs/img/sponsors/2-nexthub.pngbin0 -> 2562 bytes
-rw-r--r--docs/img/sponsors/2-opbeat.pngbin0 -> 11603 bytes
-rw-r--r--docs/img/sponsors/2-prorenata.pngbin0 -> 4051 bytes
-rw-r--r--docs/img/sponsors/2-rapasso.pngbin0 -> 13667 bytes
-rw-r--r--docs/img/sponsors/2-schuberg_philis.pngbin0 -> 21870 bytes
-rw-r--r--docs/img/sponsors/2-security_compass.pngbin0 -> 4107 bytes
-rw-r--r--docs/img/sponsors/2-sga.pngbin0 -> 11112 bytes
-rw-r--r--docs/img/sponsors/2-sirono.pngbin0 -> 4941 bytes
-rw-r--r--docs/img/sponsors/2-vinta.pngbin0 -> 6844 bytes
-rw-r--r--docs/img/sponsors/3-aba.pngbin0 -> 18974 bytes
-rw-r--r--docs/img/sponsors/3-aditium.pngbin0 -> 3028 bytes
-rw-r--r--docs/img/sponsors/3-alwaysdata.pngbin0 -> 9349 bytes
-rw-r--r--docs/img/sponsors/3-ax_semantics.pngbin0 -> 11509 bytes
-rw-r--r--docs/img/sponsors/3-beefarm.pngbin0 -> 13066 bytes
-rw-r--r--docs/img/sponsors/3-blimp.pngbin0 -> 6241 bytes
-rw-r--r--docs/img/sponsors/3-brightloop.pngbin0 -> 6864 bytes
-rw-r--r--docs/img/sponsors/3-cantemo.gifbin0 -> 4526 bytes
-rw-r--r--docs/img/sponsors/3-crosswordtracker.pngbin0 -> 6715 bytes
-rw-r--r--docs/img/sponsors/3-fluxility.pngbin0 -> 10064 bytes
-rw-r--r--docs/img/sponsors/3-garfo.pngbin0 -> 3322 bytes
-rw-r--r--docs/img/sponsors/3-gizmag.pngbin0 -> 5370 bytes
-rw-r--r--docs/img/sponsors/3-holvi.pngbin0 -> 7533 bytes
-rw-r--r--docs/img/sponsors/3-imt_computer_services.pngbin0 -> 70397 bytes
-rw-r--r--docs/img/sponsors/3-infinite_code.pngbin0 -> 21786 bytes
-rw-r--r--docs/img/sponsors/3-ipushpull.pngbin0 -> 10089 bytes
-rw-r--r--docs/img/sponsors/3-isl.pngbin0 -> 19203 bytes
-rw-r--r--docs/img/sponsors/3-life_the_game.pngbin0 -> 5485 bytes
-rw-r--r--docs/img/sponsors/3-makespace.pngbin0 -> 8420 bytes
-rw-r--r--docs/img/sponsors/3-nephila.pngbin0 -> 8415 bytes
-rw-r--r--docs/img/sponsors/3-openeye.pngbin0 -> 14155 bytes
-rw-r--r--docs/img/sponsors/3-pathwright.pngbin0 -> 13036 bytes
-rw-r--r--docs/img/sponsors/3-phurba.pngbin0 -> 3064 bytes
-rw-r--r--docs/img/sponsors/3-pkgfarm.pngbin0 -> 2275 bytes
-rw-r--r--docs/img/sponsors/3-providenz.pngbin0 -> 12580 bytes
-rw-r--r--docs/img/sponsors/3-safari.pngbin0 -> 4419 bytes
-rw-r--r--docs/img/sponsors/3-shippo.pngbin0 -> 7345 bytes
-rw-r--r--docs/img/sponsors/3-teonite.pngbin0 -> 7882 bytes
-rw-r--r--docs/img/sponsors/3-thermondo-gmbh.pngbin0 -> 20046 bytes
-rw-r--r--docs/img/sponsors/3-tivix.pngbin0 -> 3552 bytes
-rw-r--r--docs/img/sponsors/3-trackmaven.pngbin0 -> 5331 bytes
-rw-r--r--docs/img/sponsors/3-transcode.pngbin0 -> 8615 bytes
-rw-r--r--docs/img/sponsors/3-triggered_messaging.pngbin0 -> 10509 bytes
-rw-r--r--docs/img/sponsors/3-vzzual.pngbin0 -> 12008 bytes
-rw-r--r--docs/img/sponsors/3-wildfish.pngbin0 -> 4137 bytes
-rw-r--r--docs/index.md2
-rw-r--r--docs/template.html25
-rw-r--r--docs/topics/browsable-api.md7
-rw-r--r--docs/topics/documenting-your-api.md2
-rw-r--r--docs/topics/kickstarter-announcement.md162
-rw-r--r--docs/topics/release-notes.md27
-rw-r--r--docs/tutorial/1-serialization.md24
-rw-r--r--docs/tutorial/2-requests-and-responses.md8
-rw-r--r--docs/tutorial/3-class-based-views.md4
-rw-r--r--docs/tutorial/4-authentication-and-permissions.md18
-rw-r--r--docs/tutorial/5-relationships-and-hyperlinked-apis.md26
-rw-r--r--docs/tutorial/6-viewsets-and-routers.md6
-rw-r--r--docs/tutorial/quickstart.md45
-rwxr-xr-xmkdocs.py6
-rw-r--r--rest_framework/__init__.py2
-rw-r--r--rest_framework/authentication.py14
-rw-r--r--rest_framework/authtoken/models.py2
-rw-r--r--rest_framework/authtoken/serializers.py11
-rw-r--r--rest_framework/compat.py14
-rw-r--r--rest_framework/fields.py35
-rw-r--r--rest_framework/filters.py4
-rw-r--r--rest_framework/generics.py25
-rw-r--r--rest_framework/parsers.py6
-rw-r--r--rest_framework/relations.py2
-rw-r--r--rest_framework/renderers.py48
-rw-r--r--rest_framework/request.py4
-rw-r--r--rest_framework/response.py6
-rw-r--r--rest_framework/serializers.py59
-rw-r--r--rest_framework/templates/rest_framework/base.html6
-rw-r--r--rest_framework/templates/rest_framework/login_base.html15
-rw-r--r--rest_framework/templatetags/rest_framework.py6
-rw-r--r--rest_framework/test.py2
-rw-r--r--rest_framework/tests/models.py10
-rw-r--r--rest_framework/tests/test_authentication.py17
-rw-r--r--rest_framework/tests/test_fields.py42
-rw-r--r--rest_framework/tests/test_genericrelations.py18
-rw-r--r--rest_framework/tests/test_generics.py74
-rw-r--r--rest_framework/tests/test_parsers.py4
-rw-r--r--rest_framework/tests/test_relations.py24
-rw-r--r--rest_framework/tests/test_renderers.py13
-rw-r--r--rest_framework/tests/test_serializer.py31
-rw-r--r--rest_framework/tests/test_serializers.py5
-rw-r--r--rest_framework/tests/test_urlizer.py38
-rw-r--r--rest_framework/tests/test_views.py16
-rw-r--r--rest_framework/throttling.py2
-rw-r--r--rest_framework/urls.py8
-rw-r--r--rest_framework/utils/mediatypes.py2
-rw-r--r--tox.ini78
130 files changed, 969 insertions, 253 deletions
diff --git a/.travis.yml b/.travis.yml
index 60b48cba..7f1fda83 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,25 +5,24 @@ python:
- "2.7"
- "3.2"
- "3.3"
+ - "3.4"
env:
- - DJANGO="https://www.djangoproject.com/download/1.7b1/tarball/"
- - DJANGO="django==1.6.2"
- - DJANGO="django==1.5.5"
- - DJANGO="django==1.4.10"
+ - DJANGO="https://www.djangoproject.com/download/1.7c2/tarball/"
+ - DJANGO="django==1.6.5"
+ - DJANGO="django==1.5.8"
+ - DJANGO="django==1.4.13"
- DJANGO="django==1.3.7"
install:
- pip install $DJANGO
- - pip install defusedxml==0.3 Pillow==2.3.0
+ - pip install defusedxml==0.3 Pillow==2.3.0 django-guardian==1.2.3
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211; fi"
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.2.4; fi"
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4; fi"
- - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-guardian==1.1.1; fi"
- "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4; fi"
- "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.7; fi"
- - "if [[ ${TRAVIS_PYTHON_VERSION::1} == '3' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi"
- - "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7b1/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi"
+ - "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7c2/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi"
- export PYTHONPATH=.
script:
@@ -32,13 +31,16 @@ script:
matrix:
exclude:
- python: "2.6"
- env: DJANGO="https://www.djangoproject.com/download/1.7b1/tarball/"
+ env: DJANGO="https://www.djangoproject.com/download/1.7c2/tarball/"
- python: "3.2"
- env: DJANGO="django==1.4.10"
+ env: DJANGO="django==1.4.13"
- python: "3.2"
env: DJANGO="django==1.3.7"
- python: "3.3"
- env: DJANGO="django==1.4.10"
+ env: DJANGO="django==1.4.13"
- python: "3.3"
env: DJANGO="django==1.3.7"
-
+ - python: "3.4"
+ env: DJANGO="django==1.4.13"
+ - python: "3.4"
+ env: DJANGO="django==1.3.7"
diff --git a/README.md b/README.md
index 73e4b13f..da5f27ae 100644
--- a/README.md
+++ b/README.md
@@ -136,6 +136,7 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
[build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master
[travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master
[twitter]: https://twitter.com/_tomchristie
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index 88a7a011..1cb37d67 100755
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -119,7 +119,7 @@ Unauthenticated responses that are denied permission will result in an `HTTP 401
This authentication scheme 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` scheme, include `rest_framework.authtoken` in your `INSTALLED_APPS` setting:
+To use the `TokenAuthentication` scheme you'll need to [configure the authentication classes](#setting-the-authentication-scheme) to include `TokenAuthentication`, and additionally include `rest_framework.authtoken` in your `INSTALLED_APPS` setting:
INSTALLED_APPS = (
...
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index 67fa65d2..b41e0ebc 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -62,7 +62,7 @@ A dictionary of error codes to error messages.
### `widget`
Used only if rendering the field to HTML.
-This argument sets the widget that should be used to render the field.
+This argument sets the widget that should be used to render the field. For more details, and a list of available widgets, see [the Django documentation on form widgets][django-widgets].
### `label`
@@ -184,7 +184,9 @@ Corresponds to `django.db.models.fields.SlugField`.
## ChoiceField
-A field that can accept a value out of a limited set of choices.
+A field that can accept a value out of a limited set of choices. Optionally takes a `blank_display_value` parameter that customizes the display value of an empty choice.
+
+**Signature:** `ChoiceField(choices=(), blank_display_value=None)`
## EmailField
@@ -345,7 +347,7 @@ As an example, let's create a field that can be used represent the class name of
"""
Serialize the object's class name.
"""
- return obj.__class__
+ return obj.__class__.__name__
# Third party packages
@@ -355,9 +357,21 @@ The following third party packages are also available.
The [drf-compound-fields][drf-compound-fields] package provides "compound" serializer fields, such as lists of simple values, which can be described by other fields rather than serializers with the `many=True` option. Also provided are fields for typed dictionaries and values that can be either a specific type or a list of items of that type.
+## DRF Extra Fields
+
+The [drf-extra-fields][drf-extra-fields] package provides extra serializer fields for REST framework, including `Base64ImageField` and `PointField` classes.
+
+## django-rest-framework-gis
+
+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.
+
+
[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
[ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
[strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior
+[django-widgets]: https://docs.djangoproject.com/en/dev/ref/forms/widgets/
[iso8601]: http://www.w3.org/TR/NOTE-datetime
[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
diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md
index d6c4b1c1..ec5ab61f 100644
--- a/docs/api-guide/filtering.md
+++ b/docs/api-guide/filtering.md
@@ -24,7 +24,7 @@ For example:
from myapp.serializers import PurchaseSerializer
from rest_framework import generics
- class PurchaseList(generics.ListAPIView)
+ class PurchaseList(generics.ListAPIView):
serializer_class = PurchaseSerializer
def get_queryset(self):
@@ -46,7 +46,7 @@ For example if your URL config contained an entry like this:
You could then write a view that returned a purchase queryset filtered by the username portion of the URL:
- class PurchaseList(generics.ListAPIView)
+ class PurchaseList(generics.ListAPIView):
serializer_class = PurchaseSerializer
def get_queryset(self):
@@ -63,7 +63,7 @@ A final example of filtering the initial queryset would be to determine the init
We can override `.get_queryset()` to deal with URLs such as `http://example.com/api/purchases?username=denvercoder9`, and filter the queryset only if the `username` parameter is included in the URL:
- class PurchaseList(generics.ListAPIView)
+ class PurchaseList(generics.ListAPIView):
serializer_class = PurchaseSerializer
def get_queryset(self):
@@ -199,8 +199,7 @@ This enables us to make queries like:
http://example.com/api/products?manufacturer__name=foo
-This is nice, but it shows underlying model structure in REST API, which may
-be undesired, but you can use:
+This is nice, but it exposes the Django's double underscore convention as part of the API. If you instead want to explicitly name the filter argument you can instead explicitly include it on the `FilterSet` class:
import django_filters
from myapp.models import Product
@@ -208,7 +207,6 @@ be undesired, but you can use:
from rest_framework import generics
class ProductFilter(django_filters.FilterSet):
-
manufacturer = django_filters.CharFilter(name="manufacturer__name")
class Meta:
diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md
index fb927ea8..e9efe709 100755
--- a/docs/api-guide/generic-views.md
+++ b/docs/api-guide/generic-views.md
@@ -43,6 +43,12 @@ For more complex cases you might also want to override various methods on the vi
return 20
return 100
+ def list(self, request):
+ # Note the use of `get_queryset()` instead of `self.queryset`
+ queryset = self.get_queryset()
+ 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.
url(r'^/users/', ListCreateAPIView.as_view(model=User), name='user-list')
@@ -63,14 +69,14 @@ Each of the concrete generic views provided is built by combining `GenericAPIVie
The following attributes control the basic view behavior.
-* `queryset` - The queryset that should be used for returning objects from this view. Typically, you must either set this attribute, or override the `get_queryset()` method.
+* `queryset` - The queryset that should be used for returning objects from this view. Typically, you must either set this attribute, or override the `get_queryset()` method. If you are overriding a view method, it is important that you call `get_queryset()` instead of accessing this property directly, as `queryset` will get evaluated once, and those results will be cached for all subsequent requests.
* `serializer_class` - The serializer class that should be used for validating and deserializing input, and for serializing output. Typically, you must either set this attribute, or override the `get_serializer_class()` method.
* `lookup_field` - The model field that should be used to for performing object lookup of individual model instances. Defaults to `'pk'`. Note that when using hyperlinked APIs you'll need to ensure that *both* the API views *and* the serializer classes set the lookup fields if you need to use a custom value.
* `lookup_url_kwarg` - The URL keyword argument that should be used for object lookup. The URL conf should include a keyword argument corresponding to this value. If unset this defaults to using the same value as `lookup_field`.
**Shortcuts**:
-* `model` - This shortcut may be used instead of setting either (or both) of the `queryset`/`serializer_class` attributes, although using the explicit style is generally preferred. If used instead of `serializer_class`, then then `DEFAULT_MODEL_SERIALIZER_CLASS` setting will determine the base serializer class. Note that `model` is only ever used for generating a default queryset or serializer class - the `queryset` and `serializer_class` attributes are always preferred if provided.
+* `model` - This shortcut may be used instead of setting either (or both) of the `queryset`/`serializer_class` attributes, although using the explicit style is generally preferred. If used instead of `serializer_class`, then `DEFAULT_MODEL_SERIALIZER_CLASS` setting will determine the base serializer class. Note that `model` is only ever used for generating a default queryset or serializer class - the `queryset` and `serializer_class` attributes are always preferred if provided.
**Pagination**:
@@ -93,6 +99,8 @@ The following attributes are used to control pagination when used with list view
Returns the queryset that should be used for list views, and that should be used as the base for lookups in detail views. Defaults to returning the queryset specified by the `queryset` attribute, or the default queryset for the model if the `model` shortcut is being 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.
For example:
@@ -187,7 +195,7 @@ Remember that the `pre_save()` method is not called by `GenericAPIView` itself,
You won't typically need to override the following methods, although you might need to call into them if you're writing custom views using `GenericAPIView`.
* `get_serializer_context(self)` - Returns a dictionary containing any extra context that should be supplied to the serializer. Defaults to including `'request'`, `'view'` and `'format'` keys.
-* `get_serializer(self, instance=None, data=None, files=None, many=False, partial=False)` - Returns a serializer instance.
+* `get_serializer(self, instance=None, data=None, files=None, many=False, partial=False, allow_add_remove=False)` - Returns a serializer instance.
* `get_pagination_serializer(self, page)` - Returns a serializer instance to use with paginated data.
* `paginate_queryset(self, queryset)` - Paginate a queryset if required, either returning a page object, or `None` if pagination is not configured for this view.
* `filter_queryset(self, queryset)` - Given a queryset, filter it with whichever filter backends are in use, returning a new queryset.
diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md
index efc4ae7f..e57aed1a 100644
--- a/docs/api-guide/pagination.md
+++ b/docs/api-guide/pagination.md
@@ -103,6 +103,7 @@ You can also set the pagination style on a per-view basis, using the `ListAPIVie
max_paginate_by = 100
Note that using a `paginate_by` value of `None` will turn off pagination for the view.
+Note if you use the `PAGINATE_BY_PARAM` settings, you also have to set the `paginate_by_param` attribute in your view to `None` in order to turn off pagination for those requests that contain the `paginate_by_param` parameter.
For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods.
@@ -157,4 +158,4 @@ The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin`
[cite]: https://docs.djangoproject.com/en/dev/topics/pagination/
[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/
-[paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin \ No newline at end of file
+[paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin
diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md
index 6a0f48f4..38ae3d0a 100644
--- a/docs/api-guide/permissions.md
+++ b/docs/api-guide/permissions.md
@@ -36,6 +36,12 @@ For example:
self.check_object_permissions(self.request, obj)
return obj
+#### Limitations of object level permissions
+
+For performance reasons the generic views will not automatically apply object level permissions to each instance in a queryset when returning a list of objects.
+
+Often when you're using object level permissions you'll also want to [filter the queryset][filtering] appropriately, to ensure that users only have visibility onto instances that they are permitted to view.
+
## Setting the permission policy
The default permission policy may be set globally, using the `DEFAULT_PERMISSION_CLASSES` setting. For example.
@@ -56,7 +62,7 @@ You can also set the authentication policy on a per-view, or per-viewset basis,
using the `APIView` class based views.
from rest_framework.permissions import IsAuthenticated
- from rest_framework.responses import Response
+ from rest_framework.response import Response
from rest_framework.views import APIView
class ExampleView(APIView):
@@ -237,7 +243,8 @@ The [REST Condition][rest-condition] package is another extension for building c
[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
+[filtering]: filtering.md
+[contribauth]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#custom-permissions
[objectpermissions]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#handling-object-permissions
[guardian]: https://github.com/lukaszb/django-guardian
[get_objects_for_user]: http://pythonhosted.org/django-guardian/api/guardian.shortcuts.html#get-objects-for-user
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index 7798827b..7a3429bf 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -138,6 +138,26 @@ Renders the request data into `YAML`.
Requires the `pyyaml` package to be installed.
+Note that non-ascii characters will be rendered using `\uXXXX` character escape. For example:
+
+ unicode black star: "\u2605"
+
+**.media_type**: `application/yaml`
+
+**.format**: `'.yaml'`
+
+**.charset**: `utf-8`
+
+## UnicodeYAMLRenderer
+
+Renders the request data into `YAML`.
+
+Requires the `pyyaml` package to be installed.
+
+Note that non-ascii characters will not be character escaped. For example:
+
+ unicode black star: ★
+
**.media_type**: `application/yaml`
**.format**: `'.yaml'`
diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md
index 7efc140a..64f05af3 100644
--- a/docs/api-guide/routers.md
+++ b/docs/api-guide/routers.md
@@ -179,7 +179,16 @@ The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (an
app.router.register_model(MyModel)
+## DRF-extensions
+
+The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions-routers] for creating [nested viewsets][drf-extensions-nested-viewsets], [collection level controllers][drf-extensions-collection-level-controllers] with [customizable endpoint names][drf-extensions-customizable-endpoint-names].
+
[cite]: http://guides.rubyonrails.org/routing.html
[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers
[wq.db]: http://wq.io/wq.db
[wq.db-router]: http://wq.io/docs/app.py
+[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/
+[drf-extensions-routers]: http://chibisov.github.io/drf-extensions/docs/#routers
+[drf-extensions-nested-viewsets]: http://chibisov.github.io/drf-extensions/docs/#nested-routes
+[drf-extensions-collection-level-controllers]: http://chibisov.github.io/drf-extensions/docs/#collection-level-controllers
+[drf-extensions-customizable-endpoint-names]: http://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name \ No newline at end of file
diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md
index 7ee060af..29b7851b 100644
--- a/docs/api-guide/serializers.md
+++ b/docs/api-guide/serializers.md
@@ -73,8 +73,8 @@ Sometimes when serializing objects, you may not want to represent everything exa
If you need to customize the serialized value of a particular field, you can do this by creating a `transform_<fieldname>` method. For example if you needed to render some markdown from a text field:
- description = serializers.TextField()
- description_html = serializers.TextField(source='description', read_only=True)
+ description = serializers.CharField()
+ description_html = serializers.CharField(source='description', read_only=True)
def transform_description_html(self, obj, value):
from django.contrib.markup.templatetags.markup import markdown
@@ -464,7 +464,7 @@ For more specific requirements such as specifying a different lookup for each fi
model = Account
fields = ('url', 'account_name', 'users', 'created')
-## Overiding the URL field behavior
+## Overriding the URL field behavior
The name of the URL field defaults to 'url'. You can override this globally, by using the `URL_FIELD_NAME` setting.
@@ -478,7 +478,7 @@ You can also override this on a per-serializer basis by using the `url_field_nam
**Note**: The generic view implementations normally generate a `Location` header in response to successful `POST` requests. Serializers using `url_field_name` option will not have this header automatically included by the view. If you need to do so you will ned to also override the view's `get_success_headers()` method.
-You can also overide the URL field's view name and lookup field without overriding the field explicitly, by using the `view_name` and `lookup_field` options, like so:
+You can also override the URL field's view name and lookup field without overriding the field explicitly, by using the `view_name` and `lookup_field` options, like so:
class AccountSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
@@ -580,7 +580,21 @@ The following custom model serializer could be used as a base class for model se
def get_pk_field(self, model_field):
return None
+---
+
+# Third party packages
+
+The following third party packages are also available.
+
+## MongoengineModelSerializer
+
+The [django-rest-framework-mongoengine][mongoengine] package provides a `MongoEngineModelSerializer` serializer class that supports using MongoDB as the storage layer for Django REST framework.
+
+## GeoFeatureModelSerializer
+The [django-rest-framework-gis][django-rest-framework-gis] package provides a `GeoFeatureModelSerializer` serializer class that supports GeoJSON both for read and write operations.
[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
diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md
index b7c320f0..92f4c22b 100644
--- a/docs/api-guide/throttling.md
+++ b/docs/api-guide/throttling.md
@@ -58,7 +58,7 @@ using the `APIView` class based views.
Or, if you're using the `@api_view` decorator with function based views.
- @api_view('GET')
+ @api_view(['GET'])
@throttle_classes([UserRateThrottle])
def example_view(request, format=None):
content = {
diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md
index 23b16575..aa2ceb7f 100644
--- a/docs/api-guide/viewsets.md
+++ b/docs/api-guide/viewsets.md
@@ -137,12 +137,12 @@ The `@action` and `@link` decorators can additionally take extra arguments that
def set_password(self, request, pk=None):
...
-The `@action` decorator will route `POST` requests by default, but may also accept other HTTP methods, by using the `method` argument. For example:
+The `@action` decorator will route `POST` requests by default, but may also accept other HTTP methods, by using the `methods` argument. For example:
@action(methods=['POST', 'DELETE'])
def unset_password(self, request, pk=None):
...
-
+
The two new actions will then be available at the urls `^users/{pk}/set_password/$` and `^users/{pk}/unset_password/$`
diff --git a/docs/css/default.css b/docs/css/default.css
index af6a9cc0..7f3acfed 100644
--- a/docs/css/default.css
+++ b/docs/css/default.css
@@ -307,3 +307,76 @@ table {
.side-nav {
overflow-y: scroll;
}
+
+
+ul.sponsor.diamond li a {
+ float: left;
+ width: 600px;
+ height: 20px;
+ text-align: center;
+ margin: 10px 70px;
+ padding: 300px 0 0 0;
+ background-position: 0 50%;
+ background-size: 600px auto;
+ background-repeat: no-repeat;
+ font-size: 200%;
+}
+
+@media (max-width: 1000px) {
+ ul.sponsor.diamond li a {
+ float: left;
+ width: 300px;
+ height: 20px;
+ text-align: center;
+ margin: 10px 40px;
+ padding: 300px 0 0 0;
+ background-position: 0 50%;
+ background-size: 280px auto;
+ background-repeat: no-repeat;
+ font-size: 150%;
+ }
+}
+
+ul.sponsor.platinum li a {
+ float: left;
+ width: 300px;
+ height: 20px;
+ text-align: center;
+ margin: 10px 40px;
+ padding: 300px 0 0 0;
+ background-position: 0 50%;
+ background-size: 280px auto;
+ background-repeat: no-repeat;
+ font-size: 150%;
+}
+
+ul.sponsor.gold li a {
+ float: left;
+ width: 130px;
+ height: 20px;
+ text-align: center;
+ margin: 10px 30px;
+ padding: 150px 0 0 0;
+ background-position: 0 50%;
+ background-size: 130px auto;
+ background-repeat: no-repeat;
+ font-size: 120%;
+}
+
+ul.sponsor.silver li a {
+ float: left;
+ width: 130px;
+ height: 20px;
+ text-align: center;
+ margin: 10px 30px;
+ padding: 150px 0 0 0;
+ background-position: 0 50%;
+ background-size: 130px auto;
+ background-repeat: no-repeat;
+ font-size: 120%;
+}
+
+ul.sponsor {
+ list-style: none;
+ display: block;
+}
diff --git a/docs/img/sponsors/0-eventbrite.png b/docs/img/sponsors/0-eventbrite.png
new file mode 100644
index 00000000..6c739293
--- /dev/null
+++ b/docs/img/sponsors/0-eventbrite.png
Binary files differ
diff --git a/docs/img/sponsors/1-cyan.png b/docs/img/sponsors/1-cyan.png
new file mode 100644
index 00000000..d6b55b4c
--- /dev/null
+++ b/docs/img/sponsors/1-cyan.png
Binary files differ
diff --git a/docs/img/sponsors/1-divio.png b/docs/img/sponsors/1-divio.png
new file mode 100644
index 00000000..8ced88f8
--- /dev/null
+++ b/docs/img/sponsors/1-divio.png
Binary files differ
diff --git a/docs/img/sponsors/1-kuwaitnet.png b/docs/img/sponsors/1-kuwaitnet.png
new file mode 100644
index 00000000..8b2d0550
--- /dev/null
+++ b/docs/img/sponsors/1-kuwaitnet.png
Binary files differ
diff --git a/docs/img/sponsors/1-lulu.png b/docs/img/sponsors/1-lulu.png
new file mode 100644
index 00000000..8a28bfa9
--- /dev/null
+++ b/docs/img/sponsors/1-lulu.png
Binary files differ
diff --git a/docs/img/sponsors/1-potato.png b/docs/img/sponsors/1-potato.png
new file mode 100644
index 00000000..ad38abdd
--- /dev/null
+++ b/docs/img/sponsors/1-potato.png
Binary files differ
diff --git a/docs/img/sponsors/1-purplebit.png b/docs/img/sponsors/1-purplebit.png
new file mode 100644
index 00000000..0df63bf6
--- /dev/null
+++ b/docs/img/sponsors/1-purplebit.png
Binary files differ
diff --git a/docs/img/sponsors/1-runscope.png b/docs/img/sponsors/1-runscope.png
new file mode 100644
index 00000000..d80a4b85
--- /dev/null
+++ b/docs/img/sponsors/1-runscope.png
Binary files differ
diff --git a/docs/img/sponsors/1-simple-energy.png b/docs/img/sponsors/1-simple-energy.png
new file mode 100644
index 00000000..f59f7374
--- /dev/null
+++ b/docs/img/sponsors/1-simple-energy.png
Binary files differ
diff --git a/docs/img/sponsors/1-vokal_interactive.png b/docs/img/sponsors/1-vokal_interactive.png
new file mode 100644
index 00000000..431482dc
--- /dev/null
+++ b/docs/img/sponsors/1-vokal_interactive.png
Binary files differ
diff --git a/docs/img/sponsors/1-wiredrive.png b/docs/img/sponsors/1-wiredrive.png
new file mode 100644
index 00000000..c9befefe
--- /dev/null
+++ b/docs/img/sponsors/1-wiredrive.png
Binary files differ
diff --git a/docs/img/sponsors/2-byte.png b/docs/img/sponsors/2-byte.png
new file mode 100644
index 00000000..2c3777b5
--- /dev/null
+++ b/docs/img/sponsors/2-byte.png
Binary files differ
diff --git a/docs/img/sponsors/2-compile.png b/docs/img/sponsors/2-compile.png
new file mode 100644
index 00000000..858aa09d
--- /dev/null
+++ b/docs/img/sponsors/2-compile.png
Binary files differ
diff --git a/docs/img/sponsors/2-crate.png b/docs/img/sponsors/2-crate.png
new file mode 100644
index 00000000..6ef6b5da
--- /dev/null
+++ b/docs/img/sponsors/2-crate.png
Binary files differ
diff --git a/docs/img/sponsors/2-cryptico.png b/docs/img/sponsors/2-cryptico.png
new file mode 100644
index 00000000..2d86afe8
--- /dev/null
+++ b/docs/img/sponsors/2-cryptico.png
Binary files differ
diff --git a/docs/img/sponsors/2-django.png b/docs/img/sponsors/2-django.png
new file mode 100644
index 00000000..c89e19cb
--- /dev/null
+++ b/docs/img/sponsors/2-django.png
Binary files differ
diff --git a/docs/img/sponsors/2-galileo_press.png b/docs/img/sponsors/2-galileo_press.png
new file mode 100644
index 00000000..f77e6c0a
--- /dev/null
+++ b/docs/img/sponsors/2-galileo_press.png
Binary files differ
diff --git a/docs/img/sponsors/2-heroku.png b/docs/img/sponsors/2-heroku.png
new file mode 100644
index 00000000..22447659
--- /dev/null
+++ b/docs/img/sponsors/2-heroku.png
Binary files differ
diff --git a/docs/img/sponsors/2-hipflask.png b/docs/img/sponsors/2-hipflask.png
new file mode 100644
index 00000000..c74735c3
--- /dev/null
+++ b/docs/img/sponsors/2-hipflask.png
Binary files differ
diff --git a/docs/img/sponsors/2-hipo.png b/docs/img/sponsors/2-hipo.png
new file mode 100644
index 00000000..2b854c6d
--- /dev/null
+++ b/docs/img/sponsors/2-hipo.png
Binary files differ
diff --git a/docs/img/sponsors/2-koordinates.png b/docs/img/sponsors/2-koordinates.png
new file mode 100644
index 00000000..f38601b3
--- /dev/null
+++ b/docs/img/sponsors/2-koordinates.png
Binary files differ
diff --git a/docs/img/sponsors/2-laterpay.png b/docs/img/sponsors/2-laterpay.png
new file mode 100644
index 00000000..75eb97d3
--- /dev/null
+++ b/docs/img/sponsors/2-laterpay.png
Binary files differ
diff --git a/docs/img/sponsors/2-lightning_kite.png b/docs/img/sponsors/2-lightning_kite.png
new file mode 100644
index 00000000..ffdced04
--- /dev/null
+++ b/docs/img/sponsors/2-lightning_kite.png
Binary files differ
diff --git a/docs/img/sponsors/2-mirus_research.png b/docs/img/sponsors/2-mirus_research.png
new file mode 100644
index 00000000..b1544070
--- /dev/null
+++ b/docs/img/sponsors/2-mirus_research.png
Binary files differ
diff --git a/docs/img/sponsors/2-nexthub.png b/docs/img/sponsors/2-nexthub.png
new file mode 100644
index 00000000..9bf76e0b
--- /dev/null
+++ b/docs/img/sponsors/2-nexthub.png
Binary files differ
diff --git a/docs/img/sponsors/2-opbeat.png b/docs/img/sponsors/2-opbeat.png
new file mode 100644
index 00000000..c71a5241
--- /dev/null
+++ b/docs/img/sponsors/2-opbeat.png
Binary files differ
diff --git a/docs/img/sponsors/2-prorenata.png b/docs/img/sponsors/2-prorenata.png
new file mode 100644
index 00000000..f5e8bb76
--- /dev/null
+++ b/docs/img/sponsors/2-prorenata.png
Binary files differ
diff --git a/docs/img/sponsors/2-rapasso.png b/docs/img/sponsors/2-rapasso.png
new file mode 100644
index 00000000..618e294b
--- /dev/null
+++ b/docs/img/sponsors/2-rapasso.png
Binary files differ
diff --git a/docs/img/sponsors/2-schuberg_philis.png b/docs/img/sponsors/2-schuberg_philis.png
new file mode 100644
index 00000000..fd9282ee
--- /dev/null
+++ b/docs/img/sponsors/2-schuberg_philis.png
Binary files differ
diff --git a/docs/img/sponsors/2-security_compass.png b/docs/img/sponsors/2-security_compass.png
new file mode 100644
index 00000000..abd63dbe
--- /dev/null
+++ b/docs/img/sponsors/2-security_compass.png
Binary files differ
diff --git a/docs/img/sponsors/2-sga.png b/docs/img/sponsors/2-sga.png
new file mode 100644
index 00000000..2b2a3b3b
--- /dev/null
+++ b/docs/img/sponsors/2-sga.png
Binary files differ
diff --git a/docs/img/sponsors/2-sirono.png b/docs/img/sponsors/2-sirono.png
new file mode 100644
index 00000000..0a243001
--- /dev/null
+++ b/docs/img/sponsors/2-sirono.png
Binary files differ
diff --git a/docs/img/sponsors/2-vinta.png b/docs/img/sponsors/2-vinta.png
new file mode 100644
index 00000000..4f4d75bc
--- /dev/null
+++ b/docs/img/sponsors/2-vinta.png
Binary files differ
diff --git a/docs/img/sponsors/3-aba.png b/docs/img/sponsors/3-aba.png
new file mode 100644
index 00000000..cefa3dd6
--- /dev/null
+++ b/docs/img/sponsors/3-aba.png
Binary files differ
diff --git a/docs/img/sponsors/3-aditium.png b/docs/img/sponsors/3-aditium.png
new file mode 100644
index 00000000..0952b08c
--- /dev/null
+++ b/docs/img/sponsors/3-aditium.png
Binary files differ
diff --git a/docs/img/sponsors/3-alwaysdata.png b/docs/img/sponsors/3-alwaysdata.png
new file mode 100644
index 00000000..4095774b
--- /dev/null
+++ b/docs/img/sponsors/3-alwaysdata.png
Binary files differ
diff --git a/docs/img/sponsors/3-ax_semantics.png b/docs/img/sponsors/3-ax_semantics.png
new file mode 100644
index 00000000..c072e028
--- /dev/null
+++ b/docs/img/sponsors/3-ax_semantics.png
Binary files differ
diff --git a/docs/img/sponsors/3-beefarm.png b/docs/img/sponsors/3-beefarm.png
new file mode 100644
index 00000000..3348df42
--- /dev/null
+++ b/docs/img/sponsors/3-beefarm.png
Binary files differ
diff --git a/docs/img/sponsors/3-blimp.png b/docs/img/sponsors/3-blimp.png
new file mode 100644
index 00000000..494bf792
--- /dev/null
+++ b/docs/img/sponsors/3-blimp.png
Binary files differ
diff --git a/docs/img/sponsors/3-brightloop.png b/docs/img/sponsors/3-brightloop.png
new file mode 100644
index 00000000..8d5e85a6
--- /dev/null
+++ b/docs/img/sponsors/3-brightloop.png
Binary files differ
diff --git a/docs/img/sponsors/3-cantemo.gif b/docs/img/sponsors/3-cantemo.gif
new file mode 100644
index 00000000..17b1e8d0
--- /dev/null
+++ b/docs/img/sponsors/3-cantemo.gif
Binary files differ
diff --git a/docs/img/sponsors/3-crosswordtracker.png b/docs/img/sponsors/3-crosswordtracker.png
new file mode 100644
index 00000000..f72362ea
--- /dev/null
+++ b/docs/img/sponsors/3-crosswordtracker.png
Binary files differ
diff --git a/docs/img/sponsors/3-fluxility.png b/docs/img/sponsors/3-fluxility.png
new file mode 100644
index 00000000..eacd7da9
--- /dev/null
+++ b/docs/img/sponsors/3-fluxility.png
Binary files differ
diff --git a/docs/img/sponsors/3-garfo.png b/docs/img/sponsors/3-garfo.png
new file mode 100644
index 00000000..a9bdea0a
--- /dev/null
+++ b/docs/img/sponsors/3-garfo.png
Binary files differ
diff --git a/docs/img/sponsors/3-gizmag.png b/docs/img/sponsors/3-gizmag.png
new file mode 100644
index 00000000..a8d41bd0
--- /dev/null
+++ b/docs/img/sponsors/3-gizmag.png
Binary files differ
diff --git a/docs/img/sponsors/3-holvi.png b/docs/img/sponsors/3-holvi.png
new file mode 100644
index 00000000..255e391e
--- /dev/null
+++ b/docs/img/sponsors/3-holvi.png
Binary files differ
diff --git a/docs/img/sponsors/3-imt_computer_services.png b/docs/img/sponsors/3-imt_computer_services.png
new file mode 100644
index 00000000..00643c97
--- /dev/null
+++ b/docs/img/sponsors/3-imt_computer_services.png
Binary files differ
diff --git a/docs/img/sponsors/3-infinite_code.png b/docs/img/sponsors/3-infinite_code.png
new file mode 100644
index 00000000..7a8fdcf1
--- /dev/null
+++ b/docs/img/sponsors/3-infinite_code.png
Binary files differ
diff --git a/docs/img/sponsors/3-ipushpull.png b/docs/img/sponsors/3-ipushpull.png
new file mode 100644
index 00000000..e70b8bad
--- /dev/null
+++ b/docs/img/sponsors/3-ipushpull.png
Binary files differ
diff --git a/docs/img/sponsors/3-isl.png b/docs/img/sponsors/3-isl.png
new file mode 100644
index 00000000..0bf0cf7c
--- /dev/null
+++ b/docs/img/sponsors/3-isl.png
Binary files differ
diff --git a/docs/img/sponsors/3-life_the_game.png b/docs/img/sponsors/3-life_the_game.png
new file mode 100644
index 00000000..9292685e
--- /dev/null
+++ b/docs/img/sponsors/3-life_the_game.png
Binary files differ
diff --git a/docs/img/sponsors/3-makespace.png b/docs/img/sponsors/3-makespace.png
new file mode 100644
index 00000000..80b79361
--- /dev/null
+++ b/docs/img/sponsors/3-makespace.png
Binary files differ
diff --git a/docs/img/sponsors/3-nephila.png b/docs/img/sponsors/3-nephila.png
new file mode 100644
index 00000000..a905fa93
--- /dev/null
+++ b/docs/img/sponsors/3-nephila.png
Binary files differ
diff --git a/docs/img/sponsors/3-openeye.png b/docs/img/sponsors/3-openeye.png
new file mode 100644
index 00000000..573140ed
--- /dev/null
+++ b/docs/img/sponsors/3-openeye.png
Binary files differ
diff --git a/docs/img/sponsors/3-pathwright.png b/docs/img/sponsors/3-pathwright.png
new file mode 100644
index 00000000..71be3b28
--- /dev/null
+++ b/docs/img/sponsors/3-pathwright.png
Binary files differ
diff --git a/docs/img/sponsors/3-phurba.png b/docs/img/sponsors/3-phurba.png
new file mode 100644
index 00000000..657d872c
--- /dev/null
+++ b/docs/img/sponsors/3-phurba.png
Binary files differ
diff --git a/docs/img/sponsors/3-pkgfarm.png b/docs/img/sponsors/3-pkgfarm.png
new file mode 100644
index 00000000..9224cc2e
--- /dev/null
+++ b/docs/img/sponsors/3-pkgfarm.png
Binary files differ
diff --git a/docs/img/sponsors/3-providenz.png b/docs/img/sponsors/3-providenz.png
new file mode 100644
index 00000000..55d9c992
--- /dev/null
+++ b/docs/img/sponsors/3-providenz.png
Binary files differ
diff --git a/docs/img/sponsors/3-safari.png b/docs/img/sponsors/3-safari.png
new file mode 100644
index 00000000..c03e40e8
--- /dev/null
+++ b/docs/img/sponsors/3-safari.png
Binary files differ
diff --git a/docs/img/sponsors/3-shippo.png b/docs/img/sponsors/3-shippo.png
new file mode 100644
index 00000000..4f5ae133
--- /dev/null
+++ b/docs/img/sponsors/3-shippo.png
Binary files differ
diff --git a/docs/img/sponsors/3-teonite.png b/docs/img/sponsors/3-teonite.png
new file mode 100644
index 00000000..0c098478
--- /dev/null
+++ b/docs/img/sponsors/3-teonite.png
Binary files differ
diff --git a/docs/img/sponsors/3-thermondo-gmbh.png b/docs/img/sponsors/3-thermondo-gmbh.png
new file mode 100644
index 00000000..fe8691c8
--- /dev/null
+++ b/docs/img/sponsors/3-thermondo-gmbh.png
Binary files differ
diff --git a/docs/img/sponsors/3-tivix.png b/docs/img/sponsors/3-tivix.png
new file mode 100644
index 00000000..bc2616a6
--- /dev/null
+++ b/docs/img/sponsors/3-tivix.png
Binary files differ
diff --git a/docs/img/sponsors/3-trackmaven.png b/docs/img/sponsors/3-trackmaven.png
new file mode 100644
index 00000000..3880e370
--- /dev/null
+++ b/docs/img/sponsors/3-trackmaven.png
Binary files differ
diff --git a/docs/img/sponsors/3-transcode.png b/docs/img/sponsors/3-transcode.png
new file mode 100644
index 00000000..1faad69d
--- /dev/null
+++ b/docs/img/sponsors/3-transcode.png
Binary files differ
diff --git a/docs/img/sponsors/3-triggered_messaging.png b/docs/img/sponsors/3-triggered_messaging.png
new file mode 100644
index 00000000..4f8e5063
--- /dev/null
+++ b/docs/img/sponsors/3-triggered_messaging.png
Binary files differ
diff --git a/docs/img/sponsors/3-vzzual.png b/docs/img/sponsors/3-vzzual.png
new file mode 100644
index 00000000..98edce02
--- /dev/null
+++ b/docs/img/sponsors/3-vzzual.png
Binary files differ
diff --git a/docs/img/sponsors/3-wildfish.png b/docs/img/sponsors/3-wildfish.png
new file mode 100644
index 00000000..fa13ea70
--- /dev/null
+++ b/docs/img/sponsors/3-wildfish.png
Binary files differ
diff --git a/docs/index.md b/docs/index.md
index 2a4ad885..dd407497 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -201,6 +201,7 @@ General guides to using REST framework.
* [2.0 Announcement][rest-framework-2-announcement]
* [2.2 Announcement][2.2-announcement]
* [2.3 Announcement][2.3-announcement]
+* [Kickstarter Announcement][kickstarter-announcement]
* [Release Notes][release-notes]
* [Credits][credits]
@@ -325,6 +326,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[rest-framework-2-announcement]: topics/rest-framework-2-announcement.md
[2.2-announcement]: topics/2.2-announcement.md
[2.3-announcement]: topics/2.3-announcement.md
+[kickstarter-announcement]: topics/kickstarter-announcement.md
[release-notes]: topics/release-notes.md
[credits]: topics/credits.md
diff --git a/docs/template.html b/docs/template.html
index a397d067..ac225679 100644
--- a/docs/template.html
+++ b/docs/template.html
@@ -33,6 +33,21 @@
})();
</script>
+ <style>
+span.fusion-wrap a {
+ display: block;
+ margin-top: 10px;
+ color: black;
+}
+
+a.fusion-poweredby {
+ display: block;
+ margin-top: 10px;
+}
+@media (max-width: 767px) {
+ div.promo {display: none;}
+}
+</style>
</head>
<body onload="prettyPrint()" class="{{ page_id }}-page">
@@ -106,6 +121,7 @@
<li><a href="{{ base_url }}/topics/rest-framework-2-announcement{{ suffix }}">2.0 Announcement</a></li>
<li><a href="{{ base_url }}/topics/2.2-announcement{{ suffix }}">2.2 Announcement</a></li>
<li><a href="{{ base_url }}/topics/2.3-announcement{{ suffix }}">2.3 Announcement</a></li>
+ <li><a href="{{ base_url }}/topics/kickstarter-announcement{{ suffix }}">Kickstarter Announcement</a></li>
<li><a href="{{ base_url }}/topics/release-notes{{ suffix }}">Release Notes</a></li>
<li><a href="{{ base_url }}/topics/credits{{ suffix }}">Credits</a></li>
</ul>
@@ -169,11 +185,9 @@
<div id="table-of-contents">
<ul class="nav nav-list side-nav well sidebar-nav-fixed">
{{ toc }}
- <div>
-
-{{ ad_block }}
-
-</div>
+ <div class="promo">
+ {{ ad_block }}
+ </div>
</ul>
</div>
@@ -199,6 +213,7 @@
<script src="{{ base_url }}/js/jquery-1.8.1-min.js"></script>
<script src="{{ base_url }}/js/prettify-1.0.js"></script>
<script src="{{ base_url }}/js/bootstrap-2.1.1-min.js"></script>
+
<script>
//$('.side-nav').scrollspy()
var shiftWindow = function() { scrollBy(0, -50) };
diff --git a/docs/topics/browsable-api.md b/docs/topics/browsable-api.md
index e32db695..ad812f4b 100644
--- a/docs/topics/browsable-api.md
+++ b/docs/topics/browsable-api.md
@@ -69,6 +69,7 @@ For more specific CSS tweaks than simply overriding the default bootstrap theme
All of the blocks available in the browsable API base template that can be used in your `api.html`.
+* `body` - The entire html `<body>`.
* `bodyclass` - Class attribute for the `<body>` tag, empty by default.
* `bootstrap_theme` - CSS for the Bootstrap theme.
* `bootstrap_navbar_variant` - CSS class for the navbar.
@@ -167,10 +168,10 @@ You can now add the `autocomplete_light.ChoiceWidget` widget to the serializer f
[bootstrap]: http://getbootstrap.com
[cerulean]: ../img/cerulean.png
[slate]: ../img/slate.png
-[bcustomize]: http://twitter.github.com/bootstrap/customize.html#variables
+[bcustomize]: http://getbootstrap.com/2.3.2/customize.html
[bswatch]: http://bootswatch.com/
-[bcomponents]: http://twitter.github.com/bootstrap/components.html
-[bcomponentsnav]: http://twitter.github.com/bootstrap/components.html#navbar
+[bcomponents]: http://getbootstrap.com/2.3.2/components.html
+[bcomponentsnav]: http://getbootstrap.com/2.3.2/components.html#navbar
[autocomplete-packages]: https://www.djangopackages.com/grids/g/auto-complete/
[django-autocomplete-light]: https://github.com/yourlabs/django-autocomplete-light
[django-autocomplete-light-install]: http://django-autocomplete-light.readthedocs.org/en/latest/#install
diff --git a/docs/topics/documenting-your-api.md b/docs/topics/documenting-your-api.md
index 6291c924..e20f9712 100644
--- a/docs/topics/documenting-your-api.md
+++ b/docs/topics/documenting-your-api.md
@@ -95,7 +95,7 @@ You can modify the response behavior to `OPTIONS` requests by overriding the `me
To be fully RESTful an API should present its available actions as hypermedia controls in the responses that it sends.
-In this approach, rather than documenting the available API endpoints up front, the description instead concentrates on the *media types* that are used. The available actions take may be taken on any given URL are not strictly fixed, but are instead made available by the presence of link and form controls in the returned document.
+In this approach, rather than documenting the available API endpoints up front, the description instead concentrates on the *media types* that are used. The available actions that may be taken on any given URL are not strictly fixed, but are instead made available by the presence of link and form controls in the returned document.
To implement a hypermedia API you'll need to decide on an appropriate media type for the API, and implement a custom renderer and parser for that media type. The [REST, Hypermedia & HATEOAS][hypermedia-docs] section of the documentation includes pointers to background reading, as well as links to various hypermedia formats.
diff --git a/docs/topics/kickstarter-announcement.md b/docs/topics/kickstarter-announcement.md
new file mode 100644
index 00000000..84dc8511
--- /dev/null
+++ b/docs/topics/kickstarter-announcement.md
@@ -0,0 +1,162 @@
+# Kickstarting Django REST framework 3
+
+---
+
+<iframe width="480" height="360" src="https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3/widget/video.html" frameborder="0" scrolling="no"> </iframe>
+
+---
+
+In order to continue to drive the project forward, I'm launching a Kickstarter campaign to help fund the development of a major new release - Django REST framework 3.
+
+## Project details
+
+This new release will allow us to comprehensively address some of the shortcomings of the framework, and will aim to include the following:
+
+* Faster, simpler and easier-to-use serializers.
+* An alternative admin-style interface for the browsable API.
+* Search and filtering controls made accessible in the browsable API.
+* Alternative API pagination styles.
+* Documentation around API versioning.
+* Triage of outstanding tickets.
+* Improving the ongoing quality and maintainability of the project.
+
+Full details are available now on the [project page](https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3).
+
+If you're interested in helping make sustainable open source development a reality please [visit the Kickstarter page](https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3) and consider funding the project.
+
+I can't wait to see where this takes us!
+
+Many thanks to everyone for your support so far,
+
+ Tom Christie :)
+
+---
+
+## Sponsors
+
+We've now blazed way past all our goals, with a staggering £30,000 (~$50,000), meaning I'll be in a position to work on the project significantly beyond what we'd originally planned for. I owe a huge debt of gratitude to all the wonderful companies and individuals who have been backing the project so generously, and making this possible.
+
+---
+
+### Platinum sponsors
+
+Our platinum sponsors have each made a hugely substantial contribution to the future development of Django REST framework, and I simply can't thank them enough.
+
+<ul class="sponsor diamond">
+<li><a href="https://www.eventbrite.com/" rel="nofollow" style="background-image:url(../img/sponsors/0-eventbrite.png);">Eventbrite</a></li>
+</ul>
+
+<ul class="sponsor platinum">
+<li><a href="https://www.divio.ch/" rel="nofollow" style="background-image:url(../img/sponsors/1-divio.png);">Divio</a></li>
+<li><a href="http://company.onlulu.com/en/" rel="nofollow" style="background-image:url(../img/sponsors/1-lulu.png);">Lulu</a></li>
+<li><a href="https://p.ota.to/" rel="nofollow" style="background-image:url(../img/sponsors/1-potato.png);">Potato</a></li>
+<li><a href="http://www.wiredrive.com/" rel="nofollow" style="background-image:url(../img/sponsors/1-wiredrive.png);">Wiredrive</a></li>
+<li><a href="http://www.cyaninc.com/" rel="nofollow" style="background-image:url(../img/sponsors/1-cyan.png);">Cyan</a></li>
+<li><a href="https://www.runscope.com/" rel="nofollow" style="background-image:url(../img/sponsors/1-runscope.png);">Runscope</a></li>
+<li><a href="http://simpleenergy.com/" rel="nofollow" style="background-image:url(../img/sponsors/1-simple-energy.png);">Simple Energy</a></li>
+<li><a href="http://vokalinteractive.com/" rel="nofollow" style="background-image:url(../img/sponsors/1-vokal_interactive.png);">VOKAL Interactive</a></li>
+<li><a href="http://www.purplebit.com/" rel="nofollow" style="background-image:url(../img/sponsors/1-purplebit.png);">Purple Bit</a></li>
+<li><a href="http://www.kuwaitnet.net/" rel="nofollow" style="background-image:url(../img/sponsors/1-kuwaitnet.png);">KuwaitNET</a></li>
+</ul>
+
+<div style="clear: both"></div>
+
+---
+
+### Gold sponsors
+
+Our gold sponsors include companies large and small. Many thanks for their significant funding of the project and their commitment to sustainable open-source development.
+
+<ul class="sponsor gold">
+<li><a href="https://laterpay.net/" rel="nofollow" style="background-image:url(../img/sponsors/2-laterpay.png);">LaterPay</a></li>
+<li><a href="https://www.schubergphilis.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-schuberg_philis.png);">Schuberg Philis</a></li>
+<li><a href="http://prorenata.se/" rel="nofollow" style="background-image:url(../img/sponsors/2-prorenata.png);">ProReNata AB</a></li>
+<li><a href="https://www.sgawebsites.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-sga.png);">SGA Websites</a></li>
+<li><a href="http://www.sirono.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-sirono.png);">Sirono</a></li>
+<li><a href="http://www.vinta.com.br/" rel="nofollow" style="background-image:url(../img/sponsors/2-vinta.png);">Vinta Software Studio</a></li>
+<li><a href="http://www.rapasso.nl/index.php/en" rel="nofollow" style="background-image:url(../img/sponsors/2-rapasso.png);">Rapasso</a></li>
+<li><a href="https://mirusresearch.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-mirus_research.png);">Mirus Research</a></li>
+<li><a href="http://hipolabs.com" rel="nofollow" style="background-image:url(../img/sponsors/2-hipo.png);">Hipo</a></li>
+<li><a href="http://www.byte.nl" rel="nofollow" style="background-image:url(../img/sponsors/2-byte.png);">Byte</a></li>
+<li><a href="http://lightningkite.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-lightning_kite.png);">Lightning Kite</a></li>
+<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="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>
+<li><a href="https://www.djangoproject.com/foundation/" rel="nofollow" style="background-image:url(../img/sponsors/2-django.png);">Django Software Foundation</a></li>
+<li><a href="http://www.hipflaskapp.com" rel="nofollow" style="background-image:url(../img/sponsors/2-hipflask.png);">Hipflask</a></li>
+<li><a href="http://www.crate.io/" rel="nofollow" style="background-image:url(../img/sponsors/2-crate.png);">Crate</a></li>
+<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://envisionlinux.org/blog" rel="nofollow">Envision Linux</a></li>
+</ul>
+
+<div style="clear: both; padding-bottom: 40px;"></div>
+
+**Individual backers**: Xitij Ritesh Patel, Howard Sandford, Simon Haugk.
+
+---
+
+### Silver sponsors
+
+The serious financial contribution that our silver sponsors have made is very much appreciated. I'd like to say a particular thank&nbsp;you to individuals who have choosen to privately support the project at this level.
+
+<ul class="sponsor silver">
+<li><a href="http://www.imtapps.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-imt_computer_services.png);">IMT Computer Services</a></li>
+<li><a href="http://wildfish.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-wildfish.png);">Wildfish</a></li>
+<li><a href="http://www.thermondo.de/" rel="nofollow" style="background-image:url(../img/sponsors/3-thermondo-gmbh.png);">Thermondo GmbH</a></li>
+<li><a href="http://providenz.fr/" rel="nofollow" style="background-image:url(../img/sponsors/3-providenz.png);">Providenz</a></li>
+<li><a href="https://www.alwaysdata.com" rel="nofollow" style="background-image:url(../img/sponsors/3-alwaysdata.png);">alwaysdata.com</a></li>
+<li><a href="http://www.triggeredmessaging.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-triggered_messaging.png);">Triggered Messaging</a></li>
+<li><a href="https://www.ipushpull.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-ipushpull.png);">PushPull Technology Ltd</a></li>
+<li><a href="http://www.transcode.de/" rel="nofollow" style="background-image:url(../img/sponsors/3-transcode.png);">Transcode</a></li>
+<li><a href="https://garfo.io/" rel="nofollow" style="background-image:url(../img/sponsors/3-garfo.png);">Garfo</a></li>
+<li><a href="https://goshippo.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-shippo.png);">Shippo</a></li>
+<li><a href="http://www.gizmag.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-gizmag.png);">Gizmag</a></li>
+<li><a href="http://www.tivix.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-tivix.png);">Tivix</a></li>
+<li><a href="http://www.safaribooksonline.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-safari.png);">Safari</a></li>
+<li><a href="http://brightloop.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-brightloop.png);">Bright Loop</a></li>
+<li><a href="http://www.aba-systems.com.au/" rel="nofollow" style="background-image:url(../img/sponsors/3-aba.png);">ABA Systems</a></li>
+<li><a href="http://beefarm.ru/" rel="nofollow" style="background-image:url(../img/sponsors/3-beefarm.png);">beefarm.ru</a></li>
+<li><a href="http://www.vzzual.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-vzzual.png);">Vzzual.com</a></li>
+<li><a href="http://infinite-code.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-infinite_code.png);">Infinite Code</a></li>
+<li><a href="http://crosswordtracker.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-crosswordtracker.png);">Crossword Tracker</a></li>
+<li><a href="https://www.pkgfarm.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-pkgfarm.png);">PkgFarm</a></li>
+<li><a href="http://life.tl/" rel="nofollow" style="background-image:url(../img/sponsors/3-life_the_game.png);">Life. The Game.</a></li>
+<li><a href="http://blimp.io/" rel="nofollow" style="background-image:url(../img/sponsors/3-blimp.png);">Blimp</a></li>
+<li><a href="http://pathwright.com" rel="nofollow" style="background-image:url(../img/sponsors/3-pathwright.png);">Pathwright</a></li>
+<li><a href="http://fluxility.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-fluxility.png);">Fluxility</a></li>
+<li><a href="http://teonite.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-teonite.png);">Teonite</a></li>
+<li><a href="http://trackmaven.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-trackmaven.png);">TrackMaven</a></li>
+<li><a href="http://www.phurba.net/" rel="nofollow" style="background-image:url(../img/sponsors/3-phurba.png);">Phurba</a></li>
+<li><a href="http://www.nephila.co.uk/" rel="nofollow" style="background-image:url(../img/sponsors/3-nephila.png);">Nephila</a></li>
+<li><a href="http://www.aditium.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-aditium.png);">Aditium</a></li>
+<li><a href="http://www.eyesopen.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-openeye.png);">OpenEye Scientific Software</a></li>
+<li><a href="https://holvi.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-holvi.png);">Holvi</a></li>
+<li><a href="http://cantemo.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-cantemo.gif);">Cantemo</a></li>
+<li><a href="https://www.makespace.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-makespace.png);">MakeSpace</a></li>
+<li><a href="https://www.ax-semantics.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-ax_semantics.png);">AX Semantics</a></li>
+<li><a href="http://istrategylabs.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-isl.png);">ISL</a></li>
+</ul>
+
+<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.
+
+---
+
+### Advocates
+
+The following individuals made a significant financial contribution to the development of Django REST framework 3, for which I can only offer a huge, warm and sincere thank you!
+
+**Individual backers**: Jure Cuhalev, Kevin Brolly, Ferenc Szalai, Dougal Matthews, Stefan Foulis, Carlos Hernando, Alen Mujezinovic, Ross Crawford-d'Heureuse, George Kappel, Alasdair Nicol, John Carr, Steve Winton, Trey, Manuel Miranda, David Horn, Vince Mi, Daniel Sears, Jamie Matthews, Ryan Currah, Marty Kemka, Scott Nixon, Moshin Elahi, Kevin Campbell, Jose Antonio Leiva Izquierdo, Kevin Stone, Andrew Godwin, Tijs Teulings, Roger Boardman, Xavier Antoviaque, Darian Moody, Lujeni, Jon Dugan, Wiley Kestner, Daniel C. Silverstein, Daniel Hahler, Subodh Nijsure, Philipp Weidenhiller, Yusuke Muraoka, Danny Roa, Reto Aebersold, Kyle Getrost, Décébal Hormuz, James Dacosta, Matt Long, Mauro Rocco, Tyrel Souza, Ryan Campbell, Ville Jyrkkä, Charalampos Papaloizou, Nikolai Røed Kristiansen, Antoni Aloy López, Celia Oakley, Michał Krawczak, Ivan VenOsdel, Tim Watts, Martin Warne, Nicola Jordan, Ryan Kaskel.
+
+**Corporate backers**: Savannah Informatics, Prism Skylabs, Musical Operating Devices.
+
+---
+
+### Supporters
+
+There were also almost 300 further individuals choosing to help fund the project at other levels or choosing to give anonymously. Again, thank you, thank you, thank you! \ No newline at end of file
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index 0010f687..ea4c912c 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -40,6 +40,29 @@ You can determine your currently installed version using `pip freeze`:
## 2.3.x series
+### 2.3.14
+
+**Date**: 12th June 2014
+
+* **Security fix**: Escape request path when it is include as part of the login and logout links in the browsable API.
+* `help_text` and `verbose_name` automatically set for related fields on `ModelSerializer`.
+* Fix nested serializers linked through a backward foreign key relation.
+* Fix bad links for the `BrowsableAPIRenderer` with `YAMLRenderer`.
+* Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode.
+* Fix `parse_header` argument convertion.
+* Fix mediatype detection under Python 3.
+* Web browseable API now offers blank option on dropdown when the field is not required.
+* `APIException` representation improved for logging purposes.
+* Allow source="*" within nested serializers.
+* Better support for custom oauth2 provider backends.
+* Fix field validation if it's optional and has no value.
+* Add `SEARCH_PARAM` and `ORDERING_PARAM`.
+* Fix `APIRequestFactory` to support arguments within the url string for GET.
+* Allow three transport modes for access tokens when accessing a protected resource.
+* Fix `QueryDict` encoding on request objects.
+* Ensure throttle keys do not contain spaces, as those are invalid if using `memcached`.
+* Support `blank_display_value` on `ChoiceField`.
+
### 2.3.13
**Date**: 6th March 2014
@@ -112,11 +135,11 @@ You can determine your currently installed version using `pip freeze`:
* Bugfix: `client.force_authenticate(None)` should also clear session info if it exists.
* Bugfix: Client sending empty string instead of file now clears `FileField`.
* Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`.
-* Bugfix: Clients setting `page=0` now simply returns the default page size, instead of disabling pagination. [*]
+* Bugfix: Clients setting `page_size=0` now simply returns the default page size, instead of disabling pagination. [*]
---
-[*] Note that the change in `page=0` behaviour fixes what is considered to be a bug in how clients can effect the pagination size. However if you were relying on this behavior you will need to add the following mixin to your list views in order to preserve the existing behavior.
+[*] Note that the change in `page_size=0` behaviour fixes what is considered to be a bug in how clients can effect the pagination size. However if you were relying on this behavior you will need to add the following mixin to your list views in order to preserve the existing behavior.
class DisablePaginationMixin(object):
def get_paginate_by(self, queryset=None):
diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md
index 979c4a3e..96214f5b 100644
--- a/docs/tutorial/1-serialization.md
+++ b/docs/tutorial/1-serialization.md
@@ -81,8 +81,8 @@ For the purposes of this tutorial we're going to start by creating a simple `Sni
LEXERS = [item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted((item, item) for item in get_all_styles())
-
-
+
+
class Snippet(models.Model):
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=100, blank=True, default='')
@@ -94,7 +94,7 @@ For the purposes of this tutorial we're going to start by creating a simple `Sni
style = models.CharField(choices=STYLE_CHOICES,
default='friendly',
max_length=100)
-
+
class Meta:
ordering = ('created',)
@@ -104,7 +104,7 @@ Don't forget to sync the database for the first time.
## Creating a Serializer class
-The first thing we need to get started on our Web API is provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similar to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following.
+The first thing we need to get started on our Web API is to provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similar to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following.
from django.forms import widgets
from rest_framework import serializers
@@ -122,12 +122,12 @@ The first thing we need to get started on our Web API is provide a way of serial
default='python')
style = serializers.ChoiceField(choices=STYLE_CHOICES,
default='friendly')
-
+
def restore_object(self, attrs, instance=None):
"""
Create or update a new snippet instance, given a dictionary
of deserialized field values.
-
+
Note that if we don't define this method, then deserializing
data will simply return a dictionary of items.
"""
@@ -143,7 +143,7 @@ The first thing we need to get started on our Web API is provide a way of serial
# Create new instance
return Snippet(**attrs)
-The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
+The first part of the serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
Notice that we can also use various attributes that would typically be used on form fields, such as `widget=widgets.Textarea`. These can be used to control how the serializer should render when displayed as an HTML form. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial.
@@ -180,7 +180,7 @@ At this point we've translated the model instance into Python native datatypes.
content
# '{"pk": 2, "title": "", "code": "print \\"hello, world\\"\\n", "linenos": false, "language": "python", "style": "friendly"}'
-Deserialization is similar. First we parse a stream into Python native datatypes...
+Deserialization is similar. First we parse a stream into Python native datatypes...
# This import will use either `StringIO.StringIO` or `io.BytesIO`
# as appropriate, depending on if we're running Python 2 or Python 3.
@@ -196,7 +196,7 @@ Deserialization is similar. First we parse a stream into Python native datatype
# True
serializer.object
# <Snippet: Snippet object>
-
+
Notice how similar the API is to working with forms. The similarity should become even more apparent when we start writing views that use our serializer.
We can also serialize querysets instead of model instances. To do so we simply add a `many=True` flag to the serializer arguments.
@@ -264,7 +264,7 @@ The root of our API is going to be a view that supports listing all the existing
return JSONResponse(serializer.data, status=201)
return JSONResponse(serializer.errors, status=400)
-Note that because we want to be able to POST to this view from clients that won't have a CSRF token we need to mark the view as `csrf_exempt`. This isn't something that you'd normally want to do, and REST framework views actually use more sensible behavior than this, but it'll do for our purposes right now.
+Note that because we want to be able to POST to this view from clients that won't have a CSRF token we need to mark the view as `csrf_exempt`. This isn't something that you'd normally want to do, and REST framework views actually use more sensible behavior than this, but it'll do for our purposes right now.
We'll also need a view which corresponds to an individual snippet, and can be used to retrieve, update or delete the snippet.
@@ -277,11 +277,11 @@ We'll also need a view which corresponds to an individual snippet, and can be us
snippet = Snippet.objects.get(pk=pk)
except Snippet.DoesNotExist:
return HttpResponse(status=404)
-
+
if request.method == 'GET':
serializer = SnippetSerializer(snippet)
return JSONResponse(serializer.data)
-
+
elif request.method == 'PUT':
data = JSONParser().parse(request)
serializer = SnippetSerializer(snippet, data=data)
diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md
index 603edd08..e70bbbfc 100644
--- a/docs/tutorial/2-requests-and-responses.md
+++ b/docs/tutorial/2-requests-and-responses.md
@@ -33,7 +33,7 @@ The wrappers also provide behaviour such as returning `405 Method Not Allowed` r
## Pulling it all together
-Okay, let's go ahead and start using these new components to write a few views.
+Okay, let's go ahead and start using these new components to write a few views.
We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and delete that. Once that's done we can start refactoring our views slightly.
@@ -69,7 +69,7 @@ Here is the view for an individual snippet, in the `views.py` module.
def snippet_detail(request, pk):
"""
Retrieve, update or delete a snippet instance.
- """
+ """
try:
snippet = Snippet.objects.get(pk=pk)
except Snippet.DoesNotExist:
@@ -115,7 +115,7 @@ Now update the `urls.py` file slightly, to append a set of `format_suffix_patter
url(r'^snippets/$', 'snippet_list'),
url(r'^snippets/(?P<pk>[0-9]+)$', 'snippet_detail'),
)
-
+
urlpatterns = format_suffix_patterns(urlpatterns)
We don't necessarily need to add these extra url patterns in, but it gives us a simple, clean way of referring to a specific format.
@@ -146,7 +146,7 @@ Similarly, we can control the format of the request that we send, using the `Con
curl -X POST http://127.0.0.1:8000/snippets/ -d "code=print 123"
{"id": 3, "title": "", "code": "print 123", "linenos": false, "language": "python", "style": "friendly"}
-
+
# POST using JSON
curl -X POST http://127.0.0.1:8000/snippets/ -d '{"code": "print 456"}' -H "Content-Type: application/json"
diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md
index b37bc31b..e04072ca 100644
--- a/docs/tutorial/3-class-based-views.md
+++ b/docs/tutorial/3-class-based-views.md
@@ -30,7 +30,7 @@ We'll start by rewriting the root view as a class based view. All this involves
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
-So far, so good. It looks pretty similar to the previous case, but we've got better separation between the different HTTP methods. We'll also need to update the instance view in `views.py`.
+So far, so good. It looks pretty similar to the previous case, but we've got better separation between the different HTTP methods. We'll also need to update the instance view in `views.py`.
class SnippetDetail(APIView):
"""
@@ -72,7 +72,7 @@ We'll also need to refactor our `urls.py` slightly now we're using class based v
url(r'^snippets/$', views.SnippetList.as_view()),
url(r'^snippets/(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view()),
)
-
+
urlpatterns = format_suffix_patterns(urlpatterns)
Okay, we're done. If you run the development server everything should be working just as before.
diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md
index 432371f3..74ad9a55 100644
--- a/docs/tutorial/4-authentication-and-permissions.md
+++ b/docs/tutorial/4-authentication-and-permissions.md
@@ -44,11 +44,11 @@ When that's all done we'll need to update our database tables.
Normally we'd create a database migration in order to do that, but for the purposes of this tutorial, let's just delete the database and start again.
rm tmp.db
- python ./manage.py syncdb
+ python manage.py syncdb
You might also want to create a few different users, to use for testing the API. The quickest way to do this will be with the `createsuperuser` command.
- python ./manage.py createsuperuser
+ python manage.py createsuperuser
## Adding endpoints for our User models
@@ -73,12 +73,12 @@ We'll also add a couple of views to `views.py`. We'd like to just use read-only
class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
-
-
+
+
class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
-
+
Make sure to also import the `UserSerializer` class
from snippets.serializers import UserSerializer
@@ -129,7 +129,7 @@ Then, add the following property to **both** the `SnippetList` and `SnippetDetai
If you open a browser and navigate to the browsable API at the moment, you'll find that you're no longer able to create new code snippets. In order to do so we'd need to be able to login as a user.
-We can add a login view for use with the browsable API, by editing the URLconf in our project-level urls.py file.
+We can add a login view for use with the browsable API, by editing the URLconf in our project-level `urls.py` file.
Add the following import at the top of the file:
@@ -157,8 +157,8 @@ To do that we're going to need to create a custom permission.
In the snippets app, create a new file, `permissions.py`
from rest_framework import permissions
-
-
+
+
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
@@ -201,7 +201,7 @@ If we try to create a snippet without authenticating, we'll get an error:
We can make a successful request by including the username and password of one of the users we created earlier.
curl -X POST http://127.0.0.1:8000/snippets/ -d "code=print 789" -u tom:password
-
+
{"id": 5, "owner": "tom", "title": "foo", "code": "print 789", "linenos": false, "language": "python", "style": "friendly"}
## Summary
diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md
index 2cf44bf9..9c61fe3d 100644
--- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md
+++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md
@@ -1,10 +1,10 @@
# Tutorial 5: Relationships & Hyperlinked APIs
-At the moment relationships within our API are represented by using primary keys. In this part of the tutorial we'll improve the cohesion and discoverability of our API, by instead using hyperlinking for relationships.
+At the moment relationships within our API are represented by using primary keys. In this part of the tutorial we'll improve the cohesion and discoverability of our API, by instead using hyperlinking for relationships.
## Creating an endpoint for the root of our API
-Right now we have endpoints for 'snippets' and 'users', but we don't have a single entry point to our API. To create one, we'll use a regular function-based view and the `@api_view` decorator we introduced earlier.
+Right now we have endpoints for 'snippets' and 'users', but we don't have a single entry point to our API. To create one, we'll use a regular function-based view and the `@api_view` decorator we introduced earlier. In your `snippets/views.py` add:
from rest_framework import renderers
from rest_framework.decorators import api_view
@@ -29,7 +29,7 @@ Unlike all our other API endpoints, we don't want to use JSON, but instead just
The other thing we need to consider when creating the code highlight view is that there's no existing concrete generic view that we can use. We're not returning an object instance, but instead a property of an object instance.
-Instead of using a concrete generic view, we'll use the base class for representing instances, and create our own `.get()` method. In your snippets.views add:
+Instead of using a concrete generic view, we'll use the base class for representing instances, and create our own `.get()` method. In your `snippets/views.py` add:
from rest_framework import renderers
from rest_framework.response import Response
@@ -37,13 +37,13 @@ Instead of using a concrete generic view, we'll use the base class for represent
class SnippetHighlight(generics.GenericAPIView):
queryset = Snippet.objects.all()
renderer_classes = (renderers.StaticHTMLRenderer,)
-
+
def get(self, request, *args, **kwargs):
snippet = self.get_object()
return Response(snippet.highlighted)
As usual we need to add the new views that we've created in to our URLconf.
-We'll add a url pattern for our new API root:
+We'll add a url pattern for our new API root in `snippets/urls.py`:
url(r'^$', 'api_root'),
@@ -73,21 +73,21 @@ The `HyperlinkedModelSerializer` has the following differences from `ModelSerial
* Relationships use `HyperlinkedRelatedField`,
instead of `PrimaryKeyRelatedField`.
-We can easily re-write our existing serializers to use hyperlinking.
+We can easily re-write our existing serializers to use hyperlinking. In your `snippets/serializers.py` add:
class SnippetSerializer(serializers.HyperlinkedModelSerializer):
owner = serializers.Field(source='owner.username')
highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html')
-
+
class Meta:
model = Snippet
fields = ('url', 'highlight', 'owner',
'title', 'code', 'linenos', 'language', 'style')
-
-
+
+
class UserSerializer(serializers.HyperlinkedModelSerializer):
snippets = serializers.HyperlinkedRelatedField(many=True, view_name='snippet-detail')
-
+
class Meta:
model = User
fields = ('url', 'username', 'snippets')
@@ -105,7 +105,7 @@ If we're going to have a hyperlinked API, we need to make sure we name our URL p
* Our user serializer includes a field that refers to `'snippet-detail'`.
* Our snippet and user serializers include `'url'` fields that by default will refer to `'{model_name}-detail'`, which in this case will be `'snippet-detail'` and `'user-detail'`.
-After adding all those names into our URLconf, our final `'urls.py'` file should look something like this:
+After adding all those names into our URLconf, our final `snippets/urls.py` file should look something like this:
# API endpoints
urlpatterns = format_suffix_patterns(patterns('snippets.views',
@@ -126,9 +126,9 @@ After adding all those names into our URLconf, our final `'urls.py'` file should
views.UserDetail.as_view(),
name='user-detail')
))
-
+
# Login and logout views for the browsable API
- urlpatterns += patterns('',
+ urlpatterns += patterns('',
url(r'^api-auth/', include('rest_framework.urls',
namespace='rest_framework')),
)
diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md
index 870632f1..04b42f2e 100644
--- a/docs/tutorial/6-viewsets-and-routers.md
+++ b/docs/tutorial/6-viewsets-and-routers.md
@@ -21,7 +21,7 @@ First of all let's refactor our `UserList` and `UserDetail` views into a single
queryset = User.objects.all()
serializer_class = UserSerializer
-Here we've used `ReadOnlyModelViewSet` class to automatically provide the default 'read-only' operations. We're still setting the `queryset` and `serializer_class` attributes exactly as we did when we were using regular views, but we no longer need to provide the same information to two separate classes.
+Here we've used the `ReadOnlyModelViewSet` class to automatically provide the default 'read-only' operations. We're still setting the `queryset` and `serializer_class` attributes exactly as we did when we were using regular views, but we no longer need to provide the same information to two separate classes.
Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighlight` view classes. We can remove the three views, and again replace them with a single class.
@@ -85,7 +85,7 @@ In the `urls.py` file we bind our `ViewSet` classes into a set of concrete views
Notice how we're creating multiple views from each `ViewSet` class, by binding the http methods to the required action for each view.
-Now that we've bound our resources into concrete views, that we can register the views with the URL conf as usual.
+Now that we've bound our resources into concrete views, we can register the views with the URL conf as usual.
urlpatterns = format_suffix_patterns(patterns('snippets.views',
url(r'^$', 'api_root'),
@@ -138,7 +138,7 @@ You can review the final [tutorial code][repo] on GitHub, or try out a live exam
## Onwards and upwards
-We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start:
+We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here are a few places you can start:
* Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests.
* Join the [REST framework discussion group][group], and help build the community.
diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md
index 8bf8c7f5..98e5f439 100644
--- a/docs/tutorial/quickstart.md
+++ b/docs/tutorial/quickstart.md
@@ -6,8 +6,8 @@ We're going to create a simple API to allow admin users to view and edit the use
Create a new Django project named `tutorial`, then start a new app called `quickstart`.
- # Set up a new project
- django-admin.py startproject tutorial
+ # Create the project directory
+ mkdir tutorial
cd tutorial
# Create a virtualenv to isolate our package dependencies locally
@@ -18,6 +18,9 @@ Create a new Django project named `tutorial`, then start a new app called `quick
pip install django
pip install djangorestframework
+ # Set up a new project
+ django-admin.py startproject tutorial
+
# Create a new app
python manage.py startapp quickstart
@@ -46,14 +49,14 @@ First up we're going to define some serializers in `quickstart/serializers.py` t
from django.contrib.auth.models import User, Group
from rest_framework import serializers
-
-
+
+
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('url', 'username', 'email', 'groups')
-
-
+
+
class GroupSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Group
@@ -68,16 +71,16 @@ Right, we'd better write some views then. Open `quickstart/views.py` and get ty
from django.contrib.auth.models import User, Group
from rest_framework import viewsets
from quickstart.serializers import UserSerializer, GroupSerializer
-
-
+
+
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
queryset = User.objects.all()
serializer_class = UserSerializer
-
-
+
+
class GroupViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows groups to be viewed or edited.
@@ -144,22 +147,22 @@ We're now ready to test the API we've built. Let's fire up the server from the
We can now access our API, both from the command-line, using tools like `curl`...
- bash: curl -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/
+ bash: curl -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/
{
- "count": 2,
- "next": null,
- "previous": null,
+ "count": 2,
+ "next": null,
+ "previous": null,
"results": [
{
- "email": "admin@example.com",
- "groups": [],
- "url": "http://127.0.0.1:8000/users/1/",
+ "email": "admin@example.com",
+ "groups": [],
+ "url": "http://127.0.0.1:8000/users/1/",
"username": "admin"
- },
+ },
{
- "email": "tom@example.com",
- "groups": [ ],
- "url": "http://127.0.0.1:8000/users/2/",
+ "email": "tom@example.com",
+ "groups": [ ],
+ "url": "http://127.0.0.1:8000/users/2/",
"username": "tom"
}
]
diff --git a/mkdocs.py b/mkdocs.py
index f973096f..adeb6053 100755
--- a/mkdocs.py
+++ b/mkdocs.py
@@ -142,7 +142,7 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
toc += template + '\n'
if filename == 'index.md':
- main_title = 'Django REST framework - APIs made easy'
+ main_title = 'Django REST framework - Web APIs for Django'
else:
main_title = main_title + ' - Django REST framework'
@@ -162,8 +162,8 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir):
output = output.replace('{{ canonical_url }}', canonical_url)
if filename =='index.md':
- output = output.replace('{{ ad_block }}', """<hr><p><strong>The team behind REST framework is launching a new API service.</strong></p>
-<p>If you want to be first in line when we start issuing invitations, please <a href="http://brightapi.com">sign up here</a>.</p>""")
+ output = output.replace('{{ ad_block }}', """<hr/>
+ <script type="text/javascript" src="//cdn.fusionads.net/fusion.js?zoneid=1332&serve=C6SDP2Y&placement=djangorestframework" id="_fusionads_js"></script>""")
else:
output = output.replace('{{ ad_block }}', '')
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 2d76b55d..01036cef 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -8,7 +8,7 @@ ______ _____ _____ _____ __ _
"""
__title__ = 'Django REST framework'
-__version__ = '2.3.13'
+__version__ = '2.3.14'
__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 da9ca510..887ef5d7 100644
--- a/rest_framework/authentication.py
+++ b/rest_framework/authentication.py
@@ -310,6 +310,13 @@ class OAuth2Authentication(BaseAuthentication):
auth = get_authorization_header(request).split()
+ if len(auth) == 1:
+ msg = 'Invalid bearer header. No credentials provided.'
+ raise exceptions.AuthenticationFailed(msg)
+ elif len(auth) > 2:
+ msg = 'Invalid bearer header. Token string should not contain spaces.'
+ raise exceptions.AuthenticationFailed(msg)
+
if auth and auth[0].lower() == b'bearer':
access_token = auth[1]
elif 'access_token' in request.POST:
@@ -319,13 +326,6 @@ class OAuth2Authentication(BaseAuthentication):
else:
return None
- if len(auth) == 1:
- msg = 'Invalid bearer header. No credentials provided.'
- raise exceptions.AuthenticationFailed(msg)
- elif len(auth) > 2:
- msg = 'Invalid bearer header. Token string should not contain spaces.'
- raise exceptions.AuthenticationFailed(msg)
-
return self.authenticate_credentials(request, access_token)
def authenticate_credentials(self, request, access_token):
diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py
index 8eac2cc4..167fa531 100644
--- a/rest_framework/authtoken/models.py
+++ b/rest_framework/authtoken/models.py
@@ -34,7 +34,7 @@ class Token(models.Model):
return super(Token, self).save(*args, **kwargs)
def generate_key(self):
- return binascii.hexlify(os.urandom(20))
+ return binascii.hexlify(os.urandom(20)).decode()
def __unicode__(self):
return self.key
diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py
index 60a3740e..99e99ae3 100644
--- a/rest_framework/authtoken/serializers.py
+++ b/rest_framework/authtoken/serializers.py
@@ -1,4 +1,6 @@
from django.contrib.auth import authenticate
+from django.utils.translation import ugettext_lazy as _
+
from rest_framework import serializers
@@ -15,10 +17,13 @@ class AuthTokenSerializer(serializers.Serializer):
if user:
if not user.is_active:
- raise serializers.ValidationError('User account is disabled.')
+ msg = _('User account is disabled.')
+ raise serializers.ValidationError(msg)
attrs['user'] = user
return attrs
else:
- raise serializers.ValidationError('Unable to login with provided credentials.')
+ msg = _('Unable to login with provided credentials.')
+ raise serializers.ValidationError(msg)
else:
- raise serializers.ValidationError('Must include "username" and "password"')
+ msg = _('Must include "username" and "password"')
+ raise serializers.ValidationError(msg)
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index d155f554..9ad8b0d2 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -48,11 +48,15 @@ try:
except ImportError:
django_filters = None
-# guardian is optional
-try:
- import guardian
-except ImportError:
- guardian = None
+# Django-guardian is optional. Import only if guardian is in INSTALLED_APPS
+# Fixes (#1712). We keep the try/except for the test suite.
+guardian = None
+if 'guardian' in settings.INSTALLED_APPS:
+ try:
+ import guardian
+ import guardian.shortcuts # Fixes #1624
+ except ImportError:
+ pass
# cStringIO only if it's available, otherwise StringIO
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 68b95682..6caae924 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -62,7 +62,7 @@ def get_component(obj, attr_name):
def readable_datetime_formats(formats):
format = ', '.join(formats).replace(ISO_8601,
- 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]')
+ 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]')
return humanize_strptime(format)
@@ -154,7 +154,12 @@ class Field(object):
def widget_html(self):
if not self.widget:
return ''
- return self.widget.render(self._name, self._value)
+
+ attrs = {}
+ if 'id' not in self.widget.attrs:
+ attrs['id'] = self._name
+
+ return self.widget.render(self._name, self._value, attrs=attrs)
def label_tag(self):
return '<label for="%s">%s:</label>' % (self._name, self.label)
@@ -164,7 +169,7 @@ class Field(object):
Called to set up a field prior to field_to_native or field_from_native.
parent - The parent serializer.
- model_field - The model field this field corresponds to, if one exists.
+ field_name - The name of the field being initialized.
"""
self.parent = parent
self.root = parent.root or parent
@@ -182,7 +187,7 @@ class Field(object):
def field_to_native(self, obj, field_name):
"""
- Given and object and a field name, returns the value that should be
+ Given an object and a field name, returns the value that should be
serialized for that field.
"""
if obj is None:
@@ -289,7 +294,7 @@ class WritableField(Field):
self.validators = self.default_validators + validators
self.default = default if default is not None else self.default
- # Widgets are ony used for HTML forms.
+ # Widgets are only used for HTML forms.
widget = widget or self.widget
if isinstance(widget, type):
widget = widget()
@@ -469,8 +474,12 @@ class CharField(WritableField):
self.validators.append(validators.MaxLengthValidator(max_length))
def from_native(self, value):
- if isinstance(value, six.string_types) or value is None:
+ if isinstance(value, six.string_types):
return value
+
+ if value is None:
+ return ''
+
return smart_text(value)
@@ -501,7 +510,7 @@ class SlugField(CharField):
class ChoiceField(WritableField):
type_name = 'ChoiceField'
- type_label = 'multiple choice'
+ type_label = 'choice'
form_field_class = forms.ChoiceField
widget = widgets.Select
default_error_messages = {
@@ -509,12 +518,16 @@ class ChoiceField(WritableField):
'the available choices.'),
}
- def __init__(self, choices=(), *args, **kwargs):
+ def __init__(self, choices=(), blank_display_value=None, *args, **kwargs):
self.empty = kwargs.pop('empty', '')
super(ChoiceField, self).__init__(*args, **kwargs)
self.choices = choices
if not self.required:
- self.choices = BLANK_CHOICE_DASH + self.choices
+ if blank_display_value is None:
+ blank_choice = BLANK_CHOICE_DASH
+ else:
+ blank_choice = [('', blank_display_value)]
+ self.choices = blank_choice + self.choices
def _get_choices(self):
return self._choices
@@ -1018,9 +1031,9 @@ class SerializerMethodField(Field):
A field that gets its value by calling a method on the serializer it's attached to.
"""
- def __init__(self, method_name):
+ def __init__(self, method_name, *args, **kwargs):
self.method_name = method_name
- super(SerializerMethodField, self).__init__()
+ super(SerializerMethodField, self).__init__(*args, **kwargs)
def field_to_native(self, obj, field_name):
value = getattr(self.parent, self.method_name)(obj)
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
index 96d15eb9..c3b846ae 100644
--- a/rest_framework/filters.py
+++ b/rest_framework/filters.py
@@ -116,6 +116,10 @@ class OrderingFilter(BaseFilterBackend):
def get_ordering(self, request):
"""
Ordering is set by a comma delimited ?ordering=... query parameter.
+
+ The `ordering` query parameter can be overridden by setting
+ the `ordering_param` value on the OrderingFilter or by
+ specifying an `ORDERING_PARAM` value in the API settings.
"""
params = request.QUERY_PARAMS.get(self.ordering_param)
if params:
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 7bac510f..aea636f1 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -43,6 +43,10 @@ class GenericAPIView(views.APIView):
# You'll need to either set these attributes,
# or override `get_queryset()`/`get_serializer_class()`.
+ # If you are overriding a view method, it is important that you call
+ # `get_queryset()` instead of accessing the `queryset` property directly,
+ # as `queryset` will get evaluated only once, and those results are cached
+ # for all subsequent requests.
queryset = None
serializer_class = None
@@ -90,8 +94,8 @@ class GenericAPIView(views.APIView):
'view': self
}
- def get_serializer(self, instance=None, data=None,
- files=None, many=False, partial=False):
+ def get_serializer(self, instance=None, data=None, files=None, many=False,
+ partial=False, allow_add_remove=False):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
@@ -99,7 +103,9 @@ class GenericAPIView(views.APIView):
serializer_class = self.get_serializer_class()
context = self.get_serializer_context()
return serializer_class(instance, data=data, files=files,
- many=many, partial=partial, context=context)
+ many=many, partial=partial,
+ allow_add_remove=allow_add_remove,
+ context=context)
def get_pagination_serializer(self, page):
"""
@@ -183,7 +189,13 @@ class GenericAPIView(views.APIView):
"""
Returns the list of filter backends that this view requires.
"""
- filter_backends = self.filter_backends or []
+ if self.filter_backends is None:
+ filter_backends = []
+ else:
+ # Note that we are returning a *copy* of the class attribute,
+ # so that it is safe for the view to mutate it if needed.
+ filter_backends = list(self.filter_backends)
+
if not filter_backends and self.filter_backend:
warnings.warn(
'The `filter_backend` attribute and `FILTER_BACKEND` setting '
@@ -193,6 +205,7 @@ class GenericAPIView(views.APIView):
PendingDeprecationWarning, stacklevel=2
)
filter_backends = [self.filter_backend]
+
return filter_backends
@@ -256,6 +269,10 @@ class GenericAPIView(views.APIView):
This must be an iterable, and may be a queryset.
Defaults to using `self.queryset`.
+ 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.
+
You may want to override this if you need to provide different
querysets depending on the incoming request.
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py
index f1b3e38d..4990971b 100644
--- a/rest_framework/parsers.py
+++ b/rest_framework/parsers.py
@@ -10,7 +10,7 @@ from django.core.files.uploadhandler import StopFutureHandlers
from django.http import QueryDict
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter
-from rest_framework.compat import etree, six, yaml
+from rest_framework.compat import etree, six, yaml, force_text
from rest_framework.exceptions import ParseError
from rest_framework import renderers
import json
@@ -288,7 +288,7 @@ class FileUploadParser(BaseParser):
try:
meta = parser_context['request'].META
- disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'])
- return disposition[1]['filename']
+ disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode('utf-8'))
+ return force_text(disposition[1]['filename'])
except (AttributeError, KeyError):
pass
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 308545ce..3463954d 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -59,6 +59,8 @@ class RelatedField(WritableField):
super(RelatedField, self).__init__(*args, **kwargs)
if not self.required:
+ # Accessed in ModelChoiceIterator django/forms/models.py:1034
+ # If set adds empty choice.
self.empty_label = BLANK_CHOICE_DASH[0][1]
self.queryset = queryset
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 7a7da561..7048d87d 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -54,32 +54,37 @@ class JSONRenderer(BaseRenderer):
format = 'json'
encoder_class = encoders.JSONEncoder
ensure_ascii = True
- charset = None
- # JSON is a binary encoding, that can be encoded as utf-8, utf-16 or utf-32.
+
+ # We don't set a charset because JSON is a binary encoding,
+ # that can be encoded as utf-8, utf-16 or utf-32.
# See: http://www.ietf.org/rfc/rfc4627.txt
# Also: http://lucumr.pocoo.org/2013/7/19/application-mimetypes-and-encodings/
+ charset = None
+
+ def get_indent(self, accepted_media_type, renderer_context):
+ if accepted_media_type:
+ # If the media type looks like 'application/json; indent=4',
+ # then pretty print the result.
+ base_media_type, params = parse_header(accepted_media_type.encode('ascii'))
+ try:
+ return max(min(int(params['indent']), 8), 0)
+ except (KeyError, ValueError, TypeError):
+ pass
+
+ # If 'indent' is provided in the context, then pretty print the result.
+ # E.g. If we're being called by the BrowsableAPIRenderer.
+ return renderer_context.get('indent', None)
+
def render(self, data, accepted_media_type=None, renderer_context=None):
"""
- Render `data` into JSON.
+ Render `data` into JSON, returning a bytestring.
"""
if data is None:
return bytes()
- # If 'indent' is provided in the context, then pretty print the result.
- # E.g. If we're being called by the BrowsableAPIRenderer.
renderer_context = renderer_context or {}
- indent = renderer_context.get('indent', None)
-
- if accepted_media_type:
- # If the media type looks like 'application/json; indent=4',
- # then pretty print the result.
- base_media_type, params = parse_header(accepted_media_type.encode('ascii'))
- indent = params.get('indent', indent)
- try:
- indent = max(min(int(indent), 8), 0)
- except (ValueError, TypeError):
- indent = None
+ indent = self.get_indent(accepted_media_type, renderer_context)
ret = json.dumps(data, cls=self.encoder_class,
indent=indent, ensure_ascii=self.ensure_ascii)
@@ -193,6 +198,7 @@ class YAMLRenderer(BaseRenderer):
format = 'yaml'
encoder = encoders.SafeDumper
charset = 'utf-8'
+ ensure_ascii = True
def render(self, data, accepted_media_type=None, renderer_context=None):
"""
@@ -203,7 +209,15 @@ class YAMLRenderer(BaseRenderer):
if data is None:
return ''
- return yaml.dump(data, stream=None, encoding=self.charset, Dumper=self.encoder)
+ return yaml.dump(data, stream=None, encoding=self.charset, Dumper=self.encoder, allow_unicode=not self.ensure_ascii)
+
+
+class UnicodeYAMLRenderer(YAMLRenderer):
+ """
+ Renderer which serializes to YAML.
+ Does *not* apply character escaping for non-ascii characters.
+ """
+ ensure_ascii = False
class TemplateHTMLRenderer(BaseRenderer):
diff --git a/rest_framework/request.py b/rest_framework/request.py
index 40467c03..dc696e36 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -280,8 +280,8 @@ class Request(object):
self._method = self._request.method
# Allow X-HTTP-METHOD-OVERRIDE header
- self._method = self.META.get('HTTP_X_HTTP_METHOD_OVERRIDE',
- self._method)
+ if 'HTTP_X_HTTP_METHOD_OVERRIDE' in self.META:
+ self._method = self.META['HTTP_X_HTTP_METHOD_OVERRIDE'].upper()
def _load_stream(self):
"""
diff --git a/rest_framework/response.py b/rest_framework/response.py
index 1dc6abcf..25b78524 100644
--- a/rest_framework/response.py
+++ b/rest_framework/response.py
@@ -5,6 +5,7 @@ it is initialized with unrendered data, instead of a pre-rendered string.
The appropriate renderer is called during Django's template response rendering.
"""
from __future__ import unicode_literals
+import django
from django.core.handlers.wsgi import STATUS_CODE_TEXT
from django.template.response import SimpleTemplateResponse
from rest_framework.compat import six
@@ -15,8 +16,11 @@ class Response(SimpleTemplateResponse):
An HttpResponse that allows its data to be rendered into
arbitrary media types.
"""
+ # TODO: remove that once Django 1.3 isn't supported
+ if django.VERSION >= (1, 4):
+ rendering_attrs = SimpleTemplateResponse.rendering_attrs + ['_closable_objects']
- def __init__(self, data=None, status=200,
+ def __init__(self, data=None, status=None,
template_name=None, headers=None,
exception=False, content_type=None):
"""
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index cb7539e0..43d339da 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -16,10 +16,12 @@ import datetime
import inspect
import types
from decimal import Decimal
+from django.contrib.contenttypes.generic import GenericForeignKey
from django.core.paginator import Page
from django.db import models
from django.forms import widgets
from django.utils.datastructures import SortedDict
+from django.core.exceptions import ObjectDoesNotExist
from rest_framework.compat import get_concrete_model, six
from rest_framework.settings import api_settings
@@ -31,8 +33,8 @@ from rest_framework.settings import api_settings
# This helps keep the separation between model fields, form fields, and
# serializer fields more explicit.
-from rest_framework.relations import *
-from rest_framework.fields import *
+from rest_framework.relations import * # NOQA
+from rest_framework.fields import * # NOQA
def _resolve_model(obj):
@@ -47,7 +49,7 @@ def _resolve_model(obj):
String representations should have the format:
'appname.ModelName'
"""
- if type(obj) == str and len(obj.split('.')) == 2:
+ if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
app_name, model_name = obj.split('.')
return models.get_model(app_name, model_name)
elif inspect.isclass(obj) and issubclass(obj, models.Model):
@@ -343,7 +345,7 @@ class BaseSerializer(WritableField):
for field_name, field in self.fields.items():
if field.read_only and obj is None:
- continue
+ continue
field.initialize(parent=self, field_name=field_name)
key = self.get_field_key(field_name)
value = field.field_to_native(obj, field_name)
@@ -757,9 +759,9 @@ class ModelSerializer(Serializer):
field.read_only = True
ret[accessor_name] = field
-
+
# Ensure that 'read_only_fields' is an iterable
- assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple'
+ assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple'
# Add the `read_only` flag to any fields that have been specified
# in the `read_only_fields` option
@@ -774,10 +776,10 @@ class ModelSerializer(Serializer):
"on serializer '%s'." %
(field_name, self.__class__.__name__))
ret[field_name].read_only = True
-
+
# Ensure that 'write_only_fields' is an iterable
- assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple'
-
+ assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple'
+
for field_name in self.opts.write_only_fields:
assert field_name not in self.base_fields.keys(), (
"field '%s' on serializer '%s' specified in "
@@ -788,7 +790,7 @@ class ModelSerializer(Serializer):
"Non-existant field '%s' specified in `write_only_fields` "
"on serializer '%s'." %
(field_name, self.__class__.__name__))
- ret[field_name].write_only = True
+ ret[field_name].write_only = True
return ret
@@ -827,6 +829,19 @@ class ModelSerializer(Serializer):
if model_field:
kwargs['required'] = not(model_field.null or model_field.blank)
+ if model_field.help_text is not None:
+ kwargs['help_text'] = model_field.help_text
+ if model_field.verbose_name is not None:
+ kwargs['label'] = model_field.verbose_name
+
+ if not model_field.editable:
+ kwargs['read_only'] = True
+
+ if model_field.verbose_name is not None:
+ kwargs['label'] = model_field.verbose_name
+
+ if model_field.help_text is not None:
+ kwargs['help_text'] = model_field.help_text
return PrimaryKeyRelatedField(**kwargs)
@@ -943,6 +958,8 @@ class ModelSerializer(Serializer):
# Forward m2m relations
for field in meta.many_to_many + meta.virtual_fields:
+ if isinstance(field, GenericForeignKey):
+ continue
if field.name in attrs:
m2m_data[field.name] = attrs.pop(field.name)
@@ -952,17 +969,15 @@ class ModelSerializer(Serializer):
if isinstance(self.fields.get(field_name, None), Serializer):
nested_forward_relations[field_name] = attrs[field_name]
- # Update an existing instance...
- if instance is not None:
- for key, val in attrs.items():
- try:
- setattr(instance, key, val)
- except ValueError:
- self._errors[key] = self.error_messages['required']
+ # Create an empty instance of the model
+ if instance is None:
+ instance = self.opts.model()
- # ...or create a new instance
- else:
- instance = self.opts.model(**attrs)
+ for key, val in attrs.items():
+ try:
+ setattr(instance, key, val)
+ except ValueError:
+ self._errors[key] = [self.error_messages['required']]
# Any relations that cannot be set until we've
# saved the model get hidden away on these
@@ -1087,6 +1102,10 @@ class HyperlinkedModelSerializer(ModelSerializer):
if model_field:
kwargs['required'] = not(model_field.null or model_field.blank)
+ if model_field.help_text is not None:
+ kwargs['help_text'] = model_field.help_text
+ if model_field.verbose_name is not None:
+ kwargs['label'] = model_field.verbose_name
if self.opts.lookup_field:
kwargs['lookup_field'] = self.opts.lookup_field
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index 7067ee2f..ee96b6ee 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -24,6 +24,7 @@
{% endblock %}
</head>
+ {% block body %}
<body class="{% block bodyclass %}{% endblock %} container">
<div class="wrapper">
@@ -93,7 +94,7 @@
{% endif %}
{% if options_form %}
- <form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right">
+ <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>
@@ -101,7 +102,7 @@
{% endif %}
{% if delete_form %}
- <form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right">
+ <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>
@@ -230,4 +231,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 be9a0072..312a1138 100644
--- a/rest_framework/templates/rest_framework/login_base.html
+++ b/rest_framework/templates/rest_framework/login_base.html
@@ -1,17 +1,8 @@
+{% extends "rest_framework/base.html" %}
{% load url from future %}
{% load rest_framework %}
-<html>
-
- <head>
- {% 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/default.css" %}"/>
- {% endblock %}
- </head>
+ {% block body %}
<body class="container">
<div class="container-fluid" style="margin-top: 30px">
@@ -50,4 +41,4 @@
</div><!-- /.row-fluid -->
</div><!-- /.container-fluid -->
</body>
-</html>
+ {% endblock %}
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
index beb8c5b0..a155d8d2 100644
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -122,7 +122,7 @@ def optional_login(request):
except NoReverseMatch:
return ''
- snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, request.path)
+ snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, escape(request.path))
return snippet
@@ -136,7 +136,7 @@ def optional_logout(request):
except NoReverseMatch:
return ''
- snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, request.path)
+ snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, escape(request.path))
return snippet
@@ -180,7 +180,7 @@ def add_class(value, css_class):
# Bunch of stuff cloned from urlize
-TRAILING_PUNCTUATION = ['.', ',', ':', ';', '.)', '"', "'"]
+TRAILING_PUNCTUATION = ['.', ',', ':', ';', '.)', '"', "']", "'}", "'"]
WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('&lt;', '&gt;'),
('"', '"'), ("'", "'")]
word_split_re = re.compile(r'(\s+)')
diff --git a/rest_framework/test.py b/rest_framework/test.py
index 79982cb0..d4ec50a0 100644
--- a/rest_framework/test.py
+++ b/rest_framework/test.py
@@ -36,7 +36,7 @@ class APIRequestFactory(DjangoRequestFactory):
"""
if not data:
- return ('', None)
+ return ('', content_type)
assert format is None or content_type is None, (
'You may not set both `format` and `content_type`.'
diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py
index 6c8f2342..fba3f8f7 100644
--- a/rest_framework/tests/models.py
+++ b/rest_framework/tests/models.py
@@ -105,6 +105,7 @@ class Album(RESTFrameworkModel):
title = models.CharField(max_length=100, unique=True)
ref = models.CharField(max_length=10, unique=True, null=True, blank=True)
+
class Photo(RESTFrameworkModel):
description = models.TextField()
album = models.ForeignKey(Album)
@@ -112,7 +113,8 @@ class Photo(RESTFrameworkModel):
# Model for issue #324
class BlankFieldModel(RESTFrameworkModel):
- title = models.CharField(max_length=100, blank=True, null=False)
+ title = models.CharField(max_length=100, blank=True, null=False,
+ default="title")
# Model for issue #380
@@ -143,14 +145,16 @@ class ForeignKeyTarget(RESTFrameworkModel):
class ForeignKeySource(RESTFrameworkModel):
name = models.CharField(max_length=100)
- target = models.ForeignKey(ForeignKeyTarget, related_name='sources')
+ target = models.ForeignKey(ForeignKeyTarget, related_name='sources',
+ help_text='Target', verbose_name='Target')
# Nullable ForeignKey
class NullableForeignKeySource(RESTFrameworkModel):
name = models.CharField(max_length=100)
target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
- related_name='nullable_sources')
+ related_name='nullable_sources',
+ verbose_name='Optional target object')
# OneToOne
diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py
index c37d2a51..34bf2910 100644
--- a/rest_framework/tests/test_authentication.py
+++ b/rest_framework/tests/test_authentication.py
@@ -19,7 +19,7 @@ from rest_framework.authentication import (
OAuth2Authentication
)
from rest_framework.authtoken.models import Token
-from rest_framework.compat import patterns, url, include
+from rest_framework.compat import patterns, url, include, six
from rest_framework.compat import oauth2_provider, oauth2_provider_scope
from rest_framework.compat import oauth, oauth_provider
from rest_framework.test import APIRequestFactory, APIClient
@@ -195,6 +195,12 @@ class TokenAuthTests(TestCase):
token = Token.objects.create(user=self.user)
self.assertTrue(bool(token.key))
+ def test_generate_key_returns_string(self):
+ """Ensure generate_key returns a string"""
+ token = Token()
+ key = token.generate_key()
+ self.assertTrue(isinstance(key, six.string_types))
+
def test_token_login_json(self):
"""Ensure token login view using JSON POST works."""
client = APIClient(enforce_csrf_checks=True)
@@ -544,6 +550,15 @@ class OAuth2Tests(TestCase):
self.assertEqual(response.status_code, 401)
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
+ def test_get_form_with_wrong_authorization_header_token_missing(self):
+ """Ensure that a missing token lead to the correct HTTP error status code"""
+ auth = "Bearer"
+ response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth)
+ self.assertEqual(response.status_code, 401)
+ response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth)
+ self.assertEqual(response.status_code, 401)
+
+ @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
def test_get_form_passing_auth(self):
"""Ensure GETing form over OAuth with correct client credentials succeed"""
auth = self._create_authorization_header()
diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py
index e127feef..17d12f23 100644
--- a/rest_framework/tests/test_fields.py
+++ b/rest_framework/tests/test_fields.py
@@ -4,6 +4,7 @@ General serializer field tests.
from __future__ import unicode_literals
import datetime
+import re
from decimal import Decimal
from uuid import uuid4
from django.core import validators
@@ -103,6 +104,16 @@ class BasicFieldTests(TestCase):
keys = list(field.to_native(ret).keys())
self.assertEqual(keys, ['c', 'b', 'a', 'z'])
+ def test_widget_html_attributes(self):
+ """
+ Make sure widget_html() renders the correct attributes
+ """
+ r = re.compile('(\S+)=["\']?((?:.(?!["\']?\s+(?:\S+)=|[>"\']))+.)["\']?')
+ form = TimeFieldModelSerializer().data
+ attributes = r.findall(form.fields['clock'].widget_html())
+ self.assertIn(('name', 'clock'), attributes)
+ self.assertIn(('id', 'clock'), attributes)
+
class DateFieldTest(TestCase):
"""
@@ -312,7 +323,7 @@ class DateTimeFieldTest(TestCase):
f.from_native('04:61:59')
except validators.ValidationError as e:
self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
- "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]"])
+ "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]"])
else:
self.fail("ValidationError was not properly raised")
@@ -326,7 +337,7 @@ class DateTimeFieldTest(TestCase):
f.from_native('04 -- 31')
except validators.ValidationError as e:
self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: "
- "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]"])
+ "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]"])
else:
self.fail("ValidationError was not properly raised")
@@ -706,6 +717,15 @@ class ChoiceFieldTests(TestCase):
f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES)
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES)
+ def test_blank_choice_display(self):
+ blank = 'No Preference'
+ f = serializers.ChoiceField(
+ required=False,
+ choices=SAMPLE_CHOICES,
+ blank_display_value=blank,
+ )
+ self.assertEqual(f.choices, [('', blank)] + SAMPLE_CHOICES)
+
def test_invalid_choice_model(self):
s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'})
self.assertFalse(s.is_valid())
@@ -982,3 +1002,21 @@ class BooleanField(TestCase):
bool_field = serializers.BooleanField(required=True)
self.assertFalse(BooleanRequiredSerializer(data={}).is_valid())
+
+
+class SerializerMethodFieldTest(TestCase):
+ """
+ Tests for the SerializerMethodField field_to_native() behavior
+ """
+ class SerializerTest(serializers.Serializer):
+ def get_my_test(self, obj):
+ return obj.my_test[0:5]
+
+ class Example():
+ my_test = 'Hey, this is a test !'
+
+ def test_field_to_native(self):
+ s = serializers.SerializerMethodField('get_my_test')
+ s.initialize(self.SerializerTest(), 'name')
+ result = s.field_to_native(self.Example(), None)
+ self.assertEqual(result, 'Hey, ')
diff --git a/rest_framework/tests/test_genericrelations.py b/rest_framework/tests/test_genericrelations.py
index fa09c9e6..46a2d863 100644
--- a/rest_framework/tests/test_genericrelations.py
+++ b/rest_framework/tests/test_genericrelations.py
@@ -131,3 +131,21 @@ class TestGenericRelations(TestCase):
}
]
self.assertEqual(serializer.data, expected)
+
+ def test_restore_object_generic_fk(self):
+ """
+ Ensure an object with a generic foreign key can be restored.
+ """
+
+ class TagSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Tag
+ exclude = ('content_type', 'object_id')
+
+ serializer = TagSerializer()
+
+ bookmark = Bookmark(url='http://example.com')
+ attrs = {'tagged_item': bookmark, 'tag': 'example'}
+
+ tag = serializer.restore_object(attrs)
+ self.assertEqual(tag.tagged_item, bookmark)
diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py
index 996bd5b0..57d327cc 100644
--- a/rest_framework/tests/test_generics.py
+++ b/rest_framework/tests/test_generics.py
@@ -5,6 +5,7 @@ from django.test import TestCase
from rest_framework import generics, renderers, serializers, status
from rest_framework.test import APIRequestFactory
from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel
+from rest_framework.tests.models import ForeignKeySource, ForeignKeyTarget
from rest_framework.compat import six
factory = APIRequestFactory()
@@ -28,6 +29,13 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView):
return queryset.exclude(text='filtered out')
+class FKInstanceView(generics.RetrieveUpdateDestroyAPIView):
+ """
+ FK: example description for OPTIONS.
+ """
+ model = ForeignKeySource
+
+
class SlugSerializer(serializers.ModelSerializer):
slug = serializers.Field() # read only
@@ -407,6 +415,72 @@ class TestInstanceView(TestCase):
self.assertFalse(self.objects.filter(id=999).exists())
+class TestFKInstanceView(TestCase):
+ def setUp(self):
+ """
+ Create 3 BasicModel instances.
+ """
+ items = ['foo', 'bar', 'baz']
+ for item in items:
+ t = ForeignKeyTarget(name=item)
+ t.save()
+ ForeignKeySource(name='source_' + item, target=t).save()
+
+ self.objects = ForeignKeySource.objects
+ self.data = [
+ {'id': obj.id, 'name': obj.name}
+ for obj in self.objects.all()
+ ]
+ self.view = FKInstanceView.as_view()
+
+ def test_options_root_view(self):
+ """
+ OPTIONS requests to ListCreateAPIView should return metadata
+ """
+ request = factory.options('/999')
+ with self.assertNumQueries(1):
+ response = self.view(request, pk=999).render()
+ expected = {
+ 'name': 'Fk Instance',
+ 'description': 'FK: example description for OPTIONS.',
+ 'renders': [
+ 'application/json',
+ 'text/html'
+ ],
+ 'parses': [
+ 'application/json',
+ 'application/x-www-form-urlencoded',
+ 'multipart/form-data'
+ ],
+ 'actions': {
+ 'PUT': {
+ 'id': {
+ 'type': 'integer',
+ 'required': False,
+ 'read_only': True,
+ 'label': 'ID'
+ },
+ 'name': {
+ 'type': 'string',
+ 'required': True,
+ 'read_only': False,
+ 'label': 'name',
+ 'max_length': 100
+ },
+ 'target': {
+ 'type': 'field',
+ 'required': True,
+ 'read_only': False,
+ 'label': 'Target',
+ 'help_text': 'Target'
+ }
+ }
+ }
+ }
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data, expected)
+
+
class TestOverriddenGetObject(TestCase):
"""
Test cases for a RetrieveUpdateDestroyAPIView that does NOT use the
diff --git a/rest_framework/tests/test_parsers.py b/rest_framework/tests/test_parsers.py
index 7699e10c..8af90677 100644
--- a/rest_framework/tests/test_parsers.py
+++ b/rest_framework/tests/test_parsers.py
@@ -96,7 +96,7 @@ class TestFileUploadParser(TestCase):
request = MockRequest()
request.upload_handlers = (MemoryFileUploadHandler(),)
request.META = {
- 'HTTP_CONTENT_DISPOSITION': 'Content-Disposition: inline; filename=file.txt'.encode('utf-8'),
+ 'HTTP_CONTENT_DISPOSITION': 'Content-Disposition: inline; filename=file.txt',
'HTTP_CONTENT_LENGTH': 14,
}
self.parser_context = {'request': request, 'kwargs': {}}
@@ -112,4 +112,4 @@ class TestFileUploadParser(TestCase):
def test_get_filename(self):
parser = FileUploadParser()
filename = parser.get_filename(self.stream, None, self.parser_context)
- self.assertEqual(filename, 'file.txt'.encode('utf-8'))
+ self.assertEqual(filename, 'file.txt')
diff --git a/rest_framework/tests/test_relations.py b/rest_framework/tests/test_relations.py
index f52e0e1e..37ac826b 100644
--- a/rest_framework/tests/test_relations.py
+++ b/rest_framework/tests/test_relations.py
@@ -2,8 +2,10 @@
General tests for relational fields.
"""
from __future__ import unicode_literals
+from django import get_version
from django.db import models
from django.test import TestCase
+from django.utils import unittest
from rest_framework import serializers
from rest_framework.tests.models import BlogPost
@@ -118,3 +120,25 @@ class RelatedFieldSourceTests(TestCase):
(serializers.ModelSerializer,), attrs)
with self.assertRaises(AttributeError):
TestSerializer(data={'name': 'foo'})
+
+@unittest.skipIf(get_version() < '1.6.0', 'Upstream behaviour changed in v1.6')
+class RelatedFieldChoicesTests(TestCase):
+ """
+ Tests for #1408 "Web browseable API doesn't have blank option on drop down list box"
+ https://github.com/tomchristie/django-rest-framework/issues/1408
+ """
+ def test_blank_option_is_added_to_choice_if_required_equals_false(self):
+ """
+
+ """
+ post = BlogPost(title="Checking blank option is added")
+ post.save()
+
+ queryset = BlogPost.objects.all()
+ field = serializers.RelatedField(required=False, queryset=queryset)
+
+ choice_count = BlogPost.objects.count()
+ widget_count = len(field.widget.choices)
+
+ self.assertEqual(widget_count, choice_count + 1, 'BLANK_CHOICE_DASH option should have been added')
+
diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py
index c7bf772e..7cb7d0f9 100644
--- a/rest_framework/tests/test_renderers.py
+++ b/rest_framework/tests/test_renderers.py
@@ -12,7 +12,7 @@ from rest_framework.compat import yaml, etree, patterns, url, include, six, Stri
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
- XMLRenderer, JSONPRenderer, BrowsableAPIRenderer, UnicodeJSONRenderer
+ XMLRenderer, JSONPRenderer, BrowsableAPIRenderer, UnicodeJSONRenderer, UnicodeYAMLRenderer
from rest_framework.parsers import YAMLParser, XMLParser
from rest_framework.settings import api_settings
from rest_framework.test import APIRequestFactory
@@ -467,6 +467,17 @@ if yaml:
self.assertTrue(string in content, '%r not in %r' % (string, content))
+ class UnicodeYAMLRendererTests(TestCase):
+ """
+ Tests specific for the Unicode YAML Renderer
+ """
+ def test_proper_encoding(self):
+ obj = {'countries': ['United Kingdom', 'France', 'España']}
+ renderer = UnicodeYAMLRenderer()
+ content = renderer.render(obj, 'application/yaml')
+ self.assertEqual(content.strip(), 'countries: [United Kingdom, France, España]'.encode('utf-8'))
+
+
class XMLRendererTestCase(TestCase):
"""
Tests specific to the XML Renderer
diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py
index 3ee2b38a..fb2eac0b 100644
--- a/rest_framework/tests/test_serializer.py
+++ b/rest_framework/tests/test_serializer.py
@@ -9,7 +9,8 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers, fields, relations
from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel,
- ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel)
+ ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel,
+ ForeignKeySource, ManyToManySource)
from rest_framework.tests.models import BasicModelSerializer
import datetime
import pickle
@@ -176,6 +177,16 @@ class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer):
fields = ['some_integer']
+class ForeignKeySourceSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = ForeignKeySource
+
+
+class HyperlinkedForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = ForeignKeySource
+
+
class BasicTests(TestCase):
def setUp(self):
self.comment = Comment(
@@ -674,7 +685,7 @@ class ModelValidationTests(TestCase):
photo_serializer = PhotoSerializer(instance=photo, data={'album': ''}, partial=True)
self.assertFalse(photo_serializer.is_valid())
self.assertTrue('album' in photo_serializer.errors)
- self.assertEqual(photo_serializer.errors['album'], photo_serializer.error_messages['required'])
+ self.assertEqual(photo_serializer.errors['album'], [photo_serializer.error_messages['required']])
def test_foreign_key_with_partial(self):
"""
@@ -1225,6 +1236,9 @@ class BlankFieldTests(TestCase):
def test_create_model_null_field(self):
serializer = self.model_serializer_class(data={'title': None})
self.assertEqual(serializer.is_valid(), True)
+ serializer.save()
+ self.assertIsNot(serializer.object.pk, None)
+ self.assertEqual(serializer.object.title, '')
def test_create_not_blank_field(self):
"""
@@ -1600,6 +1614,19 @@ class ManyFieldHelpTextTest(TestCase):
self.assertEqual('Some help text.', rel_field.help_text)
+class AttributeMappingOnAutogeneratedRelatedFields(TestCase):
+
+ def test_primary_key_related_field(self):
+ serializer = ForeignKeySourceSerializer()
+ self.assertEqual(serializer.fields['target'].help_text, 'Target')
+ self.assertEqual(serializer.fields['target'].label, 'Target')
+
+ def test_hyperlinked_related_field(self):
+ serializer = HyperlinkedForeignKeySourceSerializer()
+ self.assertEqual(serializer.fields['target'].help_text, 'Target')
+ self.assertEqual(serializer.fields['target'].label, 'Target')
+
+
@unittest.skipUnless(PIL is not None, 'PIL is not installed')
class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
diff --git a/rest_framework/tests/test_serializers.py b/rest_framework/tests/test_serializers.py
index 082a400c..120510ac 100644
--- a/rest_framework/tests/test_serializers.py
+++ b/rest_framework/tests/test_serializers.py
@@ -3,6 +3,7 @@ from django.test import TestCase
from rest_framework.serializers import _resolve_model
from rest_framework.tests.models import BasicModel
+from rest_framework.compat import six
class ResolveModelTests(TestCase):
@@ -19,6 +20,10 @@ class ResolveModelTests(TestCase):
resolved_model = _resolve_model('tests.BasicModel')
self.assertEqual(resolved_model, BasicModel)
+ def test_resolve_unicode_representation(self):
+ resolved_model = _resolve_model(six.text_type('tests.BasicModel'))
+ self.assertEqual(resolved_model, BasicModel)
+
def test_resolve_non_django_model(self):
with self.assertRaises(ValueError):
_resolve_model(TestCase)
diff --git a/rest_framework/tests/test_urlizer.py b/rest_framework/tests/test_urlizer.py
new file mode 100644
index 00000000..3dc8e8fe
--- /dev/null
+++ b/rest_framework/tests/test_urlizer.py
@@ -0,0 +1,38 @@
+from __future__ import unicode_literals
+from django.test import TestCase
+from rest_framework.templatetags.rest_framework import urlize_quoted_links
+import sys
+
+
+class URLizerTests(TestCase):
+ """
+ Test if both JSON and YAML URLs are transformed into links well
+ """
+ def _urlize_dict_check(self, data):
+ """
+ For all items in dict test assert that the value is urlized key
+ """
+ for original, urlized in data.items():
+ assert urlize_quoted_links(original, nofollow=False) == urlized
+
+ def test_json_with_url(self):
+ """
+ Test if JSON URLs are transformed into links well
+ """
+ data = {}
+ data['"url": "http://api/users/1/", '] = \
+ '&quot;url&quot;: &quot;<a href="http://api/users/1/">http://api/users/1/</a>&quot;, '
+ data['"foo_set": [\n "http://api/foos/1/"\n], '] = \
+ '&quot;foo_set&quot;: [\n &quot;<a href="http://api/foos/1/">http://api/foos/1/</a>&quot;\n], '
+ self._urlize_dict_check(data)
+
+ def test_yaml_with_url(self):
+ """
+ Test if YAML URLs are transformed into links well
+ """
+ data = {}
+ data['''{users: 'http://api/users/'}'''] = \
+ '''{users: &#39;<a href="http://api/users/">http://api/users/</a>&#39;}'''
+ data['''foo_set: ['http://api/foos/1/']'''] = \
+ '''foo_set: [&#39;<a href="http://api/foos/1/">http://api/foos/1/</a>&#39;]'''
+ self._urlize_dict_check(data)
diff --git a/rest_framework/tests/test_views.py b/rest_framework/tests/test_views.py
index 65c7e50e..77b113ee 100644
--- a/rest_framework/tests/test_views.py
+++ b/rest_framework/tests/test_views.py
@@ -1,5 +1,6 @@
from __future__ import unicode_literals
+import sys
import copy
from django.test import TestCase
from rest_framework import status
@@ -11,6 +12,11 @@ from rest_framework.views import APIView
factory = APIRequestFactory()
+if sys.version_info[:2] >= (3, 4):
+ JSON_ERROR = 'JSON parse error - Expecting value:'
+else:
+ JSON_ERROR = 'JSON parse error - No JSON object could be decoded'
+
class BasicView(APIView):
def get(self, request, *args, **kwargs):
@@ -48,7 +54,7 @@ def sanitise_json_error(error_dict):
of json.
"""
ret = copy.copy(error_dict)
- chop = len('JSON parse error - No JSON object could be decoded')
+ chop = len(JSON_ERROR)
ret['detail'] = ret['detail'][:chop]
return ret
@@ -61,7 +67,7 @@ class ClassBasedViewIntegrationTests(TestCase):
request = factory.post('/', 'f00bar', content_type='application/json')
response = self.view(request)
expected = {
- 'detail': 'JSON parse error - No JSON object could be decoded'
+ 'detail': JSON_ERROR
}
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
@@ -76,7 +82,7 @@ class ClassBasedViewIntegrationTests(TestCase):
request = factory.post('/', form_data)
response = self.view(request)
expected = {
- 'detail': 'JSON parse error - No JSON object could be decoded'
+ 'detail': JSON_ERROR
}
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
@@ -90,7 +96,7 @@ class FunctionBasedViewIntegrationTests(TestCase):
request = factory.post('/', 'f00bar', content_type='application/json')
response = self.view(request)
expected = {
- 'detail': 'JSON parse error - No JSON object could be decoded'
+ 'detail': JSON_ERROR
}
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
@@ -105,7 +111,7 @@ class FunctionBasedViewIntegrationTests(TestCase):
request = factory.post('/', form_data)
response = self.view(request)
expected = {
- 'detail': 'JSON parse error - No JSON object could be decoded'
+ 'detail': JSON_ERROR
}
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(sanitise_json_error(response.data), expected)
diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py
index efa9fb94..91be9cfd 100644
--- a/rest_framework/throttling.py
+++ b/rest_framework/throttling.py
@@ -157,6 +157,8 @@ class AnonRateThrottle(SimpleRateThrottle):
ident = request.META.get('HTTP_X_FORWARDED_FOR')
if ident is None:
ident = request.META.get('REMOTE_ADDR')
+ else:
+ ident = ''.join(ident.split())
return self.cache_format % {
'scope': self.scope,
diff --git a/rest_framework/urls.py b/rest_framework/urls.py
index 9c4719f1..5d70f899 100644
--- a/rest_framework/urls.py
+++ b/rest_framework/urls.py
@@ -2,15 +2,15 @@
Login and logout views for the browsable API.
Add these to your root URLconf if you're using the browsable API and
-your API requires authentication.
-
-The urls must be namespaced as 'rest_framework', and you should make sure
-your authentication settings include `SessionAuthentication`.
+your API requires authentication:
urlpatterns = patterns('',
...
url(r'^auth', include('rest_framework.urls', namespace='rest_framework'))
)
+
+The urls must be namespaced as 'rest_framework', and you should make sure
+your authentication settings include `SessionAuthentication`.
"""
from __future__ import unicode_literals
from rest_framework.compat import patterns, url
diff --git a/rest_framework/utils/mediatypes.py b/rest_framework/utils/mediatypes.py
index c09c2933..92f99efd 100644
--- a/rest_framework/utils/mediatypes.py
+++ b/rest_framework/utils/mediatypes.py
@@ -74,7 +74,7 @@ class _MediaType(object):
return 0
elif self.sub_type == '*':
return 1
- elif not self.params or self.params.keys() == ['q']:
+ elif not self.params or list(self.params.keys()) == ['q']:
return 2
return 3
diff --git a/tox.ini b/tox.ini
index 855ab0ce..72d156f9 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,127 +1,153 @@
[tox]
downloadcache = {toxworkdir}/cache/
-envlist = py3.3-django1.7,py3.2-django1.7,py2.7-django1.7,py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,py2.7-django1.4,py2.6-django1.4,py2.7-django1.3,py2.6-django1.3
+envlist =
+ py3.4-django1.7,py3.3-django1.7,py3.2-django1.7,py2.7-django1.7,
+ py3.4-django1.6,py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6,
+ py3.4-django1.5,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,
+ py2.7-django1.4,py2.6-django1.4,
+ py2.7-django1.3,py2.6-django1.3
[testenv]
commands = {envpython} rest_framework/runtests/runtests.py
+[testenv:py3.4-django1.7]
+basepython = python3.4
+deps = https://www.djangoproject.com/download/1.7c2/tarball/
+ django-filter==0.7
+ defusedxml==0.3
+ Pillow==2.3.0
+
[testenv:py3.3-django1.7]
basepython = python3.3
-deps = https://www.djangoproject.com/download/1.7b1/tarball/
+deps = https://www.djangoproject.com/download/1.7c2/tarball/
django-filter==0.7
defusedxml==0.3
Pillow==2.3.0
[testenv:py3.2-django1.7]
basepython = python3.2
-deps = https://www.djangoproject.com/download/1.7b1/tarball/
+deps = https://www.djangoproject.com/download/1.7c2/tarball/
django-filter==0.7
defusedxml==0.3
Pillow==2.3.0
[testenv:py2.7-django1.7]
basepython = python2.7
-deps = https://www.djangoproject.com/download/1.7b1/tarball/
+deps = https://www.djangoproject.com/download/1.7c2/tarball/
+ django-filter==0.7
+ defusedxml==0.3
+ # django-oauth-plus==2.2.1
+ # oauth2==1.5.211
+ # django-oauth2-provider==0.2.4
+ django-guardian==1.2.3
+ Pillow==2.3.0
+
+[testenv:py3.4-django1.6]
+basepython = python3.4
+deps = Django==1.6.3
django-filter==0.7
defusedxml==0.3
- django-oauth-plus==2.2.1
- oauth2==1.5.211
- django-oauth2-provider==0.2.4
- django-guardian==1.1.1
Pillow==2.3.0
[testenv:py3.3-django1.6]
basepython = python3.3
-deps = Django==1.6
+deps = Django==1.6.3
django-filter==0.7
defusedxml==0.3
Pillow==2.3.0
[testenv:py3.2-django1.6]
basepython = python3.2
-deps = Django==1.6
+deps = Django==1.6.3
django-filter==0.7
defusedxml==0.3
Pillow==2.3.0
[testenv:py2.7-django1.6]
basepython = python2.7
-deps = Django==1.6
+deps = Django==1.6.3
django-filter==0.7
defusedxml==0.3
django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.4
- django-guardian==1.1.1
+ django-guardian==1.2.3
Pillow==2.3.0
[testenv:py2.6-django1.6]
basepython = python2.6
-deps = Django==1.6
+deps = Django==1.6.3
django-filter==0.7
defusedxml==0.3
django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.4
- django-guardian==1.1.1
+ django-guardian==1.2.3
+ Pillow==2.3.0
+
+[testenv:py3.4-django1.5]
+basepython = python3.4
+deps = django==1.5.6
+ django-filter==0.7
+ defusedxml==0.3
Pillow==2.3.0
[testenv:py3.3-django1.5]
basepython = python3.3
-deps = django==1.5.5
+deps = django==1.5.6
django-filter==0.7
defusedxml==0.3
Pillow==2.3.0
[testenv:py3.2-django1.5]
basepython = python3.2
-deps = django==1.5.5
+deps = django==1.5.6
django-filter==0.7
defusedxml==0.3
Pillow==2.3.0
[testenv:py2.7-django1.5]
basepython = python2.7
-deps = django==1.5.5
+deps = django==1.5.6
django-filter==0.7
defusedxml==0.3
django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.3
- django-guardian==1.1.1
+ django-guardian==1.2.3
Pillow==2.3.0
[testenv:py2.6-django1.5]
basepython = python2.6
-deps = django==1.5.5
+deps = django==1.5.6
django-filter==0.7
defusedxml==0.3
django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.3
- django-guardian==1.1.1
+ django-guardian==1.2.3
Pillow==2.3.0
[testenv:py2.7-django1.4]
basepython = python2.7
-deps = django==1.4.10
+deps = django==1.4.11
django-filter==0.7
defusedxml==0.3
django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.3
- django-guardian==1.1.1
+ django-guardian==1.2.3
Pillow==2.3.0
[testenv:py2.6-django1.4]
basepython = python2.6
-deps = django==1.4.10
+deps = django==1.4.11
django-filter==0.7
defusedxml==0.3
django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.3
- django-guardian==1.1.1
+ django-guardian==1.2.3
Pillow==2.3.0
[testenv:py2.7-django1.3]
@@ -132,7 +158,7 @@ deps = django==1.3.5
django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.3
- django-guardian==1.1.1
+ django-guardian==1.2.3
Pillow==2.3.0
[testenv:py2.6-django1.3]
@@ -143,5 +169,5 @@ deps = django==1.3.5
django-oauth-plus==2.2.1
oauth2==1.5.211
django-oauth2-provider==0.2.3
- django-guardian==1.1.1
+ django-guardian==1.2.3
Pillow==2.3.0