aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/api-guide/filtering.md4
-rw-r--r--docs/api-guide/renderers.md71
-rw-r--r--docs/api-guide/responses.md3
-rw-r--r--docs/topics/credits.md6
-rw-r--r--docs/topics/rest-hypermedia-hateoas.md4
-rw-r--r--docs/tutorial/6-viewsets-and-routers.md4
-rw-r--r--docs/tutorial/quickstart.md44
-rw-r--r--rest_framework/compat.py13
-rw-r--r--rest_framework/fields.py26
-rw-r--r--rest_framework/renderers.py27
-rw-r--r--rest_framework/response.py20
-rw-r--r--rest_framework/serializers.py15
-rw-r--r--rest_framework/static/rest_framework/css/bootstrap-tweaks.css4
-rw-r--r--rest_framework/templates/rest_framework/form.html2
-rw-r--r--rest_framework/tests/fields.py15
-rw-r--r--rest_framework/tests/htmlrenderer.py10
-rw-r--r--rest_framework/tests/negotiation.py9
-rw-r--r--rest_framework/tests/renderers.py46
-rw-r--r--rest_framework/tests/response.py88
-rw-r--r--rest_framework/tests/serializer.py105
20 files changed, 444 insertions, 72 deletions
diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md
index a710ad7d..8226aa42 100644
--- a/docs/api-guide/filtering.md
+++ b/docs/api-guide/filtering.md
@@ -87,7 +87,7 @@ The default filter backends may be set globally, using the `DEFAULT_FILTER_BACKE
'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',)
}
-You can also set the authentication policy on a per-view, or per-viewset basis,
+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):
@@ -216,7 +216,7 @@ 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 `'order'` to the required field name. For example:
+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
diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md
index b9a9fd7a..63641d23 100644
--- a/docs/api-guide/renderers.md
+++ b/docs/api-guide/renderers.md
@@ -67,7 +67,7 @@ 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 ASCII encoding.
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`.
@@ -75,6 +75,20 @@ The client may additionally include an `'indent'` media type parameter, in which
**.format**: `'.json'`
+**.charset**: `iso-8859-1`
+
+## UnicodeJSONRenderer
+
+Renders the request data into `JSON`, using utf-8 encoding.
+
+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`.
+
+**.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 +101,8 @@ The javascript callback function must be set by the client including a `callback
**.format**: `'.jsonp'`
+**.charset**: `iso-8859-1`
+
## YAMLRenderer
Renders the request data into `YAML`.
@@ -97,6 +113,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 +127,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 +163,8 @@ If you're building websites that use `TemplateHTMLRenderer` along with other ren
**.format**: `'.html'`
+**.charset**: `utf-8`
+
See also: `StaticHTMLRenderer`
## StaticHTMLRenderer
@@ -163,6 +185,8 @@ You can use `TemplateHTMLRenderer` either to return regular HTML pages using RES
**.format**: `'.html'`
+**.charset**: `utf-8`
+
See also: `TemplateHTMLRenderer`
## BrowsableAPIRenderer
@@ -173,12 +197,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 +233,34 @@ 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):
- if isinstance(data, basestring):
- return data
- return smart_unicode(data)
+ 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)
+
+If the renderer returns a raw bytestring, 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):
+ return data
---
@@ -252,6 +300,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 explictly 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.
@@ -282,11 +339,11 @@ Templates will render with a `RequestContext` which includes the `status_code` a
The following third party packages are also available.
-## MessagePack
+### MessagePack
[MessagePack][messagepack] is a fast, efficient binary serialization format. [Juan Riaza][juanriaza] maintains the [djangorestframework-msgpack][djangorestframework-msgpack] package which provides MessagePack renderer and parser support for REST framework.
-## CSV
+### CSV
Comma-separated values are a plain-text tabular data format, that can be easily imported into spreadsheet applications. [Mjumbe Poe][mjumbewu] maintains the [djangorestframework-csv][djangorestframework-csv] package which provides CSV renderer support for REST framework.
diff --git a/docs/api-guide/responses.md b/docs/api-guide/responses.md
index 794f9377..59632ad5 100644
--- a/docs/api-guide/responses.md
+++ b/docs/api-guide/responses.md
@@ -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.
---
diff --git a/docs/topics/credits.md b/docs/topics/credits.md
index d805c0c1..7f8883a5 100644
--- a/docs/topics/credits.md
+++ b/docs/topics/credits.md
@@ -130,6 +130,9 @@ The following people have helped make REST framework great.
* Òscar Vilaplana - [grimborg]
* Ryan Kaskel - [ryankask]
* Andy McKay - [andymckay]
+* Matteo Suppo - [matteosuppo]
+* Karol Majta - [lolek09]
+* David Jones - [commonorgarden]
Many thanks to everyone who's contributed to the project.
@@ -296,3 +299,6 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[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
diff --git a/docs/topics/rest-hypermedia-hateoas.md b/docs/topics/rest-hypermedia-hateoas.md
index 43e5a8c6..aeba579f 100644
--- a/docs/topics/rest-hypermedia-hateoas.md
+++ b/docs/topics/rest-hypermedia-hateoas.md
@@ -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
diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md
index 277804e2..4ed10e82 100644
--- a/docs/tutorial/6-viewsets-and-routers.md
+++ b/docs/tutorial/6-viewsets-and-routers.md
@@ -1,4 +1,4 @@
-# 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',
diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md
index 52fe3acf..c41cb63f 100644
--- a/docs/tutorial/quickstart.md
+++ b/docs/tutorial/quickstart.md
@@ -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 dependancies 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 manange.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
@@ -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/
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index cd39f544..76dc0052 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -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__')
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 46fb3152..d772c400 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -26,7 +26,7 @@ from rest_framework import ISO_8601
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
@@ -45,7 +45,6 @@ def is_simple_callable(obj):
len_defaults = len(defaults) if defaults else 0
return len_args <= len_defaults
-
def get_component(obj, attr_name):
"""
Given an object, and an attribute name,
@@ -175,7 +174,8 @@ 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):
# Make sure we preserve field ordering, if it exists
@@ -183,7 +183,7 @@ class Field(object):
for key, val in value.items():
ret[key] = self.to_native(val)
return ret
- return smart_text(value)
+ return force_text(value)
def attributes(self):
"""
@@ -396,11 +396,25 @@ class URLField(CharField):
class SlugField(CharField):
type_name = 'SlugField'
-
+ 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):
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'
form_field_class = forms.ChoiceField
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 2b56d6e4..b4fa55bd 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -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')
@@ -49,6 +49,8 @@ class JSONRenderer(BaseRenderer):
media_type = 'application/json'
format = 'json'
encoder_class = encoders.JSONEncoder
+ ensure_ascii = True
+ charset = 'iso-8859-1'
def render(self, data, accepted_media_type=None, renderer_context=None):
"""
@@ -72,7 +74,17 @@ 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)
+
+ if not self.ensure_ascii:
+ return bytes(ret.encode(self.charset))
+ return ret
+
+
+class UnicodeJSONRenderer(JSONRenderer):
+ ensure_ascii = False
+ charset = 'utf-8'
class JSONPRenderer(JSONRenderer):
@@ -115,6 +127,7 @@ class XMLRenderer(BaseRenderer):
media_type = 'application/xml'
format = 'xml'
+ charset = 'utf-8'
def render(self, data, accepted_media_type=None, renderer_context=None):
"""
@@ -164,6 +177,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):
"""
@@ -204,6 +218,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 +290,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 +312,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 +337,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
@@ -337,6 +354,8 @@ class BrowsableAPIRenderer(BaseRenderer):
try:
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
diff --git a/rest_framework/response.py b/rest_framework/response.py
index 26e4ab37..110ccb13 100644
--- a/rest_framework/response.py
+++ b/rest_framework/response.py
@@ -18,7 +18,7 @@ class Response(SimpleTemplateResponse):
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):
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 425dd18c..31f261e1 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -739,17 +739,22 @@ class ModelSerializer(Serializer):
if issubclass(model_field.__class__, models.TextField):
kwargs['widget'] = widgets.Textarea
- # TODO: TypedChoiceField?
- if model_field.flatchoices: # This ModelField contains choices
- kwargs['choices'] = model_field.flatchoices
- return ChoiceField(**kwargs)
-
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'],
diff --git a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
index 9b520156..6bfb778c 100644
--- a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
+++ b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css
@@ -104,6 +104,10 @@ html, body {
margin-bottom: 0;
}
+.well form .help-block {
+ color: #999;
+}
+
.nav-tabs {
border: 0;
}
diff --git a/rest_framework/templates/rest_framework/form.html b/rest_framework/templates/rest_framework/form.html
index dc7acc70..b27f652e 100644
--- a/rest_framework/templates/rest_framework/form.html
+++ b/rest_framework/templates/rest_framework/form.html
@@ -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>
diff --git a/rest_framework/tests/fields.py b/rest_framework/tests/fields.py
index dad69975..a3104206 100644
--- a/rest_framework/tests/fields.py
+++ b/rest_framework/tests/fields.py
@@ -769,6 +769,21 @@ class SlugFieldTests(TestCase):
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 20)
+ def test_invalid_slug(self):
+ """
+ Make sure an invalid slug raises ValidationError
+ """
+ class SlugFieldSerializer(serializers.ModelSerializer):
+ slug_field = serializers.SlugField(source='slug_field', max_length=20, required=True)
+
+ class Meta:
+ model = self.SlugFieldModel
+
+ s = SlugFieldSerializer(data={'slug_field': 'a b'})
+
+ self.assertEqual(s.is_valid(), False)
+ self.assertEqual(s.errors, {'slug_field': ["Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens."]})
+
class URLFieldTests(TestCase):
"""
diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py
index 8f2e2b5a..5d18a6e8 100644
--- a/rest_framework/tests/htmlrenderer.py
+++ b/rest_framework/tests/htmlrenderer.py
@@ -66,19 +66,19 @@ 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):
@@ -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')
diff --git a/rest_framework/tests/negotiation.py b/rest_framework/tests/negotiation.py
index 43721b84..7f84827f 100644
--- a/rest_framework/tests/negotiation.py
+++ b/rest_framework/tests/negotiation.py
@@ -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()]
diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py
index 40bac9cb..1b2b9279 100644
--- a/rest_framework/tests/renderers.py
+++ b/rest_framework/tests/renderers.py
@@ -1,3 +1,6 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
from decimal import Decimal
from django.core.cache import cache
from django.test import TestCase
@@ -8,7 +11,7 @@ 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
@@ -134,7 +137,7 @@ class RendererEndToEndTests(TestCase):
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 +145,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 +159,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 +167,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 +178,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 +195,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 +203,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 +216,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)
@@ -254,6 +257,23 @@ class JSONRendererTests(TestCase):
content = renderer.render(obj, 'application/json; indent=2')
self.assertEqual(strip_trailing_whitespace(content), _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"]}')
+
+
+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):
"""
@@ -269,7 +289,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=iso-8859-1')
self.assertEqual(resp.content,
('callback(%s);' % _flat_repr).encode('ascii'))
@@ -280,7 +300,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=iso-8859-1')
self.assertEqual(resp.content,
('callback(%s);' % _flat_repr).encode('ascii'))
@@ -292,7 +312,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=iso-8859-1')
self.assertEqual(resp.content,
('%s(%s);' % (callback_func, _flat_repr)).encode('ascii'))
diff --git a/rest_framework/tests/response.py b/rest_framework/tests/response.py
index d49aaa1f..4e04ac5c 100644
--- a/rest_framework/tests/response.py
+++ b/rest_framework/tests/response.py
@@ -25,6 +25,9 @@ class MockJsonRenderer(BaseRenderer):
media_type = 'application/json'
+class MockTextMediaRenderer(BaseRenderer):
+ media_type = 'text/html'
+
DUMMYSTATUS = status.HTTP_200_OK
DUMMYCONTENT = 'dummycontent'
@@ -48,13 +51,26 @@ class RendererB(BaseRenderer):
return RENDERER_B_SERIALIZER(data)
+class RendererC(RendererB):
+ media_type = 'mock/rendererc'
+ format = 'formatc'
+ charset = "rendererc"
+
+
class MockView(APIView):
- renderer_classes = (RendererA, RendererB)
+ renderer_classes = (RendererA, RendererB, RendererC)
def get(self, request, **kwargs):
return Response(DUMMYCONTENT, status=DUMMYSTATUS)
+class MockViewSettingContentType(APIView):
+ renderer_classes = (RendererA, RendererB, RendererC)
+
+ def get(self, request, **kwargs):
+ return Response(DUMMYCONTENT, status=DUMMYSTATUS, content_type='setbyview')
+
+
class HTMLView(APIView):
renderer_classes = (BrowsableAPIRenderer, )
@@ -83,9 +99,11 @@ class HTMLNewModelView(generics.ListCreateAPIView):
new_model_viewset_router = routers.DefaultRouter()
new_model_viewset_router.register(r'', HTMLNewModelViewSet)
+
urlpatterns = patterns('',
- url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
- url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
+ url(r'^setbyview$', MockViewSettingContentType.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
+ url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
+ url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^html$', HTMLView.as_view()),
url(r'^html1$', HTMLView1.as_view()),
url(r'^html_new_model$', HTMLNewModelView.as_view()),
@@ -105,7 +123,7 @@ class RendererIntegrationTests(TestCase):
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)
@@ -113,13 +131,13 @@ class RendererIntegrationTests(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)
@@ -127,7 +145,7 @@ class RendererIntegrationTests(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)
@@ -135,7 +153,7 @@ class RendererIntegrationTests(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)
@@ -146,7 +164,7 @@ class RendererIntegrationTests(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)
@@ -154,7 +172,7 @@ class RendererIntegrationTests(TestCase):
"""If a 'format' query is specified, the renderer with the matching
format attribute should serialize the response."""
resp = self.client.get('/?format=%s' % RendererB.format)
- 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)
@@ -162,7 +180,7 @@ class RendererIntegrationTests(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)
@@ -171,7 +189,7 @@ class RendererIntegrationTests(TestCase):
the renderer with the matching format attribute should serialize the response."""
resp = self.client.get('/?format=%s' % RendererB.format,
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)
@@ -202,19 +220,59 @@ class Issue467Tests(TestCase):
urls = 'rest_framework.tests.response'
+ def test_form_has_label_and_help_text(self):
+ resp = self.client.get('/html_new_model')
+ self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
+ self.assertContains(resp, 'Text comes here')
+ self.assertContains(resp, 'Text description.')
+
+
+class Issue807Tests(TestCase):
+ """
+ Covers #807
+ """
+
+ urls = 'rest_framework.tests.response'
+
+ def test_does_not_append_charset_by_default(self):
+ """
+ Renderers don't include a charset unless set explicitly.
+ """
+ headers = {"HTTP_ACCEPT": RendererA.media_type}
+ resp = self.client.get('/', **headers)
+ expected = "{0}; charset={1}".format(RendererA.media_type, 'utf-8')
+ self.assertEqual(expected, resp['Content-Type'])
+
+ def test_if_there_is_charset_specified_on_renderer_it_gets_appended(self):
+ """
+ If renderer class has charset attribute declared, it gets appended
+ to Response's Content-Type
+ """
+ headers = {"HTTP_ACCEPT": RendererC.media_type}
+ resp = self.client.get('/', **headers)
+ expected = "{0}; charset={1}".format(RendererC.media_type, RendererC.charset)
+ self.assertEqual(expected, resp['Content-Type'])
+
+ def test_content_type_set_explictly_on_response(self):
+ """
+ The content type may be set explictly on the response.
+ """
+ headers = {"HTTP_ACCEPT": RendererC.media_type}
+ resp = self.client.get('/setbyview', **headers)
+ self.assertEqual('setbyview', resp['Content-Type'])
+
def test_viewset_label_help_text(self):
param = '?%s=%s' % (
api_settings.URL_ACCEPT_OVERRIDE,
'text/html'
)
resp = self.client.get('/html_new_model_viewset/' + param)
- self.assertEqual(resp['Content-Type'], 'text/html')
+ self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
self.assertContains(resp, 'Text comes here')
self.assertContains(resp, 'Text description.')
def test_form_has_label_and_help_text(self):
resp = self.client.get('/html_new_model')
- self.assertEqual(resp['Content-Type'], 'text/html')
+ self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8')
self.assertContains(resp, 'Text comes here')
self.assertContains(resp, 'Text description.')
-
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index 8f901a85..1772ee37 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -1,8 +1,9 @@
from __future__ import unicode_literals
from django.db import models
from django.db.models.fields import BLANK_CHOICE_DASH
-from django.utils.datastructures import MultiValueDict
from django.test import TestCase
+from django.utils.datastructures import MultiValueDict
+from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers, fields, relations
from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel,
@@ -1324,7 +1325,35 @@ class DeserializeListTestCase(TestCase):
self.assertEqual(serializer.errors, expected)
+# Test for issue 747
+
+class LazyStringModel(object):
+ def __init__(self, lazystring):
+ self.lazystring = lazystring
+
+
+class LazyStringSerializer(serializers.Serializer):
+ lazystring = serializers.Field()
+
+ def restore_object(self, attrs, instance=None):
+ if instance is not None:
+ instance.lazystring = attrs.get('lazystring', instance.lazystring)
+ return instance
+ return LazyStringModel(**attrs)
+
+
+class LazyStringsTestCase(TestCase):
+ def setUp(self):
+ self.model = LazyStringModel(lazystring=_('lazystring'))
+
+ def test_lazy_strings_are_translated(self):
+ serializer = LazyStringSerializer(self.model)
+ self.assertEqual(type(serializer.data['lazystring']),
+ type('lazystring'))
+
+
# Test for issue #467
+
class FieldLabelTest(TestCase):
def setUp(self):
self.serializer_class = BasicModelSerializer
@@ -1345,7 +1374,7 @@ class FieldLabelTest(TestCase):
"""
self.assertEquals('Label', fields.Field(label='Label', help_text='Help').label)
self.assertEquals('Help', fields.CharField(label='Label', help_text='Help').help_text)
- self.assertEquals('Label', relations.ManyHyperlinkedRelatedField(view_name='fake', label='Label', help_text='Help').label)
+ self.assertEquals('Label', relations.HyperlinkedRelatedField(view_name='fake', label='Label', help_text='Help', many=True).label)
class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
@@ -1428,3 +1457,75 @@ class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
def test_url_field(self):
self.field_test('url_field')
+
+class DefaultValuesOnAutogeneratedFieldsTests(TestCase):
+
+ def setUp(self):
+ class DVOAFModel(RESTFrameworkModel):
+ positive_integer_field = models.PositiveIntegerField(blank=True)
+ positive_small_integer_field = models.PositiveSmallIntegerField(blank=True)
+ email_field = models.EmailField(blank=True)
+ file_field = models.FileField(blank=True)
+ image_field = models.ImageField(blank=True)
+ slug_field = models.SlugField(blank=True)
+ url_field = models.URLField(blank=True)
+
+ class DVOAFSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = DVOAFModel
+
+ self.serializer_class = DVOAFSerializer
+ self.fields_attributes = {
+ 'positive_integer_field': [
+ ('min_value', 0),
+ ],
+ 'positive_small_integer_field': [
+ ('min_value', 0),
+ ],
+ 'email_field': [
+ ('max_length', 75),
+ ],
+ 'file_field': [
+ ('max_length', 100),
+ ],
+ 'image_field': [
+ ('max_length', 100),
+ ],
+ 'slug_field': [
+ ('max_length', 50),
+ ],
+ 'url_field': [
+ ('max_length', 200),
+ ],
+ }
+
+ def field_test(self, field):
+ serializer = self.serializer_class(data={})
+ self.assertEqual(serializer.is_valid(), True)
+
+ for attribute in self.fields_attributes[field]:
+ self.assertEqual(
+ getattr(serializer.fields[field], attribute[0]),
+ attribute[1]
+ )
+
+ def test_positive_integer_field(self):
+ self.field_test('positive_integer_field')
+
+ def test_positive_small_integer_field(self):
+ self.field_test('positive_small_integer_field')
+
+ def test_email_field(self):
+ self.field_test('email_field')
+
+ def test_file_field(self):
+ self.field_test('file_field')
+
+ def test_image_field(self):
+ self.field_test('image_field')
+
+ def test_slug_field(self):
+ self.field_test('slug_field')
+
+ def test_url_field(self):
+ self.field_test('url_field')