aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Christie2015-01-05 11:02:28 +0000
committerTom Christie2015-01-05 11:02:28 +0000
commit6168f60ba80a84768172437c09957c1fab05f014 (patch)
tree4922399b5996d6cd0fdeb4aad2fbb99698f3a1c8
parent889a0bdeca942995ab32bf19c3d9fdbfaeec58ec (diff)
parent8cf37449715c32c4a692667814466c7f32e8734f (diff)
downloaddjango-rest-framework-6168f60ba80a84768172437c09957c1fab05f014.tar.bz2
Merge branch 'master' into version-3.1
-rw-r--r--README.md2
-rw-r--r--docs/api-guide/fields.md2
-rw-r--r--docs/api-guide/format-suffixes.md12
-rwxr-xr-xdocs/api-guide/generic-views.md4
-rw-r--r--docs/api-guide/relations.md1
-rw-r--r--docs/api-guide/routers.md50
-rw-r--r--docs/api-guide/serializers.md2
-rw-r--r--docs/index.md2
-rw-r--r--docs/topics/3.0-announcement.md1
-rw-r--r--docs/tutorial/1-serialization.md4
-rw-r--r--docs/tutorial/6-viewsets-and-routers.md2
-rw-r--r--rest_framework/__init__.py2
-rw-r--r--rest_framework/compat.py10
-rw-r--r--rest_framework/fields.py13
-rw-r--r--rest_framework/pagination.py8
-rw-r--r--rest_framework/routers.py21
-rw-r--r--rest_framework/templates/rest_framework/inline/checkbox_multiple.html2
-rw-r--r--rest_framework/templates/rest_framework/vertical/list_fieldset.html4
-rw-r--r--rest_framework/utils/field_mapping.py15
-rw-r--r--rest_framework/utils/model_meta.py57
-rw-r--r--tests/test_fields.py22
-rw-r--r--tests/test_model_serializer.py2
-rw-r--r--tests/test_routers.py144
23 files changed, 272 insertions, 110 deletions
diff --git a/README.md b/README.md
index 8b17901c..4c9d765e 100644
--- a/README.md
+++ b/README.md
@@ -156,7 +156,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
# License
-Copyright (c) 2011-2014, Tom Christie
+Copyright (c) 2011-2015, Tom Christie
All rights reserved.
Redistribution and use in source and binary forms, with or without
diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md
index f06db56c..946e355d 100644
--- a/docs/api-guide/fields.md
+++ b/docs/api-guide/fields.md
@@ -480,7 +480,7 @@ Let's look at an example of serializing a class that represents an RGB color val
class ColorField(serializers.Field):
"""
- Color objects are serialized into "rgb(#, #, #)" notation.
+ Color objects are serialized into 'rgb(#, #, #)' notation.
"""
def to_representation(self, obj):
return "rgb(%d, %d, %d)" % (obj.red, obj.green, obj.blue)
diff --git a/docs/api-guide/format-suffixes.md b/docs/api-guide/format-suffixes.md
index 20c1e995..35dbcd39 100644
--- a/docs/api-guide/format-suffixes.md
+++ b/docs/api-guide/format-suffixes.md
@@ -55,6 +55,18 @@ The name of the kwarg used may be modified by using the `FORMAT_SUFFIX_KWARG` se
Also note that `format_suffix_patterns` does not support descending into `include` URL patterns.
+### Using with `i18n_patterns`
+
+If using the `i18n_patterns` function provided by Django, as well as `format_suffix_patterns` you should make sure that the `i18n_patterns` function is applied as the final, or outermost function. For example:
+
+ url patterns = [
+ …
+ ]
+
+ urlpatterns = i18n_patterns(
+ format_suffix_patterns(urlpatterns, allowed=['json', 'html'])
+ )
+
---
## Accept headers vs. format suffixes
diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md
index f5bbdfdd..6374e305 100755
--- a/docs/api-guide/generic-views.md
+++ b/docs/api-guide/generic-views.md
@@ -214,6 +214,8 @@ You won't typically need to override the following methods, although you might n
The mixin classes provide the actions that are used to provide the basic view behavior. Note that the mixin classes provide action methods rather than defining the handler methods, such as `.get()` and `.post()`, directly. This allows for more flexible composition of behavior.
+The mixin classes can be imported from `rest_framework.mixins`.
+
## ListModelMixin
Provides a `.list(request, *args, **kwargs)` method, that implements listing a queryset.
@@ -258,6 +260,8 @@ If an object is deleted this returns a `204 No Content` response, otherwise it w
The following classes are the concrete generic views. If you're using generic views this is normally the level you'll be working at unless you need heavily customized behavior.
+The view classes can be imported from `rest_framework.generics`.
+
## CreateAPIView
Used for **create-only** endpoints.
diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md
index e56db229..50e3b7b5 100644
--- a/docs/api-guide/relations.md
+++ b/docs/api-guide/relations.md
@@ -231,6 +231,7 @@ This field is always read-only.
* `view_name` - The view name that should be used as the target of the relationship. If you're using [the standard router classes][routers] this will be a string with the format `<model_name>-detail`. **required**.
* `lookup_field` - The field on the target that should be used for the lookup. Should correspond to a URL keyword argument on the referenced view. Default is `'pk'`.
+* `lookup_url_kwarg` - The name of the keyword argument defined in the URL conf that corresponds to the lookup field. Defaults to using the same value as `lookup_field`.
* `format` - If using format suffixes, hyperlinked fields will use the same format suffix for the target unless overridden by using the `format` argument.
---
diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md
index 929a1710..3a8a8f6c 100644
--- a/docs/api-guide/routers.md
+++ b/docs/api-guide/routers.md
@@ -49,6 +49,38 @@ This means you'll need to explicitly set the `base_name` argument when registeri
---
+### Using `include` with routers
+
+The `.urls` attribute on a router instance is simply a standard list of URL patterns. There are a number of different styles for how you can include these URLs.
+
+For example, you can append `router.urls` to a list of existing views…
+
+ router = routers.SimpleRouter()
+ router.register(r'users', UserViewSet)
+ router.register(r'accounts', AccountViewSet)
+
+ urlpatterns = [
+ url(r'^forgot-password/$, ForgotPasswordFormView.as_view(),
+ ]
+
+ urlpatterns += router.urls
+
+Alternatively you can use Django's `include` function, like so…
+
+ urlpatterns = [
+ url(r'^forgot-password/$, ForgotPasswordFormView.as_view(),
+ url(r'^', include(router.urls))
+ ]
+
+Router URL patterns can also be namespaces.
+
+ urlpatterns = [
+ url(r'^forgot-password/$, ForgotPasswordFormView.as_view(),
+ url(r'^api/', include(router.urls, namespace='api'))
+ ]
+
+If using namespacing with hyperlinked serializers you'll also need to ensure that any `view_name` parameters on the serializers correctly reflect the namespace. In the example above you'd need to include a parameter such as `view_name='api:user-detail'` for serializer fields hyperlinked to the user detail view.
+
### Extra link and actions
Any methods on the viewset decorated with `@detail_route` or `@list_route` will also be routed.
@@ -68,6 +100,24 @@ The following URL pattern would additionally be generated:
* URL pattern: `^users/{pk}/set_password/$` Name: `'user-set-password'`
+If you do not want to use the default URL generated for your custom action, you can instead use the url_path parameter to customize it.
+
+For example, if you want to change the URL for our custom action to `^users/{pk}/change-password/$`, you could write:
+
+ from myapp.permissions import IsAdminOrIsSelf
+ from rest_framework.decorators import detail_route
+
+ class UserViewSet(ModelViewSet):
+ ...
+
+ @detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf], url_path='change-password')
+ def set_password(self, request, pk=None):
+ ...
+
+The above example would now generate the following URL pattern:
+
+* URL pattern: `^users/{pk}/change-password/$` Name: `'user-change-password'`
+
For more information see the viewset documentation on [marking extra actions for routing][route-decorators].
# API Guide
diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md
index dcbbd5f2..9a9d5032 100644
--- a/docs/api-guide/serializers.md
+++ b/docs/api-guide/serializers.md
@@ -384,7 +384,7 @@ This manager class now more nicely encapsulates that user instances and profile
has_support_contract=validated_data['profile']['has_support_contract']
)
-For more details on this approach see the Django documentation on [model managers](model-managers), and [this blogpost on using model and manger classes](encapsulation-blogpost).
+For more details on this approach see the Django documentation on [model managers](model-managers), and [this blogpost on using model and manager classes](encapsulation-blogpost).
## Dealing with multiple objects
diff --git a/docs/index.md b/docs/index.md
index f7b3ed2a..7ccec12f 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -231,7 +231,7 @@ Send a description of the issue via email to [rest-framework-security@googlegrou
## License
-Copyright (c) 2011-2014, Tom Christie
+Copyright (c) 2011-2015, Tom Christie
All rights reserved.
Redistribution and use in source and binary forms, with or without
diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md
index 0710766f..68d24782 100644
--- a/docs/topics/3.0-announcement.md
+++ b/docs/topics/3.0-announcement.md
@@ -940,6 +940,7 @@ The default JSON renderer will return float objects for un-coerced `Decimal` ins
* The serializer `ChoiceField` does not currently display nested choices, as was the case in 2.4. This will be address as part of 3.1.
* Due to the new templated form rendering, the 'widget' option is no longer valid. This means there's no easy way of using third party "autocomplete" widgets for rendering select inputs that contain a large number of choices. You'll either need to use a regular select or a plain text input. We may consider addressing this in 3.1 or 3.2 if there's sufficient demand.
* Some of the default validation error messages were rewritten and might no longer be pre-translated. You can still [create language files with Django][django-localization] if you wish to localize them.
+* `APIException` subclasses could previously take could previously take any arbitrary type in the `detail` argument. These exceptions now use translatable text strings, and as a result call `force_text` on the `detail` argument, which *must be a string*. If you need complex arguments to an `APIException` class, you should subclass it and override the `__init__()` method. Typically you'll instead want to use a custom exception handler to provide for non-standard error responses.
---
diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md
index ff507a2b..60a3d989 100644
--- a/docs/tutorial/1-serialization.md
+++ b/docs/tutorial/1-serialization.md
@@ -124,7 +124,7 @@ The first part of the serializer class defines the fields that get serialized/de
A serializer class is very similar to a Django `Form` class, and includes similar validation flags on the various fields, such as `required`, `max_length` and `default`.
-The field flags can also control how the serializer should be displayed in certain circumstances, such as when rendering to HTML. The `style={'type': 'textarea'}` flag above is equivelent to using `widget=widgets.Textarea` on a Django `Form` class. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial.
+The field flags can also control how the serializer should be displayed in certain circumstances, such as when rendering to HTML. The `{'base_template': 'textarea.html'}` flag above is equivelent to using `widget=widgets.Textarea` on a Django `Form` class. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial.
We can actually also save ourselves some time by using the `ModelSerializer` class, as we'll see later, but for now we'll keep our serializer definition explicit.
@@ -206,7 +206,7 @@ One nice property that serializers have is that you can inspect all the fields i
SnippetSerializer():
id = IntegerField(label='ID', read_only=True)
title = CharField(allow_blank=True, max_length=100, required=False)
- code = CharField(style={'type': 'textarea'})
+ code = CharField(style={'base_template': 'textarea.html'})
linenos = BooleanField(required=False)
language = ChoiceField(choices=[('Clipper', 'FoxPro'), ('Cucumber', 'Gherkin'), ('RobotFramework', 'RobotFramework'), ('abap', 'ABAP'), ('ada', 'Ada')...
style = ChoiceField(choices=[('autumn', 'autumn'), ('borland', 'borland'), ('bw', 'bw'), ('colorful', 'colorful')...
diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md
index d55a60de..63dff73f 100644
--- a/docs/tutorial/6-viewsets-and-routers.md
+++ b/docs/tutorial/6-viewsets-and-routers.md
@@ -53,6 +53,8 @@ Notice that we've also used the `@detail_route` decorator to create a custom act
Custom actions which use the `@detail_route` decorator will respond to `GET` requests. We can use the `methods` argument if we wanted an action that responded to `POST` requests.
+The URLs for custom actions by default depend on the method name itself. If you want to change the way url should be constructed, you can include url_path as a decorator keyword argument.
+
## Binding ViewSets to URLs explicitly
The handler methods only get bound to the actions when we define the URLConf.
diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py
index 6808b74b..dec89b3e 100644
--- a/rest_framework/__init__.py
+++ b/rest_framework/__init__.py
@@ -11,7 +11,7 @@ __title__ = 'Django REST framework'
__version__ = '3.0.2'
__author__ = 'Tom Christie'
__license__ = 'BSD 2-Clause'
-__copyright__ = 'Copyright 2011-2014 Tom Christie'
+__copyright__ = 'Copyright 2011-2015 Tom Christie'
# Version synonym
VERSION = __version__
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index 3c8fb0da..7241da27 100644
--- a/rest_framework/compat.py
+++ b/rest_framework/compat.py
@@ -55,6 +55,16 @@ except ImportError:
from django.http import HttpResponse as HttpResponseBase
+# request only provides `resolver_match` from 1.5 onwards.
+def get_resolver_match(request):
+ try:
+ return request.resolver_match
+ except AttributeError:
+ # Django < 1.5
+ from django.core.urlresolvers import resolve
+ return resolve(request.path_info)
+
+
# django-filter is optional
try:
import django_filters
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index c40dc3fb..aab80982 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -273,7 +273,11 @@ class Field(object):
return empty
return self.default_empty_html
ret = dictionary[self.field_name]
- return self.default_empty_html if (ret == '') else ret
+ if ret == '' and self.allow_null:
+ # If the field is blank, and null is a valid value then
+ # determine if we should use null instead.
+ return '' if getattr(self, 'allow_blank', False) else None
+ return ret
return dictionary.get(self.field_name, empty)
def get_attribute(self, instance):
@@ -545,8 +549,6 @@ class CharField(Field):
'min_length': _('Ensure this field has at least {min_length} characters.')
}
initial = ''
- coerce_blank_to_null = False
- default_empty_html = ''
def __init__(self, **kwargs):
self.allow_blank = kwargs.pop('allow_blank', False)
@@ -560,11 +562,6 @@ class CharField(Field):
message = self.error_messages['min_length'].format(min_length=min_length)
self.validators.append(MinLengthValidator(min_length, message=message))
- if self.allow_null and (not self.allow_blank) and (self.default is empty):
- # HTML input cannot represent `None` values, so we need to
- # forcibly coerce empty HTML values to `None` if `allow_null=True`.
- self.default_empty_html = None
-
def run_validation(self, data=empty):
# Test for the empty string here so that it does not get validated,
# and so that subclasses do not need to handle it explicitly
diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py
index fb451285..f31e5fa4 100644
--- a/rest_framework/pagination.py
+++ b/rest_framework/pagination.py
@@ -68,10 +68,16 @@ class BasePaginationSerializer(serializers.Serializer):
except AttributeError:
object_serializer = DefaultObjectSerializer
- self.fields[results_field] = serializers.ListSerializer(
+ try:
+ list_serializer_class = object_serializer.Meta.list_serializer_class
+ except AttributeError:
+ list_serializer_class = serializers.ListSerializer
+
+ self.fields[results_field] = list_serializer_class(
child=object_serializer(),
source='object_list'
)
+ self.fields[results_field].bind(field_name=results_field, parent=self)
class PaginationSerializer(BasePaginationSerializer):
diff --git a/rest_framework/routers.py b/rest_framework/routers.py
index 6e99f14d..61f3ccab 100644
--- a/rest_framework/routers.py
+++ b/rest_framework/routers.py
@@ -21,7 +21,7 @@ from django.conf.urls import patterns, url
from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import NoReverseMatch
from rest_framework import views
-from rest_framework.compat import OrderedDict
+from rest_framework.compat import get_resolver_match, OrderedDict
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.urlpatterns import format_suffix_patterns
@@ -176,23 +176,27 @@ class SimpleRouter(BaseRouter):
if isinstance(route, DynamicDetailRoute):
# Dynamic detail routes (@detail_route decorator)
for httpmethods, methodname in detail_routes:
+ method_kwargs = getattr(viewset, methodname).kwargs
+ url_path = method_kwargs.pop("url_path", None) or methodname
initkwargs = route.initkwargs.copy()
- initkwargs.update(getattr(viewset, methodname).kwargs)
+ initkwargs.update(method_kwargs)
ret.append(Route(
- url=replace_methodname(route.url, methodname),
+ url=replace_methodname(route.url, url_path),
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
- name=replace_methodname(route.name, methodname),
+ name=replace_methodname(route.name, url_path),
initkwargs=initkwargs,
))
elif isinstance(route, DynamicListRoute):
# Dynamic list routes (@list_route decorator)
for httpmethods, methodname in list_routes:
+ method_kwargs = getattr(viewset, methodname).kwargs
+ url_path = method_kwargs.pop("url_path", None) or methodname
initkwargs = route.initkwargs.copy()
- initkwargs.update(getattr(viewset, methodname).kwargs)
+ initkwargs.update(method_kwargs)
ret.append(Route(
- url=replace_methodname(route.url, methodname),
+ url=replace_methodname(route.url, url_path),
mapping=dict((httpmethod, methodname) for httpmethod in httpmethods),
- name=replace_methodname(route.name, methodname),
+ name=replace_methodname(route.name, url_path),
initkwargs=initkwargs,
))
else:
@@ -288,7 +292,10 @@ class DefaultRouter(SimpleRouter):
def get(self, request, *args, **kwargs):
ret = OrderedDict()
+ namespace = get_resolver_match(request).namespace
for key, url_name in api_root_dict.items():
+ if namespace:
+ url_name = namespace + ':' + url_name
try:
ret[key] = reverse(
url_name,
diff --git a/rest_framework/templates/rest_framework/inline/checkbox_multiple.html b/rest_framework/templates/rest_framework/inline/checkbox_multiple.html
index 6caf6440..09349686 100644
--- a/rest_framework/templates/rest_framework/inline/checkbox_multiple.html
+++ b/rest_framework/templates/rest_framework/inline/checkbox_multiple.html
@@ -5,7 +5,7 @@
{% for key, text in field.choices.items %}
<div class="checkbox">
<label>
- <input type="checkbox" name="{{ rest_framework/field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}>
+ <input type="checkbox" name="{{ field.name }}" value="{{ key }}" {% if key in field.value %}checked{% endif %}>
{{ text }}
</label>
</div>
diff --git a/rest_framework/templates/rest_framework/vertical/list_fieldset.html b/rest_framework/templates/rest_framework/vertical/list_fieldset.html
index 1d86c7f2..82d7b5f4 100644
--- a/rest_framework/templates/rest_framework/vertical/list_fieldset.html
+++ b/rest_framework/templates/rest_framework/vertical/list_fieldset.html
@@ -1,8 +1,4 @@
<fieldset>
{% if field.label %}<legend {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</legend>{% endif %}
-<!-- {% if field.label %}<legend {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</legend>{% endif %}
- {% for field_item in field.value.field_items.values() %}
- {{ renderer.render_field(field_item, layout=layout) }}
- {% endfor %} -->
<p>Lists are not currently supported in HTML input.</p>
</fieldset>
diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py
index fca97b4b..cba40d31 100644
--- a/rest_framework/utils/field_mapping.py
+++ b/rest_framework/utils/field_mapping.py
@@ -10,6 +10,11 @@ from rest_framework.validators import UniqueValidator
import inspect
+NUMERIC_FIELD_TYPES = (
+ models.IntegerField, models.FloatField, models.DecimalField
+)
+
+
class ClassLookupDict(object):
"""
Takes a dictionary with classes as keys.
@@ -80,7 +85,7 @@ def get_field_kwargs(field_name, model_field):
kwargs['decimal_places'] = decimal_places
if isinstance(model_field, models.TextField):
- kwargs['style'] = {'type': 'textarea'}
+ kwargs['style'] = {'base_template': 'textarea.html'}
if isinstance(model_field, models.AutoField) or not model_field.editable:
# If this field is read-only, then return early.
@@ -106,7 +111,7 @@ def get_field_kwargs(field_name, model_field):
# Ensure that max_length is passed explicitly as a keyword arg,
# rather than as a validator.
max_length = getattr(model_field, 'max_length', None)
- if max_length is not None:
+ if max_length is not None and isinstance(model_field, models.CharField):
kwargs['max_length'] = max_length
validator_kwarg = [
validator for validator in validator_kwarg
@@ -119,7 +124,7 @@ def get_field_kwargs(field_name, model_field):
validator.limit_value for validator in validator_kwarg
if isinstance(validator, validators.MinLengthValidator)
), None)
- if min_length is not None:
+ if min_length is not None and isinstance(model_field, models.CharField):
kwargs['min_length'] = min_length
validator_kwarg = [
validator for validator in validator_kwarg
@@ -132,7 +137,7 @@ def get_field_kwargs(field_name, model_field):
validator.limit_value for validator in validator_kwarg
if isinstance(validator, validators.MaxValueValidator)
), None)
- if max_value is not None:
+ if max_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
kwargs['max_value'] = max_value
validator_kwarg = [
validator for validator in validator_kwarg
@@ -145,7 +150,7 @@ def get_field_kwargs(field_name, model_field):
validator.limit_value for validator in validator_kwarg
if isinstance(validator, validators.MinValueValidator)
), None)
- if min_value is not None:
+ if min_value is not None and isinstance(model_field, NUMERIC_FIELD_TYPES):
kwargs['min_value'] = min_value
validator_kwarg = [
validator for validator in validator_kwarg
diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py
index dfc387ca..b9fb6f67 100644
--- a/rest_framework/utils/model_meta.py
+++ b/rest_framework/utils/model_meta.py
@@ -35,7 +35,7 @@ def _resolve_model(obj):
Resolve supplied `obj` to a Django model class.
`obj` must be a Django model class itself, or a string
- representation of one. Useful in situtations like GH #1225 where
+ representation of one. Useful in situations like GH #1225 where
Django may not have resolved a string-based reference to a model in
another model's foreign key definition.
@@ -56,23 +56,44 @@ def _resolve_model(obj):
def get_field_info(model):
"""
- Given a model class, returns a `FieldInfo` instance containing metadata
- about the various field types on the model.
+ Given a model class, returns a `FieldInfo` instance, which is a
+ `namedtuple`, containing metadata about the various field types on the model
+ including information about their relationships.
"""
opts = model._meta.concrete_model._meta
- # Deal with the primary key.
+ pk = _get_pk(opts)
+ fields = _get_fields(opts)
+ forward_relations = _get_forward_relationships(opts)
+ reverse_relations = _get_reverse_relationships(opts)
+ fields_and_pk = _merge_fields_and_pk(pk, fields)
+ relationships = _merge_relationships(forward_relations, reverse_relations)
+
+ return FieldInfo(pk, fields, forward_relations, reverse_relations,
+ fields_and_pk, relationships)
+
+
+def _get_pk(opts):
pk = opts.pk
while pk.rel and pk.rel.parent_link:
- # If model is a child via multitable inheritance, use parent's pk.
+ # If model is a child via multi-table inheritance, use parent's pk.
pk = pk.rel.to._meta.pk
- # Deal with regular fields.
+ return pk
+
+
+def _get_fields(opts):
fields = OrderedDict()
for field in [field for field in opts.fields if field.serialize and not field.rel]:
fields[field.name] = field
- # Deal with forward relationships.
+ return fields
+
+
+def _get_forward_relationships(opts):
+ """
+ Returns an `OrderedDict` of field names to `RelationInfo`.
+ """
forward_relations = OrderedDict()
for field in [field for field in opts.fields if field.serialize and field.rel]:
forward_relations[field.name] = RelationInfo(
@@ -93,7 +114,13 @@ def get_field_info(model):
)
)
- # Deal with reverse relationships.
+ return forward_relations
+
+
+def _get_reverse_relationships(opts):
+ """
+ Returns an `OrderedDict` of field names to `RelationInfo`.
+ """
reverse_relations = OrderedDict()
for relation in opts.get_all_related_objects():
accessor_name = relation.get_accessor_name()
@@ -117,18 +144,20 @@ def get_field_info(model):
)
)
- # Shortcut that merges both regular fields and the pk,
- # for simplifying regular field lookup.
+ return reverse_relations
+
+
+def _merge_fields_and_pk(pk, fields):
fields_and_pk = OrderedDict()
fields_and_pk['pk'] = pk
fields_and_pk[pk.name] = pk
fields_and_pk.update(fields)
- # Shortcut that merges both forward and reverse relationships
+ return fields_and_pk
- relations = OrderedDict(
+
+def _merge_relationships(forward_relations, reverse_relations):
+ return OrderedDict(
list(forward_relations.items()) +
list(reverse_relations.items())
)
-
- return FieldInfo(pk, fields, forward_relations, reverse_relations, fields_and_pk, relations)
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 04c721d3..775d4618 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -223,8 +223,8 @@ class MockHTMLDict(dict):
getlist = None
-class TestCharHTMLInput:
- def test_empty_html_checkbox(self):
+class TestHTMLInput:
+ def test_empty_html_charfield(self):
class TestSerializer(serializers.Serializer):
message = serializers.CharField(default='happy')
@@ -232,23 +232,31 @@ class TestCharHTMLInput:
assert serializer.is_valid()
assert serializer.validated_data == {'message': 'happy'}
- def test_empty_html_checkbox_allow_null(self):
+ def test_empty_html_charfield_allow_null(self):
class TestSerializer(serializers.Serializer):
message = serializers.CharField(allow_null=True)
- serializer = TestSerializer(data=MockHTMLDict())
+ serializer = TestSerializer(data=MockHTMLDict({'message': ''}))
assert serializer.is_valid()
assert serializer.validated_data == {'message': None}
- def test_empty_html_checkbox_allow_null_allow_blank(self):
+ def test_empty_html_datefield_allow_null(self):
+ class TestSerializer(serializers.Serializer):
+ expiry = serializers.DateField(allow_null=True)
+
+ serializer = TestSerializer(data=MockHTMLDict({'expiry': ''}))
+ assert serializer.is_valid()
+ assert serializer.validated_data == {'expiry': None}
+
+ def test_empty_html_charfield_allow_null_allow_blank(self):
class TestSerializer(serializers.Serializer):
message = serializers.CharField(allow_null=True, allow_blank=True)
- serializer = TestSerializer(data=MockHTMLDict({}))
+ serializer = TestSerializer(data=MockHTMLDict({'message': ''}))
assert serializer.is_valid()
assert serializer.validated_data == {'message': ''}
- def test_empty_html_required_false(self):
+ def test_empty_html_charfield_required_false(self):
class TestSerializer(serializers.Serializer):
message = serializers.CharField(required=False)
diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py
index 603faf47..db0f2e6f 100644
--- a/tests/test_model_serializer.py
+++ b/tests/test_model_serializer.py
@@ -119,7 +119,7 @@ class TestRegularFieldMappings(TestCase):
positive_small_integer_field = IntegerField()
slug_field = SlugField(max_length=100)
small_integer_field = IntegerField()
- text_field = CharField(style={'type': 'textarea'})
+ text_field = CharField(style={'base_template': 'textarea.html'})
time_field = TimeField()
url_field = URLField(max_length=100)
custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>)
diff --git a/tests/test_routers.py b/tests/test_routers.py
index 34306146..86113f5d 100644
--- a/tests/test_routers.py
+++ b/tests/test_routers.py
@@ -1,17 +1,53 @@
from __future__ import unicode_literals
-from django.conf.urls import patterns, url, include
+from django.conf.urls import url, include
from django.db import models
from django.test import TestCase
from django.core.exceptions import ImproperlyConfigured
-from rest_framework import serializers, viewsets, mixins, permissions
+from rest_framework import serializers, viewsets, permissions
from rest_framework.decorators import detail_route, list_route
from rest_framework.response import Response
from rest_framework.routers import SimpleRouter, DefaultRouter
from rest_framework.test import APIRequestFactory
+from collections import namedtuple
factory = APIRequestFactory()
-urlpatterns = patterns('',)
+
+class RouterTestModel(models.Model):
+ uuid = models.CharField(max_length=20)
+ text = models.CharField(max_length=200)
+
+
+class NoteSerializer(serializers.HyperlinkedModelSerializer):
+ url = serializers.HyperlinkedIdentityField(view_name='routertestmodel-detail', lookup_field='uuid')
+
+ class Meta:
+ model = RouterTestModel
+ fields = ('url', 'uuid', 'text')
+
+
+class NoteViewSet(viewsets.ModelViewSet):
+ queryset = RouterTestModel.objects.all()
+ serializer_class = NoteSerializer
+ lookup_field = 'uuid'
+
+
+class MockViewSet(viewsets.ModelViewSet):
+ queryset = None
+ serializer_class = None
+
+
+notes_router = SimpleRouter()
+notes_router.register(r'notes', NoteViewSet)
+
+namespaced_router = DefaultRouter()
+namespaced_router.register(r'example', MockViewSet, base_name='example')
+
+urlpatterns = [
+ url(r'^non-namespaced/', include(namespaced_router.urls)),
+ url(r'^namespaced/', include(namespaced_router.urls, namespace='example')),
+ url(r'^example/', include(notes_router.urls)),
+]
class BasicViewSet(viewsets.ViewSet):
@@ -63,9 +99,26 @@ class TestSimpleRouter(TestCase):
self.assertEqual(route.mapping[method], endpoint)
-class RouterTestModel(models.Model):
- uuid = models.CharField(max_length=20)
- text = models.CharField(max_length=200)
+class TestRootView(TestCase):
+ urls = 'tests.test_routers'
+
+ def test_retrieve_namespaced_root(self):
+ response = self.client.get('/namespaced/')
+ self.assertEqual(
+ response.data,
+ {
+ "example": "http://testserver/namespaced/example/",
+ }
+ )
+
+ def test_retrieve_non_namespaced_root(self):
+ response = self.client.get('/non-namespaced/')
+ self.assertEqual(
+ response.data,
+ {
+ "example": "http://testserver/non-namespaced/example/",
+ }
+ )
class TestCustomLookupFields(TestCase):
@@ -75,51 +128,29 @@ class TestCustomLookupFields(TestCase):
urls = 'tests.test_routers'
def setUp(self):
- class NoteSerializer(serializers.HyperlinkedModelSerializer):
- url = serializers.HyperlinkedIdentityField(view_name='routertestmodel-detail', lookup_field='uuid')
-
- class Meta:
- model = RouterTestModel
- fields = ('url', 'uuid', 'text')
-
- class NoteViewSet(viewsets.ModelViewSet):
- queryset = RouterTestModel.objects.all()
- serializer_class = NoteSerializer
- lookup_field = 'uuid'
-
- self.router = SimpleRouter()
- self.router.register(r'notes', NoteViewSet)
-
- from tests import test_routers
- urls = getattr(test_routers, 'urlpatterns')
- urls += patterns(
- '',
- url(r'^', include(self.router.urls)),
- )
-
RouterTestModel.objects.create(uuid='123', text='foo bar')
def test_custom_lookup_field_route(self):
- detail_route = self.router.urls[-1]
+ detail_route = notes_router.urls[-1]
detail_url_pattern = detail_route.regex.pattern
self.assertIn('<uuid>', detail_url_pattern)
def test_retrieve_lookup_field_list_view(self):
- response = self.client.get('/notes/')
+ response = self.client.get('/example/notes/')
self.assertEqual(
response.data,
[{
- "url": "http://testserver/notes/123/",
+ "url": "http://testserver/example/notes/123/",
"uuid": "123", "text": "foo bar"
}]
)
def test_retrieve_lookup_field_detail_view(self):
- response = self.client.get('/notes/123/')
+ response = self.client.get('/example/notes/123/')
self.assertEqual(
response.data,
{
- "url": "http://testserver/notes/123/",
+ "url": "http://testserver/example/notes/123/",
"uuid": "123", "text": "foo bar"
}
)
@@ -261,6 +292,14 @@ class DynamicListAndDetailViewSet(viewsets.ViewSet):
def detail_route_get(self, request, *args, **kwargs):
return Response({'method': 'link2'})
+ @list_route(url_path="list_custom-route")
+ def list_custom_route_get(self, request, *args, **kwargs):
+ return Response({'method': 'link1'})
+
+ @detail_route(url_path="detail_custom-route")
+ def detail_custom_route_get(self, request, *args, **kwargs):
+ return Response({'method': 'link2'})
+
class TestDynamicListAndDetailRouter(TestCase):
def setUp(self):
@@ -269,35 +308,30 @@ class TestDynamicListAndDetailRouter(TestCase):
def test_list_and_detail_route_decorators(self):
routes = self.router.get_routes(DynamicListAndDetailViewSet)
decorator_routes = [r for r in routes if not (r.name.endswith('-list') or r.name.endswith('-detail'))]
+
+ MethodNamesMap = namedtuple('MethodNamesMap', 'method_name url_path')
# Make sure all these endpoints exist and none have been clobbered
- for i, endpoint in enumerate(['list_route_get', 'list_route_post', 'detail_route_get', 'detail_route_post']):
+ for i, endpoint in enumerate([MethodNamesMap('list_custom_route_get', 'list_custom-route'),
+ MethodNamesMap('list_route_get', 'list_route_get'),
+ MethodNamesMap('list_route_post', 'list_route_post'),
+ MethodNamesMap('detail_custom_route_get', 'detail_custom-route'),
+ MethodNamesMap('detail_route_get', 'detail_route_get'),
+ MethodNamesMap('detail_route_post', 'detail_route_post')
+ ]):
route = decorator_routes[i]
# check url listing
- if endpoint.startswith('list_'):
+ method_name = endpoint.method_name
+ url_path = endpoint.url_path
+
+ if method_name.startswith('list_'):
self.assertEqual(route.url,
- '^{{prefix}}/{0}{{trailing_slash}}$'.format(endpoint))
+ '^{{prefix}}/{0}{{trailing_slash}}$'.format(url_path))
else:
self.assertEqual(route.url,
- '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(endpoint))
+ '^{{prefix}}/{{lookup}}/{0}{{trailing_slash}}$'.format(url_path))
# check method to function mapping
- if endpoint.endswith('_post'):
+ if method_name.endswith('_post'):
method_map = 'post'
else:
method_map = 'get'
- self.assertEqual(route.mapping[method_map], endpoint)
-
-
-class TestRootWithAListlessViewset(TestCase):
- def setUp(self):
- class NoteViewSet(mixins.RetrieveModelMixin,
- viewsets.GenericViewSet):
- model = RouterTestModel
-
- self.router = DefaultRouter()
- self.router.register(r'notes', NoteViewSet)
- self.view = self.router.urls[0].callback
-
- def test_api_root(self):
- request = factory.get('/')
- response = self.view(request)
- self.assertEqual(response.data, {})
+ self.assertEqual(route.mapping[method_map], method_name)