Commit 42b61ffc by Òscar Vilaplana

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

Merged work in progress for Issue 192 expose fields for options
parents fecadaca c0f3a1c3
...@@ -381,6 +381,15 @@ Note that reverse generic keys, expressed using the `GenericRelation` field, can ...@@ -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`.
......
...@@ -67,7 +67,7 @@ If your API includes views that can serve both regular webpages and API response ...@@ -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 ...@@ -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 ...@@ -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
......
...@@ -35,6 +35,17 @@ A suitable replacement theme can be generated using Bootstrap's [Customize Tool] ...@@ -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.
......
...@@ -127,6 +127,11 @@ The following people have helped make REST framework great. ...@@ -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. ...@@ -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
...@@ -495,3 +495,16 @@ except ImportError: ...@@ -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__')
...@@ -27,7 +27,7 @@ from rest_framework.compat import (timezone, parse_date, parse_datetime, ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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)
......
...@@ -8,6 +8,7 @@ from __future__ import unicode_literals ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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__)
......
...@@ -36,6 +36,7 @@ class BaseRenderer(object): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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
......
...@@ -18,7 +18,7 @@ class Response(SimpleTemplateResponse): ...@@ -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): ...@@ -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): ...@@ -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
......
...@@ -378,23 +378,27 @@ class BaseSerializer(WritableField): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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:
......
...@@ -19,4 +19,163 @@ a single block in the template. ...@@ -19,4 +19,163 @@ a single block in the template.
.navbar-inverse .brand:hover a { .navbar-inverse .brand:hover a {
color: white; color: white;
text-decoration: none; text-decoration: none;
} }
\ No newline at end of file
/* 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;
}
...@@ -69,152 +69,3 @@ pre { ...@@ -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;
}
...@@ -13,8 +13,10 @@ ...@@ -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 @@ ...@@ -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 @@ ...@@ -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">
......
...@@ -4,8 +4,10 @@ ...@@ -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>
......
...@@ -13,6 +13,7 @@ from django.test import TestCase ...@@ -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): ...@@ -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__:
......
...@@ -121,8 +121,27 @@ class TestRootView(TestCase): ...@@ -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): ...@@ -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)
......
...@@ -66,19 +66,19 @@ class TemplateHTMLRendererTests(TestCase): ...@@ -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): ...@@ -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')
...@@ -3,19 +3,24 @@ from django.test import TestCase ...@@ -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()]
......
...@@ -108,6 +108,51 @@ class ModelPermissionsIntegrationTests(TestCase): ...@@ -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)
......
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): ...@@ -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): ...@@ -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): ...@@ -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):
""" """
......
# -*- 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 ...@@ -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): ...@@ -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):
""" """
......
...@@ -21,6 +21,9 @@ class MockJsonRenderer(BaseRenderer): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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'])
...@@ -71,6 +71,10 @@ class APIView(View): ...@@ -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): ...@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment