From 4b691c402707775c3048a90531024f3bc5be6f91 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 20 Sep 2012 13:06:27 +0100 Subject: Change package name: djangorestframework -> rest_framework --- rest_framework/renderers.py | 402 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 402 insertions(+) create mode 100644 rest_framework/renderers.py (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py new file mode 100644 index 00000000..66394af3 --- /dev/null +++ b/rest_framework/renderers.py @@ -0,0 +1,402 @@ +""" +Renderers are used to serialize a View's output into specific media types. + +Django REST framework also provides HTML and PlainText renderers that help self-document the API, +by serializing the output along with documentation regarding the View, output status and headers, +and providing forms and links depending on the allowed methods, renderers and parsers on the View. +""" +from django import forms +from django.template import RequestContext, loader +from django.utils import simplejson as json + +from rest_framework.compat import yaml +from rest_framework.settings import api_settings +from rest_framework.utils import dict2xml +from rest_framework.utils import encoders +from rest_framework.utils.breadcrumbs import get_breadcrumbs +from rest_framework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches +from rest_framework import VERSION +from rest_framework.fields import FloatField, IntegerField, DateTimeField, DateField, EmailField, CharField, BooleanField + +import string + + +__all__ = ( + 'BaseRenderer', + 'TemplateRenderer', + 'JSONRenderer', + 'JSONPRenderer', + 'DocumentingHTMLRenderer', + 'DocumentingXHTMLRenderer', + 'DocumentingPlainTextRenderer', + 'XMLRenderer', + 'YAMLRenderer' +) + + +class BaseRenderer(object): + """ + All renderers must extend this class, set the :attr:`media_type` attribute, + and override the :meth:`render` method. + """ + + _FORMAT_QUERY_PARAM = 'format' + + media_type = None + format = None + + def __init__(self, view=None): + self.view = view + + def can_handle_format(self, format): + return format == self.format + + def can_handle_media_type(self, media_type): + """ + Returns `True` if this renderer is able to deal with the given + media type. + + The default implementation for this function is to check the media type + argument against the media_type attribute set on the class to see if + they match. + + This may be overridden to provide for other behavior, but typically + you'll instead want to just set the `media_type` attribute on the class. + """ + return media_type_matches(self.media_type, media_type) + + def render(self, obj=None, media_type=None): + """ + Given an object render it into a string. + + The requested media type is also passed to this method, + as it may contain parameters relevant to how the parser + should render the output. + EG: ``application/json; indent=4`` + + By default render simply returns the output as-is. + Override this method to provide for other behavior. + """ + if obj is None: + return '' + + return str(obj) + + +class JSONRenderer(BaseRenderer): + """ + Renderer which serializes to JSON + """ + + media_type = 'application/json' + format = 'json' + encoder_class = encoders.JSONEncoder + + def render(self, obj=None, media_type=None): + """ + Renders *obj* into serialized JSON. + """ + if obj is None: + return '' + + # If the media type looks like 'application/json; indent=4', then + # pretty print the result. + indent = get_media_type_params(media_type).get('indent', None) + sort_keys = False + try: + indent = max(min(int(indent), 8), 0) + sort_keys = True + except (ValueError, TypeError): + indent = None + + return json.dumps(obj, cls=self.encoder_class, indent=indent, sort_keys=sort_keys) + + +class JSONPRenderer(JSONRenderer): + """ + Renderer which serializes to JSONP + """ + + media_type = 'application/javascript' + format = 'jsonp' + renderer_class = JSONRenderer + callback_parameter = 'callback' + + def _get_callback(self): + return self.view.request.GET.get(self.callback_parameter, self.callback_parameter) + + def _get_renderer(self): + return self.renderer_class(self.view) + + def render(self, obj=None, media_type=None): + callback = self._get_callback() + json = self._get_renderer().render(obj, media_type) + return "%s(%s);" % (callback, json) + + +class XMLRenderer(BaseRenderer): + """ + Renderer which serializes to XML. + """ + + media_type = 'application/xml' + format = 'xml' + + def render(self, obj=None, media_type=None): + """ + Renders *obj* into serialized XML. + """ + if obj is None: + return '' + return dict2xml(obj) + + +class YAMLRenderer(BaseRenderer): + """ + Renderer which serializes to YAML. + """ + + media_type = 'application/yaml' + format = 'yaml' + + def render(self, obj=None, media_type=None): + """ + Renders *obj* into serialized YAML. + """ + if obj is None: + return '' + + return yaml.safe_dump(obj) + + +class TemplateRenderer(BaseRenderer): + """ + A Base class provided for convenience. + + Render the object simply by using the given template. + To create a template renderer, subclass this class, and set + the :attr:`media_type` and :attr:`template` attributes. + """ + + media_type = None + template = None + + def render(self, obj=None, media_type=None): + """ + Renders *obj* using the :attr:`template` specified on the class. + """ + if obj is None: + return '' + + template = loader.get_template(self.template) + context = RequestContext(self.view.request, {'object': obj}) + return template.render(context) + + +class DocumentingTemplateRenderer(BaseRenderer): + """ + Base class for renderers used to self-document the API. + Implementing classes should extend this class and set the template attribute. + """ + + template = None + + def _get_content(self, view, request, obj, media_type): + """ + Get the content as if it had been rendered by a non-documenting renderer. + + (Typically this will be the content as it would have been if the Resource had been + requested with an 'Accept: */*' header, although with verbose style formatting if appropriate.) + """ + + # Find the first valid renderer and render the content. (Don't use another documenting renderer.) + renderers = [renderer for renderer in view.renderer_classes + if not issubclass(renderer, DocumentingTemplateRenderer)] + if not renderers: + return '[No renderers were found]' + + media_type = add_media_type_param(media_type, 'indent', '4') + content = renderers[0](view).render(obj, media_type) + if not all(char in string.printable for char in content): + return '[%d bytes of binary content]' + + return content + + def _get_form_instance(self, view, method): + """ + Get a form, possibly bound to either the input or output data. + In the absence on of the Resource having an associated form then + provide a form that can be used to submit arbitrary content. + """ + if not hasattr(self.view, 'get_serializer'): # No serializer, no form. + return + # We need to map our Fields to Django's Fields. + field_mapping = dict([ + [FloatField.__name__, forms.FloatField], + [IntegerField.__name__, forms.IntegerField], + [DateTimeField.__name__, forms.DateTimeField], + [DateField.__name__, forms.DateField], + [EmailField.__name__, forms.EmailField], + [CharField.__name__, forms.CharField], + [BooleanField.__name__, forms.BooleanField] + ]) + + # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python + fields = {} + object, data = None, None + if hasattr(self.view, 'object'): + object = self.view.object + serializer = self.view.get_serializer(instance=object) + for k, v in serializer.fields.items(): + fields[k] = field_mapping[v.__class__.__name__]() + OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) + if object and not self.view.request.method == 'DELETE': # Don't fill in the form when the object is deleted + data = serializer.data + form_instance = OnTheFlyForm(data) + return form_instance + + def _get_generic_content_form(self, view): + """ + Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms + (Which are typically application/x-www-form-urlencoded) + """ + + # 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 value as the content of the request. + if not getattr(view.request, '_USE_FORM_OVERLOADING', False): + return None + + # NB. http://jacobian.org/writing/dynamic-form-generation/ + class GenericContentForm(forms.Form): + def __init__(self, view, request): + """We don't know the names of the fields we want to set until the point the form is instantiated, + as they are determined by the Resource the form is being created against. + Add the fields dynamically.""" + super(GenericContentForm, self).__init__() + + contenttype_choices = [(media_type, media_type) for media_type in view._parsed_media_types] + initial_contenttype = view._default_parser.media_type + + self.fields[request._CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type', + choices=contenttype_choices, + initial=initial_contenttype) + self.fields[request._CONTENT_PARAM] = forms.CharField(label='Content', + widget=forms.Textarea) + + # If either of these reserved parameters are turned off then content tunneling is not possible + if self.view.request._CONTENTTYPE_PARAM is None or self.view.request._CONTENT_PARAM is None: + return None + + # Okey doke, let's do it + return GenericContentForm(view, view.request) + + def get_name(self): + try: + return self.view.get_name() + except AttributeError: + return self.view.__doc__ + + def get_description(self, html=None): + if html is None: + html = bool('html' in self.format) + try: + return self.view.get_description(html) + except AttributeError: + return self.view.__doc__ + + def render(self, obj=None, media_type=None): + """ + Renders *obj* using the :attr:`template` set on the class. + + The context used in the template contains all the information + needed to self-document the response to this request. + """ + + content = self._get_content(self.view, self.view.request, obj, media_type) + + put_form_instance = self._get_form_instance(self.view, 'put') + post_form_instance = self._get_form_instance(self.view, 'post') + + name = self.get_name() + description = self.get_description() + + breadcrumb_list = get_breadcrumbs(self.view.request.path) + + template = loader.get_template(self.template) + context = RequestContext(self.view.request, { + 'content': content, + 'view': self.view, + 'request': self.view.request, + 'response': self.view.response, + 'description': description, + 'name': name, + 'version': VERSION, + 'breadcrumblist': breadcrumb_list, + 'allowed_methods': self.view.allowed_methods, + 'available_formats': self.view._rendered_formats, + 'put_form': put_form_instance, + 'post_form': post_form_instance, + 'FORMAT_PARAM': self._FORMAT_QUERY_PARAM, + 'METHOD_PARAM': getattr(self.view, '_METHOD_PARAM', None), + 'api_settings': api_settings + }) + + ret = template.render(context) + + # Munge DELETE Response code to allow us to return content + # (Do this *after* we've rendered the template so that we include + # the normal deletion response code in the output) + if self.view.response.status_code == 204: + self.view.response.status_code = 200 + + return ret + + +class DocumentingHTMLRenderer(DocumentingTemplateRenderer): + """ + Renderer which provides a browsable HTML interface for an API. + See the examples at http://api.django-rest-framework.org to see this in action. + """ + + media_type = 'text/html' + format = 'html' + template = 'rest_framework/api.html' + + +class DocumentingXHTMLRenderer(DocumentingTemplateRenderer): + """ + Identical to DocumentingHTMLRenderer, except with an xhtml media type. + We need this to be listed in preference to xml in order to return HTML to WebKit based browsers, + given their Accept headers. + """ + + media_type = 'application/xhtml+xml' + format = 'xhtml' + template = 'rest_framework/api.html' + + +class DocumentingPlainTextRenderer(DocumentingTemplateRenderer): + """ + Renderer that serializes the object with the default renderer, but also provides plain-text + documentation of the returned status and headers, and of the resource's name and description. + Useful for browsing an API with command line tools. + """ + + media_type = 'text/plain' + format = 'txt' + template = 'rest_framework/api.txt' + + +DEFAULT_RENDERERS = ( + JSONRenderer, + JSONPRenderer, + DocumentingHTMLRenderer, + DocumentingXHTMLRenderer, + DocumentingPlainTextRenderer, + XMLRenderer +) + +if yaml: + DEFAULT_RENDERERS += (YAMLRenderer, ) +else: + YAMLRenderer = None -- cgit v1.2.3 From 7efc6e820580fde5a38cbee5cec05cbc87ce36e4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 20 Sep 2012 13:22:07 +0100 Subject: Remove dumbass __all__ variables --- rest_framework/renderers.py | 13 ------------- 1 file changed, 13 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 66394af3..a00ca0a5 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -21,19 +21,6 @@ from rest_framework.fields import FloatField, IntegerField, DateTimeField, DateF import string -__all__ = ( - 'BaseRenderer', - 'TemplateRenderer', - 'JSONRenderer', - 'JSONPRenderer', - 'DocumentingHTMLRenderer', - 'DocumentingXHTMLRenderer', - 'DocumentingPlainTextRenderer', - 'XMLRenderer', - 'YAMLRenderer' -) - - class BaseRenderer(object): """ All renderers must extend this class, set the :attr:`media_type` attribute, -- cgit v1.2.3 From f4670c89969503919cd597529f19174e67acd388 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 20 Sep 2012 14:00:53 +0100 Subject: Hack out bunch of unneccesary private methods on View class --- rest_framework/renderers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index a00ca0a5..dd9228d8 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -261,8 +261,9 @@ class DocumentingTemplateRenderer(BaseRenderer): Add the fields dynamically.""" super(GenericContentForm, self).__init__() - contenttype_choices = [(media_type, media_type) for media_type in view._parsed_media_types] - initial_contenttype = view._default_parser.media_type + parsed_media_types = [parser.media_type for parser in view.parser_classes] + contenttype_choices = [(media_type, media_type) for media_type in parsed_media_types] + initial_contenttype = parsed_media_types[0] self.fields[request._CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type', choices=contenttype_choices, @@ -320,7 +321,7 @@ class DocumentingTemplateRenderer(BaseRenderer): 'version': VERSION, 'breadcrumblist': breadcrumb_list, 'allowed_methods': self.view.allowed_methods, - 'available_formats': self.view._rendered_formats, + 'available_formats': [renderer.format for renderer in self.view.renderer_classes], 'put_form': put_form_instance, 'post_form': post_form_instance, 'FORMAT_PARAM': self._FORMAT_QUERY_PARAM, -- cgit v1.2.3 From d9cba6398e2323b8d8cd34f791442528517e01b4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 20 Sep 2012 17:44:34 +0100 Subject: Clean up bits of templates etc --- rest_framework/renderers.py | 130 +++++++++++++++----------------------------- 1 file changed, 43 insertions(+), 87 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index dd9228d8..58bd11f0 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -5,10 +5,10 @@ Django REST framework also provides HTML and PlainText renderers that help self- by serializing the output along with documentation regarding the View, output status and headers, and providing forms and links depending on the allowed methods, renderers and parsers on the View. """ +import string from django import forms from django.template import RequestContext, loader from django.utils import simplejson as json - from rest_framework.compat import yaml from rest_framework.settings import api_settings from rest_framework.utils import dict2xml @@ -16,9 +16,7 @@ from rest_framework.utils import encoders from rest_framework.utils.breadcrumbs import get_breadcrumbs from rest_framework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches from rest_framework import VERSION -from rest_framework.fields import FloatField, IntegerField, DateTimeField, DateField, EmailField, CharField, BooleanField - -import string +from rest_framework import serializers class BaseRenderer(object): @@ -27,8 +25,6 @@ class BaseRenderer(object): and override the :meth:`render` method. """ - _FORMAT_QUERY_PARAM = 'format' - media_type = None format = None @@ -72,7 +68,7 @@ class BaseRenderer(object): class JSONRenderer(BaseRenderer): """ - Renderer which serializes to JSON + Renderer which serializes to json. """ media_type = 'application/json' @@ -81,7 +77,7 @@ class JSONRenderer(BaseRenderer): def render(self, obj=None, media_type=None): """ - Renders *obj* into serialized JSON. + Render `obj` into json. """ if obj is None: return '' @@ -96,28 +92,37 @@ class JSONRenderer(BaseRenderer): except (ValueError, TypeError): indent = None - return json.dumps(obj, cls=self.encoder_class, indent=indent, sort_keys=sort_keys) + return json.dumps(obj, cls=self.encoder_class, + indent=indent, sort_keys=sort_keys) class JSONPRenderer(JSONRenderer): """ - Renderer which serializes to JSONP + Renderer which serializes to json, + wrapping the json output in a callback function. """ media_type = 'application/javascript' format = 'jsonp' - renderer_class = JSONRenderer callback_parameter = 'callback' + default_callback = 'callback' - def _get_callback(self): - return self.view.request.GET.get(self.callback_parameter, self.callback_parameter) - - def _get_renderer(self): - return self.renderer_class(self.view) + def get_callback(self): + """ + Determine the name of the callback to wrap around the json output. + """ + params = self.view.request.GET + return params.get(self.callback_parameter, self.default_callback) def render(self, obj=None, media_type=None): - callback = self._get_callback() - json = self._get_renderer().render(obj, media_type) + """ + Renders into jsonp, wrapping the json output in a callback function. + + Clients may set the callback function name using a query parameter + on the URL, for example: ?callback=exampleCallbackName + """ + callback = self.get_callback() + json = super(JSONPRenderer, self).render(obj, media_type) return "%s(%s);" % (callback, json) @@ -180,13 +185,13 @@ class TemplateRenderer(BaseRenderer): return template.render(context) -class DocumentingTemplateRenderer(BaseRenderer): +class DocumentingHTMLRenderer(BaseRenderer): """ - Base class for renderers used to self-document the API. - Implementing classes should extend this class and set the template attribute. + HTML renderer used to self-document the API. """ - - template = None + media_type = 'text/html' + format = 'html' + template = 'rest_framework/api.html' def _get_content(self, view, request, obj, media_type): """ @@ -198,7 +203,7 @@ class DocumentingTemplateRenderer(BaseRenderer): # Find the first valid renderer and render the content. (Don't use another documenting renderer.) renderers = [renderer for renderer in view.renderer_classes - if not issubclass(renderer, DocumentingTemplateRenderer)] + if not issubclass(renderer, DocumentingHTMLRenderer)] if not renderers: return '[No renderers were found]' @@ -219,13 +224,13 @@ class DocumentingTemplateRenderer(BaseRenderer): return # We need to map our Fields to Django's Fields. field_mapping = dict([ - [FloatField.__name__, forms.FloatField], - [IntegerField.__name__, forms.IntegerField], - [DateTimeField.__name__, forms.DateTimeField], - [DateField.__name__, forms.DateField], - [EmailField.__name__, forms.EmailField], - [CharField.__name__, forms.CharField], - [BooleanField.__name__, forms.BooleanField] + [serializers.FloatField.__name__, forms.FloatField], + [serializers.IntegerField.__name__, forms.IntegerField], + [serializers.DateTimeField.__name__, forms.DateTimeField], + [serializers.DateField.__name__, forms.DateField], + [serializers.EmailField.__name__, forms.EmailField], + [serializers.CharField.__name__, forms.CharField], + [serializers.BooleanField.__name__, forms.BooleanField] ]) # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python @@ -299,8 +304,11 @@ class DocumentingTemplateRenderer(BaseRenderer): The context used in the template contains all the information needed to self-document the response to this request. """ + view = self.view + request = view.request + response = view.response - content = self._get_content(self.view, self.view.request, obj, media_type) + content = self._get_content(view, request, obj, media_type) put_form_instance = self._get_form_instance(self.view, 'put') post_form_instance = self._get_form_instance(self.view, 'post') @@ -313,9 +321,9 @@ class DocumentingTemplateRenderer(BaseRenderer): template = loader.get_template(self.template) context = RequestContext(self.view.request, { 'content': content, - 'view': self.view, - 'request': self.view.request, - 'response': self.view.response, + 'view': view, + 'request': request, + 'response': response, 'description': description, 'name': name, 'version': VERSION, @@ -324,8 +332,6 @@ class DocumentingTemplateRenderer(BaseRenderer): 'available_formats': [renderer.format for renderer in self.view.renderer_classes], 'put_form': put_form_instance, 'post_form': post_form_instance, - 'FORMAT_PARAM': self._FORMAT_QUERY_PARAM, - 'METHOD_PARAM': getattr(self.view, '_METHOD_PARAM', None), 'api_settings': api_settings }) @@ -338,53 +344,3 @@ class DocumentingTemplateRenderer(BaseRenderer): self.view.response.status_code = 200 return ret - - -class DocumentingHTMLRenderer(DocumentingTemplateRenderer): - """ - Renderer which provides a browsable HTML interface for an API. - See the examples at http://api.django-rest-framework.org to see this in action. - """ - - media_type = 'text/html' - format = 'html' - template = 'rest_framework/api.html' - - -class DocumentingXHTMLRenderer(DocumentingTemplateRenderer): - """ - Identical to DocumentingHTMLRenderer, except with an xhtml media type. - We need this to be listed in preference to xml in order to return HTML to WebKit based browsers, - given their Accept headers. - """ - - media_type = 'application/xhtml+xml' - format = 'xhtml' - template = 'rest_framework/api.html' - - -class DocumentingPlainTextRenderer(DocumentingTemplateRenderer): - """ - Renderer that serializes the object with the default renderer, but also provides plain-text - documentation of the returned status and headers, and of the resource's name and description. - Useful for browsing an API with command line tools. - """ - - media_type = 'text/plain' - format = 'txt' - template = 'rest_framework/api.txt' - - -DEFAULT_RENDERERS = ( - JSONRenderer, - JSONPRenderer, - DocumentingHTMLRenderer, - DocumentingXHTMLRenderer, - DocumentingPlainTextRenderer, - XMLRenderer -) - -if yaml: - DEFAULT_RENDERERS += (YAMLRenderer, ) -else: - YAMLRenderer = None -- cgit v1.2.3 From b7b7ca5af98cae434b1022b294634f8b09ce298c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 25 Sep 2012 13:35:28 +0100 Subject: Don't display readonly fields --- rest_framework/renderers.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 58bd11f0..eeba7339 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -240,6 +240,8 @@ class DocumentingHTMLRenderer(BaseRenderer): object = self.view.object serializer = self.view.get_serializer(instance=object) for k, v in serializer.fields.items(): + if v.readonly: + continue fields[k] = field_mapping[v.__class__.__name__]() OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) if object and not self.view.request.method == 'DELETE': # Don't fill in the form when the object is deleted -- cgit v1.2.3 From ee36e4ab0c0508a590c6b73a23ec82b7f1e49bd0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 27 Sep 2012 21:51:46 +0100 Subject: Only display forms when user has permissions. #159 --- rest_framework/renderers.py | 71 +++++++++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 22 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index eeba7339..35a2a733 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -5,6 +5,7 @@ Django REST framework also provides HTML and PlainText renderers that help self- by serializing the output along with documentation regarding the View, output status and headers, and providing forms and links depending on the allowed methods, renderers and parsers on the View. """ +import copy import string from django import forms from django.template import RequestContext, loader @@ -193,7 +194,7 @@ class DocumentingHTMLRenderer(BaseRenderer): format = 'html' template = 'rest_framework/api.html' - def _get_content(self, view, request, obj, media_type): + def get_content(self, view, request, obj, media_type): """ Get the content as if it had been rendered by a non-documenting renderer. @@ -214,14 +215,31 @@ class DocumentingHTMLRenderer(BaseRenderer): return content - def _get_form_instance(self, view, method): + def get_form(self, view, method, request): """ Get a form, possibly bound to either the input or output data. In the absence on of the Resource having an associated form then provide a form that can be used to submit arbitrary content. """ - if not hasattr(self.view, 'get_serializer'): # No serializer, no form. - return + if not method in view.allowed_methods: + return # Not a valid method + + if not api_settings.FORM_METHOD_OVERRIDE: + return # Cannot use form overloading + + temp = request._method + request._method = method.upper() + if not view.has_permission(request): + request._method = temp + return # Don't have permission + request._method = temp + + if method == 'DELETE' or method == 'OPTIONS': + return True # Don't actually need to return a form + + if not getattr(view, 'get_serializer', None): + return self.get_generic_content_form(view) + # We need to map our Fields to Django's Fields. field_mapping = dict([ [serializers.FloatField.__name__, forms.FloatField], @@ -236,20 +254,20 @@ class DocumentingHTMLRenderer(BaseRenderer): # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python fields = {} object, data = None, None - if hasattr(self.view, 'object'): - object = self.view.object - serializer = self.view.get_serializer(instance=object) + if getattr(view, 'object', None): + object = view.object + serializer = view.get_serializer(instance=object) for k, v in serializer.fields.items(): if v.readonly: continue fields[k] = field_mapping[v.__class__.__name__]() OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) - if object and not self.view.request.method == 'DELETE': # Don't fill in the form when the object is deleted + if object and not view.request.method == 'DELETE': # Don't fill in the form when the object is deleted data = serializer.data form_instance = OnTheFlyForm(data) return form_instance - def _get_generic_content_form(self, view): + def get_generic_content_form(self, view): """ Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms (Which are typically application/x-www-form-urlencoded) @@ -257,7 +275,8 @@ class DocumentingHTMLRenderer(BaseRenderer): # 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 value as the content of the request. - if not getattr(view.request, '_USE_FORM_OVERLOADING', False): + if not (api_settings.FORM_CONTENT_OVERRIDE + and api_settings.FORM_CONTENTTYPE_OVERRIDE): return None # NB. http://jacobian.org/writing/dynamic-form-generation/ @@ -272,11 +291,15 @@ class DocumentingHTMLRenderer(BaseRenderer): contenttype_choices = [(media_type, media_type) for media_type in parsed_media_types] initial_contenttype = parsed_media_types[0] - self.fields[request._CONTENTTYPE_PARAM] = forms.ChoiceField(label='Content Type', - choices=contenttype_choices, - initial=initial_contenttype) - self.fields[request._CONTENT_PARAM] = forms.CharField(label='Content', - widget=forms.Textarea) + self.fields[api_settings.FORM_CONTENTTYPE_OVERRIDE] = forms.ChoiceField( + label='Content Type', + choices=contenttype_choices, + initial=initial_contenttype + ) + self.fields[api_settings.FORM_CONTENT_OVERRIDE] = forms.CharField( + label='Content', + widget=forms.Textarea + ) # If either of these reserved parameters are turned off then content tunneling is not possible if self.view.request._CONTENTTYPE_PARAM is None or self.view.request._CONTENT_PARAM is None: @@ -310,10 +333,12 @@ class DocumentingHTMLRenderer(BaseRenderer): request = view.request response = view.response - content = self._get_content(view, request, obj, media_type) + content = self.get_content(view, request, obj, media_type) - put_form_instance = self._get_form_instance(self.view, 'put') - post_form_instance = self._get_form_instance(self.view, 'post') + put_form = self.get_form(view, 'PUT', request) + post_form = self.get_form(view, 'POST', request) + delete_form = self.get_form(view, 'DELETE', request) + options_form = self.get_form(view, 'OPTIONS', request) name = self.get_name() description = self.get_description() @@ -330,10 +355,12 @@ class DocumentingHTMLRenderer(BaseRenderer): 'name': name, 'version': VERSION, 'breadcrumblist': breadcrumb_list, - 'allowed_methods': self.view.allowed_methods, - 'available_formats': [renderer.format for renderer in self.view.renderer_classes], - 'put_form': put_form_instance, - 'post_form': post_form_instance, + 'allowed_methods': view.allowed_methods, + 'available_formats': [renderer.format for renderer in view.renderer_classes], + 'put_form': put_form, + 'post_form': post_form, + 'delete_form': delete_form, + 'options_form': options_form, 'api_settings': api_settings }) -- cgit v1.2.3 From 84f775803994ccd82671dd4a6f9b4d87aa36dc31 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 28 Sep 2012 11:53:51 +0100 Subject: Remove support for doctests --- rest_framework/renderers.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 35a2a733..8f2a3570 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -5,13 +5,13 @@ Django REST framework also provides HTML and PlainText renderers that help self- by serializing the output along with documentation regarding the View, output status and headers, and providing forms and links depending on the allowed methods, renderers and parsers on the View. """ -import copy import string from django import forms from django.template import RequestContext, loader from django.utils import simplejson as json from rest_framework.compat import yaml from rest_framework.settings import api_settings +from rest_framework.request import clone_request from rest_framework.utils import dict2xml from rest_framework.utils import encoders from rest_framework.utils.breadcrumbs import get_breadcrumbs @@ -227,12 +227,9 @@ class DocumentingHTMLRenderer(BaseRenderer): if not api_settings.FORM_METHOD_OVERRIDE: return # Cannot use form overloading - temp = request._method - request._method = method.upper() + request = clone_request(request, method) if not view.has_permission(request): - request._method = temp return # Don't have permission - request._method = temp if method == 'DELETE' or method == 'OPTIONS': return True # Don't actually need to return a form -- cgit v1.2.3 From c30e0795bebd9980a66ae7db1a0d8c43f77d4c11 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Oct 2012 09:26:15 +0100 Subject: Rename generic views --- rest_framework/renderers.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 8f2a3570..b3d27630 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -239,27 +239,29 @@ class DocumentingHTMLRenderer(BaseRenderer): # We need to map our Fields to Django's Fields. field_mapping = dict([ - [serializers.FloatField.__name__, forms.FloatField], - [serializers.IntegerField.__name__, forms.IntegerField], - [serializers.DateTimeField.__name__, forms.DateTimeField], - [serializers.DateField.__name__, forms.DateField], - [serializers.EmailField.__name__, forms.EmailField], - [serializers.CharField.__name__, forms.CharField], - [serializers.BooleanField.__name__, forms.BooleanField] + [serializers.FloatField.__name__, forms.FloatField], + [serializers.IntegerField.__name__, forms.IntegerField], + [serializers.DateTimeField.__name__, forms.DateTimeField], + [serializers.DateField.__name__, forms.DateField], + [serializers.EmailField.__name__, forms.EmailField], + [serializers.CharField.__name__, forms.CharField], + [serializers.BooleanField.__name__, forms.BooleanField] ]) # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python fields = {} - object, data = None, None + obj, data = None, None if getattr(view, 'object', None): - object = view.object - serializer = view.get_serializer(instance=object) + obj = view.object + + serializer = view.get_serializer(instance=obj) for k, v in serializer.fields.items(): if v.readonly: continue fields[k] = field_mapping[v.__class__.__name__]() + OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) - if object and not view.request.method == 'DELETE': # Don't fill in the form when the object is deleted + if obj and not view.request.method == 'DELETE': # Don't fill in the form when the object is deleted data = serializer.data form_instance = OnTheFlyForm(data) return form_instance -- cgit v1.2.3 From 1a05942166abfc68f83caea535aa44733b1e37a9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Oct 2012 09:45:27 +0100 Subject: Stopgap fix for model fields rendering in browseable API --- rest_framework/renderers.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index b3d27630..9484e29b 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -238,15 +238,16 @@ class DocumentingHTMLRenderer(BaseRenderer): return self.get_generic_content_form(view) # We need to map our Fields to Django's Fields. - field_mapping = dict([ - [serializers.FloatField.__name__, forms.FloatField], - [serializers.IntegerField.__name__, forms.IntegerField], - [serializers.DateTimeField.__name__, forms.DateTimeField], - [serializers.DateField.__name__, forms.DateField], - [serializers.EmailField.__name__, forms.EmailField], - [serializers.CharField.__name__, forms.CharField], - [serializers.BooleanField.__name__, forms.BooleanField] - ]) + # TODO: Remove this and just render serializer fields directly + field_mapping = { + serializers.FloatField: forms.FloatField, + serializers.IntegerField: forms.IntegerField, + serializers.DateTimeField: forms.DateTimeField, + serializers.DateField: forms.DateField, + serializers.EmailField: forms.EmailField, + serializers.CharField: forms.CharField, + serializers.BooleanField: forms.BooleanField + } # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python fields = {} @@ -255,10 +256,13 @@ class DocumentingHTMLRenderer(BaseRenderer): obj = view.object serializer = view.get_serializer(instance=obj) - for k, v in serializer.fields.items(): + for k, v in serializer.get_fields(True).items(): if v.readonly: continue - fields[k] = field_mapping[v.__class__.__name__]() + try: + fields[k] = field_mapping[v.__class__]() + except KeyError: + fields[k] = forms.CharField OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) if obj and not view.request.method == 'DELETE': # Don't fill in the form when the object is deleted -- cgit v1.2.3 From f1f7f5d4e3cd67730c6fb2233a5e4d6afaeae636 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Oct 2012 11:50:08 +0100 Subject: Added failing test for m2m data --- rest_framework/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 9484e29b..9f0c26a5 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -262,7 +262,7 @@ class DocumentingHTMLRenderer(BaseRenderer): try: fields[k] = field_mapping[v.__class__]() except KeyError: - fields[k] = forms.CharField + fields[k] = forms.CharField() OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) if obj and not view.request.method == 'DELETE': # Don't fill in the form when the object is deleted -- cgit v1.2.3 From a366d6e61261b9050c85a76d26ccf1544f165486 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Oct 2012 16:08:20 +0100 Subject: M2M fields supported --- rest_framework/renderers.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 9f0c26a5..96c5e170 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -246,7 +246,9 @@ class DocumentingHTMLRenderer(BaseRenderer): serializers.DateField: forms.DateField, serializers.EmailField: forms.EmailField, serializers.CharField: forms.CharField, - serializers.BooleanField: forms.BooleanField + serializers.BooleanField: forms.BooleanField, + serializers.PrimaryKeyRelatedField: forms.ChoiceField, + serializers.ManyPrimaryKeyRelatedField: forms.ModelMultipleChoiceField } # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python @@ -257,10 +259,16 @@ class DocumentingHTMLRenderer(BaseRenderer): serializer = view.get_serializer(instance=obj) for k, v in serializer.get_fields(True).items(): + print k, v if v.readonly: continue + + kwargs = {} + if getattr(v, 'queryset', None): + kwargs['queryset'] = getattr(v, 'queryset', None) + try: - fields[k] = field_mapping[v.__class__]() + fields[k] = field_mapping[v.__class__](**kwargs) except KeyError: fields[k] = forms.CharField() -- cgit v1.2.3 From a02707e12f750fd0d325e528f7b0fbcd7079db73 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 3 Oct 2012 21:08:32 +0100 Subject: Tweaks --- rest_framework/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 96c5e170..5bc5d5f8 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -247,7 +247,7 @@ class DocumentingHTMLRenderer(BaseRenderer): serializers.EmailField: forms.EmailField, serializers.CharField: forms.CharField, serializers.BooleanField: forms.BooleanField, - serializers.PrimaryKeyRelatedField: forms.ChoiceField, + serializers.PrimaryKeyRelatedField: forms.ModelChoiceField, serializers.ManyPrimaryKeyRelatedField: forms.ModelMultipleChoiceField } -- cgit v1.2.3 From cc0d2601b8dfdf3f5fcee8591540b9cb4b2f3e44 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 4 Oct 2012 21:36:40 +0100 Subject: Minor fixes --- rest_framework/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 5bc5d5f8..e33fa30e 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -260,7 +260,7 @@ class DocumentingHTMLRenderer(BaseRenderer): serializer = view.get_serializer(instance=obj) for k, v in serializer.get_fields(True).items(): print k, v - if v.readonly: + if getattr(v, 'readonly', True): continue kwargs = {} -- cgit v1.2.3 From d07dc77e91c1f99b47915b3cef30b565f2618e82 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Oct 2012 10:23:47 +0100 Subject: Accepted media type uses most specific of client/renderer media types. --- rest_framework/renderers.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index e33fa30e..6a95815a 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -15,7 +15,7 @@ from rest_framework.request import clone_request from rest_framework.utils import dict2xml from rest_framework.utils import encoders from rest_framework.utils.breadcrumbs import get_breadcrumbs -from rest_framework.utils.mediatypes import get_media_type_params, add_media_type_param, media_type_matches +from rest_framework.utils.mediatypes import get_media_type_params, add_media_type_param from rest_framework import VERSION from rest_framework import serializers @@ -32,23 +32,6 @@ class BaseRenderer(object): def __init__(self, view=None): self.view = view - def can_handle_format(self, format): - return format == self.format - - def can_handle_media_type(self, media_type): - """ - Returns `True` if this renderer is able to deal with the given - media type. - - The default implementation for this function is to check the media type - argument against the media_type attribute set on the class to see if - they match. - - This may be overridden to provide for other behavior, but typically - you'll instead want to just set the `media_type` attribute on the class. - """ - return media_type_matches(self.media_type, media_type) - def render(self, obj=None, media_type=None): """ Given an object render it into a string. -- cgit v1.2.3 From 4af7fb96f7d726d56077835d7a7a6d5ad0ff0e99 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Oct 2012 11:12:52 +0100 Subject: Tidy up renderers slightly --- rest_framework/renderers.py | 65 ++++++++++++++++++--------------------------- 1 file changed, 26 insertions(+), 39 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 6a95815a..3227a03a 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -32,22 +32,8 @@ class BaseRenderer(object): def __init__(self, view=None): self.view = view - def render(self, obj=None, media_type=None): - """ - Given an object render it into a string. - - The requested media type is also passed to this method, - as it may contain parameters relevant to how the parser - should render the output. - EG: ``application/json; indent=4`` - - By default render simply returns the output as-is. - Override this method to provide for other behavior. - """ - if obj is None: - return '' - - return str(obj) + def render(self, data=None, accepted_media_type=None): + raise NotImplemented('Renderer class requires .render() to be implemented') class JSONRenderer(BaseRenderer): @@ -59,16 +45,16 @@ class JSONRenderer(BaseRenderer): format = 'json' encoder_class = encoders.JSONEncoder - def render(self, obj=None, media_type=None): + def render(self, data=None, accepted_media_type=None): """ Render `obj` into json. """ - if obj is None: + if data is None: return '' # If the media type looks like 'application/json; indent=4', then # pretty print the result. - indent = get_media_type_params(media_type).get('indent', None) + indent = get_media_type_params(accepted_media_type).get('indent', None) sort_keys = False try: indent = max(min(int(indent), 8), 0) @@ -76,7 +62,7 @@ class JSONRenderer(BaseRenderer): except (ValueError, TypeError): indent = None - return json.dumps(obj, cls=self.encoder_class, + return json.dumps(data, cls=self.encoder_class, indent=indent, sort_keys=sort_keys) @@ -98,7 +84,7 @@ class JSONPRenderer(JSONRenderer): params = self.view.request.GET return params.get(self.callback_parameter, self.default_callback) - def render(self, obj=None, media_type=None): + def render(self, data=None, accepted_media_type=None): """ Renders into jsonp, wrapping the json output in a callback function. @@ -106,7 +92,7 @@ class JSONPRenderer(JSONRenderer): on the URL, for example: ?callback=exampleCallbackName """ callback = self.get_callback() - json = super(JSONPRenderer, self).render(obj, media_type) + json = super(JSONPRenderer, self).render(data, accepted_media_type) return "%s(%s);" % (callback, json) @@ -118,13 +104,13 @@ class XMLRenderer(BaseRenderer): media_type = 'application/xml' format = 'xml' - def render(self, obj=None, media_type=None): + def render(self, data=None, accepted_media_type=None): """ Renders *obj* into serialized XML. """ - if obj is None: + if data is None: return '' - return dict2xml(obj) + return dict2xml(data) class YAMLRenderer(BaseRenderer): @@ -135,17 +121,17 @@ class YAMLRenderer(BaseRenderer): media_type = 'application/yaml' format = 'yaml' - def render(self, obj=None, media_type=None): + def render(self, data=None, accepted_media_type=None): """ Renders *obj* into serialized YAML. """ - if obj is None: + if data is None: return '' - return yaml.safe_dump(obj) + return yaml.safe_dump(data) -class TemplateRenderer(BaseRenderer): +class HTMLTemplateRenderer(BaseRenderer): """ A Base class provided for convenience. @@ -154,18 +140,19 @@ class TemplateRenderer(BaseRenderer): the :attr:`media_type` and :attr:`template` attributes. """ - media_type = None + media_type = 'text/html' + format = 'html' template = None - def render(self, obj=None, media_type=None): + def render(self, data=None, accepted_media_type=None): """ Renders *obj* using the :attr:`template` specified on the class. """ - if obj is None: + if data is None: return '' template = loader.get_template(self.template) - context = RequestContext(self.view.request, {'object': obj}) + context = RequestContext(self.view.request, {'object': data}) return template.render(context) @@ -174,10 +161,10 @@ class DocumentingHTMLRenderer(BaseRenderer): HTML renderer used to self-document the API. """ media_type = 'text/html' - format = 'html' + format = 'api' template = 'rest_framework/api.html' - def get_content(self, view, request, obj, media_type): + def get_content(self, view, request, data, accepted_media_type): """ Get the content as if it had been rendered by a non-documenting renderer. @@ -191,8 +178,8 @@ class DocumentingHTMLRenderer(BaseRenderer): if not renderers: return '[No renderers were found]' - media_type = add_media_type_param(media_type, 'indent', '4') - content = renderers[0](view).render(obj, media_type) + accepted_media_type = add_media_type_param(accepted_media_type, 'indent', '4') + content = renderers[0](view).render(data, accepted_media_type) if not all(char in string.printable for char in content): return '[%d bytes of binary content]' @@ -316,7 +303,7 @@ class DocumentingHTMLRenderer(BaseRenderer): except AttributeError: return self.view.__doc__ - def render(self, obj=None, media_type=None): + def render(self, data=None, accepted_media_type=None): """ Renders *obj* using the :attr:`template` set on the class. @@ -327,7 +314,7 @@ class DocumentingHTMLRenderer(BaseRenderer): request = view.request response = view.response - content = self.get_content(view, request, obj, media_type) + content = self.get_content(view, request, data, accepted_media_type) put_form = self.get_form(view, 'PUT', request) post_form = self.get_form(view, 'POST', request) -- cgit v1.2.3 From 26c7d6df6c0a12a2e19e07951b93de80bbfdf91c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Oct 2012 12:13:44 +0100 Subject: HTMLTemplateRenderer working --- rest_framework/renderers.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 3227a03a..4157468f 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -10,6 +10,7 @@ from django import forms from django.template import RequestContext, loader from django.utils import simplejson as json from rest_framework.compat import yaml +from rest_framework.exceptions import ConfigurationError from rest_framework.settings import api_settings from rest_framework.request import clone_request from rest_framework.utils import dict2xml @@ -142,19 +143,41 @@ class HTMLTemplateRenderer(BaseRenderer): media_type = 'text/html' format = 'html' - template = None + template_name = None def render(self, data=None, accepted_media_type=None): """ - Renders *obj* using the :attr:`template` specified on the class. + Renders data to HTML, using Django's standard template rendering. + + The template name is determined by (in order of preference): + + 1. An explicit .template_name set on the response. + 2. An explicit .template_name set on this class. + 3. The return result of calling view.get_template_names(). """ - if data is None: - return '' + view = self.view + request, response = view.request, view.response - template = loader.get_template(self.template) - context = RequestContext(self.view.request, {'object': data}) + template_names = self.get_template_names(response, view) + template = self.resolve_template(template_names) + context = self.resolve_context(data, request) return template.render(context) + def resolve_template(self, template_names): + return loader.select_template(template_names) + + def resolve_context(self, data, request): + return RequestContext(request, data) + + def get_template_names(self, response, view): + if response.template_name: + return [response.template_name] + elif self.template_name: + return [self.template_name] + elif hasattr(view, 'get_template_names'): + return view.get_template_names() + raise ConfigurationError('Returned a template response with no template_name') + class DocumentingHTMLRenderer(BaseRenderer): """ -- cgit v1.2.3 From f4d4d54e030addf64efe68f387b934262a3c173b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 8 Oct 2012 12:17:43 +0100 Subject: Adding migration and changelog docs --- rest_framework/renderers.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 4157468f..a776b0e2 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -230,8 +230,11 @@ class DocumentingHTMLRenderer(BaseRenderer): if not getattr(view, 'get_serializer', None): return self.get_generic_content_form(view) + ##### + # TODO: This is a little bit of a hack. Actually we'd like to remove + # this and just render serializer fields to html directly. + # We need to map our Fields to Django's Fields. - # TODO: Remove this and just render serializer fields directly field_mapping = { serializers.FloatField: forms.FloatField, serializers.IntegerField: forms.IntegerField, @@ -252,7 +255,6 @@ class DocumentingHTMLRenderer(BaseRenderer): serializer = view.get_serializer(instance=obj) for k, v in serializer.get_fields(True).items(): - print k, v if getattr(v, 'readonly', True): continue -- cgit v1.2.3 From 97a7f27c8219181e40dddcaf820545e08283de93 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Oct 2012 15:58:48 +0100 Subject: Rename HTMLTemplateRenderer -> HTMLRenderer, DocuemntingHTMLRenderer -> BrowseableAPIRenderer --- rest_framework/renderers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index a776b0e2..25e6ed62 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -132,7 +132,7 @@ class YAMLRenderer(BaseRenderer): return yaml.safe_dump(data) -class HTMLTemplateRenderer(BaseRenderer): +class HTMLRenderer(BaseRenderer): """ A Base class provided for convenience. @@ -179,7 +179,7 @@ class HTMLTemplateRenderer(BaseRenderer): raise ConfigurationError('Returned a template response with no template_name') -class DocumentingHTMLRenderer(BaseRenderer): +class BrowsableAPIRenderer(BaseRenderer): """ HTML renderer used to self-document the API. """ @@ -197,7 +197,7 @@ class DocumentingHTMLRenderer(BaseRenderer): # Find the first valid renderer and render the content. (Don't use another documenting renderer.) renderers = [renderer for renderer in view.renderer_classes - if not issubclass(renderer, DocumentingHTMLRenderer)] + if not issubclass(renderer, BrowsableAPIRenderer)] if not renderers: return '[No renderers were found]' -- cgit v1.2.3 From 648d2be29b0738999742f4d844caab7b7652d1ad Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 10 Oct 2012 12:15:18 +0100 Subject: Make sure JSON output in Browseable API is nicely indented --- rest_framework/renderers.py | 162 ++++++++++++++++++++++++-------------------- 1 file changed, 87 insertions(+), 75 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 25e6ed62..27a85ab1 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -16,7 +16,7 @@ from rest_framework.request import clone_request from rest_framework.utils import dict2xml from rest_framework.utils import encoders from rest_framework.utils.breadcrumbs import get_breadcrumbs -from rest_framework.utils.mediatypes import get_media_type_params, add_media_type_param +from rest_framework.utils.mediatypes import get_media_type_params from rest_framework import VERSION from rest_framework import serializers @@ -30,10 +30,7 @@ class BaseRenderer(object): media_type = None format = None - def __init__(self, view=None): - self.view = view - - def render(self, data=None, accepted_media_type=None): + def render(self, data, accepted_media_type=None, renderer_context=None): raise NotImplemented('Renderer class requires .render() to be implemented') @@ -46,22 +43,29 @@ class JSONRenderer(BaseRenderer): format = 'json' encoder_class = encoders.JSONEncoder - def render(self, data=None, accepted_media_type=None): + def render(self, data, accepted_media_type=None, renderer_context=None): """ Render `obj` into json. """ if data is None: return '' - # If the media type looks like 'application/json; indent=4', then - # pretty print the result. - indent = get_media_type_params(accepted_media_type).get('indent', None) - sort_keys = False - try: - indent = max(min(int(indent), 8), 0) - sort_keys = True - except (ValueError, TypeError): - indent = None + # If 'indent' is provided in the context, then pretty print the result. + # E.g. If we're being called by the BrowseableAPIRenderer. + renderer_context = renderer_context or {} + indent = renderer_context.get('indent', None) + sort_keys = indent and True or False + + if accepted_media_type: + # If the media type looks like 'application/json; indent=4', + # then pretty print the result. + params = get_media_type_params(accepted_media_type) + indent = params.get('indent', indent) + try: + indent = max(min(int(indent), 8), 0) + sort_keys = True + except (ValueError, TypeError): + indent = None return json.dumps(data, cls=self.encoder_class, indent=indent, sort_keys=sort_keys) @@ -78,22 +82,25 @@ class JSONPRenderer(JSONRenderer): callback_parameter = 'callback' default_callback = 'callback' - def get_callback(self): + def get_callback(self, renderer_context): """ Determine the name of the callback to wrap around the json output. """ - params = self.view.request.GET + request = renderer_context.get('request', None) + params = request and request.GET or {} return params.get(self.callback_parameter, self.default_callback) - def render(self, data=None, accepted_media_type=None): + def render(self, data, accepted_media_type=None, renderer_context=None): """ Renders into jsonp, wrapping the json output in a callback function. Clients may set the callback function name using a query parameter on the URL, for example: ?callback=exampleCallbackName """ - callback = self.get_callback() - json = super(JSONPRenderer, self).render(data, accepted_media_type) + renderer_context = renderer_context or {} + callback = self.get_callback(renderer_context) + json = super(JSONPRenderer, self).render(data, accepted_media_type, + renderer_context) return "%s(%s);" % (callback, json) @@ -105,7 +112,7 @@ class XMLRenderer(BaseRenderer): media_type = 'application/xml' format = 'xml' - def render(self, data=None, accepted_media_type=None): + def render(self, data, accepted_media_type=None, renderer_context=None): """ Renders *obj* into serialized XML. """ @@ -122,7 +129,7 @@ class YAMLRenderer(BaseRenderer): media_type = 'application/yaml' format = 'yaml' - def render(self, data=None, accepted_media_type=None): + def render(self, data, accepted_media_type=None, renderer_context=None): """ Renders *obj* into serialized YAML. """ @@ -145,7 +152,7 @@ class HTMLRenderer(BaseRenderer): format = 'html' template_name = None - def render(self, data=None, accepted_media_type=None): + def render(self, data, accepted_media_type=None, renderer_context=None): """ Renders data to HTML, using Django's standard template rendering. @@ -155,8 +162,10 @@ class HTMLRenderer(BaseRenderer): 2. An explicit .template_name set on this class. 3. The return result of calling view.get_template_names(). """ - view = self.view - request, response = view.request, view.response + renderer_context = renderer_context or {} + view = renderer_context['view'] + request = renderer_context['request'] + response = renderer_context['response'] template_names = self.get_template_names(response, view) template = self.resolve_template(template_names) @@ -187,22 +196,29 @@ class BrowsableAPIRenderer(BaseRenderer): format = 'api' template = 'rest_framework/api.html' - def get_content(self, view, request, data, accepted_media_type): + def get_default_renderer(self, view): """ - Get the content as if it had been rendered by a non-documenting renderer. - - (Typically this will be the content as it would have been if the Resource had been - requested with an 'Accept: */*' header, although with verbose style formatting if appropriate.) + Return an instance of the first valid renderer. + (Don't use another documenting renderer.) """ - - # Find the first valid renderer and render the content. (Don't use another documenting renderer.) renderers = [renderer for renderer in view.renderer_classes if not issubclass(renderer, BrowsableAPIRenderer)] if not renderers: + return None + return renderers[0]() + + def get_content(self, renderer, data, + accepted_media_type, renderer_context): + """ + Get the content as if it had been rendered by the default + non-documenting renderer. + """ + if not renderer: return '[No renderers were found]' - accepted_media_type = add_media_type_param(accepted_media_type, 'indent', '4') - content = renderers[0](view).render(data, accepted_media_type) + renderer_context['indent'] = 4 + content = renderer.render(data, accepted_media_type, renderer_context) + if not all(char in string.printable for char in content): return '[%d bytes of binary content]' @@ -228,7 +244,8 @@ class BrowsableAPIRenderer(BaseRenderer): return True # Don't actually need to return a form if not getattr(view, 'get_serializer', None): - return self.get_generic_content_form(view) + media_types = [parser.media_type for parser in view.parser_classes] + return self.get_generic_content_form(media_types) ##### # TODO: This is a little bit of a hack. Actually we'd like to remove @@ -273,9 +290,10 @@ class BrowsableAPIRenderer(BaseRenderer): form_instance = OnTheFlyForm(data) return form_instance - def get_generic_content_form(self, view): + def get_generic_content_form(self, media_types): """ - Returns a form that allows for arbitrary content types to be tunneled via standard HTML forms + Returns a form that allows for arbitrary content types to be tunneled + via standard HTML forms. (Which are typically application/x-www-form-urlencoded) """ @@ -285,74 +303,68 @@ class BrowsableAPIRenderer(BaseRenderer): and api_settings.FORM_CONTENTTYPE_OVERRIDE): return None + content_type_field = api_settings.FORM_CONTENTTYPE_OVERRIDE + content_field = api_settings.FORM_CONTENT_OVERRIDE + choices = [(media_type, media_type) for media_type in media_types] + initial = media_types[0] + # NB. http://jacobian.org/writing/dynamic-form-generation/ class GenericContentForm(forms.Form): - def __init__(self, view, request): - """We don't know the names of the fields we want to set until the point the form is instantiated, - as they are determined by the Resource the form is being created against. - Add the fields dynamically.""" + def __init__(self): super(GenericContentForm, self).__init__() - parsed_media_types = [parser.media_type for parser in view.parser_classes] - contenttype_choices = [(media_type, media_type) for media_type in parsed_media_types] - initial_contenttype = parsed_media_types[0] - - self.fields[api_settings.FORM_CONTENTTYPE_OVERRIDE] = forms.ChoiceField( + self.fields[content_type_field] = forms.ChoiceField( label='Content Type', - choices=contenttype_choices, - initial=initial_contenttype + choices=choices, + initial=initial ) - self.fields[api_settings.FORM_CONTENT_OVERRIDE] = forms.CharField( + self.fields[content_field] = forms.CharField( label='Content', widget=forms.Textarea ) - # If either of these reserved parameters are turned off then content tunneling is not possible - if self.view.request._CONTENTTYPE_PARAM is None or self.view.request._CONTENT_PARAM is None: - return None - - # Okey doke, let's do it - return GenericContentForm(view, view.request) + return GenericContentForm() - def get_name(self): + def get_name(self, view): try: - return self.view.get_name() + return view.get_name() except AttributeError: - return self.view.__doc__ + return view.__doc__ - def get_description(self, html=None): - if html is None: - html = bool('html' in self.format) + def get_description(self, view): try: - return self.view.get_description(html) + return view.get_description(html=True) except AttributeError: - return self.view.__doc__ + return view.__doc__ - def render(self, data=None, accepted_media_type=None): + def render(self, data, accepted_media_type=None, renderer_context=None): """ Renders *obj* using the :attr:`template` set on the class. The context used in the template contains all the information needed to self-document the response to this request. """ - view = self.view - request = view.request - response = view.response + accepted_media_type = accepted_media_type or '' + renderer_context = renderer_context or {} - content = self.get_content(view, request, data, accepted_media_type) + view = renderer_context['view'] + request = renderer_context['request'] + response = renderer_context['response'] + + renderer = self.get_default_renderer(view) + content = self.get_content(renderer, data, accepted_media_type, renderer_context) put_form = self.get_form(view, 'PUT', request) post_form = self.get_form(view, 'POST', request) delete_form = self.get_form(view, 'DELETE', request) options_form = self.get_form(view, 'OPTIONS', request) - name = self.get_name() - description = self.get_description() - - breadcrumb_list = get_breadcrumbs(self.view.request.path) + name = self.get_name(view) + description = self.get_description(view) + breadcrumb_list = get_breadcrumbs(request.path) template = loader.get_template(self.template) - context = RequestContext(self.view.request, { + context = RequestContext(request, { 'content': content, 'view': view, 'request': request, @@ -375,7 +387,7 @@ class BrowsableAPIRenderer(BaseRenderer): # Munge DELETE Response code to allow us to return content # (Do this *after* we've rendered the template so that we include # the normal deletion response code in the output) - if self.view.response.status_code == 204: - self.view.response.status_code = 200 + if response.status_code == 204: + response.status_code = 200 return ret -- cgit v1.2.3 From db6b5a990b98cd0a6530b1bbf97f62c61b616fc0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 10 Oct 2012 12:32:23 +0100 Subject: Fix key ordering in json. (sort if it's a sorted dictc representation, or not otherwise) --- rest_framework/renderers.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 27a85ab1..91cf16bb 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -54,7 +54,6 @@ class JSONRenderer(BaseRenderer): # E.g. If we're being called by the BrowseableAPIRenderer. renderer_context = renderer_context or {} indent = renderer_context.get('indent', None) - sort_keys = indent and True or False if accepted_media_type: # If the media type looks like 'application/json; indent=4', @@ -63,12 +62,10 @@ class JSONRenderer(BaseRenderer): indent = params.get('indent', indent) try: indent = max(min(int(indent), 8), 0) - sort_keys = True except (ValueError, TypeError): indent = None - return json.dumps(data, cls=self.encoder_class, - indent=indent, sort_keys=sort_keys) + return json.dumps(data, cls=self.encoder_class, indent=indent) class JSONPRenderer(JSONRenderer): -- cgit v1.2.3 From d905d1cbd3a20191835be1a5bddee0aabf136ec6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 10 Oct 2012 16:34:00 +0100 Subject: Fix yaml rendering --- rest_framework/renderers.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 91cf16bb..e5e4134b 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -18,7 +18,7 @@ from rest_framework.utils import encoders from rest_framework.utils.breadcrumbs import get_breadcrumbs from rest_framework.utils.mediatypes import get_media_type_params from rest_framework import VERSION -from rest_framework import serializers +from rest_framework import serializers, parsers class BaseRenderer(object): @@ -125,6 +125,7 @@ class YAMLRenderer(BaseRenderer): media_type = 'application/yaml' format = 'yaml' + encoder = encoders.SafeDumper def render(self, data, accepted_media_type=None, renderer_context=None): """ @@ -133,7 +134,7 @@ class YAMLRenderer(BaseRenderer): if data is None: return '' - return yaml.safe_dump(data) + return yaml.dump(data, stream=None, Dumper=self.encoder) class HTMLRenderer(BaseRenderer): @@ -240,7 +241,8 @@ class BrowsableAPIRenderer(BaseRenderer): if method == 'DELETE' or method == 'OPTIONS': return True # Don't actually need to return a form - if not getattr(view, 'get_serializer', None): + if (not getattr(view, 'get_serializer', None) or + not parsers.FormParser in getattr(view, 'parser_classes')): media_types = [parser.media_type for parser in view.parser_classes] return self.get_generic_content_form(media_types) -- cgit v1.2.3 From 9c1fba3483b7e81da0744464dcf23a5f12711de2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 15 Oct 2012 13:27:50 +0100 Subject: Tweak parsers to take parser_context --- rest_framework/renderers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index e5e4134b..2a3b0b6c 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -7,6 +7,7 @@ and providing forms and links depending on the allowed methods, renderers and pa """ import string from django import forms +from django.http.multipartparser import parse_header from django.template import RequestContext, loader from django.utils import simplejson as json from rest_framework.compat import yaml @@ -16,7 +17,6 @@ from rest_framework.request import clone_request from rest_framework.utils import dict2xml from rest_framework.utils import encoders from rest_framework.utils.breadcrumbs import get_breadcrumbs -from rest_framework.utils.mediatypes import get_media_type_params from rest_framework import VERSION from rest_framework import serializers, parsers @@ -58,7 +58,7 @@ class JSONRenderer(BaseRenderer): if accepted_media_type: # If the media type looks like 'application/json; indent=4', # then pretty print the result. - params = get_media_type_params(accepted_media_type) + base_media_type, params = parse_header(accepted_media_type) indent = params.get('indent', indent) try: indent = max(min(int(indent), 8), 0) -- cgit v1.2.3 From 3c8f01b985396c9bfe802f0d1e25bbb59ea2a1a9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 15 Oct 2012 14:03:36 +0100 Subject: Explicit CSRF failure message. Fixes #60. --- rest_framework/renderers.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 2a3b0b6c..94d253c9 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -235,8 +235,11 @@ class BrowsableAPIRenderer(BaseRenderer): return # Cannot use form overloading request = clone_request(request, method) - if not view.has_permission(request): - return # Don't have permission + try: + if not view.has_permission(request): + return # Don't have permission + except: + return # Don't have permission and exception explicitly raise if method == 'DELETE' or method == 'OPTIONS': return True # Don't actually need to return a form -- cgit v1.2.3 From fb56f215ae50da0aebe99e05036ece259fd3e6f1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 17 Oct 2012 22:39:07 +0100 Subject: Added `media_type` to `.parse()` - Consistency with renderer API. --- rest_framework/renderers.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 94d253c9..23fd961b 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -1,9 +1,10 @@ """ -Renderers are used to serialize a View's output into specific media types. +Renderers are used to serialize a response into specific media types. -Django REST framework also provides HTML and PlainText renderers that help self-document the API, -by serializing the output along with documentation regarding the View, output status and headers, -and providing forms and links depending on the allowed methods, renderers and parsers on the View. +They give us a generic way of being able to handle various media types +on the response, such as JSON encoded data or HTML output. + +REST framework also provides an HTML renderer the renders the browseable API. """ import string from django import forms @@ -23,8 +24,8 @@ from rest_framework import serializers, parsers class BaseRenderer(object): """ - All renderers must extend this class, set the :attr:`media_type` attribute, - and override the :meth:`render` method. + All renderers should extend this class, setting the `media_type` + and `format` attributes, and override the `.render()` method. """ media_type = None -- cgit v1.2.3 From 643d3491a65237fef6932ef8833472c243ad7ee8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 18 Oct 2012 23:48:52 +0100 Subject: First pass at pastebin tutorial --- rest_framework/renderers.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 23fd961b..936bec36 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -279,13 +279,22 @@ class BrowsableAPIRenderer(BaseRenderer): continue kwargs = {} + kwargs['required'] = v.required if getattr(v, 'queryset', None): - kwargs['queryset'] = getattr(v, 'queryset', None) + kwargs['queryset'] = v.queryset + if getattr(v, 'widget', None): + kwargs['widget'] = v.widget + if getattr(v, 'initial', None): + kwargs['initial'] = v.initial + if getattr(v, 'help_text', None): + kwargs['help_text'] = v.help_text + kwargs['label'] = k + print kwargs try: fields[k] = field_mapping[v.__class__](**kwargs) except KeyError: - fields[k] = forms.CharField() + fields[k] = forms.CharField(**kwargs) OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) if obj and not view.request.method == 'DELETE': # Don't fill in the form when the object is deleted -- cgit v1.2.3 From dab177e29e45b657bb43705979c8e601d5a1b31b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Oct 2012 09:20:54 +0100 Subject: Drop help_text --- rest_framework/renderers.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 936bec36..de2276ad 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -286,8 +286,6 @@ class BrowsableAPIRenderer(BaseRenderer): kwargs['widget'] = v.widget if getattr(v, 'initial', None): kwargs['initial'] = v.initial - if getattr(v, 'help_text', None): - kwargs['help_text'] = v.help_text kwargs['label'] = k print kwargs -- cgit v1.2.3 From a7390fe7044c4f10d15fdbe9de9e594d0ffa2e05 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 19 Oct 2012 09:47:01 +0100 Subject: Fix up widget choices --- rest_framework/renderers.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index de2276ad..ba5489bc 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -6,6 +6,7 @@ on the response, such as JSON encoded data or HTML output. REST framework also provides an HTML renderer the renders the browseable API. """ +import copy import string from django import forms from django.http.multipartparser import parse_header @@ -283,11 +284,19 @@ class BrowsableAPIRenderer(BaseRenderer): if getattr(v, 'queryset', None): kwargs['queryset'] = v.queryset if getattr(v, 'widget', None): - kwargs['widget'] = v.widget + widget = copy.deepcopy(v.widget) + # If choices have friendly readable names, + # then add in the identities too + if getattr(widget, 'choices', None): + choices = widget.choices + if any([ident != desc for (ident, desc) in choices]): + choices = [(ident, "%s (%s)" % (desc, ident)) + for (ident, desc) in choices] + widget.choices = choices + kwargs['widget'] = widget if getattr(v, 'initial', None): kwargs['initial'] = v.initial kwargs['label'] = k - print kwargs try: fields[k] = field_mapping[v.__class__](**kwargs) -- cgit v1.2.3 From 93f1aa4f69df85add114c9730a01b50d013a844a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 21 Oct 2012 17:41:05 +0100 Subject: Remove `initial` kwarg, add `default`. --- rest_framework/renderers.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index ba5489bc..b2dbffd2 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -281,8 +281,10 @@ class BrowsableAPIRenderer(BaseRenderer): kwargs = {} kwargs['required'] = v.required + if getattr(v, 'queryset', None): kwargs['queryset'] = v.queryset + if getattr(v, 'widget', None): widget = copy.deepcopy(v.widget) # If choices have friendly readable names, @@ -294,8 +296,10 @@ class BrowsableAPIRenderer(BaseRenderer): for (ident, desc) in choices] widget.choices = choices kwargs['widget'] = widget - if getattr(v, 'initial', None): - kwargs['initial'] = v.initial + + if getattr(v, 'default', None) is not None: + kwargs['initial'] = v.default + kwargs['label'] = k try: -- cgit v1.2.3 From c30712a5c8e89c7d3e235c72867288a4cb5c8c85 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 21 Oct 2012 22:23:54 +0200 Subject: Remove redundant check if method=='DELETE' --- rest_framework/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 23fd961b..ba07f6cd 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -288,7 +288,7 @@ class BrowsableAPIRenderer(BaseRenderer): fields[k] = forms.CharField() OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) - if obj and not view.request.method == 'DELETE': # Don't fill in the form when the object is deleted + if obj: data = serializer.data form_instance = OnTheFlyForm(data) return form_instance -- cgit v1.2.3 From ab1a12bfecf49061cb31dff05add26d96078771e Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 21 Oct 2012 23:04:12 +0200 Subject: Refactoring BrowsableAPIRenderer --- rest_framework/renderers.py | 52 +++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 23 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index ba07f6cd..43175861 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -223,11 +223,9 @@ class BrowsableAPIRenderer(BaseRenderer): return content - def get_form(self, view, method, request): + def show_form_for_method(self, view, method, request): """ - Get a form, possibly bound to either the input or output data. - In the absence on of the Resource having an associated form then - provide a form that can be used to submit arbitrary content. + Returns True if a form should be shown for this method. """ if not method in view.allowed_methods: return # Not a valid method @@ -241,20 +239,9 @@ class BrowsableAPIRenderer(BaseRenderer): return # Don't have permission except: return # Don't have permission and exception explicitly raise + return True - if method == 'DELETE' or method == 'OPTIONS': - return True # Don't actually need to return a form - - if (not getattr(view, 'get_serializer', None) or - not parsers.FormParser in getattr(view, 'parser_classes')): - media_types = [parser.media_type for parser in view.parser_classes] - return self.get_generic_content_form(media_types) - - ##### - # TODO: This is a little bit of a hack. Actually we'd like to remove - # this and just render serializer fields to html directly. - - # We need to map our Fields to Django's Fields. + def serializer_to_form_fields(self, serializer): field_mapping = { serializers.FloatField: forms.FloatField, serializers.IntegerField: forms.IntegerField, @@ -267,13 +254,7 @@ class BrowsableAPIRenderer(BaseRenderer): serializers.ManyPrimaryKeyRelatedField: forms.ModelMultipleChoiceField } - # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python fields = {} - obj, data = None, None - if getattr(view, 'object', None): - obj = view.object - - serializer = view.get_serializer(instance=obj) for k, v in serializer.get_fields(True).items(): if getattr(v, 'readonly', True): continue @@ -286,6 +267,31 @@ class BrowsableAPIRenderer(BaseRenderer): fields[k] = field_mapping[v.__class__](**kwargs) except KeyError: fields[k] = forms.CharField() + return fields + + def get_form(self, view, method, request): + """ + Get a form, possibly bound to either the input or output data. + In the absence on of the Resource having an associated form then + provide a form that can be used to submit arbitrary content. + """ + if not self.show_form_for_method(view, method, request): + return + + if method == 'DELETE' or method == 'OPTIONS': + return True # Don't actually need to return a form + + if not getattr(view, 'get_serializer', None) or not parsers.FormParser in view.parser_classes: + media_types = [parser.media_type for parser in view.parser_classes] + return self.get_generic_content_form(media_types) + + # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python + obj, data = None, None + if getattr(view, 'object', None): + obj = view.object + + serializer = view.get_serializer(instance=obj) + fields = self.serializer_to_form_fields(serializer) OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) if obj: -- cgit v1.2.3 From 2efb5f8a14ffc321a1a9e88548abfa8b0782aae4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 12:46:15 +0100 Subject: Object-level permissions respected by Browseable API --- rest_framework/renderers.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index c64fb517..1a8b1d97 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -224,7 +224,7 @@ class BrowsableAPIRenderer(BaseRenderer): return content - def show_form_for_method(self, view, method, request): + def show_form_for_method(self, view, method, request, obj): """ Returns True if a form should be shown for this method. """ @@ -236,7 +236,7 @@ class BrowsableAPIRenderer(BaseRenderer): request = clone_request(request, method) try: - if not view.has_permission(request): + if not view.has_permission(request, obj): return # Don't have permission except: return # Don't have permission and exception explicitly raise @@ -295,7 +295,8 @@ class BrowsableAPIRenderer(BaseRenderer): In the absence on of the Resource having an associated form then provide a form that can be used to submit arbitrary content. """ - if not self.show_form_for_method(view, method, request): + obj = getattr(view, 'object', None) + if not self.show_form_for_method(view, method, request, obj): return if method == 'DELETE' or method == 'OPTIONS': @@ -305,17 +306,13 @@ class BrowsableAPIRenderer(BaseRenderer): media_types = [parser.media_type for parser in view.parser_classes] return self.get_generic_content_form(media_types) - # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python - obj, data = None, None - if getattr(view, 'object', None): - obj = view.object - serializer = view.get_serializer(instance=obj) fields = self.serializer_to_form_fields(serializer) + # Creating an on the fly form see: + # http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) - if obj: - data = serializer.data + data = (obj is not None) and serializer.data or None form_instance = OnTheFlyForm(data) return form_instance -- cgit v1.2.3 From 12c363c1fe237d0357e6020b44890926856b9191 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Oct 2012 18:12:56 +0000 Subject: TemplateHTMLRenderer, StaticHTMLRenderer --- rest_framework/renderers.py | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 1a8b1d97..cfe4df6d 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -139,13 +139,24 @@ class YAMLRenderer(BaseRenderer): return yaml.dump(data, stream=None, Dumper=self.encoder) -class HTMLRenderer(BaseRenderer): +class TemplateHTMLRenderer(BaseRenderer): """ - A Base class provided for convenience. + An HTML renderer for use with templates. - Render the object simply by using the given template. - To create a template renderer, subclass this class, and set - the :attr:`media_type` and :attr:`template` attributes. + The data supplied to the Response object should be a dictionary that will + be used as context for the template. + + The template name is determined by (in order of preference): + + 1. An explicit `.template_name` attribute set on the response. + 2. An explicit `.template_name` attribute set on this class. + 3. The return result of calling `view.get_template_names()`. + + For example: + data = {'users': User.objects.all()} + return Response(data, template_name='users.html') + + For pre-rendered HTML, see StaticHTMLRenderer. """ media_type = 'text/html' @@ -188,6 +199,26 @@ class HTMLRenderer(BaseRenderer): raise ConfigurationError('Returned a template response with no template_name') +class StaticHTMLRenderer(BaseRenderer): + """ + An HTML renderer class that simply returns pre-rendered HTML. + + The data supplied to the Response object should be a string representing + the pre-rendered HTML content. + + For example: + data = 'example' + return Response(data) + + For template rendered HTML, see TemplateHTMLRenderer. + """ + media_type = 'text/html' + format = 'html' + + def render(self, data, accepted_media_type=None, renderer_context=None): + return data + + class BrowsableAPIRenderer(BaseRenderer): """ HTML renderer used to self-document the API. -- cgit v1.2.3 From 6e4ab09aae8295e4ef722d59894bc2934435ae46 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Oct 2012 20:21:45 +0000 Subject: readonly -> read_only --- rest_framework/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index cfe4df6d..938e8664 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -288,7 +288,7 @@ class BrowsableAPIRenderer(BaseRenderer): fields = {} for k, v in serializer.get_fields(True).items(): - if getattr(v, 'readonly', True): + if getattr(v, 'read_only', True): continue kwargs = {} -- cgit v1.2.3 From 4cdd0b845e10c433358f210c84a2b3fe28543c68 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 30 Oct 2012 13:59:31 +0000 Subject: Final docs tweaks for 2.0 --- rest_framework/renderers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'rest_framework/renderers.py') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 938e8664..8dff0c77 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -283,7 +283,9 @@ class BrowsableAPIRenderer(BaseRenderer): serializers.CharField: forms.CharField, serializers.BooleanField: forms.BooleanField, serializers.PrimaryKeyRelatedField: forms.ModelChoiceField, - serializers.ManyPrimaryKeyRelatedField: forms.ModelMultipleChoiceField + serializers.ManyPrimaryKeyRelatedField: forms.ModelMultipleChoiceField, + serializers.HyperlinkedRelatedField: forms.ModelChoiceField, + serializers.ManyHyperlinkedRelatedField: forms.ModelMultipleChoiceField } fields = {} -- cgit v1.2.3