mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-27 03:54:01 +03:00
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:
commit
42b61ffcd7
|
@ -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].
|
||||
|
||||
## ManyToManyFields with a Through Model
|
||||
|
||||
By default, relational fields that target a ``ManyToManyField`` with a
|
||||
``through`` model specified are set to read-only.
|
||||
|
||||
If you exlicitly specify a relational field pointing to a
|
||||
``ManyToManyField`` with a through model, be sure to set ``read_only``
|
||||
to ``True``.
|
||||
|
||||
## Advanced Hyperlinked fields
|
||||
|
||||
If you have very specific requirements for the style of your hyperlinked relationships you can override `HyperlinkedRelatedField`.
|
||||
|
|
|
@ -67,7 +67,7 @@ If your API includes views that can serve both regular webpages and API response
|
|||
|
||||
## JSONRenderer
|
||||
|
||||
Renders the request data into `JSON`.
|
||||
Renders the request data into `JSON` 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`.
|
||||
|
||||
|
@ -75,6 +75,10 @@ The client may additionally include an `'indent'` media type parameter, in which
|
|||
|
||||
**.format**: `'.json'`
|
||||
|
||||
## UnicodeJSONRenderer
|
||||
|
||||
Same as `JSONRenderer` but doesn't enforce ASCII encoding
|
||||
|
||||
## 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.
|
||||
|
@ -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`.
|
||||
* 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.
|
||||
|
||||
**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
|
||||
|
|
|
@ -35,6 +35,17 @@ A suitable replacement theme can be generated using Bootstrap's [Customize Tool]
|
|||
|
||||
You can also change the navbar variant, which by default is `navbar-inverse`, using the `bootstrap_navbar_variant` block. The empty `{% block bootstrap_navbar_variant %}{% endblock %}` will use the original Bootstrap navbar style.
|
||||
|
||||
Full Example
|
||||
|
||||
{% extends "rest_framework/base.html" %}
|
||||
|
||||
{% block bootstrap_theme %}
|
||||
<link rel="stylesheet" href="/path/to/yourtheme/bootstrap.min.css' type="text/css">
|
||||
{% endblock %}
|
||||
|
||||
{% block bootstrap_navbar_variant %}{% endblock %}
|
||||
|
||||
|
||||
For more specific CSS tweaks, use the `style` block instead.
|
||||
|
||||
|
||||
|
|
|
@ -127,6 +127,11 @@ The following people have helped make REST framework great.
|
|||
* Craig de Stigter - [craigds]
|
||||
* Pablo Recio - [pyriku]
|
||||
* Brian Zambrano - [brianz]
|
||||
* Òscar Vilaplana - [grimborg]
|
||||
* Ryan Kaskel - [ryankask]
|
||||
* Andy McKay - [andymckay]
|
||||
* Matteo Suppo - [matteosuppo]
|
||||
* Karol Majta - [lolek09]
|
||||
|
||||
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
|
||||
[pyriku]: https://github.com/pyriku
|
||||
[brianz]: https://github.com/brianz
|
||||
[grimborg]: https://github.com/grimborg
|
||||
[ryankask]: https://github.com/ryankask
|
||||
[andymckay]: https://github.com/andymckay
|
||||
[matteosuppo]: https://github.com/matteosuppo
|
||||
[lolek09]: https://github.com/lolek09
|
||||
|
|
|
@ -495,3 +495,16 @@ except ImportError:
|
|||
oauth2_provider_forms = None
|
||||
oauth2_provider_scope = None
|
||||
oauth2_constants = None
|
||||
|
||||
# Handle lazy strings
|
||||
from django.utils.functional import Promise
|
||||
|
||||
if six.PY3:
|
||||
def is_non_str_iterable(obj):
|
||||
if (isinstance(obj, str) or
|
||||
(isinstance(obj, Promise) and obj._delegate_text)):
|
||||
return False
|
||||
return hasattr(obj, '__iter__')
|
||||
else:
|
||||
def is_non_str_iterable(obj):
|
||||
return hasattr(obj, '__iter__')
|
||||
|
|
|
@ -27,7 +27,7 @@ from rest_framework.compat import (timezone, parse_date, parse_datetime,
|
|||
parse_time)
|
||||
from rest_framework.compat import BytesIO
|
||||
from rest_framework.compat import six
|
||||
from rest_framework.compat import smart_text
|
||||
from rest_framework.compat import smart_text, force_text, is_non_str_iterable
|
||||
from rest_framework.settings import api_settings
|
||||
|
||||
|
||||
|
@ -76,7 +76,6 @@ def is_simple_callable(obj):
|
|||
len_defaults = len(defaults) if defaults else 0
|
||||
return len_args <= len_defaults
|
||||
|
||||
|
||||
def get_component(obj, attr_name):
|
||||
"""
|
||||
Given an object, and an attribute name,
|
||||
|
@ -137,7 +136,7 @@ def humanize_field(field):
|
|||
humanized = {
|
||||
'type': humanize_field_type(field.__class__),
|
||||
'required': getattr(field, 'required', False),
|
||||
'label': field.label,
|
||||
'label': getattr(field, 'label', None),
|
||||
}
|
||||
optional_attrs = ['read_only', 'help_text']
|
||||
for attr in optional_attrs:
|
||||
|
@ -256,7 +255,8 @@ class Field(object):
|
|||
|
||||
if is_protected_type(value):
|
||||
return value
|
||||
elif hasattr(value, '__iter__') and not isinstance(value, (dict, six.string_types)):
|
||||
elif (is_non_str_iterable(value) and
|
||||
not isinstance(value, (dict, six.string_types))):
|
||||
return [self.to_native(item) for item in value]
|
||||
elif isinstance(value, dict):
|
||||
# Make sure we preserve field ordering, if it exists
|
||||
|
@ -264,7 +264,7 @@ class Field(object):
|
|||
for key, val in value.items():
|
||||
ret[key] = self.to_native(val)
|
||||
return ret
|
||||
return smart_text(value)
|
||||
return force_text(value)
|
||||
|
||||
def attributes(self):
|
||||
"""
|
||||
|
@ -470,7 +470,6 @@ class URLField(CharField):
|
|||
type_name = 'URLField'
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
kwargs['max_length'] = kwargs.get('max_length', 200)
|
||||
kwargs['validators'] = [validators.URLValidator()]
|
||||
super(URLField, self).__init__(**kwargs)
|
||||
|
||||
|
@ -479,7 +478,6 @@ class SlugField(CharField):
|
|||
type_name = 'SlugField'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['max_length'] = kwargs.get('max_length', 50)
|
||||
super(SlugField, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ from __future__ import unicode_literals
|
|||
from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.urlresolvers import resolve, get_script_prefix, NoReverseMatch
|
||||
from django import forms
|
||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||
from django.forms import widgets
|
||||
from django.forms.models import ModelChoiceIterator
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
@ -47,7 +48,7 @@ class RelatedField(WritableField):
|
|||
DeprecationWarning, stacklevel=2)
|
||||
kwargs['required'] = not kwargs.pop('null')
|
||||
|
||||
self.queryset = kwargs.pop('queryset', None)
|
||||
queryset = kwargs.pop('queryset', None)
|
||||
self.many = kwargs.pop('many', self.many)
|
||||
if self.many:
|
||||
self.widget = self.many_widget
|
||||
|
@ -56,6 +57,11 @@ class RelatedField(WritableField):
|
|||
kwargs['read_only'] = kwargs.pop('read_only', self.read_only)
|
||||
super(RelatedField, self).__init__(*args, **kwargs)
|
||||
|
||||
if not self.required:
|
||||
self.empty_label = BLANK_CHOICE_DASH[0][1]
|
||||
|
||||
self.queryset = queryset
|
||||
|
||||
def initialize(self, parent, field_name):
|
||||
super(RelatedField, self).initialize(parent, field_name)
|
||||
if self.queryset is None and not self.read_only:
|
||||
|
@ -442,7 +448,7 @@ class HyperlinkedRelatedField(RelatedField):
|
|||
raise Exception('Writable related fields must include a `queryset` argument')
|
||||
|
||||
try:
|
||||
http_prefix = value.startswith('http:') or value.startswith('https:')
|
||||
http_prefix = value.startswith(('http:', 'https:'))
|
||||
except AttributeError:
|
||||
msg = self.error_messages['incorrect_type']
|
||||
raise ValidationError(msg % type(value).__name__)
|
||||
|
|
|
@ -36,6 +36,7 @@ class BaseRenderer(object):
|
|||
|
||||
media_type = None
|
||||
format = None
|
||||
charset = None
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
raise NotImplemented('Renderer class requires .render() to be implemented')
|
||||
|
@ -49,6 +50,7 @@ class JSONRenderer(BaseRenderer):
|
|||
media_type = 'application/json'
|
||||
format = 'json'
|
||||
encoder_class = encoders.JSONEncoder
|
||||
ensure_ascii = True
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
|
@ -72,7 +74,12 @@ class JSONRenderer(BaseRenderer):
|
|||
except (ValueError, TypeError):
|
||||
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):
|
||||
|
@ -115,6 +122,7 @@ class XMLRenderer(BaseRenderer):
|
|||
|
||||
media_type = 'application/xml'
|
||||
format = 'xml'
|
||||
charset = 'utf-8'
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
|
@ -164,6 +172,7 @@ class YAMLRenderer(BaseRenderer):
|
|||
media_type = 'application/yaml'
|
||||
format = 'yaml'
|
||||
encoder = encoders.SafeDumper
|
||||
charset = 'utf-8'
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
|
@ -204,6 +213,7 @@ class TemplateHTMLRenderer(BaseRenderer):
|
|||
'%(status_code)s.html',
|
||||
'api_exception.html'
|
||||
]
|
||||
charset = 'utf-8'
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
"""
|
||||
|
@ -275,6 +285,7 @@ class StaticHTMLRenderer(TemplateHTMLRenderer):
|
|||
"""
|
||||
media_type = 'text/html'
|
||||
format = 'html'
|
||||
charset = 'utf-8'
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
renderer_context = renderer_context or {}
|
||||
|
@ -296,6 +307,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
media_type = 'text/html'
|
||||
format = 'api'
|
||||
template = 'rest_framework/api.html'
|
||||
charset = 'utf-8'
|
||||
|
||||
def get_default_renderer(self, view):
|
||||
"""
|
||||
|
@ -321,7 +333,7 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
content = renderer.render(data, accepted_media_type, renderer_context)
|
||||
|
||||
if not all(char in string.printable for char in content):
|
||||
return '[%d bytes of binary content]'
|
||||
return '[%d bytes of binary content]' % len(content)
|
||||
|
||||
return content
|
||||
|
||||
|
@ -337,6 +349,8 @@ class BrowsableAPIRenderer(BaseRenderer):
|
|||
|
||||
try:
|
||||
view.check_permissions(request)
|
||||
if obj is not None:
|
||||
view.check_object_permissions(request, obj)
|
||||
except exceptions.APIException:
|
||||
return False # Doesn't have permissions
|
||||
return True
|
||||
|
|
|
@ -18,7 +18,7 @@ class Response(SimpleTemplateResponse):
|
|||
|
||||
def __init__(self, data=None, status=200,
|
||||
template_name=None, headers=None,
|
||||
exception=False):
|
||||
exception=False, charset=None):
|
||||
"""
|
||||
Alters the init arguments slightly.
|
||||
For example, drop 'template_name', and instead use 'data'.
|
||||
|
@ -30,6 +30,7 @@ class Response(SimpleTemplateResponse):
|
|||
self.data = data
|
||||
self.template_name = template_name
|
||||
self.exception = exception
|
||||
self.charset = charset
|
||||
|
||||
if headers:
|
||||
for name, value in six.iteritems(headers):
|
||||
|
@ -46,7 +47,14 @@ class Response(SimpleTemplateResponse):
|
|||
assert context, ".renderer_context not set on Response"
|
||||
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)
|
||||
|
||||
@property
|
||||
|
|
|
@ -378,23 +378,27 @@ class BaseSerializer(WritableField):
|
|||
# Set the serializer object if it exists
|
||||
obj = getattr(self.parent.object, field_name) if self.parent.object else None
|
||||
|
||||
if value in (None, ''):
|
||||
into[(self.source or field_name)] = None
|
||||
if self.source == '*':
|
||||
if value:
|
||||
into.update(value)
|
||||
else:
|
||||
kwargs = {
|
||||
'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
|
||||
if value in (None, ''):
|
||||
into[(self.source or field_name)] = None
|
||||
else:
|
||||
# Propagate errors up to our parent
|
||||
raise NestedValidationError(serializer.errors)
|
||||
kwargs = {
|
||||
'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):
|
||||
"""
|
||||
|
@ -587,11 +591,16 @@ class ModelSerializer(Serializer):
|
|||
forward_rels += [field for field in opts.many_to_many if field.serialize]
|
||||
|
||||
for model_field in forward_rels:
|
||||
has_through_model = False
|
||||
|
||||
if model_field.rel:
|
||||
to_many = isinstance(model_field,
|
||||
models.fields.related.ManyToManyField)
|
||||
related_model = model_field.rel.to
|
||||
|
||||
if to_many and not model_field.rel.through._meta.auto_created:
|
||||
has_through_model = True
|
||||
|
||||
if model_field.rel and nested:
|
||||
if len(inspect.getargspec(self.get_nested_field).args) == 2:
|
||||
warnings.warn(
|
||||
|
@ -620,6 +629,9 @@ class ModelSerializer(Serializer):
|
|||
field = self.get_field(model_field)
|
||||
|
||||
if field:
|
||||
if has_through_model:
|
||||
field.read_only = True
|
||||
|
||||
ret[model_field.name] = field
|
||||
|
||||
# Deal with reverse relationships
|
||||
|
@ -637,6 +649,12 @@ class ModelSerializer(Serializer):
|
|||
continue
|
||||
related_model = relation.model
|
||||
to_many = relation.field.rel.multiple
|
||||
has_through_model = False
|
||||
is_m2m = isinstance(relation.field,
|
||||
models.fields.related.ManyToManyField)
|
||||
|
||||
if is_m2m and not relation.field.rel.through._meta.auto_created:
|
||||
has_through_model = True
|
||||
|
||||
if nested:
|
||||
field = self.get_nested_field(None, related_model, to_many)
|
||||
|
@ -644,6 +662,9 @@ class ModelSerializer(Serializer):
|
|||
field = self.get_related_field(None, related_model, to_many)
|
||||
|
||||
if field:
|
||||
if has_through_model:
|
||||
field.read_only = True
|
||||
|
||||
ret[accessor_name] = field
|
||||
|
||||
# Add the `read_only` flag to any fields that have bee specified
|
||||
|
@ -723,6 +744,27 @@ class ModelSerializer(Serializer):
|
|||
kwargs['choices'] = model_field.flatchoices
|
||||
return ChoiceField(**kwargs)
|
||||
|
||||
# put this below the ChoiceField because min_value isn't a valid initializer
|
||||
if issubclass(model_field.__class__, models.PositiveIntegerField) or\
|
||||
issubclass(model_field.__class__, models.PositiveSmallIntegerField):
|
||||
kwargs['min_value'] = 0
|
||||
|
||||
attribute_dict = {
|
||||
models.CharField: ['max_length'],
|
||||
models.CommaSeparatedIntegerField: ['max_length'],
|
||||
models.DecimalField: ['max_digits', 'decimal_places'],
|
||||
models.EmailField: ['max_length'],
|
||||
models.FileField: ['max_length'],
|
||||
models.ImageField: ['max_length'],
|
||||
models.SlugField: ['max_length'],
|
||||
models.URLField: ['max_length'],
|
||||
}
|
||||
|
||||
if model_field.__class__ in attribute_dict:
|
||||
attributes = attribute_dict[model_field.__class__]
|
||||
for attribute in attributes:
|
||||
kwargs.update({attribute: getattr(model_field, attribute)})
|
||||
|
||||
try:
|
||||
return self.field_mapping[model_field.__class__](**kwargs)
|
||||
except KeyError:
|
||||
|
|
|
@ -19,4 +19,163 @@ a single block in the template.
|
|||
.navbar-inverse .brand:hover a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* custom navigation styles */
|
||||
.wrapper .navbar{
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.navbar .navbar-inner{
|
||||
background: #2C2C2C;
|
||||
color: white;
|
||||
border: none;
|
||||
border-top: 5px solid #A30000;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand:hover{
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-list > .active > a, .nav-list > .active > a:hover {
|
||||
background: #2c2c2c;
|
||||
}
|
||||
|
||||
.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{
|
||||
color: #A30000;
|
||||
}
|
||||
.navbar .navbar-inner .dropdown-menu li a:hover{
|
||||
background: #eeeeee;
|
||||
color: #c20000;
|
||||
}
|
||||
|
||||
/*=== dabapps bootstrap styles ====*/
|
||||
|
||||
html{
|
||||
width:100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
body, .navbar .navbar-inner .container-fluid {
|
||||
max-width: 1150px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
body{
|
||||
background: url("../img/grid.png") repeat-x;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
#content{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* sticky footer and footer */
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
.wrapper {
|
||||
min-height: 100%;
|
||||
height: auto !important;
|
||||
height: 100%;
|
||||
margin: 0 auto -60px;
|
||||
}
|
||||
|
||||
.form-switcher {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.well {
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.well .form-actions {
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.well form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.nav-tabs > li {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.nav-tabs li a {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.nav-tabs > .active > a {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.nav-tabs > .active > a:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.tabbable.first-tab-active .tab-content
|
||||
{
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
#footer, #push {
|
||||
height: 60px; /* .push must be the same height as .footer */
|
||||
}
|
||||
|
||||
#footer{
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#footer p {
|
||||
text-align: center;
|
||||
color: gray;
|
||||
border-top: 1px solid #DDD;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
#footer a {
|
||||
color: gray;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#footer a:hover {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* custom general page styles */
|
||||
.hero-unit h2, .hero-unit h1{
|
||||
color: #A30000;
|
||||
}
|
||||
|
||||
body a, body a{
|
||||
color: #A30000;
|
||||
}
|
||||
|
||||
body a:hover{
|
||||
color: #c20000;
|
||||
}
|
||||
|
||||
#content a span{
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.request-info {
|
||||
clear:both;
|
||||
}
|
||||
|
|
|
@ -69,152 +69,3 @@ pre {
|
|||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
/*=== dabapps bootstrap styles ====*/
|
||||
|
||||
html{
|
||||
width:100%;
|
||||
background: none;
|
||||
}
|
||||
|
||||
body, .navbar .navbar-inner .container-fluid {
|
||||
max-width: 1150px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
body{
|
||||
background: url("../img/grid.png") repeat-x;
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
#content{
|
||||
margin: 0;
|
||||
}
|
||||
/* custom navigation styles */
|
||||
.wrapper .navbar{
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.navbar .navbar-inner{
|
||||
background: #2C2C2C;
|
||||
color: white;
|
||||
border: none;
|
||||
border-top: 5px solid #A30000;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand{
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nav-list > .active > a, .nav-list > .active > a:hover {
|
||||
background: #2c2c2c;
|
||||
}
|
||||
|
||||
.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li{
|
||||
color: #A30000;
|
||||
}
|
||||
.navbar .navbar-inner .dropdown-menu li a:hover{
|
||||
background: #eeeeee;
|
||||
color: #c20000;
|
||||
}
|
||||
|
||||
/* custom general page styles */
|
||||
.hero-unit h2, .hero-unit h1{
|
||||
color: #A30000;
|
||||
}
|
||||
|
||||
body a, body a{
|
||||
color: #A30000;
|
||||
}
|
||||
|
||||
body a:hover{
|
||||
color: #c20000;
|
||||
}
|
||||
|
||||
#content a span{
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* sticky footer and footer */
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
.wrapper {
|
||||
min-height: 100%;
|
||||
height: auto !important;
|
||||
height: 100%;
|
||||
margin: 0 auto -60px;
|
||||
}
|
||||
|
||||
.form-switcher {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.well {
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.well .form-actions {
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.well form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.nav-tabs > li {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.nav-tabs li a {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.nav-tabs > .active > a {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.nav-tabs > .active > a:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.tabbable.first-tab-active .tab-content
|
||||
{
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
#footer, #push {
|
||||
height: 60px; /* .push must be the same height as .footer */
|
||||
}
|
||||
|
||||
#footer{
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#footer p {
|
||||
text-align: center;
|
||||
color: gray;
|
||||
border-top: 1px solid #DDD;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
#footer a {
|
||||
color: gray;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#footer a:hover {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
|
|
|
@ -13,8 +13,10 @@
|
|||
<title>{% block title %}Django REST framework{% endblock %}</title>
|
||||
|
||||
{% block style %}
|
||||
{% block bootstrap_theme %}<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>{% endblock %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
|
||||
{% block bootstrap_theme %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/prettify.css" %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
|
||||
{% endblock %}
|
||||
|
@ -30,8 +32,8 @@
|
|||
<div class="navbar {% block bootstrap_navbar_variant %}navbar-inverse{% endblock %}">
|
||||
<div class="navbar-inner">
|
||||
<div class="container-fluid">
|
||||
<span class="brand" href="/">
|
||||
{% block branding %}<a href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
|
||||
<span href="/">
|
||||
{% block branding %}<a class='brand' href='http://django-rest-framework.org'>Django REST framework <span class="version">{{ version }}</span></a>{% endblock %}
|
||||
</span>
|
||||
<ul class="nav pull-right">
|
||||
{% block userlinks %}
|
||||
|
@ -109,8 +111,7 @@
|
|||
<div class="content-main">
|
||||
<div class="page-header"><h1>{{ name }}</h1></div>
|
||||
{{ description }}
|
||||
|
||||
<div class="request-info">
|
||||
<div class="request-info" style="clear: both" >
|
||||
<pre class="prettyprint"><b>{{ request.method }}</b> {{ request.get_full_path }}</pre>
|
||||
</div>
|
||||
<div class="response-info">
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
|
||||
<head>
|
||||
{% block style %}
|
||||
{% block bootstrap_theme %}<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>{% endblock %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
|
||||
{% block bootstrap_theme %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap.min.css" %}"/>
|
||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/bootstrap-tweaks.css" %}"/>
|
||||
{% endblock %}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "rest_framework/css/default.css" %}"/>
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
|
|
@ -13,6 +13,7 @@ from django.test import TestCase
|
|||
from django.core import validators
|
||||
from rest_framework import serializers
|
||||
from rest_framework.serializers import Serializer
|
||||
from rest_framework.tests.models import RESTFrameworkModel
|
||||
from rest_framework.fields import Field
|
||||
from collections import namedtuple
|
||||
from uuid import uuid4
|
||||
|
@ -693,6 +694,129 @@ class ChoiceFieldTests(TestCase):
|
|||
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):
|
||||
def test_standard_type_classes(self):
|
||||
for field_type_name in forms.fields.__all__:
|
||||
|
|
|
@ -121,8 +121,27 @@ class TestRootView(TestCase):
|
|||
'text/html'
|
||||
],
|
||||
'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.data, expected)
|
||||
|
||||
|
@ -238,8 +257,27 @@ class TestInstanceView(TestCase):
|
|||
'text/html'
|
||||
],
|
||||
'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.data, expected)
|
||||
|
||||
|
|
|
@ -66,19 +66,19 @@ class TemplateHTMLRendererTests(TestCase):
|
|||
def test_simple_html_view(self):
|
||||
response = self.client.get('/')
|
||||
self.assertContains(response, "example: foobar")
|
||||
self.assertEqual(response['Content-Type'], 'text/html')
|
||||
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
||||
|
||||
def test_not_found_html_view(self):
|
||||
response = self.client.get('/not_found')
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertEqual(response.content, six.b("404 Not Found"))
|
||||
self.assertEqual(response['Content-Type'], 'text/html')
|
||||
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
||||
|
||||
def test_permission_denied_html_view(self):
|
||||
response = self.client.get('/permission_denied')
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.content, six.b("403 Forbidden"))
|
||||
self.assertEqual(response['Content-Type'], 'text/html')
|
||||
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
||||
|
||||
|
||||
class TemplateHTMLRendererExceptionTests(TestCase):
|
||||
|
@ -109,10 +109,10 @@ class TemplateHTMLRendererExceptionTests(TestCase):
|
|||
response = self.client.get('/not_found')
|
||||
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
|
||||
self.assertEqual(response.content, six.b("404: Not found"))
|
||||
self.assertEqual(response['Content-Type'], 'text/html')
|
||||
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
||||
|
||||
def test_permission_denied_html_view_with_template(self):
|
||||
response = self.client.get('/permission_denied')
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
self.assertEqual(response.content, six.b("403: Permission denied"))
|
||||
self.assertEqual(response['Content-Type'], 'text/html')
|
||||
self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8')
|
||||
|
|
|
@ -3,19 +3,24 @@ from django.test import TestCase
|
|||
from django.test.client import RequestFactory
|
||||
from rest_framework.negotiation import DefaultContentNegotiation
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.renderers import BaseRenderer
|
||||
|
||||
|
||||
factory = RequestFactory()
|
||||
|
||||
|
||||
class MockJSONRenderer(object):
|
||||
class MockJSONRenderer(BaseRenderer):
|
||||
media_type = 'application/json'
|
||||
|
||||
|
||||
class MockHTMLRenderer(object):
|
||||
class MockHTMLRenderer(BaseRenderer):
|
||||
media_type = 'text/html'
|
||||
|
||||
|
||||
class NoCharsetSpecifiedRenderer(BaseRenderer):
|
||||
media_type = 'my/media'
|
||||
|
||||
|
||||
class TestAcceptedMediaType(TestCase):
|
||||
def setUp(self):
|
||||
self.renderers = [MockJSONRenderer(), MockHTMLRenderer()]
|
||||
|
|
|
@ -108,6 +108,51 @@ class ModelPermissionsIntegrationTests(TestCase):
|
|||
response = instance_view(request, pk='2')
|
||||
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
||||
|
||||
def test_options_permitted(self):
|
||||
request = factory.options('/', content_type='application/json',
|
||||
HTTP_AUTHORIZATION=self.permitted_credentials)
|
||||
response = root_view(request, pk='1')
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertIn('actions', response.data)
|
||||
self.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):
|
||||
text = models.CharField(max_length=100)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.db import models
|
||||
from django.test import TestCase
|
||||
from rest_framework import serializers
|
||||
from rest_framework.tests.models import (
|
||||
|
@ -127,6 +128,7 @@ class PKManyToManyTests(TestCase):
|
|||
# Ensure source 4 is added, and everything else is as expected
|
||||
queryset = ManyToManySource.objects.all()
|
||||
serializer = ManyToManySourceSerializer(queryset, many=True)
|
||||
self.assertFalse(serializer.fields['targets'].read_only)
|
||||
expected = [
|
||||
{'id': 1, 'name': 'source-1', 'targets': [1]},
|
||||
{'id': 2, 'name': 'source-2', 'targets': [1, 2]},
|
||||
|
@ -138,6 +140,7 @@ class PKManyToManyTests(TestCase):
|
|||
def test_reverse_many_to_many_create(self):
|
||||
data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]}
|
||||
serializer = ManyToManyTargetSerializer(data=data)
|
||||
self.assertFalse(serializer.fields['sources'].read_only)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
obj = serializer.save()
|
||||
self.assertEqual(serializer.data, data)
|
||||
|
@ -426,8 +429,69 @@ class PKNullableOneToOneTests(TestCase):
|
|||
self.assertEqual(serializer.data, expected)
|
||||
|
||||
|
||||
# The below models and tests ensure that serializer fields corresponding
|
||||
# to a ManyToManyField field with a user-specified ``through`` model are
|
||||
# set to read only
|
||||
|
||||
|
||||
class ManyToManyThroughTarget(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
|
||||
class ManyToManyThrough(models.Model):
|
||||
source = models.ForeignKey('ManyToManyThroughSource')
|
||||
target = models.ForeignKey(ManyToManyThroughTarget)
|
||||
|
||||
|
||||
class ManyToManyThroughSource(models.Model):
|
||||
name = models.CharField(max_length=100)
|
||||
targets = models.ManyToManyField(ManyToManyThroughTarget,
|
||||
related_name='sources',
|
||||
through='ManyToManyThrough')
|
||||
|
||||
|
||||
class ManyToManyThroughTargetSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ManyToManyThroughTarget
|
||||
fields = ('id', 'name', 'sources')
|
||||
|
||||
|
||||
class ManyToManyThroughSourceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ManyToManyThroughSource
|
||||
fields = ('id', 'name', 'targets')
|
||||
|
||||
|
||||
class PKManyToManyThroughTests(TestCase):
|
||||
def setUp(self):
|
||||
self.source = ManyToManyThroughSource.objects.create(
|
||||
name='through-source-1')
|
||||
self.target = ManyToManyThroughTarget.objects.create(
|
||||
name='through-target-1')
|
||||
|
||||
def test_many_to_many_create(self):
|
||||
data = {'id': 2, 'name': 'source-2', 'targets': [self.target.pk]}
|
||||
serializer = ManyToManyThroughSourceSerializer(data=data)
|
||||
self.assertTrue(serializer.fields['targets'].read_only)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
obj = serializer.save()
|
||||
self.assertEqual(obj.name, 'source-2')
|
||||
self.assertEqual(obj.targets.count(), 0)
|
||||
|
||||
def test_many_to_many_reverse_create(self):
|
||||
data = {'id': 2, 'name': 'target-2', 'sources': [self.source.pk]}
|
||||
serializer = ManyToManyThroughTargetSerializer(data=data)
|
||||
self.assertTrue(serializer.fields['sources'].read_only)
|
||||
self.assertTrue(serializer.is_valid())
|
||||
serializer.save()
|
||||
obj = serializer.save()
|
||||
self.assertEqual(obj.name, 'target-2')
|
||||
self.assertEqual(obj.sources.count(), 0)
|
||||
|
||||
|
||||
# Regression tests for #694 (`source` attribute on related fields)
|
||||
|
||||
|
||||
class PrimaryKeyRelatedFieldSourceTests(TestCase):
|
||||
def test_related_manager_source(self):
|
||||
"""
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from decimal import Decimal
|
||||
from django.core.cache import cache
|
||||
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.views import APIView
|
||||
from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \
|
||||
XMLRenderer, JSONPRenderer, BrowsableAPIRenderer
|
||||
XMLRenderer, JSONPRenderer, BrowsableAPIRenderer, UnicodeJSONRenderer
|
||||
from rest_framework.parsers import YAMLParser, XMLParser
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework.compat import StringIO
|
||||
|
@ -254,6 +255,23 @@ class JSONRendererTests(TestCase):
|
|||
content = renderer.render(obj, 'application/json; indent=2')
|
||||
self.assertEqual(strip_trailing_whitespace(content), _indented_repr)
|
||||
|
||||
def test_check_ascii(self):
|
||||
obj = {'countries': ['United Kingdom', 'France', 'España']}
|
||||
renderer = JSONRenderer()
|
||||
content = renderer.render(obj, 'application/json')
|
||||
self.assertEqual(content, '{"countries": ["United Kingdom", "France", "Espa\\u00f1a"]}')
|
||||
|
||||
|
||||
class UnicodeJSONRendererTests(TestCase):
|
||||
"""
|
||||
Tests specific for the Unicode JSON Renderer
|
||||
"""
|
||||
def test_proper_encoding(self):
|
||||
obj = {'countries': ['United Kingdom', 'France', 'España']}
|
||||
renderer = UnicodeJSONRenderer()
|
||||
content = renderer.render(obj, 'application/json')
|
||||
self.assertEqual(content, '{"countries": ["United Kingdom", "France", "España"]}')
|
||||
|
||||
|
||||
class JSONPRendererTests(TestCase):
|
||||
"""
|
||||
|
|
|
@ -21,6 +21,9 @@ class MockJsonRenderer(BaseRenderer):
|
|||
media_type = 'application/json'
|
||||
|
||||
|
||||
class MockTextMediaRenderer(BaseRenderer):
|
||||
media_type = 'text/html'
|
||||
|
||||
DUMMYSTATUS = status.HTTP_200_OK
|
||||
DUMMYCONTENT = 'dummycontent'
|
||||
|
||||
|
@ -44,13 +47,26 @@ class RendererB(BaseRenderer):
|
|||
return RENDERER_B_SERIALIZER(data)
|
||||
|
||||
|
||||
class RendererC(RendererB):
|
||||
media_type = 'mock/rendererc'
|
||||
format = 'formatc'
|
||||
charset = "rendererc"
|
||||
|
||||
|
||||
class MockView(APIView):
|
||||
renderer_classes = (RendererA, RendererB)
|
||||
renderer_classes = (RendererA, RendererB, RendererC)
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
return Response(DUMMYCONTENT, status=DUMMYSTATUS)
|
||||
|
||||
|
||||
class MockViewSettingCharset(APIView):
|
||||
renderer_classes = (RendererA, RendererB, RendererC)
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
return Response(DUMMYCONTENT, status=DUMMYSTATUS, charset='setbyview')
|
||||
|
||||
|
||||
class HTMLView(APIView):
|
||||
renderer_classes = (BrowsableAPIRenderer, )
|
||||
|
||||
|
@ -64,10 +80,10 @@ class HTMLView1(APIView):
|
|||
def get(self, request, **kwargs):
|
||||
return Response('text')
|
||||
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
|
||||
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB])),
|
||||
url(r'^setbyview$', MockViewSettingCharset.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
|
||||
url(r'^.*\.(?P<format>.+)$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
|
||||
url(r'^$', MockView.as_view(renderer_classes=[RendererA, RendererB, RendererC])),
|
||||
url(r'^html$', HTMLView.as_view()),
|
||||
url(r'^html1$', HTMLView1.as_view()),
|
||||
url(r'^restframework', include('rest_framework.urls', namespace='rest_framework'))
|
||||
|
@ -173,3 +189,38 @@ class Issue122Tests(TestCase):
|
|||
Test if no infinite recursion occurs.
|
||||
"""
|
||||
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'])
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
from __future__ import unicode_literals
|
||||
from django.db import models
|
||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
from django.test import TestCase
|
||||
from django.utils.datastructures import MultiValueDict
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel,
|
||||
BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel,
|
||||
ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo)
|
||||
ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel)
|
||||
import datetime
|
||||
import pickle
|
||||
|
||||
|
@ -91,6 +92,17 @@ class PersonSerializer(serializers.ModelSerializer):
|
|||
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):
|
||||
"""
|
||||
Testing for #652.
|
||||
|
@ -418,6 +430,17 @@ class ValidationTests(TestCase):
|
|||
except:
|
||||
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 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):
|
||||
def test_implicit_nesting(self):
|
||||
|
||||
|
@ -1242,3 +1322,185 @@ class DeserializeListTestCase(TestCase):
|
|||
self.assertFalse(serializer.is_valid())
|
||||
expected = [{}, {'email': ['This field is required.']}, {}]
|
||||
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')
|
||||
|
|
|
@ -71,6 +71,10 @@ class APIView(View):
|
|||
actions = {}
|
||||
|
||||
for method in self.allowed_methods:
|
||||
# skip HEAD and OPTIONS
|
||||
if method in ('HEAD', 'OPTIONS'):
|
||||
continue
|
||||
|
||||
cloned_request = clone_request(request, method)
|
||||
try:
|
||||
self.check_permissions(cloned_request)
|
||||
|
@ -81,11 +85,13 @@ class APIView(View):
|
|||
field_name_types = {}
|
||||
for name, field in serializer.fields.iteritems():
|
||||
from rest_framework.fields import humanize_field
|
||||
humanize_field(field)
|
||||
field_name_types[name] = field.__class__.__name__
|
||||
field_name_types[name] = humanize_field(field)
|
||||
|
||||
actions[method] = field_name_types
|
||||
except:
|
||||
except exceptions.PermissionDenied:
|
||||
# don't add this method
|
||||
pass
|
||||
except exceptions.NotAuthenticated:
|
||||
# don't add this method
|
||||
pass
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user