Commit 6aef157d by Rense VanderHoek

Merge remote-tracking branch 'tomchristie/master'

parents c774a4c3 b8c9c809
...@@ -57,7 +57,7 @@ Note that setting a `default` value implies that the field is not required. Incl ...@@ -57,7 +57,7 @@ Note that setting a `default` value implies that the field is not required. Incl
### `source` ### `source`
The name of the attribute that will be used to populate the field. May be a method that only takes a `self` argument, such as `URLField('get_absolute_url')`, or may use dotted notation to traverse attributes, such as `EmailField(source='user.email')`. The name of the attribute that will be used to populate the field. May be a method that only takes a `self` argument, such as `URLField(source='get_absolute_url')`, or may use dotted notation to traverse attributes, such as `EmailField(source='user.email')`.
The value `source='*'` has a special meaning, and is used to indicate that the entire object should be passed through to the field. This can be useful for creating nested representations, or for fields which require access to the complete object in order to determine the output representation. The value `source='*'` has a special meaning, and is used to indicate that the entire object should be passed through to the field. This can be useful for creating nested representations, or for fields which require access to the complete object in order to determine the output representation.
...@@ -85,9 +85,9 @@ A value that should be used for pre-populating the value of HTML form fields. ...@@ -85,9 +85,9 @@ A value that should be used for pre-populating the value of HTML form fields.
### `style` ### `style`
A dictionary of key-value pairs that can be used to control how renderers should render the field. The API for this should still be considered experimental, and will be formalized with the 3.1 release. A dictionary of key-value pairs that can be used to control how renderers should render the field.
Two options are currently used in HTML form generation, `'input_type'` and `'base_template'`. Two examples here are `'input_type'` and `'base_template'`:
# Use <input type="password"> for the input. # Use <input type="password"> for the input.
password = serializers.CharField( password = serializers.CharField(
...@@ -100,7 +100,7 @@ Two options are currently used in HTML form generation, `'input_type'` and `'bas ...@@ -100,7 +100,7 @@ Two options are currently used in HTML form generation, `'input_type'` and `'bas
style = {'base_template': 'radio.html'} style = {'base_template': 'radio.html'}
} }
**Note**: The `style` argument replaces the old-style version 2.x `widget` keyword argument. Because REST framework 3 now uses templated HTML form generation, the `widget` option that was used to support Django built-in widgets can no longer be supported. Version 3.3 is planned to include public API support for customizing HTML form generation. For more details see the [HTML & Forms][html-and-forms] documentation.
--- ---
...@@ -658,6 +658,7 @@ The [django-rest-framework-gis][django-rest-framework-gis] package provides geog ...@@ -658,6 +658,7 @@ The [django-rest-framework-gis][django-rest-framework-gis] package provides geog
The [django-rest-framework-hstore][django-rest-framework-hstore] package provides an `HStoreField` to support [django-hstore][django-hstore] `DictionaryField` model field. 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 [cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data
[html-and-forms]: ../topics/html-and-forms.md
[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS [FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS
[ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 [ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
[strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior [strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior
......
...@@ -83,6 +83,10 @@ We can override `.get_queryset()` to deal with URLs such as `http://example.com/ ...@@ -83,6 +83,10 @@ We can override `.get_queryset()` to deal with URLs such as `http://example.com/
As well as being able to override the default queryset, REST framework also includes support for generic filtering backends that allow you to easily construct complex searches and filters. As well as being able to override the default queryset, REST framework also includes support for generic filtering backends that allow you to easily construct complex searches and filters.
Generic filters can also present themselves as HTML controls in the browsable API and admin API.
![Filter Example](../img/filter-controls.png)
## Setting filter backends ## Setting filter backends
The default filter backends may be set globally, using the `DEFAULT_FILTER_BACKENDS` setting. For example. The default filter backends may be set globally, using the `DEFAULT_FILTER_BACKENDS` setting. For example.
...@@ -141,6 +145,13 @@ To use REST framework's `DjangoFilterBackend`, first install `django-filter`. ...@@ -141,6 +145,13 @@ To use REST framework's `DjangoFilterBackend`, first install `django-filter`.
pip install django-filter pip install django-filter
If you are using the browsable API or admin API you may also want to install `crispy-forms`, which will enhance the presentation of the filter forms in HTML views, by allowing them to render Bootstrap 3 HTML.
pip install django-crispy-forms
With crispy forms installed, the browsable API will present a filtering control for `DjangoFilterBackend`, like so:
![Django Filter](../../docs/img/django-filter.png)
#### Specifying filter fields #### Specifying filter fields
...@@ -237,6 +248,10 @@ For more details on using filter sets see the [django-filter documentation][djan ...@@ -237,6 +248,10 @@ For more details on using filter sets see the [django-filter documentation][djan
The `SearchFilter` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin]. The `SearchFilter` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin].
When in use, the browsable API will include a `SearchFilter` control:
![Search Filter](../../docs/img/search-filter.png)
The `SearchFilter` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`. The `SearchFilter` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`.
class UserListView(generics.ListAPIView): class UserListView(generics.ListAPIView):
...@@ -274,7 +289,11 @@ For more details, see the [Django documentation][search-django-admin]. ...@@ -274,7 +289,11 @@ For more details, see the [Django documentation][search-django-admin].
## OrderingFilter ## OrderingFilter
The `OrderingFilter` class supports simple query parameter controlled ordering of results. By default, the query parameter is named `'ordering'`, but this may by overridden with the `ORDERING_PARAM` setting. The `OrderingFilter` class supports simple query parameter controlled ordering of results.
![Ordering Filter](../../docs/img/ordering-filter.png)
By default, the query parameter is named `'ordering'`, but this may by overridden with the `ORDERING_PARAM` setting.
For example, to order users by username: For example, to order users by username:
...@@ -389,6 +408,14 @@ For example, you might need to restrict users to only being able to see objects ...@@ -389,6 +408,14 @@ For example, you might need to restrict users to only being able to see objects
We could achieve the same behavior by overriding `get_queryset()` on the views, but using a filter backend allows you to more easily add this restriction to multiple views, or to apply it across the entire API. We could achieve the same behavior by overriding `get_queryset()` on the views, but using a filter backend allows you to more easily add this restriction to multiple views, or to apply it across the entire API.
## Customizing the interface
Generic filters may also present an interface in the browsable API. To do so you should implement a `to_html()` method which returns a rendered HTML representation of the filter. This method should have the following signature:
`to_html(self, request, queryset, view)`
The method should return a rendered HTML string.
# Third party packages # Third party packages
The following third party packages provide additional filter implementations. The following third party packages provide additional filter implementations.
......
...@@ -288,7 +288,7 @@ Would serialize to a nested representation like this: ...@@ -288,7 +288,7 @@ Would serialize to a nested representation like this:
# Writable nested serializers # Writable nested serializers
Be default nested serializers are read-only. If you want to to support write-operations to a nested serializer field you'll need to create either or both of the `create()` and/or `update()` methods, in order to explicitly specify how the child relationships should be saved. By default nested serializers are read-only. If you want to support write-operations to a nested serializer field you'll need to create `create()` and/or `update()` methods in order to explicitly specify how the child relationships should be saved.
class TrackSerializer(serializers.ModelSerializer): class TrackSerializer(serializers.ModelSerializer):
class Meta: class Meta:
......
...@@ -197,9 +197,19 @@ Note that views that have nested or list serializers for their input won't work ...@@ -197,9 +197,19 @@ Note that views that have nested or list serializers for their input won't work
## HTMLFormRenderer ## HTMLFormRenderer
Renders data returned by a serializer into an HTML form. The output of this renderer does not include the enclosing `<form>` tags or an submit actions, as you'll probably need those to include the desired method and URL. Also note that the `HTMLFormRenderer` does not yet support including field error messages. Renders data returned by a serializer into an HTML form. The output of this renderer does not include the enclosing `<form>` tags, a hidden CSRF input or any submit buttons.
**Note**: The `HTMLFormRenderer` class is intended for internal use with the browsable API and admin interface. It should not be considered a fully documented or stable API. The template used by the `HTMLFormRenderer` class, and the context submitted to it **may be subject to change**. If you need to use this renderer class it is advised that you either make a local copy of the class and templates, or follow the release note on REST framework upgrades closely. This renderer is not intended to be used directly, but can instead be used in templates by passing a serializer instance to the `render_form` template tag.
{% load rest_framework %}
<form action="/submit-report/" method="post">
{% csrf_token %}
{% render_form serializer %}
<input type="submit" value="Save" />
</form>
For more information see the [HTML & Forms][html-and-forms] documentation.
**.media_type**: `text/html` **.media_type**: `text/html`
...@@ -207,7 +217,7 @@ Renders data returned by a serializer into an HTML form. The output of this ren ...@@ -207,7 +217,7 @@ Renders data returned by a serializer into an HTML form. The output of this ren
**.charset**: `utf-8` **.charset**: `utf-8`
**.template**: `'rest_framework/form.html'` **.template**: `'rest_framework/horizontal/form.html'`
## MultiPartRenderer ## MultiPartRenderer
...@@ -455,6 +465,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily ...@@ -455,6 +465,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily
[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process [cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process
[conneg]: content-negotiation.md [conneg]: content-negotiation.md
[html-and-forms]: ../topics/html-and-forms.md
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers [browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
[testing]: testing.md [testing]: testing.md
[HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas [HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas
......
...@@ -287,7 +287,7 @@ Similarly if a nested representation should be a list of items, you should pass ...@@ -287,7 +287,7 @@ Similarly if a nested representation should be a list of items, you should pass
## Writable nested representations ## Writable nested representations
When dealing with nested representations that support deserializing the data, an errors with nested objects will be nested under the field name of the nested object. When dealing with nested representations that support deserializing the data, any errors with nested objects will be nested under the field name of the nested object.
serializer = CommentSerializer(data={'user': {'email': 'foobar', 'username': 'doe'}, 'content': 'baz'}) serializer = CommentSerializer(data={'user': {'email': 'foobar', 'username': 'doe'}, 'content': 'baz'})
serializer.is_valid() serializer.is_valid()
...@@ -356,7 +356,7 @@ It is possible that a third party package, providing automatic support some kind ...@@ -356,7 +356,7 @@ It is possible that a third party package, providing automatic support some kind
#### Handling saving related instances in model manager classes #### Handling saving related instances in model manager classes
An alternative to saving multiple related instances in the serializer is to write custom model manager classes handle creating the correct instances. An alternative to saving multiple related instances in the serializer is to write custom model manager classes that handle creating the correct instances.
For example, suppose we wanted to ensure that `User` instances and `Profile` instances are always created together as a pair. We might write a custom manager class that looks something like this: For example, suppose we wanted to ensure that `User` instances and `Profile` instances are always created together as a pair. We might write a custom manager class that looks something like this:
...@@ -405,7 +405,7 @@ To serialize a queryset or list of objects instead of a single object instance, ...@@ -405,7 +405,7 @@ To serialize a queryset or list of objects instead of a single object instance,
#### Deserializing multiple objects #### Deserializing multiple objects
The default behavior for deserializing multiple objects is to support multiple object creation, but not support multiple object updates. For more information on how to support or customize either of these cases, see the [ListSerializer](#ListSerializer) documentation below. The default behavior for deserializing multiple objects is to support multiple object creation, but not support multiple object updates. For more information on how to support or customize either of these cases, see the [ListSerializer](#listserializer) documentation below.
## Including extra context ## Including extra context
...@@ -478,7 +478,7 @@ For example: ...@@ -478,7 +478,7 @@ For example:
model = Account model = Account
fields = '__all__' fields = '__all__'
You can set the `exclude` attribute of the to a list of fields to be excluded from the serializer. You can set the `exclude` attribute to a list of fields to be excluded from the serializer.
For example: For example:
...@@ -551,7 +551,7 @@ Please review the [Validators Documentation](/api-guide/validators/) for details ...@@ -551,7 +551,7 @@ Please review the [Validators Documentation](/api-guide/validators/) for details
## Additional keyword arguments ## Additional keyword arguments
There is also a shortcut allowing you to specify arbitrary additional keyword arguments on fields, using the `extra_kwargs` option. Similarly to `read_only_fields` this means you do not need to explicitly declare the field on the serializer. There is also a shortcut allowing you to specify arbitrary additional keyword arguments on fields, using the `extra_kwargs` option. As in the case of `read_only_fields`, this means you do not need to explicitly declare the field on the serializer.
This option is a dictionary, mapping field names to a dictionary of keyword arguments. For example: This option is a dictionary, mapping field names to a dictionary of keyword arguments. For example:
...@@ -832,7 +832,7 @@ This class implements the same basic API as the `Serializer` class: ...@@ -832,7 +832,7 @@ This class implements the same basic API as the `Serializer` class:
* `.data` - Returns the outgoing primitive representation. * `.data` - Returns the outgoing primitive representation.
* `.is_valid()` - Deserializes and validates incoming data. * `.is_valid()` - Deserializes and validates incoming data.
* `.validated_data` - Returns the validated incoming data. * `.validated_data` - Returns the validated incoming data.
* `.errors` - Returns an errors during validation. * `.errors` - Returns any errors during validation.
* `.save()` - Persists the validated data into an object instance. * `.save()` - Persists the validated data into an object instance.
There are four methods that can be overridden, depending on what functionality you want the serializer class to support: There are four methods that can be overridden, depending on what functionality you want the serializer class to support:
......
...@@ -72,7 +72,7 @@ The following settings keys are also used to control versioning: ...@@ -72,7 +72,7 @@ The following settings keys are also used to control versioning:
* `DEFAULT_VERSION`. The value that should be used for `request.version` when no versioning information is present. Defaults to `None`. * `DEFAULT_VERSION`. The value that should be used for `request.version` when no versioning information is present. Defaults to `None`.
* `ALLOWED_VERSIONS`. If set, this value will restrict the set of versions that may be returned by the versioning scheme, and will raise an error if the provided version if not in this set. Note that the value used for the `DEFAULT_VERSION` setting is always considered to be part of the `ALLOWED_VERSIONS` set. Defaults to `None`. * `ALLOWED_VERSIONS`. If set, this value will restrict the set of versions that may be returned by the versioning scheme, and will raise an error if the provided version if not in this set. Note that the value used for the `DEFAULT_VERSION` setting is always considered to be part of the `ALLOWED_VERSIONS` set. Defaults to `None`.
* `VERSION_PARAMETER`. The string that should used for any versioning parameters, such as in the media type or URL query parameters. Defaults to `'version'`. * `VERSION_PARAM`. The string that should used for any versioning parameters, such as in the media type or URL query parameters. Defaults to `'version'`.
You can also set your versioning class plus those three values on a per-view or a per-viewset basis by defining your own versioning scheme and using the `default_version`, `allowed_versions` and `version_param` class variables. For example, if you want to use `URLPathVersioning`: You can also set your versioning class plus those three values on a per-view or a per-viewset basis by defining your own versioning scheme and using the `default_version`, `allowed_versions` and `version_param` class variables. For example, if you want to use `URLPathVersioning`:
......
...@@ -12,9 +12,7 @@ ...@@ -12,9 +12,7 @@
--- ---
**Note**: This is the documentation for the **version 3.2** of REST framework. Documentation for [version 2.4](http://tomchristie.github.io/rest-framework-2-docs/) is also available. **Note**: This is the documentation for the **version 3** of REST framework. Documentation for [version 2](http://tomchristie.github.io/rest-framework-2-docs/) is also available.
For more details see the 3.2 [announcement][3.2-announcement] and [release notes][release-notes].
--- ---
...@@ -31,7 +29,7 @@ For more details see the 3.2 [announcement][3.2-announcement] and [release notes ...@@ -31,7 +29,7 @@ For more details see the 3.2 [announcement][3.2-announcement] and [release notes
<img alt="Django REST Framework" title="Logo by Jake 'Sid' Smith" src="img/logo.png" width="600px" style="display: block; margin: 0 auto 0 auto"> <img alt="Django REST Framework" title="Logo by Jake 'Sid' Smith" src="img/logo.png" width="600px" style="display: block; margin: 0 auto 0 auto">
</p> </p>
Django REST framework is a powerful and flexible toolkit that makes it easy to build Web APIs. Django REST framework is a powerful and flexible toolkit for building Web APIs.
Some reasons you might want to use REST framework: Some reasons you might want to use REST framework:
...@@ -52,13 +50,14 @@ Some reasons you might want to use REST framework: ...@@ -52,13 +50,14 @@ Some reasons you might want to use REST framework:
REST framework requires the following: REST framework requires the following:
* Python (2.6.5+, 2.7, 3.2, 3.3, 3.4, 3.5) * Python (2.7, 3.2, 3.3, 3.4, 3.5)
* Django (1.7+, 1.8, 1.9) * Django (1.7+, 1.8, 1.9)
The following packages are optional: The following packages are optional:
* [Markdown][markdown] (2.1.0+) - Markdown support for the browsable API. * [Markdown][markdown] (2.1.0+) - Markdown support for the browsable API.
* [django-filter][django-filter] (0.9.2+) - Filtering support. * [django-filter][django-filter] (0.9.2+) - Filtering support.
* [django-crispy-forms][django-crispy-forms] - Improved HTML display for filtering.
* [django-guardian][django-guardian] (1.1.1+) - Object level permissions support. * [django-guardian][django-guardian] (1.1.1+) - Object level permissions support.
## Installation ## Installation
...@@ -191,7 +190,9 @@ The API guide is your complete reference manual to all the functionality provide ...@@ -191,7 +190,9 @@ The API guide is your complete reference manual to all the functionality provide
General guides to using REST framework. General guides to using REST framework.
* [Documenting your API][documenting-your-api] * [Documenting your API][documenting-your-api]
* [Internationalization][internationalization]
* [AJAX, CSRF & CORS][ajax-csrf-cors] * [AJAX, CSRF & CORS][ajax-csrf-cors]
* [HTML & Forms][html-and-forms]
* [Browser enhancements][browser-enhancements] * [Browser enhancements][browser-enhancements]
* [The Browsable API][browsableapi] * [The Browsable API][browsableapi]
* [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas] * [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas]
...@@ -201,7 +202,9 @@ General guides to using REST framework. ...@@ -201,7 +202,9 @@ General guides to using REST framework.
* [3.0 Announcement][3.0-announcement] * [3.0 Announcement][3.0-announcement]
* [3.1 Announcement][3.1-announcement] * [3.1 Announcement][3.1-announcement]
* [3.2 Announcement][3.2-announcement] * [3.2 Announcement][3.2-announcement]
* [3.3 Announcement][3.3-announcement]
* [Kickstarter Announcement][kickstarter-announcement] * [Kickstarter Announcement][kickstarter-announcement]
* [Funding][funding]
* [Release Notes][release-notes] * [Release Notes][release-notes]
## Development ## Development
...@@ -303,8 +306,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -303,8 +306,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[settings]: api-guide/settings.md [settings]: api-guide/settings.md
[documenting-your-api]: topics/documenting-your-api.md [documenting-your-api]: topics/documenting-your-api.md
[internationalization]: topics/documenting-your-api.md [internationalization]: topics/internationalization.md
[ajax-csrf-cors]: topics/ajax-csrf-cors.md [ajax-csrf-cors]: topics/ajax-csrf-cors.md
[html-and-forms]: topics/html-and-forms.md
[browser-enhancements]: topics/browser-enhancements.md [browser-enhancements]: topics/browser-enhancements.md
[browsableapi]: topics/browsable-api.md [browsableapi]: topics/browsable-api.md
[rest-hypermedia-hateoas]: topics/rest-hypermedia-hateoas.md [rest-hypermedia-hateoas]: topics/rest-hypermedia-hateoas.md
...@@ -314,7 +318,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ...@@ -314,7 +318,9 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[3.0-announcement]: topics/3.0-announcement.md [3.0-announcement]: topics/3.0-announcement.md
[3.1-announcement]: topics/3.1-announcement.md [3.1-announcement]: topics/3.1-announcement.md
[3.2-announcement]: topics/3.2-announcement.md [3.2-announcement]: topics/3.2-announcement.md
[3.3-announcement]: topics/3.3-announcement.md
[kickstarter-announcement]: topics/kickstarter-announcement.md [kickstarter-announcement]: topics/kickstarter-announcement.md
[funding]: topics/funding.md
[release-notes]: topics/release-notes.md [release-notes]: topics/release-notes.md
[tox]: http://testrun.org/tox/latest/ [tox]: http://testrun.org/tox/latest/
......
# Django REST framework 3.3
The 3.3 release marks the final work in the Kickstarter funded series. We'd like to offer a final resounding **thank you** to all our wonderful sponsors and supporters.
The amount of work that has been achieved as a direct result of the funding is immense. We've added a huge amounts of new functionality, resolved nearly 2,000 tickets, and redesigned & refined large parts of the project.
In order to continue driving REST framework forward, we're introducing [monthly paid plans](https://fund.django-rest-framework.org/topics/funding). These plans include various sponsorship rewards, and will ensure that the project remains sustainable and well supported.
We strongly believe that collaboratively funded software development yields outstanding results for a relatively low investment-per-head. If you or your company use REST framework commercially, then we would strongly urge you to participate in this latest funding drive, and help us continue to build an increasingly polished & professional product.
---
## Release notes
Significant new functionality in the 3.3 release includes:
* Filters presented as HTML controls in the browsable API.
* A [forms API][forms-api], allowing serializers to be rendered as HTML forms.
* Django 1.9 support.
* A [`JSONField` serializer field][jsonfield], corresponding to Django 1.9's Postgres `JSONField` model field.
* Browsable API support [via AJAX][ajax-form], rather than server side request overloading.
![Filter Controls](../img/filter-controls.png)
*Example of the new filter controls*
---
## Supported versions
This release drops support for Django 1.5 and 1.6. Django 1.7, 1.8 or 1.9 are now required.
This brings our supported versions into line with Django's [currently supported versions][django-supported-versions]
## Deprecations
The AJAX based support for the browsable API means that there are a number of internal cleanups in the `request` class. For the vast majority of developers this should largely remain transparent:
* To support form based `PUT` and `DELETE`, or to support form content types such as JSON, you should now use the [AJAX forms][ajax-forms] javascript library. This replaces the previous 'method and content type overloading' that required significant internal complexity to the request class.
* The `accept` query parameter is no longer supported by the default content negotiation class. If you require it then you'll need to [use a custom content negotiation class](browser-enhancements.md#url-based-accept-headers).
* The custom `HTTP_X_HTTP_METHOD_OVERRIDE` header is no longer supported by default. If you require it then you'll need to [use custom middleware](browser-enhancements.md#http-header-based-method-overriding).
The following pagination view attributes and settings have been moved into attributes on the pagination class since 3.1. Their usage was formerly deprecated, and has now been removed entirely, in line with the deprecation policy.
* `view.paginate_by` - Use `paginator.page_size` instead.
* `view.page_query_param` - Use `paginator.page_query_param` instead.
* `view.paginate_by_param` - Use `paginator.page_size_query_param` instead.
* `view.max_paginate_by` - Use `paginator.max_page_size` instead.
* `settings.PAGINATE_BY` - Use `paginator.page_size` instead.
* `settings.PAGINATE_BY_PARAM` - Use `paginator.page_size_query_param` instead.
* `settings.MAX_PAGINATE_BY` - Use `paginator.max_page_size` instead.
The `ModelSerializer` and `HyperlinkedModelSerializer` classes should now include either a `fields` or `exclude` option, although the `fields = '__all__'` shortcut may be used. Failing to include either of these two options is currently pending deprecation, and will be removed entirely in the 3.5 release. This behavior brings `ModelSerializer` more closely in line with Django's `ModelForm` behavior.
[forms-api]: html-and-forms.md
[ajax-form]: https://github.com/tomchristie/ajax-form
[jsonfield]: ../../api-guide/fields#jsonfield
[django-supported-versions]: https://www.djangoproject.com/download/#supported-versions
\ No newline at end of file
# HTML & Forms
REST framework is suitable for returning both API style responses, and regular HTML pages. Additionally, serializers can used as HTML forms and rendered in templates.
## Rendering HTML
In order to return HTML responses you'll need to either `TemplateHTMLRenderer`, or `StaticHTMLRenderer`.
The `TemplateHTMLRenderer` class expects the response to contain a dictionary of context data, and renders an HTML page based on a template that must be specified either in the view or on the response.
The `StaticHTMLRender` class expects the response to contain a string of the pre-rendered HTML content.
Because static HTML pages typically have different behavior from API responses you'll probably need to write any HTML views explicitly, rather than relying on the built-in generic views.
Here's an example of a view that returns a list of "Profile" instances, rendered in an HTML template:
**views.py**:
from my_project.example.models import Profile
from rest_framework.renderers import TemplateHTMLRenderer
from rest_framework.views import APIView
class ProfileList(APIView):
renderer_classes = [TemplateHTMLRenderer]
template_name = 'profile_list.html'
def get(self, request):
queryset = Profile.objects.all()
return Response({'profiles': queryset})
**profile_list.html**:
<html><body>
<h1>Profiles</h1>
<ul>
{% for profile in profiles %}
<li>{{ profile.name }}</li>
{% endfor %}
</ul>
</body></html>
## Rendering Forms
Serializers may be rendered as forms by using the `render_form` template tag, and including the serializer instance as context to the template.
The following view demonstrates an example of using a serializer in a template for viewing and updating a model instance:
**views.py**:
from django.shortcuts import get_object_or_404
from my_project.example.models import Profile
from rest_framework.renderers import TemplateHTMLRenderer
from rest_framework.views import APIView
class ProfileDetail(APIView):
renderer_classes = [TemplateHTMLRenderer]
template_name = 'profile_detail.html'
def get(self, request, pk):
profile = get_object_or_404(Profile, pk=pk)
serializer = ProfileSerializer(profile)
return Response({'serializer': serializer, 'profile': profile})
def post(self, request, pk):
profile = get_object_or_404(Profile, pk=pk)
serializer = ProfileSerializer(profile)
if not serializer.is_valid():
return Response({'serializer': serializer, 'profile': profile}) return redirect('profile-list')
**profile_detail.html**:
{% load rest_framework %}
<html><body>
<h1>Profile - {{ profile.name }}</h1>
<form action="{% url 'profile-detail' pk=profile.pk '%}" method="POST">
{% csrf_token %}
{% render_form serializer %}
<input type="submit" value="Save">
</form>
</body></html>
### Using template packs
The `render_form` tag takes an optional `template_pack` argument, that specifies which template directory should be used for rendering the form and form fields.
REST framework includes three built-in template packs, all based on Bootstrap 3. The built-in styles are `horizontal`, `vertical`, and `inline`. The default style is `horizontal`. To use any of these template packs you'll want to also include the Bootstrap 3 CSS.
The following HTML will link to a CDN hosted version of the Bootstrap 3 CSS:
<head>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
</head>
Third party packages may include alternate template packs, by bundling a template directory containing the necessary form and field templates.
Let's take a look at how to render each of the three available template packs. For these examples we'll use a single serializer class to present a "Login" form.
class LoginSerializer(serializers.Serializer):
email = serializers.EmailField(
max_length=100,
style={'placeholder': 'Email'}
)
password = serializers.CharField(
max_length=100,
style={'input_type': 'password', 'placeholder': 'Password'}
)
remember_me = serializers.BooleanField() ---
#### `rest_framework/vertical`
Presents form labels above their corresponding control inputs, using the standard Bootstrap layout.
*This is the default template pack.*
{% load rest_framework %}
...
<form action="{% url 'login' %}" method="post" novalidate>
{% csrf_token %}
{% render_form serializer template_pack='rest_framework/vertical' %}
<button type="submit" class="btn btn-default">Sign in</button>
</form>
![Vertical form example](../img/vertical.png)
---
#### `rest_framework/horizontal`
Presents labels and controls alongside each other, using a 2/10 column split.
*This is the form style used in the browsable API and admin renderers.*
{% load rest_framework %}
...
<form class="form-horizontal" action="{% url 'login' %}" method="post" novalidate>
{% csrf_token %}
{% render_form serializer %}
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default">Sign in</button>
</div>
</div>
</form>
![Horizontal form example](../img/horizontal.png)
---
#### `rest_framework/inline`
A compact form style that presents all the controls inline.
{% load rest_framework %}
...
<form class="form-inline" action="{% url 'login' %}" method="post" novalidate>
{% csrf_token %}
{% render_form serializer template_pack='rest_framework/inline' %}
<button type="submit" class="btn btn-default">Sign in</button>
</form>
![Inline form example](../img/inline.png)
## Field styles
Serializer fields can have their rendering style customized by using the `style` keyword argument. This argument is a dictionary of options that control the template and layout used.
The most common way to customize the field style is to use the `base_template` style keyword argument to select which template in the template pack should be use.
For example, to render a `CharField` as an HTML textarea rather than the default HTML input, you would use something like this:
details = serializers.CharField(
max_length=1000,
style={'base_template': 'textarea.html'}
)
If you instead want a field to be rendered using a custom template that is *not part of an included template pack*, you can instead use the `template` style option, to fully specify a template name:
details = serializers.CharField(
max_length=1000,
style={'template': 'my-field-templates/custom-input.html'}
)
Field templates can also use additional style properties, depending on their type. For example, the `textarea.html` template also accepts a `rows` property that can be used to affect the sizing of the control.
details = serializers.CharField(
max_length=1000,
style={'base_template': 'textarea.html', 'rows': 10}
)
The complete list of `base_template` options and their associated style options is listed below.
base_template | Valid field types | Additional style options
----|----|----
input.html | Any string, numeric or date/time field | input_type, placeholder, hide_label
textarea.html | `CharField` | rows, placeholder, hide_label
select.html | `ChoiceField` or relational field types | hide_label
radio.html | `ChoiceField` or relational field types | inline, hide_label
select_multiple.html | `MultipleChoiceField` or relational fields with `many=True` | hide_label
checkbox_multiple.html | `MultipleChoiceField` or relational fields with `many=True` | inline, hide_label
checkbox.html | `BooleanField` | hide_label
fieldset.html | Nested serializer | hide_label
list_fieldset.html | `ListField` or nested serializer with `many=True` | hide_label
...@@ -42,9 +42,19 @@ You can determine your currently installed version using `pip freeze`: ...@@ -42,9 +42,19 @@ You can determine your currently installed version using `pip freeze`:
### 3.3.0 ### 3.3.0
**Date**: NOT YET RELEASED **Date**: [27th October 2015][3.3.0-milestone]
* Removed support for Django Versions 1.5 & 1.6 ([#3421][gh3421], [#3429][gh3429]) * HTML controls for filters. ([#3315][gh3315])
* Forms API. ([#3475][gh3475])
* AJAX browsable API. ([#3410][gh3410])
* Added JSONField. ([#3454][gh3454])
* Correctly map `to_field` when creating `ModelSerializer` relational fields. ([#3526][gh3526])
* Include keyword arguments when mapping `FilePathField` to a serializer field. ([#3536][gh3536])
* Map appropriate model `error_messages` on `ModelSerializer` uniqueness constraints. ([#3435][gh3435])
* Include `max_length` constraint for `ModelSerializer` fields mapped from TextField. ([#3509][gh3509])
* Added support for Django 1.9. ([#3450][gh3450], [#3525][gh3525])
* Removed support for Django 1.5 & 1.6. ([#3421][gh3421], [#3429][gh3429])
* Removed 'south' migrations. ([#3495][gh3495])
## 3.2.x series ## 3.2.x series
...@@ -543,5 +553,17 @@ For older release notes, [please see the version 2.x documentation][old-release- ...@@ -543,5 +553,17 @@ For older release notes, [please see the version 2.x documentation][old-release-
[gh3415]: https://github.com/tomchristie/django-rest-framework/issues/3415 [gh3415]: https://github.com/tomchristie/django-rest-framework/issues/3415
<!-- 3.3.0 --> <!-- 3.3.0 -->
[gh3421]: https://github.com/tomchristie/django-rest-framework/pulls/3421 [gh3315]: https://github.com/tomchristie/django-rest-framework/issues/3315
[gh3429]: https://github.com/tomchristie/django-rest-framework/pull/3429 [gh3410]: https://github.com/tomchristie/django-rest-framework/issues/3410
[gh3435]: https://github.com/tomchristie/django-rest-framework/issues/3435
[gh3450]: https://github.com/tomchristie/django-rest-framework/issues/3450
[gh3454]: https://github.com/tomchristie/django-rest-framework/issues/3454
[gh3475]: https://github.com/tomchristie/django-rest-framework/issues/3475
[gh3495]: https://github.com/tomchristie/django-rest-framework/issues/3495
[gh3509]: https://github.com/tomchristie/django-rest-framework/issues/3509
[gh3421]: https://github.com/tomchristie/django-rest-framework/issues/3421
[gh3525]: https://github.com/tomchristie/django-rest-framework/issues/3525
[gh3526]: https://github.com/tomchristie/django-rest-framework/issues/3526
[gh3429]: https://github.com/tomchristie/django-rest-framework/issues/3429
[gh3536]: https://github.com/tomchristie/django-rest-framework/issues/3536
...@@ -237,6 +237,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque ...@@ -237,6 +237,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
### Misc ### Misc
* [cookiecutter-django-rest][cookiecutter-django-rest] - A cookiecutter template that takes care of the setup and configuration so you can focus on making your REST apis awesome.
* [djangorestrelationalhyperlink][djangorestrelationalhyperlink] - A hyperlinked serialiser that can can be used to alter relationships via hyperlinks, but otherwise like a hyperlink model serializer. * [djangorestrelationalhyperlink][djangorestrelationalhyperlink] - A hyperlinked serialiser that can can be used to alter relationships via hyperlinks, but otherwise like a hyperlink model serializer.
* [django-rest-swagger][django-rest-swagger] - An API documentation generator for Swagger UI. * [django-rest-swagger][django-rest-swagger] - An API documentation generator for Swagger UI.
* [django-rest-framework-proxy][django-rest-framework-proxy] - Proxy to redirect incoming request to another API server. * [django-rest-framework-proxy][django-rest-framework-proxy] - Proxy to redirect incoming request to another API server.
...@@ -346,4 +347,5 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque ...@@ -346,4 +347,5 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque
[django-rest-framework-braces]: https://github.com/dealertrack/django-rest-framework-braces [django-rest-framework-braces]: https://github.com/dealertrack/django-rest-framework-braces
[dry-rest-permissions]: https://github.com/Helioscene/dry-rest-permissions [dry-rest-permissions]: https://github.com/Helioscene/dry-rest-permissions
[django-url-filter]: https://github.com/miki725/django-url-filter [django-url-filter]: https://github.com/miki725/django-url-filter
[cookiecutter-django-rest]: https://github.com/agconti/cookiecutter-django-rest
[drf-haystack]: http://drf-haystack.readthedocs.org/en/latest/ [drf-haystack]: http://drf-haystack.readthedocs.org/en/latest/
...@@ -47,6 +47,7 @@ pages: ...@@ -47,6 +47,7 @@ pages:
- 'Documenting your API': 'topics/documenting-your-api.md' - 'Documenting your API': 'topics/documenting-your-api.md'
- 'Internationalization': 'topics/internationalization.md' - 'Internationalization': 'topics/internationalization.md'
- 'AJAX, CSRF & CORS': 'topics/ajax-csrf-cors.md' - 'AJAX, CSRF & CORS': 'topics/ajax-csrf-cors.md'
- 'HTML & Forms': 'topics/html-and-forms.md'
- 'Browser Enhancements': 'topics/browser-enhancements.md' - 'Browser Enhancements': 'topics/browser-enhancements.md'
- 'The Browsable API': 'topics/browsable-api.md' - 'The Browsable API': 'topics/browsable-api.md'
- 'REST, Hypermedia & HATEOAS': 'topics/rest-hypermedia-hateoas.md' - 'REST, Hypermedia & HATEOAS': 'topics/rest-hypermedia-hateoas.md'
...@@ -56,5 +57,7 @@ pages: ...@@ -56,5 +57,7 @@ pages:
- '3.0 Announcement': 'topics/3.0-announcement.md' - '3.0 Announcement': 'topics/3.0-announcement.md'
- '3.1 Announcement': 'topics/3.1-announcement.md' - '3.1 Announcement': 'topics/3.1-announcement.md'
- '3.2 Announcement': 'topics/3.2-announcement.md' - '3.2 Announcement': 'topics/3.2-announcement.md'
- '3.3 Announcement': 'topics/3.3-announcement.md'
- 'Kickstarter Announcement': 'topics/kickstarter-announcement.md' - 'Kickstarter Announcement': 'topics/kickstarter-announcement.md'
- 'Funding': 'topics/funding.md'
- 'Release Notes': 'topics/release-notes.md' - 'Release Notes': 'topics/release-notes.md'
# -*- coding: utf-8 -*-
from south.db import db
from south.v2 import SchemaMigration
try:
from django.contrib.auth import get_user_model
except ImportError: # django < 1.5
from django.contrib.auth.models import User
else:
User = get_user_model()
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'Token'
db.create_table('authtoken_token', (
('key', self.gf('django.db.models.fields.CharField')(max_length=40, primary_key=True)),
('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='auth_token', unique=True, to=orm['%s.%s' % (User._meta.app_label, User._meta.object_name)])),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
))
db.send_create_signal('authtoken', ['Token'])
def backwards(self, orm):
# Deleting model 'Token'
db.delete_table('authtoken_token')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
"%s.%s" % (User._meta.app_label, User._meta.module_name): {
'Meta': {'object_name': User._meta.module_name, 'db_table': repr(User._meta.db_table)},
},
'authtoken.token': {
'Meta': {'object_name': 'Token'},
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'key': ('django.db.models.fields.CharField', [], {'max_length': '40', 'primary_key': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'auth_token'", 'unique': 'True', 'to': "orm['%s.%s']" % (User._meta.app_label, User._meta.object_name)})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['authtoken']
...@@ -77,6 +77,26 @@ try: ...@@ -77,6 +77,26 @@ try:
except ImportError: except ImportError:
django_filters = None django_filters = None
# django-crispy-forms is optional
try:
import crispy_forms
except ImportError:
crispy_forms = None
if django.VERSION >= (1, 6):
def clean_manytomany_helptext(text):
return text
else:
# Up to version 1.5 many to many fields automatically suffix
# the `help_text` attribute with hardcoded text.
def clean_manytomany_helptext(text):
if text.endswith(' Hold down "Control", or "Command" on a Mac, to select more than one.'):
text = text[:-69]
return text
# Django-guardian is optional. Import only if guardian is in INSTALLED_APPS # Django-guardian is optional. Import only if guardian is in INSTALLED_APPS
# Fixes (#1712). We keep the try/except for the test suite. # Fixes (#1712). We keep the try/except for the test suite.
guardian = None guardian = None
......
...@@ -30,10 +30,10 @@ def _force_text_recursive(data): ...@@ -30,10 +30,10 @@ def _force_text_recursive(data):
return ReturnList(ret, serializer=data.serializer) return ReturnList(ret, serializer=data.serializer)
return data return data
elif isinstance(data, dict): elif isinstance(data, dict):
ret = dict([ ret = {
(key, _force_text_recursive(value)) key: _force_text_recursive(value)
for key, value in data.items() for key, value in data.items()
]) }
if isinstance(data, ReturnDict): if isinstance(data, ReturnDict):
return ReturnDict(ret, serializer=data.serializer) return ReturnDict(ret, serializer=data.serializer)
return data return data
......
...@@ -604,8 +604,8 @@ class BooleanField(Field): ...@@ -604,8 +604,8 @@ class BooleanField(Field):
} }
default_empty_html = False default_empty_html = False
initial = False initial = False
TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True)) TRUE_VALUES = {'t', 'T', 'true', 'True', 'TRUE', '1', 1, True}
FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False)) FALSE_VALUES = {'f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False}
def __init__(self, **kwargs): def __init__(self, **kwargs):
assert 'allow_null' not in kwargs, '`allow_null` is not a valid option. Use `NullBooleanField` instead.' assert 'allow_null' not in kwargs, '`allow_null` is not a valid option. Use `NullBooleanField` instead.'
...@@ -634,9 +634,9 @@ class NullBooleanField(Field): ...@@ -634,9 +634,9 @@ class NullBooleanField(Field):
'invalid': _('"{input}" is not a valid boolean.') 'invalid': _('"{input}" is not a valid boolean.')
} }
initial = None initial = None
TRUE_VALUES = set(('t', 'T', 'true', 'True', 'TRUE', '1', 1, True)) TRUE_VALUES = {'t', 'T', 'true', 'True', 'TRUE', '1', 1, True}
FALSE_VALUES = set(('f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False)) FALSE_VALUES = {'f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False}
NULL_VALUES = set(('n', 'N', 'null', 'Null', 'NULL', '', None)) NULL_VALUES = {'n', 'N', 'null', 'Null', 'NULL', '', None}
def __init__(self, **kwargs): def __init__(self, **kwargs):
assert 'allow_null' not in kwargs, '`allow_null` is not a valid option.' assert 'allow_null' not in kwargs, '`allow_null` is not a valid option.'
...@@ -1241,9 +1241,9 @@ class ChoiceField(Field): ...@@ -1241,9 +1241,9 @@ class ChoiceField(Field):
# Map the string representation of choices to the underlying value. # Map the string representation of choices to the underlying value.
# Allows us to deal with eg. integer choices while supporting either # Allows us to deal with eg. integer choices while supporting either
# integer or string input, but still get the correct datatype out. # integer or string input, but still get the correct datatype out.
self.choice_strings_to_values = dict([ self.choice_strings_to_values = {
(six.text_type(key), key) for key in self.choices.keys() six.text_type(key): key for key in self.choices.keys()
]) }
self.allow_blank = kwargs.pop('allow_blank', False) self.allow_blank = kwargs.pop('allow_blank', False)
...@@ -1302,15 +1302,15 @@ class MultipleChoiceField(ChoiceField): ...@@ -1302,15 +1302,15 @@ class MultipleChoiceField(ChoiceField):
if not self.allow_empty and len(data) == 0: if not self.allow_empty and len(data) == 0:
self.fail('empty') self.fail('empty')
return set([ return {
super(MultipleChoiceField, self).to_internal_value(item) super(MultipleChoiceField, self).to_internal_value(item)
for item in data for item in data
]) }
def to_representation(self, value): def to_representation(self, value):
return set([ return {
self.choice_strings_to_values.get(six.text_type(item), item) for item in value self.choice_strings_to_values.get(six.text_type(item), item) for item in value
]) }
class FilePathField(ChoiceField): class FilePathField(ChoiceField):
...@@ -1508,19 +1508,19 @@ class DictField(Field): ...@@ -1508,19 +1508,19 @@ class DictField(Field):
data = html.parse_html_dict(data) data = html.parse_html_dict(data)
if not isinstance(data, dict): if not isinstance(data, dict):
self.fail('not_a_dict', input_type=type(data).__name__) self.fail('not_a_dict', input_type=type(data).__name__)
return dict([ return {
(six.text_type(key), self.child.run_validation(value)) six.text_type(key): self.child.run_validation(value)
for key, value in data.items() for key, value in data.items()
]) }
def to_representation(self, value): def to_representation(self, value):
""" """
List of object instances -> List of dicts of primitive datatypes. List of object instances -> List of dicts of primitive datatypes.
""" """
return dict([ return {
(six.text_type(key), self.child.to_representation(val)) six.text_type(key): self.child.to_representation(val)
for key, val in value.items() for key, val in value.items()
]) }
class JSONField(Field): class JSONField(Field):
......
...@@ -7,14 +7,57 @@ from __future__ import unicode_literals ...@@ -7,14 +7,57 @@ from __future__ import unicode_literals
import operator import operator
from functools import reduce from functools import reduce
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db import models from django.db import models
from django.template import Context, loader
from django.utils import six from django.utils import six
from django.utils.translation import ugettext_lazy as _
from rest_framework.compat import distinct, django_filters, guardian from rest_framework.compat import (
crispy_forms, distinct, django_filters, guardian
)
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
FilterSet = django_filters and django_filters.FilterSet or None if 'crispy_forms' in settings.INSTALLED_APPS and crispy_forms and django_filters:
# If django-crispy-forms is installed, use it to get a bootstrap3 rendering
# of the DjangoFilterBackend controls when displayed as HTML.
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit
class FilterSet(django_filters.FilterSet):
def __init__(self, *args, **kwargs):
super(FilterSet, self).__init__(*args, **kwargs)
for field in self.form.fields.values():
field.help_text = None
layout_components = list(self.form.fields.keys()) + [
Submit('', _('Submit'), css_class='btn-default'),
]
helper = FormHelper()
helper.form_method = 'GET'
helper.template_pack = 'bootstrap3'
helper.layout = Layout(*layout_components)
self.form.helper = helper
filter_template = 'rest_framework/filters/django_filter_crispyforms.html'
elif django_filters:
# If django-crispy-forms is not installed, use the standard
# 'form.as_p' rendering when DjangoFilterBackend is displayed as HTML.
class FilterSet(django_filters.FilterSet):
def __init__(self, *args, **kwargs):
super(FilterSet, self).__init__(*args, **kwargs)
for field in self.form.fields.values():
field.help_text = None
filter_template = 'rest_framework/filters/django_filter.html'
else:
FilterSet = None
filter_template = None
class BaseFilterBackend(object): class BaseFilterBackend(object):
...@@ -34,6 +77,7 @@ class DjangoFilterBackend(BaseFilterBackend): ...@@ -34,6 +77,7 @@ class DjangoFilterBackend(BaseFilterBackend):
A filter backend that uses django-filter. A filter backend that uses django-filter.
""" """
default_filter_set = FilterSet default_filter_set = FilterSet
template = filter_template
def __init__(self): def __init__(self):
assert django_filters, 'Using DjangoFilterBackend, but django-filter is not installed' assert django_filters, 'Using DjangoFilterBackend, but django-filter is not installed'
...@@ -55,7 +99,7 @@ class DjangoFilterBackend(BaseFilterBackend): ...@@ -55,7 +99,7 @@ class DjangoFilterBackend(BaseFilterBackend):
return filter_class return filter_class
if filter_fields: if filter_fields:
class AutoFilterSet(self.default_filter_set): class AutoFilterSet(FilterSet):
class Meta: class Meta:
model = queryset.model model = queryset.model
fields = filter_fields fields = filter_fields
...@@ -72,10 +116,20 @@ class DjangoFilterBackend(BaseFilterBackend): ...@@ -72,10 +116,20 @@ class DjangoFilterBackend(BaseFilterBackend):
return queryset return queryset
def to_html(self, request, queryset, view):
cls = self.get_filter_class(view, queryset)
filter_instance = cls(request.query_params, queryset=queryset)
context = Context({
'filter': filter_instance
})
template = loader.get_template(self.template)
return template.render(context)
class SearchFilter(BaseFilterBackend): class SearchFilter(BaseFilterBackend):
# The URL query parameter used for the search. # The URL query parameter used for the search.
search_param = api_settings.SEARCH_PARAM search_param = api_settings.SEARCH_PARAM
template = 'rest_framework/filters/search.html'
def get_search_terms(self, request): def get_search_terms(self, request):
""" """
...@@ -99,7 +153,6 @@ class SearchFilter(BaseFilterBackend): ...@@ -99,7 +153,6 @@ class SearchFilter(BaseFilterBackend):
def filter_queryset(self, request, queryset, view): def filter_queryset(self, request, queryset, view):
search_fields = getattr(view, 'search_fields', None) search_fields = getattr(view, 'search_fields', None)
search_terms = self.get_search_terms(request) search_terms = self.get_search_terms(request)
if not search_fields or not search_terms: if not search_fields or not search_terms:
...@@ -123,11 +176,25 @@ class SearchFilter(BaseFilterBackend): ...@@ -123,11 +176,25 @@ class SearchFilter(BaseFilterBackend):
# in the resulting queryset. # in the resulting queryset.
return distinct(queryset, base) return distinct(queryset, base)
def to_html(self, request, queryset, view):
if not getattr(view, 'search_fields', None):
return ''
term = self.get_search_terms(request)
term = term[0] if term else ''
context = Context({
'param': self.search_param,
'term': term
})
template = loader.get_template(self.template)
return template.render(context)
class OrderingFilter(BaseFilterBackend): class OrderingFilter(BaseFilterBackend):
# The URL query parameter used for the ordering. # The URL query parameter used for the ordering.
ordering_param = api_settings.ORDERING_PARAM ordering_param = api_settings.ORDERING_PARAM
ordering_fields = None ordering_fields = None
template = 'rest_framework/filters/ordering.html'
def get_ordering(self, request, queryset, view): def get_ordering(self, request, queryset, view):
""" """
...@@ -153,7 +220,7 @@ class OrderingFilter(BaseFilterBackend): ...@@ -153,7 +220,7 @@ class OrderingFilter(BaseFilterBackend):
return (ordering,) return (ordering,)
return ordering return ordering
def remove_invalid_fields(self, queryset, fields, view): def get_valid_fields(self, queryset, view):
valid_fields = getattr(view, 'ordering_fields', self.ordering_fields) valid_fields = getattr(view, 'ordering_fields', self.ordering_fields)
if valid_fields is None: if valid_fields is None:
...@@ -164,15 +231,30 @@ class OrderingFilter(BaseFilterBackend): ...@@ -164,15 +231,30 @@ class OrderingFilter(BaseFilterBackend):
"'serializer_class' or 'ordering_fields' attribute.") "'serializer_class' or 'ordering_fields' attribute.")
raise ImproperlyConfigured(msg % self.__class__.__name__) raise ImproperlyConfigured(msg % self.__class__.__name__)
valid_fields = [ valid_fields = [
field.source or field_name (field.source or field_name, field.label)
for field_name, field in serializer_class().fields.items() for field_name, field in serializer_class().fields.items()
if not getattr(field, 'write_only', False) if not getattr(field, 'write_only', False) and not field.source == '*'
] ]
elif valid_fields == '__all__': elif valid_fields == '__all__':
# View explicitly allows filtering on any model field # View explicitly allows filtering on any model field
valid_fields = [field.name for field in queryset.model._meta.fields] valid_fields = [
valid_fields += queryset.query.aggregates.keys() (field.name, getattr(field, 'label', field.name.title()))
for field in queryset.model._meta.fields
]
valid_fields += [
(key, key.title().split('__'))
for key in queryset.query.aggregates.keys()
]
else:
valid_fields = [
(item, item) if isinstance(item, six.string_types) else item
for item in valid_fields
]
return valid_fields
def remove_invalid_fields(self, queryset, fields, view):
valid_fields = [item[0] for item in self.get_valid_fields(queryset, view)]
return [term for term in fields if term.lstrip('-') in valid_fields] return [term for term in fields if term.lstrip('-') in valid_fields]
def filter_queryset(self, request, queryset, view): def filter_queryset(self, request, queryset, view):
...@@ -183,6 +265,25 @@ class OrderingFilter(BaseFilterBackend): ...@@ -183,6 +265,25 @@ class OrderingFilter(BaseFilterBackend):
return queryset return queryset
def get_template_context(self, request, queryset, view):
current = self.get_ordering(request, queryset, view)
current = None if current is None else current[0]
options = []
for key, label in self.get_valid_fields(queryset, view):
options.append((key, '%s - ascending' % label))
options.append(('-' + key, '%s - descending' % label))
return {
'request': request,
'current': current,
'param': self.ordering_param,
'options': options,
}
def to_html(self, request, queryset, view):
template = loader.get_template(self.template)
context = Context(self.get_template_context(request, queryset, view))
return template.render(context)
class DjangoObjectPermissionsFilter(BaseFilterBackend): class DjangoObjectPermissionsFilter(BaseFilterBackend):
""" """
......
...@@ -77,7 +77,7 @@ class SimpleMetadata(BaseMetadata): ...@@ -77,7 +77,7 @@ class SimpleMetadata(BaseMetadata):
the fields that are accepted for 'PUT' and 'POST' methods. the fields that are accepted for 'PUT' and 'POST' methods.
""" """
actions = {} actions = {}
for method in set(['PUT', 'POST']) & set(view.allowed_methods): for method in {'PUT', 'POST'} & set(view.allowed_methods):
view.request = clone_request(request, method) view.request = clone_request(request, method)
try: try:
# Test global permissions # Test global permissions
......
...@@ -5,7 +5,6 @@ be used for paginated responses. ...@@ -5,7 +5,6 @@ be used for paginated responses.
""" """
from __future__ import unicode_literals from __future__ import unicode_literals
import warnings
from base64 import b64decode, b64encode from base64 import b64decode, b64encode
from collections import OrderedDict, namedtuple from collections import OrderedDict, namedtuple
...@@ -79,11 +78,7 @@ def _get_displayed_page_numbers(current, final): ...@@ -79,11 +78,7 @@ def _get_displayed_page_numbers(current, final):
# We always include the first two pages, last two pages, and # We always include the first two pages, last two pages, and
# two pages either side of the current page. # two pages either side of the current page.
included = set(( included = {1, current - 1, current, current + 1, final}
1,
current - 1, current, current + 1,
final
))
# If the break would only exclude a single page number then we # If the break would only exclude a single page number then we
# may as well include the page number instead of the break. # may as well include the page number instead of the break.
...@@ -190,63 +185,11 @@ class PageNumberPagination(BasePagination): ...@@ -190,63 +185,11 @@ class PageNumberPagination(BasePagination):
invalid_page_message = _('Invalid page "{page_number}": {message}.') invalid_page_message = _('Invalid page "{page_number}": {message}.')
def _handle_backwards_compat(self, view):
"""
Prior to version 3.1, pagination was handled in the view, and the
attributes were set there. The attributes should now be set on
the pagination class. The old style continues to work but is deprecated
and will be fully removed in version 3.3.
"""
assert not (
getattr(view, 'pagination_serializer_class', None) or
getattr(api_settings, 'DEFAULT_PAGINATION_SERIALIZER_CLASS', None)
), (
"The pagination_serializer_class attribute and "
"DEFAULT_PAGINATION_SERIALIZER_CLASS setting have been removed as "
"part of the 3.1 pagination API improvement. See the pagination "
"documentation for details on the new API."
)
for (settings_key, attr_name) in (
('PAGINATE_BY', 'page_size'),
('PAGINATE_BY_PARAM', 'page_size_query_param'),
('MAX_PAGINATE_BY', 'max_page_size')
):
value = getattr(api_settings, settings_key, None)
if value is not None:
setattr(self, attr_name, value)
warnings.warn(
"The `%s` settings key is deprecated. "
"Use the `%s` attribute on the pagination class instead." % (
settings_key, attr_name
),
DeprecationWarning,
)
for (view_attr, attr_name) in (
('paginate_by', 'page_size'),
('page_query_param', 'page_query_param'),
('paginate_by_param', 'page_size_query_param'),
('max_paginate_by', 'max_page_size')
):
value = getattr(view, view_attr, None)
if value is not None:
setattr(self, attr_name, value)
warnings.warn(
"The `%s` view attribute is deprecated. "
"Use the `%s` attribute on the pagination class instead." % (
view_attr, attr_name
),
DeprecationWarning,
)
def paginate_queryset(self, queryset, request, view=None): def paginate_queryset(self, queryset, request, view=None):
""" """
Paginate a queryset if required, either returning a Paginate a queryset if required, either returning a
page object, or `None` if pagination is not configured for this view. page object, or `None` if pagination is not configured for this view.
""" """
self._handle_backwards_compat(view)
page_size = self.get_page_size(request) page_size = self.get_page_size(request)
if not page_size: if not page_size:
return None return None
......
...@@ -249,7 +249,7 @@ class HTMLFormRenderer(BaseRenderer): ...@@ -249,7 +249,7 @@ class HTMLFormRenderer(BaseRenderer):
media_type = 'text/html' media_type = 'text/html'
format = 'form' format = 'form'
charset = 'utf-8' charset = 'utf-8'
template_pack = 'rest_framework/horizontal/' template_pack = 'rest_framework/vertical/'
base_template = 'form.html' base_template = 'form.html'
default_style = ClassLookupDict({ default_style = ClassLookupDict({
...@@ -341,26 +341,16 @@ class HTMLFormRenderer(BaseRenderer): ...@@ -341,26 +341,16 @@ class HTMLFormRenderer(BaseRenderer):
Render serializer data and return an HTML form, as a string. Render serializer data and return an HTML form, as a string.
""" """
form = data.serializer form = data.serializer
meta = getattr(form, 'Meta', None)
style = getattr(meta, 'style', {}) style = renderer_context.get('style', {})
if 'template_pack' not in style: if 'template_pack' not in style:
style['template_pack'] = self.template_pack style['template_pack'] = self.template_pack
if 'base_template' not in style:
style['base_template'] = self.base_template
style['renderer'] = self style['renderer'] = self
# This API needs to be finessed and finalized for 3.1 template_pack = style['template_pack'].strip('/')
if 'template' in renderer_context: template_name = template_pack + '/' + self.base_template
template_name = renderer_context['template']
elif 'template' in style:
template_name = style['template']
else:
template_name = style['template_pack'].strip('/') + '/' + style['base_template']
renderer_context = renderer_context or {}
request = renderer_context['request']
template = loader.get_template(template_name) template = loader.get_template(template_name)
context = RequestContext(request, { context = Context({
'form': form, 'form': form,
'style': style 'style': style
}) })
...@@ -374,6 +364,7 @@ class BrowsableAPIRenderer(BaseRenderer): ...@@ -374,6 +364,7 @@ class BrowsableAPIRenderer(BaseRenderer):
media_type = 'text/html' media_type = 'text/html'
format = 'api' format = 'api'
template = 'rest_framework/api.html' template = 'rest_framework/api.html'
filter_template = 'rest_framework/filters/base.html'
charset = 'utf-8' charset = 'utf-8'
form_renderer_class = HTMLFormRenderer form_renderer_class = HTMLFormRenderer
...@@ -505,10 +496,7 @@ class BrowsableAPIRenderer(BaseRenderer): ...@@ -505,10 +496,7 @@ class BrowsableAPIRenderer(BaseRenderer):
return form_renderer.render( return form_renderer.render(
serializer.data, serializer.data,
self.accepted_media_type, self.accepted_media_type,
dict( {'style': {'template_pack': 'rest_framework/horizontal'}}
list(self.renderer_context.items()) +
[('template', 'rest_framework/api_form.html')]
)
) )
def get_raw_data_form(self, data, view, method, request): def get_raw_data_form(self, data, view, method, request):
...@@ -584,6 +572,37 @@ class BrowsableAPIRenderer(BaseRenderer): ...@@ -584,6 +572,37 @@ class BrowsableAPIRenderer(BaseRenderer):
def get_breadcrumbs(self, request): def get_breadcrumbs(self, request):
return get_breadcrumbs(request.path, request) return get_breadcrumbs(request.path, request)
def get_filter_form(self, data, view, request):
if not hasattr(view, 'get_queryset') or not hasattr(view, 'filter_backends'):
return
# Infer if this is a list view or not.
paginator = getattr(view, 'paginator', None)
if isinstance(data, list):
pass
elif (paginator is not None and data is not None):
try:
paginator.get_results(data)
except (TypeError, KeyError):
return
elif not isinstance(data, list):
return
queryset = view.get_queryset()
elements = []
for backend in view.filter_backends:
if hasattr(backend, 'to_html'):
html = backend().to_html(request, queryset, view)
if html:
elements.append(html)
if not elements:
return
template = loader.get_template(self.filter_template)
context = Context({'elements': elements})
return template.render(context)
def get_context(self, data, accepted_media_type, renderer_context): def get_context(self, data, accepted_media_type, renderer_context):
""" """
Returns the context used to render. Returns the context used to render.
...@@ -631,6 +650,8 @@ class BrowsableAPIRenderer(BaseRenderer): ...@@ -631,6 +650,8 @@ class BrowsableAPIRenderer(BaseRenderer):
'delete_form': self.get_rendered_html_form(data, view, 'DELETE', request), 'delete_form': self.get_rendered_html_form(data, view, 'DELETE', request),
'options_form': self.get_rendered_html_form(data, view, 'OPTIONS', request), 'options_form': self.get_rendered_html_form(data, view, 'OPTIONS', request),
'filter_form': self.get_filter_form(data, view, request),
'raw_data_put_form': raw_data_put_form, 'raw_data_put_form': raw_data_put_form,
'raw_data_post_form': raw_data_post_form, 'raw_data_post_form': raw_data_post_form,
'raw_data_patch_form': raw_data_patch_form, 'raw_data_patch_form': raw_data_patch_form,
......
...@@ -174,7 +174,7 @@ class SimpleRouter(BaseRouter): ...@@ -174,7 +174,7 @@ class SimpleRouter(BaseRouter):
url_path = initkwargs.pop("url_path", None) or methodname url_path = initkwargs.pop("url_path", None) or methodname
ret.append(Route( ret.append(Route(
url=replace_methodname(route.url, url_path), url=replace_methodname(route.url, url_path),
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods), mapping={httpmethod: methodname for httpmethod in httpmethods},
name=replace_methodname(route.name, url_path), name=replace_methodname(route.name, url_path),
initkwargs=initkwargs, initkwargs=initkwargs,
)) ))
......
...@@ -125,10 +125,10 @@ class BaseSerializer(Field): ...@@ -125,10 +125,10 @@ class BaseSerializer(Field):
} }
if allow_empty is not None: if allow_empty is not None:
list_kwargs['allow_empty'] = allow_empty list_kwargs['allow_empty'] = allow_empty
list_kwargs.update(dict([ list_kwargs.update({
(key, value) for key, value in kwargs.items() key: value for key, value in kwargs.items()
if key in LIST_SERIALIZER_KWARGS if key in LIST_SERIALIZER_KWARGS
])) })
meta = getattr(cls, 'Meta', None) meta = getattr(cls, 'Meta', None)
list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer) list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer)
return list_serializer_class(*args, **list_kwargs) return list_serializer_class(*args, **list_kwargs)
...@@ -305,10 +305,10 @@ def get_validation_error_detail(exc): ...@@ -305,10 +305,10 @@ def get_validation_error_detail(exc):
elif isinstance(exc.detail, dict): elif isinstance(exc.detail, dict):
# If errors may be a dict we use the standard {key: list of values}. # If errors may be a dict we use the standard {key: list of values}.
# Here we ensure that all the values are *lists* of errors. # Here we ensure that all the values are *lists* of errors.
return dict([ return {
(key, value if isinstance(value, list) else [value]) key: value if isinstance(value, list) else [value]
for key, value in exc.detail.items() for key, value in exc.detail.items()
]) }
elif isinstance(exc.detail, list): elif isinstance(exc.detail, list):
# Errors raised as a list are non-field errors. # Errors raised as a list are non-field errors.
return { return {
...@@ -794,6 +794,7 @@ class ModelSerializer(Serializer): ...@@ -794,6 +794,7 @@ class ModelSerializer(Serializer):
if ModelJSONField is not None: if ModelJSONField is not None:
serializer_field_mapping[ModelJSONField] = JSONField serializer_field_mapping[ModelJSONField] = JSONField
serializer_related_field = PrimaryKeyRelatedField serializer_related_field = PrimaryKeyRelatedField
serializer_related_to_field = SlugRelatedField
serializer_url_field = HyperlinkedIdentityField serializer_url_field = HyperlinkedIdentityField
serializer_choice_field = ChoiceField serializer_choice_field = ChoiceField
...@@ -1129,6 +1130,11 @@ class ModelSerializer(Serializer): ...@@ -1129,6 +1130,11 @@ class ModelSerializer(Serializer):
field_class = self.serializer_related_field field_class = self.serializer_related_field
field_kwargs = get_relation_kwargs(field_name, relation_info) field_kwargs = get_relation_kwargs(field_name, relation_info)
to_field = field_kwargs.pop('to_field', None)
if to_field and to_field != 'id':
field_kwargs['slug_field'] = to_field
field_class = self.serializer_related_to_field
# `view_name` is only valid for hyperlinked relationships. # `view_name` is only valid for hyperlinked relationships.
if not issubclass(field_class, HyperlinkedRelatedField): if not issubclass(field_class, HyperlinkedRelatedField):
field_kwargs.pop('view_name', None) field_kwargs.pop('view_name', None)
...@@ -1237,13 +1243,10 @@ class ModelSerializer(Serializer): ...@@ -1237,13 +1243,10 @@ class ModelSerializer(Serializer):
for model_field in model_fields.values(): for model_field in model_fields.values():
# Include each of the `unique_for_*` field names. # Include each of the `unique_for_*` field names.
unique_constraint_names |= set([ unique_constraint_names |= {model_field.unique_for_date, model_field.unique_for_month,
model_field.unique_for_date, model_field.unique_for_year}
model_field.unique_for_month,
model_field.unique_for_year
])
unique_constraint_names -= set([None]) unique_constraint_names -= {None}
# Include each of the `unique_together` field names, # Include each of the `unique_together` field names,
# so long as all the field names are included on the serializer. # so long as all the field names are included on the serializer.
...@@ -1357,10 +1360,10 @@ class ModelSerializer(Serializer): ...@@ -1357,10 +1360,10 @@ class ModelSerializer(Serializer):
# which may map onto a model field. Any dotted field name lookups # which may map onto a model field. Any dotted field name lookups
# cannot map to a field, and must be a traversal, so we're not # cannot map to a field, and must be a traversal, so we're not
# including those. # including those.
field_names = set([ field_names = {
field.source for field in self.fields.values() field.source for field in self.fields.values()
if (field.source != '*') and ('.' not in field.source) if (field.source != '*') and ('.' not in field.source)
]) }
# Note that we make sure to check `unique_together` both on the # Note that we make sure to check `unique_together` both on the
# base model class, but also on any parent classes. # base model class, but also on any parent classes.
......
...@@ -111,11 +111,6 @@ DEFAULTS = { ...@@ -111,11 +111,6 @@ DEFAULTS = {
'COMPACT_JSON': True, 'COMPACT_JSON': True,
'COERCE_DECIMAL_TO_STRING': True, 'COERCE_DECIMAL_TO_STRING': True,
'UPLOADED_FILES_USE_URL': True, 'UPLOADED_FILES_USE_URL': True,
# Pending deprecation:
'PAGINATE_BY': None,
'PAGINATE_BY_PARAM': None,
'MAX_PAGINATE_BY': None
} }
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -73,3 +73,11 @@ pre { ...@@ -73,3 +73,11 @@ pre {
border-bottom: none; border-bottom: none;
padding-bottom: 0px; padding-bottom: 0px;
} }
#filtersModal form input[type=submit] {
width: auto;
}
#filtersModal .modal-body h2 {
margin-top: 0
}
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -111,6 +111,13 @@ ...@@ -111,6 +111,13 @@
</form> </form>
{% endif %} {% endif %}
{% if filter_form %}
<button style="float: right; margin-right: 10px" data-toggle="modal" data-target="#filtersModal" class="btn btn-default">
<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span>
{% trans "Filters" %}
</button>
{% endif %}
<div class="content-main"> <div class="content-main">
<div class="page-header"> <div class="page-header">
<h1>{{ name }}</h1> <h1>{{ name }}</h1>
...@@ -218,8 +225,10 @@ ...@@ -218,8 +225,10 @@
</div> </div>
{% endif %} {% endif %}
{% if filter_form %}{{ filter_form }}{% endif %}
{% block script %} {% block script %}
<script src="{% static "rest_framework/js/jquery-1.11.3-min.js" %}"></script> <script src="{% static "rest_framework/js/jquery-1.11.3.min.js" %}"></script>
<script src="{% static "rest_framework/js/ajax-form.js" %}"></script> <script src="{% static "rest_framework/js/ajax-form.js" %}"></script>
<script src="{% static "rest_framework/js/csrf.js" %}"></script> <script src="{% static "rest_framework/js/csrf.js" %}"></script>
<script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script> <script src="{% static "rest_framework/js/bootstrap.min.js" %}"></script>
......
{% load rest_framework %}
{% csrf_token %}
{% for field in form %}
{% if not field.read_only %}
{% render_field field style=style %}
{% endif %}
{% endfor %}
<!-- form.non_field_errors -->
{% load staticfiles %} {% load staticfiles %}
{% load rest_framework %} {% load rest_framework %}
{% load i18n %}
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
...@@ -105,6 +106,13 @@ ...@@ -105,6 +106,13 @@
</form> </form>
{% endif %} {% endif %}
{% if filter_form %}
<button style="float: right; margin-right: 10px" data-toggle="modal" data-target="#filtersModal" class="btn btn-default">
<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span>
{% trans "Filters" %}
</button>
{% endif %}
<div class="content-main"> <div class="content-main">
<div class="page-header"> <div class="page-header">
<h1>{{ name }}</h1> <h1>{{ name }}</h1>
...@@ -154,6 +162,7 @@ ...@@ -154,6 +162,7 @@
{% with form=post_form %} {% with form=post_form %}
<form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate> <form action="{{ request.get_full_path }}" method="POST" enctype="multipart/form-data" class="form-horizontal" novalidate>
<fieldset> <fieldset>
{% csrf_token %}
{{ post_form }} {{ post_form }}
<div class="form-actions"> <div class="form-actions">
<button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button> <button class="btn btn-primary" title="Make a POST request on the {{ name }} resource">POST</button>
...@@ -245,6 +254,11 @@ ...@@ -245,6 +254,11 @@
}); });
</script> </script>
{% endblock %} {% endblock %}
{% if filter_form %}
{{ filter_form }}
{% endif %}
</body> </body>
{% endblock %} {% endblock %}
</html> </html>
<div class="modal fade" id="filtersModal" tabindex="-1" role="dialog" aria-labelledby="filters" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Filters</h4>
</div>
<div class="modal-body">
{% for element in elements %}
{% if not forloop.first %}<hr/>{% endif %}
{{ element }}
{% endfor %}
</div>
</div>
</div>
</div>
{% load i18n %}
<h2>{% trans "Field filters" %}</h2>
<form class="form" action="" method="get">
{{ filter.form.as_p }}
<button type="submit" class="btn btn-primary">{% trans "Submit" %}</button>
</form>
{% load crispy_forms_tags %}
{% load i18n %}
<h2>{% trans "Field filters" %}</h2>
{% crispy filter.form %}
{% load rest_framework %}
{% load i18n %}
<h2>{% trans "Ordering" %}</h2>
<div class="list-group">
{% for key, label in options %}
{% if key == current %}
<a href="{% add_query_param request param key %}" class="list-group-item active">
<span class="glyphicon glyphicon-ok" style="float: right" aria-hidden="true"></span> {{ label }}
</a>
{% else %}
<a href="{% add_query_param request param key %}" class="list-group-item">{{ label }}</a>
{% endif %}
{% endfor %}
</div>
{% load i18n %}
<h2>{% trans "Search" %}</h2>
<form class="form-inline">
<div class="form-group">
<div class="input-group">
<input type="text" class="form-control" style="width: 350px" name="{{ param }}" value="{{ term }}">
<span class="input-group-btn">
<button class="btn btn-default" type="submit"><span class="glyphicon glyphicon-search" aria-hidden="true"></span> Search</button>
</span>
</div>
</div>
</form>
{% load rest_framework %} {% load rest_framework %}
{% for field in form %}
<form class="form-horizontal" role="form" action="." method="POST" novalidate>
{% csrf_token %}
{% for field in form %}
{% if not field.read_only %} {% if not field.read_only %}
{% render_field field style=style %} {% render_field field style=style %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-default">Submit</button>
</div>
</div>
</form>
{% load rest_framework %} {% load rest_framework %}
{% for field in form %}
<form class="form-inline" role="form" action="." method="POST" novalidate>
{% csrf_token %}
{% for field in form %}
{% if not field.read_only %} {% if not field.read_only %}
{% render_field field style=style %} {% render_field field style=style %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<button type="submit" class="btn btn-default">Submit</button>
</form>
{% load rest_framework %} {% load rest_framework %}
{% for field in form %}
<form role="form" action="." method="POST" novalidate>
{% csrf_token %}
{% for field in form %}
{% if not field.read_only %} {% if not field.read_only %}
{% render_field field style=style %} {% render_field field style=style %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<button type="submit" class="btn btn-default">Submit</button>
</form>
...@@ -7,7 +7,7 @@ from django.core.urlresolvers import NoReverseMatch, reverse ...@@ -7,7 +7,7 @@ from django.core.urlresolvers import NoReverseMatch, reverse
from django.template import Context, loader from django.template import Context, loader
from django.utils import six from django.utils import six
from django.utils.encoding import force_text, iri_to_uri from django.utils.encoding import force_text, iri_to_uri
from django.utils.html import escape, smart_urlquote from django.utils.html import escape, format_html, smart_urlquote
from django.utils.safestring import SafeData, mark_safe from django.utils.safestring import SafeData, mark_safe
from rest_framework.renderers import HTMLFormRenderer from rest_framework.renderers import HTMLFormRenderer
...@@ -25,8 +25,14 @@ def get_pagination_html(pager): ...@@ -25,8 +25,14 @@ def get_pagination_html(pager):
@register.simple_tag @register.simple_tag
def render_field(field, style=None): def render_form(serializer, template_pack=None):
style = style or {} style = {'template_pack': template_pack} if template_pack else {}
renderer = HTMLFormRenderer()
return renderer.render(serializer.data, None, {'style': style})
@register.simple_tag
def render_field(field, style):
renderer = style.get('renderer', HTMLFormRenderer()) renderer = style.get('renderer', HTMLFormRenderer())
return renderer.render_field(field, style) return renderer.render_field(field, style)
...@@ -42,7 +48,8 @@ def optional_login(request): ...@@ -42,7 +48,8 @@ def optional_login(request):
return '' return ''
snippet = "<li><a href='{href}?next={next}'>Log in</a></li>" snippet = "<li><a href='{href}?next={next}'>Log in</a></li>"
snippet = snippet.format(href=login_url, next=escape(request.path)) snippet = format_html(snippet, href=login_url, next=escape(request.path))
return mark_safe(snippet) return mark_safe(snippet)
...@@ -65,7 +72,8 @@ def optional_logout(request, user): ...@@ -65,7 +72,8 @@ def optional_logout(request, user):
<li><a href='{href}?next={next}'>Log out</a></li> <li><a href='{href}?next={next}'>Log out</a></li>
</ul> </ul>
</li>""" </li>"""
snippet = snippet.format(user=escape(user), href=logout_url, next=escape(request.path)) snippet = format_html(snippet, user=escape(user), href=logout_url, next=escape(request.path))
return mark_safe(snippet) return mark_safe(snippet)
......
...@@ -106,6 +106,21 @@ def get_field_kwargs(field_name, model_field): ...@@ -106,6 +106,21 @@ def get_field_kwargs(field_name, model_field):
isinstance(model_field, models.TextField)): isinstance(model_field, models.TextField)):
kwargs['allow_blank'] = True kwargs['allow_blank'] = True
if isinstance(model_field, models.FilePathField):
kwargs['path'] = model_field.path
if model_field.match is not None:
kwargs['match'] = model_field.match
if model_field.recursive is not False:
kwargs['recursive'] = model_field.recursive
if model_field.allow_files is not True:
kwargs['allow_files'] = model_field.allow_files
if model_field.allow_folders is not False:
kwargs['allow_folders'] = model_field.allow_folders
if model_field.choices: if model_field.choices:
# If this model field contains choices, then return early. # If this model field contains choices, then return early.
# Further keyword arguments are not valid. # Further keyword arguments are not valid.
...@@ -123,7 +138,8 @@ def get_field_kwargs(field_name, model_field): ...@@ -123,7 +138,8 @@ def get_field_kwargs(field_name, model_field):
# Ensure that max_length is passed explicitly as a keyword arg, # Ensure that max_length is passed explicitly as a keyword arg,
# rather than as a validator. # rather than as a validator.
max_length = getattr(model_field, 'max_length', None) max_length = getattr(model_field, 'max_length', None)
if max_length is not None and isinstance(model_field, models.CharField): if max_length is not None and (isinstance(model_field, models.CharField) or
isinstance(model_field, models.TextField)):
kwargs['max_length'] = max_length kwargs['max_length'] = max_length
validator_kwarg = [ validator_kwarg = [
validator for validator in validator_kwarg validator for validator in validator_kwarg
...@@ -221,7 +237,7 @@ def get_relation_kwargs(field_name, relation_info): ...@@ -221,7 +237,7 @@ def get_relation_kwargs(field_name, relation_info):
""" """
Creates a default instance of a flat relational field. Creates a default instance of a flat relational field.
""" """
model_field, related_model, to_many, has_through_model = relation_info model_field, related_model, to_many, to_field, has_through_model = relation_info
kwargs = { kwargs = {
'queryset': related_model._default_manager, 'queryset': related_model._default_manager,
'view_name': get_detail_view_name(related_model) 'view_name': get_detail_view_name(related_model)
...@@ -230,6 +246,9 @@ def get_relation_kwargs(field_name, relation_info): ...@@ -230,6 +246,9 @@ def get_relation_kwargs(field_name, relation_info):
if to_many: if to_many:
kwargs['many'] = True kwargs['many'] = True
if to_field:
kwargs['to_field'] = to_field
if has_through_model: if has_through_model:
kwargs['read_only'] = True kwargs['read_only'] = True
kwargs.pop('queryset', None) kwargs.pop('queryset', None)
......
...@@ -26,6 +26,7 @@ RelationInfo = namedtuple('RelationInfo', [ ...@@ -26,6 +26,7 @@ RelationInfo = namedtuple('RelationInfo', [
'model_field', 'model_field',
'related_model', 'related_model',
'to_many', 'to_many',
'to_field',
'has_through_model' 'has_through_model'
]) ])
...@@ -90,6 +91,10 @@ def _get_fields(opts): ...@@ -90,6 +91,10 @@ def _get_fields(opts):
return fields return fields
def _get_to_field(field):
return field.to_fields[0] if field.to_fields else None
def _get_forward_relationships(opts): def _get_forward_relationships(opts):
""" """
Returns an `OrderedDict` of field names to `RelationInfo`. Returns an `OrderedDict` of field names to `RelationInfo`.
...@@ -100,6 +105,7 @@ def _get_forward_relationships(opts): ...@@ -100,6 +105,7 @@ def _get_forward_relationships(opts):
model_field=field, model_field=field,
related_model=_resolve_model(field.rel.to), related_model=_resolve_model(field.rel.to),
to_many=False, to_many=False,
to_field=_get_to_field(field),
has_through_model=False has_through_model=False
) )
...@@ -109,6 +115,8 @@ def _get_forward_relationships(opts): ...@@ -109,6 +115,8 @@ def _get_forward_relationships(opts):
model_field=field, model_field=field,
related_model=_resolve_model(field.rel.to), related_model=_resolve_model(field.rel.to),
to_many=True, to_many=True,
# manytomany do not have to_fields
to_field=None,
has_through_model=( has_through_model=(
not field.rel.through._meta.auto_created not field.rel.through._meta.auto_created
) )
...@@ -133,6 +141,7 @@ def _get_reverse_relationships(opts): ...@@ -133,6 +141,7 @@ def _get_reverse_relationships(opts):
model_field=None, model_field=None,
related_model=related, related_model=related,
to_many=relation.field.rel.multiple, to_many=relation.field.rel.multiple,
to_field=_get_to_field(relation.field),
has_through_model=False has_through_model=False
) )
...@@ -144,6 +153,8 @@ def _get_reverse_relationships(opts): ...@@ -144,6 +153,8 @@ def _get_reverse_relationships(opts):
model_field=None, model_field=None,
related_model=related, related_model=related,
to_many=True, to_many=True,
# manytomany do not have to_fields
to_field=None,
has_through_model=( has_through_model=(
(getattr(relation.field.rel, 'through', None) is not None) and (getattr(relation.field.rel, 'through', None) is not None) and
not relation.field.rel.through._meta.auto_created not relation.field.rel.through._meta.auto_created
......
...@@ -112,7 +112,7 @@ class NestedBoundField(BoundField): ...@@ -112,7 +112,7 @@ class NestedBoundField(BoundField):
if isinstance(value, (list, dict)): if isinstance(value, (list, dict)):
values[key] = value values[key] = value
else: else:
values[key] = '' if value is None else force_text(value) values[key] = '' if (value is None or value is False) else force_text(value)
return self.__class__(self._field, values, self.errors, self._prefix) return self.__class__(self._field, values, self.errors, self._prefix)
......
...@@ -100,11 +100,11 @@ class UniqueTogetherValidator(object): ...@@ -100,11 +100,11 @@ class UniqueTogetherValidator(object):
if self.instance is not None: if self.instance is not None:
return return
missing = dict([ missing = {
(field_name, self.missing_message) field_name: self.missing_message
for field_name in self.fields for field_name in self.fields
if field_name not in attrs if field_name not in attrs
]) }
if missing: if missing:
raise ValidationError(missing) raise ValidationError(missing)
...@@ -120,10 +120,10 @@ class UniqueTogetherValidator(object): ...@@ -120,10 +120,10 @@ class UniqueTogetherValidator(object):
attrs[field_name] = getattr(self.instance, field_name) attrs[field_name] = getattr(self.instance, field_name)
# Determine the filter keyword arguments and filter the queryset. # Determine the filter keyword arguments and filter the queryset.
filter_kwargs = dict([ filter_kwargs = {
(field_name, attrs[field_name]) field_name: attrs[field_name]
for field_name in self.fields for field_name in self.fields
]) }
return queryset.filter(**filter_kwargs) return queryset.filter(**filter_kwargs)
def exclude_current_instance(self, attrs, queryset): def exclude_current_instance(self, attrs, queryset):
...@@ -184,11 +184,11 @@ class BaseUniqueForValidator(object): ...@@ -184,11 +184,11 @@ class BaseUniqueForValidator(object):
The `UniqueFor<Range>Validator` classes always force an implied The `UniqueFor<Range>Validator` classes always force an implied
'required' state on the fields they are applied to. 'required' state on the fields they are applied to.
""" """
missing = dict([ missing = {
(field_name, self.missing_message) field_name: self.missing_message
for field_name in [self.field, self.date_field] for field_name in [self.field, self.date_field]
if field_name not in attrs if field_name not in attrs
]) }
if missing: if missing:
raise ValidationError(missing) raise ValidationError(missing)
......
...@@ -45,6 +45,16 @@ class TestSimpleBoundField: ...@@ -45,6 +45,16 @@ class TestSimpleBoundField:
assert serializer['amount'].errors is None assert serializer['amount'].errors is None
assert serializer['amount'].name == 'amount' assert serializer['amount'].name == 'amount'
def test_as_form_fields(self):
class ExampleSerializer(serializers.Serializer):
bool_field = serializers.BooleanField()
null_field = serializers.IntegerField(allow_null=True)
serializer = ExampleSerializer(data={'bool_field': False, 'null_field': None})
assert serializer.is_valid()
assert serializer['bool_field'].as_form_field().value == ''
assert serializer['null_field'].as_form_field().value == ''
class TestNestedBoundField: class TestNestedBoundField:
def test_nested_empty_bound_field(self): def test_nested_empty_bound_field(self):
...@@ -67,3 +77,16 @@ class TestNestedBoundField: ...@@ -67,3 +77,16 @@ class TestNestedBoundField:
assert serializer['nested']['amount'].value is None assert serializer['nested']['amount'].value is None
assert serializer['nested']['amount'].errors is None assert serializer['nested']['amount'].errors is None
assert serializer['nested']['amount'].name == 'nested.amount' assert serializer['nested']['amount'].name == 'nested.amount'
def test_as_form_fields(self):
class Nested(serializers.Serializer):
bool_field = serializers.BooleanField()
null_field = serializers.IntegerField(allow_null=True)
class ExampleSerializer(serializers.Serializer):
nested = Nested()
serializer = ExampleSerializer(data={'nested': {'bool_field': False, 'null_field': None}})
assert serializer.is_valid()
assert serializer['nested']['bool_field'].as_form_field().value == ''
assert serializer['nested']['null_field'].as_form_field().value == ''
...@@ -63,10 +63,11 @@ class RegularFieldsModel(models.Model): ...@@ -63,10 +63,11 @@ class RegularFieldsModel(models.Model):
positive_small_integer_field = models.PositiveSmallIntegerField() positive_small_integer_field = models.PositiveSmallIntegerField()
slug_field = models.SlugField(max_length=100) slug_field = models.SlugField(max_length=100)
small_integer_field = models.SmallIntegerField() small_integer_field = models.SmallIntegerField()
text_field = models.TextField() text_field = models.TextField(max_length=100)
time_field = models.TimeField() time_field = models.TimeField()
url_field = models.URLField(max_length=100) url_field = models.URLField(max_length=100)
custom_field = CustomField() custom_field = CustomField()
file_path_field = models.FilePathField(path='/tmp/')
def method(self): def method(self):
return 'method' return 'method'
...@@ -161,11 +162,13 @@ class TestRegularFieldMappings(TestCase): ...@@ -161,11 +162,13 @@ class TestRegularFieldMappings(TestCase):
positive_small_integer_field = IntegerField() positive_small_integer_field = IntegerField()
slug_field = SlugField(max_length=100) slug_field = SlugField(max_length=100)
small_integer_field = IntegerField() small_integer_field = IntegerField()
text_field = CharField(style={'base_template': 'textarea.html'}) text_field = CharField(max_length=100, style={'base_template': 'textarea.html'})
time_field = TimeField() time_field = TimeField()
url_field = URLField(max_length=100) url_field = URLField(max_length=100)
custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>) custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>)
file_path_field = FilePathField(path='/tmp/')
""") """)
self.assertEqual(unicode_repr(TestSerializer()), expected) self.assertEqual(unicode_repr(TestSerializer()), expected)
def test_field_options(self): def test_field_options(self):
......
...@@ -147,41 +147,6 @@ class TestPaginationDisabledIntegration: ...@@ -147,41 +147,6 @@ class TestPaginationDisabledIntegration:
assert response.data == list(range(1, 101)) assert response.data == list(range(1, 101))
class TestDeprecatedStylePagination:
"""
Integration tests for deprecated style of setting pagination
attributes on the view.
"""
def setup(self):
class PassThroughSerializer(serializers.BaseSerializer):
def to_representation(self, item):
return item
class ExampleView(generics.ListAPIView):
serializer_class = PassThroughSerializer
queryset = range(1, 101)
pagination_class = pagination.PageNumberPagination
paginate_by = 20
page_query_param = 'page_number'
self.view = ExampleView.as_view()
def test_paginate_by_attribute_on_view(self):
request = factory.get('/?page_number=2')
response = self.view(request)
assert response.status_code == status.HTTP_200_OK
assert response.data == {
'results': [
21, 22, 23, 24, 25, 26, 27, 28, 29, 30,
31, 32, 33, 34, 35, 36, 37, 38, 39, 40
],
'previous': 'http://testserver/',
'next': 'http://testserver/?page_number=3',
'count': 100
}
class TestPageNumberPagination: class TestPageNumberPagination:
""" """
Unit tests for `pagination.PageNumberPagination`. Unit tests for `pagination.PageNumberPagination`.
......
...@@ -19,9 +19,9 @@ commands = ./runtests.py --fast {posargs} --coverage ...@@ -19,9 +19,9 @@ commands = ./runtests.py --fast {posargs} --coverage
setenv = setenv =
PYTHONDONTWRITEBYTECODE=1 PYTHONDONTWRITEBYTECODE=1
deps = deps =
django17: Django==1.7.10 # Should track maximum supported django17: Django==1.7.10
django18: Django==1.8.4 # Should track maximum supported django18: Django==1.8.4
django19: https://www.djangoproject.com/download/1.9a1/tarball/ django19: https://www.djangoproject.com/download/1.9b1/tarball/
-rrequirements/requirements-testing.txt -rrequirements/requirements-testing.txt
-rrequirements/requirements-optionals.txt -rrequirements/requirements-optionals.txt
...@@ -40,21 +40,21 @@ deps = ...@@ -40,21 +40,21 @@ deps =
# Specify explicitly to exclude Django Guardian against Django 1.9 # Specify explicitly to exclude Django Guardian against Django 1.9
[testenv:py27-django19] [testenv:py27-django19]
deps = deps =
https://www.djangoproject.com/download/1.9a1/tarball/ https://www.djangoproject.com/download/1.9b1/tarball/
-rrequirements/requirements-testing.txt -rrequirements/requirements-testing.txt
markdown==2.5.2 markdown==2.5.2
django-filter==0.10.0 django-filter==0.10.0
[testenv:py34-django19] [testenv:py34-django19]
deps = deps =
https://www.djangoproject.com/download/1.9a1/tarball/ https://www.djangoproject.com/download/1.9b1/tarball/
-rrequirements/requirements-testing.txt -rrequirements/requirements-testing.txt
markdown==2.5.2 markdown==2.5.2
django-filter==0.10.0 django-filter==0.10.0
[testenv:py35-django19] [testenv:py35-django19]
deps = deps =
https://www.djangoproject.com/download/1.9a1/tarball/ https://www.djangoproject.com/download/1.9b1/tarball/
-rrequirements/requirements-testing.txt -rrequirements/requirements-testing.txt
markdown==2.5.2 markdown==2.5.2
django-filter==0.10.0 django-filter==0.10.0
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment