aboutsummaryrefslogtreecommitdiffstats
path: root/rest_framework
diff options
context:
space:
mode:
authorCarlton Gibson2014-12-01 11:22:39 +0100
committerCarlton Gibson2014-12-01 11:22:39 +0100
commitef26f43de4a0c9ac3081c06a383b5d3d4d007797 (patch)
treebf6abcd15f0e58f4fa79a83cd4e051b8987dd311 /rest_framework
parentc50a42bddc66e28d624cd3caadd2d63502ac2e6e (diff)
parent72c4ec4e189796e506655e275cd9c77abe98e1b9 (diff)
downloaddjango-rest-framework-ef26f43de4a0c9ac3081c06a383b5d3d4d007797.tar.bz2
Merge branch 'master' of github.com:tomchristie/django-rest-framework
Diffstat (limited to 'rest_framework')
-rw-r--r--rest_framework/authtoken/views.py14
-rw-r--r--rest_framework/decorators.py4
-rw-r--r--rest_framework/exceptions.py78
-rw-r--r--rest_framework/fields.py20
-rw-r--r--rest_framework/metadata.py5
-rw-r--r--rest_framework/relations.py19
-rw-r--r--rest_framework/renderers.py19
-rw-r--r--rest_framework/request.py2
-rw-r--r--rest_framework/response.py4
-rw-r--r--rest_framework/serializers.py107
-rw-r--r--rest_framework/templates/rest_framework/api_form.html8
-rw-r--r--rest_framework/templates/rest_framework/base.html7
-rw-r--r--rest_framework/templates/rest_framework/horizontal/list_fieldset.html3
-rw-r--r--rest_framework/templates/rest_framework/horizontal/select.html3
-rw-r--r--rest_framework/templates/rest_framework/inline/list_fieldset.html1
-rw-r--r--rest_framework/templates/rest_framework/inline/select.html5
-rw-r--r--rest_framework/templates/rest_framework/login_base.html9
-rw-r--r--rest_framework/templates/rest_framework/vertical/list_fieldset.html1
-rw-r--r--rest_framework/templates/rest_framework/vertical/select.html5
-rw-r--r--rest_framework/utils/model_meta.py7
-rw-r--r--rest_framework/validators.py14
21 files changed, 233 insertions, 102 deletions
diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py
index 103abb27..b75c2e25 100644
--- a/rest_framework/authtoken/views.py
+++ b/rest_framework/authtoken/views.py
@@ -1,5 +1,4 @@
from rest_framework.views import APIView
-from rest_framework import status
from rest_framework import parsers
from rest_framework import renderers
from rest_framework.response import Response
@@ -12,16 +11,13 @@ class ObtainAuthToken(APIView):
permission_classes = ()
parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,)
renderer_classes = (renderers.JSONRenderer,)
- serializer_class = AuthTokenSerializer
- model = Token
def post(self, request):
- serializer = self.serializer_class(data=request.data)
- if serializer.is_valid():
- user = serializer.validated_data['user']
- token, created = Token.objects.get_or_create(user=user)
- return Response({'token': token.key})
- return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+ serializer = AuthTokenSerializer(data=request.data)
+ serializer.is_valid(raise_exception=True)
+ user = serializer.validated_data['user']
+ token, created = Token.objects.get_or_create(user=user)
+ return Response({'token': token.key})
obtain_auth_token = ObtainAuthToken.as_view()
diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py
index d28d6e22..325435b3 100644
--- a/rest_framework/decorators.py
+++ b/rest_framework/decorators.py
@@ -12,12 +12,14 @@ from rest_framework.views import APIView
import types
-def api_view(http_method_names):
+def api_view(http_method_names=None):
"""
Decorator that converts a function-based view into an APIView subclass.
Takes a list of allowed methods for the view as an argument.
"""
+ if http_method_names is None:
+ http_method_names = ['GET']
def decorator(func):
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index 0b06d6e6..906de3b0 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -5,20 +5,44 @@ In addition Django's built in 403 and 404 exceptions are handled.
(`django.http.Http404` and `django.core.exceptions.PermissionDenied`)
"""
from __future__ import unicode_literals
+
+from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ungettext_lazy
from rest_framework import status
+from rest_framework.compat import force_text
import math
+def _force_text_recursive(data):
+ """
+ Descend into a nested data structure, forcing any
+ lazy translation strings into plain text.
+ """
+ if isinstance(data, list):
+ return [
+ _force_text_recursive(item) for item in data
+ ]
+ elif isinstance(data, dict):
+ return dict([
+ (key, _force_text_recursive(value))
+ for key, value in data.items()
+ ])
+ return force_text(data)
+
+
class APIException(Exception):
"""
Base class for REST framework exceptions.
Subclasses should provide `.status_code` and `.default_detail` properties.
"""
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
- default_detail = 'A server error occured'
+ default_detail = _('A server error occured')
def __init__(self, detail=None):
- self.detail = detail or self.default_detail
+ if detail is not None:
+ self.detail = force_text(detail)
+ else:
+ self.detail = force_text(self.default_detail)
def __str__(self):
return self.detail
@@ -39,7 +63,7 @@ class ValidationError(APIException):
# The details should always be coerced to a list if not already.
if not isinstance(detail, dict) and not isinstance(detail, list):
detail = [detail]
- self.detail = detail
+ self.detail = _force_text_recursive(detail)
def __str__(self):
return str(self.detail)
@@ -47,59 +71,77 @@ class ValidationError(APIException):
class ParseError(APIException):
status_code = status.HTTP_400_BAD_REQUEST
- default_detail = 'Malformed request.'
+ default_detail = _('Malformed request.')
class AuthenticationFailed(APIException):
status_code = status.HTTP_401_UNAUTHORIZED
- default_detail = 'Incorrect authentication credentials.'
+ default_detail = _('Incorrect authentication credentials.')
class NotAuthenticated(APIException):
status_code = status.HTTP_401_UNAUTHORIZED
- default_detail = 'Authentication credentials were not provided.'
+ default_detail = _('Authentication credentials were not provided.')
class PermissionDenied(APIException):
status_code = status.HTTP_403_FORBIDDEN
- default_detail = 'You do not have permission to perform this action.'
+ default_detail = _('You do not have permission to perform this action.')
class MethodNotAllowed(APIException):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED
- default_detail = "Method '%s' not allowed."
+ default_detail = _("Method '%s' not allowed.")
def __init__(self, method, detail=None):
- self.detail = detail or (self.default_detail % method)
+ if detail is not None:
+ self.detail = force_text(detail)
+ else:
+ self.detail = force_text(self.default_detail) % method
class NotAcceptable(APIException):
status_code = status.HTTP_406_NOT_ACCEPTABLE
- default_detail = "Could not satisfy the request Accept header"
+ default_detail = _('Could not satisfy the request Accept header')
def __init__(self, detail=None, available_renderers=None):
- self.detail = detail or self.default_detail
+ if detail is not None:
+ self.detail = force_text(detail)
+ else:
+ self.detail = force_text(self.default_detail)
self.available_renderers = available_renderers
class UnsupportedMediaType(APIException):
status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
- default_detail = "Unsupported media type '%s' in request."
+ default_detail = _("Unsupported media type '%s' in request.")
def __init__(self, media_type, detail=None):
- self.detail = detail or (self.default_detail % media_type)
+ if detail is not None:
+ self.detail = force_text(detail)
+ else:
+ self.detail = force_text(self.default_detail) % media_type
class Throttled(APIException):
status_code = status.HTTP_429_TOO_MANY_REQUESTS
- default_detail = 'Request was throttled.'
- extra_detail = " Expected available in %d second%s."
+ default_detail = _('Request was throttled.')
+ extra_detail = ungettext_lazy(
+ 'Expected available in %(wait)d second.',
+ 'Expected available in %(wait)d seconds.',
+ 'wait'
+ )
def __init__(self, wait=None, detail=None):
+ if detail is not None:
+ self.detail = force_text(detail)
+ else:
+ self.detail = force_text(self.default_detail)
+
if wait is None:
- self.detail = detail or self.default_detail
self.wait = None
else:
- format = (detail or self.default_detail) + self.extra_detail
- self.detail = format % (wait, wait != 1 and 's' or '')
self.wait = math.ceil(wait)
+ self.detail += ' ' + force_text(
+ self.extra_detail % {'wait': self.wait}
+ )
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 36afe7a9..ca9c479f 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -68,8 +68,8 @@ def get_attribute(instance, attrs):
return instance[attr]
except (KeyError, TypeError, AttributeError):
raise exc
- if is_simple_callable(instance):
- return instance()
+ if is_simple_callable(instance):
+ instance = instance()
return instance
@@ -181,6 +181,9 @@ class Field(object):
self.style = {} if style is None else style
self.allow_null = allow_null
+ if allow_null and self.default_empty_html is empty:
+ self.default_empty_html = None
+
if validators is not None:
self.validators = validators[:]
@@ -259,7 +262,11 @@ class Field(object):
if html.is_html_input(dictionary):
# HTML forms will represent empty fields as '', and cannot
# represent None or False values directly.
- ret = dictionary.get(self.field_name, '')
+ if self.field_name not in dictionary:
+ if getattr(self.root, 'partial', False):
+ return empty
+ return self.default_empty_html
+ ret = dictionary[self.field_name]
return self.default_empty_html if (ret == '') else ret
return dictionary.get(self.field_name, empty)
@@ -314,7 +321,6 @@ class Field(object):
value = self.to_internal_value(data)
self.run_validators(value)
- self.validate(value)
return value
def run_validators(self, value):
@@ -341,9 +347,6 @@ class Field(object):
if errors:
raise ValidationError(errors)
- def validate(self, value):
- pass
-
def to_internal_value(self, data):
"""
Transform the *incoming* primitive data into a native value.
@@ -495,6 +498,7 @@ class CharField(Field):
}
initial = ''
coerce_blank_to_null = False
+ default_empty_html = ''
def __init__(self, **kwargs):
self.allow_blank = kwargs.pop('allow_blank', False)
@@ -947,6 +951,8 @@ class ChoiceField(Field):
self.fail('invalid_choice', input=data)
def to_representation(self, value):
+ if value in ('', None):
+ return value
return self.choice_strings_to_values[six.text_type(value)]
diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py
index 90d3f2e0..de829d00 100644
--- a/rest_framework/metadata.py
+++ b/rest_framework/metadata.py
@@ -121,7 +121,10 @@ class SimpleMetadata(BaseMetadata):
if hasattr(field, 'choices'):
field_info['choices'] = [
- {'value': choice_value, 'display_name': choice_name}
+ {
+ 'value': choice_value,
+ 'display_name': force_text(choice_name, strings_only=True)
+ }
for choice_value, choice_name in field.choices.items()
]
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
index 79c8057b..d1ea497a 100644
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -49,6 +49,21 @@ class RelatedField(Field):
@classmethod
def many_init(cls, *args, **kwargs):
+ """
+ This method handles creating a parent `ManyRelatedField` instance
+ when the `many=True` keyword argument is passed.
+
+ Typically you won't need to override this method.
+
+ Note that we're over-cautious in passing most arguments to both parent
+ and child classes in order to try to cover the general case. If you're
+ overriding this method you'll probably want something much simpler, eg:
+
+ @classmethod
+ def many_init(cls, *args, **kwargs):
+ kwargs['child'] = cls()
+ return CustomManyRelatedField(*args, **kwargs)
+ """
list_kwargs = {'child_relation': cls(*args, **kwargs)}
for key in kwargs.keys():
if key in MANY_RELATION_KWARGS:
@@ -306,7 +321,9 @@ class ManyRelatedField(Field):
The `ManyRelatedField` class is responsible for handling iterating through
the values and passing each one to the child relationship.
- You shouldn't need to be using this class directly yourself.
+ This class is treated as private API.
+ You shouldn't generally need to be using this class directly yourself,
+ and should instead simply set 'many=True' on the relationship.
"""
initial = []
default_empty_html = []
diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py
index 6596fc44..e87d16d0 100644
--- a/rest_framework/renderers.py
+++ b/rest_framework/renderers.py
@@ -429,7 +429,10 @@ class HTMLFormRenderer(BaseRenderer):
style['base_template'] = self.base_template
style['renderer'] = self
- if 'template' in style:
+ # This API needs to be finessed and finalized for 3.1
+ if 'template' in renderer_context:
+ template_name = renderer_context['template']
+ elif 'template' in style:
template_name = style['template']
else:
template_name = style['template_pack'].strip('/') + '/' + style['base_template']
@@ -522,7 +525,10 @@ class BrowsableAPIRenderer(BaseRenderer):
else:
instance = None
- if request.method == method:
+ # If this is valid serializer data, and the form is for the same
+ # HTTP method as was used in the request then use the existing
+ # serializer instance, rather than dynamically creating a new one.
+ if request.method == method and serializer is not None:
try:
data = request.data
except ParseError:
@@ -555,7 +561,14 @@ class BrowsableAPIRenderer(BaseRenderer):
if data is not None:
serializer.is_valid()
form_renderer = self.form_renderer_class()
- return form_renderer.render(serializer.data, self.accepted_media_type, self.renderer_context)
+ return form_renderer.render(
+ serializer.data,
+ self.accepted_media_type,
+ dict(
+ list(self.renderer_context.items()) +
+ [('template', 'rest_framework/api_form.html')]
+ )
+ )
def get_raw_data_form(self, data, view, method, request):
"""
diff --git a/rest_framework/request.py b/rest_framework/request.py
index 096b3042..d7e74674 100644
--- a/rest_framework/request.py
+++ b/rest_framework/request.py
@@ -310,7 +310,7 @@ class Request(object):
def _load_data_and_files(self):
"""
- Parses the request content into self.DATA and self.FILES.
+ Parses the request content into `self.data`.
"""
if not _hasattr(self, '_content_type'):
self._load_method_and_content_type()
diff --git a/rest_framework/response.py b/rest_framework/response.py
index 0a7d313f..d6ca1aad 100644
--- a/rest_framework/response.py
+++ b/rest_framework/response.py
@@ -5,7 +5,6 @@ it is initialized with unrendered data, instead of a pre-rendered string.
The appropriate renderer is called during Django's template response rendering.
"""
from __future__ import unicode_literals
-import django
from django.core.handlers.wsgi import STATUS_CODE_TEXT
from django.template.response import SimpleTemplateResponse
from django.utils import six
@@ -16,9 +15,6 @@ class Response(SimpleTemplateResponse):
An HttpResponse that allows its data to be rendered into
arbitrary media types.
"""
- # TODO: remove that once Django 1.3 isn't supported
- if django.VERSION >= (1, 4):
- rendering_attrs = SimpleTemplateResponse.rendering_attrs + ['_closable_objects']
def __init__(self, data=None, status=None,
template_name=None, headers=None,
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index 84282cdb..f7aa3a7d 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -86,6 +86,15 @@ class BaseSerializer(Field):
class when `many=True` is used. You can customize it if you need to
control which keyword arguments are passed to the parent, and
which are passed to the child.
+
+ Note that we're over-cautious in passing most arguments to both parent
+ and child classes in order to try to cover the general case. If you're
+ overriding this method you'll probably want something much simpler, eg:
+
+ @classmethod
+ def many_init(cls, *args, **kwargs):
+ kwargs['child'] = cls()
+ return CustomListSerializer(*args, **kwargs)
"""
child_serializer = cls(*args, **kwargs)
list_kwargs = {'child': child_serializer}
@@ -93,7 +102,9 @@ class BaseSerializer(Field):
(key, value) for key, value in kwargs.items()
if key in LIST_SERIALIZER_KWARGS
]))
- return ListSerializer(*args, **list_kwargs)
+ meta = getattr(cls, 'Meta', None)
+ list_serializer_class = getattr(meta, 'list_serializer_class', ListSerializer)
+ return list_serializer_class(*args, **list_kwargs)
def to_internal_value(self, data):
raise NotImplementedError('`to_internal_value()` must be implemented.')
@@ -362,14 +373,9 @@ class Serializer(BaseSerializer):
for field in fields:
attribute = field.get_attribute(instance)
if attribute is None:
- value = None
+ ret[field.field_name] = None
else:
- value = field.to_representation(attribute)
- transform_method = getattr(self, 'transform_' + field.field_name, None)
- if transform_method is not None:
- value = transform_method(value)
-
- ret[field.field_name] = value
+ ret[field.field_name] = field.to_representation(attribute)
return ret
@@ -720,49 +726,62 @@ class ModelSerializer(Serializer):
# Determine if we need any additional `HiddenField` or extra keyword
# arguments to deal with `unique_for` dates that are required to
# be in the input data in order to validate it.
- unique_fields = {}
+ hidden_fields = {}
+ unique_constraint_names = set()
+
for model_field_name, field_name in model_field_mapping.items():
try:
model_field = model._meta.get_field(model_field_name)
except FieldDoesNotExist:
continue
- # Deal with each of the `unique_for_*` cases.
- for date_field_name in (
+ # Include each of the `unique_for_*` field names.
+ unique_constraint_names |= set([
model_field.unique_for_date,
model_field.unique_for_month,
model_field.unique_for_year
- ):
- if date_field_name is None:
- continue
-
- # Get the model field that is refered too.
- date_field = model._meta.get_field(date_field_name)
-
- if date_field.auto_now_add:
- default = CreateOnlyDefault(timezone.now)
- elif date_field.auto_now:
- default = timezone.now
- elif date_field.has_default():
- default = model_field.default
- else:
- default = empty
-
- if date_field_name in model_field_mapping:
- # The corresponding date field is present in the serializer
- if date_field_name not in extra_kwargs:
- extra_kwargs[date_field_name] = {}
- if default is empty:
- if 'required' not in extra_kwargs[date_field_name]:
- extra_kwargs[date_field_name]['required'] = True
- else:
- if 'default' not in extra_kwargs[date_field_name]:
- extra_kwargs[date_field_name]['default'] = default
+ ])
+
+ unique_constraint_names -= set([None])
+
+ # Include each of the `unique_together` field names,
+ # so long as all the field names are included on the serializer.
+ for parent_class in [model] + list(model._meta.parents.keys()):
+ for unique_together_list in parent_class._meta.unique_together:
+ if set(fields).issuperset(set(unique_together_list)):
+ unique_constraint_names |= set(unique_together_list)
+
+ # Now we have all the field names that have uniqueness constraints
+ # applied, we can add the extra 'required=...' or 'default=...'
+ # arguments that are appropriate to these fields, or add a `HiddenField` for it.
+ for unique_constraint_name in unique_constraint_names:
+ # Get the model field that is refered too.
+ unique_constraint_field = model._meta.get_field(unique_constraint_name)
+
+ if getattr(unique_constraint_field, 'auto_now_add', None):
+ default = CreateOnlyDefault(timezone.now)
+ elif getattr(unique_constraint_field, 'auto_now', None):
+ default = timezone.now
+ elif unique_constraint_field.has_default():
+ default = unique_constraint_field.default
+ else:
+ default = empty
+
+ if unique_constraint_name in model_field_mapping:
+ # The corresponding field is present in the serializer
+ if unique_constraint_name not in extra_kwargs:
+ extra_kwargs[unique_constraint_name] = {}
+ if default is empty:
+ if 'required' not in extra_kwargs[unique_constraint_name]:
+ extra_kwargs[unique_constraint_name]['required'] = True
else:
- # The corresponding date field is not present in the,
- # serializer. We have a default to use for the date, so
- # add in a hidden field that populates it.
- unique_fields[date_field_name] = HiddenField(default=default)
+ if 'default' not in extra_kwargs[unique_constraint_name]:
+ extra_kwargs[unique_constraint_name]['default'] = default
+ elif default is not empty:
+ # The corresponding field is not present in the,
+ # serializer. We have a default to use for it, so
+ # add in a hidden field that populates it.
+ hidden_fields[unique_constraint_name] = HiddenField(default=default)
# Now determine the fields that should be included on the serializer.
for field_name in fields:
@@ -838,12 +857,16 @@ class ModelSerializer(Serializer):
'validators', 'queryset'
]:
kwargs.pop(attr, None)
+
+ if extras.get('default') and kwargs.get('required') is False:
+ kwargs.pop('required')
+
kwargs.update(extras)
# Create the serializer field.
ret[field_name] = field_cls(**kwargs)
- for field_name, field in unique_fields.items():
+ for field_name, field in hidden_fields.items():
ret[field_name] = field
return ret
diff --git a/rest_framework/templates/rest_framework/api_form.html b/rest_framework/templates/rest_framework/api_form.html
new file mode 100644
index 00000000..96f924ed
--- /dev/null
+++ b/rest_framework/templates/rest_framework/api_form.html
@@ -0,0 +1,8 @@
+{% load rest_framework %}
+{% csrf_token %}
+{% for field in form %}
+ {% if not field.read_only %}
+ {% render_field field style=style %}
+ {% endif %}
+{% endfor %}
+<!-- form.non_field_errors -->
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index e9d99a65..e9668193 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -237,13 +237,6 @@
</div>
<!-- END Content -->
</div><!-- /.container -->
-
- <footer>
- {% block footer %}
- <p>Sponsored by <a href="http://dabapps.com/">DabApps</a>.</p>
- {% endblock %}
- </footer>
-
</div><!-- ./wrapper -->
{% block script %}
diff --git a/rest_framework/templates/rest_framework/horizontal/list_fieldset.html b/rest_framework/templates/rest_framework/horizontal/list_fieldset.html
index a30514c6..a9ff04a6 100644
--- a/rest_framework/templates/rest_framework/horizontal/list_fieldset.html
+++ b/rest_framework/templates/rest_framework/horizontal/list_fieldset.html
@@ -5,9 +5,12 @@
<legend class="control-label col-sm-2 {% if style.hide_label %}sr-only{% endif %}" style="border-bottom: 0">{{ field.label }}</legend>
</div>
{% endif %}
+ <!--
<ul>
{% for child in field.value %}
<li>TODO</li>
{% endfor %}
</ul>
+ -->
+ <p>Lists are not currently supported in HTML input.</p>
</fieldset>
diff --git a/rest_framework/templates/rest_framework/horizontal/select.html b/rest_framework/templates/rest_framework/horizontal/select.html
index 1d00f424..380b38e9 100644
--- a/rest_framework/templates/rest_framework/horizontal/select.html
+++ b/rest_framework/templates/rest_framework/horizontal/select.html
@@ -4,6 +4,9 @@
{% endif %}
<div class="col-sm-10">
<select class="form-control" name="{{ field.name }}">
+ {% if field.allow_null %}
+ <option value="" {% if not field.value %}selected{% endif %}>--------</option>
+ {% endif %}
{% for key, text in field.choices.items %}
<option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option>
{% endfor %}
diff --git a/rest_framework/templates/rest_framework/inline/list_fieldset.html b/rest_framework/templates/rest_framework/inline/list_fieldset.html
new file mode 100644
index 00000000..2ae56d7c
--- /dev/null
+++ b/rest_framework/templates/rest_framework/inline/list_fieldset.html
@@ -0,0 +1 @@
+<span>Lists are not currently supported in HTML input.</span>
diff --git a/rest_framework/templates/rest_framework/inline/select.html b/rest_framework/templates/rest_framework/inline/select.html
index e9fcebb4..53af2772 100644
--- a/rest_framework/templates/rest_framework/inline/select.html
+++ b/rest_framework/templates/rest_framework/inline/select.html
@@ -3,8 +3,11 @@
<label class="sr-only">{{ field.label }}</label>
{% endif %}
<select class="form-control" name="{{ field.name }}">
+ {% if field.allow_null %}
+ <option value="" {% if not field.value %}selected{% endif %}>--------</option>
+ {% endif %}
{% for key, text in field.choices.items %}
- <option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option>
+ <option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option>
{% endfor %}
</select>
</div>
diff --git a/rest_framework/templates/rest_framework/login_base.html b/rest_framework/templates/rest_framework/login_base.html
index 8ab682ac..e050cbdc 100644
--- a/rest_framework/templates/rest_framework/login_base.html
+++ b/rest_framework/templates/rest_framework/login_base.html
@@ -22,7 +22,7 @@
<div id="div_id_username"
class="clearfix control-group {% if form.username.errors %}error{% endif %}">
<div class="controls">
- <Label class="span4">Username:</label>
+ <label class="span4">Username:</label>
<input style="height: 25px" type="text" name="username" maxlength="100"
autocapitalize="off"
autocorrect="off" class="span12 textinput textInput"
@@ -36,9 +36,10 @@
</div>
</div>
<div id="div_id_password"
- class="clearfix control-group {% if form.password.errors %}error{% endif %}">
+ class="clearfix control-group {% if form.password.errors %}error{% endif %}"
+ style="margin-top: 10px">
<div class="controls">
- <Label class="span4">Password:</label>
+ <label class="span4">Password:</label>
<input style="height: 25px" type="password" name="password" maxlength="100"
autocapitalize="off" autocorrect="off" class="span12 textinput textInput"
id="id_password" required>
@@ -55,7 +56,7 @@
<div class="well well-small text-error" style="border: none">{{ error }}</div>
{% endfor %}
{% endif %}
- <div class="form-actions-no-box">
+ <div class="form-actions-no-box" style="margin-top: 20px">
<input type="submit" name="submit" value="Log in" class="btn btn-primary" id="submit-id-submit">
</div>
</form>
diff --git a/rest_framework/templates/rest_framework/vertical/list_fieldset.html b/rest_framework/templates/rest_framework/vertical/list_fieldset.html
index 74bbf448..1d86c7f2 100644
--- a/rest_framework/templates/rest_framework/vertical/list_fieldset.html
+++ b/rest_framework/templates/rest_framework/vertical/list_fieldset.html
@@ -4,4 +4,5 @@
{% 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/templates/rest_framework/vertical/select.html b/rest_framework/templates/rest_framework/vertical/select.html
index 7c673ebb..de72e1dd 100644
--- a/rest_framework/templates/rest_framework/vertical/select.html
+++ b/rest_framework/templates/rest_framework/vertical/select.html
@@ -3,8 +3,11 @@
<label {% if style.hide_label %}class="sr-only"{% endif %}>{{ field.label }}</label>
{% endif %}
<select class="form-control" name="{{ field.name }}">
+ {% if field.allow_null %}
+ <option value="" {% if not field.value %}selected{% endif %}>--------</option>
+ {% endif %}
{% for key, text in field.choices.items %}
- <option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option>
+ <option value="{{ key }}" {% if key == field.value %}selected{% endif %}>{{ text }}</option>
{% endfor %}
</select>
{% if field.errors %}
diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py
index 82361edf..c98725c6 100644
--- a/rest_framework/utils/model_meta.py
+++ b/rest_framework/utils/model_meta.py
@@ -6,6 +6,7 @@ relationships and their associated metadata.
Usage: `get_field_info(model)` returns a `FieldInfo` instance.
"""
from collections import namedtuple
+from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.utils import six
from rest_framework.compat import OrderedDict
@@ -43,7 +44,11 @@ def _resolve_model(obj):
"""
if isinstance(obj, six.string_types) and len(obj.split('.')) == 2:
app_name, model_name = obj.split('.')
- return models.get_model(app_name, model_name)
+ resolved_model = models.get_model(app_name, model_name)
+ if resolved_model is None:
+ msg = "Django did not return a model for {0}.{1}"
+ raise ImproperlyConfigured(msg.format(app_name, model_name))
+ return resolved_model
elif inspect.isclass(obj) and issubclass(obj, models.Model):
return obj
raise ValueError("{0} is not a Django model".format(obj))
diff --git a/rest_framework/validators.py b/rest_framework/validators.py
index fa4f1847..7ca4e6a9 100644
--- a/rest_framework/validators.py
+++ b/rest_framework/validators.py
@@ -93,6 +93,9 @@ class UniqueTogetherValidator:
The `UniqueTogetherValidator` always forces an implied 'required'
state on the fields it applies to.
"""
+ if self.instance is not None:
+ return
+
missing = dict([
(field_name, self.missing_message)
for field_name in self.fields
@@ -105,8 +108,17 @@ class UniqueTogetherValidator:
"""
Filter the queryset to all instances matching the given attributes.
"""
+ # If this is an update, then any unprovided field should
+ # have it's value set based on the existing instance attribute.
+ if self.instance is not None:
+ for field_name in self.fields:
+ if field_name not in attrs:
+ attrs[field_name] = getattr(self.instance, field_name)
+
+ # Determine the filter keyword arguments and filter the queryset.
filter_kwargs = dict([
- (field_name, attrs[field_name]) for field_name in self.fields
+ (field_name, attrs[field_name])
+ for field_name in self.fields
])
return queryset.filter(**filter_kwargs)