Merge pull request #1 from nschlemm/issue-192-expose-fields-for-options

Merged work in progress for Issue 192 expose fields for options
This commit is contained in:
Òscar Vilaplana 2013-05-19 01:59:19 -07:00
commit 42b61ffcd7
24 changed files with 949 additions and 208 deletions

View File

@ -381,6 +381,15 @@ Note that reverse generic keys, expressed using the `GenericRelation` field, can
For more information see [the Django documentation on generic relations][generic-relations]. For more information see [the Django documentation on generic relations][generic-relations].
## ManyToManyFields with a Through Model
By default, relational fields that target a ``ManyToManyField`` with a
``through`` model specified are set to read-only.
If you exlicitly specify a relational field pointing to a
``ManyToManyField`` with a through model, be sure to set ``read_only``
to ``True``.
## Advanced Hyperlinked fields ## Advanced Hyperlinked fields
If you have very specific requirements for the style of your hyperlinked relationships you can override `HyperlinkedRelatedField`. If you have very specific requirements for the style of your hyperlinked relationships you can override `HyperlinkedRelatedField`.

View File

@ -67,7 +67,7 @@ If your API includes views that can serve both regular webpages and API response
## JSONRenderer ## JSONRenderer
Renders the request data into `JSON`. Renders the request data into `JSON` enforcing 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`. 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,10 @@ The client may additionally include an `'indent'` media type parameter, in which
**.format**: `'.json'` **.format**: `'.json'`
## UnicodeJSONRenderer
Same as `JSONRenderer` but doesn't enforce ASCII encoding
## JSONPRenderer ## 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. 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.
@ -272,10 +276,10 @@ Exceptions raised and handled by an HTML renderer will attempt to render using o
* Load and render a template named `api_exception.html`. * Load and render a template named `api_exception.html`.
* Render the HTTP status code and text, for example "404 Not Found". * Render the HTTP status code and text, for example "404 Not Found".
**Note**: If `DEBUG=True`, Django's standard traceback error page will be displayed instead of rendering the HTTP status code and text.
Templates will render with a `RequestContext` which includes the `status_code` and `details` keys. Templates will render with a `RequestContext` which includes the `status_code` and `details` keys.
**Note**: If `DEBUG=True`, Django's standard traceback error page will be displayed instead of rendering the HTTP status code and text.
--- ---
# Third party packages # Third party packages

View File

@ -35,6 +35,17 @@ A suitable replacement theme can be generated using Bootstrap's [Customize Tool]
You can also change the navbar variant, which by default is `navbar-inverse`, using the `bootstrap_navbar_variant` block. The empty `{% block bootstrap_navbar_variant %}{% endblock %}` will use the original Bootstrap navbar style. You can also change the navbar variant, which by default is `navbar-inverse`, using the `bootstrap_navbar_variant` block. The empty `{% block bootstrap_navbar_variant %}{% endblock %}` will use the original Bootstrap navbar style.
Full Example
{% extends "rest_framework/base.html" %}
{% block bootstrap_theme %}
<link rel="stylesheet" href="/path/to/yourtheme/bootstrap.min.css' type="text/css">
{% endblock %}
{% block bootstrap_navbar_variant %}{% endblock %}
For more specific CSS tweaks, use the `style` block instead. For more specific CSS tweaks, use the `style` block instead.

View File

@ -127,6 +127,11 @@ The following people have helped make REST framework great.
* Craig de Stigter - [craigds] * Craig de Stigter - [craigds]
* Pablo Recio - [pyriku] * Pablo Recio - [pyriku]
* Brian Zambrano - [brianz] * Brian Zambrano - [brianz]
* Òscar Vilaplana - [grimborg]
* Ryan Kaskel - [ryankask]
* Andy McKay - [andymckay]
* Matteo Suppo - [matteosuppo]
* Karol Majta - [lolek09]
Many thanks to everyone who's contributed to the project. Many thanks to everyone who's contributed to the project.
@ -290,3 +295,8 @@ You can also contact [@_tomchristie][twitter] directly on twitter.
[craigds]: https://github.com/craigds [craigds]: https://github.com/craigds
[pyriku]: https://github.com/pyriku [pyriku]: https://github.com/pyriku
[brianz]: https://github.com/brianz [brianz]: https://github.com/brianz
[grimborg]: https://github.com/grimborg
[ryankask]: https://github.com/ryankask
[andymckay]: https://github.com/andymckay
[matteosuppo]: https://github.com/matteosuppo
[lolek09]: https://github.com/lolek09

View File

@ -495,3 +495,16 @@ except ImportError:
oauth2_provider_forms = None oauth2_provider_forms = None
oauth2_provider_scope = None oauth2_provider_scope = None
oauth2_constants = 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__')

View File

@ -27,7 +27,7 @@ from rest_framework.compat import (timezone, parse_date, parse_datetime,
parse_time) parse_time)
from rest_framework.compat import BytesIO from rest_framework.compat import BytesIO
from rest_framework.compat import six 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 from rest_framework.settings import api_settings
@ -76,7 +76,6 @@ def is_simple_callable(obj):
len_defaults = len(defaults) if defaults else 0 len_defaults = len(defaults) if defaults else 0
return len_args <= len_defaults return len_args <= len_defaults
def get_component(obj, attr_name): def get_component(obj, attr_name):
""" """
Given an object, and an attribute name, Given an object, and an attribute name,
@ -137,7 +136,7 @@ def humanize_field(field):
humanized = { humanized = {
'type': humanize_field_type(field.__class__), 'type': humanize_field_type(field.__class__),
'required': getattr(field, 'required', False), 'required': getattr(field, 'required', False),
'label': field.label, 'label': getattr(field, 'label', None),
} }
optional_attrs = ['read_only', 'help_text'] optional_attrs = ['read_only', 'help_text']
for attr in optional_attrs: for attr in optional_attrs:
@ -256,7 +255,8 @@ class Field(object):
if is_protected_type(value): if is_protected_type(value):
return 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] return [self.to_native(item) for item in value]
elif isinstance(value, dict): elif isinstance(value, dict):
# Make sure we preserve field ordering, if it exists # Make sure we preserve field ordering, if it exists
@ -264,7 +264,7 @@ class Field(object):
for key, val in value.items(): for key, val in value.items():
ret[key] = self.to_native(val) ret[key] = self.to_native(val)
return ret return ret
return smart_text(value) return force_text(value)
def attributes(self): def attributes(self):
""" """
@ -470,7 +470,6 @@ class URLField(CharField):
type_name = 'URLField' type_name = 'URLField'
def __init__(self, **kwargs): def __init__(self, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', 200)
kwargs['validators'] = [validators.URLValidator()] kwargs['validators'] = [validators.URLValidator()]
super(URLField, self).__init__(**kwargs) super(URLField, self).__init__(**kwargs)
@ -479,7 +478,6 @@ class SlugField(CharField):
type_name = 'SlugField' type_name = 'SlugField'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs['max_length'] = kwargs.get('max_length', 50)
super(SlugField, self).__init__(*args, **kwargs) super(SlugField, self).__init__(*args, **kwargs)

View File

@ -8,6 +8,7 @@ from __future__ import unicode_literals
from django.core.exceptions import ObjectDoesNotExist, ValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
from django import forms from django import forms
from django.db.models.fields import BLANK_CHOICE_DASH
from django.forms import widgets from django.forms import widgets
from django.forms.models import ModelChoiceIterator from django.forms.models import ModelChoiceIterator
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -47,7 +48,7 @@ class RelatedField(WritableField):
DeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)
kwargs['required'] = not kwargs.pop('null') kwargs['required'] = not kwargs.pop('null')
self.queryset = kwargs.pop('queryset', None) queryset = kwargs.pop('queryset', None)
self.many = kwargs.pop('many', self.many) self.many = kwargs.pop('many', self.many)
if self.many: if self.many:
self.widget = self.many_widget self.widget = self.many_widget
@ -56,6 +57,11 @@ class RelatedField(WritableField):
kwargs['read_only'] = kwargs.pop('read_only', self.read_only) kwargs['read_only'] = kwargs.pop('read_only', self.read_only)
super(RelatedField, self).__init__(*args, **kwargs) super(RelatedField, self).__init__(*args, **kwargs)
if not self.required:
self.empty_label = BLANK_CHOICE_DASH[0][1]
self.queryset = queryset
def initialize(self, parent, field_name): def initialize(self, parent, field_name):
super(RelatedField, self).initialize(parent, field_name) super(RelatedField, self).initialize(parent, field_name)
if self.queryset is None and not self.read_only: if self.queryset is None and not self.read_only:
@ -442,7 +448,7 @@ class HyperlinkedRelatedField(RelatedField):
raise Exception('Writable related fields must include a `queryset` argument') raise Exception('Writable related fields must include a `queryset` argument')
try: try:
http_prefix = value.startswith('http:') or value.startswith('https:') http_prefix = value.startswith(('http:', 'https:'))
except AttributeError: except AttributeError:
msg = self.error_messages['incorrect_type'] msg = self.error_messages['incorrect_type']
raise ValidationError(msg % type(value).__name__) raise ValidationError(msg % type(value).__name__)

View File

@ -36,6 +36,7 @@ class BaseRenderer(object):
media_type = None media_type = None
format = None format = None
charset = None
def render(self, data, accepted_media_type=None, renderer_context=None): def render(self, data, accepted_media_type=None, renderer_context=None):
raise NotImplemented('Renderer class requires .render() to be implemented') raise NotImplemented('Renderer class requires .render() to be implemented')
@ -49,6 +50,7 @@ class JSONRenderer(BaseRenderer):
media_type = 'application/json' media_type = 'application/json'
format = 'json' format = 'json'
encoder_class = encoders.JSONEncoder encoder_class = encoders.JSONEncoder
ensure_ascii = True
def render(self, data, accepted_media_type=None, renderer_context=None): def render(self, data, accepted_media_type=None, renderer_context=None):
""" """
@ -72,7 +74,12 @@ class JSONRenderer(BaseRenderer):
except (ValueError, TypeError): except (ValueError, TypeError):
indent = None indent = None
return json.dumps(data, cls=self.encoder_class, indent=indent) return json.dumps(data, cls=self.encoder_class, indent=indent, ensure_ascii=self.ensure_ascii)
class UnicodeJSONRenderer(JSONRenderer):
ensure_ascii = False
charset = 'utf-8'
class JSONPRenderer(JSONRenderer): class JSONPRenderer(JSONRenderer):
@ -115,6 +122,7 @@ class XMLRenderer(BaseRenderer):
media_type = 'application/xml' media_type = 'application/xml'
format = 'xml' format = 'xml'
charset = 'utf-8'
def render(self, data, accepted_media_type=None, renderer_context=None): def render(self, data, accepted_media_type=None, renderer_context=None):
""" """
@ -164,6 +172,7 @@ class YAMLRenderer(BaseRenderer):
media_type = 'application/yaml' media_type = 'application/yaml'
format = 'yaml' format = 'yaml'
encoder = encoders.SafeDumper encoder = encoders.SafeDumper
charset = 'utf-8'
def render(self, data, accepted_media_type=None, renderer_context=None): def render(self, data, accepted_media_type=None, renderer_context=None):
""" """
@ -204,6 +213,7 @@ class TemplateHTMLRenderer(BaseRenderer):
'%(status_code)s.html', '%(status_code)s.html',
'api_exception.html' 'api_exception.html'
] ]
charset = 'utf-8'
def render(self, data, accepted_media_type=None, renderer_context=None): def render(self, data, accepted_media_type=None, renderer_context=None):
""" """
@ -275,6 +285,7 @@ class StaticHTMLRenderer(TemplateHTMLRenderer):
""" """
media_type = 'text/html' media_type = 'text/html'
format = 'html' format = 'html'
charset = 'utf-8'
def render(self, data, accepted_media_type=None, renderer_context=None): def render(self, data, accepted_media_type=None, renderer_context=None):
renderer_context = renderer_context or {} renderer_context = renderer_context or {}
@ -296,6 +307,7 @@ class BrowsableAPIRenderer(BaseRenderer):
media_type = 'text/html' media_type = 'text/html'
format = 'api' format = 'api'
template = 'rest_framework/api.html' template = 'rest_framework/api.html'
charset = 'utf-8'
def get_default_renderer(self, view): def get_default_renderer(self, view):
""" """
@ -321,7 +333,7 @@ class BrowsableAPIRenderer(BaseRenderer):
content = renderer.render(data, accepted_media_type, renderer_context) content = renderer.render(data, accepted_media_type, renderer_context)
if not all(char in string.printable for char in content): if not all(char in string.printable for char in content):
return '[%d bytes of binary content]' return '[%d bytes of binary content]' % len(content)
return content return content
@ -337,6 +349,8 @@ class BrowsableAPIRenderer(BaseRenderer):
try: try:
view.check_permissions(request) view.check_permissions(request)
if obj is not None:
view.check_object_permissions(request, obj)
except exceptions.APIException: except exceptions.APIException:
return False # Doesn't have permissions return False # Doesn't have permissions
return True return True

View File

@ -18,7 +18,7 @@ class Response(SimpleTemplateResponse):
def __init__(self, data=None, status=200, def __init__(self, data=None, status=200,
template_name=None, headers=None, template_name=None, headers=None,
exception=False): exception=False, charset=None):
""" """
Alters the init arguments slightly. Alters the init arguments slightly.
For example, drop 'template_name', and instead use 'data'. For example, drop 'template_name', and instead use 'data'.
@ -30,6 +30,7 @@ class Response(SimpleTemplateResponse):
self.data = data self.data = data
self.template_name = template_name self.template_name = template_name
self.exception = exception self.exception = exception
self.charset = charset
if headers: if headers:
for name, value in six.iteritems(headers): for name, value in six.iteritems(headers):
@ -46,7 +47,14 @@ class Response(SimpleTemplateResponse):
assert context, ".renderer_context not set on Response" assert context, ".renderer_context not set on Response"
context['response'] = self context['response'] = self
self['Content-Type'] = media_type if self.charset is None:
self.charset = renderer.charset
if self.charset is not None:
content_type = "{0}; charset={1}".format(media_type, self.charset)
else:
content_type = media_type
self['Content-Type'] = content_type
return renderer.render(self.data, media_type, context) return renderer.render(self.data, media_type, context)
@property @property

View File

@ -378,23 +378,27 @@ class BaseSerializer(WritableField):
# Set the serializer object if it exists # Set the serializer object if it exists
obj = getattr(self.parent.object, field_name) if self.parent.object else None obj = getattr(self.parent.object, field_name) if self.parent.object else None
if value in (None, ''): if self.source == '*':
into[(self.source or field_name)] = None if value:
into.update(value)
else: else:
kwargs = { if value in (None, ''):
'instance': obj, into[(self.source or field_name)] = None
'data': value,
'context': self.context,
'partial': self.partial,
'many': self.many
}
serializer = self.__class__(**kwargs)
if serializer.is_valid():
into[self.source or field_name] = serializer.object
else: else:
# Propagate errors up to our parent kwargs = {
raise NestedValidationError(serializer.errors) 'instance': obj,
'data': value,
'context': self.context,
'partial': self.partial,
'many': self.many
}
serializer = self.__class__(**kwargs)
if serializer.is_valid():
into[self.source or field_name] = serializer.object
else:
# Propagate errors up to our parent
raise NestedValidationError(serializer.errors)
def get_identity(self, data): def get_identity(self, data):
""" """
@ -587,11 +591,16 @@ class ModelSerializer(Serializer):
forward_rels += [field for field in opts.many_to_many if field.serialize] forward_rels += [field for field in opts.many_to_many if field.serialize]
for model_field in forward_rels: for model_field in forward_rels:
has_through_model = False
if model_field.rel: if model_field.rel:
to_many = isinstance(model_field, to_many = isinstance(model_field,
models.fields.related.ManyToManyField) models.fields.related.ManyToManyField)
related_model = model_field.rel.to related_model = model_field.rel.to
if to_many and not model_field.rel.through._meta.auto_created:
has_through_model = True
if model_field.rel and nested: if model_field.rel and nested:
if len(inspect.getargspec(self.get_nested_field).args) == 2: if len(inspect.getargspec(self.get_nested_field).args) == 2:
warnings.warn( warnings.warn(
@ -620,6 +629,9 @@ class ModelSerializer(Serializer):
field = self.get_field(model_field) field = self.get_field(model_field)
if field: if field:
if has_through_model:
field.read_only = True
ret[model_field.name] = field ret[model_field.name] = field
# Deal with reverse relationships # Deal with reverse relationships
@ -637,6 +649,12 @@ class ModelSerializer(Serializer):
continue continue
related_model = relation.model related_model = relation.model
to_many = relation.field.rel.multiple to_many = relation.field.rel.multiple
has_through_model = False
is_m2m = isinstance(relation.field,
models.fields.related.ManyToManyField)
if is_m2m and not relation.field.rel.through._meta.auto_created:
has_through_model = True
if nested: if nested:
field = self.get_nested_field(None, related_model, to_many) field = self.get_nested_field(None, related_model, to_many)
@ -644,6 +662,9 @@ class ModelSerializer(Serializer):
field = self.get_related_field(None, related_model, to_many) field = self.get_related_field(None, related_model, to_many)
if field: if field:
if has_through_model:
field.read_only = True
ret[accessor_name] = field ret[accessor_name] = field
# Add the `read_only` flag to any fields that have bee specified # Add the `read_only` flag to any fields that have bee specified
@ -723,6 +744,27 @@ class ModelSerializer(Serializer):
kwargs['choices'] = model_field.flatchoices kwargs['choices'] = model_field.flatchoices
return ChoiceField(**kwargs) return ChoiceField(**kwargs)
# put this below the ChoiceField because min_value isn't a valid initializer
if issubclass(model_field.__class__, models.PositiveIntegerField) or\
issubclass(model_field.__class__, models.PositiveSmallIntegerField):
kwargs['min_value'] = 0
attribute_dict = {
models.CharField: ['max_length'],
models.CommaSeparatedIntegerField: ['max_length'],
models.DecimalField: ['max_digits', 'decimal_places'],
models.EmailField: ['max_length'],
models.FileField: ['max_length'],
models.ImageField: ['max_length'],
models.SlugField: ['max_length'],
models.URLField: ['max_length'],
}
if model_field.__class__ in attribute_dict:
attributes = attribute_dict[model_field.__class__]
for attribute in attributes:
kwargs.update({attribute: getattr(model_field, attribute)})
try: try:
return self.field_mapping[model_field.__class__](**kwargs) return self.field_mapping[model_field.__class__](**kwargs)
except KeyError: except KeyError:

View File

@ -20,3 +20,162 @@ a single block in the template.
color: white; color: white;
text-decoration: none; text-decoration: none;
} }
/* custom navigation styles */
.wrapper .navbar{
width: 100%;
position: absolute;
left: 0;
top: 0;
}
.navbar .navbar-inner{
background: #2C2C2C;
color: white;
border: none;
border-top: 5px solid #A30000;
border-radius: 0px;
}
.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand:hover{
color: white;
}
.nav-list > .active > a, .nav-list > .active > a:hover {
background: #2c2c2c;
}
.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{
color: #A30000;
}
.navbar .navbar-inner .dropdown-menu li a:hover{
background: #eeeeee;
color: #c20000;
}
/*=== dabapps bootstrap styles ====*/
html{
width:100%;
background: none;
}
body, .navbar .navbar-inner .container-fluid {
max-width: 1150px;
margin: 0 auto;
}
body{
background: url("../img/grid.png") repeat-x;
background-attachment: fixed;
}
#content{
margin: 0;
}
/* sticky footer and footer */
html, body {
height: 100%;
}
.wrapper {
min-height: 100%;
height: auto !important;
height: 100%;
margin: 0 auto -60px;
}
.form-switcher {
margin-bottom: 0;
}
.well {
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
.well .form-actions {
padding-bottom: 0;
margin-bottom: 0;
}
.well form {
margin-bottom: 0;
}
.nav-tabs {
border: 0;
}
.nav-tabs > li {
float: right;
}
.nav-tabs li a {
margin-right: 0;
}
.nav-tabs > .active > a {
background: #f5f5f5;
}
.nav-tabs > .active > a:hover {
background: #f5f5f5;
}
.tabbable.first-tab-active .tab-content
{
border-top-right-radius: 0;
}
#footer, #push {
height: 60px; /* .push must be the same height as .footer */
}
#footer{
text-align: right;
}
#footer p {
text-align: center;
color: gray;
border-top: 1px solid #DDD;
padding-top: 10px;
}
#footer a {
color: gray;
font-weight: bold;
}
#footer a:hover {
color: gray;
}
.page-header {
border-bottom: none;
padding-bottom: 0px;
margin-bottom: 20px;
}
/* custom general page styles */
.hero-unit h2, .hero-unit h1{
color: #A30000;
}
body a, body a{
color: #A30000;
}
body a:hover{
color: #c20000;
}
#content a span{
text-decoration: underline;
}
.request-info {
clear:both;
}

View File

@ -69,152 +69,3 @@ pre {
margin-bottom: 20px; margin-bottom: 20px;
} }
/*=== dabapps bootstrap styles ====*/
html{
width:100%;
background: none;
}
body, .navbar .navbar-inner .container-fluid {
max-width: 1150px;
margin: 0 auto;
}
body{
background: url("../img/grid.png") repeat-x;
background-attachment: fixed;
}
#content{
margin: 0;
}
/* custom navigation styles */
.wrapper .navbar{
width: 100%;
position: absolute;
left: 0;
top: 0;
}
.navbar .navbar-inner{
background: #2C2C2C;
color: white;
border: none;
border-top: 5px solid #A30000;
border-radius: 0px;
}
.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand{
color: white;
}
.nav-list > .active > a, .nav-list > .active > a:hover {
background: #2c2c2c;
}
.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{
color: #A30000;
}
.navbar .navbar-inner .dropdown-menu li a:hover{
background: #eeeeee;
color: #c20000;
}
/* custom general page styles */
.hero-unit h2, .hero-unit h1{
color: #A30000;
}
body a, body a{
color: #A30000;
}
body a:hover{
color: #c20000;
}
#content a span{
text-decoration: underline;
}
/* sticky footer and footer */
html, body {
height: 100%;
}
.wrapper {
min-height: 100%;
height: auto !important;
height: 100%;
margin: 0 auto -60px;
}
.form-switcher {
margin-bottom: 0;
}
.well {
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
.well .form-actions {
padding-bottom: 0;
margin-bottom: 0;
}
.well form {
margin-bottom: 0;
}
.nav-tabs {
border: 0;
}
.nav-tabs > li {
float: right;
}
.nav-tabs li a {
margin-right: 0;
}
.nav-tabs > .active > a {
background: #f5f5f5;
}
.nav-tabs > .active > a:hover {
background: #f5f5f5;
}
.tabbable.first-tab-active .tab-content
{
border-top-right-radius: 0;
}
#footer, #push {
height: 60px; /* .push must be the same height as .footer */
}
#footer{
text-align: right;
}
#footer p {
text-align: center;
color: gray;
border-top: 1px solid #DDD;
padding-top: 10px;
}
#footer a {
color: gray;
font-weight: bold;
}
#footer a:hover {
color: gray;
}

View File

@ -13,8 +13,10 @@
<title>{% block title %}Django REST framework{% endblock %}</title> <title>{% block title %}Django REST framework{% endblock %}</title>
{% block style %} {% block style %}
{% block bootstrap_theme %}<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>{% endblock %} {% block bootstrap_theme %}
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/> <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/> <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/> <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
{% endblock %} {% endblock %}
@ -30,8 +32,8 @@
<div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}"> <div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
<div class="navbar-inner"> <div class="navbar-inner">
<div class="container-fluid"> <div class="container-fluid">
<span class="brand" href="/"> <span href="/">
{% block branding %}<a href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %} {% block branding %}<a class='brand' href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
</span> </span>
<ul class="nav pull-right"> <ul class="nav pull-right">
{% block userlinks %} {% block userlinks %}
@ -109,8 +111,7 @@
<div class="content-main"> <div class="content-main">
<div class="page-header"><h1>{{ name }}</h1></div> <div class="page-header"><h1>{{ name }}</h1></div>
{{ description }} {{ description }}
<div class="request-info" style="clear: both" >
<div class="request-info">
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre> <pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
</div> </div>
<div class="response-info"> <div class="response-info">

View File

@ -4,8 +4,10 @@
<head> <head>
{% block style %} {% block style %}
{% block bootstrap_theme %}<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>{% endblock %} {% block bootstrap_theme %}
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/> <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/> <link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
{% endblock %} {% endblock %}
</head> </head>

View File

@ -13,6 +13,7 @@ from django.test import TestCase
from django.core import validators from django.core import validators
from rest_framework import serializers from rest_framework import serializers
from rest_framework.serializers import Serializer from rest_framework.serializers import Serializer
from rest_framework.tests.models import RESTFrameworkModel
from rest_framework.fields import Field from rest_framework.fields import Field
from collections import namedtuple from collections import namedtuple
from uuid import uuid4 from uuid import uuid4
@ -693,6 +694,129 @@ class ChoiceFieldTests(TestCase):
self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES) self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES)
class EmailFieldTests(TestCase):
"""
Tests for EmailField attribute values
"""
class EmailFieldModel(RESTFrameworkModel):
email_field = models.EmailField(blank=True)
class EmailFieldWithGivenMaxLengthModel(RESTFrameworkModel):
email_field = models.EmailField(max_length=150, blank=True)
def test_default_model_value(self):
class EmailFieldSerializer(serializers.ModelSerializer):
class Meta:
model = self.EmailFieldModel
serializer = EmailFieldSerializer(data={})
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 75)
def test_given_model_value(self):
class EmailFieldSerializer(serializers.ModelSerializer):
class Meta:
model = self.EmailFieldWithGivenMaxLengthModel
serializer = EmailFieldSerializer(data={})
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 150)
def test_given_serializer_value(self):
class EmailFieldSerializer(serializers.ModelSerializer):
email_field = serializers.EmailField(source='email_field', max_length=20, required=False)
class Meta:
model = self.EmailFieldModel
serializer = EmailFieldSerializer(data={})
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(getattr(serializer.fields['email_field'], 'max_length'), 20)
class SlugFieldTests(TestCase):
"""
Tests for SlugField attribute values
"""
class SlugFieldModel(RESTFrameworkModel):
slug_field = models.SlugField(blank=True)
class SlugFieldWithGivenMaxLengthModel(RESTFrameworkModel):
slug_field = models.SlugField(max_length=84, blank=True)
def test_default_model_value(self):
class SlugFieldSerializer(serializers.ModelSerializer):
class Meta:
model = self.SlugFieldModel
serializer = SlugFieldSerializer(data={})
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 50)
def test_given_model_value(self):
class SlugFieldSerializer(serializers.ModelSerializer):
class Meta:
model = self.SlugFieldWithGivenMaxLengthModel
serializer = SlugFieldSerializer(data={})
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 84)
def test_given_serializer_value(self):
class SlugFieldSerializer(serializers.ModelSerializer):
slug_field = serializers.SlugField(source='slug_field', max_length=20, required=False)
class Meta:
model = self.SlugFieldModel
serializer = SlugFieldSerializer(data={})
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(getattr(serializer.fields['slug_field'], 'max_length'), 20)
class URLFieldTests(TestCase):
"""
Tests for URLField attribute values
"""
class URLFieldModel(RESTFrameworkModel):
url_field = models.URLField(blank=True)
class URLFieldWithGivenMaxLengthModel(RESTFrameworkModel):
url_field = models.URLField(max_length=128, blank=True)
def test_default_model_value(self):
class URLFieldSerializer(serializers.ModelSerializer):
class Meta:
model = self.URLFieldModel
serializer = URLFieldSerializer(data={})
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 200)
def test_given_model_value(self):
class URLFieldSerializer(serializers.ModelSerializer):
class Meta:
model = self.URLFieldWithGivenMaxLengthModel
serializer = URLFieldSerializer(data={})
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 128)
def test_given_serializer_value(self):
class URLFieldSerializer(serializers.ModelSerializer):
url_field = serializers.URLField(source='url_field', max_length=20, required=False)
class Meta:
model = self.URLFieldWithGivenMaxLengthModel
serializer = URLFieldSerializer(data={})
self.assertEqual(serializer.is_valid(), True)
self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 20)
class HumanizedFieldType(TestCase): class HumanizedFieldType(TestCase):
def test_standard_type_classes(self): def test_standard_type_classes(self):
for field_type_name in forms.fields.__all__: for field_type_name in forms.fields.__all__:

View File

@ -121,8 +121,27 @@ class TestRootView(TestCase):
'text/html' 'text/html'
], ],
'name': 'Root', 'name': 'Root',
'description': 'Example description for OPTIONS.' 'description': 'Example description for OPTIONS.',
'actions': {}
} }
# TODO: this is just a draft for fields' metadata - needs review and decision
for method in ('GET', 'POST',):
expected['actions'][method] = {
'text': {
#'description': '',
'label': None,
'read_only': False,
'required': True,
'type': 'Single Character',
},
'id': {
#'description': '',
'label': None,
'read_only': True,
'required': False,
'type': 'Integer',
},
}
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, expected) self.assertEqual(response.data, expected)
@ -238,8 +257,27 @@ class TestInstanceView(TestCase):
'text/html' 'text/html'
], ],
'name': 'Instance', 'name': 'Instance',
'description': 'Example description for OPTIONS.' 'description': 'Example description for OPTIONS.',
'actions': {}
} }
# TODO: this is just a draft idea for fields' metadata - needs review and decision
for method in ('GET', 'PATCH', 'PUT', 'DELETE'):
expected['actions'][method] = {
'text': {
#'description': '',
'label': None,
'read_only': False,
'required': True,
'type': 'Single Character',
},
'id': {
#'description': '',
'label': None,
'read_only': True,
'required': False,
'type': 'Integer',
},
}
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data, expected) self.assertEqual(response.data, expected)

View File

@ -66,19 +66,19 @@ class TemplateHTMLRendererTests(TestCase):
def test_simple_html_view(self): def test_simple_html_view(self):
response = self.client.get('/') response = self.client.get('/')
self.assertContains(response, "example: foobar") 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): def test_not_found_html_view(self):
response = self.client.get('/not_found') response = self.client.get('/not_found')
self.assertEqual(response.status_code, status.HTTP_404_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, 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): def test_permission_denied_html_view(self):
response = self.client.get('/permission_denied') response = self.client.get('/permission_denied')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.content, six.b("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): class TemplateHTMLRendererExceptionTests(TestCase):
@ -109,10 +109,10 @@ class TemplateHTMLRendererExceptionTests(TestCase):
response = self.client.get('/not_found') response = self.client.get('/not_found')
self.assertEqual(response.status_code, status.HTTP_404_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, 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): def test_permission_denied_html_view_with_template(self):
response = self.client.get('/permission_denied') response = self.client.get('/permission_denied')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
self.assertEqual(response.content, six.b("403: Permission denied")) 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')

View File

@ -3,19 +3,24 @@ from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
from rest_framework.negotiation import DefaultContentNegotiation from rest_framework.negotiation import DefaultContentNegotiation
from rest_framework.request import Request from rest_framework.request import Request
from rest_framework.renderers import BaseRenderer
factory = RequestFactory() factory = RequestFactory()
class MockJSONRenderer(object): class MockJSONRenderer(BaseRenderer):
media_type = 'application/json' media_type = 'application/json'
class MockHTMLRenderer(object): class MockHTMLRenderer(BaseRenderer):
media_type = 'text/html' media_type = 'text/html'
class NoCharsetSpecifiedRenderer(BaseRenderer):
media_type = 'my/media'
class TestAcceptedMediaType(TestCase): class TestAcceptedMediaType(TestCase):
def setUp(self): def setUp(self):
self.renderers = [MockJSONRenderer(), MockHTMLRenderer()] self.renderers = [MockJSONRenderer(), MockHTMLRenderer()]

View File

@ -108,6 +108,51 @@ class ModelPermissionsIntegrationTests(TestCase):
response = instance_view(request, pk='2') response = instance_view(request, pk='2')
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_options_permitted(self):
request = factory.options('/', content_type='application/json',
HTTP_AUTHORIZATION=self.permitted_credentials)
response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data)
self.assertEquals(response.data['actions'].keys(), ['POST', 'GET',])
request = factory.options('/1', content_type='application/json',
HTTP_AUTHORIZATION=self.permitted_credentials)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data)
self.assertEquals(response.data['actions'].keys(), ['PUT', 'PATCH', 'DELETE', 'GET',])
def test_options_disallowed(self):
request = factory.options('/', content_type='application/json',
HTTP_AUTHORIZATION=self.disallowed_credentials)
response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data)
self.assertEquals(response.data['actions'].keys(), ['GET',])
request = factory.options('/1', content_type='application/json',
HTTP_AUTHORIZATION=self.disallowed_credentials)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data)
self.assertEquals(response.data['actions'].keys(), ['GET',])
def test_options_updateonly(self):
request = factory.options('/', content_type='application/json',
HTTP_AUTHORIZATION=self.updateonly_credentials)
response = root_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data)
self.assertEquals(response.data['actions'].keys(), ['GET',])
request = factory.options('/1', content_type='application/json',
HTTP_AUTHORIZATION=self.updateonly_credentials)
response = instance_view(request, pk='1')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertIn('actions', response.data)
self.assertEquals(response.data['actions'].keys(), ['PUT', 'PATCH', 'GET',])
class OwnerModel(models.Model): class OwnerModel(models.Model):
text = models.CharField(max_length=100) text = models.CharField(max_length=100)

View File

@ -1,4 +1,5 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import models
from django.test import TestCase from django.test import TestCase
from rest_framework import serializers from rest_framework import serializers
from rest_framework.tests.models import ( from rest_framework.tests.models import (
@ -127,6 +128,7 @@ class PKManyToManyTests(TestCase):
# Ensure source 4 is added, and everything else is as expected # Ensure source 4 is added, and everything else is as expected
queryset = ManyToManySource.objects.all() queryset = ManyToManySource.objects.all()
serializer = ManyToManySourceSerializer(queryset, many=True) serializer = ManyToManySourceSerializer(queryset, many=True)
self.assertFalse(serializer.fields['targets'].read_only)
expected = [ expected = [
{'id': 1, 'name': 'source-1', 'targets': [1]}, {'id': 1, 'name': 'source-1', 'targets': [1]},
{'id': 2, 'name': 'source-2', 'targets': [1, 2]}, {'id': 2, 'name': 'source-2', 'targets': [1, 2]},
@ -138,6 +140,7 @@ class PKManyToManyTests(TestCase):
def test_reverse_many_to_many_create(self): def test_reverse_many_to_many_create(self):
data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]} data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]}
serializer = ManyToManyTargetSerializer(data=data) serializer = ManyToManyTargetSerializer(data=data)
self.assertFalse(serializer.fields['sources'].read_only)
self.assertTrue(serializer.is_valid()) self.assertTrue(serializer.is_valid())
obj = serializer.save() obj = serializer.save()
self.assertEqual(serializer.data, data) self.assertEqual(serializer.data, data)
@ -426,8 +429,69 @@ class PKNullableOneToOneTests(TestCase):
self.assertEqual(serializer.data, expected) self.assertEqual(serializer.data, expected)
# The below models and tests ensure that serializer fields corresponding
# to a ManyToManyField field with a user-specified ``through`` model are
# set to read only
class ManyToManyThroughTarget(models.Model):
name = models.CharField(max_length=100)
class ManyToManyThrough(models.Model):
source = models.ForeignKey('ManyToManyThroughSource')
target = models.ForeignKey(ManyToManyThroughTarget)
class ManyToManyThroughSource(models.Model):
name = models.CharField(max_length=100)
targets = models.ManyToManyField(ManyToManyThroughTarget,
related_name='sources',
through='ManyToManyThrough')
class ManyToManyThroughTargetSerializer(serializers.ModelSerializer):
class Meta:
model = ManyToManyThroughTarget
fields = ('id', 'name', 'sources')
class ManyToManyThroughSourceSerializer(serializers.ModelSerializer):
class Meta:
model = ManyToManyThroughSource
fields = ('id', 'name', 'targets')
class PKManyToManyThroughTests(TestCase):
def setUp(self):
self.source = ManyToManyThroughSource.objects.create(
name='through-source-1')
self.target = ManyToManyThroughTarget.objects.create(
name='through-target-1')
def test_many_to_many_create(self):
data = {'id': 2, 'name': 'source-2', 'targets': [self.target.pk]}
serializer = ManyToManyThroughSourceSerializer(data=data)
self.assertTrue(serializer.fields['targets'].read_only)
self.assertTrue(serializer.is_valid())
obj = serializer.save()
self.assertEqual(obj.name, 'source-2')
self.assertEqual(obj.targets.count(), 0)
def test_many_to_many_reverse_create(self):
data = {'id': 2, 'name': 'target-2', 'sources': [self.source.pk]}
serializer = ManyToManyThroughTargetSerializer(data=data)
self.assertTrue(serializer.fields['sources'].read_only)
self.assertTrue(serializer.is_valid())
serializer.save()
obj = serializer.save()
self.assertEqual(obj.name, 'target-2')
self.assertEqual(obj.sources.count(), 0)
# Regression tests for #694 (`source` attribute on related fields) # Regression tests for #694 (`source` attribute on related fields)
class PrimaryKeyRelatedFieldSourceTests(TestCase): class PrimaryKeyRelatedFieldSourceTests(TestCase):
def test_related_manager_source(self): def test_related_manager_source(self):
""" """

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from decimal import Decimal from decimal import Decimal
from django.core.cache import cache from django.core.cache import cache
from django.test import TestCase from django.test import TestCase
@ -8,7 +9,7 @@ from rest_framework.compat import yaml, etree, patterns, url, include
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ 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.parsers import YAMLParser, XMLParser
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.compat import StringIO from rest_framework.compat import StringIO
@ -254,6 +255,23 @@ class JSONRendererTests(TestCase):
content = renderer.render(obj, 'application/json; indent=2') content = renderer.render(obj, 'application/json; indent=2')
self.assertEqual(strip_trailing_whitespace(content), _indented_repr) 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"]}')
class JSONPRendererTests(TestCase): class JSONPRendererTests(TestCase):
""" """

View File

@ -21,6 +21,9 @@ class MockJsonRenderer(BaseRenderer):
media_type = 'application/json' media_type = 'application/json'
class MockTextMediaRenderer(BaseRenderer):
media_type = 'text/html'
DUMMYSTATUS = status.HTTP_200_OK DUMMYSTATUS = status.HTTP_200_OK
DUMMYCONTENT = 'dummycontent' DUMMYCONTENT = 'dummycontent'
@ -44,13 +47,26 @@ class RendererB(BaseRenderer):
return RENDERER_B_SERIALIZER(data) return RENDERER_B_SERIALIZER(data)
class RendererC(RendererB):
media_type = 'mock/rendererc'
format = 'formatc'
charset = "rendererc"
class MockView(APIView): class MockView(APIView):
renderer_classes = (RendererA, RendererB) renderer_classes = (RendererA, RendererB, RendererC)
def get(self, request, **kwargs): def get(self, request, **kwargs):
return Response(DUMMYCONTENT, status=DUMMYSTATUS) return Response(DUMMYCONTENT, status=DUMMYSTATUS)
class MockViewSettingCharset(APIView):
renderer_classes = (RendererA, RendererB, RendererC)
def get(self, request, **kwargs):
return Response(DUMMYCONTENT, status=DUMMYSTATUS, charset='setbyview')
class HTMLView(APIView): class HTMLView(APIView):
renderer_classes = (BrowsableAPIRenderer, ) renderer_classes = (BrowsableAPIRenderer, )
@ -64,10 +80,10 @@ class HTMLView1(APIView):
def get(self, request, **kwargs): def get(self, request, **kwargs):
return Response('text') return Response('text')
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])), url(r'^setbyview$', MockViewSettingCharset.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])), 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'^html$', HTMLView.as_view()),
url(r'^html1$', HTMLView1.as_view()), url(r'^html1$', HTMLView1.as_view()),
url(r'^restframework', include('rest_framework.urls', namespace='rest_framework')) url(r'^restframework', include('rest_framework.urls', namespace='rest_framework'))
@ -173,3 +189,38 @@ class Issue122Tests(TestCase):
Test if no infinite recursion occurs. Test if no infinite recursion occurs.
""" """
self.client.get('/html1') self.client.get('/html1')
class Issue807Testts(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)
self.assertEqual(RendererA.media_type, 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_charset_set_explictly_on_response(self):
"""
The charset 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'])

View File

@ -1,12 +1,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.db import models from django.db import models
from django.db.models.fields import BLANK_CHOICE_DASH from django.db.models.fields import BLANK_CHOICE_DASH
from django.utils.datastructures import MultiValueDict
from django.test import TestCase 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
from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel, from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel, BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel,
ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo) ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel)
import datetime import datetime
import pickle import pickle
@ -91,6 +92,17 @@ class PersonSerializer(serializers.ModelSerializer):
read_only_fields = ('age',) read_only_fields = ('age',)
class NestedSerializer(serializers.Serializer):
info = serializers.Field()
class ModelSerializerWithNestedSerializer(serializers.ModelSerializer):
nested = NestedSerializer(source='*')
class Meta:
model = Person
class PersonSerializerInvalidReadOnly(serializers.ModelSerializer): class PersonSerializerInvalidReadOnly(serializers.ModelSerializer):
""" """
Testing for #652. Testing for #652.
@ -418,6 +430,17 @@ class ValidationTests(TestCase):
except: except:
self.fail('Wrong exception type thrown.') self.fail('Wrong exception type thrown.')
def test_writable_star_source_on_nested_serializer(self):
"""
Assert that a nested serializer instantiated with source='*' correctly
expands the data into the outer serializer.
"""
serializer = ModelSerializerWithNestedSerializer(data={
'name': 'marko',
'nested': {'info': 'hi'}},
)
self.assertEqual(serializer.is_valid(), True)
class CustomValidationTests(TestCase): class CustomValidationTests(TestCase):
class CommentSerializerWithFieldValidator(CommentSerializer): class CommentSerializerWithFieldValidator(CommentSerializer):
@ -1117,6 +1140,63 @@ class SerializerChoiceFields(TestCase):
) )
# Regression tests for #675
class Ticket(models.Model):
assigned = models.ForeignKey(
Person, related_name='assigned_tickets')
reviewer = models.ForeignKey(
Person, blank=True, null=True, related_name='reviewed_tickets')
class SerializerRelatedChoicesTest(TestCase):
def setUp(self):
super(SerializerRelatedChoicesTest, self).setUp()
class RelatedChoicesSerializer(serializers.ModelSerializer):
class Meta:
model = Ticket
fields = ('assigned', 'reviewer')
self.related_fields_serializer = RelatedChoicesSerializer
def test_empty_queryset_required(self):
serializer = self.related_fields_serializer()
self.assertEqual(serializer.fields['assigned'].queryset.count(), 0)
self.assertEqual(
[x for x in serializer.fields['assigned'].widget.choices],
[]
)
def test_empty_queryset_not_required(self):
serializer = self.related_fields_serializer()
self.assertEqual(serializer.fields['reviewer'].queryset.count(), 0)
self.assertEqual(
[x for x in serializer.fields['reviewer'].widget.choices],
[('', '---------')]
)
def test_with_some_persons_required(self):
Person.objects.create(name="Lionel Messi")
Person.objects.create(name="Xavi Hernandez")
serializer = self.related_fields_serializer()
self.assertEqual(serializer.fields['assigned'].queryset.count(), 2)
self.assertEqual(
[x for x in serializer.fields['assigned'].widget.choices],
[(1, 'Person object - 1'), (2, 'Person object - 2')]
)
def test_with_some_persons_not_required(self):
Person.objects.create(name="Lionel Messi")
Person.objects.create(name="Xavi Hernandez")
serializer = self.related_fields_serializer()
self.assertEqual(serializer.fields['reviewer'].queryset.count(), 2)
self.assertEqual(
[x for x in serializer.fields['reviewer'].widget.choices],
[('', '---------'), (1, 'Person object - 1'), (2, 'Person object - 2')]
)
class DepthTest(TestCase): class DepthTest(TestCase):
def test_implicit_nesting(self): def test_implicit_nesting(self):
@ -1242,3 +1322,185 @@ class DeserializeListTestCase(TestCase):
self.assertFalse(serializer.is_valid()) self.assertFalse(serializer.is_valid())
expected = [{}, {'email': ['This field is required.']}, {}] expected = [{}, {'email': ['This field is required.']}, {}]
self.assertEqual(serializer.errors, expected) 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'))
class AttributeMappingOnAutogeneratedFieldsTests(TestCase):
def setUp(self):
class AMOAFModel(RESTFrameworkModel):
char_field = models.CharField(max_length=1024, blank=True)
comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True)
decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True)
email_field = models.EmailField(max_length=1024, blank=True)
file_field = models.FileField(max_length=1024, blank=True)
image_field = models.ImageField(max_length=1024, blank=True)
slug_field = models.SlugField(max_length=1024, blank=True)
url_field = models.URLField(max_length=1024, blank=True)
class AMOAFSerializer(serializers.ModelSerializer):
class Meta:
model = AMOAFModel
self.serializer_class = AMOAFSerializer
self.fields_attributes = {
'char_field': [
('max_length', 1024),
],
'comma_separated_integer_field': [
('max_length', 1024),
],
'decimal_field': [
('max_digits', 64),
('decimal_places', 32),
],
'email_field': [
('max_length', 1024),
],
'file_field': [
('max_length', 1024),
],
'image_field': [
('max_length', 1024),
],
'slug_field': [
('max_length', 1024),
],
'url_field': [
('max_length', 1024),
],
}
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_char_field(self):
self.field_test('char_field')
def test_comma_separated_integer_field(self):
self.field_test('comma_separated_integer_field')
def test_decimal_field(self):
self.field_test('decimal_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')
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')

View File

@ -71,6 +71,10 @@ class APIView(View):
actions = {} actions = {}
for method in self.allowed_methods: for method in self.allowed_methods:
# skip HEAD and OPTIONS
if method in ('HEAD', 'OPTIONS'):
continue
cloned_request = clone_request(request, method) cloned_request = clone_request(request, method)
try: try:
self.check_permissions(cloned_request) self.check_permissions(cloned_request)
@ -81,11 +85,13 @@ class APIView(View):
field_name_types = {} field_name_types = {}
for name, field in serializer.fields.iteritems(): for name, field in serializer.fields.iteritems():
from rest_framework.fields import humanize_field from rest_framework.fields import humanize_field
humanize_field(field) field_name_types[name] = humanize_field(field)
field_name_types[name] = field.__class__.__name__
actions[method] = field_name_types actions[method] = field_name_types
except: except exceptions.PermissionDenied:
# don't add this method
pass
except exceptions.NotAuthenticated:
# don't add this method # don't add this method
pass pass