aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml15
-rw-r--r--README.md12
-rwxr-xr-xdocs/api-guide/authentication.md17
-rw-r--r--docs/api-guide/fields.md7
-rwxr-xr-xdocs/api-guide/generic-views.md10
-rw-r--r--docs/api-guide/permissions.md2
-rw-r--r--docs/api-guide/serializers.md14
-rw-r--r--docs/api-guide/throttling.md2
-rw-r--r--docs/api-guide/viewsets.md2
-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.md8
-rw-r--r--docs/topics/browsable-api.md7
-rw-r--r--docs/topics/documenting-your-api.md2
-rw-r--r--docs/topics/kickstarter-announcement.md131
-rw-r--r--docs/topics/release-notes.md16
-rw-r--r--docs/tutorial/1-serialization.md20
-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.md14
-rw-r--r--docs/tutorial/5-relationships-and-hyperlinked-apis.md26
-rw-r--r--docs/tutorial/quickstart.md45
-rwxr-xr-xmkdocs.py2
-rw-r--r--pytest.ini2
-rw-r--r--requirements-test.txt (renamed from optionals.txt)7
-rw-r--r--requirements.txt2
-rw-r--r--rest_framework/__init__.py8
-rw-r--r--rest_framework/authentication.py16
-rw-r--r--rest_framework/authtoken/migrations/0001_initial.py94
-rw-r--r--rest_framework/authtoken/models.py1
-rw-r--r--rest_framework/authtoken/south_migrations/0001_initial.py60
-rw-r--r--rest_framework/authtoken/south_migrations/__init__.py0
-rw-r--r--rest_framework/compat.py15
-rw-r--r--rest_framework/decorators.py7
-rw-r--r--rest_framework/exceptions.py1
-rw-r--r--rest_framework/fields.py10
-rw-r--r--rest_framework/filters.py4
-rw-r--r--rest_framework/generics.py58
-rw-r--r--rest_framework/negotiation.py6
-rw-r--r--rest_framework/permissions.py22
-rw-r--r--rest_framework/relations.py16
-rw-r--r--rest_framework/renderers.py54
-rw-r--r--rest_framework/request.py38
-rw-r--r--rest_framework/response.py12
-rw-r--r--rest_framework/serializers.py42
-rw-r--r--rest_framework/settings.py13
-rw-r--r--rest_framework/status.py4
-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.py8
-rw-r--r--rest_framework/test.py11
-rw-r--r--rest_framework/urls.py16
-rw-r--r--rest_framework/utils/encoders.py31
-rw-r--r--rest_framework/utils/formatting.py4
-rw-r--r--rest_framework/utils/mediatypes.py4
-rw-r--r--rest_framework/views.py2
-rw-r--r--rest_framework/viewsets.py10
-rwxr-xr-xruntests.py86
-rw-r--r--tests/conftest.py (renamed from conftest.py)10
-rw-r--r--tests/serializers.py1
-rw-r--r--tests/settings.py11
-rw-r--r--tests/test_authentication.py82
-rw-r--r--tests/test_breadcrumbs.py61
-rw-r--r--tests/test_fields.py32
-rw-r--r--tests/test_files.py11
-rw-r--r--tests/test_filters.py9
-rw-r--r--tests/test_genericrelations.py24
-rw-r--r--tests/test_htmlrenderer.py3
-rw-r--r--tests/test_hyperlinkedserializers.py3
-rw-r--r--tests/test_pagination.py20
-rw-r--r--tests/test_permissions.py58
-rw-r--r--tests/test_relations.py11
-rw-r--r--tests/test_relations_hyperlink.py15
-rw-r--r--tests/test_relations_pk.py12
-rw-r--r--tests/test_renderers.py59
-rw-r--r--tests/test_request.py3
-rw-r--r--tests/test_response.py3
-rw-r--r--tests/test_reverse.py3
-rw-r--r--tests/test_routers.py9
-rw-r--r--tests/test_serializer.py44
-rw-r--r--tests/test_serializer_bulk_update.py6
-rw-r--r--tests/test_serializer_nested.py2
-rw-r--r--tests/test_serializers.py4
-rw-r--r--tests/test_status.py2
-rw-r--r--tests/test_templatetags.py4
-rw-r--r--tests/test_testing.py17
-rw-r--r--tests/test_throttling.py48
-rw-r--r--tests/test_urlizer.py1
-rw-r--r--tox.ini39
156 files changed, 1152 insertions, 577 deletions
diff --git a/.travis.yml b/.travis.yml
index 01f3209e..ececf3e9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -8,32 +8,33 @@ python:
- "3.4"
env:
- - DJANGO="https://www.djangoproject.com/download/1.7.b4/tarball/"
+ - DJANGO="https://www.djangoproject.com/download/1.7c2/tarball/"
- DJANGO="django==1.6.5"
- DJANGO="django==1.5.8"
- DJANGO="django==1.4.13"
install:
- pip install $DJANGO
- - pip install defusedxml==0.3 Pillow==2.3.0
+ - pip install defusedxml==0.3
+ - pip install Pillow==2.3.0
+ - pip install django-guardian==1.2.3
- pip install pytest-django==2.6.1
+ - pip install flake8==2.2.2
- "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.7.b4/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:
- - py.test
+ - ./runtests.py
matrix:
exclude:
- python: "2.6"
- env: DJANGO="https://www.djangoproject.com/download/1.7.b4/tarball/"
+ env: DJANGO="https://www.djangoproject.com/download/1.7c2/tarball/"
- python: "3.2"
env: DJANGO="django==1.4.13"
- python: "3.3"
diff --git a/README.md b/README.md
index eea002b4..0eaf5c83 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,3 @@
----
-
-#### Django REST framework 3 - Kickstarter announcement!
-
-We are currently running a Kickstarter campaign to help fund the development of Django REST framework 3.
-
-If you want to help drive sustainable open-source development forward, then **please check out [the Kickstarter project](https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3) and consider funding us.**
-
----
-
# Django REST framework
[![build-status-image]][travis]
@@ -18,7 +8,7 @@ If you want to help drive sustainable open-source development forward, then **pl
# Overview
-Django REST framework is a powerful and flexible toolkit that makes it easy to build Web APIs.
+Django REST framework is a powerful and flexible toolkit for building Web APIs.
Some reasons you might want to use REST framework:
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index 1cb37d67..343466ee 100755
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -126,7 +126,13 @@ To use the `TokenAuthentication` scheme you'll need to [configure the authentica
'rest_framework.authtoken'
)
-Make sure to run `manage.py syncdb` after changing your settings. The `authtoken` database tables are managed by south (see [Schema migrations](#schema-migrations) below).
+
+---
+
+**Note:** Make sure to run `manage.py syncdb` after changing your settings. The `rest_framework.authtoken` app provides both Django (from v1.7) and South database migrations. See [Schema migrations](#schema-migrations) below.
+
+---
+
You'll also need to create tokens for your users.
@@ -198,7 +204,14 @@ Note that the default `obtain_auth_token` view explicitly uses JSON requests and
#### Schema migrations
-The `rest_framework.authtoken` app includes a south migration that will create the authtoken table.
+The `rest_framework.authtoken` app includes both Django native migrations (for Django versions >1.7) and South migrations (for Django versions <1.7) that will create the authtoken table.
+
+----
+
+**Note**: From REST Framework v2.4.0 using South with Django <1.7 requires upgrading South v1.0+
+
+----
+
If you're using a [custom user model][custom-user-model] you'll need to make sure that any initial migration that creates the user table runs before the authtoken table is created.
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index d23f3d19..95d9fad3 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`
@@ -362,12 +362,17 @@ The [drf-compound-fields][drf-compound-fields] package provides "compound" seria
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/generic-views.md b/docs/api-guide/generic-views.md
index bb748981..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,7 +69,7 @@ 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`.
@@ -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:
diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md
index c44b22de..38ae3d0a 100644
--- a/docs/api-guide/permissions.md
+++ b/docs/api-guide/permissions.md
@@ -244,7 +244,7 @@ The [REST Condition][rest-condition] package is another extension for building c
[authentication]: authentication.md
[throttling]: throttling.md
[filtering]: filtering.md
-[contribauth]: https://docs.djangoproject.com/en/1.0/topics/auth/#permissions
+[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/serializers.md b/docs/api-guide/serializers.md
index cedf1ff7..29b7851b 100644
--- a/docs/api-guide/serializers.md
+++ b/docs/api-guide/serializers.md
@@ -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 d223f9b3..832304f1 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 dc5d01a2..b32f5a80 100644
--- a/docs/api-guide/viewsets.md
+++ b/docs/api-guide/viewsets.md
@@ -151,7 +151,7 @@ The `@action` decorator will route `POST` requests by default, but may also acce
@detail_route(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 dd060ecc..6abc4f04 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -9,14 +9,6 @@
---
-#### Django REST framework 3 - Kickstarter announcement!
-
-We are currently running a Kickstarter campaign to help fund the development of Django REST framework 3.
-
-If you want to help drive sustainable open-source development **please [check out the Kickstarter project](https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3) and consider funding us.**
-
----
-
<p>
<h1 style="position: absolute;
width: 1px;
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
index 98cf12e3..84dc8511 100644
--- a/docs/topics/kickstarter-announcement.md
+++ b/docs/topics/kickstarter-announcement.md
@@ -29,3 +29,134 @@ 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 f6bbb815..b0e5b198 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -40,6 +40,11 @@ You can determine your currently installed version using `pip freeze`:
### 2.4.0
+* Added compatibility with Django 1.7's native migrations.
+
+ **IMPORTANT**: In order to continue to use South with Django <1.7 you **must** upgrade to
+ South v1.0.
+
* Use py.test
* `@detail_route` and `@list_route` decorators replace `@action` and `@link`.
* `six` no longer bundled. For Django <= 1.4.1, install `six` package.
@@ -52,6 +57,7 @@ You can determine your currently installed version using `pip freeze`:
## 2.3.x series
+
### 2.3.14
**Date**: 12th June 2014
@@ -76,8 +82,6 @@ You can determine your currently installed version using `pip freeze`:
* Support `blank_display_value` on `ChoiceField`.
### 2.3.13
-## 2.3.x series
-
**Date**: 6th March 2014
@@ -183,9 +187,9 @@ You can determine your currently installed version using `pip freeze`:
* Added `trailing_slash` option to routers.
* Include support for `HttpStreamingResponse`.
* Support wider range of default serializer validation when used with custom model fields.
-* UTF-8 Support for browsable API descriptions.
+* UTF-8 Support for browsable API descriptions.
* OAuth2 provider uses timezone aware datetimes when supported.
-* Bugfix: Return error correctly when OAuth non-existent consumer occurs.
+* Bugfix: Return error correctly when OAuth non-existent consumer occurs.
* Bugfix: Allow `FileUploadParser` to correctly filename if provided as URL kwarg.
* Bugfix: Fix `ScopedRateThrottle`.
@@ -226,7 +230,7 @@ You can determine your currently installed version using `pip freeze`:
* Added SearchFilter
* Added OrderingFilter
* Added GenericViewSet
-* Bugfix: Multiple `@action` and `@link` methods now allowed on viewsets.
+* Bugfix: Multiple `@action` and `@link` methods now allowed on viewsets.
* Bugfix: Fix API Root view issue with DjangoModelPermissions
### 2.3.2
@@ -279,7 +283,7 @@ You can determine your currently installed version using `pip freeze`:
* Long HTTP headers in browsable API are broken in multiple lines when possible.
* Bugfix: Fix regression with DjangoFilterBackend not worthing correctly with single object views.
* Bugfix: OAuth should fail hard when invalid token used.
-* Bugfix: Fix serializer potentially returning `None` object for models that define `__bool__` or `__len__`.
+* Bugfix: Fix serializer potentially returning `None` object for models that define `__bool__` or `__len__`.
### 2.2.5
diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md
index 55b19457..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',)
@@ -122,12 +122,12 @@ The first thing we need to get started on our Web API is to provide a way of ser
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.
"""
@@ -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 491df160..74ad9a55 100644
--- a/docs/tutorial/4-authentication-and-permissions.md
+++ b/docs/tutorial/4-authentication-and-permissions.md
@@ -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/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 529d2314..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'
diff --git a/pytest.ini b/pytest.ini
deleted file mode 100644
index bbd083ac..00000000
--- a/pytest.ini
+++ /dev/null
@@ -1,2 +0,0 @@
-[pytest]
-addopts = --tb=short
diff --git a/optionals.txt b/requirements-test.txt
index 262e7644..411daeba 100644
--- a/optionals.txt
+++ b/requirements-test.txt
@@ -1,3 +1,10 @@
+# Test requirements
+pytest-django==2.6
+pytest==2.5.2
+pytest-cov==1.6
+flake8==2.2.2
+
+# Optional packages
markdown>=2.1.0
PyYAML>=3.10
defusedxml>=0.3
diff --git a/requirements.txt b/requirements.txt
index 360acb14..730c1d07 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1 @@
--e .
Django>=1.3
-pytest-django==2.6
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 01036cef..f30012b9 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -1,9 +1,9 @@
"""
-______ _____ _____ _____ __ _
-| ___ \ ___/ ___|_ _| / _| | |
-| |_/ / |__ \ `--. | | | |_ _ __ __ _ _ __ ___ _____ _____ _ __| | __
+______ _____ _____ _____ __
+| ___ \ ___/ ___|_ _| / _| | |
+| |_/ / |__ \ `--. | | | |_ _ __ __ _ _ __ ___ _____ _____ _ __| |__
| /| __| `--. \ | | | _| '__/ _` | '_ ` _ \ / _ \ \ /\ / / _ \| '__| |/ /
-| |\ \| |___/\__/ / | | | | | | | (_| | | | | | | __/\ V V / (_) | | | <
+| |\ \| |___/\__/ / | | | | | | | (_| | | | | | | __/\ V V / (_) | | | <
\_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_|
"""
diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py
index cbc83574..5721a869 100644
--- a/rest_framework/authentication.py
+++ b/rest_framework/authentication.py
@@ -21,7 +21,7 @@ def get_authorization_header(request):
Hide some test client ickyness where the header can be unicode.
"""
auth = request.META.get('HTTP_AUTHORIZATION', b'')
- if type(auth) == type(''):
+ if isinstance(auth, type('')):
# Work around django test client oddness
auth = auth.encode(HTTP_HEADER_ENCODING)
return auth
@@ -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/migrations/0001_initial.py b/rest_framework/authtoken/migrations/0001_initial.py
index d5965e40..2e5d6b47 100644
--- a/rest_framework/authtoken/migrations/0001_initial.py
+++ b/rest_framework/authtoken/migrations/0001_initial.py
@@ -1,67 +1,27 @@
-# -*- coding: utf-8 -*-
-import datetime
-from south.db import db
-from south.v2 import SchemaMigration
-from django.db import models
-
-from rest_framework.settings import api_settings
-
-
-try:
- from django.contrib.auth import get_user_model
-except ImportError: # django < 1.5
- from django.contrib.auth.models import User
-else:
- User = get_user_model()
-
-
-class Migration(SchemaMigration):
-
- def forwards(self, orm):
- # Adding model 'Token'
- db.create_table('authtoken_token', (
- ('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)),
- ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm['%s.%s' % (User._meta.app_label, User._meta.object_name)])),
- ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
- ))
- db.send_create_signal('authtoken', ['Token'])
-
-
- def backwards(self, orm):
- # Deleting model 'Token'
- db.delete_table('authtoken_token')
-
-
- models = {
- 'auth.group': {
- 'Meta': {'object_name': 'Group'},
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
- 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
- },
- 'auth.permission': {
- 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
- 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
- },
- "%s.%s" % (User._meta.app_label, User._meta.module_name): {
- 'Meta': {'object_name': User._meta.module_name},
- },
- 'authtoken.token': {
- 'Meta': {'object_name': 'Token'},
- 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
- 'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}),
- 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'auth_token'", 'unique': 'True', 'to': "orm['%s.%s']" % (User._meta.app_label, User._meta.object_name)})
- },
- 'contenttypes.contenttype': {
- 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
- 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
- }
- }
-
- complete_apps = ['authtoken']
+# encoding: utf8
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+from django.conf import settings
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Token',
+ fields=[
+ ('key', models.CharField(max_length=40, serialize=False, primary_key=True)),
+ ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, to_field='id')),
+ ('created', models.DateTimeField(auto_now_add=True)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=(models.Model,),
+ ),
+ ]
diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py
index 167fa531..db21d44c 100644
--- a/rest_framework/authtoken/models.py
+++ b/rest_framework/authtoken/models.py
@@ -1,6 +1,5 @@
import binascii
import os
-from hashlib import sha1
from django.conf import settings
from django.db import models
diff --git a/rest_framework/authtoken/south_migrations/0001_initial.py b/rest_framework/authtoken/south_migrations/0001_initial.py
new file mode 100644
index 00000000..926de02b
--- /dev/null
+++ b/rest_framework/authtoken/south_migrations/0001_initial.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+from south.db import db
+from south.v2 import SchemaMigration
+
+try:
+ from django.contrib.auth import get_user_model
+except ImportError: # django < 1.5
+ from django.contrib.auth.models import User
+else:
+ User = get_user_model()
+
+
+class Migration(SchemaMigration):
+
+ def forwards(self, orm):
+ # Adding model 'Token'
+ db.create_table('authtoken_token', (
+ ('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)),
+ ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm['%s.%s' % (User._meta.app_label, User._meta.object_name)])),
+ ('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
+ ))
+ db.send_create_signal('authtoken', ['Token'])
+
+ def backwards(self, orm):
+ # Deleting model 'Token'
+ db.delete_table('authtoken_token')
+
+ models = {
+ 'auth.group': {
+ 'Meta': {'object_name': 'Group'},
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+ },
+ 'auth.permission': {
+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+ },
+ "%s.%s" % (User._meta.app_label, User._meta.module_name): {
+ 'Meta': {'object_name': User._meta.module_name},
+ },
+ 'authtoken.token': {
+ 'Meta': {'object_name': 'Token'},
+ 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}),
+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'auth_token'", 'unique': 'True', 'to': "orm['%s.%s']" % (User._meta.app_label, User._meta.object_name)})
+ },
+ 'contenttypes.contenttype': {
+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+ }
+ }
+
+ complete_apps = ['authtoken']
diff --git a/rest_framework/authtoken/south_migrations/__init__.py b/rest_framework/authtoken/south_migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/rest_framework/authtoken/south_migrations/__init__.py
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 6a5cbbe4..4b16a8ca 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -44,12 +44,15 @@ except ImportError:
django_filters = None
-# django-guardian is optional
-try:
- import guardian
- import guardian.shortcuts # Fixes #1624
-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/decorators.py b/rest_framework/decorators.py
index 18e41a18..e06d6ff5 100644
--- a/rest_framework/decorators.py
+++ b/rest_framework/decorators.py
@@ -131,6 +131,7 @@ def list_route(methods=['get'], **kwargs):
return func
return decorator
+
# These are now pending deprecation, in favor of `detail_route` and `list_route`.
def link(**kwargs):
@@ -139,11 +140,13 @@ def link(**kwargs):
"""
msg = 'link is pending deprecation. Use detail_route instead.'
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
+
def decorator(func):
func.bind_to_methods = ['get']
func.detail = True
func.kwargs = kwargs
return func
+
return decorator
@@ -153,9 +156,11 @@ def action(methods=['post'], **kwargs):
"""
msg = 'action is pending deprecation. Use detail_route instead.'
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
+
def decorator(func):
func.bind_to_methods = methods
func.detail = True
func.kwargs = kwargs
return func
- return decorator \ No newline at end of file
+
+ return decorator
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index 5f774a9f..97dab77e 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -23,6 +23,7 @@ class APIException(Exception):
def __str__(self):
return self.detail
+
class ParseError(APIException):
status_code = status.HTTP_400_BAD_REQUEST
default_detail = 'Malformed request.'
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 43a74ae6..85fcbd96 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -63,8 +63,10 @@ def get_component(obj, attr_name):
def readable_datetime_formats(formats):
- format = ', '.join(formats).replace(ISO_8601,
- 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]')
+ format = ', '.join(formats).replace(
+ ISO_8601,
+ 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]'
+ )
return humanize_strptime(format)
@@ -425,7 +427,7 @@ class ModelField(WritableField):
}
-##### Typed Fields #####
+# Typed Fields
class BooleanField(WritableField):
type_name = 'BooleanField'
@@ -484,7 +486,7 @@ class URLField(CharField):
type_label = 'url'
def __init__(self, **kwargs):
- if not 'validators' in kwargs:
+ if 'validators' not in kwargs:
kwargs['validators'] = [validators.URLValidator()]
super(URLField, self).__init__(**kwargs)
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
index 57a616c2..28927eec 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 e38c52b1..77deb8e4 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -25,6 +25,7 @@ def strict_positive_int(integer_string, cutoff=None):
ret = min(ret, cutoff)
return ret
+
def get_object_or_404(queryset, *filter_args, **filter_kwargs):
"""
Same as Django's standard shortcut, but make sure to raise 404
@@ -43,6 +44,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
@@ -158,10 +163,11 @@ class GenericAPIView(views.APIView):
raise Http404(_("Page is not 'last', nor can it be converted to an int."))
try:
page = paginator.page(page_number)
- except InvalidPage as e:
- raise Http404(_('Invalid page (%(page_number)s): %(message)s') % {
- 'page_number': page_number,
- 'message': str(e)
+ except InvalidPage as exc:
+ error_format = _('Invalid page (%(page_number)s): %(message)s')
+ raise Http404(error_format % {
+ 'page_number': page_number,
+ 'message': str(exc)
})
if deprecated_style:
@@ -185,7 +191,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 '
@@ -195,12 +207,11 @@ class GenericAPIView(views.APIView):
DeprecationWarning, stacklevel=2
)
filter_backends = [self.filter_backend]
- return filter_backends
+ return filter_backends
- ########################
- ### The following methods provide default implementations
- ### that you may want to override for more complex cases.
+ # The following methods provide default implementations
+ # that you may want to override for more complex cases.
def get_paginate_by(self, queryset=None):
"""
@@ -258,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.
@@ -269,8 +284,8 @@ class GenericAPIView(views.APIView):
if self.model is not None:
return self.model._default_manager.all()
- raise ImproperlyConfigured("'%s' must define 'queryset' or 'model'"
- % self.__class__.__name__)
+ error_format = "'%s' must define 'queryset' or 'model'"
+ raise ImproperlyConfigured(error_format % self.__class__.__name__)
def get_object(self, queryset=None):
"""
@@ -324,12 +339,11 @@ class GenericAPIView(views.APIView):
return obj
- ########################
- ### The following are placeholder methods,
- ### and are intended to be overridden.
- ###
- ### The are not called by GenericAPIView directly,
- ### but are used by the mixin methods.
+ # The following are placeholder methods,
+ # and are intended to be overridden.
+ #
+ # The are not called by GenericAPIView directly,
+ # but are used by the mixin methods.
def pre_save(self, obj):
"""
@@ -401,10 +415,8 @@ class GenericAPIView(views.APIView):
return ret
-##########################################################
-### Concrete view classes that provide method handlers ###
-### by composing the mixin classes with the base view. ###
-##########################################################
+# Concrete view classes that provide method handlers
+# by composing the mixin classes with the base view.
class CreateAPIView(mixins.CreateModelMixin,
GenericAPIView):
@@ -519,9 +531,7 @@ class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
return self.destroy(request, *args, **kwargs)
-##########################
-### Deprecated classes ###
-##########################
+# Deprecated classes
class MultipleObjectAPIView(GenericAPIView):
def __init__(self, *args, **kwargs):
diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py
index 4d205c0e..ca7b5397 100644
--- a/rest_framework/negotiation.py
+++ b/rest_framework/negotiation.py
@@ -54,8 +54,10 @@ class DefaultContentNegotiation(BaseContentNegotiation):
for media_type in media_type_set:
if media_type_matches(renderer.media_type, media_type):
# Return the most specific media type as accepted.
- if (_MediaType(renderer.media_type).precedence >
- _MediaType(media_type).precedence):
+ if (
+ _MediaType(renderer.media_type).precedence >
+ _MediaType(media_type).precedence
+ ):
# Eg client requests '*/*'
# Accepted media type is 'application/json'
return renderer, renderer.media_type
diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py
index c9517138..6a1a0077 100644
--- a/rest_framework/permissions.py
+++ b/rest_framework/permissions.py
@@ -62,9 +62,11 @@ class IsAuthenticatedOrReadOnly(BasePermission):
"""
def has_permission(self, request, view):
- return (request.method in SAFE_METHODS or
- request.user and
- request.user.is_authenticated())
+ return (
+ request.method in SAFE_METHODS or
+ request.user and
+ request.user.is_authenticated()
+ )
class DjangoModelPermissions(BasePermission):
@@ -122,9 +124,11 @@ class DjangoModelPermissions(BasePermission):
perms = self.get_required_permissions(request.method, model_cls)
- return (request.user and
+ return (
+ request.user and
(request.user.is_authenticated() or not self.authenticated_users_only) and
- request.user.has_perms(perms))
+ request.user.has_perms(perms)
+ )
class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions):
@@ -212,6 +216,8 @@ class TokenHasReadWriteScope(BasePermission):
required = oauth2_constants.READ if read_only else oauth2_constants.WRITE
return oauth2_provider_scope.check(required, request.auth.scope)
- assert False, ('TokenHasReadWriteScope requires either the'
- '`OAuthAuthentication` or `OAuth2Authentication` authentication '
- 'class to be used.')
+ assert False, (
+ 'TokenHasReadWriteScope requires either the'
+ '`OAuthAuthentication` or `OAuth2Authentication` authentication '
+ 'class to be used.'
+ )
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 3b234dd5..1acbdce2 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -19,8 +19,7 @@ from rest_framework.compat import smart_text
import warnings
-##### Relational fields #####
-
+# Relational fields
# Not actually Writable, but subclasses may need to be.
class RelatedField(WritableField):
@@ -66,7 +65,7 @@ class RelatedField(WritableField):
else: # Reverse
self.queryset = manager.field.rel.to._default_manager.all()
- ### We need this stuff to make form choices work...
+ # We need this stuff to make form choices work...
def prepare_value(self, obj):
return self.to_native(obj)
@@ -113,7 +112,7 @@ class RelatedField(WritableField):
choices = property(_get_choices, _set_choices)
- ### Default value handling
+ # Default value handling
def get_default_value(self):
default = super(RelatedField, self).get_default_value()
@@ -121,7 +120,7 @@ class RelatedField(WritableField):
return []
return default
- ### Regular serializer stuff...
+ # Regular serializer stuff...
def field_to_native(self, obj, field_name):
try:
@@ -181,7 +180,7 @@ class RelatedField(WritableField):
into[(self.source or field_name)] = self.from_native(value)
-### PrimaryKey relationships
+# PrimaryKey relationships
class PrimaryKeyRelatedField(RelatedField):
"""
@@ -269,8 +268,7 @@ class PrimaryKeyRelatedField(RelatedField):
return self.to_native(pk)
-### Slug relationships
-
+# Slug relationships
class SlugRelatedField(RelatedField):
"""
@@ -305,7 +303,7 @@ class SlugRelatedField(RelatedField):
raise ValidationError(msg)
-### Hyperlinked relationships
+# Hyperlinked relationships
class HyperlinkedRelatedField(RelatedField):
"""
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 484961ad..ac7175a7 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -8,7 +8,6 @@ REST framework also provides an HTML renderer the renders the browsable API.
"""
from __future__ import unicode_literals
-import copy
import json
import django
from django import forms
@@ -54,35 +53,41 @@ 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)
+ ret = json.dumps(
+ data, cls=self.encoder_class,
+ indent=indent, ensure_ascii=self.ensure_ascii
+ )
# On python 2.x json.dumps() returns bytestrings if ensure_ascii=True,
# but if ensure_ascii=False, the return type is underspecified,
@@ -409,7 +414,7 @@ class BrowsableAPIRenderer(BaseRenderer):
"""
Returns True if a form should be shown for this method.
"""
- if not method in view.allowed_methods:
+ if method not in view.allowed_methods:
return # Not a valid method
if not api_settings.FORM_METHOD_OVERRIDE:
@@ -449,8 +454,10 @@ class BrowsableAPIRenderer(BaseRenderer):
if method in ('DELETE', 'OPTIONS'):
return True # Don't actually need to return a form
- if (not getattr(view, 'get_serializer', None)
- or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)):
+ if (
+ not getattr(view, 'get_serializer', None)
+ or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)
+ ):
return
serializer = view.get_serializer(instance=obj, data=data, files=files)
@@ -571,7 +578,7 @@ class BrowsableAPIRenderer(BaseRenderer):
'version': VERSION,
'breadcrumblist': self.get_breadcrumbs(request),
'allowed_methods': view.allowed_methods,
- 'available_formats': [renderer.format for renderer in view.renderer_classes],
+ 'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes],
'response_headers': response_headers,
'put_form': self.get_rendered_html_form(view, 'PUT', request),
@@ -620,4 +627,3 @@ class MultiPartRenderer(BaseRenderer):
def render(self, data, accepted_media_type=None, renderer_context=None):
return encode_multipart(self.BOUNDARY, data)
-
diff --git a/rest_framework/request.py b/rest_framework/request.py
index 40467c03..27532661 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -42,13 +42,20 @@ class override_method(object):
self.view = view
self.request = request
self.method = method
+ self.action = getattr(view, 'action', None)
def __enter__(self):
self.view.request = clone_request(self.request, self.method)
+ if self.action is not None:
+ # For viewsets we also set the `.action` attribute.
+ action_map = getattr(self.view, 'action_map', {})
+ self.view.action = action_map.get(self.method.lower())
return self.view.request
def __exit__(self, *args, **kwarg):
self.view.request = self.request
+ if self.action is not None:
+ self.view.action = self.action
class Empty(object):
@@ -280,16 +287,19 @@ 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):
"""
Return the content body of the request, as a stream.
"""
try:
- content_length = int(self.META.get('CONTENT_LENGTH',
- self.META.get('HTTP_CONTENT_LENGTH')))
+ content_length = int(
+ self.META.get(
+ 'CONTENT_LENGTH', self.META.get('HTTP_CONTENT_LENGTH')
+ )
+ )
except (ValueError, TypeError):
content_length = 0
@@ -313,9 +323,11 @@ class Request(object):
)
# We only need to use form overloading on form POST requests.
- if (not USE_FORM_OVERLOADING
+ if (
+ not USE_FORM_OVERLOADING
or self._request.method != 'POST'
- or not is_form_media_type(self._content_type)):
+ or not is_form_media_type(self._content_type)
+ ):
return
# At this point we're committed to parsing the request as form data.
@@ -323,15 +335,19 @@ class Request(object):
self._files = self._request.FILES
# Method overloading - change the method and remove the param from the content.
- if (self._METHOD_PARAM and
- self._METHOD_PARAM in self._data):
+ if (
+ self._METHOD_PARAM and
+ self._METHOD_PARAM in self._data
+ ):
self._method = self._data[self._METHOD_PARAM].upper()
# Content overloading - modify the content type, and force re-parse.
- if (self._CONTENT_PARAM and
+ if (
+ self._CONTENT_PARAM and
self._CONTENTTYPE_PARAM and
self._CONTENT_PARAM in self._data and
- self._CONTENTTYPE_PARAM in self._data):
+ self._CONTENTTYPE_PARAM in self._data
+ ):
self._content_type = self._data[self._CONTENTTYPE_PARAM]
self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding']))
self._data, self._files = (Empty, Empty)
@@ -387,7 +403,7 @@ class Request(object):
self._not_authenticated()
raise
- if not user_auth_tuple is None:
+ if user_auth_tuple is not None:
self._authenticator = authenticator
self._user, self._auth = user_auth_tuple
return
diff --git a/rest_framework/response.py b/rest_framework/response.py
index 1dc6abcf..80225cac 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):
"""
@@ -58,8 +62,10 @@ class Response(SimpleTemplateResponse):
ret = renderer.render(self.data, media_type, context)
if isinstance(ret, six.text_type):
- assert charset, 'renderer returned unicode, and did not specify ' \
- 'a charset value.'
+ assert charset, (
+ 'renderer returned unicode, and did not specify '
+ 'a charset value.'
+ )
return bytes(ret.encode(charset))
if not ret:
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index a15b8f22..95288671 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -449,9 +449,11 @@ class BaseSerializer(WritableField):
# If we have a model manager or similar object then we need
# to iterate through each instance.
- if (self.many and
+ if (
+ self.many and
not hasattr(obj, '__iter__') and
- is_simple_callable(getattr(obj, 'all', None))):
+ is_simple_callable(getattr(obj, 'all', None))
+ ):
obj = obj.all()
kwargs = {
@@ -601,8 +603,10 @@ class BaseSerializer(WritableField):
API schemas for auto-documentation.
"""
return SortedDict(
- [(field_name, field.metadata())
- for field_name, field in six.iteritems(self.fields)]
+ [
+ (field_name, field.metadata())
+ for field_name, field in six.iteritems(self.fields)
+ ]
)
@@ -656,8 +660,10 @@ class ModelSerializer(Serializer):
"""
cls = self.opts.model
- assert cls is not None, \
- "Serializer class '%s' is missing 'model' Meta option" % self.__class__.__name__
+ assert cls is not None, (
+ "Serializer class '%s' is missing 'model' Meta option" %
+ self.__class__.__name__
+ )
opts = cls._meta.concrete_model._meta
ret = SortedDict()
nested = bool(self.opts.depth)
@@ -668,9 +674,9 @@ class ModelSerializer(Serializer):
# If model is a child via multitable inheritance, use parent's pk
pk_field = pk_field.rel.to._meta.pk
- field = self.get_pk_field(pk_field)
- if field:
- ret[pk_field.name] = field
+ serializer_pk_field = self.get_pk_field(pk_field)
+ if serializer_pk_field:
+ ret[pk_field.name] = serializer_pk_field
# Deal with forward relationships
forward_rels = [field for field in opts.fields if field.serialize]
@@ -739,9 +745,11 @@ class ModelSerializer(Serializer):
is_m2m = isinstance(relation.field,
models.fields.related.ManyToManyField)
- if (is_m2m and
+ if (
+ is_m2m and
hasattr(relation.field.rel, 'through') and
- not relation.field.rel.through._meta.auto_created):
+ not relation.field.rel.through._meta.auto_created
+ ):
has_through_model = True
if nested:
@@ -911,10 +919,12 @@ class ModelSerializer(Serializer):
for field_name, field in self.fields.items():
field_name = field.source or field_name
- if field_name in exclusions \
- and not field.read_only \
- and (field.required or hasattr(instance, field_name)) \
- and not isinstance(field, Serializer):
+ if (
+ field_name in exclusions
+ and not field.read_only
+ and (field.required or hasattr(instance, field_name))
+ and not isinstance(field, Serializer)
+ ):
exclusions.remove(field_name)
return exclusions
@@ -976,7 +986,7 @@ class ModelSerializer(Serializer):
try:
setattr(instance, key, val)
except ValueError:
- self._errors[key] = self.error_messages['required']
+ self._errors[key] = [self.error_messages['required']]
# Any relations that cannot be set until we've
# saved the model get hidden away on these
diff --git a/rest_framework/settings.py b/rest_framework/settings.py
index 189131f1..6806a468 100644
--- a/rest_framework/settings.py
+++ b/rest_framework/settings.py
@@ -46,16 +46,12 @@ DEFAULTS = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny',
),
- 'DEFAULT_THROTTLE_CLASSES': (
- ),
- 'DEFAULT_CONTENT_NEGOTIATION_CLASS':
- 'rest_framework.negotiation.DefaultContentNegotiation',
+ 'DEFAULT_THROTTLE_CLASSES': (),
+ 'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation',
# Genric view behavior
- 'DEFAULT_MODEL_SERIALIZER_CLASS':
- 'rest_framework.serializers.ModelSerializer',
- 'DEFAULT_PAGINATION_SERIALIZER_CLASS':
- 'rest_framework.pagination.PaginationSerializer',
+ 'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.ModelSerializer',
+ 'DEFAULT_PAGINATION_SERIALIZER_CLASS': 'rest_framework.pagination.PaginationSerializer',
'DEFAULT_FILTER_BACKENDS': (),
# Throttling
@@ -120,6 +116,7 @@ DEFAULTS = {
# Pending deprecation
'FILTER_BACKEND': None,
+
}
diff --git a/rest_framework/status.py b/rest_framework/status.py
index 76435371..90a75508 100644
--- a/rest_framework/status.py
+++ b/rest_framework/status.py
@@ -10,15 +10,19 @@ from __future__ import unicode_literals
def is_informational(code):
return code >= 100 and code <= 199
+
def is_success(code):
return code >= 200 and code <= 299
+
def is_redirect(code):
return code >= 300 and code <= 399
+
def is_client_error(code):
return code >= 400 and code <= 499
+
def is_server_error(code):
return code >= 500 and code <= 599
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index 210741ed..b6e9ca5c 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -25,6 +25,7 @@
{% endblock %}
</head>
+ {% block body %}
<body class="{% block bodyclass %}{% endblock %} container">
<div class="wrapper">
@@ -94,7 +95,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>
@@ -102,7 +103,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>
@@ -231,4 +232,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 be83c2f5..43860e53 100644
--- a/rest_framework/templates/rest_framework/login_base.html
+++ b/rest_framework/templates/rest_framework/login_base.html
@@ -1,18 +1,9 @@
+{% extends "rest_framework/base.html" %}
{% load url from future %}
{% load staticfiles %}
{% 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">
@@ -51,4 +42,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 911b1b62..9110aedb 100644
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -152,8 +152,10 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
middle = middle[len(opening):]
lead = lead + opening
# Keep parentheses at the end only if they're balanced.
- if (middle.endswith(closing)
- and middle.count(closing) == middle.count(opening) + 1):
+ if (
+ middle.endswith(closing)
+ and middle.count(closing) == middle.count(opening) + 1
+ ):
middle = middle[:-len(closing)]
trail = closing + trail
@@ -164,7 +166,7 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru
url = smart_urlquote_wrapper(middle)
elif simple_url_2_re.match(middle):
url = smart_urlquote_wrapper('http://%s' % middle)
- elif not ':' in middle and simple_email_re.match(middle):
+ elif ':' not in middle and simple_email_re.match(middle):
local, domain = middle.rsplit('@', 1)
try:
domain = domain.encode('idna').decode('ascii')
diff --git a/rest_framework/test.py b/rest_framework/test.py
index 284bcee0..9242cf7c 100644
--- a/rest_framework/test.py
+++ b/rest_framework/test.py
@@ -49,9 +49,10 @@ class APIRequestFactory(DjangoRequestFactory):
else:
format = format or self.default_format
- assert format in self.renderer_classes, ("Invalid format '{0}'. "
- "Available formats are {1}. Set TEST_REQUEST_RENDERER_CLASSES "
- "to enable extra request formats.".format(
+ assert format in self.renderer_classes, (
+ "Invalid format '{0}'. Available formats are {1}. "
+ "Set TEST_REQUEST_RENDERER_CLASSES to enable "
+ "extra request formats.".format(
format,
', '.join(["'" + fmt + "'" for fmt in self.renderer_classes.keys()])
)
@@ -154,6 +155,10 @@ class APIClient(APIRequestFactory, DjangoClient):
kwargs.update(self._credentials)
return super(APIClient, self).request(**kwargs)
+ def logout(self):
+ self._credentials = {}
+ return super(APIClient, self).logout()
+
class APITransactionTestCase(testcases.TransactionTestCase):
client_class = APIClient
diff --git a/rest_framework/urls.py b/rest_framework/urls.py
index 87ec0f0a..8fa3073e 100644
--- a/rest_framework/urls.py
+++ b/rest_framework/urls.py
@@ -2,23 +2,25 @@
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 django.conf.urls import patterns, url
+from django.contrib.auth import views
template_name = {'template_name': 'rest_framework/login.html'}
-urlpatterns = patterns('django.contrib.auth.views',
- url(r'^login/$', 'login', template_name, name='login'),
- url(r'^logout/$', 'logout', template_name, name='logout'),
+urlpatterns = patterns(
+ '',
+ url(r'^login/$', views.login, template_name, name='login'),
+ url(r'^logout/$', views.logout, template_name, name='logout')
)
diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py
index c125ac8a..00ffdfba 100644
--- a/rest_framework/utils/encoders.py
+++ b/rest_framework/utils/encoders.py
@@ -98,14 +98,23 @@ else:
node.flow_style = best_style
return node
- SafeDumper.add_representer(decimal.Decimal,
- SafeDumper.represent_decimal)
-
- SafeDumper.add_representer(SortedDict,
- yaml.representer.SafeRepresenter.represent_dict)
- SafeDumper.add_representer(DictWithMetadata,
- yaml.representer.SafeRepresenter.represent_dict)
- SafeDumper.add_representer(SortedDictWithMetadata,
- yaml.representer.SafeRepresenter.represent_dict)
- SafeDumper.add_representer(types.GeneratorType,
- yaml.representer.SafeRepresenter.represent_list)
+ SafeDumper.add_representer(
+ decimal.Decimal,
+ SafeDumper.represent_decimal
+ )
+ SafeDumper.add_representer(
+ SortedDict,
+ yaml.representer.SafeRepresenter.represent_dict
+ )
+ SafeDumper.add_representer(
+ DictWithMetadata,
+ yaml.representer.SafeRepresenter.represent_dict
+ )
+ SafeDumper.add_representer(
+ SortedDictWithMetadata,
+ yaml.representer.SafeRepresenter.represent_dict
+ )
+ SafeDumper.add_representer(
+ types.GeneratorType,
+ yaml.representer.SafeRepresenter.represent_list
+ )
diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py
index 4b59ba84..6d53aed1 100644
--- a/rest_framework/utils/formatting.py
+++ b/rest_framework/utils/formatting.py
@@ -6,8 +6,6 @@ from __future__ import unicode_literals
from django.utils.html import escape
from django.utils.safestring import mark_safe
from rest_framework.compat import apply_markdown
-from rest_framework.settings import api_settings
-from textwrap import dedent
import re
@@ -40,6 +38,7 @@ def dedent(content):
return content.strip()
+
def camelcase_to_spaces(content):
"""
Translate 'CamelCaseNames' to 'Camel Case Names'.
@@ -49,6 +48,7 @@ def camelcase_to_spaces(content):
content = re.sub(camelcase_boundry, ' \\1', content).strip()
return ' '.join(content.split('_')).title()
+
def markup_description(description):
"""
Apply HTML markup to the given description.
diff --git a/rest_framework/utils/mediatypes.py b/rest_framework/utils/mediatypes.py
index 92f99efd..87b3cc6a 100644
--- a/rest_framework/utils/mediatypes.py
+++ b/rest_framework/utils/mediatypes.py
@@ -57,7 +57,7 @@ class _MediaType(object):
if key != 'q' and other.params.get(key, None) != self.params.get(key, None):
return False
- if self.sub_type != '*' and other.sub_type != '*' and other.sub_type != self.sub_type:
+ if self.sub_type != '*' and other.sub_type != '*' and other.sub_type != self.sub_type:
return False
if self.main_type != '*' and other.main_type != '*' and other.main_type != self.main_type:
@@ -79,7 +79,7 @@ class _MediaType(object):
return 3
def __str__(self):
- return unicode(self).encode('utf-8')
+ return self.__unicode__().encode('utf-8')
def __unicode__(self):
ret = "%s/%s" % (self.main_type, self.sub_type)
diff --git a/rest_framework/views.py b/rest_framework/views.py
index a2668f2c..bca0aaef 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -31,6 +31,7 @@ def get_view_name(view_cls, suffix=None):
return name
+
def get_view_description(view_cls, html=False):
"""
Given a view class, return a textual description to represent the view.
@@ -119,7 +120,6 @@ class APIView(View):
headers['Vary'] = 'Accept'
return headers
-
def http_method_not_allowed(self, request, *args, **kwargs):
"""
If `request.method` does not correspond to a handler method,
diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py
index 7eb29f99..bb5b304e 100644
--- a/rest_framework/viewsets.py
+++ b/rest_framework/viewsets.py
@@ -127,11 +127,11 @@ class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
class ModelViewSet(mixins.CreateModelMixin,
- mixins.RetrieveModelMixin,
- mixins.UpdateModelMixin,
- mixins.DestroyModelMixin,
- mixins.ListModelMixin,
- GenericViewSet):
+ mixins.RetrieveModelMixin,
+ mixins.UpdateModelMixin,
+ mixins.DestroyModelMixin,
+ mixins.ListModelMixin,
+ GenericViewSet):
"""
A viewset that provides default `create()`, `retrieve()`, `update()`,
`partial_update()`, `destroy()` and `list()` actions.
diff --git a/runtests.py b/runtests.py
new file mode 100755
index 00000000..4da05ac3
--- /dev/null
+++ b/runtests.py
@@ -0,0 +1,86 @@
+#! /usr/bin/env python
+from __future__ import print_function
+
+import pytest
+import sys
+import os
+import subprocess
+
+
+PYTEST_ARGS = {
+ 'default': ['tests'],
+ 'fast': ['tests', '-q'],
+}
+
+FLAKE8_ARGS = ['rest_framework', 'tests', '--ignore=E501']
+
+
+sys.path.append(os.path.dirname(__file__))
+
+def exit_on_failure(ret, message=None):
+ if ret:
+ sys.exit(ret)
+
+def flake8_main(args):
+ print('Running flake8 code linting')
+ ret = subprocess.call(['flake8'] + args)
+ print('flake8 failed' if ret else 'flake8 passed')
+ return ret
+
+def split_class_and_function(string):
+ class_string, function_string = string.split('.', 1)
+ return "%s and %s" % (class_string, function_string)
+
+def is_function(string):
+ # `True` if it looks like a test function is included in the string.
+ return string.startswith('test_') or '.test_' in string
+
+def is_class(string):
+ # `True` if first character is uppercase - assume it's a class name.
+ return string[0] == string[0].upper()
+
+
+if __name__ == "__main__":
+ try:
+ sys.argv.remove('--nolint')
+ except ValueError:
+ run_flake8 = True
+ else:
+ run_flake8 = False
+
+ try:
+ sys.argv.remove('--lintonly')
+ except ValueError:
+ run_tests = True
+ else:
+ run_tests = False
+
+ try:
+ sys.argv.remove('--fast')
+ except ValueError:
+ style = 'default'
+ else:
+ style = 'fast'
+ run_flake8 = False
+
+ if len(sys.argv) > 1:
+ pytest_args = sys.argv[1:]
+ first_arg = pytest_args[0]
+ if first_arg.startswith('-'):
+ # `runtests.py [flags]`
+ pytest_args = ['tests'] + pytest_args
+ elif is_class(first_arg) and is_function(first_arg):
+ # `runtests.py TestCase.test_function [flags]`
+ expression = split_class_and_function(first_arg)
+ pytest_args = ['tests', '-k', expression] + pytest_args[1:]
+ elif is_class(first_arg) or is_function(first_arg):
+ # `runtests.py TestCase [flags]`
+ # `runtests.py test_function [flags]`
+ pytest_args = ['tests', '-k', pytest_args[0]] + pytest_args[1:]
+ else:
+ pytest_args = PYTEST_ARGS[style]
+
+ if run_tests:
+ exit_on_failure(pytest.main(pytest_args))
+ if run_flake8:
+ exit_on_failure(flake8_main(FLAKE8_ARGS))
diff --git a/conftest.py b/tests/conftest.py
index fa5184dd..f3723aea 100644
--- a/conftest.py
+++ b/tests/conftest.py
@@ -47,8 +47,8 @@ def pytest_configure():
)
try:
- import oauth_provider
- import oauth2
+ import oauth_provider # NOQA
+ import oauth2 # NOQA
except ImportError:
pass
else:
@@ -57,7 +57,7 @@ def pytest_configure():
)
try:
- import provider
+ import provider # NOQA
except ImportError:
pass
else:
@@ -68,13 +68,13 @@ def pytest_configure():
# guardian is optional
try:
- import guardian
+ import guardian # NOQA
except ImportError:
pass
else:
settings.ANONYMOUS_USER_ID = -1
settings.AUTHENTICATION_BACKENDS = (
- 'django.contrib.auth.backends.ModelBackend', # default
+ 'django.contrib.auth.backends.ModelBackend',
'guardian.backends.ObjectPermissionBackend',
)
settings.INSTALLED_APPS += (
diff --git a/tests/serializers.py b/tests/serializers.py
index f2f85b6e..be7b3772 100644
--- a/tests/serializers.py
+++ b/tests/serializers.py
@@ -1,5 +1,4 @@
from rest_framework import serializers
-
from tests.models import NullableForeignKeySource
diff --git a/tests/settings.py b/tests/settings.py
index de41dc66..91c9ed09 100644
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -68,7 +68,6 @@ SECRET_KEY = 'u@x-aj9(hoh#rb-^ymf#g2jx_hp0vj7u5#b@ag1n^seu9e!%cy'
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
-# 'django.template.loaders.eggs.Loader',
)
MIDDLEWARE_CLASSES = (
@@ -104,8 +103,8 @@ INSTALLED_APPS = (
# OAuth is optional and won't work if there is no oauth_provider & oauth2
try:
- import oauth_provider
- import oauth2
+ import oauth_provider # NOQA
+ import oauth2 # NOQA
except ImportError:
pass
else:
@@ -114,7 +113,7 @@ else:
)
try:
- import provider
+ import provider # NOQA
except ImportError:
pass
else:
@@ -125,13 +124,13 @@ else:
# guardian is optional
try:
- import guardian
+ import guardian # NOQA
except ImportError:
pass
else:
ANONYMOUS_USER_ID = -1
AUTHENTICATION_BACKENDS = (
- 'django.contrib.auth.backends.ModelBackend', # default
+ 'django.contrib.auth.backends.ModelBackend', # default
'guardian.backends.ObjectPermissionBackend',
)
INSTALLED_APPS += (
diff --git a/tests/test_authentication.py b/tests/test_authentication.py
index 5b97d60b..9db4f62d 100644
--- a/tests/test_authentication.py
+++ b/tests/test_authentication.py
@@ -45,26 +45,39 @@ class MockView(APIView):
return HttpResponse({'a': 1, 'b': 2, 'c': 3})
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
(r'^session/$', MockView.as_view(authentication_classes=[SessionAuthentication])),
(r'^basic/$', MockView.as_view(authentication_classes=[BasicAuthentication])),
(r'^token/$', MockView.as_view(authentication_classes=[TokenAuthentication])),
(r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'),
(r'^oauth/$', MockView.as_view(authentication_classes=[OAuthAuthentication])),
- (r'^oauth-with-scope/$', MockView.as_view(authentication_classes=[OAuthAuthentication],
- permission_classes=[permissions.TokenHasReadWriteScope]))
+ (
+ r'^oauth-with-scope/$',
+ MockView.as_view(
+ authentication_classes=[OAuthAuthentication],
+ permission_classes=[permissions.TokenHasReadWriteScope]
+ )
+ )
)
+
class OAuth2AuthenticationDebug(OAuth2Authentication):
allow_query_params_token = True
if oauth2_provider is not None:
- urlpatterns += patterns('',
+ urlpatterns += patterns(
+ '',
url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')),
url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])),
url(r'^oauth2-test-debug/$', MockView.as_view(authentication_classes=[OAuth2AuthenticationDebug])),
- url(r'^oauth2-with-scope-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication],
- permission_classes=[permissions.TokenHasReadWriteScope])),
+ url(
+ r'^oauth2-with-scope-test/$',
+ MockView.as_view(
+ authentication_classes=[OAuth2Authentication],
+ permission_classes=[permissions.TokenHasReadWriteScope]
+ )
+ )
)
@@ -278,12 +291,16 @@ class OAuthTests(TestCase):
self.TOKEN_KEY = "token_key"
self.TOKEN_SECRET = "token_secret"
- self.consumer = Consumer.objects.create(key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET,
- name='example', user=self.user, status=self.consts.ACCEPTED)
+ self.consumer = Consumer.objects.create(
+ key=self.CONSUMER_KEY, secret=self.CONSUMER_SECRET,
+ name='example', user=self.user, status=self.consts.ACCEPTED
+ )
self.scope = Scope.objects.create(name="resource name", url="api/")
- self.token = OAuthToken.objects.create(user=self.user, consumer=self.consumer, scope=self.scope,
- token_type=OAuthToken.ACCESS, key=self.TOKEN_KEY, secret=self.TOKEN_SECRET, is_approved=True
+ self.token = OAuthToken.objects.create(
+ user=self.user, consumer=self.consumer, scope=self.scope,
+ token_type=OAuthToken.ACCESS, key=self.TOKEN_KEY, secret=self.TOKEN_SECRET,
+ is_approved=True
)
def _create_authorization_header(self):
@@ -501,24 +518,24 @@ class OAuth2Tests(TestCase):
self.REFRESH_TOKEN = "refresh_token"
self.oauth2_client = oauth2_provider.oauth2.models.Client.objects.create(
- client_id=self.CLIENT_ID,
- client_secret=self.CLIENT_SECRET,
- redirect_uri='',
- client_type=0,
- name='example',
- user=None,
- )
+ client_id=self.CLIENT_ID,
+ client_secret=self.CLIENT_SECRET,
+ redirect_uri='',
+ client_type=0,
+ name='example',
+ user=None,
+ )
self.access_token = oauth2_provider.oauth2.models.AccessToken.objects.create(
- token=self.ACCESS_TOKEN,
- client=self.oauth2_client,
- user=self.user,
- )
+ token=self.ACCESS_TOKEN,
+ client=self.oauth2_client,
+ user=self.user,
+ )
self.refresh_token = oauth2_provider.oauth2.models.RefreshToken.objects.create(
- user=self.user,
- access_token=self.access_token,
- client=self.oauth2_client
- )
+ user=self.user,
+ access_token=self.access_token,
+ client=self.oauth2_client
+ )
def _create_authorization_header(self, token=None):
return "Bearer {0}".format(token or self.access_token.token)
@@ -551,6 +568,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()
@@ -560,8 +586,10 @@ class OAuth2Tests(TestCase):
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
def test_post_form_passing_auth_url_transport(self):
"""Ensure GETing form over OAuth with correct client credentials in form data succeed"""
- response = self.csrf_client.post('/oauth2-test/',
- data={'access_token': self.access_token.token})
+ response = self.csrf_client.post(
+ '/oauth2-test/',
+ data={'access_token': self.access_token.token}
+ )
self.assertEqual(response.status_code, 200)
@unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed')
diff --git a/tests/test_breadcrumbs.py b/tests/test_breadcrumbs.py
index f26c3eaf..780fd5c4 100644
--- a/tests/test_breadcrumbs.py
+++ b/tests/test_breadcrumbs.py
@@ -24,7 +24,8 @@ class NestedResourceRoot(APIView):
class NestedResourceInstance(APIView):
pass
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
url(r'^$', Root.as_view()),
url(r'^resource/$', ResourceRoot.as_view()),
url(r'^resource/(?P<key>[0-9]+)$', ResourceInstance.as_view()),
@@ -40,34 +41,60 @@ class BreadcrumbTests(TestCase):
def test_root_breadcrumbs(self):
url = '/'
- self.assertEqual(get_breadcrumbs(url), [('Root', '/')])
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [('Root', '/')]
+ )
def test_resource_root_breadcrumbs(self):
url = '/resource/'
- self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
- ('Resource Root', '/resource/')])
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [
+ ('Root', '/'),
+ ('Resource Root', '/resource/')
+ ]
+ )
def test_resource_instance_breadcrumbs(self):
url = '/resource/123'
- self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
- ('Resource Root', '/resource/'),
- ('Resource Instance', '/resource/123')])
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [
+ ('Root', '/'),
+ ('Resource Root', '/resource/'),
+ ('Resource Instance', '/resource/123')
+ ]
+ )
def test_nested_resource_breadcrumbs(self):
url = '/resource/123/'
- self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
- ('Resource Root', '/resource/'),
- ('Resource Instance', '/resource/123'),
- ('Nested Resource Root', '/resource/123/')])
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [
+ ('Root', '/'),
+ ('Resource Root', '/resource/'),
+ ('Resource Instance', '/resource/123'),
+ ('Nested Resource Root', '/resource/123/')
+ ]
+ )
def test_nested_resource_instance_breadcrumbs(self):
url = '/resource/123/abc'
- self.assertEqual(get_breadcrumbs(url), [('Root', '/'),
- ('Resource Root', '/resource/'),
- ('Resource Instance', '/resource/123'),
- ('Nested Resource Root', '/resource/123/'),
- ('Nested Resource Instance', '/resource/123/abc')])
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [
+ ('Root', '/'),
+ ('Resource Root', '/resource/'),
+ ('Resource Instance', '/resource/123'),
+ ('Nested Resource Root', '/resource/123/'),
+ ('Nested Resource Instance', '/resource/123/abc')
+ ]
+ )
def test_broken_url_breadcrumbs_handled_gracefully(self):
url = '/foobar'
- self.assertEqual(get_breadcrumbs(url), [('Root', '/')])
+ self.assertEqual(
+ get_breadcrumbs(url),
+ [('Root', '/')]
+ )
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 73b15641..094ac1eb 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -648,7 +648,7 @@ class DecimalFieldTest(TestCase):
s = DecimalSerializer(data={'decimal_field': '123'})
self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is less than or equal to 100.']})
+ self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is less than or equal to 100.']})
def test_raise_min_value(self):
"""
@@ -660,7 +660,7 @@ class DecimalFieldTest(TestCase):
s = DecimalSerializer(data={'decimal_field': '99'})
self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is greater than or equal to 100.']})
+ self.assertEqual(s.errors, {'decimal_field': ['Ensure this value is greater than or equal to 100.']})
def test_raise_max_digits(self):
"""
@@ -672,7 +672,7 @@ class DecimalFieldTest(TestCase):
s = DecimalSerializer(data={'decimal_field': '123.456'})
self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 5 digits in total.']})
+ self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 5 digits in total.']})
def test_raise_max_decimal_places(self):
"""
@@ -684,7 +684,7 @@ class DecimalFieldTest(TestCase):
s = DecimalSerializer(data={'decimal_field': '123.4567'})
self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 3 decimal places.']})
+ self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 3 decimal places.']})
def test_raise_max_whole_digits(self):
"""
@@ -696,7 +696,7 @@ class DecimalFieldTest(TestCase):
s = DecimalSerializer(data={'decimal_field': '12345.6'})
self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 4 digits in total.']})
+ self.assertEqual(s.errors, {'decimal_field': ['Ensure that there are no more than 4 digits in total.']})
class ChoiceFieldTests(TestCase):
@@ -729,7 +729,7 @@ class ChoiceFieldTests(TestCase):
def test_invalid_choice_model(self):
s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'})
self.assertFalse(s.is_valid())
- self.assertEqual(s.errors, {'choice': ['Select a valid choice. wrong_value is not one of the available choices.']})
+ self.assertEqual(s.errors, {'choice': ['Select a valid choice. wrong_value is not one of the available choices.']})
self.assertEqual(s.data['choice'], '')
def test_empty_choice_model(self):
@@ -875,7 +875,7 @@ class SlugFieldTests(TestCase):
s = SlugFieldSerializer(data={'slug_field': 'a b'})
self.assertEqual(s.is_valid(), False)
- self.assertEqual(s.errors, {'slug_field': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]})
+ self.assertEqual(s.errors, {'slug_field': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]})
class URLFieldTests(TestCase):
@@ -1002,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/tests/test_files.py b/tests/test_files.py
index 78f4cf42..af110df9 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -85,11 +85,8 @@ class FileSerializerTests(TestCase):
"""
Validation should still function when no data dictionary is provided.
"""
- now = datetime.datetime.now()
- file = BytesIO(six.b('stuff'))
- file.name = 'stuff.txt'
- file.size = len(file.getvalue())
- uploaded_file = UploadedFile(file=file, created=now)
-
- serializer = UploadedFileSerializer(files={'file': file})
+ uploaded_file = BytesIO(six.b('stuff'))
+ uploaded_file.name = 'stuff.txt'
+ uploaded_file.size = len(uploaded_file.getvalue())
+ serializer = UploadedFileSerializer(files={'file': uploaded_file})
self.assertFalse(serializer.is_valid())
diff --git a/tests/test_filters.py b/tests/test_filters.py
index 38ddf4e4..47bffd43 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -86,7 +86,8 @@ if django_filters:
def get_queryset(self):
return FilterableItem.objects.all()
- urlpatterns = patterns('',
+ urlpatterns = patterns(
+ '',
url(r'^(?P<pk>\d+)/$', FilterClassDetailView.as_view(), name='detail-view'),
url(r'^$', FilterClassRootView.as_view(), name='root-view'),
url(r'^get-queryset/$', GetQuerysetView.as_view(),
@@ -677,8 +678,8 @@ class SensitiveOrderingFilterTests(TestCase):
self.assertEqual(
response.data,
[
- {'id': 1, username_field: 'userA'}, # PassB
- {'id': 2, username_field: 'userB'}, # PassC
- {'id': 3, username_field: 'userC'}, # PassA
+ {'id': 1, username_field: 'userA'}, # PassB
+ {'id': 2, username_field: 'userB'}, # PassC
+ {'id': 3, username_field: 'userC'}, # PassA
]
)
diff --git a/tests/test_genericrelations.py b/tests/test_genericrelations.py
index 3a8f3c7f..95295eaa 100644
--- a/tests/test_genericrelations.py
+++ b/tests/test_genericrelations.py
@@ -117,18 +117,18 @@ class TestGenericRelations(TestCase):
serializer = TagSerializer(Tag.objects.all(), many=True)
expected = [
- {
- 'tag': 'django',
- 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
- },
- {
- 'tag': 'python',
- 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
- },
- {
- 'tag': 'reminder',
- 'tagged_item': 'Note: Remember the milk'
- }
+ {
+ 'tag': 'django',
+ 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
+ },
+ {
+ 'tag': 'python',
+ 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
+ },
+ {
+ 'tag': 'reminder',
+ 'tagged_item': 'Note: Remember the milk'
+ }
]
self.assertEqual(serializer.data, expected)
diff --git a/tests/test_htmlrenderer.py b/tests/test_htmlrenderer.py
index 88d11c46..5a680f99 100644
--- a/tests/test_htmlrenderer.py
+++ b/tests/test_htmlrenderer.py
@@ -34,7 +34,8 @@ def not_found(request):
raise Http404()
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
url(r'^$', example),
url(r'^permission_denied$', permission_denied),
url(r'^not_found$', not_found),
diff --git a/tests/test_hyperlinkedserializers.py b/tests/test_hyperlinkedserializers.py
index d478ea73..d4548539 100644
--- a/tests/test_hyperlinkedserializers.py
+++ b/tests/test_hyperlinkedserializers.py
@@ -94,7 +94,8 @@ class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView):
model_serializer_class = serializers.HyperlinkedModelSerializer
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'),
url(r'^basic/(?P<pk>\d+)/$', BasicDetail.as_view(), name='basicmodel-detail'),
url(r'^anchor/(?P<pk>\d+)/$', AnchorDetail.as_view(), name='anchor-detail'),
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index 293146c0..80c33e2e 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -1,7 +1,6 @@
from __future__ import unicode_literals
import datetime
from decimal import Decimal
-from django.db import models
from django.core.paginator import Paginator
from django.test import TestCase
from django.utils import unittest
@@ -12,6 +11,7 @@ from .models import BasicModel, FilterableItem
factory = APIRequestFactory()
+
# Helper function to split arguments out of an url
def split_arguments_from_url(url):
if '?' not in url:
@@ -274,8 +274,8 @@ class TestUnpaginated(TestCase):
BasicModel(text=i).save()
self.objects = BasicModel.objects
self.data = [
- {'id': obj.id, 'text': obj.text}
- for obj in self.objects.all()
+ {'id': obj.id, 'text': obj.text}
+ for obj in self.objects.all()
]
self.view = DefaultPageSizeKwargView.as_view()
@@ -302,8 +302,8 @@ class TestCustomPaginateByParam(TestCase):
BasicModel(text=i).save()
self.objects = BasicModel.objects
self.data = [
- {'id': obj.id, 'text': obj.text}
- for obj in self.objects.all()
+ {'id': obj.id, 'text': obj.text}
+ for obj in self.objects.all()
]
self.view = PaginateByParamView.as_view()
@@ -363,11 +363,11 @@ class TestMaxPaginateByParam(TestCase):
self.assertEqual(response.data['results'], self.data[:3])
-### Tests for context in pagination serializers
+# Tests for context in pagination serializers
class CustomField(serializers.Field):
def to_native(self, value):
- if not 'view' in self.context:
+ if 'view' not in self.context:
raise RuntimeError("context isn't getting passed into custom field")
return "value"
@@ -377,7 +377,7 @@ class BasicModelSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
super(BasicModelSerializer, self).__init__(*args, **kwargs)
- if not 'view' in self.context:
+ if 'view' not in self.context:
raise RuntimeError("context isn't getting passed into serializer init")
@@ -398,7 +398,7 @@ class TestContextPassedToCustomField(TestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
-### Tests for custom pagination serializers
+# Tests for custom pagination serializers
class LinksSerializer(serializers.Serializer):
next = pagination.NextPageField(source='*')
@@ -483,8 +483,6 @@ class NonIntegerPaginator(object):
class TestNonIntegerPagination(TestCase):
-
-
def test_custom_pagination_serializer(self):
objects = ['john', 'paul', 'george', 'ringo']
paginator = NonIntegerPaginator(objects, 2)
diff --git a/tests/test_permissions.py b/tests/test_permissions.py
index a2cb0c36..93f8020f 100644
--- a/tests/test_permissions.py
+++ b/tests/test_permissions.py
@@ -12,6 +12,7 @@ import base64
factory = APIRequestFactory()
+
class RootView(generics.ListCreateAPIView):
model = BasicModel
authentication_classes = [authentication.BasicAuthentication]
@@ -101,42 +102,54 @@ class ModelPermissionsIntegrationTests(TestCase):
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_options_permitted(self):
- request = factory.options('/',
- HTTP_AUTHORIZATION=self.permitted_credentials)
+ request = factory.options(
+ '/',
+ HTTP_AUTHORIZATION=self.permitted_credentials
+ )
response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data)
self.assertEqual(list(response.data['actions'].keys()), ['POST'])
- request = factory.options('/1',
- HTTP_AUTHORIZATION=self.permitted_credentials)
+ request = factory.options(
+ '/1',
+ HTTP_AUTHORIZATION=self.permitted_credentials
+ )
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data)
self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
def test_options_disallowed(self):
- request = factory.options('/',
- HTTP_AUTHORIZATION=self.disallowed_credentials)
+ request = factory.options(
+ '/',
+ HTTP_AUTHORIZATION=self.disallowed_credentials
+ )
response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotIn('actions', response.data)
- request = factory.options('/1',
- HTTP_AUTHORIZATION=self.disallowed_credentials)
+ request = factory.options(
+ '/1',
+ HTTP_AUTHORIZATION=self.disallowed_credentials
+ )
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotIn('actions', response.data)
def test_options_updateonly(self):
- request = factory.options('/',
- HTTP_AUTHORIZATION=self.updateonly_credentials)
+ request = factory.options(
+ '/',
+ HTTP_AUTHORIZATION=self.updateonly_credentials
+ )
response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotIn('actions', response.data)
- request = factory.options('/1',
- HTTP_AUTHORIZATION=self.updateonly_credentials)
+ request = factory.options(
+ '/1',
+ HTTP_AUTHORIZATION=self.updateonly_credentials
+ )
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data)
@@ -153,6 +166,7 @@ class BasicPermModel(models.Model):
# add, change, delete built in to django
)
+
# Custom object-level permission, that includes 'view' permissions
class ViewObjectPermissions(permissions.DjangoObjectPermissions):
perms_map = {
@@ -205,7 +219,7 @@ class ObjectPermissionsIntegrationTests(TestCase):
app_label = BasicPermModel._meta.app_label
f = '{0}_{1}'.format
perms = {
- 'view': f('view', model_name),
+ 'view': f('view', model_name),
'change': f('change', model_name),
'delete': f('delete', model_name)
}
@@ -246,21 +260,27 @@ class ObjectPermissionsIntegrationTests(TestCase):
# Update
def test_can_update_permissions(self):
- request = factory.patch('/1', {'text': 'foobar'}, format='json',
- HTTP_AUTHORIZATION=self.credentials['writeonly'])
+ request = factory.patch(
+ '/1', {'text': 'foobar'}, format='json',
+ HTTP_AUTHORIZATION=self.credentials['writeonly']
+ )
response = object_permissions_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data.get('text'), 'foobar')
def test_cannot_update_permissions(self):
- request = factory.patch('/1', {'text': 'foobar'}, format='json',
- HTTP_AUTHORIZATION=self.credentials['deleteonly'])
+ request = factory.patch(
+ '/1', {'text': 'foobar'}, format='json',
+ HTTP_AUTHORIZATION=self.credentials['deleteonly']
+ )
response = object_permissions_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_cannot_update_permissions_non_existing(self):
- request = factory.patch('/999', {'text': 'foobar'}, format='json',
- HTTP_AUTHORIZATION=self.credentials['deleteonly'])
+ request = factory.patch(
+ '/999', {'text': 'foobar'}, format='json',
+ HTTP_AUTHORIZATION=self.credentials['deleteonly']
+ )
response = object_permissions_view(request, pk='999')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
diff --git a/tests/test_relations.py b/tests/test_relations.py
index cd276d30..bc1db69f 100644
--- a/tests/test_relations.py
+++ b/tests/test_relations.py
@@ -108,19 +108,25 @@ class RelatedFieldSourceTests(TestCase):
doesn't exist.
"""
from tests.models import ManyToManySource
+
class Meta:
model = ManyToManySource
+
attrs = {
'name': serializers.SlugRelatedField(
slug_field='name', source='banzai'),
'Meta': Meta,
}
- TestSerializer = type(str('TestSerializer'),
- (serializers.ModelSerializer,), attrs)
+ TestSerializer = type(
+ str('TestSerializer'),
+ (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):
"""
@@ -141,4 +147,3 @@ class RelatedFieldChoicesTests(TestCase):
widget_count = len(field.widget.choices)
self.assertEqual(widget_count, choice_count + 1, 'BLANK_CHOICE_DASH option should have been added')
-
diff --git a/tests/test_relations_hyperlink.py b/tests/test_relations_hyperlink.py
index ab1c6664..0c8eb254 100644
--- a/tests/test_relations_hyperlink.py
+++ b/tests/test_relations_hyperlink.py
@@ -16,7 +16,8 @@ request = factory.get('/') # Just to ensure we have a request in the serializer
def dummy_view(request, pk):
pass
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
url(r'^dummyurl/(?P<pk>[0-9]+)/$', dummy_view, name='dummy-url'),
url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
@@ -86,9 +87,9 @@ class HyperlinkedManyToManyTests(TestCase):
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
expected = [
- {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/']},
- {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
- {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
+ {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/']},
+ {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
+ {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
]
self.assertEqual(serializer.data, expected)
@@ -114,9 +115,9 @@ class HyperlinkedManyToManyTests(TestCase):
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True, context={'request': request})
expected = [
- {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']},
- {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
- {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
+ {'url': 'http://testserver/manytomanysource/1/', 'name': 'source-1', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']},
+ {'url': 'http://testserver/manytomanysource/2/', 'name': 'source-2', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/']},
+ {'url': 'http://testserver/manytomanysource/3/', 'name': 'source-3', 'targets': ['http://testserver/manytomanytarget/1/', 'http://testserver/manytomanytarget/2/', 'http://testserver/manytomanytarget/3/']}
]
self.assertEqual(serializer.data, expected)
diff --git a/tests/test_relations_pk.py b/tests/test_relations_pk.py
index ff59b250..c051b076 100644
--- a/tests/test_relations_pk.py
+++ b/tests/test_relations_pk.py
@@ -65,9 +65,9 @@ class PKManyToManyTests(TestCase):
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True)
expected = [
- {'id': 1, 'name': 'source-1', 'targets': [1]},
- {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
- {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}
+ {'id': 1, 'name': 'source-1', 'targets': [1]},
+ {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
+ {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}
]
self.assertEqual(serializer.data, expected)
@@ -93,9 +93,9 @@ class PKManyToManyTests(TestCase):
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True)
expected = [
- {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]},
- {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
- {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}
+ {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]},
+ {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
+ {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}
]
self.assertEqual(serializer.data, expected)
diff --git a/tests/test_renderers.py b/tests/test_renderers.py
index 1d8adfa7..0403cde2 100644
--- a/tests/test_renderers.py
+++ b/tests/test_renderers.py
@@ -76,7 +76,6 @@ class MockGETView(APIView):
return Response({'foo': ['bar', 'baz']})
-
class MockPOSTView(APIView):
def post(self, request, **kwargs):
return Response({'foo': request.DATA})
@@ -102,7 +101,8 @@ class HTMLView1(APIView):
def get(self, request, **kwargs):
return Response('text')
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
url(r'^cache$', MockGETView.as_view()),
@@ -312,16 +312,22 @@ class JSONRendererTests(TestCase):
class Dict(MutableMapping):
def __init__(self):
self._dict = dict()
+
def __getitem__(self, key):
return self._dict.__getitem__(key)
+
def __setitem__(self, key, value):
return self._dict.__setitem__(key, value)
+
def __delitem__(self, key):
return self._dict.__delitem__(key)
+
def __iter__(self):
return self._dict.__iter__()
+
def __len__(self):
return self._dict.__len__()
+
def keys(self):
return self._dict.keys()
@@ -330,22 +336,24 @@ class JSONRendererTests(TestCase):
x[2] = 3
ret = JSONRenderer().render(x)
data = json.loads(ret.decode('utf-8'))
- self.assertEquals(data, {'key': 'string value', '2': 3})
+ self.assertEquals(data, {'key': 'string value', '2': 3})
def test_render_obj_with_getitem(self):
class DictLike(object):
def __init__(self):
self._dict = {}
+
def set(self, value):
self._dict = dict(value)
+
def __getitem__(self, key):
return self._dict[key]
-
+
x = DictLike()
x.set({'a': 1, 'b': 'string'})
with self.assertRaises(TypeError):
JSONRenderer().render(x)
-
+
def test_without_content_type_args(self):
"""
Test basic JSON rendering.
@@ -394,35 +402,47 @@ class JSONPRendererTests(TestCase):
"""
Test JSONP rendering with View JSON Renderer.
"""
- resp = self.client.get('/jsonp/jsonrenderer',
- HTTP_ACCEPT='application/javascript')
+ resp = self.client.get(
+ '/jsonp/jsonrenderer',
+ HTTP_ACCEPT='application/javascript'
+ )
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
- self.assertEqual(resp.content,
- ('callback(%s);' % _flat_repr).encode('ascii'))
+ self.assertEqual(
+ resp.content,
+ ('callback(%s);' % _flat_repr).encode('ascii')
+ )
def test_without_callback_without_json_renderer(self):
"""
Test JSONP rendering without View JSON Renderer.
"""
- resp = self.client.get('/jsonp/nojsonrenderer',
- HTTP_ACCEPT='application/javascript')
+ resp = self.client.get(
+ '/jsonp/nojsonrenderer',
+ HTTP_ACCEPT='application/javascript'
+ )
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
- self.assertEqual(resp.content,
- ('callback(%s);' % _flat_repr).encode('ascii'))
+ self.assertEqual(
+ resp.content,
+ ('callback(%s);' % _flat_repr).encode('ascii')
+ )
def test_with_callback(self):
"""
Test JSONP rendering with callback function name.
"""
callback_func = 'myjsonpcallback'
- resp = self.client.get('/jsonp/nojsonrenderer?callback=' + callback_func,
- HTTP_ACCEPT='application/javascript')
+ resp = self.client.get(
+ '/jsonp/nojsonrenderer?callback=' + callback_func,
+ HTTP_ACCEPT='application/javascript'
+ )
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
- self.assertEqual(resp.content,
- ('%s(%s);' % (callback_func, _flat_repr)).encode('ascii'))
+ self.assertEqual(
+ resp.content,
+ ('%s(%s);' % (callback_func, _flat_repr)).encode('ascii')
+ )
if yaml:
@@ -467,7 +487,6 @@ if yaml:
def assertYAMLContains(self, content, string):
self.assertTrue(string in content, '%r not in %r' % (string, content))
-
class UnicodeYAMLRendererTests(TestCase):
"""
Tests specific for the Unicode YAML Renderer
@@ -592,13 +611,13 @@ class CacheRenderTest(TestCase):
""" Return any errors that would be raised if `obj' is pickled
Courtesy of koffie @ http://stackoverflow.com/a/7218986/109897
"""
- if seen == None:
+ if seen is None:
seen = []
try:
state = obj.__getstate__()
except AttributeError:
return
- if state == None:
+ if state is None:
return
if isinstance(state, tuple):
if not isinstance(state[0], dict):
diff --git a/tests/test_request.py b/tests/test_request.py
index 0cde0fb4..8b048b5c 100644
--- a/tests/test_request.py
+++ b/tests/test_request.py
@@ -272,7 +272,8 @@ class MockView(APIView):
return Response(status=status.INTERNAL_SERVER_ERROR)
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
(r'^$', MockView.as_view()),
)
diff --git a/tests/test_response.py b/tests/test_response.py
index 0551f4a8..c28f186e 100644
--- a/tests/test_response.py
+++ b/tests/test_response.py
@@ -100,7 +100,8 @@ new_model_viewset_router = routers.DefaultRouter()
new_model_viewset_router.register(r'', HTMLNewModelViewSet)
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
url(r'^setbyview$', MockViewSettingContentType.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
diff --git a/tests/test_reverse.py b/tests/test_reverse.py
index 0d3fddf0..675a9d5a 100644
--- a/tests/test_reverse.py
+++ b/tests/test_reverse.py
@@ -10,7 +10,8 @@ factory = APIRequestFactory()
def null_view(request):
pass
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
url(r'^view$', null_view, name='view'),
)
diff --git a/tests/test_routers.py b/tests/test_routers.py
index 381569bd..b076f134 100644
--- a/tests/test_routers.py
+++ b/tests/test_routers.py
@@ -93,7 +93,8 @@ class TestCustomLookupFields(TestCase):
from tests import test_routers
urls = getattr(test_routers, 'urlpatterns')
- urls += patterns('',
+ urls += patterns(
+ '',
url(r'^', include(self.router.urls)),
)
@@ -104,7 +105,8 @@ class TestCustomLookupFields(TestCase):
def test_retrieve_lookup_field_list_view(self):
response = self.client.get('/notes/')
- self.assertEqual(response.data,
+ self.assertEqual(
+ response.data,
[{
"url": "http://testserver/notes/123/",
"uuid": "123", "text": "foo bar"
@@ -113,7 +115,8 @@ class TestCustomLookupFields(TestCase):
def test_retrieve_lookup_field_detail_view(self):
response = self.client.get('/notes/123/')
- self.assertEqual(response.data,
+ self.assertEqual(
+ response.data,
{
"url": "http://testserver/notes/123/",
"uuid": "123", "text": "foo bar"
diff --git a/tests/test_serializer.py b/tests/test_serializer.py
index 6a1a3521..90f37cf2 100644
--- a/tests/test_serializer.py
+++ b/tests/test_serializer.py
@@ -7,10 +7,12 @@ from django.utils import unittest
from django.utils.datastructures import MultiValueDict
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers, fields, relations
-from tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
- BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel,
- ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel,
- ForeignKeySource, ManyToManySource)
+from tests.models import (
+ HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
+ BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel,
+ DefaultValueModel, ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo,
+ RESTFrameworkModel, ForeignKeySource
+)
from tests.models import BasicModelSerializer
import datetime
import pickle
@@ -99,6 +101,7 @@ class ActionItemSerializer(serializers.ModelSerializer):
class Meta:
model = ActionItem
+
class ActionItemSerializerOptionalFields(serializers.ModelSerializer):
"""
Intended to test that fields with `required=False` are excluded from validation.
@@ -109,6 +112,7 @@ class ActionItemSerializerOptionalFields(serializers.ModelSerializer):
model = ActionItem
fields = ('title',)
+
class ActionItemSerializerCustomRestore(serializers.ModelSerializer):
class Meta:
@@ -295,8 +299,10 @@ class BasicTests(TestCase):
in the Meta data
"""
serializer = PersonSerializer(self.person)
- self.assertEqual(set(serializer.data.keys()),
- set(['name', 'age', 'info']))
+ self.assertEqual(
+ set(serializer.data.keys()),
+ set(['name', 'age', 'info'])
+ )
def test_field_with_dictionary(self):
"""
@@ -331,9 +337,9 @@ class BasicTests(TestCase):
— id field is not populated if `data` is accessed prior to `save()`
"""
serializer = ActionItemSerializer(self.actionitem)
- self.assertIsNone(serializer.data.get('id',None), 'New instance. `id` should not be set.')
+ self.assertIsNone(serializer.data.get('id', None), 'New instance. `id` should not be set.')
serializer.save()
- self.assertIsNotNone(serializer.data.get('id',None), 'Model is saved. `id` should be set.')
+ self.assertIsNotNone(serializer.data.get('id', None), 'Model is saved. `id` should be set.')
def test_fields_marked_as_not_required_are_excluded_from_validation(self):
"""
@@ -409,7 +415,7 @@ class ValidationTests(TestCase):
mistaken for not having a default."""
data = {
'title': 'Some action item',
- #No 'done' value.
+ # No 'done' value.
}
serializer = ActionItemSerializer(self.actionitem, data=data)
self.assertEqual(serializer.is_valid(), True)
@@ -660,10 +666,10 @@ class ModelValidationTests(TestCase):
serializer.save()
second_serializer = AlbumsSerializer(data={'title': 'a'})
self.assertFalse(second_serializer.is_valid())
- self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.'],})
+ self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']})
third_serializer = AlbumsSerializer(data=[{'title': 'b', 'ref': '1'}, {'title': 'c'}], many=True)
self.assertFalse(third_serializer.is_valid())
- self.assertEqual(third_serializer.errors, [{'ref': ['Album with this Ref already exists.']}, {}])
+ self.assertEqual(third_serializer.errors, [{'ref': ['Album with this Ref already exists.']}, {}])
def test_foreign_key_is_null_with_partial(self):
"""
@@ -686,7 +692,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):
"""
@@ -959,7 +965,7 @@ class WritableFieldDefaultValueTests(TestCase):
self.assertEqual(got, self.expected)
def test_get_default_value_with_callable(self):
- field = self.create_field(default=lambda : self.expected)
+ field = self.create_field(default=lambda: self.expected)
got = field.get_default_value()
self.assertEqual(got, self.expected)
@@ -974,7 +980,7 @@ class WritableFieldDefaultValueTests(TestCase):
self.assertIsNone(got)
def test_get_default_value_returns_non_True_values(self):
- values = [None, '', False, 0, [], (), {}] # values that assumed as 'False' in the 'if' clause
+ values = [None, '', False, 0, [], (), {}] # values that assumed as 'False' in the 'if' clause
for expected in values:
field = self.create_field(default=expected)
got = field.get_default_value()
@@ -1276,7 +1282,7 @@ class BlankFieldTests(TestCase):
self.fail('Exception raised on save() after validation passes')
-#test for issue #460
+# Test for issue #460
class SerializerPickleTests(TestCase):
"""
Test pickleability of the output of Serializers
@@ -1500,7 +1506,7 @@ class NestedSerializerContextTests(TestCase):
callable = serializers.SerializerMethodField('_callable')
def _callable(self, instance):
- if not 'context_item' in self.context:
+ if 'context_item' not in self.context:
raise RuntimeError("context isn't getting passed into 2nd level nested serializer")
return "success"
@@ -1513,7 +1519,7 @@ class NestedSerializerContextTests(TestCase):
callable = serializers.SerializerMethodField("_callable")
def _callable(self, instance):
- if not 'context_item' in self.context:
+ if 'context_item' not in self.context:
raise RuntimeError("context isn't getting passed into 1st level nested serializer")
return "success"
@@ -1816,7 +1822,7 @@ class MetadataSerializerTestCase(TestCase):
self.assertEqual(expected, metadata)
-### Regression test for #840
+# Regression test for #840
class SimpleModel(models.Model):
text = models.CharField(max_length=100)
@@ -1850,7 +1856,7 @@ class FieldValidationRemovingAttr(TestCase):
self.assertEqual(serializer.object.text, 'foo')
-### Regression test for #878
+# Regression test for #878
class SimpleTargetModel(models.Model):
text = models.CharField(max_length=100)
diff --git a/tests/test_serializer_bulk_update.py b/tests/test_serializer_bulk_update.py
index 8b0ded1a..67a8ed0d 100644
--- a/tests/test_serializer_bulk_update.py
+++ b/tests/test_serializer_bulk_update.py
@@ -83,9 +83,9 @@ class BulkCreateSerializerTests(TestCase):
self.assertEqual(serializer.is_valid(), False)
expected_errors = [
- {'non_field_errors': ['Invalid data']},
- {'non_field_errors': ['Invalid data']},
- {'non_field_errors': ['Invalid data']}
+ {'non_field_errors': ['Invalid data']},
+ {'non_field_errors': ['Invalid data']},
+ {'non_field_errors': ['Invalid data']}
]
self.assertEqual(serializer.errors, expected_errors)
diff --git a/tests/test_serializer_nested.py b/tests/test_serializer_nested.py
index 6d69ffbd..c09c24db 100644
--- a/tests/test_serializer_nested.py
+++ b/tests/test_serializer_nested.py
@@ -328,12 +328,14 @@ class NestedModelSerializerUpdateTests(TestCase):
class BlogPostSerializer(serializers.ModelSerializer):
comments = BlogPostCommentSerializer(many=True, source='blogpostcomment_set')
+
class Meta:
model = models.BlogPost
fields = ('id', 'title', 'comments')
class PersonSerializer(serializers.ModelSerializer):
posts = BlogPostSerializer(many=True, source='blogpost_set')
+
class Meta:
model = models.Person
fields = ('id', 'name', 'age', 'posts')
diff --git a/tests/test_serializers.py b/tests/test_serializers.py
index 2e276f15..09de9f4c 100644
--- a/tests/test_serializers.py
+++ b/tests/test_serializers.py
@@ -1,9 +1,7 @@
-from django.db import models
from django.test import TestCase
-
+from rest_framework.compat import six
from rest_framework.serializers import _resolve_model
from tests.models import BasicModel
-from rest_framework.compat import six
class ResolveModelTests(TestCase):
diff --git a/tests/test_status.py b/tests/test_status.py
index 7b1bdae3..721a6e30 100644
--- a/tests/test_status.py
+++ b/tests/test_status.py
@@ -30,4 +30,4 @@ class TestStatus(TestCase):
self.assertFalse(is_server_error(499))
self.assertTrue(is_server_error(500))
self.assertTrue(is_server_error(599))
- self.assertFalse(is_server_error(600)) \ No newline at end of file
+ self.assertFalse(is_server_error(600))
diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py
index d4da0c23..75ee0eaa 100644
--- a/tests/test_templatetags.py
+++ b/tests/test_templatetags.py
@@ -11,7 +11,7 @@ class TemplateTagTests(TestCase):
def test_add_query_param_with_non_latin_charactor(self):
# Ensure we don't double-escape non-latin characters
- # that are present in the querystring.
+ # that are present in the querystring.
# See #1314.
request = factory.get("/", {'q': '查询'})
json_url = add_query_param(request, "format", "json")
@@ -48,4 +48,4 @@ class Issue1386Tests(TestCase):
self.assertEqual(i, res)
# example from issue #1386, this shouldn't raise an exception
- _ = urlize_quoted_links("asdf:[/p]zxcv.com")
+ urlize_quoted_links("asdf:[/p]zxcv.com")
diff --git a/tests/test_testing.py b/tests/test_testing.py
index e2e4e217..9c472026 100644
--- a/tests/test_testing.py
+++ b/tests/test_testing.py
@@ -28,7 +28,8 @@ def session_view(request):
})
-urlpatterns = patterns('',
+urlpatterns = patterns(
+ '',
url(r'^view/$', view),
url(r'^session-view/$', session_view),
)
@@ -99,6 +100,17 @@ class TestAPITestClient(TestCase):
self.assertEqual(response.status_code, 403)
self.assertEqual(response.data, expected)
+ def test_can_logout(self):
+ """
+ `logout()` reset stored credentials
+ """
+ self.client.credentials(HTTP_AUTHORIZATION='example')
+ response = self.client.get('/view/')
+ self.assertEqual(response.data['auth'], 'example')
+ self.client.logout()
+ response = self.client.get('/view/')
+ self.assertEqual(response.data['auth'], b'')
+
class TestAPIRequestFactory(TestCase):
def test_csrf_exempt_by_default(self):
@@ -131,7 +143,8 @@ class TestAPIRequestFactory(TestCase):
assertion error.
"""
factory = APIRequestFactory()
- self.assertRaises(AssertionError, factory.post,
+ self.assertRaises(
+ AssertionError, factory.post,
path='/view/', data={'example': 1}, format='xml'
)
diff --git a/tests/test_throttling.py b/tests/test_throttling.py
index 8c5eefe9..b0cb2fe7 100644
--- a/tests/test_throttling.py
+++ b/tests/test_throttling.py
@@ -27,7 +27,7 @@ class NonTimeThrottle(BaseThrottle):
if not hasattr(self.__class__, 'called'):
self.__class__.called = True
return True
- return False
+ return False
class MockView(APIView):
@@ -125,36 +125,42 @@ class ThrottlingTests(TestCase):
"""
Ensure for second based throttles.
"""
- self.ensure_response_header_contains_proper_throttle_field(MockView,
- ((0, None),
- (0, None),
- (0, None),
- (0, '1')
- ))
+ self.ensure_response_header_contains_proper_throttle_field(
+ MockView, (
+ (0, None),
+ (0, None),
+ (0, None),
+ (0, '1')
+ )
+ )
def test_minutes_fields(self):
"""
Ensure for minute based throttles.
"""
- self.ensure_response_header_contains_proper_throttle_field(MockView_MinuteThrottling,
- ((0, None),
- (0, None),
- (0, None),
- (0, '60')
- ))
+ self.ensure_response_header_contains_proper_throttle_field(
+ MockView_MinuteThrottling, (
+ (0, None),
+ (0, None),
+ (0, None),
+ (0, '60')
+ )
+ )
def test_next_rate_remains_constant_if_followed(self):
"""
If a client follows the recommended next request rate,
the throttling rate should stay constant.
"""
- self.ensure_response_header_contains_proper_throttle_field(MockView_MinuteThrottling,
- ((0, None),
- (20, None),
- (40, None),
- (60, None),
- (80, None)
- ))
+ self.ensure_response_header_contains_proper_throttle_field(
+ MockView_MinuteThrottling, (
+ (0, None),
+ (20, None),
+ (40, None),
+ (60, None),
+ (80, None)
+ )
+ )
def test_non_time_throttle(self):
"""
@@ -170,7 +176,7 @@ class ThrottlingTests(TestCase):
self.assertTrue(MockView_NonTimeThrottling.throttle_classes[0].called)
response = MockView_NonTimeThrottling.as_view()(request)
- self.assertFalse('X-Throttle-Wait-Seconds' in response)
+ self.assertFalse('X-Throttle-Wait-Seconds' in response)
class ScopedRateThrottleTests(TestCase):
diff --git a/tests/test_urlizer.py b/tests/test_urlizer.py
index 3dc8e8fe..a77aa22a 100644
--- a/tests/test_urlizer.py
+++ b/tests/test_urlizer.py
@@ -1,7 +1,6 @@
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):
diff --git a/tox.ini b/tox.ini
index 19192308..6f588de1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,17 +1,24 @@
[tox]
downloadcache = {toxworkdir}/cache/
envlist =
+ flake8,
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.4,py2.6-django1.4
[testenv]
-commands = py.test -q
+commands = ./runtests.py --fast
+
+[testenv:flake8]
+basepython = python2.7
+deps = pytest==2.5.2
+ flake8==2.2.2
+commands = ./runtests.py --lintonly
[testenv:py3.4-django1.7]
basepython = python3.4
-deps = https://www.djangoproject.com/download/1.7b2/tarball/
+deps = https://www.djangoproject.com/download/1.7c2/tarball/
django-filter==0.7
defusedxml==0.3
Pillow==2.3.0
@@ -19,7 +26,7 @@ deps = https://www.djangoproject.com/download/1.7b2/tarball/
[testenv:py3.3-django1.7]
basepython = python3.3
-deps = https://www.djangoproject.com/download/1.7b2/tarball/
+deps = https://www.djangoproject.com/download/1.7c2/tarball/
django-filter==0.7
defusedxml==0.3
Pillow==2.3.0
@@ -27,7 +34,7 @@ deps = https://www.djangoproject.com/download/1.7b2/tarball/
[testenv:py3.2-django1.7]
basepython = python3.2
-deps = https://www.djangoproject.com/download/1.7b2/tarball/
+deps = https://www.djangoproject.com/download/1.7c2/tarball/
django-filter==0.7
defusedxml==0.3
Pillow==2.3.0
@@ -35,13 +42,13 @@ deps = https://www.djangoproject.com/download/1.7b2/tarball/
[testenv:py2.7-django1.7]
basepython = python2.7
-deps = https://www.djangoproject.com/download/1.7b2/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.1.1
+ # 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
pytest-django==2.6.1
@@ -77,7 +84,7 @@ deps = Django==1.6.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
pytest-django==2.6.1
@@ -89,7 +96,7 @@ deps = Django==1.6.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
pytest-django==2.6.1
@@ -125,7 +132,7 @@ deps = django==1.5.6
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
pytest-django==2.6.1
@@ -137,7 +144,7 @@ deps = django==1.5.6
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
pytest-django==2.6.1
@@ -149,7 +156,7 @@ deps = django==1.4.11
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
pytest-django==2.6.1
@@ -161,6 +168,6 @@ deps = django==1.4.11
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
pytest-django==2.6.1