diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f3ff22334..c794963e1 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -689,10 +689,10 @@ class DateTimeField(Field): return value if self.format.lower() == ISO_8601: - ret = value.isoformat() - if ret.endswith('+00:00'): - ret = ret[:-6] + 'Z' - return ret + value = value.isoformat() + if value.endswith('+00:00'): + value = value[:-6] + 'Z' + return value return value.strftime(self.format) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 8c1356727..988b9ede4 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -127,7 +127,7 @@ class HyperlinkedRelatedField(RelatedField): attributes are not configured to correctly match the URL conf. """ # Unsaved objects will not yet have a valid URL. - if obj.pk is None: + if obj.pk: return None lookup_value = getattr(obj, self.lookup_field) @@ -248,11 +248,13 @@ class ManyRelation(Field): You shouldn't need to be using this class directly yourself. """ + initial = [] def __init__(self, child_relation=None, *args, **kwargs): self.child_relation = child_relation assert child_relation is not None, '`child_relation` is a required argument.' super(ManyRelation, self).__init__(*args, **kwargs) + self.child_relation.bind(field_name='', parent=self) def to_internal_value(self, data): return [ diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 297c60d8f..931dd434f 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -377,23 +377,21 @@ class HTMLFormRenderer(BaseRenderer): serializers.TimeField: 'time', }) - def render_field(self, field, value, errors, layout=None): + def render_field(self, field, layout=None): layout = layout or 'vertical' style_type = field.style.get('type', 'default') if style_type == 'textarea' and layout == 'inline': style_type = 'default' input_type = self.input_type[field] - if input_type == 'datetime-local': - value = value.rstrip('Z') + if input_type == 'datetime-local' and isinstance(field.value, six.text_type): + field.value = field.value.rstrip('Z') base = self.field_templates[field][style_type] template_name = 'rest_framework/fields/' + layout + '/' + base template = loader.get_template(template_name) context = Context({ 'field': field, - 'value': value, - 'errors': errors, 'input_type': input_type }) @@ -408,7 +406,7 @@ class HTMLFormRenderer(BaseRenderer): template = loader.get_template(self.template) context = RequestContext(request, { - 'form': data, + 'form': data.serializer, 'layout': getattr(getattr(data, 'Meta', None), 'layout', 'horizontal'), 'renderer': self }) @@ -479,27 +477,29 @@ class BrowsableAPIRenderer(BaseRenderer): return False # Doesn't have permissions return True - def get_rendered_html_form(self, view, method, request): + def get_rendered_html_form(self, data, view, method, request): """ Return a string representing a rendered HTML form, possibly bound to either the input or output data. In the absence of the View having an associated form then return None. """ + serializer = getattr(data, 'serializer', None) + if serializer and not getattr(serializer, 'many', False): + instance = getattr(serializer, 'instance', None) + else: + instance = None + if request.method == method: try: data = request.data - # files = request.FILES except ParseError: data = None - # files = None else: data = None - # files = None with override_method(view, request, method) as request: - obj = getattr(view, 'object', None) - if not self.show_form_for_method(view, method, request, obj): + if not self.show_form_for_method(view, method, request, instance): return if method in ('DELETE', 'OPTIONS'): @@ -511,19 +511,24 @@ class BrowsableAPIRenderer(BaseRenderer): ): return - serializer = view.get_serializer(instance=obj, data=data) - serializer.is_valid() - data = serializer.data - + serializer = view.get_serializer(instance=instance, data=data) + if data is not None: + serializer.is_valid() form_renderer = self.form_renderer_class() - return form_renderer.render(data, self.accepted_media_type, self.renderer_context) + return form_renderer.render(serializer.data, self.accepted_media_type, self.renderer_context) - def get_raw_data_form(self, view, method, request): + def get_raw_data_form(self, data, view, method, request): """ Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms. (Which are typically application/x-www-form-urlencoded) """ + serializer = getattr(data, 'serializer', None) + if serializer and not getattr(serializer, 'many', False): + instance = getattr(serializer, 'instance', None) + else: + instance = None + with override_method(view, request, method) as request: # If we're not using content overloading there's no point in # supplying a generic form, as the view won't treat the form's @@ -533,8 +538,7 @@ class BrowsableAPIRenderer(BaseRenderer): return None # Check permissions - obj = getattr(view, 'object', None) - if not self.show_form_for_method(view, method, request, obj): + if not self.show_form_for_method(view, method, request, instance): return # If possible, serialize the initial content for the generic form @@ -545,8 +549,8 @@ class BrowsableAPIRenderer(BaseRenderer): # corresponding renderer that can be used to render the data. # Get a read-only version of the serializer - serializer = view.get_serializer(instance=obj) - if obj is None: + serializer = view.get_serializer(instance=instance) + if instance is None: for name, field in serializer.fields.items(): if getattr(field, 'read_only', None): del serializer.fields[name] @@ -606,9 +610,9 @@ class BrowsableAPIRenderer(BaseRenderer): renderer = self.get_default_renderer(view) - raw_data_post_form = self.get_raw_data_form(view, 'POST', request) - raw_data_put_form = self.get_raw_data_form(view, 'PUT', request) - raw_data_patch_form = self.get_raw_data_form(view, 'PATCH', request) + raw_data_post_form = self.get_raw_data_form(data, view, 'POST', request) + raw_data_put_form = self.get_raw_data_form(data, view, 'PUT', request) + raw_data_patch_form = self.get_raw_data_form(data, view, 'PATCH', request) raw_data_put_or_patch_form = raw_data_put_form or raw_data_patch_form response_headers = dict(response.items()) @@ -632,10 +636,10 @@ class BrowsableAPIRenderer(BaseRenderer): 'available_formats': [renderer_cls.format for renderer_cls in view.renderer_classes], 'response_headers': response_headers, - # 'put_form': self.get_rendered_html_form(view, 'PUT', request), - # 'post_form': self.get_rendered_html_form(view, 'POST', request), - # 'delete_form': self.get_rendered_html_form(view, 'DELETE', request), - # 'options_form': self.get_rendered_html_form(view, 'OPTIONS', request), + 'put_form': self.get_rendered_html_form(data, view, 'PUT', request), + 'post_form': self.get_rendered_html_form(data, view, 'POST', request), + 'delete_form': self.get_rendered_html_form(data, view, 'DELETE', request), + 'options_form': self.get_rendered_html_form(data, view, 'OPTIONS', request), 'raw_data_put_form': raw_data_put_form, 'raw_data_post_form': raw_data_post_form, diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5da812477..0f24ed402 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -14,7 +14,6 @@ from django.core.exceptions import ImproperlyConfigured, ValidationError from django.db import models from django.utils import six from django.utils.datastructures import SortedDict -from collections import namedtuple from rest_framework.fields import empty, set_value, Field, SkipField from rest_framework.settings import api_settings from rest_framework.utils import html, model_meta, representation @@ -38,8 +37,8 @@ from rest_framework.relations import * # NOQA from rest_framework.fields import * # NOQA -FieldResult = namedtuple('FieldResult', ['field', 'value', 'error']) - +# BaseSerializer +# -------------- class BaseSerializer(Field): """ @@ -113,11 +112,6 @@ class BaseSerializer(Field): if not hasattr(self, '_data'): if self.instance is not None: self._data = self.to_representation(self.instance) - elif self._initial_data is not None: - self._data = dict([ - (field_name, field.get_value(self._initial_data)) - for field_name, field in self.fields.items() - ]) else: self._data = self.get_initial() return self._data @@ -137,6 +131,79 @@ class BaseSerializer(Field): return self._validated_data +# Serializer & ListSerializer classes +# ----------------------------------- + +class ReturnDict(SortedDict): + """ + Return object from `serialier.data` for the `Serializer` class. + Includes a backlink to the serializer instance for renderers + to use if they need richer field information. + """ + def __init__(self, *args, **kwargs): + self.serializer = kwargs.pop('serializer') + super(ReturnDict, self).__init__(*args, **kwargs) + + +class ReturnList(list): + """ + Return object from `serialier.data` for the `SerializerList` class. + Includes a backlink to the serializer instance for renderers + to use if they need richer field information. + """ + def __init__(self, *args, **kwargs): + self.serializer = kwargs.pop('serializer') + super(ReturnList, self).__init__(*args, **kwargs) + + +class BoundField(object): + """ + A field object that also includes `.value` and `.error` properties. + Returned when iterating over a serializer instance, + providing an API similar to Django forms and form fields. + """ + def __init__(self, field, value, errors): + self._field = field + self.value = value + self.errors = errors + + def __getattr__(self, attr_name): + return getattr(self._field, attr_name) + + @property + def _proxy_class(self): + return self._field.__class__ + + +class BindingDict(object): + """ + This dict-like object is used to store fields on a serializer. + + This ensures that whenever fields are added to the serializer we call + `field.bind()` so that the `field_name` and `parent` attributes + can be set correctly. + """ + def __init__(self, serializer): + self.serializer = serializer + self.fields = SortedDict() + + def __setitem__(self, key, field): + self.fields[key] = field + field.bind(field_name=key, parent=self.serializer) + + def __getitem__(self, key): + return self.fields[key] + + def __delitem__(self, key): + del self.fields[key] + + def items(self): + return self.fields.items() + + def values(self): + return self.fields.values() + + class SerializerMetaclass(type): """ This metaclass sets a dictionary named `base_fields` on the class. @@ -167,35 +234,6 @@ class SerializerMetaclass(type): return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs) -class BindingDict(object): - """ - This dict-like object is used to store fields on a serializer. - - This ensures that whenever fields are added to the serializer we call - `field.bind()` so that the `field_name` and `parent` attributes - can be set correctly. - """ - def __init__(self, serializer): - self.serializer = serializer - self.fields = SortedDict() - - def __setitem__(self, key, field): - self.fields[key] = field - field.bind(field_name=key, parent=self.serializer) - - def __getitem__(self, key): - return self.fields[key] - - def __delitem__(self, key): - del self.fields[key] - - def items(self): - return self.fields.items() - - def values(self): - return self.fields.values() - - @six.add_metaclass(SerializerMetaclass) class Serializer(BaseSerializer): def __init__(self, *args, **kwargs): @@ -212,10 +250,18 @@ class Serializer(BaseSerializer): return copy.deepcopy(self._declared_fields) def get_initial(self): - return dict([ + if self._initial_data is not None: + return ReturnDict([ + (field_name, field.get_value(self._initial_data)) + for field_name, field in self.fields.items() + ], serializer=self) + #return self.to_representation(self._initial_data) + + return ReturnDict([ (field.field_name, field.get_initial()) for field in self.fields.values() - ]) + if not field.write_only + ], serializer=self) def get_value(self, dictionary): # We override the default field access in order to support @@ -288,7 +334,7 @@ class Serializer(BaseSerializer): """ Object instance -> Dict of primitive datatypes. """ - ret = SortedDict() + ret = ReturnDict(serializer=self) fields = [field for field in self.fields.values() if not field.write_only] for field in fields: @@ -302,11 +348,9 @@ class Serializer(BaseSerializer): def __iter__(self): errors = self.errors if hasattr(self, '_errors') else {} for field in self.fields.values(): - if field.read_only: - continue value = self.data.get(field.field_name) if self.data else None error = errors.get(field.field_name) - yield FieldResult(field, value, error) + yield BoundField(field, value, error) def __repr__(self): return representation.serializer_repr(self, indent=1) @@ -317,7 +361,7 @@ class Serializer(BaseSerializer): class ListSerializer(BaseSerializer): child = None - initial = [] + many = True def __init__(self, *args, **kwargs): self.child = kwargs.pop('child', copy.deepcopy(self.child)) @@ -326,6 +370,11 @@ class ListSerializer(BaseSerializer): super(ListSerializer, self).__init__(*args, **kwargs) self.child.bind(field_name='', parent=self) + def get_initial(self): + if self._initial_data is not None: + return self.to_representation(self._initial_data) + return ReturnList(serializer=self) + def get_value(self, dictionary): # We override the default field access in order to support # lists in HTML forms. @@ -345,7 +394,10 @@ class ListSerializer(BaseSerializer): """ List of object instances -> List of dicts of primitive datatypes. """ - return [self.child.to_representation(item) for item in data] + return ReturnList( + [self.child.to_representation(item) for item in data], + serializer=self + ) def create(self, attrs_list): return [self.child.create(attrs) for attrs in attrs_list] @@ -354,6 +406,9 @@ class ListSerializer(BaseSerializer): return representation.list_repr(self, indent=1) +# ModelSerializer & HyperlinkedModelSerializer +# -------------------------------------------- + class ModelSerializer(Serializer): _field_mapping = ClassLookupDict({ models.AutoField: IntegerField, diff --git a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css index 6fa1e6cb5..84389b1d1 100644 --- a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css +++ b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css @@ -10,6 +10,12 @@ a single block in the template. background: transparent; border-top-color: transparent; padding-top: 0; + text-align: right; +} + +#generic-content-form textarea { + font-family:Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace; + font-size: 80%; } .navbar-inverse .brand a { @@ -29,7 +35,7 @@ a single block in the template. z-index: 3; } -.navbar .navbar-inner { +.navbar { background: #2C2C2C; color: white; border: none; @@ -37,7 +43,7 @@ a single block in the template. border-radius: 0px; } -.navbar .navbar-inner .nav li, .navbar .navbar-inner .nav li a, .navbar .navbar-inner .brand:hover { +.navbar .nav li, .navbar .nav li a, .navbar .brand:hover { color: white; } @@ -45,11 +51,11 @@ a single block in the template. background: #2C2C2C; } -.navbar .navbar-inner .dropdown-menu li a, .navbar .navbar-inner .dropdown-menu li { +.navbar .dropdown-menu li a, .navbar .dropdown-menu li { color: #A30000; } -.navbar .navbar-inner .dropdown-menu li a:hover { +.navbar .dropdown-menu li a:hover { background: #EEEEEE; color: #C20000; } @@ -61,10 +67,10 @@ html { background: none; } -body, .navbar .navbar-inner .container-fluid { +/*body, .navbar .container-fluid { max-width: 1150px; margin: 0 auto; -} +}*/ body { background: url("../img/grid.png") repeat-x; diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index a84ccf269..2e03dd987 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -15,7 +15,8 @@ {% block style %} {% block bootstrap_theme %} - + + {% endblock %} @@ -26,44 +27,42 @@ {% block body %} -
+{{ field.help_text }}
{% endif %}{{ field.help_text }}
{% endif %}