From 94cd1369437f84adefb46462439b46dc5208ab1d Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Thu, 29 Aug 2013 17:35:15 +1200 Subject: [PATCH 01/53] add transform_ methods to serializers, which basically do the opposite of validate_ on a per-field basis. --- rest_framework/serializers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 31cfa3447..8ba1b1959 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -304,6 +304,9 @@ class BaseSerializer(WritableField): field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) + method = getattr(self, 'transform_%s' % field_name, None) + if callable(method): + value = method(obj, value) ret[key] = value ret.fields[key] = field return ret From 59cce01b3359aa009e697a99eabbf2ef322b28e2 Mon Sep 17 00:00:00 2001 From: Philip Douglas Date: Thu, 12 Sep 2013 16:03:20 +0100 Subject: [PATCH 02/53] Fix error when serializer gets files but no data --- rest_framework/serializers.py | 2 +- rest_framework/tests/test_files.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a63c7f6c2..778e72d1a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -156,7 +156,7 @@ class BaseSerializer(WritableField): self.context = context or {} - self.init_data = data + self.init_data = data or {} self.init_files = files self.object = instance self.fields = self.get_fields() diff --git a/rest_framework/tests/test_files.py b/rest_framework/tests/test_files.py index c13c38b86..127e379e9 100644 --- a/rest_framework/tests/test_files.py +++ b/rest_framework/tests/test_files.py @@ -80,3 +80,16 @@ class FileSerializerTests(TestCase): serializer = UploadedFileSerializer(data={'created': now, 'file': 'abc'}) self.assertFalse(serializer.is_valid()) self.assertEqual(serializer.errors, {'file': [errmsg]}) + + def test_validation_with_no_data(self): + """ + Validation should still function when no data dictionary is provided. + """ + now = datetime.datetime.now() + file = BytesIO(six.b('stuff')) + file.name = 'stuff.txt' + file.size = len(file.getvalue()) + uploaded_file = UploadedFile(file=file, created=now) + + serializer = UploadedFileSerializer(files={'file': file}) + self.assertFalse(serializer.is_valid()) \ No newline at end of file From 6e4bdb55969171c87296aba9711dbc77f8a1e366 Mon Sep 17 00:00:00 2001 From: Philip Douglas Date: Thu, 12 Sep 2013 16:04:33 +0100 Subject: [PATCH 03/53] Add missing newline at the end of test file --- rest_framework/tests/test_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_files.py b/rest_framework/tests/test_files.py index 127e379e9..78f4cf425 100644 --- a/rest_framework/tests/test_files.py +++ b/rest_framework/tests/test_files.py @@ -92,4 +92,4 @@ class FileSerializerTests(TestCase): uploaded_file = UploadedFile(file=file, created=now) serializer = UploadedFileSerializer(files={'file': file}) - self.assertFalse(serializer.is_valid()) \ No newline at end of file + self.assertFalse(serializer.is_valid()) From 272a6abf91c51b44781d27af5352c7e36c8fa91c Mon Sep 17 00:00:00 2001 From: Philip Douglas Date: Fri, 13 Sep 2013 10:46:24 +0100 Subject: [PATCH 04/53] Try a more localised fix to the data=None problem --- rest_framework/fields.py | 1 + rest_framework/serializers.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 210c2537d..0c3817b57 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -306,6 +306,7 @@ class WritableField(Field): return try: + data = data or {} if self.use_files: files = files or {} try: diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 778e72d1a..a63c7f6c2 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -156,7 +156,7 @@ class BaseSerializer(WritableField): self.context = context or {} - self.init_data = data or {} + self.init_data = data self.init_files = files self.object = instance self.fields = self.get_fields() From b74c5235c509738c7afea0be0dd8283bb8339ebe Mon Sep 17 00:00:00 2001 From: Colin Huang Date: Sun, 15 Sep 2013 21:56:43 -0700 Subject: [PATCH 05/53] [Add]: CustomValidationTests.test_partial_update This test is to make sure that validate_ is not called when partial=True and is not found in .data. --- rest_framework/tests/test_serializer.py | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index c24976603..9792685ec 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -496,6 +496,33 @@ class CustomValidationTests(TestCase): self.assertFalse(serializer.is_valid()) self.assertEqual(serializer.errors, {'email': ['Enter a valid email address.']}) + def test_partial_update(self): + """ + Make sure that validate_email isn't called when partial=True and email + isn't found in data. + """ + initial_data = { + 'email': 'tom@example.com', + 'content': 'A test comment', + 'created': datetime.datetime(2012, 1, 1) + } + + serializer = self.CommentSerializerWithFieldValidator(data=initial_data) + self.assertEqual(serializer.is_valid(), True) + instance = serializer.object + + new_content = 'An *updated* test comment' + partial_data = { + 'content': new_content + } + + serializer = self.CommentSerializerWithFieldValidator(instance=instance, + data=partial_data, + partial=True) + self.assertEqual(serializer.is_valid(), True) + instance = serializer.object + self.assertEqual(instance.content, new_content) + class PositiveIntegerAsChoiceTests(TestCase): def test_positive_integer_in_json_is_correctly_parsed(self): From 75d6446c8799763dccde0f5f03fbcae39c18dc7f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 26 Sep 2013 16:09:08 +0100 Subject: [PATCH 06/53] Allow .template_name attribute specified on view. Closes #1000 --- rest_framework/renderers.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 2ce51e97b..a27160d46 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -272,7 +272,9 @@ class TemplateHTMLRenderer(BaseRenderer): return [self.template_name] elif hasattr(view, 'get_template_names'): return view.get_template_names() - raise ImproperlyConfigured('Returned a template response with no template_name') + elif hasattr(view, 'template_name'): + return [view.template_name] + raise ImproperlyConfigured('Returned a template response with no `template_name` attribute set on either the view or response') def get_exception_template(self, response): template_names = [name % {'status_code': response.status_code} @@ -388,7 +390,7 @@ class HTMLFormRenderer(BaseRenderer): # likely change at some point. self.renderer_context = renderer_context or {} - request = renderer_context['request'] + request = self.renderer_context['request'] # Creating an on the fly form see: # http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python @@ -419,8 +421,13 @@ class BrowsableAPIRenderer(BaseRenderer): """ renderers = [renderer for renderer in view.renderer_classes if not issubclass(renderer, BrowsableAPIRenderer)] + non_template_renderers = [renderer for renderer in renderers + if not hasattr(renderer, 'get_template_names')] + if not renderers: return None + elif non_template_renderers: + return non_template_renderers[0]() return renderers[0]() def get_content(self, renderer, data, From 49847584a8524883181e4118a9df9bb62f597271 Mon Sep 17 00:00:00 2001 From: Krzysztof Jurewicz Date: Sun, 29 Sep 2013 12:49:38 +0200 Subject: [PATCH 07/53] Minor documentation fix. --- docs/topics/ajax-csrf-cors.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/ajax-csrf-cors.md b/docs/topics/ajax-csrf-cors.md index 0555b84dd..97dd4710b 100644 --- a/docs/topics/ajax-csrf-cors.md +++ b/docs/topics/ajax-csrf-cors.md @@ -6,7 +6,7 @@ ## Javascript clients -If your building a javascript client to interface with your Web API, you'll need to consider if the client can use the same authentication policy that is used by the rest of the website, and also determine if you need to use CSRF tokens or CORS headers. +If you’re building a JavaScript client to interface with your Web API, you'll need to consider if the client can use the same authentication policy that is used by the rest of the website, and also determine if you need to use CSRF tokens or CORS headers. AJAX requests that are made within the same context as the API they are interacting with will typically use `SessionAuthentication`. This ensures that once a user has logged in, any AJAX requests made can be authenticated using the same session-based authentication that is used for the rest of the website. From 8a1d3275795a6eea931cb0b67465c88d745bd2b6 Mon Sep 17 00:00:00 2001 From: Doron Pearl Date: Mon, 30 Sep 2013 14:08:46 -0400 Subject: [PATCH 08/53] corrected doc for throttle_classes decorator the decorator actually expects an array and otherwise raise an exception. --- docs/api-guide/throttling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index cc4692171..fc1525df6 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -59,7 +59,7 @@ using the `APIView` class based views. Or, if you're using the `@api_view` decorator with function based views. @api_view('GET') - @throttle_classes(UserRateThrottle) + @throttle_classes([UserRateThrottle]) def example_view(request, format=None): content = { 'status': 'request was permitted' From 61dd9ce914382ad7b26e2654a590215885308828 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 1 Oct 2013 15:47:47 -0600 Subject: [PATCH 09/53] Update release-notes.md Simple typo fix. --- docs/topics/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index e4294ae33..3df8869a2 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -58,7 +58,7 @@ You can determine your currently installed version using `pip freeze`: * 'Raw data' and 'HTML form' tab preference in browseable API now saved between page views. * Bugfix: `required=True` argument fixed for boolean serializer fields. * Bugfix: `client.force_authenticate(None)` should also clear session info if it exists. -* Bugfix: Client sending emptry string instead of file now clears `FileField`. +* Bugfix: Client sending empty string instead of file now clears `FileField`. * Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`. ### 2.3.7 From a14f1e886402b8d0f742fdbb912b03a4004e1735 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 2 Oct 2013 13:45:35 +0100 Subject: [PATCH 10/53] Serializers can now be rendered directly to HTML --- rest_framework/fields.py | 21 +++++++ rest_framework/renderers.py | 62 +------------------ rest_framework/serializers.py | 19 +++++- .../templates/rest_framework/form.html | 12 ++-- 4 files changed, 49 insertions(+), 65 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 210c2537d..16344d01b 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -123,6 +123,7 @@ class Field(object): use_files = False form_field_class = forms.CharField type_label = 'field' + widget = None def __init__(self, source=None, label=None, help_text=None): self.parent = None @@ -134,9 +135,29 @@ class Field(object): if label is not None: self.label = smart_text(label) + else: + self.label = None if help_text is not None: self.help_text = strip_multiple_choice_msg(smart_text(help_text)) + else: + self.help_text = None + + self._errors = [] + self._value = None + self._name = None + + @property + def errors(self): + return self._errors + + def widget_html(self): + if not self.widget: + return '' + return self.widget.render(self._name, self._value) + + def label_tag(self): + return '' % (self._name, self.label) def initialize(self, parent, field_name): """ diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index a27160d46..0e17edafb 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -336,71 +336,15 @@ class HTMLFormRenderer(BaseRenderer): template = 'rest_framework/form.html' charset = 'utf-8' - def data_to_form_fields(self, data): - fields = {} - for key, val in data.fields.items(): - if getattr(val, 'read_only', True): - # Don't include read-only fields. - continue - - if getattr(val, 'fields', None): - # Nested data not supported by HTML forms. - continue - - kwargs = {} - kwargs['required'] = val.required - - #if getattr(v, 'queryset', None): - # kwargs['queryset'] = v.queryset - - if getattr(val, 'choices', None) is not None: - kwargs['choices'] = val.choices - - if getattr(val, 'regex', None) is not None: - kwargs['regex'] = val.regex - - if getattr(val, 'widget', None): - widget = copy.deepcopy(val.widget) - kwargs['widget'] = widget - - if getattr(val, 'default', None) is not None: - kwargs['initial'] = val.default - - if getattr(val, 'label', None) is not None: - kwargs['label'] = val.label - - if getattr(val, 'help_text', None) is not None: - kwargs['help_text'] = val.help_text - - fields[key] = val.form_field_class(**kwargs) - - return fields - def render(self, data, accepted_media_type=None, renderer_context=None): """ Render serializer data and return an HTML form, as a string. """ - # The HTMLFormRenderer currently uses something of a hack to render - # the content, by translating each of the serializer fields into - # an html form field, creating a dynamic form using those fields, - # and then rendering that form. - - # This isn't strictly neccessary, as we could render the serilizer - # fields to HTML directly. The implementation is historical and will - # likely change at some point. - - self.renderer_context = renderer_context or {} - request = self.renderer_context['request'] - - # Creating an on the fly form see: - # http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python - fields = self.data_to_form_fields(data) - DynamicForm = type(str('DynamicForm'), (forms.Form,), fields) - data = None if data.empty else data + renderer_context = renderer_context or {} + request = renderer_context['request'] template = loader.get_template(self.template) - context = RequestContext(request, {'form': DynamicForm(data)}) - + context = RequestContext(request, {'form': data}) return template.render(context) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8d2e0feb8..206a8123b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -32,6 +32,13 @@ from rest_framework.relations import * from rest_framework.fields import * +def pretty_name(name): + """Converts 'first_name' to 'First name'""" + if not name: + return '' + return name.replace('_', ' ').capitalize() + + class RelationsList(list): _deleted = [] @@ -306,7 +313,17 @@ class BaseSerializer(WritableField): for field_name, field in self.fields.items(): field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) - value = field.field_to_native(obj, field_name) + if self._errors: + value = self.init_data.get(field_name) + else: + value = field.field_to_native(obj, field_name) + + field._errors = self._errors.get(key) if self._errors else None + field._name = field_name + field._value = value + if not field.label: + field.label = pretty_name(key) + ret[key] = value ret.fields[key] = field return ret diff --git a/rest_framework/templates/rest_framework/form.html b/rest_framework/templates/rest_framework/form.html index b27f652e9..b1e148df7 100644 --- a/rest_framework/templates/rest_framework/form.html +++ b/rest_framework/templates/rest_framework/form.html @@ -1,13 +1,15 @@ {% load rest_framework %} {% csrf_token %} {{ form.non_field_errors }} -{% for field in form %} -
+{% for field in form.fields.values %} + {% if not field.read_only %} +
{{ field.label_tag|add_class:"control-label" }}
- {{ field }} - {{ field.help_text }} - + {{ field.widget_html }} + {% if field.help_text %}{{ field.help_text }}{% endif %} + {% for error in field.errors %}{{ error }}{% endfor %}
+ {% endif %} {% endfor %} From 8d4ba478cc5725b4de6ab86b4825b1ea94cb4c7b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 2 Oct 2013 16:13:34 +0100 Subject: [PATCH 11/53] Fix rendering of forms and add error rendering on HTML form --- rest_framework/renderers.py | 16 +++++++++--- rest_framework/serializers.py | 26 +++++++++---------- .../templates/rest_framework/base.html | 4 +-- .../rest_framework/raw_data_form.html | 12 +++++++++ 4 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 rest_framework/templates/rest_framework/raw_data_form.html diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 0e17edafb..fe4f43d48 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -419,6 +419,13 @@ class BrowsableAPIRenderer(BaseRenderer): In the absence of the View having an associated form then return None. """ + if request.method == method: + data = request.DATA + files = request.FILES + 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): @@ -431,9 +438,10 @@ class BrowsableAPIRenderer(BaseRenderer): or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes)): return - serializer = view.get_serializer(instance=obj) - + serializer = view.get_serializer(instance=obj, data=data, files=files) + serializer.is_valid() data = serializer.data + form_renderer = self.form_renderer_class() return form_renderer.render(data, self.accepted_media_type, self.renderer_context) @@ -525,6 +533,7 @@ 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_put_or_patch_form = raw_data_put_form or raw_data_patch_form @@ -543,12 +552,11 @@ class BrowsableAPIRenderer(BaseRenderer): 'put_form': self.get_rendered_html_form(view, 'PUT', request), 'post_form': self.get_rendered_html_form(view, 'POST', request), - 'patch_form': self.get_rendered_html_form(view, 'PATCH', request), 'delete_form': self.get_rendered_html_form(view, 'DELETE', request), 'options_form': self.get_rendered_html_form(view, 'OPTIONS', request), 'raw_data_put_form': raw_data_put_form, - 'raw_data_post_form': self.get_raw_data_form(view, 'POST', request), + 'raw_data_post_form': raw_data_post_form, 'raw_data_patch_form': raw_data_patch_form, 'raw_data_put_or_patch_form': raw_data_put_or_patch_form, diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 206a8123b..bc9f73d11 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -308,24 +308,14 @@ class BaseSerializer(WritableField): """ ret = self._dict_class() ret.fields = self._dict_class() - ret.empty = obj is None for field_name, field in self.fields.items(): field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) - if self._errors: - value = self.init_data.get(field_name) - else: - value = field.field_to_native(obj, field_name) - - field._errors = self._errors.get(key) if self._errors else None - field._name = field_name - field._value = value - if not field.label: - field.label = pretty_name(key) - + value = field.field_to_native(obj, field_name) ret[key] = value - ret.fields[key] = field + ret.fields[key] = self.augment_field(field, field_name, key, value) + return ret def from_native(self, data, files): @@ -333,6 +323,7 @@ class BaseSerializer(WritableField): Deserialize primitives -> objects. """ self._errors = {} + if data is not None or files is not None: attrs = self.restore_fields(data, files) if attrs is not None: @@ -343,6 +334,15 @@ class BaseSerializer(WritableField): if not self._errors: return self.restore_object(attrs, instance=getattr(self, 'object', None)) + def augment_field(self, field, field_name, key, value): + # This horrible stuff is to manage serializers rendering to HTML + field._errors = self._errors.get(key) if self._errors else None + field._name = field_name + field._value = self.init_data.get(key) if self._errors and self.init_data else value + if not field.label: + field.label = pretty_name(key) + return field + def field_to_native(self, obj, field_name): """ Override default so that the serializer can be used as a nested field diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 2776d5500..33be36db8 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -151,7 +151,7 @@ {% with form=raw_data_post_form %}
- {% include "rest_framework/form.html" %} + {% include "rest_framework/raw_data_form.html" %}
@@ -188,7 +188,7 @@ {% with form=raw_data_put_or_patch_form %}
- {% include "rest_framework/form.html" %} + {% include "rest_framework/raw_data_form.html" %}
{% if raw_data_put_form %} diff --git a/rest_framework/templates/rest_framework/raw_data_form.html b/rest_framework/templates/rest_framework/raw_data_form.html new file mode 100644 index 000000000..075279f7e --- /dev/null +++ b/rest_framework/templates/rest_framework/raw_data_form.html @@ -0,0 +1,12 @@ +{% load rest_framework %} +{% csrf_token %} +{{ form.non_field_errors }} +{% for field in form %} +
+ {{ field.label_tag|add_class:"control-label" }} +
+ {{ field }} + {{ field.help_text }} +
+
+{% endfor %} From dc650f77b5f377bbfab3da66455feb57b195a1da Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Thu, 3 Oct 2013 11:34:42 +1300 Subject: [PATCH 12/53] add tests for transform_fieldname methods --- rest_framework/tests/test_serializer.py | 35 +++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index c24976603..ca876eae4 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -1643,3 +1643,38 @@ class SerializerSupportsManyRelationships(TestCase): serializer = SimpleSlugSourceModelSerializer(data={'text': 'foo', 'targets': [1, 2]}) self.assertTrue(serializer.is_valid()) self.assertEqual(serializer.data, {'text': 'foo', 'targets': [1, 2]}) + + +class TransformMethodsSerializer(serializers.Serializer): + a = serializers.CharField() + b_renamed = serializers.CharField(source='b') + + def transform_a(self, obj, value): + return value.lower() + + def transform_b_renamed(self, obj, value): + if value is not None: + return 'and ' + value + + +class TestSerializerTransformMethods(TestCase): + def setUp(self): + self.s = TransformMethodsSerializer() + + def test_transform_methods(self): + self.assertEqual( + self.s.to_native({'a': 'GREEN EGGS', 'b': 'HAM'}), + { + 'a': 'green eggs', + 'b_renamed': 'and HAM', + } + ) + + def test_missing_fields(self): + self.assertEqual( + self.s.to_native({'a': 'GREEN EGGS'}), + { + 'a': 'green eggs', + 'b_renamed': None, + } + ) From 42bbf6907e041d6abe773854b9aaa53eded82f4e Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Thu, 3 Oct 2013 12:38:42 +1300 Subject: [PATCH 13/53] docs: add paragraph on transform_fieldname methods --- docs/api-guide/serializers.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index bbc8d019d..2d3e999f9 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -63,6 +63,21 @@ At this point we've translated the model instance into Python native datatypes. json # '{"email": "leila@example.com", "content": "foo bar", "created": "2012-08-22T16:20:09.822"}' +### Customizing field representation + +Sometimes when serializing objects, you may not want to represent everything exactly the way it is in your model. + +If you need to customize the serialized value of a particular field, you can do this by creating a `transform_` method. For example if you needed to render some markdown from a text field: + + description = serializers.TextField() + description_html = serializers.TextField(source='description', read_only=True) + + def transform_description_html(self, obj, value): + from django.contrib.markup.templatetags.markup import markdown + return markdown(value) + +These methods are essentially the reverse of `validate_` (see *Validation* below.) + ## Deserializing objects Deserialization is similar. First we parse a stream into Python native datatypes... From f6301636fb52dc6e02fd55e1c07c0be0a3b4ebfd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 3 Oct 2013 15:18:38 +0100 Subject: [PATCH 14/53] Drop erronous left-over bit of docs. Closes #1147 --- docs/api-guide/serializers.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index a3cd1d6ab..6b91aa764 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -84,7 +84,6 @@ Deserialization is similar. First we parse a stream into Python native datatype # True serializer.object # - >>> serializer.deserialize('json', stream) When deserializing data, we can either create a new instance, or update an existing instance. From 3e94f4dc709143d577433c164873654e7c0579f8 Mon Sep 17 00:00:00 2001 From: Henry Clifford Date: Fri, 4 Oct 2013 10:49:56 -0400 Subject: [PATCH 15/53] support args on get_object_or_404 --- rest_framework/generics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 7d1bf7945..4f134bce6 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -25,13 +25,13 @@ def strict_positive_int(integer_string, cutoff=None): ret = min(ret, cutoff) return ret -def get_object_or_404(queryset, **filter_kwargs): +def get_object_or_404(queryset, *filter_args, **filter_kwargs): """ Same as Django's standard shortcut, but make sure to raise 404 if the filter_kwargs don't match the required types. """ try: - return _get_object_or_404(queryset, **filter_kwargs) + return _get_object_or_404(queryset, *filter_args, **filter_kwargs) except (TypeError, ValueError): raise Http404 From 3833f1d55f97db33596242d89dbed997221f3b82 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 4 Oct 2013 15:55:57 +0100 Subject: [PATCH 16/53] Added @hcliff for #1153. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index b5dce5040..586bb0f00 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -170,6 +170,7 @@ The following people have helped make REST framework great. * Ben Reilly - [bwreilly] * Tai Lee - [mrmachine] * Markus Kaiserswerth - [mkai] +* Henry Clifford - [hcliff] Many thanks to everyone who's contributed to the project. @@ -376,3 +377,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [bwreilly]: https://github.com/bwreilly [mrmachine]: https://github.com/mrmachine [mkai]: https://github.com/mkai +[hcliff]: https://github.com/hcliff From 48a38386afb8a8619d2f089ebce364c7a0a845b4 Mon Sep 17 00:00:00 2001 From: dpetzel Date: Sat, 5 Oct 2013 19:29:25 -0400 Subject: [PATCH 17/53] **very minor** typo fix --- docs/tutorial/1-serialization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 22d29285b..e1c0009c3 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -225,7 +225,7 @@ For the moment we won't use any of REST framework's other features, we'll just w We'll start off by creating a subclass of HttpResponse that we can use to render any data we return into `json`. -Edit the `snippet/views.py` file, and add the following. +Edit the `snippets/views.py` file, and add the following. from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt From 864497eebbd39ab3e811c589a44a43176caef1bf Mon Sep 17 00:00:00 2001 From: dpetzel Date: Sat, 5 Oct 2013 23:16:58 -0400 Subject: [PATCH 18/53] Be sure to import UserSerializer --- docs/tutorial/4-authentication-and-permissions.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 393d879a4..510aa2439 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -75,6 +75,10 @@ We'll also add a couple of views. We'd like to just use read-only views for the class UserDetail(generics.RetrieveAPIView): queryset = User.objects.all() serializer_class = UserSerializer + +Make sure to also import the `UserSerializer` class + + from snippets.serializers import UserSerializer Finally we need to add those views into the API, by referencing them from the URL conf. From 9e29c6389529210978d58cee78e437b901f9daa2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 10 Oct 2013 17:33:22 +0100 Subject: [PATCH 19/53] Ensure read-only fields don't break with current HTML renderer behavior --- rest_framework/fields.py | 3 +++ rest_framework/serializers.py | 2 ++ rest_framework/tests/test_serializer.py | 3 +-- rest_framework/tests/test_serializer_empty.py | 15 +++++++++++++++ 4 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 rest_framework/tests/test_serializer_empty.py diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 16344d01b..6b039f6c2 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -777,6 +777,7 @@ class IntegerField(WritableField): type_name = 'IntegerField' type_label = 'integer' form_field_class = forms.IntegerField + empty = 0 default_error_messages = { 'invalid': _('Enter a whole number.'), @@ -808,6 +809,7 @@ class FloatField(WritableField): type_name = 'FloatField' type_label = 'float' form_field_class = forms.FloatField + empty = 0 default_error_messages = { 'invalid': _("'%s' value must be a float."), @@ -828,6 +830,7 @@ class DecimalField(WritableField): type_name = 'DecimalField' type_label = 'decimal' form_field_class = forms.DecimalField + empty = Decimal('0') default_error_messages = { 'invalid': _('Enter a number.'), diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index bc9f73d11..8e9456885 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -310,6 +310,8 @@ class BaseSerializer(WritableField): ret.fields = self._dict_class() for field_name, field in self.fields.items(): + if field.read_only and obj is None: + continue field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 8d246b010..d4e5a93f6 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -159,8 +159,7 @@ class BasicTests(TestCase): expected = { 'email': '', 'content': '', - 'created': None, - 'sub_comment': '' + 'created': None } self.assertEqual(serializer.data, expected) diff --git a/rest_framework/tests/test_serializer_empty.py b/rest_framework/tests/test_serializer_empty.py new file mode 100644 index 000000000..30cff3615 --- /dev/null +++ b/rest_framework/tests/test_serializer_empty.py @@ -0,0 +1,15 @@ +from django.test import TestCase +from rest_framework import serializers + + +class EmptySerializerTestCase(TestCase): + def test_empty_serializer(self): + class FooBarSerializer(serializers.Serializer): + foo = serializers.IntegerField() + bar = serializers.SerializerMethodField('get_bar') + + def get_bar(self, obj): + return 'bar' + + serializer = FooBarSerializer() + self.assertEquals(serializer.data, {'foo': 0}) From 7c3769f04b5ec2cd14dcbd7e3601d59092255906 Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Fri, 11 Oct 2013 15:31:55 +1300 Subject: [PATCH 20/53] fix writing into foreign key with non-null source --- rest_framework/serializers.py | 2 +- .../tests/test_serializer_nested.py | 67 +++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 33db82ee1..fa5ac1431 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -403,7 +403,7 @@ class BaseSerializer(WritableField): return # Set the serializer object if it exists - obj = getattr(self.parent.object, field_name) if self.parent.object else None + obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None obj = obj.all() if is_simple_callable(getattr(obj, 'all', None)) else obj if self.source == '*': diff --git a/rest_framework/tests/test_serializer_nested.py b/rest_framework/tests/test_serializer_nested.py index 71d0e24b5..e454235a1 100644 --- a/rest_framework/tests/test_serializer_nested.py +++ b/rest_framework/tests/test_serializer_nested.py @@ -244,3 +244,70 @@ class WritableNestedSerializerObjectTests(TestCase): serializer = self.AlbumSerializer(data=data, many=True) self.assertEqual(serializer.is_valid(), True) self.assertEqual(serializer.object, expected_object) + + +class ForeignKeyNestedSerializerUpdateTests(TestCase): + def setUp(self): + class Artist(object): + def __init__(self, name): + self.name = name + + def __eq__(self, other): + return self.name == other.name + + class Album(object): + def __init__(self, name, artist): + self.name, self.artist = name, artist + + def __eq__(self, other): + return self.name == other.name and self.artist == other.artist + + class ArtistSerializer(serializers.Serializer): + name = serializers.CharField() + + def restore_object(self, attrs, instance=None): + if instance: + instance.name = attrs['name'] + else: + instance = Artist(attrs['name']) + return instance + + class AlbumSerializer(serializers.Serializer): + name = serializers.CharField() + by = ArtistSerializer(source='artist') + + def restore_object(self, attrs, instance=None): + if instance: + instance.name = attrs['name'] + instance.artist = attrs['artist'] + else: + instance = Album(attrs['name'], attrs['artist']) + return instance + + self.Artist = Artist + self.Album = Album + self.AlbumSerializer = AlbumSerializer + + def test_create_via_foreign_key_with_source(self): + """ + Check that we can both *create* and *update* into objects across + ForeignKeys that have a `source` specified. + Regression test for # + """ + data = { + 'name': 'Discovery', + 'by': {'name': 'Daft Punk'}, + } + + expected = self.Album(artist=self.Artist('Daft Punk'), name='Discovery') + + # create + serializer = self.AlbumSerializer(data=data) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.object, expected) + + # update + original = self.Album(artist=self.Artist('The Bats'), name='Free All the Monsters') + serializer = self.AlbumSerializer(instance=original, data=data) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.object, expected) From 86ea969e1154de20a53fc5b853e8340508648e98 Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Fri, 11 Oct 2013 15:50:07 +1300 Subject: [PATCH 21/53] fix ticket link in test docstring --- rest_framework/tests/test_serializer_nested.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_serializer_nested.py b/rest_framework/tests/test_serializer_nested.py index e454235a1..029f8bffd 100644 --- a/rest_framework/tests/test_serializer_nested.py +++ b/rest_framework/tests/test_serializer_nested.py @@ -292,7 +292,7 @@ class ForeignKeyNestedSerializerUpdateTests(TestCase): """ Check that we can both *create* and *update* into objects across ForeignKeys that have a `source` specified. - Regression test for # + Regression test for #1170 """ data = { 'name': 'Discovery', From 89ac03af26a63a2126165e8995f7936798ce0450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20L=C3=A1zaro?= Date: Sat, 12 Oct 2013 20:31:33 +0200 Subject: [PATCH 22/53] Add missing commas in relations.md --- docs/api-guide/relations.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 5ec4b22fc..b9d96b5e3 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -54,7 +54,7 @@ Would serialize to the following representation. { 'album_name': 'Things We Lost In The Fire', - 'artist': 'Low' + 'artist': 'Low', 'tracks': [ '1: Sunflower', '2: Whitetail', @@ -86,7 +86,7 @@ Would serialize to a representation like this: { 'album_name': 'The Roots', - 'artist': 'Undun' + 'artist': 'Undun', 'tracks': [ 89, 90, @@ -121,7 +121,7 @@ Would serialize to a representation like this: { 'album_name': 'Graceland', - 'artist': 'Paul Simon' + 'artist': 'Paul Simon', 'tracks': [ 'http://www.example.com/api/tracks/45/', 'http://www.example.com/api/tracks/46/', @@ -159,7 +159,7 @@ Would serialize to a representation like this: { 'album_name': 'Dear John', - 'artist': 'Loney Dear' + 'artist': 'Loney Dear', 'tracks': [ 'Airport Surroundings', 'Everything Turns to You', @@ -194,7 +194,7 @@ Would serialize to a representation like this: { 'album_name': 'The Eraser', - 'artist': 'Thom Yorke' + 'artist': 'Thom Yorke', 'track_listing': 'http://www.example.com/api/track_list/12/', } @@ -234,7 +234,7 @@ Would serialize to a nested representation like this: { 'album_name': 'The Grey Album', - 'artist': 'Danger Mouse' + 'artist': 'Danger Mouse', 'tracks': [ {'order': 1, 'title': 'Public Service Announcement'}, {'order': 2, 'title': 'What More Can I Say'}, @@ -271,7 +271,7 @@ This custom field would then serialize to the following representation. { 'album_name': 'Sometimes I Wish We Were an Eagle', - 'artist': 'Bill Callahan' + 'artist': 'Bill Callahan', 'tracks': [ 'Track 1: Jim Cain (04:39)', 'Track 2: Eid Ma Clack Shaw (04:19)', From c6be12f02b5e07e412c8c91b368566a85364b907 Mon Sep 17 00:00:00 2001 From: Colin Huang Date: Sun, 15 Sep 2013 18:03:52 -0700 Subject: [PATCH 23/53] [Fix]: Error with partial=True and validate_ The error occurs when serializer is set with partial=True and a field-level validation is defined on a field, for which there's no corresponding update value in .data --- rest_framework/serializers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a63c7f6c2..0b5ae042a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -255,10 +255,13 @@ class BaseSerializer(WritableField): for field_name, field in self.fields.items(): if field_name in self._errors: continue + + source = field.source or field_name + if self.partial and source not in attrs: + continue try: validate_method = getattr(self, 'validate_%s' % field_name, None) if validate_method: - source = field.source or field_name attrs = validate_method(attrs, source) except ValidationError as err: self._errors[field_name] = self._errors.get(field_name, []) + list(err.messages) From e83bc003234418fc6b21b841de216319491bd38d Mon Sep 17 00:00:00 2001 From: Rikki Date: Wed, 16 Oct 2013 03:03:51 +0100 Subject: [PATCH 24/53] Added name of file to edit So reader doesn't have to remember, or check through all the files to find where this code fragment was, mention the file name when it is relevant. --- docs/tutorial/2-requests-and-responses.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 6ff97f37d..ba9eb7237 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -35,7 +35,7 @@ The wrappers also provide behaviour such as returning `405 Method Not Allowed` r Okay, let's go ahead and start using these new components to write a few views. -We don't need our `JSONResponse` class anymore, so go ahead and delete that. Once that's done we can start refactoring our views slightly. +We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and delete that. Once that's done we can start refactoring our views slightly. from rest_framework import status from rest_framework.decorators import api_view @@ -64,7 +64,7 @@ We don't need our `JSONResponse` class anymore, so go ahead and delete that. On Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious. -Here is the view for an individual snippet. +Here is the view for an individual snippet (still in `views.py`). @api_view(['GET', 'PUT', 'DELETE']) def snippet_detail(request, pk): From cb123e896ed2dca230088296db9663af5a53252d Mon Sep 17 00:00:00 2001 From: Rikki Date: Wed, 16 Oct 2013 03:08:43 +0100 Subject: [PATCH 25/53] Mention name of file to edit To reduce unnecessary cognitive load of the learner, name the file they are putting this code in. --- docs/tutorial/3-class-based-views.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index 9fc424fee..67a75d9f4 100644 --- a/docs/tutorial/3-class-based-views.md +++ b/docs/tutorial/3-class-based-views.md @@ -4,7 +4,7 @@ We can also write our API views using class based views, rather than function ba ## Rewriting our API using class based views -We'll start by rewriting the root view as a class based view. All this involves is a little bit of refactoring. +We'll start by rewriting the root view as a class based view. All this involves is a little bit of refactoring of `views.py`. from snippets.models import Snippet from snippets.serializers import SnippetSerializer @@ -30,7 +30,7 @@ We'll start by rewriting the root view as a class based view. All this involves return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -So far, so good. It looks pretty similar to the previous case, but we've got better separation between the different HTTP methods. We'll also need to update the instance view. +So far, so good. It looks pretty similar to the previous case, but we've got better separation between the different HTTP methods. We'll also need to update the instance view in `views.py`. class SnippetDetail(APIView): """ @@ -62,7 +62,7 @@ So far, so good. It looks pretty similar to the previous case, but we've got be That's looking good. Again, it's still pretty similar to the function based view right now. -We'll also need to refactor our URLconf slightly now we're using class based views. +We'll also need to refactor our `urls.py` slightly now we're using class based views. from django.conf.urls import patterns, url from rest_framework.urlpatterns import format_suffix_patterns @@ -83,7 +83,7 @@ One of the big wins of using class based views is that it allows us to easily co The create/retrieve/update/delete operations that we've been using so far are going to be pretty similar for any model-backed API views we create. Those bits of common behaviour are implemented in REST framework's mixin classes. -Let's take a look at how we can compose our views by using the mixin classes. +Let's take a look at how we can compose our `views.py` by using the mixin classes. from snippets.models import Snippet from snippets.serializers import SnippetSerializer @@ -126,7 +126,7 @@ Pretty similar. Again we're using the `GenericAPIView` class to provide the cor ## Using generic class based views -Using the mixin classes we've rewritten the views to use slightly less code than before, but we can go one step further. REST framework provides a set of already mixed-in generic views that we can use. +Using the mixin classes we've rewritten the views to use slightly less code than before, but we can go one step further. REST framework provides a set of already mixed-in generic views that we can use to trim down `views.py` even more. from snippets.models import Snippet from snippets.serializers import SnippetSerializer From bf6084895263f827a80191fd6ed4eb437b555f9a Mon Sep 17 00:00:00 2001 From: Rikki Date: Wed, 16 Oct 2013 03:21:43 +0100 Subject: [PATCH 26/53] Using the filenames where relevant Sometimes it's hard to tell which file the code is intended to go in. Now it spells it out. --- docs/tutorial/4-authentication-and-permissions.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 510aa2439..ecf92a7b1 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -12,7 +12,7 @@ Currently our API doesn't have any restrictions on who can edit or delete code s We're going to make a couple of changes to our `Snippet` model class. First, let's add a couple of fields. One of those fields will be used to represent the user who created the code snippet. The other field will be used to store the highlighted HTML representation of the code. -Add the following two fields to the model. +Add the following two fields to the `Snippet` model in `models.py`. owner = models.ForeignKey('auth.User', related_name='snippets') highlighted = models.TextField() @@ -52,7 +52,7 @@ You might also want to create a few different users, to use for testing the API. ## Adding endpoints for our User models -Now that we've got some users to work with, we'd better add representations of those users to our API. Creating a new serializer is easy: +Now that we've got some users to work with, we'd better add representations of those users to our API. Creating a new serializer is easy. In `serializers.py` add: from django.contrib.auth.models import User @@ -65,7 +65,7 @@ Now that we've got some users to work with, we'd better add representations of t Because `'snippets'` is a *reverse* relationship on the User model, it will not be included by default when using the `ModelSerializer` class, so we needed to add an explicit field for it. -We'll also add a couple of views. We'd like to just use read-only views for the user representations, so we'll use the `ListAPIView` and `RetrieveAPIView` generic class based views. +We'll also add a couple of views to `views.py`. We'd like to just use read-only views for the user representations, so we'll use the `ListAPIView` and `RetrieveAPIView` generic class based views. class UserList(generics.ListAPIView): queryset = User.objects.all() @@ -80,7 +80,7 @@ Make sure to also import the `UserSerializer` class from snippets.serializers import UserSerializer -Finally we need to add those views into the API, by referencing them from the URL conf. +Finally we need to add those views into the API, by referencing them from the URL conf. Add the following to the patterns in `urls.py`. url(r'^users/$', views.UserList.as_view()), url(r'^users/(?P[0-9]+)/$', views.UserDetail.as_view()), @@ -98,7 +98,7 @@ On **both** the `SnippetList` and `SnippetDetail` view classes, add the followin ## Updating our serializer -Now that snippets are associated with the user that created them, let's update our `SnippetSerializer` to reflect that. Add the following field to the serializer definition: +Now that snippets are associated with the user that created them, let's update our `SnippetSerializer` to reflect that. Add the following field to the serializer definition in `serializers.py`: owner = serializers.Field(source='owner.username') From d31fd33f4bbd52fa60949b15c2614528991e2c7a Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Tue, 8 Oct 2013 16:15:15 +0200 Subject: [PATCH 27/53] Allow to customize description so that markup can be accepted if needed. --- rest_framework/templates/rest_framework/base.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 33be36db8..7ab17dff4 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -110,7 +110,9 @@
+ {% block description %} {{ description }} + {% endblock %}
{{ request.method }} {{ request.get_full_path }}
From 8a5fea06f01ed4c5114ec0743516b6e6179c88b4 Mon Sep 17 00:00:00 2001 From: badaud_t Date: Thu, 17 Oct 2013 01:07:50 +0200 Subject: [PATCH 28/53] Fix typo YAMLRendererTests --- rest_framework/tests/test_renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index df6f4aa63..78a7dac89 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -328,7 +328,7 @@ if yaml: class YAMLRendererTests(TestCase): """ - Tests specific to the JSON Renderer + Tests specific to the YAML Renderer """ def test_render(self): From b730aec0f46e2b849b3c597bcf1a1bcdc158e415 Mon Sep 17 00:00:00 2001 From: badaud_t Date: Thu, 17 Oct 2013 01:08:24 +0200 Subject: [PATCH 29/53] Fix decimal support with YAMLRenderer --- rest_framework/tests/test_renderers.py | 11 +++++++++++ rest_framework/utils/encoders.py | 3 +++ 2 files changed, 14 insertions(+) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 78a7dac89..76299a890 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -354,6 +354,17 @@ if yaml: data = parser.parse(StringIO(content)) self.assertEqual(obj, data) + def test_render_decimal(self): + """ + Test YAML decimal rendering. + """ + renderer = YAMLRenderer() + content = renderer.render({'field': Decimal('111.2')}, 'application/yaml') + self.assertYAMLContains(content, "field: '111.2'") + + def assertYAMLContains(self, content, string): + self.assertTrue(string in content, '%r not in %r' % (string, content)) + class XMLRendererTestCase(TestCase): """ diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 7efd5417b..35ad206b9 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -89,6 +89,9 @@ else: node.flow_style = best_style return node + SafeDumper.add_representer(decimal.Decimal, + SafeDumper.represent_decimal) + SafeDumper.add_representer(SortedDict, yaml.representer.SafeRepresenter.represent_dict) SafeDumper.add_representer(DictWithMetadata, From 545ee013e332db0a81e5bd82b76f30b7d2d08b12 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 17 Oct 2013 09:40:06 +0100 Subject: [PATCH 30/53] Added @badale, for work on #1179. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 586bb0f00..9a20028cd 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -171,6 +171,7 @@ The following people have helped make REST framework great. * Tai Lee - [mrmachine] * Markus Kaiserswerth - [mkai] * Henry Clifford - [hcliff] +* Thomas Badaud - [badale] Many thanks to everyone who's contributed to the project. @@ -378,3 +379,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [mrmachine]: https://github.com/mrmachine [mkai]: https://github.com/mkai [hcliff]: https://github.com/hcliff +[badale]: https://github.com/badale From cc3c16eaa09c7dc63592ae8bf4ee30f1af263be1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Reni=C3=A9?= Date: Mon, 14 Oct 2013 16:28:32 +0200 Subject: [PATCH 31/53] Fix a docstring to reflect what the method does --- rest_framework/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 33db82ee1..6801e24d4 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -912,7 +912,7 @@ class ModelSerializer(Serializer): def save_object(self, obj, **kwargs): """ - Save the deserialized object and return it. + Save the deserialized object. """ if getattr(obj, '_nested_forward_relations', None): # Nested relationships need to be saved before we can save the From daf927ef68ce992055e8b7bc1a07cf03ee67b742 Mon Sep 17 00:00:00 2001 From: Colin Date: Thu, 17 Oct 2013 12:28:58 -0700 Subject: [PATCH 32/53] add @tamakisquare for work on #1111 --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 9a20028cd..e9c45965d 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -172,6 +172,7 @@ The following people have helped make REST framework great. * Markus Kaiserswerth - [mkai] * Henry Clifford - [hcliff] * Thomas Badaud - [badale] +* Colin Huang - [tamakisquare] Many thanks to everyone who's contributed to the project. @@ -380,3 +381,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [mkai]: https://github.com/mkai [hcliff]: https://github.com/hcliff [badale]: https://github.com/badale +[tamakisquare]: https://github.com/tamakisquare From 78c8e6de40f89580b9a4cefb6595d52bc1a6afbc Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 18 Oct 2013 09:10:54 +0100 Subject: [PATCH 33/53] Update 2-requests-and-responses.md --- docs/tutorial/2-requests-and-responses.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index ba9eb7237..7fa4f3e4a 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -64,7 +64,7 @@ We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and de Our instance view is an improvement over the previous example. It's a little more concise, and the code now feels very similar to if we were working with the Forms API. We're also using named status codes, which makes the response meanings more obvious. -Here is the view for an individual snippet (still in `views.py`). +Here is the view for an individual snippet, in the `views.py` module. @api_view(['GET', 'PUT', 'DELETE']) def snippet_detail(request, pk): From c3aeb16557f2cbb1c1218b5af7bab646e4958234 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 18 Oct 2013 09:32:04 +0100 Subject: [PATCH 34/53] Update 3-class-based-views.md --- docs/tutorial/3-class-based-views.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index 67a75d9f4..b37bc31bd 100644 --- a/docs/tutorial/3-class-based-views.md +++ b/docs/tutorial/3-class-based-views.md @@ -83,7 +83,7 @@ One of the big wins of using class based views is that it allows us to easily co The create/retrieve/update/delete operations that we've been using so far are going to be pretty similar for any model-backed API views we create. Those bits of common behaviour are implemented in REST framework's mixin classes. -Let's take a look at how we can compose our `views.py` by using the mixin classes. +Let's take a look at how we can compose the views by using the mixin classes. Here's our `views.py` module again. from snippets.models import Snippet from snippets.serializers import SnippetSerializer @@ -126,7 +126,7 @@ Pretty similar. Again we're using the `GenericAPIView` class to provide the cor ## Using generic class based views -Using the mixin classes we've rewritten the views to use slightly less code than before, but we can go one step further. REST framework provides a set of already mixed-in generic views that we can use to trim down `views.py` even more. +Using the mixin classes we've rewritten the views to use slightly less code than before, but we can go one step further. REST framework provides a set of already mixed-in generic views that we can use to trim down our `views.py` module even more. from snippets.models import Snippet from snippets.serializers import SnippetSerializer From 63e6a3b4925bf54e80ae63502a0353136e846b31 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sat, 19 Oct 2013 20:43:23 -0700 Subject: [PATCH 35/53] paginator should validate page and provide default - use the standard paginator.validate_number method rather strict_postive_int. - support optional paginator method, default_page_number, to get the default page number rather than hard-coding it to 1 - this allows supporting non-integer based pagination which can be an important performance tweak on extermely large datasets or high request loads - relatively thorough unit tests of the changes --- rest_framework/generics.py | 14 +++- rest_framework/tests/test_pagination.py | 88 +++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 4f134bce6..6b42a1d5f 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -145,10 +145,18 @@ class GenericAPIView(views.APIView): allow_empty_first_page=self.allow_empty) page_kwarg = self.kwargs.get(self.page_kwarg) page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg) - page = page_kwarg or page_query_param or 1 + page = page_kwarg or page_query_param + if not page: + # we didn't recieve a page + if hasattr(paginator, 'default_page_number'): + # our paginator has a method that will provide a default + page = paginator.default_page_number() + else: + # fall back on the base default value + page = 1 try: - page_number = strict_positive_int(page) - except ValueError: + page_number = paginator.validate_number(page) + except InvalidPage: if page == 'last': page_number = paginator.num_pages else: diff --git a/rest_framework/tests/test_pagination.py b/rest_framework/tests/test_pagination.py index d6bc7895c..a1118f1ec 100644 --- a/rest_framework/tests/test_pagination.py +++ b/rest_framework/tests/test_pagination.py @@ -430,3 +430,91 @@ class TestCustomPaginationSerializer(TestCase): 'objects': ['john', 'paul'] } self.assertEqual(serializer.data, expected) + + +class NonIntegerPage(object): + + def __init__(self, paginator, object_list, prev_token, token, next_token): + self.paginator = paginator + self.object_list = object_list + self.prev_token = prev_token + self.token = token + self.next_token = next_token + + def has_next(self): + return not not self.next_token + + def next_page_number(self): + return self.next_token + + def has_previous(self): + return not not self.prev_token + + def previous_page_number(self): + return self.prev_token + + +class NonIntegerPaginator(object): + + def __init__(self, object_list, per_page): + self.object_list = object_list + self.per_page = per_page + + def count(self): + # pretend like we don't know how many pages we have + return None + + def default_page_token(self): + return None + + def page(self, token=None): + if token: + try: + first = self.object_list.index(token) + except ValueError: + first = 0 + else: + first = 0 + n = len(self.object_list) + last = min(first + self.per_page, n) + prev_token = self.object_list[last - (2 * self.per_page)] if first else None + next_token = self.object_list[last] if last < n else None + return NonIntegerPage(self, self.object_list[first:last], prev_token, token, next_token) + + +class TestNonIntegerPagination(TestCase): + + + def test_custom_pagination_serializer(self): + objects = ['john', 'paul', 'george', 'ringo'] + paginator = NonIntegerPaginator(objects, 2) + + request = APIRequestFactory().get('/foobar') + serializer = CustomPaginationSerializer( + instance=paginator.page(), + context={'request': request} + ) + expected = { + 'links': { + 'next': 'http://testserver/foobar?page={0}'.format(objects[2]), + 'prev': None + }, + 'total_results': None, + 'objects': objects[:2] + } + self.assertEqual(serializer.data, expected) + + request = APIRequestFactory().get('/foobar') + serializer = CustomPaginationSerializer( + instance=paginator.page('george'), + context={'request': request} + ) + expected = { + 'links': { + 'next': None, + 'prev': 'http://testserver/foobar?page={0}'.format(objects[0]), + }, + 'total_results': None, + 'objects': objects[2:] + } + self.assertEqual(serializer.data, expected) From ed9c3258a6f9df6fabb569a65f3eb3363affa523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Mon, 21 Oct 2013 10:24:06 +0200 Subject: [PATCH 36/53] Remove the detail=None from APIException signature The documentation not match with the implementation. The APIException doesn't have detail parameter in the constructor class, actually doesn't have constructor method at all. --- docs/api-guide/exceptions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index 0c48783a3..c46d415e4 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -82,7 +82,7 @@ Note that the exception handler will only be called for responses generated by r ## APIException -**Signature:** `APIException(detail=None)` +**Signature:** `APIException()` The **base class** for all exceptions raised inside REST framework. From 70b0798118c8c02903421bca03e0406fe65d737f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 21 Oct 2013 09:30:01 +0100 Subject: [PATCH 37/53] Add invite signup --- docs/template.html | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/template.html b/docs/template.html index a20c81110..749d0afe3 100644 --- a/docs/template.html +++ b/docs/template.html @@ -167,7 +167,32 @@
+ + +
From 76672787cdba6a4ab8173b51fa099c910556889b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 21 Oct 2013 09:47:07 +0100 Subject: [PATCH 38/53] Added . Closes #1188. --- docs/api-guide/generic-views.md | 3 ++- rest_framework/generics.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index dc0076dff..24fc0bc71 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -65,7 +65,8 @@ The following attributes control the basic view behavior. * `queryset` - The queryset that should be used for returning objects from this view. Typically, you must either set this attribute, or override the `get_queryset()` method. * `serializer_class` - The serializer class that should be used for validating and deserializing input, and for serializing output. Typically, you must either set this attribute, or override the `get_serializer_class()` method. -* `lookup_field` - The field that should be used to lookup individual model instances. Defaults to `'pk'`. The URL conf should include a keyword argument corresponding to this value. More complex lookup styles can be supported by overriding the `get_object()` method. Note that when using hyperlinked APIs you'll need to ensure that *both* the API views *and* the serializer classes use lookup fields that correctly correspond with the URL conf. +* `lookup_field` - The model field that should be used to for performing object lookup of individual model instances. Defaults to `'pk'`. Note that when using hyperlinked APIs you'll need to ensure that *both* the API views *and* the serializer classes set the lookup fields if you need to use a custom value. +* `lookup_url_kwarg` - The URL keyword argument that should be used for object lookup. The URL conf should include a keyword argument corresponding to this value. If unset this defaults to using the same value as `lookup_field`. **Shortcuts**: diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 4f134bce6..f46dea762 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -54,6 +54,7 @@ class GenericAPIView(views.APIView): # If you want to use object lookups other than pk, set this attribute. # For more complex lookup requirements override `get_object()`. lookup_field = 'pk' + lookup_url_kwarg = None # Pagination settings paginate_by = api_settings.PAGINATE_BY @@ -278,9 +279,11 @@ class GenericAPIView(views.APIView): pass # Deprecation warning # Perform the lookup filtering. + # Note that `pk` and `slug` are deprecated styles of lookup filtering. + lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field + lookup = self.kwargs.get(lookup_url_kwarg, None) pk = self.kwargs.get(self.pk_url_kwarg, None) slug = self.kwargs.get(self.slug_url_kwarg, None) - lookup = self.kwargs.get(self.lookup_field, None) if lookup is not None: filter_kwargs = {self.lookup_field: lookup} From 216ac8a5c1ba39bf24e4e91b6fac7e0ac1dee7e4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 21 Oct 2013 17:19:28 +0100 Subject: [PATCH 39/53] Use lookup_url_kwarg in presave if required --- rest_framework/mixins.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 426865ff9..4606c78b6 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -158,7 +158,8 @@ class UpdateModelMixin(object): Set any attributes on the object that are implicit in the request. """ # pk and/or slug attributes are implicit in the URL. - lookup = self.kwargs.get(self.lookup_field, None) + lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field + lookup = self.kwargs.get(lookup_url_kwarg, None) pk = self.kwargs.get(self.pk_url_kwarg, None) slug = self.kwargs.get(self.slug_url_kwarg, None) slug_field = slug and self.slug_field or None From f0a129dcda3d671b88b5049d9ddaec53a4b32faf Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Mon, 21 Oct 2013 14:23:06 -0700 Subject: [PATCH 40/53] retract the default page stuff. better way comming in a seperate pr --- rest_framework/generics.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 6b42a1d5f..4015ab20a 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -145,15 +145,7 @@ class GenericAPIView(views.APIView): allow_empty_first_page=self.allow_empty) page_kwarg = self.kwargs.get(self.page_kwarg) page_query_param = self.request.QUERY_PARAMS.get(self.page_kwarg) - page = page_kwarg or page_query_param - if not page: - # we didn't recieve a page - if hasattr(paginator, 'default_page_number'): - # our paginator has a method that will provide a default - page = paginator.default_page_number() - else: - # fall back on the base default value - page = 1 + page = page_kwarg or page_query_param or 1 try: page_number = paginator.validate_number(page) except InvalidPage: From c36122a7ba2cdc69f94f5732f26428329be54200 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Mon, 21 Oct 2013 14:26:21 -0700 Subject: [PATCH 41/53] remove stray func from test --- rest_framework/tests/test_pagination.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/rest_framework/tests/test_pagination.py b/rest_framework/tests/test_pagination.py index a1118f1ec..cadb515fa 100644 --- a/rest_framework/tests/test_pagination.py +++ b/rest_framework/tests/test_pagination.py @@ -464,9 +464,6 @@ class NonIntegerPaginator(object): # pretend like we don't know how many pages we have return None - def default_page_token(self): - return None - def page(self, token=None): if token: try: From fa87fac61b87858e80788fc233591fa11dbc18e7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 22 Oct 2013 10:21:06 +0100 Subject: [PATCH 42/53] Added @ross for work on #1187. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index e9c45965d..cd3b37107 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -173,6 +173,7 @@ The following people have helped make REST framework great. * Henry Clifford - [hcliff] * Thomas Badaud - [badale] * Colin Huang - [tamakisquare] +* Ross McFarland - [ross] Many thanks to everyone who's contributed to the project. @@ -382,3 +383,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [hcliff]: https://github.com/hcliff [badale]: https://github.com/badale [tamakisquare]: https://github.com/tamakisquare +[ross]: https://github.com/ross From 25c9d552c05527f4b8b257d59cd7be39005f3668 Mon Sep 17 00:00:00 2001 From: Jacek Bzdak Date: Tue, 22 Oct 2013 13:11:14 +0200 Subject: [PATCH 43/53] Explained a bit more about django-filter implementation. Well, I spent some time trying to gues how djang-filter works, and if this changes would be introduced, I would have saved this time. --- docs/api-guide/filtering.md | 43 +++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 784aa585e..bcb0bb413 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -165,8 +165,8 @@ For more advanced filtering requirements you can specify a `FilterSet` class tha from rest_framework import generics class ProductFilter(django_filters.FilterSet): - min_price = django_filters.NumberFilter(lookup_type='gte') - max_price = django_filters.NumberFilter(lookup_type='lte') + min_price = django_filters.NumberFilter(name="price", lookup_type='gte') + max_price = django_filters.NumberFilter(name="price", lookup_type='lte') class Meta: model = Product fields = ['category', 'in_stock', 'min_price', 'max_price'] @@ -176,12 +176,51 @@ For more advanced filtering requirements you can specify a `FilterSet` class tha serializer_class = ProductSerializer filter_class = ProductFilter + Which will allow you to make requests such as: http://example.com/api/products?category=clothing&max_price=10.00 For more details on using filter sets see the [django-filter documentation][django-filter-docs]. +You can also span relationships using `django-filter`, let's assume that each +product has foreign key to `Manufacturer` model, so we create filter that +filters using `Manufacturer` name. For example: + + import django_filters + from myapp.models import Product + from myapp.serializers import ProductSerializer + from rest_framework import generics + + class ProductFilter(django_filters.FilterSet): + class Meta: + model = Product + fields = ['category', 'in_stock', 'manufacturer__name`] + +This enables us to make queries like: + + http://example.com/api/products?manufacturer__name=foo + +This is nice, but it shows underlying model structure in REST API, which may +be undesired, but you can use: + + import django_filters + from myapp.models import Product + from myapp.serializers import ProductSerializer + from rest_framework import generics + + class ProductFilter(django_filters.FilterSet): + + manufacturer = django_filters.CharFilter(name="manufacturer__name") + + class Meta: + model = Product + fields = ['category', 'in_stock', 'manufacturer`] + +And now you can execute: + + http://example.com/api/products?manufacturer=foo + --- **Hints & Tips** From cc9c7cd8a479b7fa76a66b8669e4a62fd78be867 Mon Sep 17 00:00:00 2001 From: Jacek Bzdak Date: Tue, 22 Oct 2013 13:15:48 +0200 Subject: [PATCH 44/53] Small documentation fix --- docs/api-guide/filtering.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index bcb0bb413..a0132ffcb 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -181,8 +181,6 @@ Which will allow you to make requests such as: http://example.com/api/products?category=clothing&max_price=10.00 -For more details on using filter sets see the [django-filter documentation][django-filter-docs]. - You can also span relationships using `django-filter`, let's assume that each product has foreign key to `Manufacturer` model, so we create filter that filters using `Manufacturer` name. For example: @@ -220,6 +218,8 @@ be undesired, but you can use: And now you can execute: http://example.com/api/products?manufacturer=foo + +For more details on using filter sets see the [django-filter documentation][django-filter-docs]. --- From f92d8bd9721d788e3017c16fb285189c88112a46 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 22 Oct 2013 12:21:26 +0100 Subject: [PATCH 45/53] Added @jbzdak, for the nice docs improvements in #1191. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index cd3b37107..bcf77b032 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -174,6 +174,7 @@ The following people have helped make REST framework great. * Thomas Badaud - [badale] * Colin Huang - [tamakisquare] * Ross McFarland - [ross] +* Jacek Bzdak - [jbzdak] Many thanks to everyone who's contributed to the project. @@ -384,3 +385,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [badale]: https://github.com/badale [tamakisquare]: https://github.com/tamakisquare [ross]: https://github.com/ross +[jbzdak]: https://github.com/jbzdak From 6b3500b684a43fb67c42231859fa27cf5193298a Mon Sep 17 00:00:00 2001 From: alexanderlukanin13 Date: Thu, 24 Oct 2013 17:52:52 +0600 Subject: [PATCH 46/53] Fixed UnicodeEncodeError when POST JSON via web interface; added test --- rest_framework/request.py | 2 +- rest_framework/tests/test_request.py | 32 +++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index 977d4d965..b883d0d4f 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -334,7 +334,7 @@ class Request(object): self._CONTENT_PARAM in self._data and self._CONTENTTYPE_PARAM in self._data): self._content_type = self._data[self._CONTENTTYPE_PARAM] - self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(HTTP_HEADER_ENCODING)) + self._stream = BytesIO(self._data[self._CONTENT_PARAM].encode(self.parser_context['encoding'])) self._data, self._files = (Empty, Empty) def _parse(self): diff --git a/rest_framework/tests/test_request.py b/rest_framework/tests/test_request.py index 969d8024a..a60e7615e 100644 --- a/rest_framework/tests/test_request.py +++ b/rest_framework/tests/test_request.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals from django.contrib.auth.models import User from django.contrib.auth import authenticate, login, logout from django.contrib.sessions.middleware import SessionMiddleware +from django.core.handlers.wsgi import WSGIRequest from django.test import TestCase from rest_framework import status from rest_framework.authentication import SessionAuthentication @@ -15,12 +16,13 @@ from rest_framework.parsers import ( MultiPartParser, JSONParser ) -from rest_framework.request import Request +from rest_framework.request import Request, Empty from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.test import APIRequestFactory, APIClient from rest_framework.views import APIView from rest_framework.compat import six +from io import BytesIO import json @@ -146,6 +148,34 @@ class TestContentParsing(TestCase): request.parsers = (JSONParser(), ) self.assertEqual(request.DATA, json_data) + def test_form_POST_unicode(self): + """ + JSON POST via default web interface with unicode data + """ + # Note: environ and other variables here have simplified content compared to real Request + CONTENT = b'_content_type=application%2Fjson&_content=%7B%22request%22%3A+4%2C+%22firm%22%3A+1%2C+%22text%22%3A+%22%D0%9F%D1%80%D0%B8%D0%B2%D0%B5%D1%82%21%22%7D' + environ = { + 'REQUEST_METHOD': 'POST', + 'CONTENT_TYPE': 'application/x-www-form-urlencoded', + 'CONTENT_LENGTH': len(CONTENT), + 'wsgi.input': BytesIO(CONTENT), + } + wsgi_request = WSGIRequest(environ=environ) + wsgi_request._load_post_and_files() + parsers = (JSONParser(), FormParser(), MultiPartParser()) + parser_context = { + 'encoding': 'utf-8', + 'kwargs': {}, + 'args': (), + } + request = Request(wsgi_request, parsers=parsers, parser_context=parser_context) + method = request.method + self.assertEqual(method, 'POST') + self.assertEqual(request._content_type, 'application/json') + self.assertEqual(request._stream.getvalue(), b'{"request": 4, "firm": 1, "text": "\xd0\x9f\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82!"}') + self.assertEqual(request._data, Empty) + self.assertEqual(request._files, Empty) + # def test_accessing_post_after_data_form(self): # """ # Ensures request.POST can be accessed after request.DATA in From 63023078856e78fa043df96378137fd7acc2c1de Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 24 Oct 2013 13:45:16 +0100 Subject: [PATCH 47/53] Update comment in `get_parser_context`. --- rest_framework/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 853e64614..e863af6dd 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -154,8 +154,8 @@ class APIView(View): Returns a dict that is passed through to Parser.parse(), as the `parser_context` keyword argument. """ - # Note: Additionally `request` will also be added to the context - # by the Request object. + # Note: Additionally `request` and `encoding` will also be added + # to the context by the Request object. return { 'view': self, 'args': getattr(self, 'args', ()), From 4d894fd39ec5670e72756c26908468fd743354c6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 24 Oct 2013 13:50:05 +0100 Subject: [PATCH 48/53] Added @alexanderlukanin13 for fix #1198. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index bcf77b032..028dfffc8 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -175,6 +175,7 @@ The following people have helped make REST framework great. * Colin Huang - [tamakisquare] * Ross McFarland - [ross] * Jacek Bzdak - [jbzdak] +* Alexander Lukanin - [alexanderlukanin13] Many thanks to everyone who's contributed to the project. @@ -386,3 +387,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [tamakisquare]: https://github.com/tamakisquare [ross]: https://github.com/ross [jbzdak]: https://github.com/jbzdak +[alexanderlukanin13]: https://github.com/alexanderlukanin13 From c92af2b1dd25acebe440f667ede3bad4906b9b28 Mon Sep 17 00:00:00 2001 From: Yamila Date: Thu, 24 Oct 2013 15:56:53 +0200 Subject: [PATCH 49/53] Typo on generic-views.md --- docs/api-guide/generic-views.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 24fc0bc71..9681c8c72 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -125,7 +125,7 @@ Note that if your API doesn't include any object level permissions, you may opti Returns the class that should be used for the serializer. Defaults to returning the `serializer_class` attribute, or dynamically generating a serializer class if the `model` shortcut is being used. -May be override to provide dynamic behavior such as using different serializers for read and write operations, or providing different serializers to different types of uesr. +May be override to provide dynamic behavior such as using different serializers for read and write operations, or providing different serializers to different types of users. For example: From 82e9ddcf7a5cb5fda81e84326bb6f8181ccdffab Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Thu, 24 Oct 2013 15:39:02 +0200 Subject: [PATCH 50/53] Added get_filter_backends method --- docs/api-guide/generic-views.md | 20 ++++++++++++++++++-- rest_framework/generics.py | 12 +++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 24fc0bc71..8fedcdaa0 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -121,6 +121,22 @@ For example: Note that if your API doesn't include any object level permissions, you may optionally exclude the ``self.check_object_permissions, and simply return the object from the `get_object_or_404` lookup. +#### `get_filter_backends(self)` + +Returns the classes that should be used to filter the queryset. Defaults to returning the `filter_backends` attribute. + +May be override to provide more complex behavior with filters, as using different (or even exlusive) lists of filter_backends depending on different criteria. + +For example: + + def get_filter_backends(self): + if "geo_route" in self.request.QUERY_PARAMS: + return (GeoRouteFilter, CategoryFilter) + elif "geo_point" in self.request.QUERY_PARAMS: + return (GeoPointFilter, CategoryFilter) + + return (CategoryFilter,) + #### `get_serializer_class(self)` Returns the class that should be used for the serializer. Defaults to returning the `serializer_class` attribute, or dynamically generating a serializer class if the `model` shortcut is being used. @@ -328,7 +344,7 @@ You can then simply apply this mixin to a view or viewset anytime you need to ap serializer_class = UserSerializer lookup_fields = ('account', 'username') -Using custom mixins is a good option if you have custom behavior that needs to be used +Using custom mixins is a good option if you have custom behavior that needs to be used ## Creating custom base classes @@ -337,7 +353,7 @@ If you are using a mixin across multiple views, you can take this a step further class BaseRetrieveView(MultipleFieldLookupMixin, generics.RetrieveAPIView): pass - + class BaseRetrieveUpdateDestroyView(MultipleFieldLookupMixin, generics.RetrieveUpdateDestroyAPIView): pass diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 6d204cf5f..7cb80a84c 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -175,6 +175,14 @@ class GenericAPIView(views.APIView): method if you want to apply the configured filtering backend to the default queryset. """ + for backend in self.get_filter_backends(): + queryset = backend().filter_queryset(self.request, queryset, self) + return queryset + + def get_filter_backends(self): + """ + Returns the list of filter backends that this view requires. + """ filter_backends = self.filter_backends or [] if not filter_backends and self.filter_backend: warnings.warn( @@ -185,10 +193,8 @@ class GenericAPIView(views.APIView): PendingDeprecationWarning, stacklevel=2 ) filter_backends = [self.filter_backend] + return filter_backends - for backend in filter_backends: - queryset = backend().filter_queryset(self.request, queryset, self) - return queryset ######################## ### The following methods provide default implementations From 2ddf7869e3ce57d056c5b0546154c7bbe524cc09 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 24 Oct 2013 15:29:40 +0100 Subject: [PATCH 51/53] Added @yamila-moreno for work on #1199. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 028dfffc8..e6de1dbe3 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -176,6 +176,7 @@ The following people have helped make REST framework great. * Ross McFarland - [ross] * Jacek Bzdak - [jbzdak] * Alexander Lukanin - [alexanderlukanin13] +* Yamila Moreno - [yamila-moreno] Many thanks to everyone who's contributed to the project. @@ -388,3 +389,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [ross]: https://github.com/ross [jbzdak]: https://github.com/jbzdak [alexanderlukanin13]: https://github.com/alexanderlukanin13 +[yamila-moreno]: https://github.com/yamila-moreno From be55a3c5c7f0c573129903e29b7c9dfc02dd5958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Rozto=C4=8Dil?= Date: Thu, 24 Oct 2013 17:53:02 +0200 Subject: [PATCH 52/53] Removed commented-out credits from footer to make django-debug-toolbar work. The comment, although valid, caused that the Django debug toolbar's injected HTML was partially commented-out and thus the toolbar didn't work as expected. --- rest_framework/templates/rest_framework/base.html | 3 --- 1 file changed, 3 deletions(-) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 7ab17dff4..495163b64 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -221,9 +221,6 @@
{% block footer %} - {% endblock %} {% block script %} From 7d5499bcac379a506f78fc0065ebe31c8d01240f Mon Sep 17 00:00:00 2001 From: Kit Randel Date: Fri, 25 Oct 2013 11:45:33 +1300 Subject: [PATCH 53/53] In the API test client example 'data' was not defined. There's also no need to define 'expected' as we can just test against the dict. --- docs/api-guide/testing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 35c1f7660..4a8a91682 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -205,10 +205,10 @@ You can use any of REST framework's test case classes as you would for the regul Ensure we can create a new account object. """ url = reverse('account-list') - expected = {'name': 'DabApps'} + data = {'name': 'DabApps'} response = self.client.post(url, data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response.data, expected) + self.assertEqual(response.data, data) ---