diff options
| author | Carlton Gibson | 2014-12-01 11:22:39 +0100 |
|---|---|---|
| committer | Carlton Gibson | 2014-12-01 11:22:39 +0100 |
| commit | ef26f43de4a0c9ac3081c06a383b5d3d4d007797 (patch) | |
| tree | bf6abcd15f0e58f4fa79a83cd4e051b8987dd311 /rest_framework | |
| parent | c50a42bddc66e28d624cd3caadd2d63502ac2e6e (diff) | |
| parent | 72c4ec4e189796e506655e275cd9c77abe98e1b9 (diff) | |
| download | django-rest-framework-ef26f43de4a0c9ac3081c06a383b5d3d4d007797.tar.bz2 | |
Merge branch 'master' of github.com:tomchristie/django-rest-framework
Diffstat (limited to 'rest_framework')
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) |
