aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml55
-rw-r--r--CONTRIBUTING.md4
-rw-r--r--MANIFEST.in2
-rw-r--r--README.md76
-rwxr-xr-xdocs/api-guide/authentication.md15
-rw-r--r--docs/api-guide/exceptions.md2
-rw-r--r--docs/api-guide/fields.md5
-rw-r--r--docs/api-guide/filtering.md4
-rw-r--r--docs/api-guide/format-suffixes.md13
-rwxr-xr-xdocs/api-guide/generic-views.md22
-rw-r--r--docs/api-guide/permissions.md10
-rw-r--r--docs/api-guide/renderers.md12
-rw-r--r--docs/api-guide/routers.md4
-rw-r--r--docs/api-guide/serializers.md6
-rw-r--r--docs/api-guide/settings.md6
-rw-r--r--docs/img/sponsors/2-wusawork.pngbin0 -> 12067 bytes
-rw-r--r--docs/index.md48
-rw-r--r--docs/template.html2
-rw-r--r--docs/topics/2.3-announcement.md6
-rw-r--r--docs/topics/2.4-announcement.md (renamed from docs/topics/2.4-accouncement.md)24
-rw-r--r--docs/topics/contributing.md8
-rw-r--r--docs/topics/kickstarter-announcement.md7
-rw-r--r--docs/topics/release-notes.md34
-rw-r--r--docs/topics/third-party-resources.md92
-rw-r--r--docs/tutorial/1-serialization.md13
-rw-r--r--docs/tutorial/2-requests-and-responses.md9
-rw-r--r--docs/tutorial/3-class-based-views.md4
-rw-r--r--docs/tutorial/4-authentication-and-permissions.md4
-rw-r--r--docs/tutorial/5-relationships-and-hyperlinked-apis.md10
-rw-r--r--docs/tutorial/6-viewsets-and-routers.md12
-rw-r--r--docs/tutorial/quickstart.md43
-rwxr-xr-xmkdocs.py2
-rw-r--r--requirements.txt2
-rw-r--r--rest_framework/__init__.py2
-rw-r--r--rest_framework/authentication.py2
-rw-r--r--rest_framework/authtoken/migrations/0001_initial.py7
-rw-r--r--rest_framework/authtoken/serializers.py2
-rw-r--r--rest_framework/fields.py10
-rw-r--r--rest_framework/filters.py1
-rw-r--r--rest_framework/generics.py18
-rw-r--r--rest_framework/pagination.py9
-rw-r--r--rest_framework/parsers.py20
-rw-r--r--rest_framework/permissions.py3
-rw-r--r--rest_framework/routers.py24
-rw-r--r--rest_framework/serializers.py28
-rw-r--r--rest_framework/static/rest_framework/css/bootstrap-tweaks.css127
-rw-r--r--rest_framework/static/rest_framework/css/default.css40
-rw-r--r--rest_framework/templates/rest_framework/base.html430
-rw-r--r--rest_framework/templates/rest_framework/login_base.html35
-rw-r--r--rest_framework/templatetags/rest_framework.py21
-rw-r--r--rest_framework/test.py2
-rw-r--r--rest_framework/urls.py2
-rw-r--r--rest_framework/utils/formatting.py6
-rw-r--r--rest_framework/views.py10
-rw-r--r--rest_framework/viewsets.py3
-rw-r--r--tests/browsable_api/__init__.py0
-rw-r--r--tests/browsable_api/auth_urls.py10
-rw-r--r--tests/browsable_api/no_auth_urls.py9
-rw-r--r--tests/browsable_api/test_browsable_api.py65
-rw-r--r--tests/browsable_api/views.py15
-rw-r--r--tests/conftest.py1
-rw-r--r--tests/test_authentication.py12
-rw-r--r--tests/test_description.py24
-rw-r--r--tests/test_fields.py12
-rw-r--r--tests/test_filters.py69
-rw-r--r--tests/test_generics.py39
-rw-r--r--tests/test_pagination.py19
-rw-r--r--tests/test_parsers.py24
-rw-r--r--tests/test_routers.py18
-rw-r--r--tests/test_validation.py40
-rw-r--r--tox.ini10
71 files changed, 1191 insertions, 534 deletions
diff --git a/.travis.yml b/.travis.yml
index ececf3e9..a5b6d7d9 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,43 +1,28 @@
language: python
-python:
- - "2.6"
- - "2.7"
- - "3.2"
- - "3.3"
- - "3.4"
+python: 2.7
env:
- - DJANGO="https://www.djangoproject.com/download/1.7c2/tarball/"
- - DJANGO="django==1.6.5"
- - DJANGO="django==1.5.8"
- - DJANGO="django==1.4.13"
+ - TOX_ENV=flake8
+ - TOX_ENV=py3.4-django1.7
+ - TOX_ENV=py3.3-django1.7
+ - TOX_ENV=py3.2-django1.7
+ - TOX_ENV=py2.7-django1.7
+ - TOX_ENV=py3.4-django1.6
+ - TOX_ENV=py3.3-django1.6
+ - TOX_ENV=py3.2-django1.6
+ - TOX_ENV=py2.7-django1.6
+ - TOX_ENV=py2.6-django1.6
+ - TOX_ENV=py3.4-django1.5
+ - TOX_ENV=py3.3-django1.5
+ - TOX_ENV=py3.2-django1.5
+ - TOX_ENV=py2.7-django1.5
+ - TOX_ENV=py2.6-django1.5
+ - TOX_ENV=py2.7-django1.4
+ - TOX_ENV=py2.6-django1.4
install:
- - pip install $DJANGO
- - 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 [[ ${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 [[ ${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=.
+ - "pip install tox --download-cache $HOME/.pip-cache"
script:
- - ./runtests.py
-
-matrix:
- exclude:
- - python: "2.6"
- env: DJANGO="https://www.djangoproject.com/download/1.7c2/tarball/"
- - python: "3.2"
- env: DJANGO="django==1.4.13"
- - python: "3.3"
- env: DJANGO="django==1.4.13"
- - python: "3.4"
- env: DJANGO="django==1.4.13"
+ - tox -e $TOX_ENV
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ff6018b8..a6dd05a0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -62,10 +62,10 @@ To run the tests, clone the repository, and then:
virtualenv env
env/bin/activate
pip install -r requirements.txt
- pip install -r optionals.txt
+ pip install -r requirements-test.txt
# Run the tests
- py.test
+ ./runtests.py
You can also use the excellent [`tox`][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run:
diff --git a/MANIFEST.in b/MANIFEST.in
index 15c4d0b0..d407865f 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,2 +1,4 @@
recursive-include rest_framework/static *.js *.css *.png
recursive-include rest_framework/templates *.html
+recursive-exclude * __pycache__
+recursive-exclude * *.py[co]
diff --git a/README.md b/README.md
index 7052ab63..428fb8e9 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ There is a live example API for testing purposes, [available here][sandbox].
# Requirements
-* Python (2.6.5+, 2.7, 3.2, 3.3)
+* Python (2.6.5+, 2.7, 3.2, 3.3, 3.4)
* Django (1.4.2+, 1.5, 1.6, 1.7)
# Installation
@@ -39,40 +39,51 @@ Add `'rest_framework'` to your `INSTALLED_APPS` setting.
INSTALLED_APPS = (
...
- 'rest_framework',
+ 'rest_framework',
)
# Example
Let's take a look at a quick example of using REST framework to build a simple model-backed API for accessing users and groups.
-Here's our project's root `urls.py` module:
+Startup up a new project like so...
+
+ pip install django
+ pip install djangorestframework
+ django-admin.py startproject example .
+ ./manage.py syncdb
+
+Now edit the `example/urls.py` module in your project:
```python
-from django.conf.urls.defaults import url, patterns, include
-from django.contrib.auth.models import User, Group
-from rest_framework import viewsets, routers
+from django.conf.urls import url, include
+from django.contrib.auth.models import User
+from rest_framework import serializers, viewsets, routers
+
+# Serializers define the API representation.
+class UserSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = User
+ fields = ('url', 'username', 'email', 'is_staff')
+
# ViewSets define the view behavior.
class UserViewSet(viewsets.ModelViewSet):
- model = User
-
-class GroupViewSet(viewsets.ModelViewSet):
- model = Group
+ queryset = User.objects.all()
+ serializer_class = UserSerializer
-# Routers provide an easy way of automatically determining the URL conf
+# Routers provide a way of automatically determining the URL conf.
router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
-router.register(r'groups', GroupViewSet)
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browseable API.
-urlpatterns = patterns('',
+urlpatterns = [
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
-)
+]
```
We'd also like to configure a couple of settings for our API.
@@ -80,12 +91,12 @@ We'd also like to configure a couple of settings for our API.
Add the following to your `settings.py` module:
```python
-REST_FRAMEWORK = {
- # Use hyperlinked styles by default.
- # Only used if the `serializer_class` attribute is not set on a view.
- 'DEFAULT_MODEL_SERIALIZER_CLASS':
- 'rest_framework.serializers.HyperlinkedModelSerializer',
+INSTALLED_APPS = (
+ ... # Make sure to include the default installed apps here.
+ 'rest_framework',
+)
+REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
@@ -93,10 +104,35 @@ REST_FRAMEWORK = {
]
}
```
-Don't forget to make sure you've also added `rest_framework` to your `INSTALLED_APPS` setting.
That's it, we're done!
+ ./manage.py runserver
+
+You can now open the API in your browser at `http://127.0.0.1:8000/`, and view your new 'users' API. If you use the `Login` control in the top right corner you'll also be able to add, create and delete users from the system.
+
+You can also interact with the API using command line tools such as [`curl`](http://curl.haxx.se/). For example, to list the users endpoint:
+
+ $ curl -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/
+ [
+ {
+ "url": "http://127.0.0.1:8000/users/1/",
+ "username": "admin",
+ "email": "admin@example.com",
+ "is_staff": true,
+ }
+ ]
+
+Or to create a new user:
+
+ $ curl -X POST -d username=new -d email=new@example.com -d is_staff=false -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/
+ {
+ "url": "http://127.0.0.1:8000/users/2/",
+ "username": "new",
+ "email": "new@example.com",
+ "is_staff": false,
+ }
+
# Documentation & Support
Full documentation for the project is available at [http://www.django-rest-framework.org][docs].
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index 343466ee..3a5156fd 100755
--- a/docs/api-guide/authentication.md
+++ b/docs/api-guide/authentication.md
@@ -168,12 +168,13 @@ The `curl` command line tool may be useful for testing token authenticated APIs.
If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal.
+ from django.conf import settings
from django.contrib.auth import get_user_model
from django.db.models.signals import post_save
from django.dispatch import receiver
from rest_framework.authtoken.models import Token
- @receiver(post_save, sender=get_user_model())
+ @receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
if created:
Token.objects.create(user=instance)
@@ -190,9 +191,10 @@ If you've already created some users, you can generate tokens for all existing u
When using `TokenAuthentication`, you may want to provide a mechanism for clients to obtain a token given the username and password. REST framework provides a built-in view to provide this behavior. To use it, add the `obtain_auth_token` view to your URLconf:
- urlpatterns += patterns('',
- url(r'^api-token-auth/', 'rest_framework.authtoken.views.obtain_auth_token')
- )
+ from rest_framework.authtoken import views
+ urlpatterns += [
+ url(r'^api-token-auth/', views.obtain_auth_token)
+ ]
Note that the URL part of the pattern can be whatever you want to use.
@@ -414,6 +416,10 @@ The [HawkREST][hawkrest] library builds on the [Mohawk][mohawk] library to let y
HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a way to achieve origin authentication and message integrity for HTTP messages. Similar to [Amazon's HTTP Signature scheme][amazon-http-signature], used by many of its services, it permits stateless, per-request authentication. [Elvio Toccalino][etoccalino] maintains the [djangorestframework-httpsignature][djangorestframework-httpsignature] package which provides an easy to use HTTP Signature Authentication mechanism.
+## Djoser
+
+[Djoser][djoser] library provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation. The package works with a custom user model and it uses token based authentication. This is a ready to use REST implementation of Django authentication system.
+
[cite]: http://jacobian.org/writing/rest-worst-practices/
[http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2
[http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
@@ -448,3 +454,4 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a
[hawk]: https://github.com/hueniverse/hawk
[mohawk]: http://mohawk.readthedocs.org/en/latest/
[mac]: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05
+[djoser]: https://github.com/sunscrapers/djoser
diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md
index 66e18173..e61dcfa9 100644
--- a/docs/api-guide/exceptions.md
+++ b/docs/api-guide/exceptions.md
@@ -84,7 +84,7 @@ Note that the exception handler will only be called for responses generated by r
**Signature:** `APIException()`
-The **base class** for all exceptions raised inside REST framework.
+The **base class** for all exceptions raised inside an `APIView` class or `@api_view`.
To provide a custom exception, subclass `APIException` and set the `.status_code` and `.default_detail` properties on the class.
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index 95d9fad3..bfbff2ad 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -366,6 +366,9 @@ The [drf-extra-fields][drf-extra-fields] package provides extra serializer field
The [django-rest-framework-gis][django-rest-framework-gis] package provides geographic addons for django rest framework like a `GeometryField` field and a GeoJSON serializer.
+## django-rest-framework-hstore
+
+The [django-rest-framework-hstore][django-rest-framework-hstore] package provides an `HStoreField` to support [django-hstore][django-hstore] `DictionaryField` model field.
[cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data
[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS
@@ -376,3 +379,5 @@ The [django-rest-framework-gis][django-rest-framework-gis] package provides geog
[drf-compound-fields]: http://drf-compound-fields.readthedocs.org
[drf-extra-fields]: https://github.com/Hipo/drf-extra-fields
[django-rest-framework-gis]: https://github.com/djangonauts/django-rest-framework-gis
+[django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore
+[django-hstore]: https://github.com/djangonauts/django-hstore
diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md
index ec5ab61f..cfeb4334 100644
--- a/docs/api-guide/filtering.md
+++ b/docs/api-guide/filtering.md
@@ -193,7 +193,7 @@ filters using `Manufacturer` name. For example:
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
- fields = ['category', 'in_stock', 'manufacturer__name`]
+ fields = ['category', 'in_stock', 'manufacturer__name']
This enables us to make queries like:
@@ -211,7 +211,7 @@ This is nice, but it exposes the Django's double underscore convention as part o
class Meta:
model = Product
- fields = ['category', 'in_stock', 'manufacturer`]
+ fields = ['category', 'in_stock', 'manufacturer']
And now you can execute:
diff --git a/docs/api-guide/format-suffixes.md b/docs/api-guide/format-suffixes.md
index 529738e3..76a3367b 100644
--- a/docs/api-guide/format-suffixes.md
+++ b/docs/api-guide/format-suffixes.md
@@ -26,12 +26,13 @@ Arguments:
Example:
from rest_framework.urlpatterns import format_suffix_patterns
-
- urlpatterns = patterns('blog.views',
- url(r'^/$', 'api_root'),
- url(r'^comments/$', 'comment_list'),
- url(r'^comments/(?P<pk>[0-9]+)/$', 'comment_detail')
- )
+ from blog import views
+
+ urlpatterns = [
+ url(r'^/$', views.apt_root),
+ url(r'^comments/$', views.comment_list),
+ url(r'^comments/(?P<pk>[0-9]+)/$', views.comment_detail)
+ ]
urlpatterns = format_suffix_patterns(urlpatterns, allowed=['json', 'html'])
diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md
index e9efe709..b1c4e65a 100755
--- a/docs/api-guide/generic-views.md
+++ b/docs/api-guide/generic-views.md
@@ -49,7 +49,7 @@ For more complex cases you might also want to override various methods on the vi
serializer = UserSerializer(queryset, many=True)
return Response(serializer.data)
-For very simple cases you might want to pass through any class attributes using the `.as_view()` method. For example, your URLconf might include something the following entry.
+For very simple cases you might want to pass through any class attributes using the `.as_view()` method. For example, your URLconf might include something like the following entry:
url(r'^/users/', ListCreateAPIView.as_view(model=User), name='user-list')
@@ -74,10 +74,6 @@ The following attributes control the basic view behavior.
* `lookup_field` - The model field that should be used to for performing object lookup of individual model instances. Defaults to `'pk'`. Note that when using hyperlinked APIs you'll need to ensure that *both* the API views *and* the serializer classes set the lookup fields if you need to use a custom value.
* `lookup_url_kwarg` - The URL keyword argument that should be used for object lookup. The URL conf should include a keyword argument corresponding to this value. If unset this defaults to using the same value as `lookup_field`.
-**Shortcuts**:
-
-* `model` - This shortcut may be used instead of setting either (or both) of the `queryset`/`serializer_class` attributes, although using the explicit style is generally preferred. If used instead of `serializer_class`, then `DEFAULT_MODEL_SERIALIZER_CLASS` setting will determine the base serializer class. Note that `model` is only ever used for generating a default queryset or serializer class - the `queryset` and `serializer_class` attributes are always preferred if provided.
-
**Pagination**:
The following attributes are used to control pagination when used with list views.
@@ -91,6 +87,10 @@ The following attributes are used to control pagination when used with list view
* `filter_backends` - A list of filter backend classes that should be used for filtering the queryset. Defaults to the same value as the `DEFAULT_FILTER_BACKENDS` setting.
+**Deprecated attributes**:
+
+* `model` - This shortcut may be used instead of setting either (or both) of the `queryset`/`serializer_class` attributes. The explicit style is preferred over the `.model` shortcut, and usage of this attribute is now deprecated.
+
### Methods
**Base methods**:
@@ -101,7 +101,7 @@ Returns the queryset that should be used for list views, and that should be used
This method should always be used rather than accessing `self.queryset` directly, as `self.queryset` gets evaluated only once, and those results are cached for all subsequent requests.
-May be overridden to provide dynamic behavior such as returning a queryset that is specific to the user making the request.
+May be overridden to provide dynamic behavior, such as returning a queryset, that is specific to the user making the request.
For example:
@@ -113,7 +113,7 @@ For example:
Returns an object instance that should be used for detail views. Defaults to using the `lookup_field` parameter to filter the base queryset.
-May be overridden to provide more complex behavior such as object lookups based on more than one URL kwarg.
+May be overridden to provide more complex behavior, such as object lookups based on more than one URL kwarg.
For example:
@@ -133,7 +133,7 @@ Note that if your API doesn't include any object level permissions, you may opti
Returns the classes that should be used to filter the queryset. Defaults to returning the `filter_backends` attribute.
-May be override to provide more complex behavior with filters, as using different (or even exlusive) lists of filter_backends depending on different criteria.
+May be overridden to provide more complex behavior with filters, such as using different (or even exlusive) lists of filter_backends depending on different criteria.
For example:
@@ -149,7 +149,7 @@ For example:
Returns the class that should be used for the serializer. Defaults to returning the `serializer_class` attribute, or dynamically generating a serializer class if the `model` shortcut is being used.
-May be override to provide dynamic behavior such as using different serializers for read and write operations, or providing different serializers to different types of users.
+May be overridden to provide dynamic behavior, such as using different serializers for read and write operations, or providing different serializers to different types of users.
For example:
@@ -162,7 +162,7 @@ For example:
Returns the page size to use with pagination. By default this uses the `paginate_by` attribute, and may be overridden by the client if the `paginate_by_param` attribute is set.
-You may want to override this method to provide more complex behavior such as modifying page sizes based on the media type of the response.
+You may want to override this method to provide more complex behavior, such as modifying page sizes based on the media type of the response.
For example:
@@ -204,7 +204,7 @@ You won't typically need to override the following methods, although you might n
# Mixins
-The mixin classes provide the actions that are used to provide the basic view behavior. Note that the mixin classes provide action methods rather than defining the handler methods such as `.get()` and `.post()` directly. This allows for more flexible composition of behavior.
+The mixin classes provide the actions that are used to provide the basic view behavior. Note that the mixin classes provide action methods rather than defining the handler methods, such as `.get()` and `.post()`, directly. This allows for more flexible composition of behavior.
## ListModelMixin
diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md
index 38ae3d0a..e867a456 100644
--- a/docs/api-guide/permissions.md
+++ b/docs/api-guide/permissions.md
@@ -114,7 +114,7 @@ This permission is suitable if you want to your API to allow read permissions to
## DjangoModelPermissions
-This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. When applied to a view that has a `.model` property, authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned.
+This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. This permission must only be applied to views that has a `.queryset` property set. Authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned.
* `POST` requests require the user to have the `add` permission on the model.
* `PUT` and `PATCH` requests require the user to have the `change` permission on the model.
@@ -124,6 +124,12 @@ The default behaviour can also be overridden to support custom model permissions
To use custom model permissions, override `DjangoModelPermissions` and set the `.perms_map` property. Refer to the source code for details.
+#### Using with views that do not include a `queryset` attribute.
+
+If you're using this permission with a view that uses an overridden `get_queryset()` method there may not be a `queryset` attribute on the view. In this case we suggest also marking the view with a sential queryset, so that this class can determine the required permissions. For example:
+
+ queryset = User.objects.none() # Required for DjangoModelPermissions
+
## DjangoModelPermissionsOrAnonReadOnly
Similar to `DjangoModelPermissions`, but also allows unauthenticated users to have read-only access to the API.
@@ -132,7 +138,7 @@ Similar to `DjangoModelPermissions`, but also allows unauthenticated users to ha
This permission class ties into Django's standard [object permissions framework][objectpermissions] that allows per-object permissions on models. In order to use this permission class, you'll also need to add a permission backend that supports object-level permissions, such as [django-guardian][guardian].
-When applied to a view that has a `.model` property, authorization will only be granted if the user *is authenticated* and has the *relevant per-object permissions* and *relevant model permissions* assigned.
+As with `DjangoModelPermissions`, this permission must only be applied to views that have a `.queryset` property. Authorization will only be granted if the user *is authenticated* and has the *relevant per-object permissions* and *relevant model permissions* assigned.
* `POST` requests require the user to have the `add` permission on the model instance.
* `PUT` and `PATCH` requests require the user to have the `change` permission on the model instance.
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index 7a3429bf..20eed70d 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -444,6 +444,11 @@ Comma-separated values are a plain-text tabular data format, that can be easily
[djangorestframework-camel-case] provides camel case JSON renderers and parsers for REST framework. This allows serializers to use Python-style underscored field names, but be exposed in the API as Javascript-style camel case field names. It is maintained by [Vitaly Babiy][vbabiy].
+## Pandas (CSV, Excel, PNG)
+
+[Django REST Pandas] provides a serializer and renderers that support additional data processing and output via the [Pandas] DataFrame API. Django REST Pandas includes renderers for Pandas-style CSV files, Excel workbooks (both `.xls` and `.xlsx`), and a number of [other formats]. It is maintained by [S. Andrew Sheppard][sheppard] as part of the [wq Project][wq].
+
+
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
[conneg]: content-negotiation.md
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
@@ -466,4 +471,9 @@ Comma-separated values are a plain-text tabular data format, that can be easily
[ultrajson]: https://github.com/esnme/ultrajson
[hzy]: https://github.com/hzy
[drf-ujson-renderer]: https://github.com/gizmag/drf-ujson-renderer
-[djangorestframework-camel-case]: https://github.com/vbabiy/djangorestframework-camel-case \ No newline at end of file
+[djangorestframework-camel-case]: https://github.com/vbabiy/djangorestframework-camel-case
+[Django REST Pandas]: https://github.com/wq/django-rest-pandas
+[Pandas]: http://pandas.pydata.org/
+[other formats]: https://github.com/wq/django-rest-pandas#supported-formats
+[sheppard]: https://github.com/sheppard
+[wq]: https://github.com/wq
diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md
index 2d760ca4..61a476b8 100644
--- a/docs/api-guide/routers.md
+++ b/docs/api-guide/routers.md
@@ -41,9 +41,9 @@ The example above would generate the following URL patterns:
**Note**: The `base_name` argument is used to specify the initial part of the view name pattern. In the example above, that's the `user` or `account` part.
-Typically you won't *need* to specify the `base-name` argument, but if you have a viewset where you've defined a custom `get_queryset` method, then the viewset may not have any `.model` or `.queryset` attribute set. If you try to register that viewset you'll see an error like this:
+Typically you won't *need* to specify the `base-name` argument, but if you have a viewset where you've defined a custom `get_queryset` method, then the viewset may not have a `.queryset` attribute set. If you try to register that viewset you'll see an error like this:
- 'base_name' argument not specified, and could not automatically determine the name from the viewset, as it does not have a '.model' or '.queryset' attribute.
+ 'base_name' argument not specified, and could not automatically determine the name from the viewset, as it does not have a '.queryset' attribute.
This means you'll need to explicitly set the `base_name` argument when registering the viewset, as it could not be automatically determined from the model name.
diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md
index 29b7851b..a3694510 100644
--- a/docs/api-guide/serializers.md
+++ b/docs/api-guide/serializers.md
@@ -594,7 +594,13 @@ The [django-rest-framework-mongoengine][mongoengine] package provides a `MongoEn
The [django-rest-framework-gis][django-rest-framework-gis] package provides a `GeoFeatureModelSerializer` serializer class that supports GeoJSON both for read and write operations.
+## HStoreSerializer
+
+The [django-rest-framework-hstore][django-rest-framework-hstore] package provides an `HStoreSerializer` to support [django-hstore][django-hstore] `DictionaryField` model field and its `schema-mode` feature.
+
[cite]: https://groups.google.com/d/topic/django-users/sVFaOfQi4wY/discussion
[relations]: relations.md
[mongoengine]: https://github.com/umutbozkurt/django-rest-framework-mongoengine
[django-rest-framework-gis]: https://github.com/djangonauts/django-rest-framework-gis
+[django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore
+[django-hstore]: https://github.com/djangonauts/django-hstore
diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md
index 8bde4d87..27a09163 100644
--- a/docs/api-guide/settings.md
+++ b/docs/api-guide/settings.md
@@ -100,12 +100,6 @@ Default: `'rest_framework.negotiation.DefaultContentNegotiation'`
*The following settings control the behavior of the generic class based views.*
-#### DEFAULT_MODEL_SERIALIZER_CLASS
-
-A class that determines the default type of model serializer that should be used by a generic view if `model` is specified, but `serializer_class` is not provided.
-
-Default: `'rest_framework.serializers.ModelSerializer'`
-
#### DEFAULT_PAGINATION_SERIALIZER_CLASS
A class the determines the default serialization style for paginated responses.
diff --git a/docs/img/sponsors/2-wusawork.png b/docs/img/sponsors/2-wusawork.png
new file mode 100644
index 00000000..5834729b
--- /dev/null
+++ b/docs/img/sponsors/2-wusawork.png
Binary files differ
diff --git a/docs/index.md b/docs/index.md
index 83e30a69..b18b71d2 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -49,7 +49,7 @@ Some reasons you might want to use REST framework:
REST framework requires the following:
-* Python (2.6.5+, 2.7, 3.2, 3.3)
+* Python (2.6.5+, 2.7, 3.2, 3.3, 3.4)
* Django (1.4.2+, 1.5, 1.6, 1.7)
The following packages are optional:
@@ -85,10 +85,10 @@ Add `'rest_framework'` to your `INSTALLED_APPS` setting.
If you're intending to use the browsable API you'll probably also want to add REST framework's login and logout views. Add the following to your root `urls.py` file.
- urlpatterns = patterns('',
+ urlpatterns = [
...
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
- )
+ ]
Note that the URL path can be whatever you want, but you must include `'rest_framework.urls'` with the `'rest_framework'` namespace.
@@ -96,16 +96,11 @@ Note that the URL path can be whatever you want, but you must include `'rest_fra
Let's take a look at a quick example of using REST framework to build a simple model-backed API.
-We'll create a read-write API for accessing users and groups.
+We'll create a read-write API for accessing information on the users of our project.
Any global settings for a REST framework API are kept in a single configuration dictionary named `REST_FRAMEWORK`. Start off by adding the following to your `settings.py` module:
REST_FRAMEWORK = {
- # Use hyperlinked styles by default.
- # Only used if the `serializer_class` attribute is not set on a view.
- 'DEFAULT_MODEL_SERIALIZER_CLASS':
- 'rest_framework.serializers.HyperlinkedModelSerializer',
-
# Use Django's standard `django.contrib.auth` permissions,
# or allow read-only access for unauthenticated users.
'DEFAULT_PERMISSION_CLASSES': [
@@ -118,34 +113,37 @@ Don't forget to make sure you've also added `rest_framework` to your `INSTALLED_
We're ready to create our API now.
Here's our project's root `urls.py` module:
- from django.conf.urls import url, patterns, include
- from django.contrib.auth.models import User, Group
- from rest_framework import viewsets, routers
+ from django.conf.urls import url, include
+ from django.contrib.auth.models import User
+ from rest_framework import routers, serializers, viewsets
+
+ # Serializers define the API representation.
+ class UserSerializer(serializers.HyperlinkedModelSerializer):
+ class Meta:
+ model = User
+ fields = ('url', 'username', 'email', 'is_staff')
# ViewSets define the view behavior.
class UserViewSet(viewsets.ModelViewSet):
- model = User
-
- class GroupViewSet(viewsets.ModelViewSet):
- model = Group
-
+ queryset = User.objects.all()
+ serializer_class = UserSerializer
# Routers provide an easy way of automatically determining the URL conf.
router = routers.DefaultRouter()
router.register(r'users', UserViewSet)
- router.register(r'groups', GroupViewSet)
-
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browseable API.
- urlpatterns = patterns('',
+ urlpatterns = [
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
- )
+ ]
+
+You can now open the API in your browser at [http://127.0.0.1:8000/](http://127.0.0.1:8000/), and view your new 'users' API. If you use the login control in the top right corner you'll also be able to add, create and delete users from the system.
## Quickstart
-Can't wait to get started? The [quickstart guide][quickstart] is the fastest way to get up and running, and building APIs with REST framework.
+Can't wait to get started? The [quickstart guide][quickstart] is the fastest way to get up and running, and building APIs with REST framework.
## Tutorial
@@ -197,10 +195,12 @@ General guides to using REST framework.
* [Browser enhancements][browser-enhancements]
* [The Browsable API][browsableapi]
* [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas]
+* [Third Party Resources][third-party-resources]
* [Contributing to REST framework][contributing]
* [2.0 Announcement][rest-framework-2-announcement]
* [2.2 Announcement][2.2-announcement]
* [2.3 Announcement][2.3-announcement]
+* [2.4 Announcement][2.4-announcement]
* [Kickstarter Announcement][kickstarter-announcement]
* [Release Notes][release-notes]
* [Credits][credits]
@@ -313,10 +313,12 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[browsableapi]: topics/browsable-api.md
[rest-hypermedia-hateoas]: topics/rest-hypermedia-hateoas.md
[contributing]: topics/contributing.md
+[third-party-resources]: topics/third-party-resources.md
[rest-framework-2-announcement]: topics/rest-framework-2-announcement.md
[2.2-announcement]: topics/2.2-announcement.md
[2.3-announcement]: topics/2.3-announcement.md
-[kickstarter-announcement]: topics/kickstarter-announcement.md
+[2.4-announcement]: topics/2.4-announcement.md
+[kickstarter-announcement]: topics/kickstarter-announcement.md
[release-notes]: topics/release-notes.md
[credits]: topics/credits.md
diff --git a/docs/template.html b/docs/template.html
index ac225679..bb3ae221 100644
--- a/docs/template.html
+++ b/docs/template.html
@@ -117,10 +117,12 @@ a.fusion-poweredby {
<li><a href="{{ base_url }}/topics/browser-enhancements{{ suffix }}">Browser enhancements</a></li>
<li><a href="{{ base_url }}/topics/browsable-api{{ suffix }}">The Browsable API</a></li>
<li><a href="{{ base_url }}/topics/rest-hypermedia-hateoas{{ suffix }}">REST, Hypermedia & HATEOAS</a></li>
+ <li><a href="{{ base_url }}/topics/third-party-resources{{ suffix }}">Third Party Resources</a></li>
<li><a href="{{ base_url }}/topics/contributing{{ suffix }}">Contributing to REST framework</a></li>
<li><a href="{{ base_url }}/topics/rest-framework-2-announcement{{ suffix }}">2.0 Announcement</a></li>
<li><a href="{{ base_url }}/topics/2.2-announcement{{ suffix }}">2.2 Announcement</a></li>
<li><a href="{{ base_url }}/topics/2.3-announcement{{ suffix }}">2.3 Announcement</a></li>
+ <li><a href="{{ base_url }}/topics/2.4-announcement{{ suffix }}">2.4 Announcement</a></li>
<li><a href="{{ base_url }}/topics/kickstarter-announcement{{ suffix }}">Kickstarter Announcement</a></li>
<li><a href="{{ base_url }}/topics/release-notes{{ suffix }}">Release Notes</a></li>
<li><a href="{{ base_url }}/topics/credits{{ suffix }}">Credits</a></li>
diff --git a/docs/topics/2.3-announcement.md b/docs/topics/2.3-announcement.md
index ba435145..7c800afa 100644
--- a/docs/topics/2.3-announcement.md
+++ b/docs/topics/2.3-announcement.md
@@ -15,7 +15,7 @@ As an example of just how simple REST framework APIs can now be, here's an API w
"""
A REST framework API for viewing and editing users and groups.
"""
- from django.conf.urls.defaults import url, patterns, include
+ from django.conf.urls.defaults import url, include
from django.contrib.auth.models import User, Group
from rest_framework import viewsets, routers
@@ -36,10 +36,10 @@ As an example of just how simple REST framework APIs can now be, here's an API w
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browseable API.
- urlpatterns = patterns('',
+ urlpatterns = [
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
- )
+ ]
The best place to get started with ViewSets and Routers is to take a look at the [newest section in the tutorial][part-6], which demonstrates their usage.
diff --git a/docs/topics/2.4-accouncement.md b/docs/topics/2.4-announcement.md
index d8e264ff..8e4f3bb2 100644
--- a/docs/topics/2.4-accouncement.md
+++ b/docs/topics/2.4-announcement.md
@@ -15,6 +15,18 @@ The optional authtoken application now includes support for *both* Django 1.7 sc
**If you are using authtoken, and you want to continue using `south`, you must upgrade your `south` package to version 1.0.**
+## Deprecation of `.model` view attribute
+
+The `.model` attribute on view classes is an optional shortcut for either or both of `.serializer_class` and `.queryset`. Its usage results in more implicit, less obvious behavior.
+
+The documentation has previously stated that usage of the more explicit style is prefered, and we're now taking that one step further and deprecating the usage of the `.model` shortcut.
+
+Doing so will mean that there are cases of API code where you'll now need to include a serializer class where you previously were just using the `.model` shortcut. However we firmly believe that it is the right trade-off to make.
+
+Removing the shortcut takes away an unneccessary layer of abstraction, and makes your codebase more explicit without any significant extra complexity. It also results in better consistency, as there's now only one way to set the serializer class and queryset attributes for the view, instead of two.
+
+The `DEFAULT_MODEL_SERIALIZER_CLASS` API setting is now also deprecated.
+
## Updated test runner
We now have a new test runner for developing against the project,, that uses the excellent [py.test](http://pytest.org) library.
@@ -116,7 +128,7 @@ There are also a number of other features and bugfixes as [listed in the release
Smarter [client IP identification for throttling][client-ip-identification], with the addition of the `NUM_PROXIES` setting.
-Added the standardized `Retry-After` header to throttled responses, as per [RFC 6585](http://tools.ietf.org/html/rfc6585). This should now be used in preference to the custom `X-Trottle-Wait-Seconds` header which will be fully deprecated in 3.0.
+Added the standardized `Retry-After` header to throttled responses, as per [RFC 6585](http://tools.ietf.org/html/rfc6585). This should now be used in preference to the custom `X-Throttle-Wait-Seconds` header which will be fully deprecated in 3.0.
## Deprecations
@@ -151,10 +163,10 @@ The next planned release will be 3.0, featuring an improved and simplified seria
Once again, many thanks to all the generous [backers and sponsors][kickstarter-sponsors] who've helped make this possible!
[lts-releases]: https://docs.djangoproject.com/en/dev/internals/release-process/#long-term-support-lts-releases
-[2-4-release-notes]: ./topics/release-notes/#240
-[view-name-and-description-settings]: ../api-guide/settings/#view-names-and-descriptions
-[client-ip-identification]: ../api-guide/throttling/#how-clients-are-identified
-[2-3-announcement]: ./topics/2.3-announcement
+[2-4-release-notes]: release-notes#240
+[view-name-and-description-settings]: ../api-guide/settings#view-names-and-descriptions
+[client-ip-identification]: ../api-guide/throttling#how-clients-are-identified
+[2-3-announcement]: 2.3-announcement
[github-labels]: https://github.com/tomchristie/django-rest-framework/issues
[github-milestones]: https://github.com/tomchristie/django-rest-framework/milestones
-[kickstarter-sponsors]: ./topics/kickstarter-announcement/#sponsors
+[kickstarter-sponsors]: kickstarter-announcement#sponsors
diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md
index 3400bc8f..4fafb1b1 100644
--- a/docs/topics/contributing.md
+++ b/docs/topics/contributing.md
@@ -210,7 +210,9 @@ We recommend the [`django-reusable-app`][django-reusable-app] template as a good
## Linking to your package
-Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main REST framework documentation.
+Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main REST framework documentation. You can add your package under **Third party packages** of the API Guide section that best applies, like [Authentication][authentication] or [Permissions][permissions]. You can also link your package under the [Third Party Resources][third-party-resources] section.
+
+We also suggest adding it to the [REST Framework][rest-framework-grid] grid on Django Packages.
[cite]: http://www.w3.org/People/Berners-Lee/FAQ.html
[code-of-conduct]: https://www.djangoproject.com/conduct/
@@ -225,3 +227,7 @@ Once your package is decently documented and available on PyPI open a pull reque
[docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs
[mou]: http://mouapp.com/
[django-reusable-app]: https://github.com/dabapps/django-reusable-app
+[authentication]: ../api-guide/authentication.md
+[permissions]: ../api-guide/permissions.md
+[third-party-resources]: third-party-resources.md
+[rest-framework-grid]: https://www.djangopackages.com/grids/g/django-rest-framework/
diff --git a/docs/topics/kickstarter-announcement.md b/docs/topics/kickstarter-announcement.md
index 6d091064..7d1f6d0e 100644
--- a/docs/topics/kickstarter-announcement.md
+++ b/docs/topics/kickstarter-announcement.md
@@ -82,7 +82,7 @@ Our gold sponsors include companies large and small. Many thanks for their signi
<li><a href="https://opbeat.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-opbeat.png);">Opbeat</a></li>
<li><a href="https://koordinates.com" rel="nofollow" style="background-image:url(../img/sponsors/2-koordinates.png);">Koordinates</a></li>
<li><a href="http://pulsecode.ca" rel="nofollow" style="background-image:url(../img/sponsors/2-pulsecode.png);">Pulsecode Inc.</a></li>
-<li><a href="http://singinghorsestudio.com" rel="nofollow" style="background-image:url(../img/sponsors/2-singing-horse.png);">Singing Horse Studio. Ltd.</a></li>
+<li><a href="http://singinghorsestudio.com" rel="nofollow" style="background-image:url(../img/sponsors/2-singing-horse.png);">Singing Horse Studio Ltd.</a></li>
<li><a href="https://www.heroku.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-heroku.png);">Heroku</a></li>
<li><a href="https://www.galileo-press.de/" rel="nofollow" style="background-image:url(../img/sponsors/2-galileo_press.png);">Galileo Press</a></li>
<li><a href="http://www.securitycompass.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-security_compass.png);">Security Compass</a></li>
@@ -92,13 +92,12 @@ Our gold sponsors include companies large and small. Many thanks for their signi
<li><a href="http://crypticocorp.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-cryptico.png);">Cryptico Corp</a></li>
<li><a href="http://www.nexthub.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-nexthub.png);">NextHub</a></li>
<li><a href="https://www.compile.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-compile.png);">Compile</a></li>
+<li><a href="http://wusawork.org" rel="nofollow" style="background-image:url(../img/sponsors/2-wusawork.png);">WusaWork</a></li>
<li><a href="http://envisionlinux.org/blog" rel="nofollow">Envision Linux</a></li>
</ul>
<div style="clear: both; padding-bottom: 40px;"></div>
-**Individual backers**: Simon Haugk.
-
---
### Silver sponsors
@@ -145,7 +144,7 @@ The serious financial contribution that our silver sponsors have made is very mu
<div style="clear: both; padding-bottom: 40px;"></div>
-**Individual backers**: Paul Hallet, <a href="http://www.paulwhippconsulting.com/">Paul Whipp</a>, Dylan Roy, Jannis Leidel, <a href="https://linovia.com/en/">Xavier Ordoquy</a>, <a href="http://spielmannsolutions.com/">Johannes Spielmann</a>, <a href="http://brooklynhacker.com/">Rob Spectre</a>, <a href="http://chrisheisel.com/">Chris Heisel</a>, Marwan Alsabbagh, Haris Ali, Tuomas Toivonen.
+**Individual backers**: Paul Hallett, <a href="http://www.paulwhippconsulting.com/">Paul Whipp</a>, Dylan Roy, Jannis Leidel, <a href="https://linovia.com/en/">Xavier Ordoquy</a>, <a href="http://spielmannsolutions.com/">Johannes Spielmann</a>, <a href="http://brooklynhacker.com/">Rob Spectre</a>, <a href="http://chrisheisel.com/">Chris Heisel</a>, Marwan Alsabbagh, Haris Ali, Tuomas Toivonen.
---
diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md
index a2b4782f..16589f3b 100644
--- a/docs/topics/release-notes.md
+++ b/docs/topics/release-notes.md
@@ -10,7 +10,7 @@ Minor version numbers (0.0.x) are used for changes that are API compatible. You
Medium version numbers (0.x.0) may include API changes, in line with the [deprecation policy][deprecation-policy]. You should read the release notes carefully before upgrading between medium point releases.
-Major version numbers (x.0.0) are reserved for substantial project milestones. No major point releases are currently planned.
+Major version numbers (x.0.0) are reserved for substantial project milestones.
## Deprecation policy
@@ -40,14 +40,45 @@ You can determine your currently installed version using `pip freeze`:
## 2.4.x series
+### 2.4.3
+
+**Date**: [19th September 2014](https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%222.4.3+Release%22+).
+
+* Support translatable view docstrings being displayed in the browsable API.
+* Support [encoded `filename*`][rfc-6266] in raw file uploads with `FileUploadParser`.
+* Allow routers to support viewsets that don't include any list routes or that don't include any detail routes.
+* Don't render an empty login control in browsable API if `login` view is not included.
+* CSRF exemption performed in `.as_view()` to prevent accidental omission if overriding `.dispatch()`.
+* Login on browsable API now displays validation errors.
+* Bugfix: Fix migration in `authtoken` application.
+* Bugfix: Allow selection of integer keys in nested choices.
+* Bugfix: Return `None` instead of `'None'` in `CharField` with `allow_none=True`.
+* Bugfix: Ensure custom model fields map to equivelent serializer fields more reliably.
+* Bugfix: `DjangoFilterBackend` no longer quietly changes queryset ordering.
+
+### 2.4.2
+
+**Date**: [3rd September 2014](https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%222.4.2+Release%22+).
+
+* Bugfix: Fix broken pagination for 2.4.x series.
+
+### 2.4.1
+
+**Date**: [1st September 2014](https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%222.4.1+Release%22+).
+
+* Bugfix: Fix broken login template for browsable API.
+
### 2.4.0
+**Date**: [29th August 2014](https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%222.4.0+Release%22+).
+
**Django version requirements**: The lowest supported version of Django is now 1.4.2.
**South version requirements**: This note applies to any users using the optional `authtoken` application, which includes an associated database migration. You must now *either* upgrade your `south` package to version 1.0, *or* instead use the built-in migration support available with Django 1.7.
* Added compatibility with Django 1.7's database migration support.
* New test runner, using `py.test`.
+* Deprecated `.model` view attribute in favor of explicit `.queryset` and `.serializer_class` attributes. The `DEFAULT_MODEL_SERIALIZER_CLASS` setting is also deprecated.
* `@detail_route` and `@list_route` decorators replace `@action` and `@link`.
* Support customizable view name and description functions, using the `VIEW_NAME_FUNCTION` and `VIEW_DESCRIPTION_FUNCTION` settings.
* Added `NUM_PROXIES` setting for smarter client IP identification.
@@ -702,3 +733,4 @@ This change will not affect user code, so long as it's following the recommended
[2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion
[announcement]: rest-framework-2-announcement.md
[#582]: https://github.com/tomchristie/django-rest-framework/issues/582
+[rfc-6266]: http://tools.ietf.org/html/rfc6266#section-4.3
diff --git a/docs/topics/third-party-resources.md b/docs/topics/third-party-resources.md
new file mode 100644
index 00000000..0317dd64
--- /dev/null
+++ b/docs/topics/third-party-resources.md
@@ -0,0 +1,92 @@
+# Third Party Resources
+
+Django REST Framework has a growing community of developers, packages, and resources.
+
+Check out a grid detailing all the packages and ecosystem around Django REST Framework at [Django Packages](https://www.djangopackages.com/grids/g/django-rest-framework/).
+
+To submit new content, [open an issue](https://github.com/tomchristie/django-rest-framework/issues/new) or [create a pull request](https://github.com/tomchristie/django-rest-framework/).
+
+## Libraries and Extensions
+
+### Authentication
+
+* [djangorestframework-digestauth](https://github.com/juanriaza/django-rest-framework-digestauth) - Provides Digest Access Authentication support.
+* [django-oauth-toolkit](https://github.com/evonove/django-oauth-toolkit) - Provides OAuth 2.0 support.
+* [doac](https://github.com/Rediker-Software/doac) - Provides OAuth 2.0 support.
+* [djangorestframework-jwt](https://github.com/GetBlimp/django-rest-framework-jwt) - Provides JSON Web Token Authentication support.
+* [hawkrest](https://github.com/kumar303/hawkrest) - Provides Hawk HTTP Authorization.
+* [djangorestframework-httpsignature](https://github.com/etoccalino/django-rest-framework-httpsignature) - Provides an easy to use HTTP Signature Authentication mechanism.
+* [djoser](https://github.com/sunscrapers/djoser) - Provides a set of views to handle basic actions such as registration, login, logout, password reset and account activation.
+
+### Permissions
+
+* [drf-any-permissions](https://github.com/kevin-brown/drf-any-permissions) - Provides alternative permission handling.
+* [djangorestframework-composed-permissions](https://github.com/niwibe/djangorestframework-composed-permissions) - Provides a simple way to define complex permissions.
+* [rest_condition](https://github.com/caxap/rest_condition) - Another extension for building complex permissions in a simple and convenient way.
+
+### Serializers
+
+* [django-rest-framework-mongoengine](https://github.com/umutbozkurt/django-rest-framework-mongoengine) - Serializer class that supports using MongoDB as the storage layer for Django REST framework.
+* [djangorestframework-gis](https://github.com/djangonauts/django-rest-framework-gis) - Geographic add-ons
+* [djangorestframework-hstore](https://github.com/djangonauts/django-rest-framework-hstore) - Serializer class to support django-hstore DictionaryField model field and its schema-mode feature.
+
+### Serializer fields
+
+* [drf-compound-fields](https://github.com/estebistec/drf-compound-fields) - Provides "compound" serializer fields, such as lists of simple values.
+* [django-extra-fields](https://github.com/Hipo/drf-extra-fields) - Provides extra serializer fields.
+
+### Views
+
+* [djangorestframework-bulk](https://github.com/miki725/django-rest-framework-bulk) - Implements generic view mixins as well as some common concrete generic views to allow to apply bulk operations via API requests.
+
+### Routers
+
+* [drf-nested-routers](https://github.com/alanjds/drf-nested-routers) - Provides routers and relationship fields for working with nested resources.
+* [wq.db.rest](http://wq.io/docs/about-rest) - Provides an admin-style model registration API with reasonable default URLs and viewsets.
+
+### Parsers
+
+* [djangorestframework-msgpack](https://github.com/juanriaza/django-rest-framework-msgpack) - Provides MessagePack renderer and parser support.
+* [djangorestframework-camel-case](https://github.com/vbabiy/djangorestframework-camel-case) - Provides camel case JSON renderers and parsers.
+
+### Renderers
+
+* [djangorestframework-csv](https://github.com/mjumbewu/django-rest-framework-csv) - Provides CSV renderer support.
+* [drf_ujson](https://github.com/gizmag/drf-ujson-renderer) - Implements JSON rendering using the UJSON package.
+* [Django REST Pandas](https://github.com/wq/django-rest-pandas) - Pandas DataFrame-powered renderers including Excel, CSV, and SVG formats.
+
+### Filtering
+
+* [djangorestframework-chain](https://github.com/philipn/django-rest-framework-chain) - Allows arbitrary chaining of both relations and lookup filters.
+
+### Misc
+
+* [djangorestrelationalhyperlink](https://github.com/fredkingham/django_rest_model_hyperlink_serializers_project) - A hyperlinked serialiser that can can be used to alter relationships via hyperlinks, but otherwise like a hyperlink model serializer.
+* [django-rest-swagger](https://github.com/marcgibbons/django-rest-swagger) - An API documentation generator for Swagger UI.
+* [django-rest-framework-proxy ](https://github.com/eofs/django-rest-framework-proxy) - Proxy to redirect incoming request to another API server.
+* [gaiarestframework](https://github.com/AppsFuel/gaiarestframework) - Utils for django-rest-framewok
+* [drf-extensions](https://github.com/chibisov/drf-extensions) - A collection of custom extensions
+* [ember-data-django-rest-adapter](https://github.com/toranb/ember-data-django-rest-adapter) - An ember-data adapter
+
+## Tutorials
+
+* [Beginner's Guide to the Django Rest Framework](http://code.tutsplus.com/tutorials/beginners-guide-to-the-django-rest-framework--cms-19786)
+* [Getting Started with Django Rest Framework and AngularJS](http://blog.kevinastone.com/getting-started-with-django-rest-framework-and-angularjs.html)
+* [End to end web app with Django-Rest-Framework & AngularJS](http://blog.mourafiq.com/post/55034504632/end-to-end-web-app-with-django-rest-framework)
+* [Start Your API - django-rest-framework part 1](https://godjango.com/41-start-your-api-django-rest-framework-part-1/)
+* [Permissions & Authentication - django-rest-framework part 2](https://godjango.com/43-permissions-authentication-django-rest-framework-part-2/)
+* [ViewSets and Routers - django-rest-framework part 3](https://godjango.com/45-viewsets-and-routers-django-rest-framework-part-3/)
+* [Django Rest Framework User Endpoint](http://richardtier.com/2014/02/25/django-rest-framework-user-endpoint/)
+* [Check credentials using Django Rest Framework](http://richardtier.com/2014/03/06/110/)
+
+## Videos
+
+* [Ember and Django Part 1 (Video)](http://www.neckbeardrepublic.com/screencasts/ember-and-django-part-1)
+* [Django Rest Framework Part 1 (Video)](http://www.neckbeardrepublic.com/screencasts/django-rest-framework-part-1)
+* [Pyowa July 2013 - Django Rest Framework (Video)](http://www.youtube.com/watch?v=E1ZrehVxpBo)
+* [django-rest-framework and angularjs (Video)](http://www.youtube.com/watch?v=Q8FRBGTJ020)
+
+## Articles
+
+* [Web API performance: profiling Django REST framework](http://dabapps.com/blog/api-performance-profiling-django-rest-framework/)
+* [API Development with Django and Django REST Framework](https://bnotions.com/api-development-with-django-and-django-rest-framework/)
diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md
index 96214f5b..b0565d91 100644
--- a/docs/tutorial/1-serialization.md
+++ b/docs/tutorial/1-serialization.md
@@ -64,9 +64,9 @@ We'll also need to add our new `snippets` app and the `rest_framework` app to `I
We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet app's URLs.
- urlpatterns = patterns('',
+ urlpatterns = [
url(r'^', include('snippets.urls')),
- )
+ ]
Okay, we're ready to roll.
@@ -297,11 +297,12 @@ We'll also need a view which corresponds to an individual snippet, and can be us
Finally we need to wire these views up. Create the `snippets/urls.py` file:
from django.conf.urls import patterns, url
+ from snippets import views
- urlpatterns = patterns('snippets.views',
- url(r'^snippets/$', 'snippet_list'),
- url(r'^snippets/(?P<pk>[0-9]+)/$', 'snippet_detail'),
- )
+ urlpatterns = [
+ url(r'^snippets/$', views.snippet_list),
+ url(r'^snippets/(?P<pk>[0-9]+)/$', views.snippet_detail),
+ ]
It's worth noting that there are a couple of edge cases we're not dealing with properly at the moment. If we send malformed `json`, or if a request is made with a method that the view doesn't handle, then we'll end up with a 500 "server error" response. Still, this'll do for now.
diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md
index e70bbbfc..136b0135 100644
--- a/docs/tutorial/2-requests-and-responses.md
+++ b/docs/tutorial/2-requests-and-responses.md
@@ -110,11 +110,12 @@ Now update the `urls.py` file slightly, to append a set of `format_suffix_patter
from django.conf.urls import patterns, url
from rest_framework.urlpatterns import format_suffix_patterns
+ from snippets import views
- urlpatterns = patterns('snippets.views',
- url(r'^snippets/$', 'snippet_list'),
- url(r'^snippets/(?P<pk>[0-9]+)$', 'snippet_detail'),
- )
+ urlpatterns = [
+ url(r'^snippets/$', views.snippet_list),
+ url(r'^snippets/(?P<pk>[0-9]+)$', views.snippet_detail),
+ ]
urlpatterns = format_suffix_patterns(urlpatterns)
diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md
index e04072ca..382f078a 100644
--- a/docs/tutorial/3-class-based-views.md
+++ b/docs/tutorial/3-class-based-views.md
@@ -68,10 +68,10 @@ We'll also need to refactor our `urls.py` slightly now we're using class based v
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views
- urlpatterns = patterns('',
+ urlpatterns = [
url(r'^snippets/$', views.SnippetList.as_view()),
url(r'^snippets/(?P<pk>[0-9]+)/$', views.SnippetDetail.as_view()),
- )
+ ]
urlpatterns = format_suffix_patterns(urlpatterns)
diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md
index 74ad9a55..9120e254 100644
--- a/docs/tutorial/4-authentication-and-permissions.md
+++ b/docs/tutorial/4-authentication-and-permissions.md
@@ -137,10 +137,10 @@ Add the following import at the top of the file:
And, at the end of the file, add a pattern to include the login and logout views for the browsable API.
- urlpatterns += patterns('',
+ urlpatterns += [
url(r'^api-auth/', include('rest_framework.urls',
namespace='rest_framework')),
- )
+ ]
The `r'^api-auth/'` part of pattern can actually be whatever URL you want to use. The only restriction is that the included urls must use the `'rest_framework'` namespace.
diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md
index 9c61fe3d..36473ce9 100644
--- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md
+++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md
@@ -108,8 +108,8 @@ If we're going to have a hyperlinked API, we need to make sure we name our URL p
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',
- url(r'^$', 'api_root'),
+ urlpatterns = format_suffix_patterns([
+ url(r'^$', views.api_root),
url(r'^snippets/$',
views.SnippetList.as_view(),
name='snippet-list'),
@@ -125,13 +125,13 @@ After adding all those names into our URLconf, our final `snippets/urls.py` file
url(r'^users/(?P<pk>[0-9]+)/$',
views.UserDetail.as_view(),
name='user-detail')
- ))
+ ])
# Login and logout views for the browsable API
- urlpatterns += patterns('',
+ urlpatterns += [
url(r'^api-auth/', include('rest_framework.urls',
namespace='rest_framework')),
- )
+ ]
## Adding pagination
diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md
index b2019520..cf37a260 100644
--- a/docs/tutorial/6-viewsets-and-routers.md
+++ b/docs/tutorial/6-viewsets-and-routers.md
@@ -87,14 +87,14 @@ Notice how we're creating multiple views from each `ViewSet` class, by binding t
Now that we've bound our resources into concrete views, we can register the views with the URL conf as usual.
- urlpatterns = format_suffix_patterns(patterns('snippets.views',
- url(r'^$', 'api_root'),
+ urlpatterns = format_suffix_patterns([
+ url(r'^$', api_root),
url(r'^snippets/$', snippet_list, name='snippet-list'),
url(r'^snippets/(?P<pk>[0-9]+)/$', snippet_detail, name='snippet-detail'),
url(r'^snippets/(?P<pk>[0-9]+)/highlight/$', snippet_highlight, name='snippet-highlight'),
url(r'^users/$', user_list, name='user-list'),
url(r'^users/(?P<pk>[0-9]+)/$', user_detail, name='user-detail')
- ))
+ ])
## Using Routers
@@ -102,7 +102,7 @@ Because we're using `ViewSet` classes rather than `View` classes, we actually do
Here's our re-wired `urls.py` file.
- from django.conf.urls import patterns, url, include
+ from django.conf.urls import url, include
from snippets import views
from rest_framework.routers import DefaultRouter
@@ -113,10 +113,10 @@ Here's our re-wired `urls.py` file.
# The API URLs are now determined automatically by the router.
# Additionally, we include the login URLs for the browseable API.
- urlpatterns = patterns('',
+ urlpatterns = [
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
- )
+ ]
Registering the viewsets with the router is similar to providing a urlpattern. We include two arguments - the URL prefix for the views, and the viewset itself.
diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md
index 98e5f439..813e9872 100644
--- a/docs/tutorial/quickstart.md
+++ b/docs/tutorial/quickstart.md
@@ -18,34 +18,23 @@ 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
-
-Next you'll need to get a database set up and synced. If you just want to use SQLite for now, then you'll want to edit your `tutorial/settings.py` module to include something like this:
-
- DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': 'database.sql',
- 'USER': '',
- 'PASSWORD': '',
- 'HOST': '',
- 'PORT': ''
- }
- }
+ # Set up a new project with a single application
+ django-admin.py startproject tutorial .
+ cd tutorial
+ django-admin.py startapp quickstart
+ cd ..
-The run `syncdb` like so:
+Now sync your database for the first time:
python manage.py syncdb
+Make sure to create an initial user named `admin` with a password of `password`. We'll authenticate as that user later in our example.
+
Once you've set up a database and got everything synced and ready to go, open up the app's directory and we'll get coding...
## Serializers
-First up we're going to define some serializers in `quickstart/serializers.py` that we'll use for our data representations.
+First up we're going to define some serializers. Let's create a new module named `tutorial/quickstart/serializers.py` that we'll use for our data representations.
from django.contrib.auth.models import User, Group
from rest_framework import serializers
@@ -66,11 +55,11 @@ Notice that we're using hyperlinked relations in this case, with `HyperlinkedMod
## Views
-Right, we'd better write some views then. Open `quickstart/views.py` and get typing.
+Right, we'd better write some views then. Open `tutorial/quickstart/views.py` and get typing.
from django.contrib.auth.models import User, Group
from rest_framework import viewsets
- from quickstart.serializers import UserSerializer, GroupSerializer
+ from tutorial.quickstart.serializers import UserSerializer, GroupSerializer
class UserViewSet(viewsets.ModelViewSet):
@@ -100,9 +89,9 @@ For trivial cases you can simply set a `model` attribute on the `ViewSet` class
Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
- from django.conf.urls import patterns, url, include
+ from django.conf.urls import url, include
from rest_framework import routers
- from quickstart import views
+ from tutorial.quickstart import views
router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
@@ -110,10 +99,10 @@ Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browseable API.
- urlpatterns = patterns('',
+ urlpatterns = [
url(r'^', include(router.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'))
- )
+ ]
Because we're using viewsets instead of views, we can automatically generate the URL conf for our API, by simply registering the viewsets with a router class.
@@ -172,6 +161,8 @@ Or directly through the browser...
![Quick start image][image]
+If you're working through the browser, make sure to login using the control in the top right corner.
+
Great, that was easy!
If you want to get a more in depth understanding of how REST framework fits together head on over to [the tutorial][tutorial], or start browsing the [API guide][guide].
diff --git a/mkdocs.py b/mkdocs.py
index adeb6053..25cb55e2 100755
--- a/mkdocs.py
+++ b/mkdocs.py
@@ -76,10 +76,12 @@ path_list = [
'topics/browser-enhancements.md',
'topics/browsable-api.md',
'topics/rest-hypermedia-hateoas.md',
+ 'topics/third-party-resources.md',
'topics/contributing.md',
'topics/rest-framework-2-announcement.md',
'topics/2.2-announcement.md',
'topics/2.3-announcement.md',
+ 'topics/2.4-announcement.md',
'topics/release-notes.md',
'topics/credits.md',
]
diff --git a/requirements.txt b/requirements.txt
index 730c1d07..8a698230 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1 @@
-Django>=1.3
+Django>=1.4.2
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index f30012b9..7f724c18 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -8,7 +8,7 @@ ______ _____ _____ _____ __
"""
__title__ = 'Django REST framework'
-__version__ = '2.3.14'
+__version__ = '2.4.3'
__author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause'
__copyright__ = 'Copyright 2011-2014 Tom Christie'
diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py
index 5721a869..f3fec05e 100644
--- a/rest_framework/authentication.py
+++ b/rest_framework/authentication.py
@@ -344,7 +344,7 @@ class OAuth2Authentication(BaseAuthentication):
user = token.user
if not user.is_active:
- msg = 'User inactive or deleted: %s' % user.username
+ msg = 'User inactive or deleted: %s' % user.get_username()
raise exceptions.AuthenticationFailed(msg)
return (user, token)
diff --git a/rest_framework/authtoken/migrations/0001_initial.py b/rest_framework/authtoken/migrations/0001_initial.py
index 2e5d6b47..769f6202 100644
--- a/rest_framework/authtoken/migrations/0001_initial.py
+++ b/rest_framework/authtoken/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# encoding: utf8
+# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
@@ -15,12 +15,11 @@ class Migration(migrations.Migration):
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')),
+ ('key', models.CharField(primary_key=True, serialize=False, max_length=40)),
('created', models.DateTimeField(auto_now_add=True)),
+ ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL, related_name='auth_token')),
],
options={
- 'abstract': False,
},
bases=(models.Model,),
),
diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py
index 99e99ae3..472e59ee 100644
--- a/rest_framework/authtoken/serializers.py
+++ b/rest_framework/authtoken/serializers.py
@@ -22,7 +22,7 @@ class AuthTokenSerializer(serializers.Serializer):
attrs['user'] = user
return attrs
else:
- msg = _('Unable to login with provided credentials.')
+ msg = _('Unable to log in with provided credentials.')
raise serializers.ValidationError(msg)
else:
msg = _('Must include "username" and "password"')
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 9d707c9b..c0253f86 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -475,8 +475,12 @@ class CharField(WritableField):
if isinstance(value, six.string_types):
return value
- if value is None and not self.allow_none:
- return ''
+ if value is None:
+ if not self.allow_none:
+ return ''
+ else:
+ # Return None explicitly because smart_text(None) == 'None'. See #1834 for details
+ return None
return smart_text(value)
@@ -559,7 +563,7 @@ class ChoiceField(WritableField):
if isinstance(v, (list, tuple)):
# This is an optgroup, so look inside the group for options
for k2, v2 in v:
- if value == smart_text(k2):
+ if value == smart_text(k2) or value == k2:
return True
else:
if value == smart_text(k) or value == k:
diff --git a/rest_framework/filters.py b/rest_framework/filters.py
index e2080013..c580f935 100644
--- a/rest_framework/filters.py
+++ b/rest_framework/filters.py
@@ -56,7 +56,6 @@ class DjangoFilterBackend(BaseFilterBackend):
class Meta:
model = queryset.model
fields = filter_fields
- order_by = True
return AutoFilterSet
return None
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 77deb8e4..a62da00b 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -252,6 +252,12 @@ class GenericAPIView(views.APIView):
if serializer_class is not None:
return serializer_class
+ warnings.warn(
+ 'The `.model` attribute on view classes is now deprecated in favor '
+ 'of the more explicit `serializer_class` and `queryset` attributes.',
+ DeprecationWarning, stacklevel=2
+ )
+
assert self.model is not None, \
"'%s' should either include a 'serializer_class' attribute, " \
"or use the 'model' attribute as a shortcut for " \
@@ -282,6 +288,11 @@ class GenericAPIView(views.APIView):
return self.queryset._clone()
if self.model is not None:
+ warnings.warn(
+ 'The `.model` attribute on view classes is now deprecated in favor '
+ 'of the more explicit `serializer_class` and `queryset` attributes.',
+ DeprecationWarning, stacklevel=2
+ )
return self.model._default_manager.all()
error_format = "'%s' must define 'queryset' or 'model'"
@@ -387,10 +398,11 @@ class GenericAPIView(views.APIView):
if method not in self.allowed_methods:
continue
- cloned_request = clone_request(request, method)
+ original_request = self.request
+ self.request = clone_request(request, method)
try:
# Test global permissions
- self.check_permissions(cloned_request)
+ self.check_permissions(self.request)
# Test object permissions
if method == 'PUT':
try:
@@ -408,6 +420,8 @@ class GenericAPIView(views.APIView):
# appropriate metadata about the fields that should be supplied.
serializer = self.get_serializer()
actions[method] = serializer.metadata()
+ finally:
+ self.request = original_request
if actions:
ret['actions'] = actions
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index d51ea929..1f5749f1 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -43,8 +43,9 @@ class DefaultObjectSerializer(serializers.Field):
as the default.
"""
- def __init__(self, source=None, context=None):
- # Note: Swallow context kwarg - only required for eg. ModelSerializer.
+ def __init__(self, source=None, many=None, context=None):
+ # Note: Swallow context and many kwargs - only required for
+ # eg. ModelSerializer.
super(DefaultObjectSerializer, self).__init__(source=source)
@@ -82,7 +83,9 @@ class BasePaginationSerializer(serializers.Serializer):
else:
context_kwarg = {}
- self.fields[results_field] = object_serializer(source='object_list', **context_kwarg)
+ self.fields[results_field] = object_serializer(source='object_list',
+ many=True,
+ **context_kwarg)
class PaginationSerializer(BasePaginationSerializer):
diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py
index aa4fd3f1..c287908d 100644
--- a/rest_framework/parsers.py
+++ b/rest_framework/parsers.py
@@ -11,7 +11,7 @@ from django.http import QueryDict
from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser
from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter
from django.utils import six
-from rest_framework.compat import etree, yaml, force_text
+from rest_framework.compat import etree, yaml, force_text, urlparse
from rest_framework.exceptions import ParseError
from rest_framework import renderers
import json
@@ -290,6 +290,22 @@ class FileUploadParser(BaseParser):
try:
meta = parser_context['request'].META
disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode('utf-8'))
- return force_text(disposition[1]['filename'])
+ filename_parm = disposition[1]
+ if 'filename*' in filename_parm:
+ return self.get_encoded_filename(filename_parm)
+ return force_text(filename_parm['filename'])
except (AttributeError, KeyError):
pass
+
+ def get_encoded_filename(self, filename_parm):
+ """
+ Handle encoded filenames per RFC6266. See also:
+ http://tools.ietf.org/html/rfc2231#section-4
+ """
+ encoded_filename = force_text(filename_parm['filename*'])
+ try:
+ charset, lang, filename = encoded_filename.split('\'', 2)
+ filename = urlparse.unquote(filename)
+ except (ValueError, LookupError):
+ filename = force_text(filename_parm['filename'])
+ return filename
diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py
index 6a1a0077..29f60d6d 100644
--- a/rest_framework/permissions.py
+++ b/rest_framework/permissions.py
@@ -108,6 +108,9 @@ class DjangoModelPermissions(BasePermission):
return [perm % kwargs for perm in self.perms_map[method]]
def has_permission(self, request, view):
+ # Note that `.model` attribute on views is deprecated, although we
+ # enforce the deprecation on the view `get_serializer_class()` and
+ # `get_queryset()` methods, rather than here.
model_cls = getattr(view, 'model', None)
queryset = getattr(view, 'queryset', None)
diff --git a/rest_framework/routers.py b/rest_framework/routers.py
index 406ebcf7..169e6e8b 100644
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -19,6 +19,8 @@ import itertools
from collections import namedtuple
from django.conf.urls import patterns, url
from django.core.exceptions import ImproperlyConfigured
+from django.core.urlresolvers import NoReverseMatch
+from django.utils.datastructures import SortedDict
from rest_framework import views
from rest_framework.response import Response
from rest_framework.reverse import reverse
@@ -128,6 +130,9 @@ class SimpleRouter(BaseRouter):
If `base_name` is not specified, attempt to automatically determine
it from the viewset.
"""
+ # Note that `.model` attribute on views is deprecated, although we
+ # enforce the deprecation on the view `get_serializer_class()` and
+ # `get_queryset()` methods, rather than here.
model_cls = getattr(viewset, 'model', None)
queryset = getattr(viewset, 'queryset', None)
if model_cls is None and queryset is not None:
@@ -135,7 +140,7 @@ class SimpleRouter(BaseRouter):
assert model_cls, '`base_name` argument not specified, and could ' \
'not automatically determine the name from the viewset, as ' \
- 'it does not have a `.model` or `.queryset` attribute.'
+ 'it does not have a `.queryset` attribute.'
return model_cls._meta.object_name.lower()
@@ -273,7 +278,7 @@ class DefaultRouter(SimpleRouter):
"""
Return a view to use as the API root.
"""
- api_root_dict = {}
+ api_root_dict = SortedDict()
list_name = self.routes[0].name
for prefix, viewset, basename in self.registry:
api_root_dict[prefix] = list_name.format(basename=basename)
@@ -281,10 +286,19 @@ class DefaultRouter(SimpleRouter):
class APIRoot(views.APIView):
_ignore_model_permissions = True
- def get(self, request, format=None):
- ret = {}
+ def get(self, request, *args, **kwargs):
+ ret = SortedDict()
for key, url_name in api_root_dict.items():
- ret[key] = reverse(url_name, request=request, format=format)
+ try:
+ ret[key] = reverse(
+ url_name,
+ request=request,
+ format=kwargs.get('format', None)
+ )
+ except NoReverseMatch:
+ # Don't bail out if eg. no list routes exist, only detail routes.
+ continue
+
return Response(ret)
return APIRoot.as_view()
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 27af7ef3..d24cb99d 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -625,6 +625,20 @@ class ModelSerializerOptions(SerializerOptions):
self.write_only_fields = getattr(meta, 'write_only_fields', ())
+def _get_class_mapping(mapping, obj):
+ """
+ Takes a dictionary with classes as keys, and an object.
+ Traverses the object's inheritance hierarchy in method
+ resolution order, and returns the first matching value
+ from the dictionary or None.
+
+ """
+ return next(
+ (mapping[cls] for cls in inspect.getmro(obj.__class__) if cls in mapping),
+ None
+ )
+
+
class ModelSerializer(Serializer):
"""
A serializer that deals with model instances and querysets.
@@ -899,15 +913,17 @@ class ModelSerializer(Serializer):
models.URLField: ['max_length'],
}
- if model_field.__class__ in attribute_dict:
- attributes = attribute_dict[model_field.__class__]
+ attributes = _get_class_mapping(attribute_dict, model_field)
+ if attributes:
for attribute in attributes:
kwargs.update({attribute: getattr(model_field, attribute)})
- try:
- return self.field_mapping[model_field.__class__](**kwargs)
- except KeyError:
- return ModelField(model_field=model_field, **kwargs)
+ serializer_field_class = _get_class_mapping(
+ self.field_mapping, model_field)
+
+ if serializer_field_class:
+ return serializer_field_class(**kwargs)
+ return ModelField(model_field=model_field, **kwargs)
def get_validation_exclusions(self, instance=None):
"""
diff --git a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
index 6bfb778c..6fa1e6cb 100644
--- a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
+++ b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
@@ -6,30 +6,30 @@ a single block in the template.
*/
-
.form-actions {
- background: transparent;
- border-top-color: transparent;
- padding-top: 0;
+ background: transparent;
+ border-top-color: transparent;
+ padding-top: 0;
}
.navbar-inverse .brand a {
- color: #999;
+ color: #999999;
}
.navbar-inverse .brand:hover a {
- color: white;
- text-decoration: none;
+ color: white;
+ text-decoration: none;
}
/* custom navigation styles */
-.wrapper .navbar{
+.navbar {
width: 100%;
- position: absolute;
+ position: fixed;
left: 0;
top: 0;
+ z-index: 3;
}
-.navbar .navbar-inner{
+.navbar .navbar-inner {
background: #2C2C2C;
color: white;
border: none;
@@ -37,25 +37,26 @@ a single block in the template.
border-radius: 0px;
}
-.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand:hover{
+.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand:hover {
color: white;
}
.nav-list > .active > a, .nav-list > .active > a:hover {
- background: #2c2c2c;
+ background: #2C2C2C;
}
-.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{
- color: #A30000;
+.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li {
+ color: #A30000;
}
-.navbar .navbar-inner .dropdown-menu li a:hover{
- background: #eeeeee;
- color: #c20000;
+
+.navbar .navbar-inner .dropdown-menu li a:hover {
+ background: #EEEEEE;
+ color: #C20000;
}
/*=== dabapps bootstrap styles ====*/
-html{
+html {
width:100%;
background: none;
}
@@ -65,121 +66,127 @@ body, .navbar .navbar-inner .container-fluid {
margin: 0 auto;
}
-body{
+body {
background: url("../img/grid.png") repeat-x;
background-attachment: fixed;
}
-#content{
- margin: 0;
+#content {
+ margin: 0;
+ padding-bottom: 60px;
}
/* sticky footer and footer */
html, body {
height: 100%;
}
+
.wrapper {
+ position: relative;
+ top: 0;
+ left: 0;
+ padding-top: 60px;
+ margin: -60px 0;
min-height: 100%;
- height: auto !important;
- height: 100%;
- margin: 0 auto -60px;
}
.form-switcher {
- margin-bottom: 0;
+ margin-bottom: 0;
}
.well {
- -webkit-box-shadow: none;
- -moz-box-shadow: none;
- box-shadow: none;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
}
.well .form-actions {
- padding-bottom: 0;
- margin-bottom: 0;
+ padding-bottom: 0;
+ margin-bottom: 0;
}
.well form {
- margin-bottom: 0;
+ margin-bottom: 0;
}
.well form .help-block {
- color: #999;
+ color: #999999;
}
.nav-tabs {
- border: 0;
+ border: 0;
}
.nav-tabs > li {
- float: right;
+ float: right;
}
.nav-tabs li a {
- margin-right: 0;
+ margin-right: 0;
}
.nav-tabs > .active > a {
- background: #f5f5f5;
+ background: #F5F5F5;
}
.nav-tabs > .active > a:hover {
- background: #f5f5f5;
-}
-
-.tabbable.first-tab-active .tab-content
-{
- border-top-right-radius: 0;
+ background: #F5F5F5;
}
-#footer, #push {
- height: 60px; /* .push must be the same height as .footer */
+.tabbable.first-tab-active .tab-content {
+ border-top-right-radius: 0;
}
-#footer{
- text-align: right;
+footer {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ clear: both;
+ z-index: 10;
+ height: 60px;
+ width: 95%;
+ margin: 0 2.5%;
}
-#footer p {
+footer p {
text-align: center;
color: gray;
- border-top: 1px solid #DDD;
+ border-top: 1px solid #DDDDDD;
padding-top: 10px;
}
-#footer a {
- color: gray;
+footer a {
+ color: gray !important;
font-weight: bold;
}
-#footer a:hover {
+footer a:hover {
color: gray;
}
.page-header {
- border-bottom: none;
- padding-bottom: 0px;
- margin-bottom: 20px;
+ border-bottom: none;
+ padding-bottom: 0px;
+ margin-bottom: 20px;
}
/* custom general page styles */
-.hero-unit h2, .hero-unit h1{
+.hero-unit h1, .hero-unit h2 {
color: #A30000;
}
-body a, body a{
+body a {
color: #A30000;
}
-body a:hover{
+body a:hover {
color: #c20000;
}
-#content a span{
- text-decoration: underline;
+#content a span {
+ text-decoration: underline;
}
.request-info {
- clear:both;
+ clear:both;
}
diff --git a/rest_framework/static/rest_framework/css/default.css b/rest_framework/static/rest_framework/css/default.css
index 0261a303..461cdfe5 100644
--- a/rest_framework/static/rest_framework/css/default.css
+++ b/rest_framework/static/rest_framework/css/default.css
@@ -3,20 +3,20 @@
content running up underneath it. */
h1 {
- font-weight: 500;
+ font-weight: 500;
}
h2, h3 {
- font-weight: 300;
+ font-weight: 300;
}
.resource-description, .response-info {
- margin-bottom: 2em;
+ margin-bottom: 2em;
}
.version:before {
- content: "v";
- opacity: 0.6;
- padding-right: 0.25em;
+ content: "v";
+ opacity: 0.6;
+ padding-right: 0.25em;
}
.version {
@@ -24,16 +24,16 @@ h2, h3 {
}
.format-option {
- font-family: Menlo, Consolas, "Andale Mono", "Lucida Console", monospace;
+ font-family: Menlo, Consolas, "Andale Mono", "Lucida Console", monospace;
}
.button-form {
- float: right;
- margin-right: 1em;
+ float: right;
+ margin-right: 1em;
}
ul.breadcrumb {
- margin: 58px 0 0 0;
+ margin: 80px 0 0 0;
}
form select, form input, form textarea {
@@ -43,17 +43,18 @@ form select, form input, form textarea {
form select[multiple] {
height: 150px;
}
+
/* To allow tooltips to work on disabled elements */
.disabled-tooltip-shield {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
}
.errorlist {
- margin-top: 0.5em;
+ margin-top: 0.5em;
}
pre {
@@ -64,8 +65,7 @@ pre {
}
.page-header {
- border-bottom: none;
- padding-bottom: 0px;
- margin-bottom: 20px;
+ border-bottom: none;
+ padding-bottom: 0px;
+ margin-bottom: 20px;
}
-
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index b6e9ca5c..3628daa0 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -4,233 +4,255 @@
<!DOCTYPE html>
<html>
<head>
- {% block head %}
+ {% block head %}
- {% block meta %}
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
- <meta name="robots" content="NONE,NOARCHIVE" />
- {% endblock %}
+ {% block meta %}
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+ <meta name="robots" content="NONE,NOARCHIVE" />
+ {% endblock %}
- <title>{% block title %}Django REST framework{% endblock %}</title>
+ <title>{% block title %}Django REST framework{% endblock %}</title>
- {% block style %}
- {% block bootstrap_theme %}
- <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
- <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
- {% endblock %}
- <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/>
- <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
- {% endblock %}
+ {% block style %}
+ {% block bootstrap_theme %}
+ <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
+ <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
+ {% endblock %}
+ <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/>
+ <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
+ {% endblock %}
- {% endblock %}
+ {% endblock %}
</head>
- {% block body %}
- <body class="{% block bodyclass %}{% endblock %} container">
-
- <div class="wrapper">
-
- {% block navbar %}
- <div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
- <div class="navbar-inner">
- <div class="container-fluid">
- <span href="/">
- {% block branding %}<a class='brand' rel="nofollow" href='http://www.django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
- </span>
- <ul class="nav pull-right">
- {% block userlinks %}
- {% if user.is_authenticated %}
- <li class="dropdown">
- <a href="#" class="dropdown-toggle" data-toggle="dropdown">
- {{ user }}
- <b class="caret"></b>
- </a>
- <ul class="dropdown-menu">
- <li>{% optional_logout request %}</li>
- </ul>
- </li>
- {% else %}
- <li>{% optional_login request %}</li>
- {% endif %}
- {% endblock %}
- </ul>
- </div>
- </div>
- </div>
- {% endblock %}
+ {% block body %}
+ <body class="{% block bodyclass %}{% endblock %} container">
- {% block breadcrumbs %}
- <ul class="breadcrumb">
- {% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
- <li>
- <a href="{{ breadcrumb_url }}" {% if forloop.last %}class="active"{% endif %}>{{ breadcrumb_name }}</a> {% if not forloop.last %}<span class="divider">&rsaquo;</span>{% endif %}
- </li>
- {% endfor %}
- </ul>
- {% endblock %}
+ <div class="wrapper">
- <!-- Content -->
- <div id="content">
-
- {% if 'GET' in allowed_methods %}
- <form id="get-form" class="pull-right">
- <fieldset>
- <div class="btn-group format-selection">
- <a class="btn btn-primary js-tooltip" href='{{ request.get_full_path }}' rel="nofollow" title="Make a GET request on the {{ name }} resource">GET</a>
-
- <button class="btn btn-primary dropdown-toggle js-tooltip" data-toggle="dropdown" title="Specify a format for the GET request">
- <span class="caret"></span>
- </button>
- <ul class="dropdown-menu">
- {% for format in available_formats %}
- <li>
- <a class="js-tooltip format-option" href='{% add_query_param request api_settings.URL_FORMAT_OVERRIDE format %}' rel="nofollow" title="Make a GET request on the {{ name }} resource with the format set to `{{ format }}`">{{ format }}</a>
- </li>
- {% endfor %}
- </ul>
+ {% block navbar %}
+ <div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
+ <div class="navbar-inner">
+ <div class="container-fluid">
+ <span>
+ {% block branding %}
+ <a class='brand' rel="nofollow" href='http://www.django-rest-framework.org'>
+ Django REST framework <span class="version">{{ version }}</span>
+ </a>
+ {% endblock %}
+ </span>
+ <ul class="nav pull-right">
+ {% block userlinks %}
+ {% if user.is_authenticated %}
+ {% optional_logout request user %}
+ {% else %}
+ {% optional_login request %}
+ {% endif %}
+ {% endblock %}
+ </ul>
+ </div>
</div>
+ </div>
+ {% endblock %}
- </fieldset>
- </form>
- {% endif %}
-
- {% if options_form %}
- <form class="button-form" action="{{ request.get_full_path }}" method="POST">
- {% csrf_token %}
- <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" />
- <button class="btn btn-primary js-tooltip" title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button>
- </form>
- {% endif %}
-
- {% if delete_form %}
- <form class="button-form" action="{{ request.get_full_path }}" method="POST">
- {% csrf_token %}
- <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" />
- <button class="btn btn-danger js-tooltip" title="Make a DELETE request on the {{ name }} resource">DELETE</button>
- </form>
- {% endif %}
-
- <div class="content-main">
- <div class="page-header"><h1>{{ name }}</h1></div>
- {% block description %}
- {{ description }}
+ {% block breadcrumbs %}
+ <ul class="breadcrumb">
+ {% for breadcrumb_name, breadcrumb_url in breadcrumblist %}
+ <li>
+ <a href="{{ breadcrumb_url }}" {% if forloop.last %}class="active"{% endif %}>
+ {{ breadcrumb_name }}
+ </a>
+ {% if not forloop.last %}<span class="divider">&rsaquo;</span>{% endif %}
+ </li>
+ {% endfor %}
+ </ul>
{% endblock %}
- <div class="request-info" style="clear: both" >
- <pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
- </div>
- <div class="response-info">
- <pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
-{% for key, val in response_headers.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>
-{% endfor %}
-</div>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
- </div>
- </div>
- {% if display_edit_forms %}
+ <!-- Content -->
+ <div id="content">
- {% if post_form or raw_data_post_form %}
- <div {% if post_form %}class="tabbable"{% endif %}>
- {% if post_form %}
- <ul class="nav nav-tabs form-switcher">
- <li><a name='html-tab' href="#object-form" data-toggle="tab">HTML form</a></li>
- <li><a name='raw-tab' href="#generic-content-form" data-toggle="tab">Raw data</a></li>
- </ul>
- {% endif %}
- <div class="well tab-content">
- {% if post_form %}
- <div class="tab-pane" id="object-form">
- {% with form=post_form %}
- <form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal">
- <fieldset>
- {{ post_form }}
- <div class="form-actions">
- <button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
- </div>
- </fieldset>
- </form>
- {% endwith %}
- </div>
- {% endif %}
- <div {% if post_form %}class="tab-pane"{% endif %} id="generic-content-form">
- {% with form=raw_data_post_form %}
- <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
- <fieldset>
- {% include "rest_framework/raw_data_form.html" %}
- <div class="form-actions">
- <button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
- </div>
- </fieldset>
- </form>
- {% endwith %}
- </div>
+ {% if 'GET' in allowed_methods %}
+ <form id="get-form" class="pull-right">
+ <fieldset>
+ <div class="btn-group format-selection">
+ <a class="btn btn-primary js-tooltip" href='{{ request.get_full_path }}'
+ rel="nofollow" title="Make a GET request on the {{ name }} resource">GET</a>
+
+ <button class="btn btn-primary dropdown-toggle js-tooltip" data-toggle="dropdown"
+ title="Specify a format for the GET request">
+ <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu">
+ {% for format in available_formats %}
+ <li>
+ <a class="js-tooltip format-option"
+ href='{% add_query_param request api_settings.URL_FORMAT_OVERRIDE format %}'
+ rel="nofollow"
+ title="Make a GET request on the {{ name }} resource with the format set to `{{ format }}`">
+ {{ format }}
+ </a>
+ </li>
+ {% endfor %}
+ </ul>
+ </div>
+ </fieldset>
+ </form>
+ {% endif %}
+
+ {% if options_form %}
+ <form class="button-form" action="{{ request.get_full_path }}" method="POST">
+ {% csrf_token %}
+ <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" />
+ <button class="btn btn-primary js-tooltip"
+ title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button>
+ </form>
+ {% endif %}
+
+ {% if delete_form %}
+ <form class="button-form" action="{{ request.get_full_path }}" method="POST">
+ {% csrf_token %}
+ <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" />
+ <button class="btn btn-danger js-tooltip"
+ title="Make a DELETE request on the {{ name }} resource">DELETE</button>
+ </form>
+ {% endif %}
+
+ <div class="content-main">
+ <div class="page-header">
+ <h1>{{ name }}</h1>
+ </div>
+ {% block description %}
+ {{ description }}
+ {% endblock %}
+ <div class="request-info" style="clear: both" >
+ <pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
+ </div>
+ <div class="response-info">
+ <pre class="prettyprint"><span class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
+{% for key, val in response_headers.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>
+{% endfor %}
+</span>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
</div>
</div>
- {% endif %}
- {% if put_form or raw_data_put_form or raw_data_patch_form %}
- <div {% if put_form %}class="tabbable"{% endif %}>
- {% if put_form %}
- <ul class="nav nav-tabs form-switcher">
- <li><a name='html-tab' href="#object-form" data-toggle="tab">HTML form</a></li>
- <li><a name='raw-tab' href="#generic-content-form" data-toggle="tab">Raw data</a></li>
- </ul>
- {% endif %}
- <div class="well tab-content">
- {% if put_form %}
- <div class="tab-pane" id="object-form">
- <form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal">
- <fieldset>
- {{ put_form }}
- <div class="form-actions">
- <button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
+ {% if display_edit_forms %}
+
+ {% if post_form or raw_data_post_form %}
+ <div {% if post_form %}class="tabbable"{% endif %}>
+ {% if post_form %}
+ <ul class="nav nav-tabs form-switcher">
+ <li>
+ <a name='html-tab' href="#post-object-form" data-toggle="tab">HTML form</a>
+ </li>
+ <li>
+ <a name='raw-tab' href="#post-generic-content-form" data-toggle="tab">Raw data</a>
+ </li>
+ </ul>
+ {% endif %}
+ <div class="well tab-content">
+ {% if post_form %}
+ <div class="tab-pane" id="post-object-form">
+ {% with form=post_form %}
+ <form action="{{ request.get_full_path }}"
+ method="POST" enctype="multipart/form-data" class="form-horizontal">
+ <fieldset>
+ {{ post_form }}
+ <div class="form-actions">
+ <button class="btn btn-primary"
+ title="Make a POST request on the {{ name }} resource">POST</button>
+ </div>
+ </fieldset>
+ </form>
+ {% endwith %}
</div>
- </fieldset>
- </form>
+ {% endif %}
+ <div {% if post_form %}class="tab-pane"{% endif %} id="post-generic-content-form">
+ {% with form=raw_data_post_form %}
+ <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
+ <fieldset>
+ {% include "rest_framework/raw_data_form.html" %}
+ <div class="form-actions">
+ <button class="btn btn-primary"
+ title="Make a POST request on the {{ name }} resource">POST</button>
+ </div>
+ </fieldset>
+ </form>
+ {% endwith %}
+ </div>
+ </div>
</div>
- {% endif %}
- <div {% if put_form %}class="tab-pane"{% endif %} id="generic-content-form">
- {% with form=raw_data_put_or_patch_form %}
- <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
- <fieldset>
- {% include "rest_framework/raw_data_form.html" %}
- <div class="form-actions">
- {% if raw_data_put_form %}
- <button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
- {% endif %}
- {% if raw_data_patch_form %}
- <button class="btn btn-primary js-tooltip" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="PATCH" title="Make a PATCH request on the {{ name }} resource">PATCH</button>
- {% endif %}
+ {% endif %}
+
+ {% if put_form or raw_data_put_form or raw_data_patch_form %}
+ <div {% if put_form %}class="tabbable"{% endif %}>
+ {% if put_form %}
+ <ul class="nav nav-tabs form-switcher">
+ <li>
+ <a name='html-tab' href="#put-object-form" data-toggle="tab">HTML form</a>
+ </li>
+ <li>
+ <a name='raw-tab' href="#put-generic-content-form" data-toggle="tab">Raw data</a>
+ </li>
+ </ul>
+ {% endif %}
+ <div class="well tab-content">
+ {% if put_form %}
+ <div class="tab-pane" id="put-object-form">
+ <form action="{{ request.get_full_path }}"
+ method="POST" enctype="multipart/form-data" class="form-horizontal">
+ <fieldset>
+ {{ put_form }}
+ <div class="form-actions">
+ <button class="btn btn-primary js-tooltip"
+ name="{{ api_settings.FORM_METHOD_OVERRIDE }}"
+ value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
+ </div>
+ </fieldset>
+ </form>
</div>
- </fieldset>
- </form>
- {% endwith %}
+ {% endif %}
+ <div {% if put_form %}class="tab-pane"{% endif %} id="put-generic-content-form">
+ {% with form=raw_data_put_or_patch_form %}
+ <form action="{{ request.get_full_path }}" method="POST" class="form-horizontal">
+ <fieldset>
+ {% include "rest_framework/raw_data_form.html" %}
+ <div class="form-actions">
+ {% if raw_data_put_form %}
+ <button class="btn btn-primary js-tooltip"
+ name="{{ api_settings.FORM_METHOD_OVERRIDE }}"
+ value="PUT" title="Make a PUT request on the {{ name }} resource">PUT</button>
+ {% endif %}
+ {% if raw_data_patch_form %}
+ <button class="btn btn-primary js-tooltip"
+ name="{{ api_settings.FORM_METHOD_OVERRIDE }}"
+ value="PATCH" title="Make a PATCH request on the {{ name }} resource">PATCH</button>
+ {% endif %}
+ </div>
+ </fieldset>
+ </form>
+ {% endwith %}
+ </div>
+ </div>
</div>
- </div>
- </div>
+ {% endif %}
{% endif %}
- {% endif %}
-
- </div>
- <!-- END content-main -->
-
- </div>
- <!-- END Content -->
-
- <div id="push"></div>
-
- </div>
+ </div>
+ <!-- END Content -->
- </div><!-- ./wrapper -->
+ <footer>
+ {% block footer %}
+ <p>Sponsored by <a href="http://dabapps.com/">DabApps</a>.</p>
+ {% endblock %}
+ </footer>
- {% block footer %}
- {% endblock %}
+ </div><!-- ./wrapper -->
- {% block script %}
- <script src="{% static "rest_framework/js/jquery-1.8.1-min.js" %}"></script>
- <script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script>
- <script src="{% static "rest_framework/js/prettify-min.js" %}"></script>
- <script src="{% static "rest_framework/js/default.js" %}"></script>
+ {% block script %}
+ <script src="{% static "rest_framework/js/jquery-1.8.1-min.js" %}"></script>
+ <script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script>
+ <script src="{% static "rest_framework/js/prettify-min.js" %}"></script>
+ <script src="{% static "rest_framework/js/default.js" %}"></script>
+ {% endblock %}
+ </body>
{% endblock %}
- </body>
- {% endblock %}
</html>
diff --git a/rest_framework/templates/rest_framework/login_base.html b/rest_framework/templates/rest_framework/login_base.html
index 43860e53..8ab682ac 100644
--- a/rest_framework/templates/rest_framework/login_base.html
+++ b/rest_framework/templates/rest_framework/login_base.html
@@ -17,21 +17,44 @@
<div class="row-fluid">
<div>
- <form action="{% url 'rest_framework:login' %}" class=" form-inline" method="post">
+ <form action="{% url 'rest_framework:login' %}" role="form" method="post">
{% csrf_token %}
- <div id="div_id_username" class="clearfix control-group">
+ <div id="div_id_username"
+ class="clearfix control-group {% if form.username.errors %}error{% endif %}">
<div class="controls">
<Label class="span4">Username:</label>
- <input style="height: 25px" type="text" name="username" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_username">
+ <input style="height: 25px" type="text" name="username" maxlength="100"
+ autocapitalize="off"
+ autocorrect="off" class="span12 textinput textInput"
+ id="id_username" required
+ {% if form.username.value %}value="{{ form.username.value }}"{% endif %}>
+ {% if form.username.errors %}
+ <p class="text-error">
+ {{ form.username.errors|striptags }}
+ </p>
+ {% endif %}
</div>
</div>
- <div id="div_id_password" class="clearfix control-group">
- <div class="controls">
+ <div id="div_id_password"
+ class="clearfix control-group {% if form.password.errors %}error{% endif %}">
+ <div class="controls">
<Label class="span4">Password:</label>
- <input style="height: 25px" type="password" name="password" maxlength="100" autocapitalize="off" autocorrect="off" class="textinput textInput" id="id_password">
+ <input style="height: 25px" type="password" name="password" maxlength="100"
+ autocapitalize="off" autocorrect="off" class="span12 textinput textInput"
+ id="id_password" required>
+ {% if form.password.errors %}
+ <p class="text-error">
+ {{ form.password.errors|striptags }}
+ </p>
+ {% endif %}
</div>
</div>
<input type="hidden" name="next" value="{{ next }}" />
+ {% if form.non_field_errors %}
+ {% for error in form.non_field_errors %}
+ <div class="well well-small text-error" style="border: none">{{ error }}</div>
+ {% endfor %}
+ {% endif %}
<div class="form-actions-no-box">
<input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit">
</div>
diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py
index b80a7d77..864d64dd 100644
--- a/rest_framework/templatetags/rest_framework.py
+++ b/rest_framework/templatetags/rest_framework.py
@@ -41,22 +41,31 @@ def optional_login(request):
except NoReverseMatch:
return ''
- snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, escape(request.path))
+ snippet = "<li><a href='{href}?next={next}'>Log in</a></li>".format(href=login_url, next=escape(request.path))
return snippet
@register.simple_tag
-def optional_logout(request):
+def optional_logout(request, user):
"""
Include a logout snippet if REST framework's logout view is in the URLconf.
"""
try:
logout_url = reverse('rest_framework:logout')
except NoReverseMatch:
- return ''
-
- snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, escape(request.path))
- return snippet
+ return '<li class="navbar-text">{user}</li>'.format(user=user)
+
+ snippet = """<li class="dropdown">
+ <a href="#" class="dropdown-toggle" data-toggle="dropdown">
+ {user}
+ <b class="caret"></b>
+ </a>
+ <ul class="dropdown-menu">
+ <li><a href='{href}?next={next}'>Log out</a></li>
+ </ul>
+ </li>"""
+
+ return snippet.format(user=user, href=logout_url, next=escape(request.path))
@register.simple_tag
diff --git a/rest_framework/test.py b/rest_framework/test.py
index f89a6dcd..9b40353a 100644
--- a/rest_framework/test.py
+++ b/rest_framework/test.py
@@ -36,7 +36,7 @@ class APIRequestFactory(DjangoRequestFactory):
Encode the data returning a two tuple of (bytes, content_type)
"""
- if not data:
+ if data is None:
return ('', content_type)
assert format is None or content_type is None, (
diff --git a/rest_framework/urls.py b/rest_framework/urls.py
index 8fa3073e..cfcee534 100644
--- a/rest_framework/urls.py
+++ b/rest_framework/urls.py
@@ -6,7 +6,7 @@ your API requires authentication:
urlpatterns = patterns('',
...
- url(r'^auth', include('rest_framework.urls', namespace='rest_framework'))
+ url(r'^auth/', include('rest_framework.urls', namespace='rest_framework'))
)
The urls must be namespaced as 'rest_framework', and you should make sure
diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py
index 6d53aed1..470af51b 100644
--- a/rest_framework/utils/formatting.py
+++ b/rest_framework/utils/formatting.py
@@ -2,11 +2,12 @@
Utility functions to return a formatted name and description for a given view.
"""
from __future__ import unicode_literals
+import re
from django.utils.html import escape
from django.utils.safestring import mark_safe
-from rest_framework.compat import apply_markdown
-import re
+
+from rest_framework.compat import apply_markdown, force_text
def remove_trailing_string(content, trailing):
@@ -28,6 +29,7 @@ def dedent(content):
as it fails to dedent multiline docstrings that include
unindented text on the initial line.
"""
+ content = force_text(content)
whitespace_counts = [len(line) - len(line.lstrip(' '))
for line in content.splitlines()[1:] if line.lstrip()]
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 23df3443..38346ab7 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -103,7 +103,9 @@ class APIView(View):
"""
view = super(APIView, cls).as_view(**initkwargs)
view.cls = cls
- return view
+ # Note: session based authentication is explicitly CSRF validated,
+ # all other authentication is CSRF exempt.
+ return csrf_exempt(view)
@property
def allowed_methods(self):
@@ -371,9 +373,9 @@ class APIView(View):
response.exception = True
return response
- # Note: session based authentication is explicitly CSRF validated,
- # all other authentication is CSRF exempt.
- @csrf_exempt
+ # Note: Views are made CSRF exempt from within `as_view` as to prevent
+ # accidental removal of this exemption in cases where `dispatch` needs to
+ # be overridden.
def dispatch(self, request, *args, **kwargs):
"""
`.dispatch()` is pretty much the same as Django's regular dispatch,
diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py
index bb5b304e..84b4bd8d 100644
--- a/rest_framework/viewsets.py
+++ b/rest_framework/viewsets.py
@@ -20,6 +20,7 @@ from __future__ import unicode_literals
from functools import update_wrapper
from django.utils.decorators import classonlymethod
+from django.views.decorators.csrf import csrf_exempt
from rest_framework import views, generics, mixins
@@ -89,7 +90,7 @@ class ViewSetMixin(object):
# resolved URL.
view.cls = cls
view.suffix = initkwargs.get('suffix', None)
- return view
+ return csrf_exempt(view)
def initialize_request(self, request, *args, **kargs):
"""
diff --git a/tests/browsable_api/__init__.py b/tests/browsable_api/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/tests/browsable_api/__init__.py
diff --git a/tests/browsable_api/auth_urls.py b/tests/browsable_api/auth_urls.py
new file mode 100644
index 00000000..bce7dcf9
--- /dev/null
+++ b/tests/browsable_api/auth_urls.py
@@ -0,0 +1,10 @@
+from __future__ import unicode_literals
+from django.conf.urls import patterns, url, include
+
+from .views import MockView
+
+urlpatterns = patterns(
+ '',
+ (r'^$', MockView.as_view()),
+ url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
+)
diff --git a/tests/browsable_api/no_auth_urls.py b/tests/browsable_api/no_auth_urls.py
new file mode 100644
index 00000000..5e3604a6
--- /dev/null
+++ b/tests/browsable_api/no_auth_urls.py
@@ -0,0 +1,9 @@
+from __future__ import unicode_literals
+from django.conf.urls import patterns
+
+from .views import MockView
+
+urlpatterns = patterns(
+ '',
+ (r'^$', MockView.as_view()),
+)
diff --git a/tests/browsable_api/test_browsable_api.py b/tests/browsable_api/test_browsable_api.py
new file mode 100644
index 00000000..5f264783
--- /dev/null
+++ b/tests/browsable_api/test_browsable_api.py
@@ -0,0 +1,65 @@
+from __future__ import unicode_literals
+from django.contrib.auth.models import User
+from django.test import TestCase
+
+from rest_framework.test import APIClient
+
+
+class DropdownWithAuthTests(TestCase):
+ """Tests correct dropdown behaviour with Auth views enabled."""
+
+ urls = 'tests.browsable_api.auth_urls'
+
+ def setUp(self):
+ self.client = APIClient(enforce_csrf_checks=True)
+ self.username = 'john'
+ self.email = 'lennon@thebeatles.com'
+ self.password = 'password'
+ self.user = User.objects.create_user(self.username, self.email, self.password)
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_name_shown_when_logged_in(self):
+ self.client.login(username=self.username, password=self.password)
+ response = self.client.get('/')
+ self.assertContains(response, 'john')
+
+ def test_logout_shown_when_logged_in(self):
+ self.client.login(username=self.username, password=self.password)
+ response = self.client.get('/')
+ self.assertContains(response, '>Log out<')
+
+ def test_login_shown_when_logged_out(self):
+ response = self.client.get('/')
+ self.assertContains(response, '>Log in<')
+
+
+class NoDropdownWithoutAuthTests(TestCase):
+ """Tests correct dropdown behaviour with Auth views NOT enabled."""
+
+ urls = 'tests.browsable_api.no_auth_urls'
+
+ def setUp(self):
+ self.client = APIClient(enforce_csrf_checks=True)
+ self.username = 'john'
+ self.email = 'lennon@thebeatles.com'
+ self.password = 'password'
+ self.user = User.objects.create_user(self.username, self.email, self.password)
+
+ def tearDown(self):
+ self.client.logout()
+
+ def test_name_shown_when_logged_in(self):
+ self.client.login(username=self.username, password=self.password)
+ response = self.client.get('/')
+ self.assertContains(response, 'john')
+
+ def test_dropdown_not_shown_when_logged_in(self):
+ self.client.login(username=self.username, password=self.password)
+ response = self.client.get('/')
+ self.assertNotContains(response, '<li class="dropdown">')
+
+ def test_dropdown_not_shown_when_logged_out(self):
+ response = self.client.get('/')
+ self.assertNotContains(response, '<li class="dropdown">')
diff --git a/tests/browsable_api/views.py b/tests/browsable_api/views.py
new file mode 100644
index 00000000..000f4e80
--- /dev/null
+++ b/tests/browsable_api/views.py
@@ -0,0 +1,15 @@
+from __future__ import unicode_literals
+
+from rest_framework.views import APIView
+from rest_framework import authentication
+from rest_framework import renderers
+from rest_framework.response import Response
+
+
+class MockView(APIView):
+
+ authentication_classes = (authentication.SessionAuthentication,)
+ renderer_classes = (renderers.BrowsableAPIRenderer,)
+
+ def get(self, request):
+ return Response({'a': 1, 'b': 2, 'c': 3})
diff --git a/tests/conftest.py b/tests/conftest.py
index f3723aea..4b33e19c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -5,6 +5,7 @@ def pytest_configure():
DEBUG_PROPAGATE_EXCEPTIONS=True,
DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:'}},
+ SITE_ID=1,
SECRET_KEY='not very secret in tests',
USE_I18N=True,
USE_L10N=True,
diff --git a/tests/test_authentication.py b/tests/test_authentication.py
index 2b9d73e4..32041f9c 100644
--- a/tests/test_authentication.py
+++ b/tests/test_authentication.py
@@ -57,7 +57,8 @@ urlpatterns = patterns(
authentication_classes=[OAuthAuthentication],
permission_classes=[permissions.TokenHasReadWriteScope]
)
- )
+ ),
+ url(r'^auth/', include('rest_framework.urls', namespace='rest_framework'))
)
@@ -134,6 +135,15 @@ class SessionAuthTests(TestCase):
def tearDown(self):
self.csrf_client.logout()
+ def test_login_view_renders_on_get(self):
+ """
+ Ensure the login template renders for a basic GET.
+
+ cf. [#1810](https://github.com/tomchristie/django-rest-framework/pull/1810)
+ """
+ response = self.csrf_client.get('/auth/login/')
+ self.assertContains(response, '<Label class="span4">Username:</label>')
+
def test_post_form_session_auth_failing_csrf(self):
"""
Ensure POSTing form over session authentication without CSRF token fails.
diff --git a/tests/test_description.py b/tests/test_description.py
index 1e481f06..0675d209 100644
--- a/tests/test_description.py
+++ b/tests/test_description.py
@@ -98,6 +98,30 @@ class TestViewNamesAndDescriptions(TestCase):
pass
self.assertEqual(MockView().get_view_description(), '')
+ def test_view_description_can_be_promise(self):
+ """
+ Ensure a view may have a docstring that is actually a lazily evaluated
+ class that can be converted to a string.
+
+ See: https://github.com/tomchristie/django-rest-framework/issues/1708
+ """
+ # use a mock object instead of gettext_lazy to ensure that we can't end
+ # up with a test case string in our l10n catalog
+ class MockLazyStr(object):
+ def __init__(self, string):
+ self.s = string
+
+ def __str__(self):
+ return self.s
+
+ def __unicode__(self):
+ return self.s
+
+ class MockView(APIView):
+ __doc__ = MockLazyStr("a gettext string")
+
+ self.assertEqual(MockView().get_view_description(), 'a gettext string')
+
def test_markdown(self):
"""
Ensure markdown to HTML works as expected.
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 094ac1eb..0ddbe48b 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -1004,6 +1004,18 @@ class BooleanField(TestCase):
self.assertFalse(BooleanRequiredSerializer(data={}).is_valid())
+class ModelCharField(TestCase):
+ """
+ Tests for CharField
+ """
+ def test_none_serializing(self):
+ class CharFieldSerializer(serializers.Serializer):
+ char = serializers.CharField(allow_none=True, required=False)
+ serializer = CharFieldSerializer(data={'char': None})
+ self.assertTrue(serializer.is_valid())
+ self.assertIsNone(serializer.object['char'])
+
+
class SerializerMethodFieldTest(TestCase):
"""
Tests for the SerializerMethodField field_to_native() behavior
diff --git a/tests/test_filters.py b/tests/test_filters.py
index 47bffd43..5722fd7c 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -408,16 +408,61 @@ class SearchFilterTests(TestCase):
)
-class OrdringFilterModel(models.Model):
+class OrderingFilterModel(models.Model):
title = models.CharField(max_length=20)
text = models.CharField(max_length=100)
class OrderingFilterRelatedModel(models.Model):
- related_object = models.ForeignKey(OrdringFilterModel,
+ related_object = models.ForeignKey(OrderingFilterModel,
related_name="relateds")
+class DjangoFilterOrderingModel(models.Model):
+ date = models.DateField()
+ text = models.CharField(max_length=10)
+
+ class Meta:
+ ordering = ['-date']
+
+
+class DjangoFilterOrderingTests(TestCase):
+ def setUp(self):
+ data = [{
+ 'date': datetime.date(2012, 10, 8),
+ 'text': 'abc'
+ }, {
+ 'date': datetime.date(2013, 10, 8),
+ 'text': 'bcd'
+ }, {
+ 'date': datetime.date(2014, 10, 8),
+ 'text': 'cde'
+ }]
+
+ for d in data:
+ DjangoFilterOrderingModel.objects.create(**d)
+
+ def test_default_ordering(self):
+ class DjangoFilterOrderingView(generics.ListAPIView):
+ model = DjangoFilterOrderingModel
+ filter_backends = (filters.DjangoFilterBackend,)
+ filter_fields = ['text']
+ ordering = ('-date',)
+
+ view = DjangoFilterOrderingView.as_view()
+ request = factory.get('/')
+ response = view(request)
+
+ self.assertEqual(
+ response.data,
+ [
+ {'id': 3, 'date': datetime.date(2014, 10, 8), 'text': 'cde'},
+ {'id': 2, 'date': datetime.date(2013, 10, 8), 'text': 'bcd'},
+ {'id': 1, 'date': datetime.date(2012, 10, 8), 'text': 'abc'}
+ ]
+ )
+
+
class OrderingFilterTests(TestCase):
def setUp(self):
# Sequence of title/text is:
@@ -436,11 +481,11 @@ class OrderingFilterTests(TestCase):
chr(idx + ord('b')) +
chr(idx + ord('c'))
)
- OrdringFilterModel(title=title, text=text).save()
+ OrderingFilterModel(title=title, text=text).save()
def test_ordering(self):
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ model = OrderingFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = ('title',)
ordering_fields = ('text',)
@@ -459,7 +504,7 @@ class OrderingFilterTests(TestCase):
def test_reverse_ordering(self):
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ model = OrderingFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = ('title',)
ordering_fields = ('text',)
@@ -478,7 +523,7 @@ class OrderingFilterTests(TestCase):
def test_incorrectfield_ordering(self):
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ model = OrderingFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = ('title',)
ordering_fields = ('text',)
@@ -497,7 +542,7 @@ class OrderingFilterTests(TestCase):
def test_default_ordering(self):
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ model = OrderingFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = ('title',)
oredering_fields = ('text',)
@@ -516,7 +561,7 @@ class OrderingFilterTests(TestCase):
def test_default_ordering_using_string(self):
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ model = OrderingFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = 'title'
ordering_fields = ('text',)
@@ -536,7 +581,7 @@ class OrderingFilterTests(TestCase):
def test_ordering_by_aggregate_field(self):
# create some related models to aggregate order by
num_objs = [2, 5, 3]
- for obj, num_relateds in zip(OrdringFilterModel.objects.all(),
+ for obj, num_relateds in zip(OrderingFilterModel.objects.all(),
num_objs):
for _ in range(num_relateds):
new_related = OrderingFilterRelatedModel(
@@ -545,11 +590,11 @@ class OrderingFilterTests(TestCase):
new_related.save()
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ model = OrderingFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = 'title'
ordering_fields = '__all__'
- queryset = OrdringFilterModel.objects.all().annotate(
+ queryset = OrderingFilterModel.objects.all().annotate(
models.Count("relateds"))
view = OrderingListView.as_view()
@@ -567,7 +612,7 @@ class OrderingFilterTests(TestCase):
def test_ordering_with_nonstandard_ordering_param(self):
with temporary_setting('ORDERING_PARAM', 'order', filters):
class OrderingListView(generics.ListAPIView):
- model = OrdringFilterModel
+ model = OrderingFilterModel
filter_backends = (filters.OrderingFilter,)
ordering = ('title',)
ordering_fields = ('text',)
diff --git a/tests/test_generics.py b/tests/test_generics.py
index e9f5bebd..97116349 100644
--- a/tests/test_generics.py
+++ b/tests/test_generics.py
@@ -681,3 +681,42 @@ class TestFilterBackendAppliedToViews(TestCase):
response = view(request).render()
self.assertContains(response, 'field_b')
self.assertNotContains(response, 'field_a')
+
+ def test_options_with_dynamic_serializer(self):
+ """
+ Ensure that OPTIONS returns correct POST json schema:
+ DynamicSerializer with single field 'field_b'
+ """
+ request = factory.options('/')
+ view = DynamicSerializerView.as_view()
+
+ with self.assertNumQueries(0):
+ response = view(request).render()
+
+ expected = {
+ 'name': 'Dynamic Serializer',
+ 'description': '',
+ 'renders': [
+ 'text/html',
+ 'application/json'
+ ],
+ 'parses': [
+ 'application/json',
+ 'application/x-www-form-urlencoded',
+ 'multipart/form-data'
+ ],
+ 'actions': {
+ 'POST': {
+ 'field_b': {
+ 'type': 'string',
+ 'required': True,
+ 'read_only': False,
+ 'label': 'field b',
+ 'max_length': 100
+ }
+ }
+ }
+ }
+
+ self.assertEqual(response.status_code, status.HTTP_200_OK)
+ self.assertEqual(response.data, expected)
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index 80c33e2e..e1c2528b 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -412,6 +412,15 @@ class CustomPaginationSerializer(pagination.BasePaginationSerializer):
results_field = 'objects'
+class CustomFooSerializer(serializers.Serializer):
+ foo = serializers.CharField()
+
+
+class CustomFooPaginationSerializer(pagination.PaginationSerializer):
+ class Meta:
+ object_serializer_class = CustomFooSerializer
+
+
class TestCustomPaginationSerializer(TestCase):
def setUp(self):
objects = ['john', 'paul', 'george', 'ringo']
@@ -434,6 +443,16 @@ class TestCustomPaginationSerializer(TestCase):
}
self.assertEqual(serializer.data, expected)
+ def test_custom_pagination_serializer_with_custom_object_serializer(self):
+ objects = [
+ {'foo': 'bar'},
+ {'foo': 'spam'}
+ ]
+ paginator = Paginator(objects, 1)
+ page = paginator.page(1)
+ serializer = CustomFooPaginationSerializer(page)
+ serializer.data
+
class NonIntegerPage(object):
diff --git a/tests/test_parsers.py b/tests/test_parsers.py
index 8af90677..3f2672df 100644
--- a/tests/test_parsers.py
+++ b/tests/test_parsers.py
@@ -1,3 +1,5 @@
+# -*- coding: utf-8 -*-
+
from __future__ import unicode_literals
from rest_framework.compat import StringIO
from django import forms
@@ -113,3 +115,25 @@ class TestFileUploadParser(TestCase):
parser = FileUploadParser()
filename = parser.get_filename(self.stream, None, self.parser_context)
self.assertEqual(filename, 'file.txt')
+
+ def test_get_encoded_filename(self):
+ parser = FileUploadParser()
+
+ self.__replace_content_disposition('inline; filename*=utf-8\'\'ÀĥƦ.txt')
+ filename = parser.get_filename(self.stream, None, self.parser_context)
+ self.assertEqual(filename, 'ÀĥƦ.txt')
+
+ self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'\'ÀĥƦ.txt')
+ filename = parser.get_filename(self.stream, None, self.parser_context)
+ self.assertEqual(filename, 'ÀĥƦ.txt')
+
+ self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8\'en-us\'ÀĥƦ.txt')
+ filename = parser.get_filename(self.stream, None, self.parser_context)
+ self.assertEqual(filename, 'ÀĥƦ.txt')
+
+ self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8--ÀĥƦ.txt')
+ filename = parser.get_filename(self.stream, None, self.parser_context)
+ self.assertEqual(filename, 'fallback.txt')
+
+ def __replace_content_disposition(self, disposition):
+ self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition
diff --git a/tests/test_routers.py b/tests/test_routers.py
index b076f134..f6f5a977 100644
--- a/tests/test_routers.py
+++ b/tests/test_routers.py
@@ -3,7 +3,7 @@ from django.conf.urls import patterns, url, include
from django.db import models
from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured
-from rest_framework import serializers, viewsets, permissions
+from rest_framework import serializers, viewsets, mixins, permissions
from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response
from rest_framework.routers import SimpleRouter, DefaultRouter
@@ -284,3 +284,19 @@ class TestDynamicListAndDetailRouter(TestCase):
else:
method_map = 'get'
self.assertEqual(route.mapping[method_map], endpoint)
+
+
+class TestRootWithAListlessViewset(TestCase):
+ def setUp(self):
+ class NoteViewSet(mixins.RetrieveModelMixin,
+ viewsets.GenericViewSet):
+ model = RouterTestModel
+
+ self.router = DefaultRouter()
+ self.router.register(r'notes', NoteViewSet)
+ self.view = self.router.urls[0].callback
+
+ def test_api_root(self):
+ request = factory.get('/')
+ response = self.view(request)
+ self.assertEqual(response.data, {})
diff --git a/tests/test_validation.py b/tests/test_validation.py
index e13e4078..a46e38ac 100644
--- a/tests/test_validation.py
+++ b/tests/test_validation.py
@@ -1,5 +1,6 @@
from __future__ import unicode_literals
from django.core.validators import MaxValueValidator
+from django.core.exceptions import ValidationError
from django.db import models
from django.test import TestCase
from rest_framework import generics, serializers, status
@@ -146,3 +147,42 @@ class TestMaxValueValidatorValidation(TestCase):
response = view(request, pk=obj.pk).render()
self.assertEqual(response.content, b'{"number_value": ["Ensure this value is less than or equal to 100."]}')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
+
+
+class TestChoiceFieldChoicesValidate(TestCase):
+ CHOICES = [
+ (0, 'Small'),
+ (1, 'Medium'),
+ (2, 'Large'),
+ ]
+
+ CHOICES_NESTED = [
+ ('Category', (
+ (1, 'First'),
+ (2, 'Second'),
+ (3, 'Third'),
+ )),
+ (4, 'Fourth'),
+ ]
+
+ def test_choices(self):
+ """
+ Make sure a value for choices works as expected.
+ """
+ f = serializers.ChoiceField(choices=self.CHOICES)
+ value = self.CHOICES[0][0]
+ try:
+ f.validate(value)
+ except ValidationError:
+ self.fail("Value %s does not validate" % str(value))
+
+ def test_nested_choices(self):
+ """
+ Make sure a nested value for choices works as expected.
+ """
+ f = serializers.ChoiceField(choices=self.CHOICES_NESTED)
+ value = self.CHOICES_NESTED[0][1][0][0]
+ try:
+ f.validate(value)
+ except ValidationError:
+ self.fail("Value %s does not validate" % str(value))
diff --git a/tox.ini b/tox.ini
index 6f588de1..b3f53cce 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,6 +9,8 @@ envlist =
[testenv]
commands = ./runtests.py --fast
+setenv =
+ PYTHONDONTWRITEBYTECODE=1
[testenv:flake8]
basepython = python2.7
@@ -18,7 +20,7 @@ commands = ./runtests.py --lintonly
[testenv:py3.4-django1.7]
basepython = python3.4
-deps = https://www.djangoproject.com/download/1.7c2/tarball/
+deps = Django==1.7
django-filter==0.7
defusedxml==0.3
Pillow==2.3.0
@@ -26,7 +28,7 @@ deps = https://www.djangoproject.com/download/1.7c2/tarball/
[testenv:py3.3-django1.7]
basepython = python3.3
-deps = https://www.djangoproject.com/download/1.7c2/tarball/
+deps = Django==1.7
django-filter==0.7
defusedxml==0.3
Pillow==2.3.0
@@ -34,7 +36,7 @@ deps = https://www.djangoproject.com/download/1.7c2/tarball/
[testenv:py3.2-django1.7]
basepython = python3.2
-deps = https://www.djangoproject.com/download/1.7c2/tarball/
+deps = Django==1.7
django-filter==0.7
defusedxml==0.3
Pillow==2.3.0
@@ -42,7 +44,7 @@ deps = https://www.djangoproject.com/download/1.7c2/tarball/
[testenv:py2.7-django1.7]
basepython = python2.7
-deps = https://www.djangoproject.com/download/1.7c2/tarball/
+deps = Django==1.7
django-filter==0.7
defusedxml==0.3
# django-oauth-plus==2.2.1