diff options
| -rw-r--r-- | README.md | 7 | ||||
| -rw-r--r-- | docs/api-guide/fields.md | 7 | ||||
| -rw-r--r-- | docs/api-guide/filtering.md | 5 | ||||
| -rw-r--r-- | docs/api-guide/renderers.md | 86 | ||||
| -rw-r--r-- | docs/api-guide/responses.md | 3 | ||||
| -rw-r--r-- | docs/css/default.css | 4 | ||||
| -rw-r--r-- | docs/index.md | 9 | ||||
| -rw-r--r-- | docs/topics/credits.md | 8 | ||||
| -rw-r--r-- | docs/topics/release-notes.md | 13 | ||||
| -rw-r--r-- | docs/topics/rest-hypermedia-hateoas.md | 4 | ||||
| -rw-r--r-- | docs/tutorial/6-viewsets-and-routers.md | 4 | ||||
| -rw-r--r-- | docs/tutorial/quickstart.md | 44 | ||||
| -rw-r--r-- | rest_framework/fields.py | 31 | ||||
| -rw-r--r-- | rest_framework/mixins.py | 20 | ||||
| -rw-r--r-- | rest_framework/renderers.py | 40 | ||||
| -rw-r--r-- | rest_framework/response.py | 22 | ||||
| -rw-r--r-- | rest_framework/serializers.py | 6 | ||||
| -rw-r--r-- | rest_framework/static/rest_framework/css/bootstrap-tweaks.css | 4 | ||||
| -rw-r--r-- | rest_framework/templates/rest_framework/form.html | 2 | ||||
| -rw-r--r-- | rest_framework/tests/fields.py | 15 | ||||
| -rw-r--r-- | rest_framework/tests/models.py | 10 | ||||
| -rw-r--r-- | rest_framework/tests/renderers.py | 36 | ||||
| -rw-r--r-- | rest_framework/tests/response.py | 88 | ||||
| -rw-r--r-- | rest_framework/tests/serializer.py | 44 |
24 files changed, 415 insertions, 97 deletions
@@ -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 diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index e117c370..b3208eb1 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -56,6 +56,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. --- diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index a710ad7d..dd4cf8d7 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): @@ -175,6 +175,7 @@ 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. --- @@ -216,7 +217,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 1661ceec..0161b54d 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -67,17 +67,45 @@ If your API includes views that can serve both regular webpages and API response ## JSONRenderer -Renders the request data into `JSON` enforcing ASCII encoding +Renders the request data into `JSON`, using utf-8 encoding. + +Note that non-ascii charaters 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 -Same as `JSONRenderer` but doesn't enforce ASCII encoding +Renders the request data into `JSON`, using utf-8 encoding. + +Note that non-ascii charaters 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 @@ -91,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`. @@ -101,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. @@ -113,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. @@ -147,6 +181,8 @@ If you're building websites that use `TemplateHTMLRenderer` along with other ren **.format**: `'.html'` +**.charset**: `utf-8` + See also: `StaticHTMLRenderer` ## StaticHTMLRenderer @@ -167,6 +203,8 @@ You can use `TemplateHTMLRenderer` either to return regular HTML pages using RES **.format**: `'.html'` +**.charset**: `utf-8` + See also: `TemplateHTMLRenderer` ## BrowsableAPIRenderer @@ -177,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` @@ -209,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): - 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) + +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): + return data --- @@ -256,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 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. @@ -297,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 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/css/default.css b/docs/css/default.css index 998efa27..5c6c72ce 100644 --- a/docs/css/default.css +++ b/docs/css/default.css @@ -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) { diff --git a/docs/index.md b/docs/index.md index 7c38efd3..222bb739 100644 --- a/docs/index.md +++ b/docs/index.md @@ -206,7 +206,13 @@ 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/ diff --git a/docs/topics/credits.md b/docs/topics/credits.md index acd6576e..4b39d452 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -132,6 +132,10 @@ The following people have helped make REST framework great. * Andy McKay - [andymckay] * Matteo Suppo - [matteosuppo] * Karol Majta - [lolek09] +* David Jones - [commonorgarden] +* Andrew Tarzwell - [atarzwell] +* Michal Dvořák - [mikee2185] +* Markus Törnqvist - [mjtorn] Many thanks to everyone who's contributed to the project. @@ -300,3 +304,7 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [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 diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 560dd305..ff011546 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -42,9 +42,20 @@ You can determine your currently installed version using `pip freeze`: ### Master +* Serializer fields now support `label` and `help_text`. +* Added `UnicodeJSONRenderer`. +* 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. -### 2.3.2 +**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 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/fields.py b/rest_framework/fields.py index d5a1394d..cb5f9a40 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -123,7 +123,7 @@ class Field(object): use_files = False form_field_class = forms.CharField - def __init__(self, source=None): + def __init__(self, source=None, label=None, help_text=None): self.parent = None self.creation_counter = Field.creation_counter @@ -131,6 +131,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. @@ -227,7 +233,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): @@ -238,7 +245,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: @@ -416,11 +423,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/mixins.py b/rest_framework/mixins.py index f3cd5868..f11def6d 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -110,16 +110,6 @@ class UpdateModelMixin(object): """ Update a model instance. """ - 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 update(self, request, *args, **kwargs): partial = kwargs.pop('partial', False) self.object = self.get_object_or_none() @@ -148,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. diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index c67c8ed6..c42b086f 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,7 +35,7 @@ class BaseRenderer(object): media_type = None format = None - charset = None + charset = 'utf-8' def render(self, data, accepted_media_type=None, renderer_context=None): raise NotImplemented('Renderer class requires .render() to be implemented') @@ -44,17 +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 '' @@ -74,12 +77,25 @@ class JSONRenderer(BaseRenderer): except (ValueError, TypeError): indent = None - return json.dumps(data, cls=self.encoder_class, indent=indent, ensure_ascii=self.ensure_ascii) + ret = json.dumps(data, cls=self.encoder_class, + indent=indent, ensure_ascii=self.ensure_ascii) + + # On python 2.x json.dumps() returns bytestrings if ensure_ascii=True, + # but if ensure_ascii=False, the return type is underspecified, + # 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): @@ -112,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): @@ -133,7 +149,7 @@ class XMLRenderer(BaseRenderer): stream = StringIO() - xml = SimplerXMLGenerator(stream, "utf-8") + xml = SimplerXMLGenerator(stream, self.charset) xml.startDocument() xml.startElement("root", {}) @@ -183,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): @@ -332,7 +348,7 @@ 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): + if renderer.charset is None: return '[%d bytes of binary content]' % len(content) return content @@ -380,7 +396,11 @@ 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) diff --git a/rest_framework/response.py b/rest_framework/response.py index 32e74a45..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, charset=None): + exception=False, content_type=None): """ Alters the init arguments slightly. For example, drop 'template_name', and instead use 'data'. @@ -30,7 +30,7 @@ class Response(SimpleTemplateResponse): self.data = data self.template_name = template_name self.exception = exception - self.charset = charset + self.content_type = content_type if headers: for name, value in six.iteritems(headers): @@ -47,15 +47,21 @@ class Response(SimpleTemplateResponse): assert context, ".renderer_context not set on Response" context['response'] = self - if self.charset is None: - self.charset = renderer.charset + charset = renderer.charset + content_type = self.content_type - if self.charset is not None: - content_type = "{0}; charset={1}".format(media_type, self.charset) - else: + 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 - return renderer.render(self.data, media_type, context) + + 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 072815df..17da8c25 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -746,6 +746,12 @@ class ModelSerializer(Serializer): 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 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 fd1fe961..22c515a9 100644 --- a/rest_framework/tests/fields.py +++ b/rest_framework/tests/fields.py @@ -778,6 +778,21 @@ class SlugFieldTests(TestCase): 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/models.py b/rest_framework/tests/models.py index 40e41a64..abf50a2d 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -1,5 +1,7 @@ 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): @@ -159,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 + diff --git a/rest_framework/tests/renderers.py b/rest_framework/tests/renderers.py index 739f9184..9096c82d 100644 --- a/rest_framework/tests/renderers.py +++ b/rest_framework/tests/renderers.py @@ -1,4 +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 @@ -27,7 +29,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 ] @@ -135,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) @@ -143,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) @@ -157,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) @@ -165,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) @@ -176,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) @@ -193,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) @@ -201,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) @@ -214,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) @@ -244,7 +246,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): """ @@ -253,13 +255,13 @@ 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"]}') + self.assertEqual(content, '{"countries": ["United Kingdom", "France", "Espa\\u00f1a"]}'.encode('utf-8')) class UnicodeJSONRendererTests(TestCase): @@ -270,7 +272,7 @@ class UnicodeJSONRendererTests(TestCase): obj = {'countries': ['United Kingdom', 'France', 'España']} renderer = UnicodeJSONRenderer() content = renderer.render(obj, 'application/json') - self.assertEqual(content, '{"countries": ["United Kingdom", "France", "España"]}') + self.assertEqual(content, '{"countries": ["United Kingdom", "France", "España"]}'.encode('utf-8')) class JSONPRendererTests(TestCase): @@ -287,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=utf-8') self.assertEqual(resp.content, ('callback(%s);' % _flat_repr).encode('ascii')) @@ -298,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=utf-8') self.assertEqual(resp.content, ('callback(%s);' % _flat_repr).encode('ascii')) @@ -310,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=utf-8') 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 8f1163e8..4e04ac5c 100644 --- a/rest_framework/tests/response.py +++ b/rest_framework/tests/response.py @@ -1,14 +1,18 @@ from __future__ import unicode_literals from django.test import TestCase +from rest_framework.tests.models import BasicModel, BasicModelSerializer from rest_framework.compat import patterns, url, include from rest_framework.response import Response from rest_framework.views import APIView +from rest_framework import generics +from rest_framework import routers from rest_framework import status from rest_framework.renderers import ( BaseRenderer, JSONRenderer, BrowsableAPIRenderer ) +from rest_framework import viewsets from rest_framework.settings import api_settings from rest_framework.compat import six @@ -60,11 +64,11 @@ class MockView(APIView): return Response(DUMMYCONTENT, status=DUMMYSTATUS) -class MockViewSettingCharset(APIView): +class MockViewSettingContentType(APIView): renderer_classes = (RendererA, RendererB, RendererC) def get(self, request, **kwargs): - return Response(DUMMYCONTENT, status=DUMMYSTATUS, charset='setbyview') + return Response(DUMMYCONTENT, status=DUMMYSTATUS, content_type='setbyview') class HTMLView(APIView): @@ -80,12 +84,30 @@ class HTMLView1(APIView): def get(self, request, **kwargs): return Response('text') + +class HTMLNewModelViewSet(viewsets.ModelViewSet): + model = BasicModel + + +class HTMLNewModelView(generics.ListCreateAPIView): + renderer_classes = (BrowsableAPIRenderer,) + permission_classes = [] + serializer_class = BasicModelSerializer + model = BasicModel + + +new_model_viewset_router = routers.DefaultRouter() +new_model_viewset_router.register(r'', HTMLNewModelViewSet) + + urlpatterns = patterns('', - url(r'^setbyview$', MockViewSettingCharset.as_view(renderer_classes=[RendererA, RendererB, RendererC])), + 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()), + url(r'^html_new_model_viewset', include(new_model_viewset_router.urls)), url(r'^restframework', include('rest_framework.urls', namespace='rest_framework')) ) @@ -101,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) @@ -109,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) @@ -123,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) @@ -131,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) @@ -142,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) @@ -150,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) @@ -158,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) @@ -167,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) @@ -191,7 +213,21 @@ class Issue122Tests(TestCase): self.client.get('/html1') -class Issue807Testts(TestCase): +class Issue467Tests(TestCase): + """ + Tests for #467 + """ + + 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 """ @@ -204,7 +240,8 @@ class Issue807Testts(TestCase): """ headers = {"HTTP_ACCEPT": RendererA.media_type} resp = self.client.get('/', **headers) - self.assertEqual(RendererA.media_type, resp['Content-Type']) + 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): """ @@ -216,11 +253,26 @@ class Issue807Testts(TestCase): expected = "{0}; charset={1}".format(RendererC.media_type, RendererC.charset) self.assertEqual(expected, resp['Content-Type']) - def test_charset_set_explictly_on_response(self): + def test_content_type_set_explictly_on_response(self): """ - The charset may be set explictly on the response. + The content type may be set explictly on the response. """ headers = {"HTTP_ACCEPT": RendererC.media_type} resp = self.client.get('/setbyview', **headers) - expected = "{0}; charset={1}".format(RendererC.media_type, 'setbyview') - self.assertEqual(expected, resp['Content-Type']) + 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; 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; 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 fd6cf6da..f2c31872 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -4,10 +4,11 @@ from django.db.models.fields import BLANK_CHOICE_DASH from django.test import TestCase from django.utils.datastructures import MultiValueDict from django.utils.translation import ugettext_lazy as _ -from rest_framework import serializers +from rest_framework import serializers, fields, relations from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel, BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel, ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel) +from rest_framework.tests.models import BasicModelSerializer import datetime import pickle @@ -128,11 +129,6 @@ class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer): fields = ['some_integer'] -class BrokenModelSerializer(serializers.ModelSerializer): - class Meta: - fields = ['some_field'] - - class BasicTests(TestCase): def setUp(self): self.comment = Comment( @@ -423,8 +419,12 @@ class ValidationTests(TestCase): Assert that a meaningful exception message is outputted when the model field is missing (e.g. when mistyping ``model``). """ + class BrokenModelSerializer(serializers.ModelSerializer): + class Meta: + fields = ['some_field'] + try: - serializer = BrokenModelSerializer() + BrokenModelSerializer() except AssertionError as e: self.assertEqual(e.args[0], "Serializer class 'BrokenModelSerializer' is missing 'model' Meta option") except: @@ -446,7 +446,7 @@ class CustomValidationTests(TestCase): class CommentSerializerWithFieldValidator(CommentSerializer): def validate_email(self, attrs, source): - value = attrs[source] + attrs[source] return attrs def validate_content(self, attrs, source): @@ -1324,8 +1324,7 @@ class DeserializeListTestCase(TestCase): self.assertEqual(serializer.errors, expected) -# test for issue 747 - +# Test for issue 747 class LazyStringModel(object): def __init__(self, lazystring): @@ -1352,6 +1351,31 @@ class LazyStringsTestCase(TestCase): type('lazystring')) +# Test for issue #467 + +class FieldLabelTest(TestCase): + def setUp(self): + self.serializer_class = BasicModelSerializer + + def test_label_from_model(self): + """ + Validates that label and help_text are correctly copied from the model class. + """ + serializer = self.serializer_class() + text_field = serializer.fields['text'] + + self.assertEqual('Text comes here', text_field.label) + self.assertEqual('Text description.', text_field.help_text) + + def test_field_ctor(self): + """ + This is check that ctor supports both label and help_text. + """ + self.assertEqual('Label', fields.Field(label='Label', help_text='Help').label) + self.assertEqual('Help', fields.CharField(label='Label', help_text='Help').help_text) + self.assertEqual('Label', relations.HyperlinkedRelatedField(view_name='fake', label='Label', help_text='Help', many=True).label) + + class AttributeMappingOnAutogeneratedFieldsTests(TestCase): def setUp(self): |
