Commit de00ec95 by Tom Christie

Merge master

parents 9428d6dd 2ca243a1
......@@ -7,18 +7,19 @@ python:
- "3.3"
env:
- DJANGO="django==1.5 --use-mirrors"
- DJANGO="django==1.4.3 --use-mirrors"
- DJANGO="django==1.3.5 --use-mirrors"
- DJANGO="https://www.djangoproject.com/download/1.6a1/tarball/"
- DJANGO="django==1.5.1 --use-mirrors"
- DJANGO="django==1.4.5 --use-mirrors"
- DJANGO="django==1.3.7 --use-mirrors"
install:
- pip install $DJANGO
- pip install defusedxml==0.3
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211 --use-mirrors; fi"
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0 --use-mirrors; fi"
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.3 --use-mirrors; fi"
- "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4 --use-mirrors; fi"
- "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi"
- "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6a1 --use-mirrors; fi"
- "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6 --use-mirrors; fi"
- export PYTHONPATH=.
script:
......@@ -27,10 +28,11 @@ script:
matrix:
exclude:
- python: "3.2"
env: DJANGO="django==1.4.3 --use-mirrors"
env: DJANGO="django==1.4.5 --use-mirrors"
- python: "3.2"
env: DJANGO="django==1.3.5 --use-mirrors"
env: DJANGO="django==1.3.7 --use-mirrors"
- python: "3.3"
env: DJANGO="django==1.4.3 --use-mirrors"
env: DJANGO="django==1.4.5 --use-mirrors"
- python: "3.3"
env: DJANGO="django==1.3.5 --use-mirrors"
env: DJANGO="django==1.3.7 --use-mirrors"
......@@ -27,7 +27,7 @@ There is a live example API for testing purposes, [available here][sandbox].
# Requirements
* Python (2.6.5+, 2.7, 3.2, 3.3)
* Django (1.3, 1.4, 1.5)
* Django (1.3, 1.4, 1.5, 1.6)
# Installation
......@@ -102,6 +102,12 @@ For questions and support, use the [REST framework discussion group][group], or
You may also want to [follow the author on Twitter][twitter].
# Security
If you believe you’ve found something in Django REST framework which has security implications, please **do not raise the issue in a public forum**.
Send a description of the issue via email to [rest-framework-security@googlegroups.com][security-mail]. The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure.
# License
Copyright (c) 2011-2013, Tom Christie
......@@ -149,3 +155,4 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[pyyaml]: http://pypi.python.org/pypi/PyYAML
[defusedxml]: https://pypi.python.org/pypi/defusedxml
[django-filter]: http://pypi.python.org/pypi/django-filter
[security-mail]: mailto:rest-framework-security@googlegroups.com
......@@ -333,7 +333,7 @@ The following example will authenticate any incoming request as the user given b
try:
user = User.objects.get(username=username)
except User.DoesNotExist:
raise authenticate.AuthenticationFailed('No such user')
raise exceptions.AuthenticationFailed('No such user')
return (user, None)
......
......@@ -43,6 +43,8 @@ Defaults to `True`.
If set, this gives the default value that will be used for the field if none is supplied. If not set the default behavior is to not populate the attribute at all.
May be set to a function or other callable, in which case the value will be evaluated each time it is used.
### `validators`
A list of Django validators that should be used to validate deserialized values.
......@@ -56,6 +58,13 @@ A dictionary of error codes to error messages.
Used only if rendering the field to HTML.
This argument sets the widget that should be used to render the field.
### `label`
A short text string that may be used as the name of the field in HTML form fields or other descriptive elements.
### `help_text`
A text string that may be used as a description of the field in HTML form fields or other descriptive elements.
---
......@@ -108,7 +117,9 @@ A field that supports both read and write operations. By itself `WritableField`
A generic field that can be tied to any arbitrary model field. The `ModelField` class delegates the task of serialization/deserialization to it's associated model field. This field can be used to create serializer fields for custom model fields, without having to create a new custom serializer field.
**Signature:** `ModelField(model_field=<Django ModelField class>)`
The `ModelField` class is generally intended for internal use, but can be used by your API if needed. In order to properly instantiate a `ModelField`, it must be passed a field that is attached to an instantiated model. For example: `ModelField(model_field=MyModel()._meta.get_field('custom_field'))`
**Signature:** `ModelField(model_field=<Django ModelField instance>)`
## SerializerMethodField
......@@ -197,7 +208,7 @@ If you want to override this behavior, you'll need to declare the `DateTimeField
class Meta:
model = Comment
Note that by default, datetime representations are deteremined by the renderer in use, although this can be explicitly overridden as detailed below.
Note that by default, datetime representations are determined by the renderer in use, although this can be explicitly overridden as detailed below.
In the case of JSON this means the default datetime representation uses the [ECMA 262 date time string specification][ecma262]. This is a subset of ISO 8601 which uses millisecond precision, and includes the 'Z' suffix for the UTC timezone, for example: `2013-01-29T12:34:56.123Z`.
......@@ -206,7 +217,7 @@ In the case of JSON this means the default datetime representation uses the [ECM
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that python `datetime` objects should be returned by `to_native`. In this case the datetime encoding will be determined by the renderer.
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
DateTime format strings may either be [python strftime formats][strftime] which explicitly specifiy the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style datetimes should be used. (eg `'2013-01-29T12:34:56.000000Z'`)
DateTime format strings may either be [python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style datetimes should be used. (eg `'2013-01-29T12:34:56.000000Z'`)
## DateField
......@@ -219,7 +230,7 @@ Corresponds to `django.db.models.fields.DateField`
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that python `date` objects should be returned by `to_native`. In this case the date encoding will be determined by the renderer.
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATE_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
Date format strings may either be [python strftime formats][strftime] which explicitly specifiy the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style dates should be used. (eg `'2013-01-29'`)
Date format strings may either be [python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style dates should be used. (eg `'2013-01-29'`)
## TimeField
......@@ -234,7 +245,7 @@ Corresponds to `django.db.models.fields.TimeField`
* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that python `time` objects should be returned by `to_native`. In this case the time encoding will be determined by the renderer.
* `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `TIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`.
Time format strings may either be [python strftime formats][strftime] which explicitly specifiy the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style times should be used. (eg `'12:34:56.000000'`)
Time format strings may either be [python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style times should be used. (eg `'12:34:56.000000'`)
## IntegerField
......@@ -285,7 +296,7 @@ Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files.
# Custom fields
If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the intial datatype, and a primative, serializable datatype. Primative datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primative objects.
If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the initial datatype, and a primative, serializable datatype. Primative datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primative objects.
The `.to_native()` method is called to convert the initial datatype into a primative, serializable datatype. The `from_native()` method is called to restore a primative datatype into it's initial representation.
......
......@@ -77,20 +77,61 @@ We can override `.get_queryset()` to deal with URLs such as `http://example.com/
# Generic Filtering
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 filters that can be specified by the client using query parameters.
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.
## Setting filter backends
The default filter backends may be set globally, using the `DEFAULT_FILTER_BACKENDS` setting. For example.
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',)
}
You can also set the filter backends on a per-view, or per-viewset basis,
using the `GenericAPIView` class based views.
class UserListView(generics.ListAPIView):
queryset = User.objects.all()
serializer = UserSerializer
filter_backends = (filters.DjangoFilterBackend,)
## Filtering and object lookups
Note that if a filter backend is configured for a view, then as well as being used to filter list views, it will also be used to filter the querysets used for returning a single object.
For instance, given the previous example, and a product with an id of `4675`, the following URL would either return the corresponding object, or return a 404 response, depending on if the filtering conditions were met by the given product instance:
http://example.com/api/products/4675/?category=clothing&max_price=10.00
## Overriding the initial queryset
Note that you can use both an overridden `.get_queryset()` and generic filtering together, and everything will work as expected. For example, if `Product` had a many-to-many relationship with `User`, named `purchase`, you might want to write a view like this:
class PurchasedProductsList(generics.ListAPIView):
"""
Return a list of all the products that the authenticated
user has ever purchased, with optional filtering.
"""
model = Product
serializer_class = ProductSerializer
filter_class = ProductFilter
def get_queryset(self):
user = self.request.user
return user.purchase_set.all()
---
# API Guide
## DjangoFilterBackend
The `DjangoFilterBackend` class supports highly customizable field filtering, using the [django-filter package][django-filter].
To use REST framework's `DjangoFilterBackend`, first install `django-filter`.
pip install django-filter
You must also set the filter backend to `DjangoFilterBackend` in your settings:
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ['rest_framework.filters.DjangoFilterBackend']
}
#### Specifying filter fields
......@@ -134,33 +175,72 @@ For more details on using filter sets see the [django-filter documentation][djan
* By default filtering is not enabled. If you want to use `DjangoFilterBackend` remember to make sure it is installed by using the `'DEFAULT_FILTER_BACKENDS'` setting.
* When using boolean fields, you should use the values `True` and `False` in the URL query parameters, rather than `0`, `1`, `true` or `false`. (The allowed boolean values are currently hardwired in Django's [NullBooleanSelect implementation][nullbooleanselect].)
* `django-filter` supports filtering across relationships, using Django's double-underscore syntax.
* For Django 1.3 support, make sure to install `django-filter` version 0.5.4, as later versions drop support for 1.3.
---
## Filtering and object lookups
## SearchFilter
Note that if a filter backend is configured for a view, then as well as being used to filter list views, it will also be used to filter the querysets used for returning a single object.
The `SearchFilterBackend` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin].
For instance, given the previous example, and a product with an id of `4675`, the following URL would either return the corresponding object, or return a 404 response, depending on if the filtering conditions were met by the given product instance:
The `SearchFilterBackend` 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`.
http://example.com/api/products/4675/?category=clothing&max_price=10.00
class UserListView(generics.ListAPIView):
queryset = User.objects.all()
serializer = UserSerializer
filter_backends = (filters.SearchFilter,)
search_fields = ('username', 'email')
## Overriding the initial queryset
This will allow the client to filter the items in the list by making queries such as:
Note that you can use both an overridden `.get_queryset()` and generic filtering together, and everything will work as expected. For example, if `Product` had a many-to-many relationship with `User`, named `purchase`, you might want to write a view like this:
http://example.com/api/users?search=russell
class PurchasedProductsList(generics.ListAPIView):
"""
Return a list of all the products that the authenticated
user has ever purchased, with optional filtering.
"""
model = Product
serializer_class = ProductSerializer
filter_class = ProductFilter
You can also perform a related lookup on a ForeignKey or ManyToManyField with the lookup API double-underscore notation:
search_fields = ('username', 'email', 'profile__profession')
By default, searches will use case-insensitive partial matches. The search parameter may contain multiple search terms, which should be whitespace and/or comma separated. If multiple search terms are used then objects will be returned in the list only if all the provided terms are matched.
The search behavior may be restricted by prepending various characters to the `search_fields`.
* '^' Starts-with search.
* '=' Exact matches.
* '@' Full-text search. (Currently only supported Django's MySQL backend.)
For example:
search_fields = ('=username', '=email')
For more details, see the [Django documentation][search-django-admin].
---
## OrderingFilter
The `OrderingFilter` class supports simple query parameter controlled ordering of results. To specify the result order, set a query parameter named `'ordering'` to the required field name. For example:
http://example.com/api/users?ordering=username
The client may also specify reverse orderings by prefixing the field name with '-', like so:
http://example.com/api/users?ordering=-username
Multiple orderings may also be specified:
http://example.com/api/users?ordering=account,username
If an `ordering` attribute is set on the view, this will be used as the default ordering.
Typically you'd instead control this by setting `order_by` on the initial queryset, but using the `ordering` parameter on the view allows you to specify the ordering in a way that it can then be passed automatically as context to a rendered template. This makes it possible to automatically render column headers differently if they are being used to order the results.
class UserListView(generics.ListAPIView):
queryset = User.objects.all()
serializer = UserSerializer
filter_backends = (filters.OrderingFilter,)
ordering = ('username',)
The `ordering` attribute may be either a string or a list/tuple of strings.
def get_queryset(self):
user = self.request.user
return user.purchase_set.all()
---
# Custom generic filtering
......@@ -169,15 +249,23 @@ You can also provide your own generic filtering backend, or write an installable
To do so override `BaseFilterBackend`, and override the `.filter_queryset(self, request, queryset, view)` method. The method should return a new, filtered queryset.
To install the filter backend, set the `'DEFAULT_FILTER_BACKENDS'` key in your `'REST_FRAMEWORK'` setting, using the dotted import path of the filter backend class.
As well as allowing clients to perform searches and filtering, generic filter backends can be useful for restricting which objects should be visible to any given request or user.
For example:
## Example
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ['custom_filters.CustomFilterBackend']
}
For example, you might need to restrict users to only being able to see objects they created.
class IsOwnerFilterBackend(filters.BaseFilterBackend):
"""
Filter that only allows users to see their own objects.
"""
def filter_queryset(self, request, queryset, view):
return queryset.filter(owner=request.user)
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.
[cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters
[django-filter]: https://github.com/alex/django-filter
[django-filter-docs]: https://django-filter.readthedocs.org/en/latest/index.html
[nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py
[search-django-admin]: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields
......@@ -147,7 +147,7 @@ If you need to test if a request is a read operation or a write operation, you s
**Note**: In versions 2.0 and 2.1, the signature for the permission checks always included an optional `obj` parameter, like so: `.has_permission(self, request, view, obj=None)`. The method would be called twice, first for the global permission checks, with no object supplied, and second for the object-level check when required.
As of version 2.2 this signature has now been replaced with two seperate method calls, which is more explict and obvious. The old style signature continues to work, but it's use will result in a `PendingDeprecationWarning`, which is silent by default. In 2.3 this will be escalated to a `DeprecationWarning`, and in 2.4 the old-style signature will be removed.
As of version 2.2 this signature has now been replaced with two separate method calls, which is more explict and obvious. The old style signature continues to work, but it's use will result in a `PendingDeprecationWarning`, which is silent by default. In 2.3 this will be escalated to a `DeprecationWarning`, and in 2.4 the old-style signature will be removed.
For more details see the [2.2 release announcement][2.2-announcement].
......
......@@ -202,9 +202,7 @@ This field is always read-only.
**Arguments**:
* `view_name` - The view name that should be used as the target of the relationship. **required**.
* `slug_field` - The field on the target that should be used for the lookup. Default is `'slug'`.
* `pk_url_kwarg` - The named url parameter for the pk field lookup. Default is `pk`.
* `slug_url_kwarg` - The named url parameter for the slug field lookup. Default is to use the same value as given for `slug_field`.
* `lookup_field` - The field on the target that should be used for the lookup. Should correspond to a URL keyword argument on the referenced view. Default is `'pk'`.
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
---
......@@ -239,7 +237,7 @@ Would serialize to a nested representation like this:
'album_name': 'The Grey Album',
'artist': 'Danger Mouse'
'tracks': [
{'order': 1, 'title': 'Public Service Annoucement'},
{'order': 1, 'title': 'Public Service Announcement'},
{'order': 2, 'title': 'What More Can I Say'},
{'order': 3, 'title': 'Encore'},
...
......@@ -383,6 +381,15 @@ Note that reverse generic keys, expressed using the `GenericRelation` field, can
For more information see [the Django documentation on generic relations][generic-relations].
## ManyToManyFields with a Through Model
By default, relational fields that target a ``ManyToManyField`` with a
``through`` model specified are set to read-only.
If you exlicitly specify a relational field pointing to a
``ManyToManyField`` with a through model, be sure to set ``read_only``
to ``True``.
## Advanced Hyperlinked fields
If you have very specific requirements for the style of your hyperlinked relationships you can override `HyperlinkedRelatedField`.
......
......@@ -14,7 +14,7 @@ The set of valid renderers for a view is always defined as a list of classes. W
The basic process of content negotiation involves examining the request's `Accept` header, to determine which media types it expects in the response. Optionally, format suffixes on the URL may be used to explicitly request a particular representation. For example the URL `http://example.com/api/users_count.json` might be an endpoint that always returns JSON data.
For more information see the documentation on [content negotation][conneg].
For more information see the documentation on [content negotiation][conneg].
## Setting the renderers
......@@ -67,14 +67,46 @@ If your API includes views that can serve both regular webpages and API response
## JSONRenderer
Renders the request data into `JSON`.
Renders the request data into `JSON`, using utf-8 encoding.
Note that non-ascii characters will be rendered using JSON's `\uXXXX` character escape. For example:
{"unicode black star": "\u2605"}
The client may additionally include an `'indent'` media type parameter, in which case the returned `JSON` will be indented. For example `Accept: application/json; indent=4`.
{
"unicode black star": "\u2605"
}
**.media_type**: `application/json`
**.format**: `'.json'`
**.charset**: `utf-8`
## UnicodeJSONRenderer
Renders the request data into `JSON`, using utf-8 encoding.
Note that non-ascii characters will not be character escaped. For example:
{"unicode black star": "★"}
The client may additionally include an `'indent'` media type parameter, in which case the returned `JSON` will be indented. For example `Accept: application/json; indent=4`.
{
"unicode black star": "★"
}
Both the `JSONRenderer` and `UnicodeJSONRenderer` styles conform to [RFC 4627][rfc4627], and are syntactically valid JSON.
**.media_type**: `application/json`
**.format**: `'.json'`
**.charset**: `utf-8`
## JSONPRenderer
Renders the request data into `JSONP`. The `JSONP` media type provides a mechanism of allowing cross-domain AJAX requests, by wrapping a `JSON` response in a javascript callback.
......@@ -87,6 +119,8 @@ The javascript callback function must be set by the client including a `callback
**.format**: `'.jsonp'`
**.charset**: `utf-8`
## YAMLRenderer
Renders the request data into `YAML`.
......@@ -97,6 +131,8 @@ Requires the `pyyaml` package to be installed.
**.format**: `'.yaml'`
**.charset**: `utf-8`
## XMLRenderer
Renders REST framework's default style of `XML` response content.
......@@ -109,6 +145,8 @@ If you are considering using `XML` for your API, you may want to consider implem
**.format**: `'.xml'`
**.charset**: `utf-8`
## TemplateHTMLRenderer
Renders data to HTML, using Django's standard template rendering.
......@@ -143,6 +181,8 @@ If you're building websites that use `TemplateHTMLRenderer` along with other ren
**.format**: `'.html'`
**.charset**: `utf-8`
See also: `StaticHTMLRenderer`
## StaticHTMLRenderer
......@@ -163,6 +203,8 @@ You can use `TemplateHTMLRenderer` either to return regular HTML pages using RES
**.format**: `'.html'`
**.charset**: `utf-8`
See also: `TemplateHTMLRenderer`
## BrowsableAPIRenderer
......@@ -173,12 +215,16 @@ Renders data into HTML for the Browsable API. This renderer will determine whic
**.format**: `'.api'`
**.charset**: `utf-8`
---
# Custom renderers
To implement a custom renderer, you should override `BaseRenderer`, set the `.media_type` and `.format` properties, and implement the `.render(self, data, media_type=None, renderer_context=None)` method.
The method should return a bytestring, which wil be used as the body of the HTTP response.
The arguments passed to the `.render()` method are:
### `data`
......@@ -205,14 +251,36 @@ The following is an example plaintext renderer that will return a response with
from rest_framework import renderers
class PlainText(renderers.BaseRenderer):
class PlainTextRenderer(renderers.BaseRenderer):
media_type = 'text/plain'
format = 'txt'
def render(self, data, media_type=None, renderer_context=None):
return data.encode(self.charset)
## Setting the character set
By default renderer classes are assumed to be using the `UTF-8` encoding. To use a different encoding, set the `charset` attribute on the renderer.
class PlainTextRenderer(renderers.BaseRenderer):
media_type = 'text/plain'
format = 'txt'
charset = 'iso-8859-1'
def render(self, data, media_type=None, renderer_context=None):
return data.encode(self.charset)
Note that if a renderer class returns a unicode string, then the response content will be coerced into a bytestring by the `Response` class, with the `charset` attribute set on the renderer used to determine the encoding.
If the renderer returns a bytestring representing raw binary content, you should set a charset value of `None`, which will ensure the `Content-Type` header of the response will not have a `charset` value set. Doing so will also ensure that the browsable API will not attempt to display the binary content as a string.
class JPEGRenderer(renderers.BaseRenderer):
media_type = 'image/jpeg'
format = 'jpg'
charset = None
def render(self, data, media_type=None, renderer_context=None):
if isinstance(data, basestring):
return data
return smart_unicode(data)
---
......@@ -252,6 +320,15 @@ For example:
data = serializer.data
return Response(data)
## Underspecifying the media type
In some cases you might want a renderer to serve a range of media types.
In this case you can underspecify the media types it should respond to, by using a `media_type` value such as `image/*`, or `*/*`.
If you underspecify the renderer's media type, you should make sure to specify the media type explicitly when you return the response, using the `content_type` attribute. For example:
return Response(data, content_type='image/png')
## Designing your media types
For the purposes of many Web APIs, simple `JSON` responses with hyperlinked relations may be sufficient. If you want to fully embrace RESTful design and [HATEOAS] you'll need to consider the design and usage of your media types in more detail.
......@@ -274,6 +351,8 @@ Exceptions raised and handled by an HTML renderer will attempt to render using o
Templates will render with a `RequestContext` which includes the `status_code` and `details` keys.
**Note**: If `DEBUG=True`, Django's standard traceback error page will be displayed instead of rendering the HTTP status code and text.
---
# Third party packages
......@@ -291,6 +370,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
[conneg]: content-negotiation.md
[browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers
[rfc4627]: http://www.ietf.org/rfc/rfc4627.txt
[cors]: http://www.w3.org/TR/cors/
[cors-docs]: ../topics/ajax-csrf-cors.md
[HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas
......
......@@ -20,7 +20,7 @@ Unless you want to heavily customize REST framework for some reason, you should
## Response()
**Signature:** `Response(data, status=None, template_name=None, headers=None)`
**Signature:** `Response(data, status=None, template_name=None, headers=None, content_type=None)`
Unlike regular `HttpResponse` objects, you do not instantiate `Response` objects with rendered content. Instead you pass in unrendered data, which may consist of any python primatives.
......@@ -34,6 +34,7 @@ Arguments:
* `status`: A status code for the response. Defaults to 200. See also [status codes][statuscodes].
* `template_name`: A template name to use if `HTMLRenderer` is selected.
* `headers`: A dictionary of HTTP headers to use in the response.
* `content_type`: The content type of the response. Typically, this will be set automatically by the renderer as determined by content negotiation, but there may be some cases where you need to specify the content type explicitly.
---
......
......@@ -66,6 +66,13 @@ This router includes routes for the standard set of `list`, `create`, `retrieve`
<tr><td>POST</td><td>@action decorated method</td></tr>
</table>
By default the URLs created by `SimpleRouter` are appending with a trailing slash.
This behavior can be modified by setting the `trailing_slash` argument to `False` when instantiating the router. For example:
router = SimpleRouter(trailing_slash=False)
Trailing slashes are conventional in Django, but are not used by default in some other frameworks such as Rails. Which style you choose to use is largely a matter of preference, although some javascript frameworks may expect a particular routing style.
## DefaultRouter
This router is similar to `SimpleRouter` as above, but additionally includes a default API root view, that returns a response containing hyperlinks to all the list views. It also generates routes for optional `.json` style format suffixes.
......@@ -83,15 +90,19 @@ This router is similar to `SimpleRouter` as above, but additionally includes a d
<tr><td>POST</td><td>@action decorated method</td></tr>
</table>
As with `SimpleRouter` the trailing slashs on the URL routes can be removed by setting the `trailing_slash` argument to `False` when instantiating the router.
router = DefaultRouter(trailing_slash=False)
# Custom Routers
Implementing a custom router isn't something you'd need to do very often, but it can be useful if you have specfic requirements about how the your URLs for your API are strutured. Doing so allows you to encapsulate the URL structure in a reusable way that ensures you don't have to write your URL patterns explicitly for each new view.
Implementing a custom router isn't something you'd need to do very often, but it can be useful if you have specific requirements about how the your URLs for your API are strutured. Doing so allows you to encapsulate the URL structure in a reusable way that ensures you don't have to write your URL patterns explicitly for each new view.
The simplest way to implement a custom router is to subclass one of the existing router classes. The `.routes` attribute is used to template the URL patterns that will be mapped to each viewset.
## Example
The following example will only route to the `list` and `retrieve` actions, and unlike the routers included by REST framework, it does not use the trailing slash convention.
The following example will only route to the `list` and `retrieve` actions, and does not use the trailing slash convention.
class ReadOnlyRouter(SimpleRouter):
"""
......
......@@ -104,7 +104,7 @@ When deserializing a list of items, errors will be returned as a list of diction
#### Field-level validation
You can specify custom field-level validation by adding `.validate_<fieldname>` methods to your `Serializer` subclass. These are analagous to `.clean_<fieldname>` methods on Django forms, but accept slightly different arguments.
You can specify custom field-level validation by adding `.validate_<fieldname>` methods to your `Serializer` subclass. These are analogous to `.clean_<fieldname>` methods on Django forms, but accept slightly different arguments.
They take a dictionary of deserialized attributes as a first argument, and the field name in that dictionary as a second argument (which will be either the name of the field or the value of the `source` argument to the field, if one was provided).
......
......@@ -125,7 +125,7 @@ Default: `None`
#### PAGINATE_BY_PARAM
The name of a query parameter, which can be used by the client to overide the default page size to use for pagination. If set to `None`, clients may not override the default page size.
The name of a query parameter, which can be used by the client to override the default page size to use for pagination. If set to `None`, clients may not override the default page size.
Default: `None`
......
......@@ -13,11 +13,11 @@ A `ViewSet` class is simply **a type of class-based View, that does not provide
The method handlers for a `ViewSet` are only bound to the corresponding actions at the point of finalizing the view, using the `.as_view()` method.
Typically, rather than exlicitly registering the views in a viewset in the urlconf, you'll register the viewset with a router class, that automatically determines the urlconf for you.
Typically, rather than explicitly registering the views in a viewset in the urlconf, you'll register the viewset with a router class, that automatically determines the urlconf for you.
## Example
Let's define a simple viewset that can be used to listing or retrieving all the users in the system.
Let's define a simple viewset that can be used to list or retrieve all the users in the system.
class UserViewSet(viewsets.ViewSet):
"""
......@@ -34,7 +34,7 @@ Let's define a simple viewset that can be used to listing or retrieving all the
serializer = UserSerializer(user)
return Response(serializer.data)
If we need to, we can bind this viewset into two seperate views, like so:
If we need to, we can bind this viewset into two separate views, like so:
user_list = UserViewSet.as_view({'get': 'list'})
user_detail = UserViewSet.as_view({'get': 'retrieve'})
......@@ -65,7 +65,7 @@ Both of these come with a trade-off. Using regular views and URL confs is more
The default routers included with REST framework will provide routes for a standard set of create/retrieve/update/destroy style operations, as shown below:
class UserViewSet(viewsets.VietSet):
class UserViewSet(viewsets.ViewSet):
"""
Example empty viewset demonstrating the standard
actions that will be handled by a router class.
......@@ -92,7 +92,7 @@ The default routers included with REST framework will provide routes for a stand
def destroy(self, request, pk=None):
pass
If you have ad-hoc methods that you need to be routed to, you can mark them as requiring routing using the `@link` or `@action` decorators. The `@link` decorator will route `GET` requests, and the `@action` decroator will route `POST` requests.
If you have ad-hoc methods that you need to be routed to, you can mark them as requiring routing using the `@link` or `@action` decorators. The `@link` decorator will route `GET` requests, and the `@action` decorator will route `POST` requests.
For example:
......@@ -126,6 +126,11 @@ The `@action` and `@link` decorators can additionally take extra arguments that
def set_password(self, request, pk=None):
...
The `@action` decorator will route `POST` requests by default, but may also accept other HTTP methods, by using the `method` argument. For example:
@action(methods=['POST', 'DELETE'])
def unset_password(self, request, pk=None):
...
---
# API Reference
......@@ -136,9 +141,15 @@ The `ViewSet` class inherits from `APIView`. You can use any of the standard at
The `ViewSet` class does not provide any implementations of actions. In order to use a `ViewSet` class you'll override the class and define the action implementations explicitly.
## GenericViewSet
The `GenericViewSet` class inherits from `GenericAPIView`, and provides the default set of `get_object`, `get_queryset` methods and other generic view base behavior, but does not include any actions by default.
In order to use a `GenericViewSet` class you'll override the class and either mixin the required mixin classes, or define the action implementations explicitly.
## ModelViewSet
The `ModelViewSet` class inherits from `GenericAPIView` and includes implementations for various actions, by mixing in the behavior of the
The `ModelViewSet` class inherits from `GenericAPIView` and includes implementations for various actions, by mixing in the behavior of the various mixin classes.
The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`, `.create()`, `.update()`, and `.destroy()`.
......@@ -188,17 +199,18 @@ Again, as with `ModelViewSet`, you can use any of the standard attributes and me
# Custom ViewSet base classes
Any standard `View` class can be turned into a `ViewSet` class by mixing in `ViewSetMixin`. You can use this to define your own base classes.
You may need to provide custom `ViewSet` classes that do not have the full set of `ModelViewSet` actions, or that customize the behavior in some other way.
## Example
For example, we can create a base viewset class that provides `retrieve`, `update` and `list` operations:
To create a base viewset class that provides `create`, `list` and `retrieve` operations, inherit from `GenericViewSet`, and mixin the required actions:
class CreateListRetrieveViewSet(mixins.CreateMixin,
mixins.ListMixin,
mixins.RetrieveMixin,
viewsets.GenericViewSet):
pass
class RetrieveUpdateListViewSet(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.ListModelMixin,
viewsets.ViewSetMixin,
generics.GenericAPIView):
"""
A viewset that provides `retrieve`, `update`, and `list` actions.
......@@ -207,6 +219,6 @@ For example, we can create a base viewset class that provides `retrieve`, `updat
"""
pass
By creating your own base `ViewSet` classes, you can provide common behavior that can be reused in multiple views across your API.
By creating your own base `ViewSet` classes, you can provide common behavior that can be reused in multiple viewsets across your API.
[cite]: http://guides.rubyonrails.org/routing.html
......@@ -103,6 +103,10 @@ pre {
overflow: hidden;
}
.nav-list > li > a {
padding: 2px 15px 3px;
}
/* Set the table of contents to static so it flows back into the content when
viewed on tablets and smaller. */
@media (max-width: 767px) {
......@@ -297,4 +301,5 @@ td, th {
table {
border-color: white;
margin-bottom: 0.6em;
}
<p class="badges">
<iframe src="http://ghbtns.com/github-btn.html?user=tomchristie&amp;repo=django-rest-framework&amp;type=watch&amp;count=true" class="github-star-button" allowtransparency="true" frameborder="0" scrolling="0" width="110px" height="20px"></iframe>
<a href="https://twitter.com/share" class="twitter-share-button" data-url="django-rest-framework.org" data-text="Checking out the totally awesome Django REST framework! http://django-rest-framework.org" data-count="none">Tweet</a>
<a href="https://twitter.com/share" class="twitter-share-button" data-url="django-rest-framework.org" data-text="Checking out the totally awesome Django REST framework! http://django-rest-framework.org" data-count="none"></a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="http://platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
<img alt="Travis build image" src="https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master" class="travis-build-image">
<img src="https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master" class="travis-build-image">
</p>
# Django REST framework
......@@ -15,7 +15,7 @@ Django REST framework is a powerful and flexible toolkit that makes it easy to b
Some reasons you might want to use REST framework:
* The Web browseable API is a huge useability win for your developers.
* The Web browseable API is a huge usability win for your developers.
* Authentication policies including OAuth1a and OAuth2 out of the box.
* Serialization that supports both ORM and non-ORM data sources.
* Customizable all the way down - just use regular function-based views if you don't need the more powerful features.
......@@ -32,7 +32,7 @@ There is a live example API for testing purposes, [available here][sandbox].
REST framework requires the following:
* Python (2.6.5+, 2.7, 3.2, 3.3)
* Django (1.3, 1.4, 1.5)
* Django (1.3, 1.4, 1.5, 1.6)
The following packages are optional:
......@@ -113,8 +113,8 @@ Here's our project's root `urls.py` module:
# Routers provide an easy way of automatically determining the URL conf
router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'groups', views.GroupViewSet)
router.register(r'users', UserViewSet)
router.register(r'groups', GroupViewSet)
# Wire up our API using automatic URL routing.
......@@ -207,6 +207,12 @@ For updates on REST framework development, you may also want to follow [the auth
<a style="padding-top: 10px" href="https://twitter.com/_tomchristie" class="twitter-follow-button" data-show-count="false">Follow @_tomchristie</a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
## Security
If you believe you’ve found something in Django REST framework which has security implications, please **do not raise the issue in a public forum**.
Send a description of the issue via email to [rest-framework-security@googlegroups.com][security-mail]. The project maintainers will then work with you to resolve any issues where required, prior to any public disclosure.
## License
Copyright (c) 2011-2013, Tom Christie
......@@ -294,6 +300,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
[stack-overflow]: http://stackoverflow.com/
[django-rest-framework-tag]: http://stackoverflow.com/questions/tagged/django-rest-framework
[django-tag]: http://stackoverflow.com/questions/tagged/django
[security-mail]: mailto:rest-framework-security@googlegroups.com
[paid-support]: http://dabapps.com/services/build/api-development/
[dabapps]: http://dabapps.com
[contact-dabapps]: http://dabapps.com/contact/
......
......@@ -75,7 +75,7 @@ This more explicit behavior on serializing and deserializing data [makes integra
The implicit to-many behavior on serializers, and the `ManyRelatedField` style classes will continue to function, but will raise a `PendingDeprecationWarning`, which can be made visible using the `-Wd` flag.
**Note**: If you need to forcibly turn off the implict "`many=True` for `__iter__` objects" behavior, you can now do so by specifying `many=False`. This will become the default (instead of the current default of `None`) once the deprecation of the implicit behavior is finalised in version 2.4.
**Note**: If you need to forcibly turn off the implicit "`many=True` for `__iter__` objects" behavior, you can now do so by specifying `many=False`. This will become the default (instead of the current default of `None`) once the deprecation of the implicit behavior is finalised in version 2.4.
### Cleaner optional relationships
......@@ -103,9 +103,9 @@ The `blank` keyword argument will continue to function, but will raise a `Pendin
### Simpler object-level permissions
Custom permissions classes previously used the signatute `.has_permission(self, request, view, obj=None)`. This method would be called twice, firstly for the global permissions check, with the `obj` parameter set to `None`, and again for the object-level permissions check when appropriate, with the `obj` parameter set to the relevant model instance.
Custom permissions classes previously used the signature `.has_permission(self, request, view, obj=None)`. This method would be called twice, firstly for the global permissions check, with the `obj` parameter set to `None`, and again for the object-level permissions check when appropriate, with the `obj` parameter set to the relevant model instance.
The global permissions check and object-level permissions check are now seperated into two seperate methods, which gives a cleaner, more obvious API.
The global permissions check and object-level permissions check are now separated into two separate methods, which gives a cleaner, more obvious API.
* Global permission checks now use the `.has_permission(self, request, view)` signature.
* Object-level permission checks use a new method `.has_object_permission(self, request, view, obj)`.
......
......@@ -30,8 +30,8 @@ As an example of just how simple REST framework APIs can now be, here's an API w
# Routers provide an easy way of automatically determining the URL conf
router = routers.DefaultRouter()
router.register(r'users', views.UserViewSet)
router.register(r'groups', views.GroupViewSet)
router.register(r'users', UserViewSet)
router.register(r'groups', GroupViewSet)
# Wire up our API using automatic URL routing.
......@@ -45,13 +45,13 @@ The best place to get started with ViewSets and Routers is to take a look at the
## Simpler views
This release rationalises the API and implementation of the generic views, dropping the dependancy on Django's `SingleObjectMixin` and `MultipleObjectMixin` classes, removing a number of unneeded attributes, and generally making the implementation more obvious and easy to work with.
This release rationalises the API and implementation of the generic views, dropping the dependency on Django's `SingleObjectMixin` and `MultipleObjectMixin` classes, removing a number of unneeded attributes, and generally making the implementation more obvious and easy to work with.
This improvement is reflected in improved documentation for the `GenericAPIView` base class, and should make it easier to determine how to override methods on the base class if you need to write customized subclasses.
## Easier Serializers
REST framework lets you be totally explict regarding how you want to represent relationships, allowing you to choose between styles such as hyperlinking or primary key relationships.
REST framework lets you be totally explicit regarding how you want to represent relationships, allowing you to choose between styles such as hyperlinking or primary key relationships.
The ability to specify exactly how you want to represent relationships is powerful, but it also introduces complexity. In order to keep things more simple, REST framework now allows you to include reverse relationships simply by including the field name in the `fields` metadata of the serializer class.
......@@ -108,7 +108,7 @@ Using the `SingleObjectAPIView` and `MultipleObjectAPIView` base classes continu
### Removed attributes
The following attributes and methods, were previously present as part of Django's generic view implementations, but were unneeded and unusedand have now been entirely removed.
The following attributes and methods, were previously present as part of Django's generic view implementations, but were unneeded and unused and have now been entirely removed.
* context_object_name
* get_context_data()
......@@ -173,7 +173,7 @@ For example:
raise Http404
return queryset
In our opinion removing lesser-used attributes like `allow_empty` helps us move towards simpler generic view implementations, making them more obvious to use and override, and re-inforcing the preferred style of developers writing their own base classes and mixins for custom behavior rather than relying on the configurability of the generic views.
In our opinion removing lesser-used attributes like `allow_empty` helps us move towards simpler generic view implementations, making them more obvious to use and override, and re-enforcing the preferred style of developers writing their own base classes and mixins for custom behavior rather than relying on the configurability of the generic views.
## Simpler URL lookups
......
......@@ -35,6 +35,17 @@ A suitable replacement theme can be generated using Bootstrap's [Customize Tool]
You can also change the navbar variant, which by default is `navbar-inverse`, using the `bootstrap_navbar_variant` block. The empty `{% block bootstrap_navbar_variant %}{% endblock %}` will use the original Bootstrap navbar style.
Full Example
{% extends "rest_framework/base.html" %}
{% block bootstrap_theme %}
<link rel="stylesheet" href="/path/to/yourtheme/bootstrap.min.css' type="text/css">
{% endblock %}
{% block bootstrap_navbar_variant %}{% endblock %}
For more specific CSS tweaks, use the `style` block instead.
......
......@@ -60,7 +60,7 @@ have any control over what is sent in the `Accept` header.
## URL based format suffixes
REST framework can take `?format=json` style URL parameters, which can be a
useful shortcut for determing which content type should be returned from
useful shortcut for determining which content type should be returned from
the view.
This is a more concise than using the `accept` override, but it also gives
......
......@@ -22,9 +22,9 @@ It's really helpful if you make sure you address issues to the correct channel.
Some tips on good issue reporting:
* When decribing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing.
* When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing.
* Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue.
* If reporting a bug, then try to include a pull request with a failing test case. This'll help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one.
* If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one.
......@@ -32,6 +32,7 @@ Some tips on good issue reporting:
# Development
* git clone & PYTHONPATH
* Pep8
* Recommend editor that runs pep8
......
......@@ -120,6 +120,25 @@ The following people have helped make REST framework great.
* Jerome Chen - [chenjyw]
* Andrew Hughes - [eyepulp]
* Daniel Hepper - [dhepper]
* Hamish Campbell - [hamishcampbell]
* Marlon Bailey - [avinash240]
* James Summerfield - [jsummerfield]
* Andy Freeland - [rouge8]
* Craig de Stigter - [craigds]
* Pablo Recio - [pyriku]
* Brian Zambrano - [brianz]
* Òscar Vilaplana - [grimborg]
* Ryan Kaskel - [ryankask]
* Andy McKay - [andymckay]
* Matteo Suppo - [matteosuppo]
* Karol Majta - [lolek09]
* David Jones - [commonorgarden]
* Andrew Tarzwell - [atarzwell]
* Michal Dvořák - [mikee2185]
* Markus Törnqvist - [mjtorn]
* Pascal Borreli - [pborreli]
* Alex Burgel - [aburgel]
* David Medina - [copitux]
Many thanks to everyone who's contributed to the project.
......@@ -133,7 +152,7 @@ Continuous integration testing is managed with [Travis CI][travis-ci].
The [live sandbox][sandbox] is hosted on [Heroku].
Various inspiration taken from the [Rails], [Piston], [Tastypie] and [Dagny] projects.
Various inspiration taken from the [Rails], [Piston], [Tastypie], [Dagny] and [django-viewsets] projects.
Development of REST framework 2.0 was sponsored by [DabApps].
......@@ -152,6 +171,7 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[piston]: https://bitbucket.org/jespern/django-piston
[tastypie]: https://github.com/toastdriven/django-tastypie
[dagny]: https://github.com/zacharyvoase/dagny
[django-viewsets]: https://github.com/BertrandBordage/django-viewsets
[dabapps]: http://lab.dabapps.com
[sandbox]: http://restframework.herokuapp.com/
[heroku]: http://www.heroku.com/
......@@ -275,3 +295,22 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[chenjyw]: https://github.com/chenjyw
[eyepulp]: https://github.com/eyepulp
[dhepper]: https://github.com/dhepper
[hamishcampbell]: https://github.com/hamishcampbell
[avinash240]: https://github.com/avinash240
[jsummerfield]: https://github.com/jsummerfield
[rouge8]: https://github.com/rouge8
[craigds]: https://github.com/craigds
[pyriku]: https://github.com/pyriku
[brianz]: https://github.com/brianz
[grimborg]: https://github.com/grimborg
[ryankask]: https://github.com/ryankask
[andymckay]: https://github.com/andymckay
[matteosuppo]: https://github.com/matteosuppo
[lolek09]: https://github.com/lolek09
[commonorgarden]: https://github.com/commonorgarden
[atarzwell]: https://github.com/atarzwell
[mikee2185]: https://github.com/mikee2185
[mjtorn]: https://github.com/mjtorn
[pborreli]: https://github.com/pborreli
[aburgel]: https://github.com/aburgel
[copitux]: https://github.com/copitux
......@@ -40,6 +40,46 @@ You can determine your currently installed version using `pip freeze`:
## 2.3.x series
### 2.3.5
**Date**: 3rd June 2013
* Added `get_url` hook to `HyperlinkedIdentityField`.
* Serializer field `default` argument may be a callable.
* `@action` decorator now accepts a `methods` argument.
* Bugfix: `request.user` should be still be accessible in renderer context if authentication fails.
* Bugfix: The `lookup_field` option on `HyperlinkedIdentityField` should apply by default to the url field on the serializer.
* Bugfix: `HyperlinkedIdentityField` should continue to support `pk_url_kwarg`, `slug_url_kwarg`, `slug_field`, in a pending deprecation state.
* Bugfix: Ensure we always return 404 instead of 500 if a lookup field cannot be converted to the correct lookup type. (Eg non-numeric `AutoInteger` pk lookup)
### 2.3.4
**Date**: 24th May 2013
* Serializer fields now support `label` and `help_text`.
* Added `UnicodeJSONRenderer`.
* `OPTIONS` requests now return metadata about fields for `POST` and `PUT` requests.
* Bugfix: `charset` now properly included in `Content-Type` of responses.
* Bugfix: Blank choice now added in browsable API on nullable relationships.
* Bugfix: Many to many relationships with `through` tables are now read-only.
* Bugfix: Serializer fields now respect model field args such as `max_length`.
* Bugfix: SlugField now performs slug validation.
* Bugfix: Lazy-translatable strings now properly serialized.
* Bugfix: Browsable API now supports bootswatch styles properly.
* Bugfix: HyperlinkedIdentityField now uses `lookup_field` kwarg.
**Note**: Responses now correctly include an appropriate charset on the `Content-Type` header. For example: `application/json; charset=utf-8`. If you have tests that check the content type of responses, you may need to update these accordingly.
### 2.3.3
**Date**: 16th May 2013
* Added SearchFilter
* Added OrderingFilter
* Added GenericViewSet
* Bugfix: Multiple `@action` and `@link` methods now allowed on viewsets.
* Bugfix: Fix API Root view issue with DjangoModelPermissions
### 2.3.2
**Date**: 8th May 2013
......@@ -78,14 +118,14 @@ You can determine your currently installed version using `pip freeze`:
**Date**: 17th April 2013
* Loud failure when view does not return a `Response` or `HttpResponse`.
* Bugfix: Fix for Django 1.3 compatiblity.
* Bugfix: Fix for Django 1.3 compatibility.
* Bugfix: Allow overridden `get_object()` to work correctly.
### 2.2.6
**Date**: 4th April 2013
* OAuth2 authentication no longer requires unneccessary URL parameters in addition to the token.
* OAuth2 authentication no longer requires unnecessary URL parameters in addition to the token.
* URL hyperlinking in browsable API now handles more cases correctly.
* Long HTTP headers in browsable API are broken in multiple lines when possible.
* Bugfix: Fix regression with DjangoFilterBackend not worthing correctly with single object views.
......
......@@ -60,7 +60,7 @@ REST framework 2 also allows you to work with both function-based and class-base
## API Design
Pretty much every aspect of REST framework has been reworked, with the aim of ironing out some of the design flaws of the previous versions. Each of the components of REST framework are cleanly decoupled, and can be used independantly of each-other, and there are no monolithic resource classes, overcomplicated mixin combinations, or opinionated serialization or URL routing decisions.
Pretty much every aspect of REST framework has been reworked, with the aim of ironing out some of the design flaws of the previous versions. Each of the components of REST framework are cleanly decoupled, and can be used independently of each-other, and there are no monolithic resource classes, overcomplicated mixin combinations, or opinionated serialization or URL routing decisions.
## The Browsable API
......@@ -70,7 +70,7 @@ Browsable Web APIs are easier to work with, visualize and debug, and generally m
With REST framework 2, the browsable API gets a snazzy new bootstrap-based theme that looks great and is even nicer to work with.
There are also some functionality improvments - actions such as as `POST` and `DELETE` will only display if the user has the appropriate permissions.
There are also some functionality improvements - actions such as as `POST` and `DELETE` will only display if the user has the appropriate permissions.
![Browsable API][image]
......
......@@ -37,8 +37,8 @@ What REST framework doesn't do is give you is machine readable hypermedia format
[cite]: http://vimeo.com/channels/restfest/page:2
[dissertation]: http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
[hypertext-driven]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
[restful-web-services]:
[building-hypermedia-apis]:
[restful-web-services]: http://www.amazon.com/Restful-Web-Services-Leonard-Richardson/dp/0596529260
[building-hypermedia-apis]: http://www.amazon.com/Building-Hypermedia-APIs-HTML5-Node/dp/1449306578
[designing-hypermedia-apis]: http://designinghypermediaapis.com/
[restisover]: http://blog.steveklabnik.com/posts/2012-02-23-rest-is-over
[readinglist]: http://blog.steveklabnik.com/posts/2012-02-27-hypermedia-api-reading-list
......
......@@ -146,6 +146,8 @@ The first thing we need to get started on our Web API is provide a way of serial
The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data.
Notice that we can also use various attributes that would typically be used on form fields, such as `widget=widgets.Testarea`. These can be used to control how the serializer should render when displayed as an HTML form. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial.
We can actually also save ourselves some time by using the `ModelSerializer` class, as we'll see later, but for now we'll keep our serializer definition explicit.
## Working with Serializers
......
......@@ -8,7 +8,7 @@ Let's introduce a couple of essential building blocks.
REST framework introduces a `Request` object that extends the regular `HttpRequest`, and provides more flexible request parsing. The core functionality of the `Request` object is the `request.DATA` attribute, which is similar to `request.POST`, but more useful for working with Web APIs.
request.POST # Only handles form data. Only works for 'POST' method.
request.DATA # Handles arbitrary data. Works any HTTP request with content.
request.DATA # Handles arbitrary data. Works for 'POST', 'PUT' and 'PATCH' methods.
## Response objects
......
......@@ -17,7 +17,7 @@ Add the following two fields to the model.
owner = models.ForeignKey('auth.User', related_name='snippets')
highlighted = models.TextField()
We'd also need to make sure that when the model is saved, that we populate the highlighted field, using the `pygments` code higlighting library.
We'd also need to make sure that when the model is saved, that we populate the highlighted field, using the `pygments` code highlighting library.
We'll need some extra imports:
......@@ -137,7 +137,7 @@ And, at the end of the file, add a pattern to include the login and logout views
The `r'^api-auth/'` part of pattern can actually be whatever URL you want to use. The only restriction is that the included urls must use the `'rest_framework'` namespace.
Now if you open up the browser again and refresh the page you'll see a 'Login' link in the top right of the page. If you log in as one of the users you created earier, you'll be able to create code snippets again.
Now if you open up the browser again and refresh the page you'll see a 'Login' link in the top right of the page. If you log in as one of the users you created earlier, you'll be able to create code snippets again.
Once you've created a few code snippets, navigate to the '/users/' endpoint, and notice that the representation includes a list of the snippet pks that are associated with each user, in each user's 'snippets' field.
......
# Tutorial 6 - ViewSets & Routers
# Tutorial 6: ViewSets & Routers
REST framework includes an abstraction for dealing with `ViewSets`, that allows the developer to concentrate on modeling the state and interactions of the API, and leave the URL construction to be handled automatically, based on common conventions.
......@@ -59,7 +59,7 @@ To see what's going on under the hood let's first explicitly create a set of vie
In the `urls.py` file we bind our `ViewSet` classes into a set of concrete views.
from snippets.resources import SnippetResource, UserResource
from snippets.views import SnippetViewSet, UserViewSet
snippet_list = SnippetViewSet.as_view({
'get': 'list',
......@@ -119,7 +119,7 @@ Registering the viewsets with the router is similar to providing a urlpattern.
The `DefaultRouter` class we're using also automatically creates the API root view for us, so we can now delete the `api_root` method from our `views` module.
## Trade-offs between views vs viewsets.
## Trade-offs between views vs viewsets
Using viewsets can be a really useful abstraction. It helps ensure that URL conventions will be consistent across your API, minimizes the amount of code you need to write, and allows you to concentrate on the interactions and representations your API provides rather than the specifics of the URL conf.
......
......@@ -2,7 +2,43 @@
We're going to create a simple API to allow admin users to view and edit the users and groups in the system.
Create a new Django project, and start a new app called `quickstart`. Once you've set up a database and got everything synced and ready to go open up the app's directory and we'll get coding...
## Project setup
Create a new Django project named `tutorial`, then start a new app called `quickstart`.
# Set up a new project
django-admin.py startproject tutorial
cd tutorial
# Create a virtualenv to isolate our package dependencies locally
virtualenv env
source env/bin/activate
# Install Django and Django REST framework into the virtualenv
pip install django
pip install djangorestframework
# Create a new app
python manage.py startapp quickstart
Next you'll need to get a database set up and synced. If you just want to use SQLite for now, then you'll want to edit your `tutorial/settings.py` module to include something like this:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'database.sql',
'USER': '',
'PASSWORD': '',
'HOST': '',
'PORT': ''
}
}
The run `syncdb` like so:
python manage.py syncdb
Once you've set up a database and got everything synced and ready to go, open up the app's directory and we'll get coding...
## Serializers
......@@ -55,7 +91,7 @@ We can easily break these down into individual views if we need to, but using vi
## URLs
Okay, now let's wire up the API URLs. On to `quickstart/urls.py`...
Okay, now let's wire up the API URLs. On to `tutorial/urls.py`...
from django.conf.urls import patterns, url, include
from rest_framework import routers
......@@ -80,7 +116,7 @@ Finally, we're including default login and logout views for use with the browsab
## Settings
We'd also like to set a few global settings. We'd like to turn on pagination, and we want our API to only be accessible to admin users.
We'd also like to set a few global settings. We'd like to turn on pagination, and we want our API to only be accessible to admin users. The settings module will be in `tutorial/settings.py`
INSTALLED_APPS = (
...
......@@ -98,6 +134,10 @@ Okay, we're done.
## Testing our API
We're now ready to test the API we've built. Let's fire up the server from the command line.
python ./manage.py runserver
We can now access our API, both from the command-line, using tools like `curl`...
bash: curl -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/
......
......@@ -4,4 +4,4 @@ defusedxml>=0.3
django-filter>=0.5.4
django-oauth-plus>=2.0
oauth2>=1.5.211
django-oauth2-provider>=0.2.3
django-oauth2-provider>=0.2.4
__version__ = '2.3.2'
__version__ = '2.3.5'
VERSION = __version__ # synonym
......
......@@ -495,3 +495,16 @@ except ImportError:
oauth2_provider_forms = None
oauth2_provider_scope = None
oauth2_constants = None
# Handle lazy strings
from django.utils.functional import Promise
if six.PY3:
def is_non_str_iterable(obj):
if (isinstance(obj, str) or
(isinstance(obj, Promise) and obj._delegate_text)):
return False
return hasattr(obj, '__iter__')
else:
def is_non_str_iterable(obj):
return hasattr(obj, '__iter__')
"""
The most imporant decorator in this module is `@api_view`, which is used
The most important decorator in this module is `@api_view`, which is used
for writing function-based views with REST framework.
There are also various decorators for setting the API policies on function
......@@ -40,7 +40,7 @@ def api_view(http_method_names):
# api_view applied with eg. string instead of list of strings
assert isinstance(http_method_names, (list, tuple)), \
'@api_view expected a list of strings, recieved %s' % type(http_method_names).__name__
'@api_view expected a list of strings, received %s' % type(http_method_names).__name__
allowed_methods = set(http_method_names) | set(('options',))
WrappedAPIView.http_method_names = [method.lower() for method in allowed_methods]
......@@ -112,18 +112,18 @@ def link(**kwargs):
Used to mark a method on a ViewSet that should be routed for GET requests.
"""
def decorator(func):
func.bind_to_method = 'get'
func.bind_to_methods = ['get']
func.kwargs = kwargs
return func
return decorator
def action(**kwargs):
def action(methods=['post'], **kwargs):
"""
Used to mark a method on a ViewSet that should be routed for POST requests.
"""
def decorator(func):
func.bind_to_method = 'post'
func.bind_to_methods = methods
func.kwargs = kwargs
return func
return decorator
......@@ -11,20 +11,21 @@ from decimal import Decimal, DecimalException
import inspect
import re
import warnings
from django.core import validators
from django.core.exceptions import ValidationError
from django.conf import settings
from django.db.models.fields import BLANK_CHOICE_DASH
from django import forms
from django.forms import widgets
from django.utils.encoding import is_protected_type
from django.utils.translation import ugettext_lazy as _
from django.utils.datastructures import SortedDict
from rest_framework import ISO_8601
from rest_framework.compat import timezone, parse_date, parse_datetime, parse_time
from rest_framework.compat import (timezone, parse_date, parse_datetime,
parse_time)
from rest_framework.compat import BytesIO
from rest_framework.compat import six
from rest_framework.compat import smart_text
from rest_framework.compat import smart_text, force_text, is_non_str_iterable
from rest_framework.settings import api_settings
......@@ -50,7 +51,7 @@ def get_component(obj, attr_name):
return that attribute on the object.
"""
if isinstance(obj, dict):
val = obj[attr_name]
val = obj.get(attr_name)
else:
val = getattr(obj, attr_name)
......@@ -60,7 +61,8 @@ def get_component(obj, attr_name):
def readable_datetime_formats(formats):
format = ', '.join(formats).replace(ISO_8601, 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]')
format = ', '.join(formats).replace(ISO_8601,
'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]')
return humanize_strptime(format)
......@@ -107,8 +109,9 @@ class Field(object):
partial = False
use_files = False
form_field_class = forms.CharField
type_label = 'field'
def __init__(self, source=None):
def __init__(self, source=None, label=None, help_text=None):
self.parent = None
self.creation_counter = Field.creation_counter
......@@ -116,6 +119,12 @@ class Field(object):
self.source = source
if label is not None:
self.label = smart_text(label)
if help_text is not None:
self.help_text = smart_text(help_text)
def initialize(self, parent, field_name):
"""
Called to set up a field prior to field_to_native or field_from_native.
......@@ -167,11 +176,16 @@ class Field(object):
if is_protected_type(value):
return value
elif hasattr(value, '__iter__') and not isinstance(value, (dict, six.string_types)):
elif (is_non_str_iterable(value) and
not isinstance(value, (dict, six.string_types))):
return [self.to_native(item) for item in value]
elif isinstance(value, dict):
return dict(map(self.to_native, (k, v)) for k, v in value.items())
return smart_text(value)
# Make sure we preserve field ordering, if it exists
ret = SortedDict()
for key, val in value.items():
ret[key] = self.to_native(val)
return ret
return force_text(value)
def attributes(self):
"""
......@@ -181,6 +195,18 @@ class Field(object):
return {'type': self.type_name}
return {}
def metadata(self):
metadata = SortedDict()
metadata['type'] = self.type_label
metadata['required'] = getattr(self, 'required', False)
optional_attrs = ['read_only', 'label', 'help_text',
'min_length', 'max_length']
for attr in optional_attrs:
value = getattr(self, attr, None)
if value is not None and value != '':
metadata[attr] = force_text(value, strings_only=True)
return metadata
class WritableField(Field):
"""
......@@ -194,7 +220,8 @@ class WritableField(Field):
widget = widgets.TextInput
default = None
def __init__(self, source=None, read_only=False, required=None,
def __init__(self, source=None, label=None, help_text=None,
read_only=False, required=None,
validators=[], error_messages=None, widget=None,
default=None, blank=None):
......@@ -205,7 +232,7 @@ class WritableField(Field):
DeprecationWarning, stacklevel=2)
required = not(blank)
super(WritableField, self).__init__(source=source)
super(WritableField, self).__init__(source=source, label=label, help_text=help_text)
self.read_only = read_only
if required is None:
......@@ -268,6 +295,9 @@ class WritableField(Field):
except KeyError:
if self.default is not None and not self.partial:
# Note: partial updates shouldn't set defaults
if is_simple_callable(self.default):
native = self.default()
else:
native = self.default
else:
if self.required:
......@@ -335,6 +365,7 @@ class ModelField(WritableField):
class BooleanField(WritableField):
type_name = 'BooleanField'
type_label = 'boolean'
form_field_class = forms.BooleanField
widget = widgets.CheckboxInput
default_error_messages = {
......@@ -357,6 +388,7 @@ class BooleanField(WritableField):
class CharField(WritableField):
type_name = 'CharField'
type_label = 'string'
form_field_class = forms.CharField
def __init__(self, max_length=None, min_length=None, *args, **kwargs):
......@@ -375,23 +407,38 @@ class CharField(WritableField):
class URLField(CharField):
type_name = 'URLField'
type_label = 'url'
def __init__(self, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', 200)
kwargs['validators'] = [validators.URLValidator()]
super(URLField, self).__init__(**kwargs)
class SlugField(CharField):
type_name = 'SlugField'
type_label = 'slug'
form_field_class = forms.SlugField
default_error_messages = {
'invalid': _("Enter a valid 'slug' consisting of letters, numbers,"
" underscores or hyphens."),
}
default_validators = [validators.validate_slug]
def __init__(self, *args, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', 50)
super(SlugField, self).__init__(*args, **kwargs)
def __deepcopy__(self, memo):
result = copy.copy(self)
memo[id(self)] = result
#result.widget = copy.deepcopy(self.widget, memo)
result.validators = self.validators[:]
return result
class ChoiceField(WritableField):
type_name = 'ChoiceField'
type_label = 'multiple choice'
form_field_class = forms.ChoiceField
widget = widgets.Select
default_error_messages = {
......@@ -402,6 +449,8 @@ class ChoiceField(WritableField):
def __init__(self, choices=(), *args, **kwargs):
super(ChoiceField, self).__init__(*args, **kwargs)
self.choices = choices
if not self.required:
self.choices = BLANK_CHOICE_DASH + self.choices
def _get_choices(self):
return self._choices
......@@ -440,6 +489,7 @@ class ChoiceField(WritableField):
class EmailField(CharField):
type_name = 'EmailField'
type_label = 'email'
form_field_class = forms.EmailField
default_error_messages = {
......@@ -463,6 +513,7 @@ class EmailField(CharField):
class RegexField(CharField):
type_name = 'RegexField'
type_label = 'regex'
form_field_class = forms.RegexField
def __init__(self, regex, max_length=None, min_length=None, *args, **kwargs):
......@@ -492,6 +543,7 @@ class RegexField(CharField):
class DateField(WritableField):
type_name = 'DateField'
type_label = 'date'
widget = widgets.DateInput
form_field_class = forms.DateField
......@@ -555,6 +607,7 @@ class DateField(WritableField):
class DateTimeField(WritableField):
type_name = 'DateTimeField'
type_label = 'datetime'
widget = widgets.DateTimeInput
form_field_class = forms.DateTimeField
......@@ -624,6 +677,7 @@ class DateTimeField(WritableField):
class TimeField(WritableField):
type_name = 'TimeField'
type_label = 'time'
widget = widgets.TimeInput
form_field_class = forms.TimeField
......@@ -680,6 +734,7 @@ class TimeField(WritableField):
class IntegerField(WritableField):
type_name = 'IntegerField'
type_label = 'integer'
form_field_class = forms.IntegerField
default_error_messages = {
......@@ -710,6 +765,7 @@ class IntegerField(WritableField):
class FloatField(WritableField):
type_name = 'FloatField'
type_label = 'float'
form_field_class = forms.FloatField
default_error_messages = {
......@@ -729,6 +785,7 @@ class FloatField(WritableField):
class DecimalField(WritableField):
type_name = 'DecimalField'
type_label = 'decimal'
form_field_class = forms.DecimalField
default_error_messages = {
......@@ -799,6 +856,7 @@ class DecimalField(WritableField):
class FileField(WritableField):
use_files = True
type_name = 'FileField'
type_label = 'file upload'
form_field_class = forms.FileField
widget = widgets.FileInput
......@@ -842,6 +900,8 @@ class FileField(WritableField):
class ImageField(FileField):
use_files = True
type_name = 'ImageField'
type_label = 'image upload'
form_field_class = forms.ImageField
default_error_messages = {
......
......@@ -3,9 +3,9 @@ Provides generic filtering backends that can be used to filter the results
returned by list views.
"""
from __future__ import unicode_literals
from django.db import models
from rest_framework.compat import django_filters
from rest_framework.compat import django_filters, six
from functools import reduce
import operator
FilterSet = django_filters and django_filters.FilterSet or None
......@@ -32,40 +32,33 @@ class DjangoFilterBackend(BaseFilterBackend):
def __init__(self):
assert django_filters, 'Using DjangoFilterBackend, but django-filter is not installed'
def get_filter_class(self, view):
def get_filter_class(self, view, queryset=None):
"""
Return the django-filters `FilterSet` used to filter the queryset.
"""
filter_class = getattr(view, 'filter_class', None)
filter_fields = getattr(view, 'filter_fields', None)
model_cls = getattr(view, 'model', None)
queryset = getattr(view, 'queryset', None)
if model_cls is None and queryset is not None:
model_cls = queryset.model
if filter_class:
filter_model = filter_class.Meta.model
assert issubclass(filter_model, model_cls), \
'FilterSet model %s does not match view model %s' % \
(filter_model, model_cls)
assert issubclass(filter_model, queryset.model), \
'FilterSet model %s does not match queryset model %s' % \
(filter_model, queryset.model)
return filter_class
if filter_fields:
assert model_cls is not None, 'Cannot use DjangoFilterBackend ' \
'on a view which does not have a .model or .queryset attribute.'
class AutoFilterSet(self.default_filter_set):
class Meta:
model = model_cls
model = queryset.model
fields = filter_fields
return AutoFilterSet
return None
def filter_queryset(self, request, queryset, view):
filter_class = self.get_filter_class(view)
filter_class = self.get_filter_class(view, queryset)
if filter_class:
return filter_class(request.QUERY_PARAMS, queryset=queryset).qs
......@@ -74,6 +67,16 @@ class DjangoFilterBackend(BaseFilterBackend):
class SearchFilter(BaseFilterBackend):
search_param = 'search' # The URL query parameter used for the search.
def get_search_terms(self, request):
"""
Search terms are set by a ?search=... query parameter,
and may be comma and/or whitespace delimited.
"""
params = request.QUERY_PARAMS.get(self.search_param, '')
return params.replace(',', ' ').split()
def construct_search(self, field_name):
if field_name.startswith('^'):
return "%s__istartswith" % field_name[1:]
......@@ -88,12 +91,53 @@ class SearchFilter(BaseFilterBackend):
search_fields = getattr(view, 'search_fields', None)
if not search_fields:
return None
return queryset
orm_lookups = [self.construct_search(str(search_field))
for search_field in self.search_fields]
for bit in self.query.split():
or_queries = [models.Q(**{orm_lookup: bit})
for search_field in search_fields]
for search_term in self.get_search_terms(request):
or_queries = [models.Q(**{orm_lookup: search_term})
for orm_lookup in orm_lookups]
queryset = queryset.filter(reduce(operator.or_, or_queries))
return queryset
class OrderingFilter(BaseFilterBackend):
ordering_param = 'ordering' # The URL query parameter used for the ordering.
def get_ordering(self, request):
"""
Search terms are set by a ?search=... query parameter,
and may be comma and/or whitespace delimited.
"""
params = request.QUERY_PARAMS.get(self.ordering_param)
if params:
return [param.strip() for param in params.split(',')]
def get_default_ordering(self, view):
ordering = getattr(view, 'ordering', None)
if isinstance(ordering, six.string_types):
return (ordering,)
return ordering
def remove_invalid_fields(self, queryset, ordering):
field_names = [field.name for field in queryset.model._meta.fields]
return [term for term in ordering if term.lstrip('-') in field_names]
def filter_queryset(self, request, queryset, view):
ordering = self.get_ordering(request)
if ordering:
# Skip any incorrect parameters
ordering = self.remove_invalid_fields(queryset, ordering)
if not ordering:
# Use 'ordering' attribtue by default
ordering = self.get_default_ordering(view)
if ordering:
return queryset.order_by(*ordering)
return queryset
......@@ -3,17 +3,28 @@ Generic views that provide commonly needed behaviour.
"""
from __future__ import unicode_literals
from django.core.exceptions import ImproperlyConfigured
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.core.paginator import Paginator, InvalidPage
from django.http import Http404
from django.shortcuts import get_object_or_404
from django.shortcuts import get_object_or_404 as _get_object_or_404
from django.utils.translation import ugettext as _
from rest_framework import views, mixins
from rest_framework.exceptions import ConfigurationError
from rest_framework import views, mixins, exceptions
from rest_framework.request import clone_request
from rest_framework.settings import api_settings
import warnings
def get_object_or_404(queryset, **filter_kwargs):
"""
Same as Django's standard shortcut, but make sure to raise 404
if the filter_kwargs don't match the required types.
"""
try:
return _get_object_or_404(queryset, **filter_kwargs)
except (TypeError, ValueError):
raise Http404
class GenericAPIView(views.APIView):
"""
Base class for all other generic views.
......@@ -274,7 +285,7 @@ class GenericAPIView(views.APIView):
)
filter_kwargs = {self.slug_field: slug}
else:
raise ConfigurationError(
raise exceptions.ConfigurationError(
'Expected view %s to be called with a URL keyword argument '
'named "%s". Fix your URL conf, or set the `.lookup_field` '
'attribute on the view correctly.' %
......@@ -310,6 +321,41 @@ class GenericAPIView(views.APIView):
"""
pass
def metadata(self, request):
"""
Return a dictionary of metadata about the view.
Used to return responses for OPTIONS requests.
We override the default behavior, and add some extra information
about the required request body for POST and PUT operations.
"""
ret = super(GenericAPIView, self).metadata(request)
actions = {}
for method in ('PUT', 'POST'):
if method not in self.allowed_methods:
continue
cloned_request = clone_request(request, method)
try:
# Test global permissions
self.check_permissions(cloned_request)
# Test object permissions
if method == 'PUT':
self.get_object()
except (exceptions.APIException, PermissionDenied, Http404):
pass
else:
# If user has appropriate permissions for the view, include
# appropriate metadata about the fields that should be supplied.
serializer = self.get_serializer()
actions[method] = serializer.metadata()
if actions:
ret['actions'] = actions
return ret
##########################################################
### Concrete view classes that provide method handlers ###
......
......@@ -10,6 +10,7 @@ from django.http import Http404
from rest_framework import status
from rest_framework.response import Response
from rest_framework.request import clone_request
import warnings
def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None):
......@@ -42,7 +43,6 @@ def _get_validation_exclusions(obj, pk=None, slug_field=None, lookup_field=None)
class CreateModelMixin(object):
"""
Create a model instance.
Should be mixed in with any `GenericAPIView`.
"""
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA, files=request.FILES)
......@@ -67,7 +67,6 @@ class CreateModelMixin(object):
class ListModelMixin(object):
"""
List a queryset.
Should be mixed in with `MultipleObjectAPIView`.
"""
empty_error = "Empty list and '%(class_name)s.allow_empty' is False."
......@@ -77,6 +76,12 @@ class ListModelMixin(object):
# Default is to allow empty querysets. This can be altered by setting
# `.allow_empty = False`, to raise 404 errors on empty querysets.
if not self.allow_empty and not self.object_list:
warnings.warn(
'The `allow_empty` parameter is due to be deprecated. '
'To use `allow_empty=False` style behavior, You should override '
'`get_queryset()` and explicitly raise a 404 on empty querysets.',
PendingDeprecationWarning
)
class_name = self.__class__.__name__
error_msg = self.empty_error % {'class_name': class_name}
raise Http404(error_msg)
......@@ -94,7 +99,6 @@ class ListModelMixin(object):
class RetrieveModelMixin(object):
"""
Retrieve a model instance.
Should be mixed in with `SingleObjectAPIView`.
"""
def retrieve(self, request, *args, **kwargs):
self.object = self.get_object()
......@@ -105,17 +109,12 @@ class RetrieveModelMixin(object):
class UpdateModelMixin(object):
"""
Update a model instance.
Should be mixed in with `SingleObjectAPIView`.
"""
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
self.object = None
try:
self.object = self.get_object()
except Http404:
# If this is a PUT-as-create operation, we need to ensure that
# we have relevant permissions, as if this was a POST request.
self.check_permissions(clone_request(request, 'POST'))
self.object = self.get_object_or_none()
if self.object is None:
created = True
save_kwargs = {'force_insert': True}
success_status_code = status.HTTP_201_CREATED
......@@ -139,6 +138,16 @@ class UpdateModelMixin(object):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
def get_object_or_none(self):
try:
return self.get_object()
except Http404:
# If this is a PUT-as-create operation, we need to ensure that
# we have relevant permissions, as if this was a POST request.
# This will either raise a PermissionDenied exception,
# or simply return None
self.check_permissions(clone_request(self.request, 'POST'))
def pre_save(self, obj):
"""
Set any attributes on the object that are implicit in the request.
......@@ -168,7 +177,6 @@ class UpdateModelMixin(object):
class DestroyModelMixin(object):
"""
Destroy a model instance.
Should be mixed in with `SingleObjectAPIView`.
"""
def destroy(self, request, *args, **kwargs):
obj = self.get_object()
......
......@@ -126,6 +126,11 @@ class DjangoModelPermissions(BasePermission):
if model_cls is None and queryset is not None:
model_cls = queryset.model
# Workaround to ensure DjangoModelPermissions are not applied
# to the root view when using DefaultRouter.
if model_cls is None and getattr(view, '_ignore_model_permissions'):
return True
assert model_cls, ('Cannot apply DjangoModelPermissions on a view that'
' does not have `.model` or `.queryset` property.')
......
......@@ -8,10 +8,11 @@ from __future__ import unicode_literals
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
from django import forms
from django.db.models.fields import BLANK_CHOICE_DASH
from django.forms import widgets
from django.forms.models import ModelChoiceIterator
from django.utils.translation import ugettext_lazy as _
from rest_framework.fields import Field, WritableField, get_component
from rest_framework.fields import Field, WritableField, get_component, is_simple_callable
from rest_framework.reverse import reverse
from rest_framework.compat import urlparse
from rest_framework.compat import smart_text
......@@ -47,7 +48,7 @@ class RelatedField(WritableField):
DeprecationWarning, stacklevel=2)
kwargs['required'] = not kwargs.pop('null')
self.queryset = kwargs.pop('queryset', None)
queryset = kwargs.pop('queryset', None)
self.many = kwargs.pop('many', self.many)
if self.many:
self.widget = self.many_widget
......@@ -56,6 +57,11 @@ class RelatedField(WritableField):
kwargs['read_only'] = kwargs.pop('read_only', self.read_only)
super(RelatedField, self).__init__(*args, **kwargs)
if not self.required:
self.empty_label = BLANK_CHOICE_DASH[0][1]
self.queryset = queryset
def initialize(self, parent, field_name):
super(RelatedField, self).initialize(parent, field_name)
if self.queryset is None and not self.read_only:
......@@ -66,7 +72,6 @@ class RelatedField(WritableField):
else: # Reverse
self.queryset = manager.field.rel.to._default_manager.all()
except Exception:
raise
msg = ('Serializer related fields must include a `queryset`' +
' argument or set `read_only=True')
raise Exception(msg)
......@@ -139,7 +144,12 @@ class RelatedField(WritableField):
return None
if self.many:
if is_simple_callable(getattr(value, 'all', None)):
return [self.to_native(item) for item in value.all()]
else:
# Also support non-queryset iterables.
# This allows us to also support plain lists of related items.
return [self.to_native(item) for item in value]
return self.to_native(value)
def field_from_native(self, data, files, field_name, into):
......@@ -221,15 +231,28 @@ class PrimaryKeyRelatedField(RelatedField):
def field_to_native(self, obj, field_name):
if self.many:
# To-many relationship
try:
queryset = None
if not self.source:
# Prefer obj.serializable_value for performance reasons
queryset = obj.serializable_value(self.source or field_name)
try:
queryset = obj.serializable_value(field_name)
except AttributeError:
pass
if queryset is None:
# RelatedManager (reverse relationship)
queryset = getattr(obj, self.source or field_name)
source = self.source or field_name
queryset = obj
for component in source.split('.'):
queryset = get_component(queryset, component)
# Forward relationship
if is_simple_callable(getattr(queryset, 'all', None)):
return [self.to_native(item.pk) for item in queryset.all()]
else:
# Also support non-queryset iterables.
# This allows us to also support plain lists of related items.
return [self.to_native(item.pk) for item in queryset]
# To-one relationship
try:
......@@ -434,7 +457,7 @@ class HyperlinkedRelatedField(RelatedField):
raise Exception('Writable related fields must include a `queryset` argument')
try:
http_prefix = value.startswith('http:') or value.startswith('https:')
http_prefix = value.startswith(('http:', 'https:'))
except AttributeError:
msg = self.error_messages['incorrect_type']
raise ValidationError(msg % type(value).__name__)
......@@ -465,17 +488,35 @@ class HyperlinkedIdentityField(Field):
"""
Represents the instance, or a property on the instance, using hyperlinking.
"""
lookup_field = 'pk'
read_only = True
# These are all pending deprecation
pk_url_kwarg = 'pk'
slug_field = 'slug'
slug_url_kwarg = None # Defaults to same as `slug_field` unless overridden
read_only = True
def __init__(self, *args, **kwargs):
# TODO: Make view_name mandatory, and have the
# HyperlinkedModelSerializer set it on-the-fly
self.view_name = kwargs.pop('view_name', None)
# Optionally the format of the target hyperlink may be specified
try:
self.view_name = kwargs.pop('view_name')
except KeyError:
msg = "HyperlinkedIdentityField requires 'view_name' argument"
raise ValueError(msg)
self.format = kwargs.pop('format', None)
lookup_field = kwargs.pop('lookup_field', None)
self.lookup_field = lookup_field or self.lookup_field
# These are pending deprecation
if 'pk_url_kwarg' in kwargs:
msg = 'pk_url_kwarg is pending deprecation. Use lookup_field instead.'
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
if 'slug_url_kwarg' in kwargs:
msg = 'slug_url_kwarg is pending deprecation. Use lookup_field instead.'
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
if 'slug_field' in kwargs:
msg = 'slug_field is pending deprecation. Use lookup_field instead.'
warnings.warn(msg, PendingDeprecationWarning, stacklevel=2)
self.slug_field = kwargs.pop('slug_field', self.slug_field)
default_slug_kwarg = self.slug_url_kwarg or self.slug_field
......@@ -487,8 +528,7 @@ class HyperlinkedIdentityField(Field):
def field_to_native(self, obj, field_name):
request = self.context.get('request', None)
format = self.context.get('format', None)
view_name = self.view_name or self.parent.opts.view_name
kwargs = {self.pk_url_kwarg: obj.pk}
view_name = self.view_name
if request is None:
warnings.warn("Using `HyperlinkedIdentityField` without including the "
......@@ -508,29 +548,51 @@ class HyperlinkedIdentityField(Field):
if format and self.format and self.format != format:
format = self.format
# Return the hyperlink, or error if incorrectly configured.
try:
return reverse(view_name, kwargs=kwargs, request=request, format=format)
return self.get_url(obj, view_name, request, format)
except NoReverseMatch:
pass
msg = (
'Could not resolve URL for hyperlinked relationship using '
'view name "%s". You may have failed to include the related '
'model in your API, or incorrectly configured the '
'`lookup_field` attribute on this field.'
)
raise Exception(msg % view_name)
slug = getattr(obj, self.slug_field, None)
def get_url(self, obj, view_name, request, format):
"""
Given an object, return the URL that hyperlinks to the object.
if not slug:
raise Exception('Could not resolve URL for field using view name "%s"' % view_name)
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
attributes are not configured to correctly match the URL conf.
"""
lookup_field = getattr(obj, self.lookup_field)
kwargs = {self.lookup_field: lookup_field}
try:
return reverse(view_name, kwargs=kwargs, request=request, format=format)
except NoReverseMatch:
pass
kwargs = {self.slug_url_kwarg: slug}
if self.pk_url_kwarg != 'pk':
# Only try pk lookup if it has been explicitly set.
# Otherwise, the default `lookup_field = 'pk'` has us covered.
kwargs = {self.pk_url_kwarg: obj.pk}
try:
return reverse(view_name, kwargs=kwargs, request=request, format=format)
except NoReverseMatch:
pass
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
slug = getattr(obj, self.slug_field, None)
if slug:
# Only use slug lookup if a slug field exists on the model
kwargs = {self.slug_url_kwarg: slug}
try:
return reverse(view_name, kwargs=kwargs, request=request, format=format)
except NoReverseMatch:
pass
raise Exception('Could not resolve URL for field using view name "%s"' % view_name)
raise NoReverseMatch()
### Old-style many classes for backwards compat
......
......@@ -9,7 +9,6 @@ REST framework also provides an HTML renderer the renders the browsable API.
from __future__ import unicode_literals
import copy
import string
import json
from django import forms
from django.http.multipartparser import parse_header
......@@ -36,6 +35,7 @@ class BaseRenderer(object):
media_type = None
format = None
charset = 'utf-8'
def render(self, data, accepted_media_type=None, renderer_context=None):
raise NotImplemented('Renderer class requires .render() to be implemented')
......@@ -43,16 +43,21 @@ class BaseRenderer(object):
class JSONRenderer(BaseRenderer):
"""
Renderer which serializes to json.
Renderer which serializes to JSON.
Applies JSON's backslash-u character escaping for non-ascii characters.
"""
media_type = 'application/json'
format = 'json'
encoder_class = encoders.JSONEncoder
ensure_ascii = True
charset = 'utf-8'
# Note that JSON encodings must be utf-8, utf-16 or utf-32.
# See: http://www.ietf.org/rfc/rfc4627.txt
def render(self, data, accepted_media_type=None, renderer_context=None):
"""
Render `obj` into json.
Render `data` into JSON.
"""
if data is None:
return ''
......@@ -72,7 +77,25 @@ class JSONRenderer(BaseRenderer):
except (ValueError, TypeError):
indent = None
return json.dumps(data, cls=self.encoder_class, indent=indent)
ret = json.dumps(data, cls=self.encoder_class,
indent=indent, ensure_ascii=self.ensure_ascii)
# On python 2.x json.dumps() returns bytestrings if ensure_ascii=True,
# but if ensure_ascii=False, the return type is underspecified,
# and may (or may not) be unicode.
# On python 3.x json.dumps() returns unicode strings.
if isinstance(ret, six.text_type):
return bytes(ret.encode(self.charset))
return ret
class UnicodeJSONRenderer(JSONRenderer):
ensure_ascii = False
charset = 'utf-8'
"""
Renderer which serializes to JSON.
Does *not* apply JSON's character escaping for non-ascii characters.
"""
class JSONPRenderer(JSONRenderer):
......@@ -105,7 +128,7 @@ class JSONPRenderer(JSONRenderer):
callback = self.get_callback(renderer_context)
json = super(JSONPRenderer, self).render(data, accepted_media_type,
renderer_context)
return "%s(%s);" % (callback, json)
return callback.encode(self.charset) + b'(' + json + b');'
class XMLRenderer(BaseRenderer):
......@@ -115,6 +138,7 @@ class XMLRenderer(BaseRenderer):
media_type = 'application/xml'
format = 'xml'
charset = 'utf-8'
def render(self, data, accepted_media_type=None, renderer_context=None):
"""
......@@ -125,7 +149,7 @@ class XMLRenderer(BaseRenderer):
stream = StringIO()
xml = SimplerXMLGenerator(stream, "utf-8")
xml = SimplerXMLGenerator(stream, self.charset)
xml.startDocument()
xml.startElement("root", {})
......@@ -164,6 +188,7 @@ class YAMLRenderer(BaseRenderer):
media_type = 'application/yaml'
format = 'yaml'
encoder = encoders.SafeDumper
charset = 'utf-8'
def render(self, data, accepted_media_type=None, renderer_context=None):
"""
......@@ -174,7 +199,7 @@ class YAMLRenderer(BaseRenderer):
if data is None:
return ''
return yaml.dump(data, stream=None, Dumper=self.encoder)
return yaml.dump(data, stream=None, encoding=self.charset, Dumper=self.encoder)
class TemplateHTMLRenderer(BaseRenderer):
......@@ -204,6 +229,7 @@ class TemplateHTMLRenderer(BaseRenderer):
'%(status_code)s.html',
'api_exception.html'
]
charset = 'utf-8'
def render(self, data, accepted_media_type=None, renderer_context=None):
"""
......@@ -275,6 +301,7 @@ class StaticHTMLRenderer(TemplateHTMLRenderer):
"""
media_type = 'text/html'
format = 'html'
charset = 'utf-8'
def render(self, data, accepted_media_type=None, renderer_context=None):
renderer_context = renderer_context or {}
......@@ -296,6 +323,7 @@ class BrowsableAPIRenderer(BaseRenderer):
media_type = 'text/html'
format = 'api'
template = 'rest_framework/api.html'
charset = 'utf-8'
def get_default_renderer(self, view):
"""
......@@ -320,8 +348,8 @@ class BrowsableAPIRenderer(BaseRenderer):
renderer_context['indent'] = 4
content = renderer.render(data, accepted_media_type, renderer_context)
if not all(char in string.printable for char in content):
return '[%d bytes of binary content]'
if renderer.charset is None:
return '[%d bytes of binary content]' % len(content)
return content
......@@ -336,7 +364,9 @@ class BrowsableAPIRenderer(BaseRenderer):
return # Cannot use form overloading
try:
view.check_permissions(clone_request(request, method))
view.check_permissions(request)
if obj is not None:
view.check_object_permissions(request, obj)
except exceptions.APIException:
return False # Doesn't have permissions
return True
......@@ -366,12 +396,40 @@ class BrowsableAPIRenderer(BaseRenderer):
if getattr(v, 'default', None) is not None:
kwargs['initial'] = v.default
kwargs['label'] = k
if getattr(v, 'label', None) is not None:
kwargs['label'] = v.label
if getattr(v, 'help_text', None) is not None:
kwargs['help_text'] = v.help_text
fields[k] = v.form_field_class(**kwargs)
return fields
def _get_form(self, view, method, request):
# We need to impersonate a request with the correct method,
# so that eg. any dynamic get_serializer_class methods return the
# correct form for each method.
restore = view.request
request = clone_request(request, method)
view.request = request
try:
return self.get_form(view, method, request)
finally:
view.request = restore
def _get_raw_data_form(self, view, method, request, media_types):
# We need to impersonate a request with the correct method,
# so that eg. any dynamic get_serializer_class methods return the
# correct form for each method.
restore = view.request
request = clone_request(request, method)
view.request = request
try:
return self.get_raw_data_form(view, method, request, media_types)
finally:
view.request = restore
def get_form(self, view, method, request):
"""
Get a form, possibly bound to either the input or output data.
......@@ -449,10 +507,7 @@ class BrowsableAPIRenderer(BaseRenderer):
def render(self, data, accepted_media_type=None, renderer_context=None):
"""
Renders *obj* using the :attr:`template` set on the class.
The context used in the template contains all the information
needed to self-document the response to this request.
Render the HTML for the browsable API representation.
"""
accepted_media_type = accepted_media_type or ''
renderer_context = renderer_context or {}
......@@ -465,15 +520,15 @@ class BrowsableAPIRenderer(BaseRenderer):
renderer = self.get_default_renderer(view)
content = self.get_content(renderer, data, accepted_media_type, renderer_context)
put_form = self.get_form(view, 'PUT', request)
post_form = self.get_form(view, 'POST', request)
patch_form = self.get_form(view, 'PATCH', request)
delete_form = self.get_form(view, 'DELETE', request)
options_form = self.get_form(view, 'OPTIONS', request)
put_form = self._get_form(view, 'PUT', request)
post_form = self._get_form(view, 'POST', request)
patch_form = self._get_form(view, 'PATCH', request)
delete_form = self._get_form(view, 'DELETE', request)
options_form = self._get_form(view, 'OPTIONS', request)
raw_data_put_form = self.get_raw_data_form(view, 'PUT', request, media_types)
raw_data_post_form = self.get_raw_data_form(view, 'POST', request, media_types)
raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request, media_types)
raw_data_put_form = self._get_raw_data_form(view, 'PUT', request, media_types)
raw_data_post_form = self._get_raw_data_form(view, 'POST', request, media_types)
raw_data_patch_form = self._get_raw_data_form(view, 'PATCH', request, media_types)
raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form
name = self.get_name(view)
......
......@@ -173,7 +173,7 @@ class Request(object):
by the authentication classes provided to the request.
"""
if not hasattr(self, '_user'):
self._authenticator, self._user, self._auth = self._authenticate()
self._authenticate()
return self._user
@user.setter
......@@ -192,7 +192,7 @@ class Request(object):
request, such as an authentication token.
"""
if not hasattr(self, '_auth'):
self._authenticator, self._user, self._auth = self._authenticate()
self._authenticate()
return self._auth
@auth.setter
......@@ -210,7 +210,7 @@ class Request(object):
to authenticate the request, or `None`.
"""
if not hasattr(self, '_authenticator'):
self._authenticator, self._user, self._auth = self._authenticate()
self._authenticate()
return self._authenticator
def _load_data_and_files(self):
......@@ -330,11 +330,18 @@ class Request(object):
Returns a three-tuple of (authenticator, user, authtoken).
"""
for authenticator in self.authenticators:
try:
user_auth_tuple = authenticator.authenticate(self)
except exceptions.APIException:
self._not_authenticated()
raise
if not user_auth_tuple is None:
user, auth = user_auth_tuple
return (authenticator, user, auth)
return self._not_authenticated()
self._authenticator = authenticator
self._user, self._auth = user_auth_tuple
return
self._not_authenticated()
def _not_authenticated(self):
"""
......@@ -343,17 +350,17 @@ class Request(object):
By default this will be (None, AnonymousUser, None).
"""
self._authenticator = None
if api_settings.UNAUTHENTICATED_USER:
user = api_settings.UNAUTHENTICATED_USER()
self._user = api_settings.UNAUTHENTICATED_USER()
else:
user = None
self._user = None
if api_settings.UNAUTHENTICATED_TOKEN:
auth = api_settings.UNAUTHENTICATED_TOKEN()
self._auth = api_settings.UNAUTHENTICATED_TOKEN()
else:
auth = None
return (None, user, auth)
self._auth = None
def __getattr__(self, attr):
"""
......
"""
The Response class in REST framework is similiar to HTTPResponse, except that
The Response class in REST framework is similar to HTTPResponse, except that
it is initialized with unrendered data, instead of a pre-rendered string.
The appropriate renderer is called during Django's template response rendering.
......@@ -12,13 +12,13 @@ from rest_framework.compat import six
class Response(SimpleTemplateResponse):
"""
An HttpResponse that allows it's data to be rendered into
An HttpResponse that allows its data to be rendered into
arbitrary media types.
"""
def __init__(self, data=None, status=200,
template_name=None, headers=None,
exception=False):
exception=False, content_type=None):
"""
Alters the init arguments slightly.
For example, drop 'template_name', and instead use 'data'.
......@@ -30,6 +30,7 @@ class Response(SimpleTemplateResponse):
self.data = data
self.template_name = template_name
self.exception = exception
self.content_type = content_type
if headers:
for name, value in six.iteritems(headers):
......@@ -46,8 +47,21 @@ class Response(SimpleTemplateResponse):
assert context, ".renderer_context not set on Response"
context['response'] = self
self['Content-Type'] = media_type
return renderer.render(self.data, media_type, context)
charset = renderer.charset
content_type = self.content_type
if content_type is None and charset is not None:
content_type = "{0}; charset={1}".format(media_type, charset)
elif content_type is None:
content_type = media_type
self['Content-Type'] = content_type
ret = renderer.render(self.data, media_type, context)
if isinstance(ret, six.text_type):
assert charset, 'renderer returned unicode, and did not specify ' \
'a charset value.'
return bytes(ret.encode(charset))
return ret
@property
def status_text(self):
......
......@@ -16,8 +16,8 @@ For example, you might have a `urls.py` that looks something like this:
from __future__ import unicode_literals
from collections import namedtuple
from django.conf.urls import url, patterns
from rest_framework.decorators import api_view
from rest_framework import views
from rest_framework.compat import patterns, url
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.urlpatterns import format_suffix_patterns
......@@ -71,7 +71,7 @@ class SimpleRouter(BaseRouter):
routes = [
# List route.
Route(
url=r'^{prefix}/$',
url=r'^{prefix}{trailing_slash}$',
mapping={
'get': 'list',
'post': 'create'
......@@ -81,7 +81,7 @@ class SimpleRouter(BaseRouter):
),
# Detail route.
Route(
url=r'^{prefix}/{lookup}/$',
url=r'^{prefix}/{lookup}{trailing_slash}$',
mapping={
'get': 'retrieve',
'put': 'update',
......@@ -94,7 +94,7 @@ class SimpleRouter(BaseRouter):
# Dynamically generated routes.
# Generated using @action or @link decorators on methods of the viewset.
Route(
url=r'^{prefix}/{lookup}/{methodname}/$',
url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$',
mapping={
'{httpmethod}': '{methodname}',
},
......@@ -103,6 +103,10 @@ class SimpleRouter(BaseRouter):
),
]
def __init__(self, trailing_slash=True):
self.trailing_slash = trailing_slash and '/' or ''
super(SimpleRouter, self).__init__()
def get_default_base_name(self, viewset):
"""
If `base_name` is not specified, attempt to automatically determine
......@@ -127,23 +131,23 @@ class SimpleRouter(BaseRouter):
"""
# Determine any `@action` or `@link` decorated methods on the viewset
dynamic_routes = {}
dynamic_routes = []
for methodname in dir(viewset):
attr = getattr(viewset, methodname)
httpmethod = getattr(attr, 'bind_to_method', None)
if httpmethod:
dynamic_routes[httpmethod] = methodname
httpmethods = getattr(attr, 'bind_to_methods', None)
if httpmethods:
dynamic_routes.append((httpmethods, methodname))
ret = []
for route in self.routes:
if route.mapping == {'{httpmethod}': '{methodname}'}:
# Dynamic routes (@link or @action decorator)
for httpmethod, methodname in dynamic_routes.items():
for httpmethods, methodname in dynamic_routes:
initkwargs = route.initkwargs.copy()
initkwargs.update(getattr(viewset, methodname).kwargs)
ret.append(Route(
url=replace_methodname(route.url, methodname),
mapping={httpmethod: methodname},
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
name=replace_methodname(route.name, methodname),
initkwargs=initkwargs,
))
......@@ -192,7 +196,11 @@ class SimpleRouter(BaseRouter):
continue
# Build the url pattern
regex = route.url.format(prefix=prefix, lookup=lookup)
regex = route.url.format(
prefix=prefix,
lookup=lookup,
trailing_slash=self.trailing_slash
)
view = viewset.as_view(mapping, **route.initkwargs)
name = route.name.format(basename=basename)
ret.append(url(regex, view, name=name))
......@@ -217,14 +225,16 @@ class DefaultRouter(SimpleRouter):
for prefix, viewset, basename in self.registry:
api_root_dict[prefix] = list_name.format(basename=basename)
@api_view(('GET',))
def api_root(request, format=None):
class APIRoot(views.APIView):
_ignore_model_permissions = True
def get(self, request, format=None):
ret = {}
for key, url_name in api_root_dict.items():
ret[key] = reverse(url_name, request=request, format=format)
return Response(ret)
return api_root
return APIRoot.as_view()
def get_urls(self):
"""
......
......@@ -10,6 +10,7 @@ import sys
sys.path.append(os.path.join(os.path.dirname(__file__), "../.."))
os.environ['DJANGO_SETTINGS_MODULE'] = 'rest_framework.runtests.settings'
import django
from django.conf import settings
from django.test.utils import get_runner
......@@ -35,7 +36,11 @@ def main():
else:
print(usage())
sys.exit(1)
failures = test_runner.run_tests(['tests' + test_case])
test_module_name = 'rest_framework.tests'
if django.VERSION[0] == 1 and django.VERSION[1] < 6:
test_module_name = 'tests'
failures = test_runner.run_tests([test_module_name + test_case])
sys.exit(failures)
......
......@@ -4,6 +4,8 @@ DEBUG = True
TEMPLATE_DEBUG = DEBUG
DEBUG_PROPAGATE_EXCEPTIONS = True
ALLOWED_HOSTS = ['*']
ADMINS = (
# ('Your Name', 'your_email@domain.com'),
)
......
......@@ -25,7 +25,7 @@ from rest_framework.compat import get_concrete_model, six
#
# example_field = serializers.CharField(...)
#
# This helps keep the seperation between model fields, form fields, and
# This helps keep the separation between model fields, form fields, and
# serializer fields more explicit.
from rest_framework.relations import *
......@@ -61,7 +61,7 @@ class DictWithMetadata(dict):
def __getstate__(self):
"""
Used by pickle (e.g., caching).
Overriden to remove the metadata from the dict, since it shouldn't be
Overridden to remove the metadata from the dict, since it shouldn't be
pickled and may in some instances be unpickleable.
"""
return dict(self)
......@@ -202,7 +202,7 @@ class BaseSerializer(WritableField):
# If 'fields' is specified, use those fields, in that order.
if self.opts.fields:
assert isinstance(self.opts.fields, (list, tuple)), '`include` must be a list or tuple'
assert isinstance(self.opts.fields, (list, tuple)), '`fields` must be a list or tuple'
new = SortedDict()
for key in self.opts.fields:
new[key] = ret[key]
......@@ -210,7 +210,7 @@ class BaseSerializer(WritableField):
# Remove anything in 'exclude'
if self.opts.exclude:
assert isinstance(self.opts.fields, (list, tuple)), '`exclude` must be a list or tuple'
assert isinstance(self.opts.exclude, (list, tuple)), '`exclude` must be a list or tuple'
for key in self.opts.exclude:
ret.pop(key, None)
......@@ -317,6 +317,7 @@ class BaseSerializer(WritableField):
self._errors = {}
if data is not None or files is not None:
attrs = self.restore_fields(data, files)
if attrs is not None:
attrs = self.perform_validation(attrs)
else:
self._errors['non_field_errors'] = ['No input provided']
......@@ -381,6 +382,10 @@ class BaseSerializer(WritableField):
obj = getattr(self.parent.object, field_name) if self.parent.object else None
obj = obj.all() if is_simple_callable(getattr(obj, 'all', None)) else obj
if self.source == '*':
if value:
into.update(value)
else:
if value in (None, ''):
into[(self.source or field_name)] = None
else:
......@@ -521,6 +526,17 @@ class BaseSerializer(WritableField):
return self.object
def metadata(self):
"""
Return a dictionary of metadata about the fields on the serializer.
Useful for things like responding to OPTIONS requests, or generating
API schemas for auto-documentation.
"""
return SortedDict(
[(field_name, field.metadata())
for field_name, field in six.iteritems(self.fields)]
)
class Serializer(six.with_metaclass(SerializerMetaclass, BaseSerializer)):
pass
......@@ -591,11 +607,16 @@ class ModelSerializer(Serializer):
forward_rels += [field for field in opts.many_to_many if field.serialize]
for model_field in forward_rels:
has_through_model = False
if model_field.rel:
to_many = isinstance(model_field,
models.fields.related.ManyToManyField)
related_model = model_field.rel.to
if to_many and not model_field.rel.through._meta.auto_created:
has_through_model = True
if model_field.rel and nested:
if len(inspect.getargspec(self.get_nested_field).args) == 2:
warnings.warn(
......@@ -624,6 +645,9 @@ class ModelSerializer(Serializer):
field = self.get_field(model_field)
if field:
if has_through_model:
field.read_only = True
ret[model_field.name] = field
# Deal with reverse relationships
......@@ -641,6 +665,12 @@ class ModelSerializer(Serializer):
continue
related_model = relation.model
to_many = relation.field.rel.multiple
has_through_model = False
is_m2m = isinstance(relation.field,
models.fields.related.ManyToManyField)
if is_m2m and not relation.field.rel.through._meta.auto_created:
has_through_model = True
if nested:
field = self.get_nested_field(None, related_model, to_many)
......@@ -648,13 +678,22 @@ class ModelSerializer(Serializer):
field = self.get_related_field(None, related_model, to_many)
if field:
if has_through_model:
field.read_only = True
ret[accessor_name] = field
# Add the `read_only` flag to any fields that have bee specified
# in the `read_only_fields` option
for field_name in self.opts.read_only_fields:
assert field_name not in self.base_fields.keys(), \
"field '%s' on serializer '%s' specfied in " \
"`read_only_fields`, but also added " \
"as an explict field. Remove it from `read_only_fields`." % \
(field_name, self.__class__.__name__)
assert field_name in ret, \
"read_only_fields on '%s' included invalid item '%s'" % \
"Noexistant field '%s' specified in `read_only_fields` " \
"on serializer '%s'." % \
(self.__class__.__name__, field_name)
ret[field_name].read_only = True
......@@ -703,25 +742,51 @@ class ModelSerializer(Serializer):
Creates a default instance of a basic non-relational field.
"""
kwargs = {}
has_default = model_field.has_default()
if model_field.null or model_field.blank or has_default:
if model_field.null or model_field.blank:
kwargs['required'] = False
if isinstance(model_field, models.AutoField) or not model_field.editable:
kwargs['read_only'] = True
if has_default:
if model_field.has_default():
kwargs['default'] = model_field.get_default()
if issubclass(model_field.__class__, models.TextField):
kwargs['widget'] = widgets.Textarea
if model_field.verbose_name is not None:
kwargs['label'] = model_field.verbose_name
if model_field.help_text is not None:
kwargs['help_text'] = model_field.help_text
# TODO: TypedChoiceField?
if model_field.flatchoices: # This ModelField contains choices
kwargs['choices'] = model_field.flatchoices
return ChoiceField(**kwargs)
# put this below the ChoiceField because min_value isn't a valid initializer
if issubclass(model_field.__class__, models.PositiveIntegerField) or\
issubclass(model_field.__class__, models.PositiveSmallIntegerField):
kwargs['min_value'] = 0
attribute_dict = {
models.CharField: ['max_length'],
models.CommaSeparatedIntegerField: ['max_length'],
models.DecimalField: ['max_digits', 'decimal_places'],
models.EmailField: ['max_length'],
models.FileField: ['max_length'],
models.ImageField: ['max_length'],
models.SlugField: ['max_length'],
models.URLField: ['max_length'],
}
if model_field.__class__ in attribute_dict:
attributes = attribute_dict[model_field.__class__]
for attribute in attributes:
kwargs.update({attribute: getattr(model_field, attribute)})
try:
return self.field_mapping[model_field.__class__](**kwargs)
except KeyError:
......@@ -867,7 +932,7 @@ class HyperlinkedModelSerializerOptions(ModelSerializerOptions):
def __init__(self, meta):
super(HyperlinkedModelSerializerOptions, self).__init__(meta)
self.view_name = getattr(meta, 'view_name', None)
self.lookup_field = getattr(meta, 'slug_field', None)
self.lookup_field = getattr(meta, 'lookup_field', None)
class HyperlinkedModelSerializer(ModelSerializer):
......@@ -879,13 +944,24 @@ class HyperlinkedModelSerializer(ModelSerializer):
_default_view_name = '%(model_name)s-detail'
_hyperlink_field_class = HyperlinkedRelatedField
url = HyperlinkedIdentityField()
# Just a placeholder to ensure 'url' is the first field
# The field itself is actually created on initialization,
# when the view_name and lookup_field arguments are available.
url = Field()
def __init__(self, *args, **kwargs):
super(HyperlinkedModelSerializer, self).__init__(*args, **kwargs)
if self.opts.view_name is None:
self.opts.view_name = self._get_default_view_name(self.opts.model)
url_field = HyperlinkedIdentityField(
view_name=self.opts.view_name,
lookup_field=self.opts.lookup_field
)
url_field.initialize(self, 'url')
self.fields['url'] = url_field
def _get_default_view_name(self, model):
"""
Return the view name to use if 'view_name' is not specified in 'Meta'
......
......@@ -20,3 +20,166 @@ a single block in the template.
color: white;
text-decoration: none;
}
/* custom navigation styles */
.wrapper .navbar{
width: 100%;
position: absolute;
left: 0;
top: 0;
}
.navbar .navbar-inner{
background: #2C2C2C;
color: white;
border: none;
border-top: 5px solid #A30000;
border-radius: 0px;
}
.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand:hover{
color: white;
}
.nav-list > .active > a, .nav-list > .active > a:hover {
background: #2c2c2c;
}
.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{
color: #A30000;
}
.navbar .navbar-inner .dropdown-menu li a:hover{
background: #eeeeee;
color: #c20000;
}
/*=== dabapps bootstrap styles ====*/
html{
width:100%;
background: none;
}
body, .navbar .navbar-inner .container-fluid {
max-width: 1150px;
margin: 0 auto;
}
body{
background: url("../img/grid.png") repeat-x;
background-attachment: fixed;
}
#content{
margin: 0;
}
/* sticky footer and footer */
html, body {
height: 100%;
}
.wrapper {
min-height: 100%;
height: auto !important;
height: 100%;
margin: 0 auto -60px;
}
.form-switcher {
margin-bottom: 0;
}
.well {
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
.well .form-actions {
padding-bottom: 0;
margin-bottom: 0;
}
.well form {
margin-bottom: 0;
}
.well form .help-block {
color: #999;
}
.nav-tabs {
border: 0;
}
.nav-tabs > li {
float: right;
}
.nav-tabs li a {
margin-right: 0;
}
.nav-tabs > .active > a {
background: #f5f5f5;
}
.nav-tabs > .active > a:hover {
background: #f5f5f5;
}
.tabbable.first-tab-active .tab-content
{
border-top-right-radius: 0;
}
#footer, #push {
height: 60px; /* .push must be the same height as .footer */
}
#footer{
text-align: right;
}
#footer p {
text-align: center;
color: gray;
border-top: 1px solid #DDD;
padding-top: 10px;
}
#footer a {
color: gray;
font-weight: bold;
}
#footer a:hover {
color: gray;
}
.page-header {
border-bottom: none;
padding-bottom: 0px;
margin-bottom: 20px;
}
/* custom general page styles */
.hero-unit h2, .hero-unit h1{
color: #A30000;
}
body a, body a{
color: #A30000;
}
body a:hover{
color: #c20000;
}
#content a span{
text-decoration: underline;
}
.request-info {
clear:both;
}
......@@ -69,152 +69,3 @@ pre {
margin-bottom: 20px;
}
/*=== dabapps bootstrap styles ====*/
html{
width:100%;
background: none;
}
body, .navbar .navbar-inner .container-fluid {
max-width: 1150px;
margin: 0 auto;
}
body{
background: url("../img/grid.png") repeat-x;
background-attachment: fixed;
}
#content{
margin: 0;
}
/* custom navigation styles */
.wrapper .navbar{
width: 100%;
position: absolute;
left: 0;
top: 0;
}
.navbar .navbar-inner{
background: #2C2C2C;
color: white;
border: none;
border-top: 5px solid #A30000;
border-radius: 0px;
}
.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand{
color: white;
}
.nav-list > .active > a, .nav-list > .active > a:hover {
background: #2c2c2c;
}
.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{
color: #A30000;
}
.navbar .navbar-inner .dropdown-menu li a:hover{
background: #eeeeee;
color: #c20000;
}
/* custom general page styles */
.hero-unit h2, .hero-unit h1{
color: #A30000;
}
body a, body a{
color: #A30000;
}
body a:hover{
color: #c20000;
}
#content a span{
text-decoration: underline;
}
/* sticky footer and footer */
html, body {
height: 100%;
}
.wrapper {
min-height: 100%;
height: auto !important;
height: 100%;
margin: 0 auto -60px;
}
.form-switcher {
margin-bottom: 0;
}
.well {
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
.well .form-actions {
padding-bottom: 0;
margin-bottom: 0;
}
.well form {
margin-bottom: 0;
}
.nav-tabs {
border: 0;
}
.nav-tabs > li {
float: right;
}
.nav-tabs li a {
margin-right: 0;
}
.nav-tabs > .active > a {
background: #f5f5f5;
}
.nav-tabs > .active > a:hover {
background: #f5f5f5;
}
.tabbable.first-tab-active .tab-content
{
border-top-right-radius: 0;
}
#footer, #push {
height: 60px; /* .push must be the same height as .footer */
}
#footer{
text-align: right;
}
#footer p {
text-align: center;
color: gray;
border-top: 1px solid #DDD;
padding-top: 10px;
}
#footer a {
color: gray;
font-weight: bold;
}
#footer a:hover {
color: gray;
}
......@@ -13,8 +13,10 @@
<title>{% block title %}Django REST framework{% endblock %}</title>
{% block style %}
{% block bootstrap_theme %}<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>{% endblock %}
{% block bootstrap_theme %}
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
{% endblock %}
......@@ -30,8 +32,8 @@
<div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
<div class="navbar-inner">
<div class="container-fluid">
<span class="brand" href="/">
{% block branding %}<a href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
<span href="/">
{% block branding %}<a class='brand' href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
</span>
<ul class="nav pull-right">
{% block userlinks %}
......@@ -109,8 +111,7 @@
<div class="content-main">
<div class="page-header"><h1>{{ name }}</h1></div>
{{ description }}
<div class="request-info">
<div class="request-info" style="clear: both" >
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
</div>
<div class="response-info">
......
......@@ -6,7 +6,7 @@
{{ field.label_tag|add_class:"control-label" }}
<div class="controls">
{{ field }}
<span class="help-inline">{{ field.help_text }}</span>
<span class="help-block">{{ field.help_text }}</span>
<!--{{ field.errors|add_class:"help-block" }}-->
</div>
</div>
......
......@@ -4,17 +4,18 @@
<head>
{% block style %}
{% block bootstrap_theme %}<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>{% endblock %}
{% block bootstrap_theme %}
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
{% endblock %}
</head>
<body class="container">
<div class="container-fluid" style="margin-top: 30px">
<div class="container-fluid" style="margin-top: 30px">
<div class="row-fluid">
<div class="well" style="width: 320px; margin-left: auto; margin-right: auto">
<div class="row-fluid">
<div>
......@@ -44,12 +45,9 @@
</div>
</form>
</div>
</div><!-- /row fluid -->
</div><!--/span-->
</div><!-- /.row-fluid -->
</div>
</div>
</div><!--/.well-->
</div><!-- /.row-fluid -->
</div><!-- /.container-fluid -->
</body>
</html>
......@@ -15,7 +15,7 @@ register = template.Library()
# When 1.3 becomes unsupported by REST framework, we can instead start to
# use the {% load staticfiles %} tag, remove the following code,
# and add a dependancy that `django.contrib.staticfiles` must be installed.
# and add a dependency that `django.contrib.staticfiles` must be installed.
# Note: We can't put this into the `compat` module because the compat import
# from rest_framework.compat import ...
......
from __future__ import unicode_literals
from django.db import models
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
def foobar():
......@@ -32,7 +34,7 @@ class Anchor(RESTFrameworkModel):
class BasicModel(RESTFrameworkModel):
text = models.CharField(max_length=100)
text = models.CharField(max_length=100, verbose_name=_("Text comes here"), help_text=_("Text description."))
class SlugBasedModel(RESTFrameworkModel):
......@@ -58,13 +60,6 @@ class ReadOnlyManyToManyModel(RESTFrameworkModel):
rel = models.ManyToManyField(Anchor)
# Model to test filtering.
class FilterableItem(RESTFrameworkModel):
text = models.CharField(max_length=100)
decimal = models.DecimalField(max_digits=4, decimal_places=2)
date = models.DateField()
# Model for regression test for #285
class Comment(RESTFrameworkModel):
......@@ -166,3 +161,9 @@ class NullableOneToOneSource(RESTFrameworkModel):
name = models.CharField(max_length=100)
target = models.OneToOneField(OneToOneTarget, null=True, blank=True,
related_name='nullable_source')
# Serializer used to test BasicModel
class BasicModelSerializer(serializers.ModelSerializer):
class Meta:
model = BasicModel
......@@ -6,6 +6,8 @@ from django.utils import unittest
from rest_framework import HTTP_HEADER_ENCODING
from rest_framework import exceptions
from rest_framework import permissions
from rest_framework import renderers
from rest_framework.response import Response
from rest_framework import status
from rest_framework.authentication import (
BaseAuthentication,
......@@ -63,7 +65,7 @@ if oauth2_provider is not None:
class BasicAuthTests(TestCase):
"""Basic authentication"""
urls = 'rest_framework.tests.authentication'
urls = 'rest_framework.tests.test_authentication'
def setUp(self):
self.csrf_client = Client(enforce_csrf_checks=True)
......@@ -102,7 +104,7 @@ class BasicAuthTests(TestCase):
class SessionAuthTests(TestCase):
"""User session authentication"""
urls = 'rest_framework.tests.authentication'
urls = 'rest_framework.tests.test_authentication'
def setUp(self):
self.csrf_client = Client(enforce_csrf_checks=True)
......@@ -149,7 +151,7 @@ class SessionAuthTests(TestCase):
class TokenAuthTests(TestCase):
"""Token authentication"""
urls = 'rest_framework.tests.authentication'
urls = 'rest_framework.tests.test_authentication'
def setUp(self):
self.csrf_client = Client(enforce_csrf_checks=True)
......@@ -243,7 +245,7 @@ class IncorrectCredentialsTests(TestCase):
class OAuthTests(TestCase):
"""OAuth 1.0a authentication"""
urls = 'rest_framework.tests.authentication'
urls = 'rest_framework.tests.test_authentication'
def setUp(self):
# these imports are here because oauth is optional and hiding them in try..except block or compat
......@@ -429,7 +431,7 @@ class OAuthTests(TestCase):
class OAuth2Tests(TestCase):
"""OAuth 2.0 authentication"""
urls = 'rest_framework.tests.authentication'
urls = 'rest_framework.tests.test_authentication'
def setUp(self):
self.csrf_client = Client(enforce_csrf_checks=True)
......@@ -553,3 +555,40 @@ class OAuth2Tests(TestCase):
auth = self._create_authorization_header(token=read_write_access_token.token)
response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth)
self.assertEqual(response.status_code, 200)
class FailingAuthAccessedInRenderer(TestCase):
def setUp(self):
class AuthAccessingRenderer(renderers.BaseRenderer):
media_type = 'text/plain'
format = 'txt'
def render(self, data, media_type=None, renderer_context=None):
request = renderer_context['request']
if request.user.is_authenticated():
return b'authenticated'
return b'not authenticated'
class FailingAuth(BaseAuthentication):
def authenticate(self, request):
raise exceptions.AuthenticationFailed('authentication failed')
class ExampleView(APIView):
authentication_classes = (FailingAuth,)
renderer_classes = (AuthAccessingRenderer,)
def get(self, request):
return Response({'foo': 'bar'})
self.view = ExampleView.as_view()
def test_failing_auth_accessed_in_renderer(self):
"""
When authentication fails the renderer should still be able to access
`request.user` without raising an exception. Particularly relevant
to HTML responses that might reasonably access `request.user`.
"""
request = factory.get('/')
response = self.view(request)
content = response.render().content
self.assertEqual(content, b'not authenticated')
......@@ -36,7 +36,7 @@ urlpatterns = patterns('',
class BreadcrumbTests(TestCase):
"""Tests the breadcrumb functionality used by the HTML renderer."""
urls = 'rest_framework.tests.breadcrumbs'
urls = 'rest_framework.tests.test_breadcrumbs'
def test_root_breadcrumbs(self):
url = '/'
......
......@@ -2,7 +2,7 @@ from __future__ import unicode_literals
from django.db import models
from django.shortcuts import get_object_or_404
from django.test import TestCase
from rest_framework import generics, serializers, status
from rest_framework import generics, renderers, serializers, status
from rest_framework.tests.utils import RequestFactory
from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel
from rest_framework.compat import six
......@@ -39,6 +39,7 @@ class SlugBasedInstanceView(InstanceView):
"""
model = SlugBasedModel
serializer_class = SlugSerializer
lookup_field = 'slug'
class TestRootView(TestCase):
......@@ -120,7 +121,25 @@ class TestRootView(TestCase):
'text/html'
],
'name': 'Root',
'description': 'Example description for OPTIONS.'
'description': 'Example description for OPTIONS.',
'actions': {
'POST': {
'text': {
'max_length': 100,
'read_only': False,
'required': True,
'type': 'string',
"label": "Text comes here",
"help_text": "Text description."
},
'id': {
'read_only': True,
'required': False,
'type': 'integer',
'label': 'ID',
},
}
}
}
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, expected)
......@@ -223,9 +242,9 @@ class TestInstanceView(TestCase):
"""
OPTIONS requests to RetrieveUpdateDestroyAPIView should return metadata
"""
request = factory.options('/')
with self.assertNumQueries(0):
response = self.view(request).render()
request = factory.options('/1')
with self.assertNumQueries(1):
response = self.view(request, pk=1).render()
expected = {
'parses': [
'application/json',
......@@ -237,11 +256,39 @@ class TestInstanceView(TestCase):
'text/html'
],
'name': 'Instance',
'description': 'Example description for OPTIONS.'
'description': 'Example description for OPTIONS.',
'actions': {
'PUT': {
'text': {
'max_length': 100,
'read_only': False,
'required': True,
'type': 'string',
'label': 'Text comes here',
'help_text': 'Text description.'
},
'id': {
'read_only': True,
'required': False,
'type': 'integer',
'label': 'ID',
},
}
}
}
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, expected)
def test_get_instance_view_incorrect_arg(self):
"""
GET requests with an incorrect pk type, should raise 404, not 500.
Regression test for #890.
"""
request = factory.get('/a')
with self.assertNumQueries(0):
response = self.view(request, pk='a').render()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_put_cannot_set_id(self):
"""
PUT requests to create a new object should not be able to set the id.
......@@ -434,22 +481,14 @@ class TestFilterBackendAppliedToViews(TestCase):
{'id': obj.id, 'text': obj.text}
for obj in self.objects.all()
]
self.root_view = RootView.as_view()
self.instance_view = InstanceView.as_view()
self.original_root_backend = getattr(RootView, 'filter_backend')
self.original_instance_backend = getattr(InstanceView, 'filter_backend')
def tearDown(self):
setattr(RootView, 'filter_backend', self.original_root_backend)
setattr(InstanceView, 'filter_backend', self.original_instance_backend)
def test_get_root_view_filters_by_name_with_filter_backend(self):
"""
GET requests to ListCreateAPIView should return filtered list.
"""
setattr(RootView, 'filter_backend', InclusiveFilterBackend)
root_view = RootView.as_view(filter_backends=(InclusiveFilterBackend,))
request = factory.get('/')
response = self.root_view(request).render()
response = root_view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 1)
self.assertEqual(response.data, [{'id': 1, 'text': 'foo'}])
......@@ -458,9 +497,9 @@ class TestFilterBackendAppliedToViews(TestCase):
"""
GET requests to ListCreateAPIView should return empty list when all models are filtered out.
"""
setattr(RootView, 'filter_backend', ExclusiveFilterBackend)
root_view = RootView.as_view(filter_backends=(ExclusiveFilterBackend,))
request = factory.get('/')
response = self.root_view(request).render()
response = root_view(request).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, [])
......@@ -468,9 +507,9 @@ class TestFilterBackendAppliedToViews(TestCase):
"""
GET requests to RetrieveUpdateDestroyAPIView should raise 404 when model filtered out.
"""
setattr(InstanceView, 'filter_backend', ExclusiveFilterBackend)
instance_view = InstanceView.as_view(filter_backends=(ExclusiveFilterBackend,))
request = factory.get('/1')
response = self.instance_view(request, pk=1).render()
response = instance_view(request, pk=1).render()
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(response.data, {'detail': 'Not found'})
......@@ -478,8 +517,40 @@ class TestFilterBackendAppliedToViews(TestCase):
"""
GET requests to RetrieveUpdateDestroyAPIView should return a single object when not excluded
"""
setattr(InstanceView, 'filter_backend', InclusiveFilterBackend)
instance_view = InstanceView.as_view(filter_backends=(InclusiveFilterBackend,))
request = factory.get('/1')
response = self.instance_view(request, pk=1).render()
response = instance_view(request, pk=1).render()
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, {'id': 1, 'text': 'foo'})
class TwoFieldModel(models.Model):
field_a = models.CharField(max_length=100)
field_b = models.CharField(max_length=100)
class DynamicSerializerView(generics.ListCreateAPIView):
model = TwoFieldModel
renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer)
def get_serializer_class(self):
if self.request.method == 'POST':
class DynamicSerializer(serializers.ModelSerializer):
class Meta:
model = TwoFieldModel
fields = ('field_b',)
return DynamicSerializer
return super(DynamicSerializerView, self).get_serializer_class()
class TestFilterBackendAppliedToViews(TestCase):
def test_dynamic_serializer_form_in_browsable_api(self):
"""
GET requests to ListCreateAPIView should return filtered list.
"""
view = DynamicSerializerView.as_view()
request = factory.get('/')
response = view(request).render()
self.assertContains(response, 'field_b')
self.assertNotContains(response, 'field_a')
......@@ -42,7 +42,7 @@ urlpatterns = patterns('',
class TemplateHTMLRendererTests(TestCase):
urls = 'rest_framework.tests.htmlrenderer'
urls = 'rest_framework.tests.test_htmlrenderer'
def setUp(self):
"""
......@@ -66,23 +66,23 @@ class TemplateHTMLRendererTests(TestCase):
def test_simple_html_view(self):
response = self.client.get('/')
self.assertContains(response, "example: foobar")
self.assertEqual(response['Content-Type'], 'text/html')
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
def test_not_found_html_view(self):
response = self.client.get('/not_found')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(response.content, six.b("404 Not Found"))
self.assertEqual(response['Content-Type'], 'text/html')
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
def test_permission_denied_html_view(self):
response = self.client.get('/permission_denied')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.content, six.b("403 Forbidden"))
self.assertEqual(response['Content-Type'], 'text/html')
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
class TemplateHTMLRendererExceptionTests(TestCase):
urls = 'rest_framework.tests.htmlrenderer'
urls = 'rest_framework.tests.test_htmlrenderer'
def setUp(self):
"""
......@@ -109,10 +109,10 @@ class TemplateHTMLRendererExceptionTests(TestCase):
response = self.client.get('/not_found')
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
self.assertEqual(response.content, six.b("404: Not found"))
self.assertEqual(response['Content-Type'], 'text/html')
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
def test_permission_denied_html_view_with_template(self):
response = self.client.get('/permission_denied')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.content, six.b("403: Permission denied"))
self.assertEqual(response['Content-Type'], 'text/html')
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
......@@ -27,6 +27,14 @@ class PhotoSerializer(serializers.Serializer):
return Photo(**attrs)
class AlbumSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='album-detail', lookup_field='title')
class Meta:
model = Album
fields = ('title', 'url')
class BasicList(generics.ListCreateAPIView):
model = BasicModel
model_serializer_class = serializers.HyperlinkedModelSerializer
......@@ -73,6 +81,8 @@ class PhotoListCreate(generics.ListCreateAPIView):
class AlbumDetail(generics.RetrieveAPIView):
model = Album
serializer_class = AlbumSerializer
lookup_field = 'title'
class OptionalRelationDetail(generics.RetrieveUpdateDestroyAPIView):
......@@ -96,7 +106,7 @@ urlpatterns = patterns('',
class TestBasicHyperlinkedView(TestCase):
urls = 'rest_framework.tests.hyperlinkedserializers'
urls = 'rest_framework.tests.test_hyperlinkedserializers'
def setUp(self):
"""
......@@ -133,7 +143,7 @@ class TestBasicHyperlinkedView(TestCase):
class TestManyToManyHyperlinkedView(TestCase):
urls = 'rest_framework.tests.hyperlinkedserializers'
urls = 'rest_framework.tests.test_hyperlinkedserializers'
def setUp(self):
"""
......@@ -180,8 +190,38 @@ class TestManyToManyHyperlinkedView(TestCase):
self.assertEqual(response.data, self.data[0])
class TestHyperlinkedIdentityFieldLookup(TestCase):
urls = 'rest_framework.tests.test_hyperlinkedserializers'
def setUp(self):
"""
Create 3 Album instances.
"""
titles = ['foo', 'bar', 'baz']
for title in titles:
album = Album(title=title)
album.save()
self.detail_view = AlbumDetail.as_view()
self.data = {
'foo': {'title': 'foo', 'url': 'http://testserver/albums/foo/'},
'bar': {'title': 'bar', 'url': 'http://testserver/albums/bar/'},
'baz': {'title': 'baz', 'url': 'http://testserver/albums/baz/'}
}
def test_lookup_field(self):
"""
GET requests to AlbumDetail view should return serialized Albums
with a url field keyed by `title`.
"""
for album in Album.objects.all():
request = factory.get('/albums/{0}/'.format(album.title))
response = self.detail_view(request, title=album.title)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, self.data[album.title])
class TestCreateWithForeignKeys(TestCase):
urls = 'rest_framework.tests.hyperlinkedserializers'
urls = 'rest_framework.tests.test_hyperlinkedserializers'
def setUp(self):
"""
......@@ -206,7 +246,7 @@ class TestCreateWithForeignKeys(TestCase):
class TestCreateWithForeignKeysAndCustomSlug(TestCase):
urls = 'rest_framework.tests.hyperlinkedserializers'
urls = 'rest_framework.tests.test_hyperlinkedserializers'
def setUp(self):
"""
......@@ -231,7 +271,7 @@ class TestCreateWithForeignKeysAndCustomSlug(TestCase):
class TestOptionalRelationHyperlinkedView(TestCase):
urls = 'rest_framework.tests.hyperlinkedserializers'
urls = 'rest_framework.tests.test_hyperlinkedserializers'
def setUp(self):
"""
......
......@@ -3,19 +3,24 @@ from django.test import TestCase
from django.test.client import RequestFactory
from rest_framework.negotiation import DefaultContentNegotiation
from rest_framework.request import Request
from rest_framework.renderers import BaseRenderer
factory = RequestFactory()
class MockJSONRenderer(object):
class MockJSONRenderer(BaseRenderer):
media_type = 'application/json'
class MockHTMLRenderer(object):
class MockHTMLRenderer(BaseRenderer):
media_type = 'text/html'
class NoCharsetSpecifiedRenderer(BaseRenderer):
media_type = 'my/media'
class TestAcceptedMediaType(TestCase):
def setUp(self):
self.renderers = [MockJSONRenderer(), MockHTMLRenderer()]
......
from __future__ import unicode_literals
import datetime
from decimal import Decimal
import django
from django.db import models
from django.core.paginator import Paginator
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils import unittest
from rest_framework import generics, status, pagination, filters, serializers
from rest_framework.compat import django_filters
from rest_framework.tests.models import BasicModel, FilterableItem
from rest_framework.tests.models import BasicModel
factory = RequestFactory()
class FilterableItem(models.Model):
text = models.CharField(max_length=100)
decimal = models.DecimalField(max_digits=4, decimal_places=2)
date = models.DateField()
class RootView(generics.ListCreateAPIView):
"""
Example description for OPTIONS.
......@@ -124,7 +130,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
model = FilterableItem
paginate_by = 10
filter_class = DecimalFilter
filter_backend = filters.DjangoFilterBackend
filter_backends = (filters.DjangoFilterBackend,)
view = FilterFieldsRootView.as_view()
......@@ -171,7 +177,7 @@ class IntegrationTestPaginationAndFiltering(TestCase):
class BasicFilterFieldsRootView(generics.ListCreateAPIView):
model = FilterableItem
paginate_by = 10
filter_backend = DecimalFilterBackend
filter_backends = (DecimalFilterBackend,)
view = BasicFilterFieldsRootView.as_view()
......
......@@ -108,6 +108,48 @@ class ModelPermissionsIntegrationTests(TestCase):
response = instance_view(request, pk='2')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_options_permitted(self):
request = factory.options('/', content_type='application/json',
HTTP_AUTHORIZATION=self.permitted_credentials)
response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data)
self.assertEqual(list(response.data['actions'].keys()), ['POST'])
request = factory.options('/1', content_type='application/json',
HTTP_AUTHORIZATION=self.permitted_credentials)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data)
self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
def test_options_disallowed(self):
request = factory.options('/', content_type='application/json',
HTTP_AUTHORIZATION=self.disallowed_credentials)
response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotIn('actions', response.data)
request = factory.options('/1', content_type='application/json',
HTTP_AUTHORIZATION=self.disallowed_credentials)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotIn('actions', response.data)
def test_options_updateonly(self):
request = factory.options('/', content_type='application/json',
HTTP_AUTHORIZATION=self.updateonly_credentials)
response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertNotIn('actions', response.data)
request = factory.options('/1', content_type='application/json',
HTTP_AUTHORIZATION=self.updateonly_credentials)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data)
self.assertEqual(list(response.data['actions'].keys()), ['PUT'])
class OwnerModel(models.Model):
text = models.CharField(max_length=100)
......
......@@ -5,6 +5,7 @@ from __future__ import unicode_literals
from django.db import models
from django.test import TestCase
from rest_framework import serializers
from rest_framework.tests.models import BlogPost
class NullModel(models.Model):
......@@ -33,7 +34,7 @@ class FieldTests(TestCase):
self.assertRaises(serializers.ValidationError, field.from_native, [])
class TestManyRelateMixin(TestCase):
class TestManyRelatedMixin(TestCase):
def test_missing_many_to_many_related_field(self):
'''
Regression test for #632
......@@ -45,3 +46,55 @@ class TestManyRelateMixin(TestCase):
into = {}
field.field_from_native({}, None, 'field_name', into)
self.assertEqual(into['field_name'], [])
# Regression tests for #694 (`source` attribute on related fields)
class RelatedFieldSourceTests(TestCase):
def test_related_manager_source(self):
"""
Relational fields should be able to use manager-returning methods as their source.
"""
BlogPost.objects.create(title='blah')
field = serializers.RelatedField(many=True, source='get_blogposts_manager')
class ClassWithManagerMethod(object):
def get_blogposts_manager(self):
return BlogPost.objects
obj = ClassWithManagerMethod()
value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, ['BlogPost object'])
def test_related_queryset_source(self):
"""
Relational fields should be able to use queryset-returning methods as their source.
"""
BlogPost.objects.create(title='blah')
field = serializers.RelatedField(many=True, source='get_blogposts_queryset')
class ClassWithQuerysetMethod(object):
def get_blogposts_queryset(self):
return BlogPost.objects.all()
obj = ClassWithQuerysetMethod()
value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, ['BlogPost object'])
def test_dotted_source(self):
"""
Source argument should support dotted.source notation.
"""
BlogPost.objects.create(title='blah')
field = serializers.RelatedField(many=True, source='a.b.c')
class ClassWithQuerysetMethod(object):
a = {
'b': {
'c': BlogPost.objects.all()
}
}
obj = ClassWithQuerysetMethod()
value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, ['BlogPost object'])
......@@ -4,6 +4,7 @@ from django.test.client import RequestFactory
from rest_framework import serializers
from rest_framework.compat import patterns, url
from rest_framework.tests.models import (
BlogPost,
ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
)
......@@ -16,6 +17,7 @@ def dummy_view(request, pk):
pass
urlpatterns = patterns('',
url(r'^dummyurl/(?P<pk>[0-9]+)/$', dummy_view, name='dummy-url'),
url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
url(r'^foreignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeysource-detail'),
......@@ -69,7 +71,7 @@ class NullableOneToOneTargetSerializer(serializers.HyperlinkedModelSerializer):
# TODO: Add test that .data cannot be accessed prior to .is_valid
class HyperlinkedManyToManyTests(TestCase):
urls = 'rest_framework.tests.relations_hyperlink'
urls = 'rest_framework.tests.test_relations_hyperlink'
def setUp(self):
for idx in range(1, 4):
......@@ -177,7 +179,7 @@ class HyperlinkedManyToManyTests(TestCase):
class HyperlinkedForeignKeyTests(TestCase):
urls = 'rest_framework.tests.relations_hyperlink'
urls = 'rest_framework.tests.test_relations_hyperlink'
def setUp(self):
target = ForeignKeyTarget(name='target-1')
......@@ -305,7 +307,7 @@ class HyperlinkedForeignKeyTests(TestCase):
class HyperlinkedNullableForeignKeyTests(TestCase):
urls = 'rest_framework.tests.relations_hyperlink'
urls = 'rest_framework.tests.test_relations_hyperlink'
def setUp(self):
target = ForeignKeyTarget(name='target-1')
......@@ -433,7 +435,7 @@ class HyperlinkedNullableForeignKeyTests(TestCase):
class HyperlinkedNullableOneToOneTests(TestCase):
urls = 'rest_framework.tests.relations_hyperlink'
urls = 'rest_framework.tests.test_relations_hyperlink'
def setUp(self):
target = OneToOneTarget(name='target-1')
......@@ -451,3 +453,72 @@ class HyperlinkedNullableOneToOneTests(TestCase):
{'url': 'http://testserver/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None},
]
self.assertEqual(serializer.data, expected)
# Regression tests for #694 (`source` attribute on related fields)
class HyperlinkedRelatedFieldSourceTests(TestCase):
urls = 'rest_framework.tests.test_relations_hyperlink'
def test_related_manager_source(self):
"""
Relational fields should be able to use manager-returning methods as their source.
"""
BlogPost.objects.create(title='blah')
field = serializers.HyperlinkedRelatedField(
many=True,
source='get_blogposts_manager',
view_name='dummy-url',
)
field.context = {'request': request}
class ClassWithManagerMethod(object):
def get_blogposts_manager(self):
return BlogPost.objects
obj = ClassWithManagerMethod()
value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, ['http://testserver/dummyurl/1/'])
def test_related_queryset_source(self):
"""
Relational fields should be able to use queryset-returning methods as their source.
"""
BlogPost.objects.create(title='blah')
field = serializers.HyperlinkedRelatedField(
many=True,
source='get_blogposts_queryset',
view_name='dummy-url',
)
field.context = {'request': request}
class ClassWithQuerysetMethod(object):
def get_blogposts_queryset(self):
return BlogPost.objects.all()
obj = ClassWithQuerysetMethod()
value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, ['http://testserver/dummyurl/1/'])
def test_dotted_source(self):
"""
Source argument should support dotted.source notation.
"""
BlogPost.objects.create(title='blah')
field = serializers.HyperlinkedRelatedField(
many=True,
source='a.b.c',
view_name='dummy-url',
)
field.context = {'request': request}
class ClassWithQuerysetMethod(object):
a = {
'b': {
'c': BlogPost.objects.all()
}
}
obj = ClassWithQuerysetMethod()
value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, ['http://testserver/dummyurl/1/'])
from __future__ import unicode_literals
from django.db import models
from django.test import TestCase
from rest_framework import serializers
from rest_framework.tests.models import ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
from rest_framework.tests.models import (
BlogPost, ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource,
)
from rest_framework.compat import six
......@@ -124,6 +128,7 @@ class PKManyToManyTests(TestCase):
# Ensure source 4 is added, and everything else is as expected
queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True)
self.assertFalse(serializer.fields['targets'].read_only)
expected = [
{'id': 1, 'name': 'source-1', 'targets': [1]},
{'id': 2, 'name': 'source-2', 'targets': [1, 2]},
......@@ -135,6 +140,7 @@ class PKManyToManyTests(TestCase):
def test_reverse_many_to_many_create(self):
data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]}
serializer = ManyToManyTargetSerializer(data=data)
self.assertFalse(serializer.fields['sources'].read_only)
self.assertTrue(serializer.is_valid())
obj = serializer.save()
self.assertEqual(serializer.data, data)
......@@ -421,3 +427,116 @@ class PKNullableOneToOneTests(TestCase):
{'id': 2, 'name': 'target-2', 'nullable_source': 1},
]
self.assertEqual(serializer.data, expected)
# The below models and tests ensure that serializer fields corresponding
# to a ManyToManyField field with a user-specified ``through`` model are
# set to read only
class ManyToManyThroughTarget(models.Model):
name = models.CharField(max_length=100)
class ManyToManyThrough(models.Model):
source = models.ForeignKey('ManyToManyThroughSource')
target = models.ForeignKey(ManyToManyThroughTarget)
class ManyToManyThroughSource(models.Model):
name = models.CharField(max_length=100)
targets = models.ManyToManyField(ManyToManyThroughTarget,
related_name='sources',
through='ManyToManyThrough')
class ManyToManyThroughTargetSerializer(serializers.ModelSerializer):
class Meta:
model = ManyToManyThroughTarget
fields = ('id', 'name', 'sources')
class ManyToManyThroughSourceSerializer(serializers.ModelSerializer):
class Meta:
model = ManyToManyThroughSource
fields = ('id', 'name', 'targets')
class PKManyToManyThroughTests(TestCase):
def setUp(self):
self.source = ManyToManyThroughSource.objects.create(
name='through-source-1')
self.target = ManyToManyThroughTarget.objects.create(
name='through-target-1')
def test_many_to_many_create(self):
data = {'id': 2, 'name': 'source-2', 'targets': [self.target.pk]}
serializer = ManyToManyThroughSourceSerializer(data=data)
self.assertTrue(serializer.fields['targets'].read_only)
self.assertTrue(serializer.is_valid())
obj = serializer.save()
self.assertEqual(obj.name, 'source-2')
self.assertEqual(obj.targets.count(), 0)
def test_many_to_many_reverse_create(self):
data = {'id': 2, 'name': 'target-2', 'sources': [self.source.pk]}
serializer = ManyToManyThroughTargetSerializer(data=data)
self.assertTrue(serializer.fields['sources'].read_only)
self.assertTrue(serializer.is_valid())
serializer.save()
obj = serializer.save()
self.assertEqual(obj.name, 'target-2')
self.assertEqual(obj.sources.count(), 0)
# Regression tests for #694 (`source` attribute on related fields)
class PrimaryKeyRelatedFieldSourceTests(TestCase):
def test_related_manager_source(self):
"""
Relational fields should be able to use manager-returning methods as their source.
"""
BlogPost.objects.create(title='blah')
field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_manager')
class ClassWithManagerMethod(object):
def get_blogposts_manager(self):
return BlogPost.objects
obj = ClassWithManagerMethod()
value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, [1])
def test_related_queryset_source(self):
"""
Relational fields should be able to use queryset-returning methods as their source.
"""
BlogPost.objects.create(title='blah')
field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_queryset')
class ClassWithQuerysetMethod(object):
def get_blogposts_queryset(self):
return BlogPost.objects.all()
obj = ClassWithQuerysetMethod()
value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, [1])
def test_dotted_source(self):
"""
Source argument should support dotted.source notation.
"""
BlogPost.objects.create(title='blah')
field = serializers.PrimaryKeyRelatedField(many=True, source='a.b.c')
class ClassWithQuerysetMethod(object):
a = {
'b': {
'c': BlogPost.objects.all()
}
}
obj = ClassWithQuerysetMethod()
value = field.field_to_native(obj, 'field_name')
self.assertEqual(value, [1])
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from decimal import Decimal
from django.core.cache import cache
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils import unittest
from django.utils.translation import ugettext_lazy as _
from rest_framework import status, permissions
from rest_framework.compat import yaml, etree, patterns, url, include
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
XMLRenderer, JSONPRenderer, BrowsableAPIRenderer
XMLRenderer, JSONPRenderer, BrowsableAPIRenderer, UnicodeJSONRenderer
from rest_framework.parsers import YAMLParser, XMLParser
from rest_framework.settings import api_settings
from rest_framework.compat import StringIO
......@@ -26,7 +30,7 @@ RENDERER_B_SERIALIZER = lambda x: ('Renderer B: %s' % x).encode('ascii')
expected_results = [
((elem for elem in [1, 2, 3]), JSONRenderer, '[1, 2, 3]') # Generator
((elem for elem in [1, 2, 3]), JSONRenderer, b'[1, 2, 3]') # Generator
]
......@@ -129,12 +133,12 @@ class RendererEndToEndTests(TestCase):
End-to-end testing of renderers using an RendererMixin on a generic view.
"""
urls = 'rest_framework.tests.renderers'
urls = 'rest_framework.tests.test_renderers'
def test_default_renderer_serializes_content(self):
"""If the Accept header is not set the default renderer should serialize the response."""
resp = self.client.get('/')
self.assertEqual(resp['Content-Type'], RendererA.media_type)
self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8')
self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
......@@ -142,13 +146,13 @@ class RendererEndToEndTests(TestCase):
"""No response must be included in HEAD requests."""
resp = self.client.head('/')
self.assertEqual(resp.status_code, DUMMYSTATUS)
self.assertEqual(resp['Content-Type'], RendererA.media_type)
self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8')
self.assertEqual(resp.content, six.b(''))
def test_default_renderer_serializes_content_on_accept_any(self):
"""If the Accept header is set to */* the default renderer should serialize the response."""
resp = self.client.get('/', HTTP_ACCEPT='*/*')
self.assertEqual(resp['Content-Type'], RendererA.media_type)
self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8')
self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
......@@ -156,7 +160,7 @@ class RendererEndToEndTests(TestCase):
"""If the Accept header is set the specified renderer should serialize the response.
(In this case we check that works for the default renderer)"""
resp = self.client.get('/', HTTP_ACCEPT=RendererA.media_type)
self.assertEqual(resp['Content-Type'], RendererA.media_type)
self.assertEqual(resp['Content-Type'], RendererA.media_type + '; charset=utf-8')
self.assertEqual(resp.content, RENDERER_A_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
......@@ -164,7 +168,7 @@ class RendererEndToEndTests(TestCase):
"""If the Accept header is set the specified renderer should serialize the response.
(In this case we check that works for a non-default renderer)"""
resp = self.client.get('/', HTTP_ACCEPT=RendererB.media_type)
self.assertEqual(resp['Content-Type'], RendererB.media_type)
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
......@@ -175,7 +179,7 @@ class RendererEndToEndTests(TestCase):
RendererB.media_type
)
resp = self.client.get('/' + param)
self.assertEqual(resp['Content-Type'], RendererB.media_type)
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
......@@ -192,7 +196,7 @@ class RendererEndToEndTests(TestCase):
RendererB.format
)
resp = self.client.get('/' + param)
self.assertEqual(resp['Content-Type'], RendererB.media_type)
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
......@@ -200,7 +204,7 @@ class RendererEndToEndTests(TestCase):
"""If a 'format' keyword arg is specified, the renderer with the matching
format attribute should serialize the response."""
resp = self.client.get('/something.formatb')
self.assertEqual(resp['Content-Type'], RendererB.media_type)
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
......@@ -213,7 +217,7 @@ class RendererEndToEndTests(TestCase):
)
resp = self.client.get('/' + param,
HTTP_ACCEPT=RendererB.media_type)
self.assertEqual(resp['Content-Type'], RendererB.media_type)
self.assertEqual(resp['Content-Type'], RendererB.media_type + '; charset=utf-8')
self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT))
self.assertEqual(resp.status_code, DUMMYSTATUS)
......@@ -235,6 +239,13 @@ class JSONRendererTests(TestCase):
Tests specific to the JSON Renderer
"""
def test_render_lazy_strings(self):
"""
JSONRenderer should deal with lazy translated strings.
"""
ret = JSONRenderer().render(_('test'))
self.assertEqual(ret, b'"test"')
def test_without_content_type_args(self):
"""
Test basic JSON rendering.
......@@ -243,7 +254,7 @@ class JSONRendererTests(TestCase):
renderer = JSONRenderer()
content = renderer.render(obj, 'application/json')
# Fix failing test case which depends on version of JSON library.
self.assertEqual(content, _flat_repr)
self.assertEqual(content.decode('utf-8'), _flat_repr)
def test_with_content_type_args(self):
"""
......@@ -252,7 +263,24 @@ class JSONRendererTests(TestCase):
obj = {'foo': ['bar', 'baz']}
renderer = JSONRenderer()
content = renderer.render(obj, 'application/json; indent=2')
self.assertEqual(strip_trailing_whitespace(content), _indented_repr)
self.assertEqual(strip_trailing_whitespace(content.decode('utf-8')), _indented_repr)
def test_check_ascii(self):
obj = {'countries': ['United Kingdom', 'France', 'España']}
renderer = JSONRenderer()
content = renderer.render(obj, 'application/json')
self.assertEqual(content, '{"countries": ["United Kingdom", "France", "Espa\\u00f1a"]}'.encode('utf-8'))
class UnicodeJSONRendererTests(TestCase):
"""
Tests specific for the Unicode JSON Renderer
"""
def test_proper_encoding(self):
obj = {'countries': ['United Kingdom', 'France', 'España']}
renderer = UnicodeJSONRenderer()
content = renderer.render(obj, 'application/json')
self.assertEqual(content, '{"countries": ["United Kingdom", "France", "España"]}'.encode('utf-8'))
class JSONPRendererTests(TestCase):
......@@ -260,7 +288,7 @@ class JSONPRendererTests(TestCase):
Tests specific to the JSONP Renderer
"""
urls = 'rest_framework.tests.renderers'
urls = 'rest_framework.tests.test_renderers'
def test_without_callback_with_json_renderer(self):
"""
......@@ -269,7 +297,7 @@ class JSONPRendererTests(TestCase):
resp = self.client.get('/jsonp/jsonrenderer',
HTTP_ACCEPT='application/javascript')
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(resp['Content-Type'], 'application/javascript')
self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
self.assertEqual(resp.content,
('callback(%s);' % _flat_repr).encode('ascii'))
......@@ -280,7 +308,7 @@ class JSONPRendererTests(TestCase):
resp = self.client.get('/jsonp/nojsonrenderer',
HTTP_ACCEPT='application/javascript')
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(resp['Content-Type'], 'application/javascript')
self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
self.assertEqual(resp.content,
('callback(%s);' % _flat_repr).encode('ascii'))
......@@ -292,7 +320,7 @@ class JSONPRendererTests(TestCase):
resp = self.client.get('/jsonp/nojsonrenderer?callback=' + callback_func,
HTTP_ACCEPT='application/javascript')
self.assertEqual(resp.status_code, status.HTTP_200_OK)
self.assertEqual(resp['Content-Type'], 'application/javascript')
self.assertEqual(resp['Content-Type'], 'application/javascript; charset=utf-8')
self.assertEqual(resp.content,
('%s(%s);' % (callback_func, _flat_repr)).encode('ascii'))
......@@ -433,7 +461,7 @@ class CacheRenderTest(TestCase):
Tests specific to caching responses
"""
urls = 'rest_framework.tests.renderers'
urls = 'rest_framework.tests.test_renderers'
cache_key = 'just_a_cache_key'
......
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