From 94cd1369437f84adefb46462439b46dc5208ab1d Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Thu, 29 Aug 2013 17:35:15 +1200 Subject: 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 31cfa344..8ba1b195 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 -- cgit v1.2.3 From 59cce01b3359aa009e697a99eabbf2ef322b28e2 Mon Sep 17 00:00:00 2001 From: Philip Douglas Date: Thu, 12 Sep 2013 16:03:20 +0100 Subject: 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 a63c7f6c..778e72d1 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 c13c38b8..127e379e 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 -- cgit v1.2.3 From 6e4bdb55969171c87296aba9711dbc77f8a1e366 Mon Sep 17 00:00:00 2001 From: Philip Douglas Date: Thu, 12 Sep 2013 16:04:33 +0100 Subject: 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 127e379e..78f4cf42 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()) -- cgit v1.2.3 From 272a6abf91c51b44781d27af5352c7e36c8fa91c Mon Sep 17 00:00:00 2001 From: Philip Douglas Date: Fri, 13 Sep 2013 10:46:24 +0100 Subject: 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 210c2537..0c3817b5 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 778e72d1..a63c7f6c 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() -- cgit v1.2.3 From b74c5235c509738c7afea0be0dd8283bb8339ebe Mon Sep 17 00:00:00 2001 From: Colin Huang Date: Sun, 15 Sep 2013 21:56:43 -0700 Subject: [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 c2497660..9792685e 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): -- cgit v1.2.3 From 3d8fad04446110db93ed2a13866e91beb9604934 Mon Sep 17 00:00:00 2001 From: Braulio Soncco Date: Wed, 18 Sep 2013 00:33:05 -0500 Subject: Fixing simple typo --- 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 30966a10..6ff97f37 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -147,7 +147,7 @@ Similarly, we can control the format of the request that we send, using the `Con # POST using form data curl -X POST http://127.0.0.1:8000/snippets/ -d "code=print 123" - {"id": 3, "title": "", "code": "123", "linenos": false, "language": "python", "style": "friendly"} + {"id": 3, "title": "", "code": "print 123", "linenos": false, "language": "python", "style": "friendly"} # POST using JSON curl -X POST http://127.0.0.1:8000/snippets/ -d '{"code": "print 456"}' -H "Content-Type: application/json" -- cgit v1.2.3 From f07a4f4ca3812fbe45d698e4ba0f9ff9099b6887 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 20 Sep 2013 14:10:16 +0200 Subject: Clear cached serializer data on `save()` + test. Fixes #1116. --- rest_framework/serializers.py | 3 +++ rest_framework/tests/test_serializer.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a63c7f6c..8d2e0feb 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -518,6 +518,9 @@ class BaseSerializer(WritableField): """ Save the deserialized object and return it. """ + # Clear cached _data, which may be invalidated by `save()` + self._data = None + if isinstance(self.object, list): [self.save_object(item, **kwargs) for item in self.object] diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index c2497660..43d24411 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models from django.db.models.fields import BLANK_CHOICE_DASH @@ -136,6 +137,7 @@ class BasicTests(TestCase): 'Happy new year!', datetime.datetime(2012, 1, 1) ) + self.actionitem = ActionItem(title='Some to do item',) self.data = { 'email': 'tom@example.com', 'content': 'Happy new year!', @@ -264,6 +266,20 @@ class BasicTests(TestCase): """ self.assertRaises(AssertionError, PersonSerializerInvalidReadOnly, []) + def test_serializer_data_is_cleared_on_save(self): + """ + Check _data attribute is cleared on `save()` + + Regression test for #1116 + — id field is not populated is `data` is accessed prior to `save()` + """ + serializer = ActionItemSerializer(self.actionitem) + self.assertIsNone(serializer.data.get('id',None), 'New instance. `id` should not be set.') + serializer.save() + self.assertIsNotNone(serializer.data.get('id',None), 'Model is saved. `id` should be set.') + + + class DictStyleSerializer(serializers.Serializer): """ -- cgit v1.2.3 From b82c44af48f25c0a60880b8d702caf6a74d80baa Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 20 Sep 2013 14:20:21 +0200 Subject: Correct typo in doc string. --- rest_framework/tests/test_serializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 43d24411..8d246b01 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -271,7 +271,7 @@ class BasicTests(TestCase): Check _data attribute is cleared on `save()` Regression test for #1116 - — id field is not populated is `data` is accessed prior to `save()` + — id field is not populated if `data` is accessed prior to `save()` """ serializer = ActionItemSerializer(self.actionitem) self.assertIsNone(serializer.data.get('id',None), 'New instance. `id` should not be set.') -- cgit v1.2.3 From de6e7accef0f144f4bcae9618604d462b1d9d321 Mon Sep 17 00:00:00 2001 From: John Mee Date: Mon, 23 Sep 2013 14:03:09 +1000 Subject: Mindnumbingly trivial single-char typo. --- docs/tutorial/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index 06eec3c4..80bb9abb 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -85,7 +85,7 @@ Right, we'd better write some views then. Open `quickstart/views.py` and get ty queryset = Group.objects.all() serializer_class = GroupSerializer -Rather that write multiple views we're grouping together all the common behavior into classes called `ViewSets`. +Rather than write multiple views we're grouping together all the common behavior into classes called `ViewSets`. We can easily break these down into individual views if we need to, but using viewsets keeps the view logic nicely organized as well as being very concise. -- cgit v1.2.3 From fda0c520bd3fb81c7eec06da70bb7abfed519d93 Mon Sep 17 00:00:00 2001 From: Grzegorz Kapkowski Date: Mon, 23 Sep 2013 16:10:46 +0200 Subject: Match docs to current code. --- 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 859e8d52..784aa585 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -195,9 +195,9 @@ For more details on using filter sets see the [django-filter documentation][djan ## SearchFilter -The `SearchFilterBackend` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin]. +The `SearchFilter` class supports simple single query parameter based searching, and is based on the [Django admin's search functionality][search-django-admin]. -The `SearchFilterBackend` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`. +The `SearchFilter` class will only be applied if the view has a `search_fields` attribute set. The `search_fields` attribute should be a list of names of text type fields on the model, such as `CharField` or `TextField`. class UserListView(generics.ListAPIView): queryset = User.objects.all() -- cgit v1.2.3 From abbe9213f98b5e1d3b53db2c1711d9221c5b257f Mon Sep 17 00:00:00 2001 From: Markus Kaiserswerth Date: Mon, 23 Sep 2013 17:48:25 +0200 Subject: Address pending deprecation of Model._meta.module_name in Django 1.6 --- rest_framework/compat.py | 8 ++++++++ rest_framework/filters.py | 4 ++-- rest_framework/permissions.py | 7 ++++--- rest_framework/tests/test_permissions.py | 4 ++-- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index b9d1dae6..581e29fc 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -80,6 +80,14 @@ except ImportError: Image = None +def get_model_name(model_cls): + try: + return model_cls._meta.model_name + except AttributeError: + # < 1.6 used module_name instead of model_name + return model_cls._meta.module_name + + def get_concrete_model(model_cls): try: return model_cls._meta.concrete_model diff --git a/rest_framework/filters.py b/rest_framework/filters.py index b8fe7f77..e287a168 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -4,7 +4,7 @@ returned by list views. """ from __future__ import unicode_literals from django.db import models -from rest_framework.compat import django_filters, six, guardian +from rest_framework.compat import django_filters, six, guardian, get_model_name from functools import reduce import operator @@ -158,7 +158,7 @@ class DjangoObjectPermissionsFilter(BaseFilterBackend): model_cls = queryset.model kwargs = { 'app_label': model_cls._meta.app_label, - 'model_name': model_cls._meta.module_name + 'model_name': get_model_name(model_cls) } permission = self.perm_format % kwargs return guardian.shortcuts.get_objects_for_user(user, permission, queryset) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 53184798..ab6655e7 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -8,7 +8,8 @@ import warnings SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] from django.http import Http404 -from rest_framework.compat import oauth2_provider_scope, oauth2_constants +from rest_framework.compat import (get_model_name, oauth2_provider_scope, + oauth2_constants) class BasePermission(object): @@ -116,7 +117,7 @@ class DjangoModelPermissions(BasePermission): """ kwargs = { 'app_label': model_cls._meta.app_label, - 'model_name': model_cls._meta.module_name + 'model_name': get_model_name(model_cls) } return [perm % kwargs for perm in self.perms_map[method]] @@ -177,7 +178,7 @@ class DjangoObjectPermissions(DjangoModelPermissions): def get_required_object_permissions(self, method, model_cls): kwargs = { 'app_label': model_cls._meta.app_label, - 'model_name': model_cls._meta.module_name + 'model_name': get_model_name(model_cls) } return [perm % kwargs for perm in self.perms_map[method]] diff --git a/rest_framework/tests/test_permissions.py b/rest_framework/tests/test_permissions.py index d08124f4..6e3a6303 100644 --- a/rest_framework/tests/test_permissions.py +++ b/rest_framework/tests/test_permissions.py @@ -4,7 +4,7 @@ from django.db import models from django.test import TestCase from django.utils import unittest from rest_framework import generics, status, permissions, authentication, HTTP_HEADER_ENCODING -from rest_framework.compat import guardian +from rest_framework.compat import guardian, get_model_name from rest_framework.filters import DjangoObjectPermissionsFilter from rest_framework.test import APIRequestFactory from rest_framework.tests.models import BasicModel @@ -202,7 +202,7 @@ class ObjectPermissionsIntegrationTests(TestCase): # give everyone model level permissions, as we are not testing those everyone = Group.objects.create(name='everyone') - model_name = BasicPermModel._meta.module_name + model_name = get_model_name(BasicPermModel) app_label = BasicPermModel._meta.app_label f = '{0}_{1}'.format perms = { -- cgit v1.2.3 From 57a51f790bdff6c1cf34f3c6d64b0bef3033d89e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 24 Sep 2013 09:21:55 +0100 Subject: Added @mkai, for work on #1126. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 4483f170..b5dce504 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -169,6 +169,7 @@ The following people have helped make REST framework great. * Edmond Wong - [edmondwong] * Ben Reilly - [bwreilly] * Tai Lee - [mrmachine] +* Markus Kaiserswerth - [mkai] Many thanks to everyone who's contributed to the project. @@ -374,3 +375,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [edmondwong]: https://github.com/edmondwong [bwreilly]: https://github.com/bwreilly [mrmachine]: https://github.com/mrmachine +[mkai]: https://github.com/mkai -- cgit v1.2.3 From 75d6446c8799763dccde0f5f03fbcae39c18dc7f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 26 Sep 2013 16:09:08 +0100 Subject: 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 2ce51e97..a27160d4 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, -- cgit v1.2.3 From 49847584a8524883181e4118a9df9bb62f597271 Mon Sep 17 00:00:00 2001 From: Krzysztof Jurewicz Date: Sun, 29 Sep 2013 12:49:38 +0200 Subject: 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 0555b84d..97dd4710 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. -- cgit v1.2.3 From 8a1d3275795a6eea931cb0b67465c88d745bd2b6 Mon Sep 17 00:00:00 2001 From: Doron Pearl Date: Mon, 30 Sep 2013 14:08:46 -0400 Subject: 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 cc469217..fc1525df 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' -- cgit v1.2.3 From 61dd9ce914382ad7b26e2654a590215885308828 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 1 Oct 2013 15:47:47 -0600 Subject: 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 e4294ae3..3df8869a 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 -- cgit v1.2.3 From a14f1e886402b8d0f742fdbb912b03a4004e1735 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 2 Oct 2013 13:45:35 +0100 Subject: Serializers can now be rendered directly to HTML --- rest_framework/fields.py | 21 ++++++++ rest_framework/renderers.py | 62 ++--------------------- rest_framework/serializers.py | 19 ++++++- rest_framework/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 210c2537..16344d01 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 a27160d4..0e17edaf 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 8d2e0feb..206a8123 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 b27f652e..b1e148df 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 %} -- cgit v1.2.3 From 8d4ba478cc5725b4de6ab86b4825b1ea94cb4c7b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 2 Oct 2013 16:13:34 +0100 Subject: Fix rendering of forms and add error rendering on HTML form --- rest_framework/renderers.py | 16 +++++++++---- rest_framework/serializers.py | 26 +++++++++++----------- rest_framework/templates/rest_framework/base.html | 4 ++-- .../templates/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 0e17edaf..fe4f43d4 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 206a8123..bc9f73d1 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 2776d550..33be36db 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 00000000..075279f7 --- /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 %} -- cgit v1.2.3 From dc650f77b5f377bbfab3da66455feb57b195a1da Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Thu, 3 Oct 2013 11:34:42 +1300 Subject: 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 c2497660..ca876eae 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, + } + ) -- cgit v1.2.3 From 42bbf6907e041d6abe773854b9aaa53eded82f4e Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Thu, 3 Oct 2013 12:38:42 +1300 Subject: 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 bbc8d019..2d3e999f 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... -- cgit v1.2.3 From f6301636fb52dc6e02fd55e1c07c0be0a3b4ebfd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 3 Oct 2013 15:18:38 +0100 Subject: 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 a3cd1d6a..6b91aa76 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. -- cgit v1.2.3 From 3e94f4dc709143d577433c164873654e7c0579f8 Mon Sep 17 00:00:00 2001 From: Henry Clifford Date: Fri, 4 Oct 2013 10:49:56 -0400 Subject: 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 7d1bf794..4f134bce 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 -- cgit v1.2.3 From 3833f1d55f97db33596242d89dbed997221f3b82 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 4 Oct 2013 15:55:57 +0100 Subject: 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 b5dce504..586bb0f0 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 -- cgit v1.2.3 From 48a38386afb8a8619d2f089ebce364c7a0a845b4 Mon Sep 17 00:00:00 2001 From: dpetzel Date: Sat, 5 Oct 2013 19:29:25 -0400 Subject: **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 22d29285..e1c0009c 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 -- cgit v1.2.3 From 864497eebbd39ab3e811c589a44a43176caef1bf Mon Sep 17 00:00:00 2001 From: dpetzel Date: Sat, 5 Oct 2013 23:16:58 -0400 Subject: 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 393d879a..510aa243 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. -- cgit v1.2.3 From 9e29c6389529210978d58cee78e437b901f9daa2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 10 Oct 2013 17:33:22 +0100 Subject: 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 16344d01..6b039f6c 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 bc9f73d1..8e945688 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 8d246b01..d4e5a93f 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 00000000..30cff361 --- /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}) -- cgit v1.2.3 From 7c3769f04b5ec2cd14dcbd7e3601d59092255906 Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Fri, 11 Oct 2013 15:31:55 +1300 Subject: fix writing into foreign key with non-null source --- rest_framework/serializers.py | 2 +- rest_framework/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 33db82ee..fa5ac143 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 71d0e24b..e454235a 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) -- cgit v1.2.3 From 86ea969e1154de20a53fc5b853e8340508648e98 Mon Sep 17 00:00:00 2001 From: Craig de Stigter Date: Fri, 11 Oct 2013 15:50:07 +1300 Subject: 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 e454235a..029f8bff 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', -- cgit v1.2.3 From 89ac03af26a63a2126165e8995f7936798ce0450 Mon Sep 17 00:00:00 2001 From: Álvaro Lázaro Date: Sat, 12 Oct 2013 20:31:33 +0200 Subject: 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 5ec4b22f..b9d96b5e 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)', -- cgit v1.2.3 From c6be12f02b5e07e412c8c91b368566a85364b907 Mon Sep 17 00:00:00 2001 From: Colin Huang Date: Sun, 15 Sep 2013 18:03:52 -0700 Subject: [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 a63c7f6c..0b5ae042 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) -- cgit v1.2.3 From e83bc003234418fc6b21b841de216319491bd38d Mon Sep 17 00:00:00 2001 From: Rikki Date: Wed, 16 Oct 2013 03:03:51 +0100 Subject: 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 6ff97f37..ba9eb723 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): -- cgit v1.2.3 From cb123e896ed2dca230088296db9663af5a53252d Mon Sep 17 00:00:00 2001 From: Rikki Date: Wed, 16 Oct 2013 03:08:43 +0100 Subject: 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 9fc424fe..67a75d9f 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 -- cgit v1.2.3 From bf6084895263f827a80191fd6ed4eb437b555f9a Mon Sep 17 00:00:00 2001 From: Rikki Date: Wed, 16 Oct 2013 03:21:43 +0100 Subject: 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 510aa243..ecf92a7b 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') -- cgit v1.2.3 From d31fd33f4bbd52fa60949b15c2614528991e2c7a Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Tue, 8 Oct 2013 16:15:15 +0200 Subject: 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 33be36db..7ab17dff 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 }}
-- cgit v1.2.3 From 8a5fea06f01ed4c5114ec0743516b6e6179c88b4 Mon Sep 17 00:00:00 2001 From: badaud_t Date: Thu, 17 Oct 2013 01:07:50 +0200 Subject: 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 df6f4aa6..78a7dac8 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): -- cgit v1.2.3 From b730aec0f46e2b849b3c597bcf1a1bcdc158e415 Mon Sep 17 00:00:00 2001 From: badaud_t Date: Thu, 17 Oct 2013 01:08:24 +0200 Subject: 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 78a7dac8..76299a89 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 7efd5417..35ad206b 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, -- cgit v1.2.3 From 545ee013e332db0a81e5bd82b76f30b7d2d08b12 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 17 Oct 2013 09:40:06 +0100 Subject: 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 586bb0f0..9a20028c 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 -- cgit v1.2.3 From cc3c16eaa09c7dc63592ae8bf4ee30f1af263be1 Mon Sep 17 00:00:00 2001 From: Bruno Renié Date: Mon, 14 Oct 2013 16:28:32 +0200 Subject: 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 33db82ee..6801e24d 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 -- cgit v1.2.3 From daf927ef68ce992055e8b7bc1a07cf03ee67b742 Mon Sep 17 00:00:00 2001 From: Colin Date: Thu, 17 Oct 2013 12:28:58 -0700 Subject: 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 9a20028c..e9c45965 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 -- cgit v1.2.3 From 78c8e6de40f89580b9a4cefb6595d52bc1a6afbc Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 18 Oct 2013 09:10:54 +0100 Subject: 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 ba9eb723..7fa4f3e4 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): -- cgit v1.2.3 From c3aeb16557f2cbb1c1218b5af7bab646e4958234 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 18 Oct 2013 09:32:04 +0100 Subject: 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 67a75d9f..b37bc31b 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 -- cgit v1.2.3 From 63e6a3b4925bf54e80ae63502a0353136e846b31 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Sat, 19 Oct 2013 20:43:23 -0700 Subject: 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 4f134bce..6b42a1d5 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 d6bc7895..a1118f1e 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) -- cgit v1.2.3 From ed9c3258a6f9df6fabb569a65f3eb3363affa523 Mon Sep 17 00:00:00 2001 From: Jesús Espino Date: Mon, 21 Oct 2013 10:24:06 +0200 Subject: 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 0c48783a..c46d415e 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. -- cgit v1.2.3 From 70b0798118c8c02903421bca03e0406fe65d737f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 21 Oct 2013 09:30:01 +0100 Subject: Add invite signup --- docs/template.html | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/template.html b/docs/template.html index a20c8111..749d0afe 100644 --- a/docs/template.html +++ b/docs/template.html @@ -167,7 +167,32 @@
+ + +
-- cgit v1.2.3 From 76672787cdba6a4ab8173b51fa099c910556889b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 21 Oct 2013 09:47:07 +0100 Subject: 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 dc0076df..24fc0bc7 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 4f134bce..f46dea76 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} -- cgit v1.2.3 From 216ac8a5c1ba39bf24e4e91b6fac7e0ac1dee7e4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 21 Oct 2013 17:19:28 +0100 Subject: 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 426865ff..4606c78b 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 -- cgit v1.2.3 From f0a129dcda3d671b88b5049d9ddaec53a4b32faf Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Mon, 21 Oct 2013 14:23:06 -0700 Subject: 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 6b42a1d5..4015ab20 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: -- cgit v1.2.3 From c36122a7ba2cdc69f94f5732f26428329be54200 Mon Sep 17 00:00:00 2001 From: Ross McFarland Date: Mon, 21 Oct 2013 14:26:21 -0700 Subject: 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 a1118f1e..cadb515f 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: -- cgit v1.2.3 From fa87fac61b87858e80788fc233591fa11dbc18e7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 22 Oct 2013 10:21:06 +0100 Subject: 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 e9c45965..cd3b3710 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 -- cgit v1.2.3 From 25c9d552c05527f4b8b257d59cd7be39005f3668 Mon Sep 17 00:00:00 2001 From: Jacek Bzdak Date: Tue, 22 Oct 2013 13:11:14 +0200 Subject: 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 784aa585..bcb0bb41 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** -- cgit v1.2.3 From cc9c7cd8a479b7fa76a66b8669e4a62fd78be867 Mon Sep 17 00:00:00 2001 From: Jacek Bzdak Date: Tue, 22 Oct 2013 13:15:48 +0200 Subject: 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 bcb0bb41..a0132ffc 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]. --- -- cgit v1.2.3 From f92d8bd9721d788e3017c16fb285189c88112a46 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 22 Oct 2013 12:21:26 +0100 Subject: 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 cd3b3710..bcf77b03 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 -- cgit v1.2.3 From 6b3500b684a43fb67c42231859fa27cf5193298a Mon Sep 17 00:00:00 2001 From: alexanderlukanin13 Date: Thu, 24 Oct 2013 17:52:52 +0600 Subject: 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 977d4d96..b883d0d4 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 969d8024..a60e7615 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 -- cgit v1.2.3 From 63023078856e78fa043df96378137fd7acc2c1de Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 24 Oct 2013 13:45:16 +0100 Subject: 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 853e6461..e863af6d 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', ()), -- cgit v1.2.3 From 4d894fd39ec5670e72756c26908468fd743354c6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 24 Oct 2013 13:50:05 +0100 Subject: 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 bcf77b03..028dfffc 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 -- cgit v1.2.3 From c92af2b1dd25acebe440f667ede3bad4906b9b28 Mon Sep 17 00:00:00 2001 From: Yamila Date: Thu, 24 Oct 2013 15:56:53 +0200 Subject: 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 24fc0bc7..9681c8c7 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: -- cgit v1.2.3 From 82e9ddcf7a5cb5fda81e84326bb6f8181ccdffab Mon Sep 17 00:00:00 2001 From: Yamila Moreno Date: Thu, 24 Oct 2013 15:39:02 +0200 Subject: 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 24fc0bc7..8fedcdaa 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 6d204cf5..7cb80a84 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 -- cgit v1.2.3 From 2ddf7869e3ce57d056c5b0546154c7bbe524cc09 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 24 Oct 2013 15:29:40 +0100 Subject: 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 028dfffc..e6de1dbe 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 -- cgit v1.2.3 From be55a3c5c7f0c573129903e29b7c9dfc02dd5958 Mon Sep 17 00:00:00 2001 From: Jakub Roztočil Date: Thu, 24 Oct 2013 17:53:02 +0200 Subject: 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 7ab17dff..495163b6 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 %} -- cgit v1.2.3 From 7d5499bcac379a506f78fc0065ebe31c8d01240f Mon Sep 17 00:00:00 2001 From: Kit Randel Date: Fri, 25 Oct 2013 11:45:33 +1300 Subject: 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 35c1f766..4a8a9168 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) --- -- cgit v1.2.3 From 458af921f36cec48ff6c27f4824d69f1aafcd18e Mon Sep 17 00:00:00 2001 From: S. Andrew Sheppard Date: Tue, 29 Oct 2013 15:10:06 -0500 Subject: minor typo --- rest_framework/viewsets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index d91323f2..7eb29f99 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -9,7 +9,7 @@ Actions are only bound to methods at the point of instantiating the views. user_detail = UserViewSet.as_view({'get': 'retrieve'}) Typically, rather than instantiate views from viewsets directly, you'll -regsiter the viewset with a router and let the URL conf be determined +register the viewset with a router and let the URL conf be determined automatically. router = DefaultRouter() -- cgit v1.2.3 From f72488d60915f2f77234bc75ccfd604cc6a4143f Mon Sep 17 00:00:00 2001 From: erkarl Date: Thu, 31 Oct 2013 03:47:23 +0200 Subject: Updated OAuth2 authentication docs. --- docs/api-guide/authentication.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 7caeac1e..1a1c68b8 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -265,6 +265,12 @@ This authentication class depends on the optional [django-oauth2-provider][djang 'provider.oauth2', ) +Then add `OAuth2Authentication` to your global `DEFAULT_AUTHENTICATION` setting: + + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework.authentication.OAuth2Authentication', + ), + You must also include the following in your root `urls.py` module: url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')), -- cgit v1.2.3 From e33435d0da0dba13fae39070b3d87ad8af47862f Mon Sep 17 00:00:00 2001 From: Rob Hudson Date: Thu, 31 Oct 2013 15:03:50 -0700 Subject: Fixed exception handling with YAML and XML parsers. --- rest_framework/parsers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 98fc0341..f1b3e38d 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -83,7 +83,7 @@ class YAMLParser(BaseParser): data = stream.read().decode(encoding) return yaml.safe_load(data) except (ValueError, yaml.parser.ParserError) as exc: - raise ParseError('YAML parse error - %s' % six.u(exc)) + raise ParseError('YAML parse error - %s' % six.text_type(exc)) class FormParser(BaseParser): @@ -153,7 +153,7 @@ class XMLParser(BaseParser): try: tree = etree.parse(stream, parser=parser, forbid_dtd=True) except (etree.ParseError, ValueError) as exc: - raise ParseError('XML parse error - %s' % six.u(exc)) + raise ParseError('XML parse error - %s' % six.text_type(exc)) data = self._xml_convert(tree.getroot()) return data -- cgit v1.2.3 From 5e0538fcda75eed82ed183c7c4dcfcda5ad35d5f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 2 Nov 2013 09:15:35 +0000 Subject: Added @robhudson for work on #1211. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index e6de1dbe..9751850b 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -177,6 +177,7 @@ The following people have helped make REST framework great. * Jacek Bzdak - [jbzdak] * Alexander Lukanin - [alexanderlukanin13] * Yamila Moreno - [yamila-moreno] +* Rob Hudson - [robhudson] Many thanks to everyone who's contributed to the project. @@ -390,3 +391,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [jbzdak]: https://github.com/jbzdak [alexanderlukanin13]: https://github.com/alexanderlukanin13 [yamila-moreno]: https://github.com/yamila-moreno +[robhudson]: https://github.com/robhudson -- cgit v1.2.3 From 53258908210b1eabd0ee204653a589d6579ac772 Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Tue, 5 Nov 2013 17:21:18 +0100 Subject: Improve handling of 'empty' values for ChoiceField The empty value defaults back to '' (for backwards-compatibility) but is changed automatically to None for ModelSerializers if the `null` property is set on the db field. --- rest_framework/fields.py | 8 ++-- rest_framework/serializers.py | 2 + rest_framework/tests/test_fields.py | 74 +++++++++++++++++++++++++++++-------- 3 files changed, 66 insertions(+), 18 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index e23fc001..6c07dbb3 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -497,6 +497,7 @@ class ChoiceField(WritableField): } def __init__(self, choices=(), *args, **kwargs): + self.empty = kwargs.pop('empty', '') super(ChoiceField, self).__init__(*args, **kwargs) self.choices = choices if not self.required: @@ -537,9 +538,10 @@ class ChoiceField(WritableField): return False def from_native(self, value): - if value in validators.EMPTY_VALUES: - return None - return super(ChoiceField, self).from_native(value) + value = super(ChoiceField, self).from_native(value) + if value == self.empty or value in validators.EMPTY_VALUES: + return self.empty + return value class EmailField(CharField): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 4210d058..5240dbf6 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -794,6 +794,8 @@ class ModelSerializer(Serializer): # TODO: TypedChoiceField? if model_field.flatchoices: # This ModelField contains choices kwargs['choices'] = model_field.flatchoices + if model_field.null: + kwargs['empty'] = None return ChoiceField(**kwargs) # put this below the ChoiceField because min_value isn't a valid initializer diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index 34fbab9c..333476ba 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -42,6 +42,31 @@ class TimeFieldModelSerializer(serializers.ModelSerializer): model = TimeFieldModel +SAMPLE_CHOICES = [ + ('red', 'Red'), + ('green', 'Green'), + ('blue', 'Blue'), +] + + +class ChoiceFieldModel(models.Model): + choice = models.CharField(choices=SAMPLE_CHOICES, blank=True, max_length=255) + + +class ChoiceFieldModelSerializer(serializers.ModelSerializer): + class Meta: + model = ChoiceFieldModel + + +class ChoiceFieldModelWithNull(models.Model): + choice = models.CharField(choices=SAMPLE_CHOICES, blank=True, null=True, max_length=255) + + +class ChoiceFieldModelWithNullSerializer(serializers.ModelSerializer): + class Meta: + model = ChoiceFieldModelWithNull + + class BasicFieldTests(TestCase): def test_auto_now_fields_read_only(self): """ @@ -667,34 +692,53 @@ class ChoiceFieldTests(TestCase): """ Tests for the ChoiceField options generator """ - - SAMPLE_CHOICES = [ - ('red', 'Red'), - ('green', 'Green'), - ('blue', 'Blue'), - ] - def test_choices_required(self): """ Make sure proper choices are rendered if field is required """ - f = serializers.ChoiceField(required=True, choices=self.SAMPLE_CHOICES) - self.assertEqual(f.choices, self.SAMPLE_CHOICES) + f = serializers.ChoiceField(required=True, choices=SAMPLE_CHOICES) + self.assertEqual(f.choices, SAMPLE_CHOICES) def test_choices_not_required(self): """ Make sure proper choices (plus blank) are rendered if the field isn't required """ - f = serializers.ChoiceField(required=False, choices=self.SAMPLE_CHOICES) - self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + self.SAMPLE_CHOICES) + f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES) + self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES) + + def test_invalid_choice_model(self): + s = ChoiceFieldModelSerializer(data={'choice' : 'wrong_value'}) + self.assertFalse(s.is_valid()) + self.assertEqual(s.errors, {'choice': [u'Select a valid choice. wrong_value is not one of the available choices.']}) + self.assertEqual(s.data['choice'], '') + + def test_empty_choice_model(self): + """ + Test that the 'empty' value is correctly passed and used depending on the 'null' property on the model field. + """ + s = ChoiceFieldModelSerializer(data={'choice' : ''}) + self.assertTrue(s.is_valid()) + self.assertEqual(s.data['choice'], '') + + s = ChoiceFieldModelWithNullSerializer(data={'choice' : ''}) + self.assertTrue(s.is_valid()) + self.assertEqual(s.data['choice'], None) def test_from_native_empty(self): """ - Make sure from_native() returns None on empty param. + Make sure from_native() returns an empty string on empty param by default. """ - f = serializers.ChoiceField(choices=self.SAMPLE_CHOICES) - result = f.from_native('') - self.assertEqual(result, None) + f = serializers.ChoiceField(choices=SAMPLE_CHOICES) + self.assertEqual(f.from_native(''), '') + self.assertEqual(f.from_native(None), '') + + def test_from_native_empty_override(self): + """ + Make sure you can override from_native() behavior regarding empty values. + """ + f = serializers.ChoiceField(choices=SAMPLE_CHOICES, empty=None) + self.assertEqual(f.from_native(''), None) + self.assertEqual(f.from_native(None), None) class EmailFieldTests(TestCase): -- cgit v1.2.3 From 5829eb7a5b0d45fe668d7ce1ad394a7b5966c70d Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Wed, 6 Nov 2013 12:51:40 +0100 Subject: Drop u'' prefix for python 3.x compatibility --- rest_framework/tests/test_fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index 333476ba..ab2cceac 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -709,7 +709,7 @@ class ChoiceFieldTests(TestCase): def test_invalid_choice_model(self): s = ChoiceFieldModelSerializer(data={'choice' : 'wrong_value'}) self.assertFalse(s.is_valid()) - self.assertEqual(s.errors, {'choice': [u'Select a valid choice. wrong_value is not one of the available choices.']}) + self.assertEqual(s.errors, {'choice': ['Select a valid choice. wrong_value is not one of the available choices.']}) self.assertEqual(s.data['choice'], '') def test_empty_choice_model(self): -- cgit v1.2.3 From 1296753b5c55de19e057eb6fde73da2b41f7032b Mon Sep 17 00:00:00 2001 From: Bruno Renié Date: Fri, 8 Nov 2013 11:58:14 +0100 Subject: Updated versions in tox and travis config --- .travis.yml | 28 ++++++++++++++-------------- tox.ini | 20 ++++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.travis.yml b/.travis.yml index d12479e9..386c1d64 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,20 +7,20 @@ python: - "3.3" env: - - DJANGO="https://www.djangoproject.com/download/1.6a1/tarball/" - - DJANGO="django==1.5.1 --use-mirrors" - - DJANGO="django==1.4.5 --use-mirrors" - - DJANGO="django==1.3.7 --use-mirrors" + - DJANGO="django==1.6" + - DJANGO="django==1.5.5" + - DJANGO="django==1.4.10" + - DJANGO="django==1.3.7" install: - pip install $DJANGO - pip install defusedxml==0.3 - - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211 --use-mirrors; fi" - - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0 --use-mirrors; fi" - - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4 --use-mirrors; fi" - - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-guardian==1.1.1 --use-mirrors; fi" - - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4 --use-mirrors; fi" - - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6 --use-mirrors; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.0; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-guardian==1.1.1; fi" + - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4; fi" + - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.6; fi" - export PYTHONPATH=. script: @@ -29,11 +29,11 @@ script: matrix: exclude: - python: "3.2" - env: DJANGO="django==1.4.5 --use-mirrors" + env: DJANGO="django==1.4.5" - python: "3.2" - env: DJANGO="django==1.3.7 --use-mirrors" + env: DJANGO="django==1.3.7" - python: "3.3" - env: DJANGO="django==1.4.5 --use-mirrors" + env: DJANGO="django==1.4.5" - python: "3.3" - env: DJANGO="django==1.3.7 --use-mirrors" + env: DJANGO="django==1.3.7" diff --git a/tox.ini b/tox.ini index 6e3b8e0a..1fa0a958 100644 --- a/tox.ini +++ b/tox.ini @@ -7,19 +7,19 @@ commands = {envpython} rest_framework/runtests/runtests.py [testenv:py3.3-django1.6] basepython = python3.3 -deps = https://www.djangoproject.com/download/1.6a1/tarball/ +deps = Django==1.6 django-filter==0.6a1 defusedxml==0.3 [testenv:py3.2-django1.6] basepython = python3.2 -deps = https://www.djangoproject.com/download/1.6a1/tarball/ +deps = Django==1.6 django-filter==0.6a1 defusedxml==0.3 [testenv:py2.7-django1.6] basepython = python2.7 -deps = https://www.djangoproject.com/download/1.6a1/tarball/ +deps = Django==1.6 django-filter==0.6a1 defusedxml==0.3 django-oauth-plus==2.0 @@ -29,7 +29,7 @@ deps = https://www.djangoproject.com/download/1.6a1/tarball/ [testenv:py2.6-django1.6] basepython = python2.6 -deps = https://www.djangoproject.com/download/1.6a1/tarball/ +deps = Django==1.6 django-filter==0.6a1 defusedxml==0.3 django-oauth-plus==2.0 @@ -39,19 +39,19 @@ deps = https://www.djangoproject.com/download/1.6a1/tarball/ [testenv:py3.3-django1.5] basepython = python3.3 -deps = django==1.5 +deps = django==1.5.5 django-filter==0.6a1 defusedxml==0.3 [testenv:py3.2-django1.5] basepython = python3.2 -deps = django==1.5 +deps = django==1.5.5 django-filter==0.6a1 defusedxml==0.3 [testenv:py2.7-django1.5] basepython = python2.7 -deps = django==1.5 +deps = django==1.5.5 django-filter==0.6a1 defusedxml==0.3 django-oauth-plus==2.0 @@ -61,7 +61,7 @@ deps = django==1.5 [testenv:py2.6-django1.5] basepython = python2.6 -deps = django==1.5 +deps = django==1.5.5 django-filter==0.6a1 defusedxml==0.3 django-oauth-plus==2.0 @@ -71,7 +71,7 @@ deps = django==1.5 [testenv:py2.7-django1.4] basepython = python2.7 -deps = django==1.4.3 +deps = django==1.4.10 django-filter==0.6a1 defusedxml==0.3 django-oauth-plus==2.0 @@ -81,7 +81,7 @@ deps = django==1.4.3 [testenv:py2.6-django1.4] basepython = python2.6 -deps = django==1.4.3 +deps = django==1.4.10 django-filter==0.6a1 defusedxml==0.3 django-oauth-plus==2.0 -- cgit v1.2.3 From d4a50429b098656e7a0855c6acf12f0aa4bc434f Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 8 Nov 2013 13:12:40 +0100 Subject: Fixed a regression with ValidationError under Django 1.6 --- rest_framework/serializers.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5240dbf6..7cdb55c8 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -42,6 +42,7 @@ def pretty_name(name): class RelationsList(list): _deleted = [] + class NestedValidationError(ValidationError): """ The default ValidationError behavior is to stringify each item in the list @@ -56,9 +57,13 @@ class NestedValidationError(ValidationError): def __init__(self, message): if isinstance(message, dict): - self.messages = [message] + self._messages = [message] else: - self.messages = message + self._messages = message + + @property + def messages(self): + return self._messages class DictWithMetadata(dict): -- cgit v1.2.3 From b7b57adee2cc5a785a5df492424969c8ba311aa8 Mon Sep 17 00:00:00 2001 From: Ben Pietravalle Date: Fri, 8 Nov 2013 13:19:40 +0000 Subject: Fix object creation with reverse M2M when related_name unspecified It seems that field.related_query_name() does not return the related_name for reverse M2M relations when related_name is not explicitly set in the M2M field definition. So, change to use obj.get_accessor_name(), where obj is an instance of RelatedObject, as are returned by a model's _meta.get_all_related_many_to_many_objects(), or as in the tuples returned by _meta.get_all_m2m_objects_with_model(). --- 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 5240dbf6..244fbe63 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -873,7 +873,7 @@ class ModelSerializer(Serializer): # Reverse m2m relations for (obj, model) in meta.get_all_related_m2m_objects_with_model(): - field_name = obj.field.related_query_name() + field_name = obj.get_accessor_name() if field_name in attrs: m2m_data[field_name] = attrs.pop(field_name) -- cgit v1.2.3 From f2ea5780d2d6e783019bceb73c79bb9445e99e0b Mon Sep 17 00:00:00 2001 From: Bruno Renié Date: Fri, 8 Nov 2013 14:58:36 +0100 Subject: Exclude 1.4 on python 3 --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 386c1d64..bcf1bae0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,11 +29,11 @@ script: matrix: exclude: - python: "3.2" - env: DJANGO="django==1.4.5" + env: DJANGO="django==1.4.10" - python: "3.2" env: DJANGO="django==1.3.7" - python: "3.3" - env: DJANGO="django==1.4.5" + env: DJANGO="django==1.4.10" - python: "3.3" env: DJANGO="django==1.3.7" -- cgit v1.2.3 From fd2c291c4d9243937a31e0e6f523016067824b83 Mon Sep 17 00:00:00 2001 From: Doğan Çeçen Date: Mon, 11 Nov 2013 11:54:30 +0200 Subject: Typo on api-guide/fields.md and serializers.py --- docs/api-guide/fields.md | 4 ++-- rest_framework/serializers.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 962c49e2..4272c9a7 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -299,9 +299,9 @@ Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files. # Custom fields -If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the initial datatype, and a primative, serializable datatype. Primative datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primative objects. +If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the initial datatype, and a primitive, serializable datatype. Primitive datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primitive objects. -The `.to_native()` method is called to convert the initial datatype into a primative, serializable datatype. The `from_native()` method is called to restore a primative datatype into it's initial representation. +The `.to_native()` method is called to convert the initial datatype into a primitive, serializable datatype. The `from_native()` method is called to restore a primitive datatype into it's initial representation. ## Examples diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 16095452..163abf4f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -6,8 +6,8 @@ form encoded input. Serialization in REST framework is a two-phase process: 1. Serializers marshal between complex types like model instances, and -python primatives. -2. The process of marshalling between python primatives and request and +python primitives. +2. The process of marshalling between python primitives and request and response content is handled by parsers and renderers. """ from __future__ import unicode_literals -- cgit v1.2.3 From 52ac2199a8b332f7a485d5c22b1a53633b4be9dd Mon Sep 17 00:00:00 2001 From: Jacob Haslehurst Date: Mon, 11 Nov 2013 22:24:37 +1100 Subject: Added drf-ujson-renderer to renderers docs drf-ujson-renderer is a third party renderer that implements JSON renderering using UltraJSON--- docs/api-guide/renderers.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 657377d9..1f286ef1 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -409,6 +409,10 @@ The following third party packages are also available. Comma-separated values are a plain-text tabular data format, that can be easily imported into spreadsheet applications. [Mjumbe Poe][mjumbewu] maintains the [djangorestframework-csv][djangorestframework-csv] package which provides CSV renderer support for REST framework. +## UltraJSON + +[UltraJSON][ultrajson] is a blazing-fast C JSON encoder which can give 2-10x performance increases on typical workloads. [Jacob Haslehurst][hzy] maintains the [drf-ujson-renderer][drf-ujson-renderer] package which implements JSON rendering using the UJSON package. + [cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process [conneg]: content-negotiation.md [browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers @@ -426,3 +430,6 @@ Comma-separated values are a plain-text tabular data format, that can be easily [mjumbewu]: https://github.com/mjumbewu [djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack [djangorestframework-csv]: https://github.com/mjumbewu/django-rest-framework-csv +[ultrajson]: https://github.com/esnme/ultrajson +[hzy]: https://github.com/hzy +[drf-ujson-renderer]: https://github.com/gizmag/drf-ujson-renderer -- cgit v1.2.3 From d1dc68d7550e90ba56a3122f8de1f38bb5aa1e3a Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 12 Nov 2013 23:40:07 +0000 Subject: Add queryset aggregates to allowed fields in OrderingFilter --- rest_framework/filters.py | 1 + rest_framework/tests/test_filters.py | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index e287a168..5c6a187c 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -124,6 +124,7 @@ class OrderingFilter(BaseFilterBackend): def remove_invalid_fields(self, queryset, ordering): field_names = [field.name for field in queryset.model._meta.fields] + field_names += queryset.query.aggregates.keys() return [term for term in ordering if term.lstrip('-') in field_names] def filter_queryset(self, request, queryset, view): diff --git a/rest_framework/tests/test_filters.py b/rest_framework/tests/test_filters.py index 379db29d..614f45cc 100644 --- a/rest_framework/tests/test_filters.py +++ b/rest_framework/tests/test_filters.py @@ -363,6 +363,12 @@ class OrdringFilterModel(models.Model): text = models.CharField(max_length=100) +class OrderingFilterRelatedModel(models.Model): + related_object = models.ForeignKey(OrdringFilterModel, + related_name="relateds") + + + class OrderingFilterTests(TestCase): def setUp(self): # Sequence of title/text is: @@ -472,3 +478,36 @@ class OrderingFilterTests(TestCase): {'id': 1, 'title': 'zyx', 'text': 'abc'}, ] ) + + def test_ordering_by_aggregate_field(self): + # create some related models to aggregate order by + num_objs = [2, 5, 3] + for obj, num_relateds in zip(OrdringFilterModel.objects.all(), + num_objs): + for _ in range(num_relateds): + new_related = OrderingFilterRelatedModel( + related_object=obj + ) + new_related.save() + + class OrderingListView(generics.ListAPIView): + model = OrdringFilterModel + filter_backends = (filters.OrderingFilter,) + ordering = 'title' + queryset = OrdringFilterModel.objects.all().annotate( + models.Count("relateds")) + + view = OrderingListView.as_view() + request = factory.get('?ordering=relateds__count') + response = view(request) + self.assertEqual( + response.data, + [ + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + ] + ) + + + -- cgit v1.2.3 From f4e610248b94f9a7708ec564fc545ce41561e6c5 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 12 Nov 2013 23:46:01 +0000 Subject: Bump version --- rest_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 2bd2991b..de82fef5 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.3.8' +__version__ = '2.3.9' VERSION = __version__ # synonym -- cgit v1.2.3 From e29942948f320088fbbf9a08cfd8579e0ef2e26f Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 13 Nov 2013 12:06:59 +0000 Subject: Undo version bump --- rest_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index de82fef5..2bd2991b 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.3.9' +__version__ = '2.3.8' VERSION = __version__ # synonym -- cgit v1.2.3 From 72d72e4d3e497000bab8019151972b0e71f6957a Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 13 Nov 2013 14:32:37 +0000 Subject: Add Alex Good to credits (Whoop!) --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 9751850b..e6c9c034 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -178,6 +178,7 @@ The following people have helped make REST framework great. * Alexander Lukanin - [alexanderlukanin13] * Yamila Moreno - [yamila-moreno] * Rob Hudson - [robhudson] +* Alex Good - [alexjg] Many thanks to everyone who's contributed to the project. @@ -392,3 +393,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [alexanderlukanin13]: https://github.com/alexanderlukanin13 [yamila-moreno]: https://github.com/yamila-moreno [robhudson]: https://github.com/robhudson +[alexjg]: https://github.com/alexjg -- cgit v1.2.3 From 6be62bc1d77f77978847584c358c7c00c34c0992 Mon Sep 17 00:00:00 2001 From: jgomezb Date: Thu, 14 Nov 2013 09:22:07 +0100 Subject: Update urlpatterns.py Allow numbers in format extension.--- rest_framework/urlpatterns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index d9143bb4..0ff137b0 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -57,6 +57,6 @@ def format_suffix_patterns(urlpatterns, suffix_required=False, allowed=None): allowed_pattern = '(%s)' % '|'.join(allowed) suffix_pattern = r'\.(?P<%s>%s)$' % (suffix_kwarg, allowed_pattern) else: - suffix_pattern = r'\.(?P<%s>[a-z]+)$' % suffix_kwarg + suffix_pattern = r'\.(?P<%s>[a-z0-9]+)$' % suffix_kwarg return apply_suffix_patterns(urlpatterns, suffix_pattern, suffix_required) -- cgit v1.2.3 From 1d1b2f765cbf3a56c677c44eee99260c8979a0a0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 15 Nov 2013 13:55:02 +0000 Subject: Version 2.3.9 --- docs/topics/release-notes.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 3df8869a..6c1447d5 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,10 +40,21 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series -### Master +### 2.3.9 +**Date**: 15th November 2013 + +* Fix Django 1.6 exception API compatibility issue caused by `ValidationError`. +* Include errors in HTML forms in browsable API. * Added JSON renderer support for numpy scalars. +* Added `transform_` hooks on serializers for easily modifying field output. * Added `get_context` hook in `BrowsableAPIRenderer`. +* Allow serializers to be passed `files` but no `data`. +* `HTMLFormRenderer` now renders serializers directly to HTML without needing to create an intermediate form object. +* Added `get_filter_backends` hook. +* Added queryset aggregates to allowed fields in `OrderingFilter`. +* Bugfix: Fix decimal suppoprt with `YAMLRenderer`. +* Bugfix: Fix submission of unicode in browsable API through raw data form. ### 2.3.8 -- cgit v1.2.3 From 7a0e2ed6f6ce2f3b33af2c228f1895273eb9bc73 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 15 Nov 2013 13:55:36 +0000 Subject: Version 2.3.9 --- rest_framework/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 2bd2991b..de82fef5 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,4 +1,4 @@ -__version__ = '2.3.8' +__version__ = '2.3.9' VERSION = __version__ # synonym -- cgit v1.2.3 From 128bda5712ef041514c5e2feadef0ad248f33f54 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 15 Nov 2013 15:24:32 +0000 Subject: Use less specfic language in UltaJSON notes --- docs/api-guide/renderers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 1f286ef1..858e2f07 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -411,7 +411,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily ## UltraJSON -[UltraJSON][ultrajson] is a blazing-fast C JSON encoder which can give 2-10x performance increases on typical workloads. [Jacob Haslehurst][hzy] maintains the [drf-ujson-renderer][drf-ujson-renderer] package which implements JSON rendering using the UJSON package. +[UltraJSON][ultrajson] is an optimized C JSON encoder which can give significantly faster JSON rendering. [Jacob Haslehurst][hzy] maintains the [drf-ujson-renderer][drf-ujson-renderer] package which implements JSON rendering using the UJSON package. [cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process [conneg]: content-negotiation.md -- cgit v1.2.3 From 4807e00bffc0116630af42b86049f197f8d4fc99 Mon Sep 17 00:00:00 2001 From: George Hickman Date: Fri, 15 Nov 2013 15:49:53 +0000 Subject: Set up wheel distribution support --- setup.cfg | 2 ++ setup.py | 1 + 2 files changed, 3 insertions(+) create mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..5e409001 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[wheel] +universal = 1 diff --git a/setup.py b/setup.py index adf083cb..96fbc4f4 100755 --- a/setup.py +++ b/setup.py @@ -45,6 +45,7 @@ version = get_version('rest_framework') if sys.argv[-1] == 'publish': os.system("python setup.py sdist upload") + os.system("python setup.py bdist_wheel upload") print("You probably want to also tag the version now:") print(" git tag -a %s -m 'version %s'" % (version, version)) print(" git push --tags") -- cgit v1.2.3 From ad7aa8fe485580e1bdff53d39fe3ea282d908a04 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Sun, 17 Nov 2013 01:27:16 +0100 Subject: Fixed the nested model serializers in case of the related_name isn’t set. --- rest_framework/serializers.py | 2 +- rest_framework/tests/test_serializer_nested.py | 35 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 163abf4f..e20e582e 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -872,7 +872,7 @@ class ModelSerializer(Serializer): # Reverse fk or one-to-one relations for (obj, model) in meta.get_all_related_objects_with_model(): - field_name = obj.field.related_query_name() + field_name = obj.get_accessor_name() if field_name in attrs: related_data[field_name] = attrs.pop(field_name) diff --git a/rest_framework/tests/test_serializer_nested.py b/rest_framework/tests/test_serializer_nested.py index 029f8bff..7114a060 100644 --- a/rest_framework/tests/test_serializer_nested.py +++ b/rest_framework/tests/test_serializer_nested.py @@ -6,6 +6,7 @@ Doesn't cover model serializers. from __future__ import unicode_literals from django.test import TestCase from rest_framework import serializers +from . import models class WritableNestedSerializerBasicTests(TestCase): @@ -311,3 +312,37 @@ class ForeignKeyNestedSerializerUpdateTests(TestCase): serializer = self.AlbumSerializer(instance=original, data=data) self.assertEqual(serializer.is_valid(), True) self.assertEqual(serializer.object, expected) + + +class NestedModelSerializerUpdateTests(TestCase): + def test_second_nested_level(self): + john = models.Person.objects.create(name="john") + + post = john.blogpost_set.create(title="Test blog post") + post.blogpostcomment_set.create(text="I hate this blog post") + post.blogpostcomment_set.create(text="I love this blog post") + + class BlogPostCommentSerializer(serializers.ModelSerializer): + class Meta: + model = models.BlogPostComment + + class BlogPostSerializer(serializers.ModelSerializer): + comments = BlogPostCommentSerializer(many=True, source='blogpostcomment_set') + class Meta: + model = models.BlogPost + fields = ('id', 'title', 'comments') + + class PersonSerializer(serializers.ModelSerializer): + posts = BlogPostSerializer(many=True, source='blogpost_set') + class Meta: + model = models.Person + fields = ('id', 'name', 'age', 'posts') + + serialize = PersonSerializer(instance=john) + deserialize = PersonSerializer(data=serialize.data, instance=john) + self.assertTrue(deserialize.is_valid()) + + result = deserialize.object + result.save() + self.assertEqual(result.id, john.id) + -- cgit v1.2.3 From f322e894aebd042a9edb0e333662d0b0ed1ba750 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Mon, 18 Nov 2013 09:21:09 +0200 Subject: Enabled syntax highlighting in the README file. --- README.md | 65 +++++++++++++++++++++++++++++++++------------------------------ 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 62883e32..68486800 100644 --- a/README.md +++ b/README.md @@ -48,48 +48,51 @@ Let's take a look at a quick example of using REST framework to build a simple m Here's our project's root `urls.py` module: - from django.conf.urls.defaults import url, patterns, include - from django.contrib.auth.models import User, Group - from rest_framework import viewsets, routers +```python +from django.conf.urls.defaults import url, patterns, include +from django.contrib.auth.models import User, Group +from rest_framework import viewsets, routers - # ViewSets define the view behavior. - class UserViewSet(viewsets.ModelViewSet): - model = User +# ViewSets define the view behavior. +class UserViewSet(viewsets.ModelViewSet): + model = User - class GroupViewSet(viewsets.ModelViewSet): - model = Group +class GroupViewSet(viewsets.ModelViewSet): + model = Group - # Routers provide an easy way of automatically determining the URL conf - router = routers.DefaultRouter() - router.register(r'users', UserViewSet) - router.register(r'groups', GroupViewSet) +# Routers provide an easy way of automatically determining the URL conf +router = routers.DefaultRouter() +router.register(r'users', UserViewSet) +router.register(r'groups', GroupViewSet) - # Wire up our API using automatic URL routing. - # Additionally, we include login URLs for the browseable API. - urlpatterns = patterns('', - url(r'^', include(router.urls)), - url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) - ) +# Wire up our API using automatic URL routing. +# Additionally, we include login URLs for the browseable API. +urlpatterns = patterns('', + url(r'^', include(router.urls)), + url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) +) +``` We'd also like to configure a couple of settings for our API. Add the following to your `settings.py` module: - REST_FRAMEWORK = { - # Use hyperlinked styles by default. - # Only used if the `serializer_class` attribute is not set on a view. - 'DEFAULT_MODEL_SERIALIZER_CLASS': - 'rest_framework.serializers.HyperlinkedModelSerializer', - - # Use Django's standard `django.contrib.auth` permissions, - # or allow read-only access for unauthenticated users. - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' - ] - } - +```python +REST_FRAMEWORK = { + # Use hyperlinked styles by default. + # Only used if the `serializer_class` attribute is not set on a view. + 'DEFAULT_MODEL_SERIALIZER_CLASS': + 'rest_framework.serializers.HyperlinkedModelSerializer', + + # Use Django's standard `django.contrib.auth` permissions, + # or allow read-only access for unauthenticated users. + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly' + ] +} +``` Don't forget to make sure you've also added `rest_framework` to your `INSTALLED_APPS` setting. That's it, we're done! -- cgit v1.2.3 From 075b8c1037588a590360ab5290b25648367a26c5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 18 Nov 2013 09:29:09 +0000 Subject: Add User import. Refs #599 --- docs/tutorial/4-authentication-and-permissions.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index ecf92a7b..b472322a 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -67,6 +67,9 @@ Because `'snippets'` is a *reverse* relationship on the User model, it will not 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. + from django.contrib.auth.models import User + + class UserList(generics.ListAPIView): queryset = User.objects.all() serializer_class = UserSerializer -- cgit v1.2.3 From a8b15f4290f4bad17d0dd599b8d5c29c155b89e5 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 18 Nov 2013 15:06:39 +0100 Subject: Another fix for nested writable serializers in case of the related_name isn’t set on the ForeignKey. --- rest_framework/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e20e582e..5059c71b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -940,11 +940,12 @@ class ModelSerializer(Serializer): del(obj._m2m_data) if getattr(obj, '_related_data', None): + related_fields = dict(((f.get_accessor_name(), f) for f, m in obj._meta.get_all_related_objects_with_model())) for accessor_name, related in obj._related_data.items(): if isinstance(related, RelationsList): # Nested reverse fk relationship for related_item in related: - fk_field = obj._meta.get_field_by_name(accessor_name)[0].field.name + fk_field = related_fields[accessor_name].field.name setattr(related_item, fk_field, obj) self.save_object(related_item) -- cgit v1.2.3 From 20325cce1808cc3facd60acb15115d285ca10b0f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 18 Nov 2013 16:10:14 +0000 Subject: Drop .html suffixes in docs --- docs/template.html | 1 + mkdocs.py | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/template.html b/docs/template.html index 749d0afe..874ace54 100644 --- a/docs/template.html +++ b/docs/template.html @@ -4,6 +4,7 @@ {{ title }} + diff --git a/mkdocs.py b/mkdocs.py index 13228a0c..380a91e7 100755 --- a/mkdocs.py +++ b/mkdocs.py @@ -19,7 +19,7 @@ if local: index = 'index.html' else: base_url = 'http://django-rest-framework.org' - suffix = '.html' + suffix = '' index = '' @@ -90,7 +90,10 @@ for idx in range(len(path_list)): path = path_list[idx] rel = '../' * path.count('/') - if idx > 0: + if idx == 1 and not local: + # Link back to '/', not '/index' + prev_url_map[path] = '/' + elif idx > 0: prev_url_map[path] = rel + path_list[idx - 1][:-3] + suffix if idx < len(path_list) - 1: @@ -143,6 +146,7 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir): else: main_title = 'Django REST framework - ' + main_title + canonical_url = base_url + '/' + relative_path[:-3] + suffix prev_url = prev_url_map.get(relative_path) next_url = next_url_map.get(relative_path) @@ -152,6 +156,7 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir): output = output.replace('{{ title }}', main_title) output = output.replace('{{ description }}', description) output = output.replace('{{ page_id }}', filename[:-3]) + output = output.replace('{{ canonical_url }}', canonical_url) if prev_url: output = output.replace('{{ prev_url }}', prev_url) -- cgit v1.2.3 From bc69a6d983072ecbcf0c7a79b0b954f0c92892a4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 18 Nov 2013 16:10:26 +0000 Subject: Add a 404 page to the docs --- docs/404.html | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 docs/404.html diff --git a/docs/404.html b/docs/404.html new file mode 100644 index 00000000..4938da6e --- /dev/null +++ b/docs/404.html @@ -0,0 +1,201 @@ + + + + + Django REST framework - 404 - Page not found + + + + + + + + + + + + + + + + + + + +
+ + + +
+
+ + + + +
+
+

404

+

Page not found

+

Try the homepage, or search the documentation.

+
+
+
+
+ +
+
+ + + + + + + + + + -- cgit v1.2.3 From 88f5921f2fb7f4480e5d3da97508c815bba17155 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 18 Nov 2013 20:15:35 +0100 Subject: Removed the DynamicSerializerView duplication --- rest_framework/tests/test_generics.py | 41 ++++++++++++++++------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py index 79cd99ac..3fcef606 100644 --- a/rest_framework/tests/test_generics.py +++ b/rest_framework/tests/test_generics.py @@ -508,6 +508,25 @@ class ExclusiveFilterBackend(object): return queryset.filter(text='other') +class TwoFieldModel(models.Model): + field_a = models.CharField(max_length=100) + field_b = models.CharField(max_length=100) + + +class DynamicSerializerView(generics.ListCreateAPIView): + model = TwoFieldModel + renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer) + + def get_serializer_class(self): + if self.request.method == 'POST': + class DynamicSerializer(serializers.ModelSerializer): + class Meta: + model = TwoFieldModel + fields = ('field_b',) + return DynamicSerializer + return super(DynamicSerializerView, self).get_serializer_class() + + class TestFilterBackendAppliedToViews(TestCase): def setUp(self): @@ -564,28 +583,6 @@ class TestFilterBackendAppliedToViews(TestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, {'id': 1, 'text': 'foo'}) - -class TwoFieldModel(models.Model): - field_a = models.CharField(max_length=100) - field_b = models.CharField(max_length=100) - - -class DynamicSerializerView(generics.ListCreateAPIView): - model = TwoFieldModel - renderer_classes = (renderers.BrowsableAPIRenderer, renderers.JSONRenderer) - - def get_serializer_class(self): - if self.request.method == 'POST': - class DynamicSerializer(serializers.ModelSerializer): - class Meta: - model = TwoFieldModel - fields = ('field_b',) - return DynamicSerializer - return super(DynamicSerializerView, self).get_serializer_class() - - -class TestFilterBackendAppliedToViews(TestCase): - def test_dynamic_serializer_form_in_browsable_api(self): """ GET requests to ListCreateAPIView should return filtered list. -- cgit v1.2.3 From 6330212453bcddab9ecc8dcf0c368c3e8af8876d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 19 Nov 2013 10:27:04 +0000 Subject: Canonical page for index should be '/', not '/index' --- mkdocs.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mkdocs.py b/mkdocs.py index 380a91e7..d1790168 100755 --- a/mkdocs.py +++ b/mkdocs.py @@ -146,7 +146,10 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir): else: main_title = 'Django REST framework - ' + main_title - canonical_url = base_url + '/' + relative_path[:-3] + suffix + if relative_path == 'index.md': + canonical_url = base_url + else: + canonical_url = base_url + '/' + relative_path[:-3] + suffix prev_url = prev_url_map.get(relative_path) next_url = next_url_map.get(relative_path) -- cgit v1.2.3 From ca2bd616d989f78d00641231e645198a6c95caa0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 19 Nov 2013 11:01:27 +0000 Subject: Remove a couple of .html suffixes in docs --- docs/index.md | 10 +++++----- docs/topics/2.2-announcement.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/index.md b/docs/index.md index bb2129f6..775fae62 100644 --- a/docs/index.md +++ b/docs/index.md @@ -255,11 +255,11 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [0.4]: https://github.com/tomchristie/django-rest-framework/tree/0.4.X [image]: img/quickstart.png [index]: . -[oauth1-section]: api-guide/authentication.html#oauthauthentication -[oauth2-section]: api-guide/authentication.html#oauth2authentication -[serializer-section]: api-guide/serializers.html#serializers -[modelserializer-section]: api-guide/serializers.html#modelserializer -[functionview-section]: api-guide/views.html#function-based-views +[oauth1-section]: api-guide/authentication#oauthauthentication +[oauth2-section]: api-guide/authentication#oauth2authentication +[serializer-section]: api-guide/serializers#serializers +[modelserializer-section]: api-guide/serializers#modelserializer +[functionview-section]: api-guide/views#function-based-views [sandbox]: http://restframework.herokuapp.com/ [quickstart]: tutorial/quickstart.md diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md index 7d276049..0f980e1c 100644 --- a/docs/topics/2.2-announcement.md +++ b/docs/topics/2.2-announcement.md @@ -151,7 +151,7 @@ From version 2.2 onwards, serializers with hyperlinked relationships *always* re [porting-python-3]: https://docs.djangoproject.com/en/dev/topics/python3/ [python-compat]: https://docs.djangoproject.com/en/dev/releases/1.5/#python-compatibility [django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy -[credits]: http://django-rest-framework.org/topics/credits.html +[credits]: http://django-rest-framework.org/topics/credits [mailing-list]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [django-rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs [marcgibbons]: https://github.com/marcgibbons/ -- cgit v1.2.3 From 9cea6880f7103c4e9407f975753c830f109c8c7c Mon Sep 17 00:00:00 2001 From: Krzysztof Jurewicz Date: Tue, 19 Nov 2013 15:49:31 +0100 Subject: Added handling of validation errors in PUT-as-create. Fixes #1035. --- rest_framework/mixins.py | 8 +++++++- rest_framework/tests/test_generics.py | 19 +++++++++++++++++-- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 4606c78b..79f79c30 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -6,6 +6,7 @@ which allows mixin classes to be composed in interesting ways. """ from __future__ import unicode_literals +from django.core.exceptions import ValidationError from django.http import Http404 from rest_framework import status from rest_framework.response import Response @@ -127,7 +128,12 @@ class UpdateModelMixin(object): files=request.FILES, partial=partial) if serializer.is_valid(): - self.pre_save(serializer.object) + try: + self.pre_save(serializer.object) + except ValidationError as err: + # full_clean on model instance may be called in pre_save, so we + # have to handle eventual errors. + return Response(err.message_dict, status=status.HTTP_400_BAD_REQUEST) self.object = serializer.save(**save_kwargs) self.post_save(self.object, created=created) return Response(serializer.data, status=success_status_code) diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py index 3fcef606..996bd5b0 100644 --- a/rest_framework/tests/test_generics.py +++ b/rest_framework/tests/test_generics.py @@ -23,6 +23,10 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView): """ model = BasicModel + def get_queryset(self): + queryset = super(InstanceView, self).get_queryset() + return queryset.exclude(text='filtered out') + class SlugSerializer(serializers.ModelSerializer): slug = serializers.Field() # read only @@ -160,10 +164,10 @@ class TestInstanceView(TestCase): """ Create 3 BasicModel intances. """ - items = ['foo', 'bar', 'baz'] + items = ['foo', 'bar', 'baz', 'filtered out'] for item in items: BasicModel(text=item).save() - self.objects = BasicModel.objects + self.objects = BasicModel.objects.exclude(text='filtered out') self.data = [ {'id': obj.id, 'text': obj.text} for obj in self.objects.all() @@ -352,6 +356,17 @@ class TestInstanceView(TestCase): updated = self.objects.get(id=1) self.assertEqual(updated.text, 'foobar') + def test_put_to_filtered_out_instance(self): + """ + PUT requests to an URL of instance which is filtered out should not be + able to create new objects. + """ + data = {'text': 'foo'} + filtered_out_pk = BasicModel.objects.filter(text='filtered out')[0].pk + request = factory.put('/{0}'.format(filtered_out_pk), data, format='json') + response = self.view(request, pk=filtered_out_pk).render() + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_put_as_create_on_id_based_url(self): """ PUT requests to RetrieveUpdateDestroyAPIView should create an object -- cgit v1.2.3 From 304b193efe076868ceca624543183791f5931726 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 19 Nov 2013 15:58:34 +0000 Subject: Update release-notes.md --- docs/topics/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 6c1447d5..0759bd9d 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,10 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series +### Master + +* Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400. + ### 2.3.9 **Date**: 15th November 2013 -- cgit v1.2.3 From 3765865b4bf69d76d5bcb8e9c8071f4380b54177 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 20 Nov 2013 17:40:56 +0000 Subject: Update 'default' explanation. Closes #1239 --- docs/api-guide/fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 4272c9a7..03c5af32 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -41,7 +41,7 @@ Defaults to `True`. ### `default` -If set, this gives the default value that will be used for the field if none is supplied. If not set the default behavior is to not populate the attribute at all. +If set, this gives the default value that will be used for the field if no input value is supplied. If not set the default behavior is to not populate the attribute at all. May be set to a function or other callable, in which case the value will be evaluated each time it is used. -- cgit v1.2.3 From 263281d71d0425d7bb9b4ebbdf1811ef637ee60a Mon Sep 17 00:00:00 2001 From: Malcolm Box Date: Thu, 21 Nov 2013 20:09:48 +0000 Subject: Fix issue #1231: JSONEncoder doesn't handle dict-like objects Check for __getitem__ and then attempt to convert to a dict. The check for __getitem__ is there as there's no universal way to check if an object is a mapping type, but this is a likely proxy --- rest_framework/tests/test_renderers.py | 57 ++++++++++++++++++++++++++++++++++ rest_framework/utils/encoders.py | 6 ++++ 2 files changed, 63 insertions(+) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 76299a89..18da6ef8 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -18,6 +18,9 @@ from rest_framework.test import APIRequestFactory import datetime import pickle import re +import UserDict +import collections +import json DUMMYSTATUS = status.HTTP_200_OK @@ -244,6 +247,60 @@ class JSONRendererTests(TestCase): ret = JSONRenderer().render(_('test')) self.assertEqual(ret, b'"test"') + def test_render_userdict_obj(self): + class DictLike(UserDict.DictMixin): + def __init__(self): + self._dict = dict() + def __getitem__(self, key): + return self._dict.__getitem__(key) + def __setitem__(self, key, value): + return self._dict.__setitem__(key, value) + def __delitem__(self, key): + return self._dict.__delitem__(key) + def keys(self): + return self._dict.keys() + x = DictLike() + x['a'] = 1 + x['b'] = "string value" + ret = JSONRenderer().render(x) + self.assertEquals(json.loads(ret), {u'a': 1, u'b': u'string value'}) + + def test_render_dict_abc_obj(self): + class Dict(collections.MutableMapping): + def __init__(self): + self._dict = dict() + def __getitem__(self, key): + return self._dict.__getitem__(key) + def __setitem__(self, key, value): + return self._dict.__setitem__(key, value) + def __delitem__(self, key): + return self._dict.__delitem__(key) + def __iter__(self): + return self._dict.__iter__() + def __len__(self): + return self._dict.__len__() + + x = Dict() + x['key'] = 'string value' + x[2] = 3 + ret = JSONRenderer().render(x) + self.assertEquals(json.loads(ret), {u'key': 'string value', u'2': 3}) + + + def test_render_obj_with_getitem(self): + class DictLike(object): + def __init__(self): + self._dict = {} + def set(self, value): + self._dict = dict(value) + def __getitem__(self, key): + return self._dict[key] + + x = DictLike() + x.set({'a': 1, 'b': 'string'}) + with self.assertRaises(TypeError): + JSONRenderer().render(x) + def test_without_content_type_args(self): """ Test basic JSON rendering. diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 35ad206b..22b1ab3d 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -44,6 +44,12 @@ class JSONEncoder(json.JSONEncoder): return str(o) elif hasattr(o, 'tolist'): return o.tolist() + elif hasattr(o, '__getitem__'): + try: + return dict(o) + except KeyError: + # Couldn't convert to a dict, fall through + pass elif hasattr(o, '__iter__'): return [i for i in o] return super(JSONEncoder, self).default(o) -- cgit v1.2.3 From 6af31ed3945fd051a6e8c08851d7a656637d1f00 Mon Sep 17 00:00:00 2001 From: Malcolm Box Date: Fri, 22 Nov 2013 10:59:48 +0000 Subject: Remove u from literals --- rest_framework/tests/test_renderers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 18da6ef8..9c9c7452 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -263,7 +263,7 @@ class JSONRendererTests(TestCase): x['a'] = 1 x['b'] = "string value" ret = JSONRenderer().render(x) - self.assertEquals(json.loads(ret), {u'a': 1, u'b': u'string value'}) + self.assertEquals(json.loads(ret), {'a': 1, 'b': 'string value'}) def test_render_dict_abc_obj(self): class Dict(collections.MutableMapping): @@ -284,7 +284,7 @@ class JSONRendererTests(TestCase): x['key'] = 'string value' x[2] = 3 ret = JSONRenderer().render(x) - self.assertEquals(json.loads(ret), {u'key': 'string value', u'2': 3}) + self.assertEquals(json.loads(ret), {'key': 'string value', '2': 3}) def test_render_obj_with_getitem(self): -- cgit v1.2.3 From a38d9d5b24501ae0e279c9afbea08e423112ba34 Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Tue, 26 Nov 2013 09:33:47 +0000 Subject: Add choices to options metadata for ChoiceField. --- rest_framework/fields.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 6c07dbb3..80eff66c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -514,6 +514,11 @@ class ChoiceField(WritableField): choices = property(_get_choices, _set_choices) + def metadata(self): + data = super(ChoiceField, self).metadata() + data['choices'] = self.choices + return data + def validate(self, value): """ Validates that the input is in self.choices. -- cgit v1.2.3 From 2484fc914159571a3867c2dae2d9b51314f4581d Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Tue, 26 Nov 2013 17:10:16 +0000 Subject: Add more context to the ChoiceField metadata. --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 80eff66c..1657e57f 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -516,7 +516,7 @@ class ChoiceField(WritableField): def metadata(self): data = super(ChoiceField, self).metadata() - data['choices'] = self.choices + data['choices'] = [{'value': v, 'name': n} for v, n in self.choices] return data def validate(self, value): -- cgit v1.2.3 From 8d09f56061a3ee82e31fb646cfa84484ae525f88 Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Wed, 27 Nov 2013 11:00:15 +0000 Subject: Add unittests for ChoiceField metadata. Rename 'name' to 'display_name'. --- rest_framework/fields.py | 2 +- rest_framework/tests/test_fields.py | 26 ++++++++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 1657e57f..0fca718e 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -516,7 +516,7 @@ class ChoiceField(WritableField): def metadata(self): data = super(ChoiceField, self).metadata() - data['choices'] = [{'value': v, 'name': n} for v, n in self.choices] + data['choices'] = [{'value': v, 'display_name': n} for v, n in self.choices] return data def validate(self, value): diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index ab2cceac..5c96bce9 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -707,20 +707,21 @@ class ChoiceFieldTests(TestCase): self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES) def test_invalid_choice_model(self): - s = ChoiceFieldModelSerializer(data={'choice' : 'wrong_value'}) + s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'}) self.assertFalse(s.is_valid()) self.assertEqual(s.errors, {'choice': ['Select a valid choice. wrong_value is not one of the available choices.']}) self.assertEqual(s.data['choice'], '') def test_empty_choice_model(self): """ - Test that the 'empty' value is correctly passed and used depending on the 'null' property on the model field. + Test that the 'empty' value is correctly passed and used depending on + the 'null' property on the model field. """ - s = ChoiceFieldModelSerializer(data={'choice' : ''}) + s = ChoiceFieldModelSerializer(data={'choice': ''}) self.assertTrue(s.is_valid()) self.assertEqual(s.data['choice'], '') - s = ChoiceFieldModelWithNullSerializer(data={'choice' : ''}) + s = ChoiceFieldModelWithNullSerializer(data={'choice': ''}) self.assertTrue(s.is_valid()) self.assertEqual(s.data['choice'], None) @@ -740,6 +741,23 @@ class ChoiceFieldTests(TestCase): self.assertEqual(f.from_native(''), None) self.assertEqual(f.from_native(None), None) + def test_metadata_choices(self): + """ + Make sure proper choices are included in the field's metadata. + """ + choices = [{'value': v, 'display_name': n} for v, n in SAMPLE_CHOICES] + f = serializers.ChoiceField(choices=SAMPLE_CHOICES) + self.assertEqual(f.metadata()['choices'], choices) + + def test_metadata_choices_not_required(self): + """ + Make sure proper choices are included in the field's metadata. + """ + choices = [{'value': v, 'display_name': n} + for v, n in models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES] + f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES) + self.assertEqual(f.metadata()['choices'], choices) + class EmailFieldTests(TestCase): """ -- cgit v1.2.3 From 2dce8d7a8a0e64f84994b6ac437e2d96920f094e Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 27 Nov 2013 13:23:49 +0200 Subject: Recommend using Pillow instead of PIL. --- docs/api-guide/fields.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 03c5af32..b0dedd39 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -286,7 +286,7 @@ An image representation. Corresponds to `django.forms.fields.ImageField`. -Requires the `PIL` package. +Requires either the `Pillow` package or `PIL` package. It is strongly recommends to use `Pillow` where possible. `PIL` is practically unmaintained and introduces [many problems][pilproblems]. Signature and validation is the same as with `FileField`. @@ -345,3 +345,4 @@ As an example, let's create a field that can be used represent the class name of [ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 [strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior [iso8601]: http://www.w3.org/TR/NOTE-datetime +[pilproblems]: http://pillow.readthedocs.org/en/latest/about.html -- cgit v1.2.3 From b8f8fb7779dc01b5117e468345aaf99304f807ac Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 27 Nov 2013 13:26:49 +0200 Subject: Updated the assertion message of the ImageField. --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 6c07dbb3..463d296f 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -966,7 +966,7 @@ class ImageField(FileField): return None from rest_framework.compat import Image - assert Image is not None, 'PIL must be installed for ImageField support' + assert Image is not None, 'Either Pillow or PIL must be installed for ImageField support.' # We need to get a file object for PIL. We might have a path or we might # have to read the data into memory. -- cgit v1.2.3 From c46106c96158a99eb2ff29c464a2fa60aff23122 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Wed, 27 Nov 2013 14:47:37 +0200 Subject: Rephrased documentation changes according to feedback on IRC. --- docs/api-guide/fields.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index b0dedd39..e05c0306 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -286,7 +286,7 @@ An image representation. Corresponds to `django.forms.fields.ImageField`. -Requires either the `Pillow` package or `PIL` package. It is strongly recommends to use `Pillow` where possible. `PIL` is practically unmaintained and introduces [many problems][pilproblems]. +Requires either the `Pillow` package or `PIL` package. The `Pillow` package is recommended, as `PIL` is no longer actively maintained. Signature and validation is the same as with `FileField`. @@ -345,4 +345,3 @@ As an example, let's create a field that can be used represent the class name of [ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 [strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior [iso8601]: http://www.w3.org/TR/NOTE-datetime -[pilproblems]: http://pillow.readthedocs.org/en/latest/about.html -- cgit v1.2.3 From 850cd83ba709e863598f8eec3d6551ef3bc3801c Mon Sep 17 00:00:00 2001 From: Stephan Groß Date: Mon, 2 Dec 2013 11:44:04 +0100 Subject: Fix TemplateHTMLRenderer example --- docs/api-guide/renderers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 858e2f07..f30fa26a 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -167,14 +167,14 @@ The template name is determined by (in order of preference): An example of a view that uses `TemplateHTMLRenderer`: - class UserDetail(generics.RetrieveUserAPIView): + class UserDetail(generics.RetrieveAPIView): """ A view that returns a templated HTML representations of a given user. """ queryset = User.objects.all() renderer_classes = (TemplateHTMLRenderer,) - def get(self, request, *args, **kwargs) + def get(self, request, *args, **kwargs): self.object = self.get_object() return Response({'user': self.object}, template_name='user_detail.html') -- cgit v1.2.3 From 699ec7236b326c97a98c6058280b822c701393fe Mon Sep 17 00:00:00 2001 From: Pablo Recio Date: Tue, 3 Dec 2013 00:07:41 +0000 Subject: Adds pre_delete and post_delete hooks on --- docs/api-guide/generic-views.md | 4 +++- rest_framework/generics.py | 12 ++++++++++++ rest_framework/mixins.py | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index b9242724..83c3e45f 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -163,12 +163,14 @@ For example: return 20 return 100 -**Save hooks**: +**Save / deletion hooks**: The following methods are provided as placeholder interfaces. They contain empty implementations and are not called directly by `GenericAPIView`, but they are overridden and used by some of the mixin classes. * `pre_save(self, obj)` - A hook that is called before saving an object. * `post_save(self, obj, created=False)` - A hook that is called after saving an object. +* `pre_delete(self, obj)` - A hook that is called before deleting an object. +* `post_delete(self, obj)` - A hook that is called after deleting an object. The `pre_save` method in particular is a useful hook for setting attributes that are implicit in the request, but are not part of the request data. For instance, you might set an attribute on the object based on the request user, or based on a URL keyword argument. diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 7cb80a84..fd411ad3 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -344,6 +344,18 @@ class GenericAPIView(views.APIView): """ pass + def pre_delete(self, obj): + """ + Placeholder method for calling before deleting an object. + """ + pass + + def post_delete(self, obj): + """ + Placeholder method for calling after saving an object. + """ + pass + def metadata(self, request): """ Return a dictionary of metadata about the view. diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 79f79c30..43950c4b 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -192,5 +192,7 @@ class DestroyModelMixin(object): """ def destroy(self, request, *args, **kwargs): obj = self.get_object() + self.pre_delete(obj) obj.delete() + self.post_delete(obj) return Response(status=status.HTTP_204_NO_CONTENT) -- cgit v1.2.3 From fe4c7d4000675a1720e7d3e02c3014a693d835aa Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Dec 2013 08:26:58 +0000 Subject: Update release-notes.md --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 0759bd9d..c7e24a5e 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -42,6 +42,7 @@ You can determine your currently installed version using `pip freeze`: ### Master +* Added `pre_delete()` and `post_delete()` method hooks. * Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400. ### 2.3.9 -- cgit v1.2.3 From c1d9a96df03ea81454d7d0e3583c68e270ed5043 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Dec 2013 08:58:05 +0000 Subject: Catch errors during parsing and set empty .DATA/.FILES before re-raising. --- rest_framework/request.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index b883d0d4..9b551aa8 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -356,7 +356,16 @@ class Request(object): if not parser: raise exceptions.UnsupportedMediaType(media_type) - parsed = parser.parse(stream, media_type, self.parser_context) + try: + parsed = parser.parse(stream, media_type, self.parser_context) + except: + # If we get an exception during parsing, fill in empty data and + # re-raise. Ensures we don't simply repeat the error when + # attempting to render the browsable renderer response, or when + # logging the request or similar. + self._data = QueryDict('', self._request._encoding) + self._files = MultiValueDict() + raise # Parser classes may return the raw data, or a # DataAndFiles object. Unpack the result as required. -- cgit v1.2.3 From b92c911cf66805f7826713c68f4e6704b2ad4589 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Dec 2013 16:05:19 +0000 Subject: Update release-notes.md --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index c7e24a5e..f9ad55f7 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -42,6 +42,7 @@ You can determine your currently installed version using `pip freeze`: ### Master +* Add in choices information for ChoiceFields in response to `OPTIONS` requests. * Added `pre_delete()` and `post_delete()` method hooks. * Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400. -- cgit v1.2.3 From 9f1918e41e1b8dcfa621b00788bab865f2fc31aa Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Dec 2013 16:06:57 +0000 Subject: Added @ian-foote, for work on #1250. Thanks! --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index e6c9c034..3395cd9e 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -179,6 +179,7 @@ The following people have helped make REST framework great. * Yamila Moreno - [yamila-moreno] * Rob Hudson - [robhudson] * Alex Good - [alexjg] +* Ian Foote - [ian-foote] Many thanks to everyone who's contributed to the project. @@ -394,3 +395,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [yamila-moreno]: https://github.com/yamila-moreno [robhudson]: https://github.com/robhudson [alexjg]: https://github.com/alexjg +[ian-foote]: https://github.com/ian-foote -- cgit v1.2.3 From 774298f145d18292b76f2bd90469e25c1950b1af Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Dec 2013 16:18:35 +0000 Subject: First pass at a test for ParseErrors breaking the browsable API --- rest_framework/tests/test_renderers.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 76299a89..549e763b 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -88,6 +88,7 @@ urlpatterns = patterns('', url(r'^cache$', MockGETView.as_view()), url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])), url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])), + url(r'^parseerror$', MockGETView.as_view(renderer_classes=[JSONRenderer, BrowsableAPIRenderer])), url(r'^html$', HTMLView.as_view()), url(r'^html1$', HTMLView1.as_view()), url(r'^api', include('rest_framework.urls', namespace='rest_framework')) @@ -219,6 +220,12 @@ class RendererEndToEndTests(TestCase): self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEqual(resp.status_code, DUMMYSTATUS) + def test_parse_error_renderers_browsable_api(self): + """Invalid data should still render the browsable API correctly.""" + resp = self.client.post('/parseerror', data='foobar', content_type='application/json', HTTP_ACCEPT='text/html') + self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8') + self.assertContains(resp.content, 'Mock GET View') + self.assertEqual(resp.status_code, status.HTTP_400_) _flat_repr = '{"foo": ["bar", "baz"]}' _indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' -- cgit v1.2.3 From 38d78b21c0a7c68c205ebe6e79433ca51fe609ce Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 3 Dec 2013 16:55:11 +0000 Subject: Remove Content-Type header from empty responses. Fixes #1196 --- docs/topics/release-notes.md | 1 + rest_framework/response.py | 4 ++++ rest_framework/tests/test_renderers.py | 18 +++++++++++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index f9ad55f7..e6085f59 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -44,6 +44,7 @@ You can determine your currently installed version using `pip freeze`: * Add in choices information for ChoiceFields in response to `OPTIONS` requests. * Added `pre_delete()` and `post_delete()` method hooks. +* Bugfix: Responses without any content no longer include an HTTP `'Content-Type'` header. * Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400. ### 2.3.9 diff --git a/rest_framework/response.py b/rest_framework/response.py index 5877c8a3..1dc6abcf 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -61,6 +61,10 @@ class Response(SimpleTemplateResponse): assert charset, 'renderer returned unicode, and did not specify ' \ 'a charset value.' return bytes(ret.encode(charset)) + + if not ret: + del self['Content-Type'] + return ret @property diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 76299a89..f7de8fd7 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -64,11 +64,16 @@ class MockView(APIView): class MockGETView(APIView): - def get(self, request, **kwargs): return Response({'foo': ['bar', 'baz']}) +class EmptyGETView(APIView): + renderer_classes = (JSONRenderer,) + + def get(self, request, **kwargs): + return Response(status=status.HTTP_204_NO_CONTENT) + class HTMLView(APIView): renderer_classes = (BrowsableAPIRenderer, ) @@ -90,6 +95,7 @@ urlpatterns = patterns('', url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])), url(r'^html$', HTMLView.as_view()), url(r'^html1$', HTMLView1.as_view()), + url(r'^empty$', EmptyGETView.as_view()), url(r'^api', include('rest_framework.urls', namespace='rest_framework')) ) @@ -219,6 +225,16 @@ class RendererEndToEndTests(TestCase): self.assertEqual(resp.content, RENDERER_B_SERIALIZER(DUMMYCONTENT)) self.assertEqual(resp.status_code, DUMMYSTATUS) + def test_204_no_content_responses_have_no_content_type_set(self): + """ + Regression test for #1196 + + https://github.com/tomchristie/django-rest-framework/issues/1196 + """ + resp = self.client.get('/empty') + self.assertEqual(resp.get('Content-Type', None), None) + self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT) + _flat_repr = '{"foo": ["bar", "baz"]}' _indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' -- cgit v1.2.3 From 3c3906e278d5e707ab1fd72bdbcb79649777df33 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 4 Dec 2013 08:51:34 +0000 Subject: Clarify wording, fixes #1133. --- docs/api-guide/viewsets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 1062cb32..4fdd9364 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -170,7 +170,7 @@ The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`, #### Example -Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes. For example: +Because `ModelViewSet` extends `GenericAPIView`, you'll normally need to provide at least the `queryset` and `serializer_class` attributes, or the `model` attribute shortcut. For example: class AccountViewSet(viewsets.ModelViewSet): """ -- cgit v1.2.3 From de5b9e39dd4c21f4dcceb7cf13c7366b0fb74ec9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 4 Dec 2013 14:59:09 +0000 Subject: First pass on contribution guide --- docs/img/travis-status.png | Bin 0 -> 10023 bytes docs/index.md | 1 + docs/template.html | 1 + docs/topics/contributing.md | 91 ++++++++++++++++++++++++++++++-------------- 4 files changed, 64 insertions(+), 29 deletions(-) create mode 100644 docs/img/travis-status.png diff --git a/docs/img/travis-status.png b/docs/img/travis-status.png new file mode 100644 index 00000000..fec98cf9 Binary files /dev/null and b/docs/img/travis-status.png differ diff --git a/docs/index.md b/docs/index.md index 775fae62..3e5adbc4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -177,6 +177,7 @@ General guides to using REST framework. * [Browser enhancements][browser-enhancements] * [The Browsable API][browsableapi] * [REST, Hypermedia & HATEOAS][rest-hypermedia-hateoas] +* [Contributing to REST framework][contributing] * [2.0 Announcement][rest-framework-2-announcement] * [2.2 Announcement][2.2-announcement] * [2.3 Announcement][2.3-announcement] diff --git a/docs/template.html b/docs/template.html index 874ace54..5a0bdbfd 100644 --- a/docs/template.html +++ b/docs/template.html @@ -102,6 +102,7 @@
  • Browser enhancements
  • The Browsable API
  • REST, Hypermedia & HATEOAS
  • +
  • Contributing to REST framework
  • 2.0 Announcement
  • 2.2 Announcement
  • 2.3 Announcement
  • diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index 123e4a8a..2b18c4f6 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -6,19 +6,27 @@ There are many ways you can contribute to Django REST framework. We'd like it to be a community-led project, so please get involved and help shape the future of the project. -# Community +## Community -If you use and enjoy REST framework please consider [staring the project on GitHub][github], and [upvoting it on Django packages][django-packages]. Doing so helps potential new users see that the project is well used, and help us continue to attract new users. +The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case. -You might also consider writing a blog post on your experience with using REST framework, writing a tutorial about using the project with a particular javascript framework, or simply sharing the love on Twitter. +If you use REST framework, we'd love you to be vocal about your experiances with it - you might consider writing a blog post on your experience with using REST framework, or publishing a tutorial about using the project with a particular javascript framework. Experiances from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are and aren't easy to understand and work with. Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag. When answering questions make sure to help future contributors find their way around by hyperlinking wherever possible to related threads and tickets, and include backlinks from those items if relevant. +## Code of conduct + +Please keep the tone polite & professional. For some users a discussion on the REST framework mailing list or ticket tracker may be their first engagement with the open source community. First impressions count, so let's try to make everyone feel welcome. + +Be mindful in the language you choose. As an example, in an environment that is heavily male-dominated, posts that start 'Hey guys,' can come across as unintentionally exclusive. It's just as easy, and more inclusive to use gender neutral language in those situations. + +The [Django code of conduct][code-of-conduct] gives a fuller set of guidelines for participating in community forums. + # Issues -It's really helpful if you make sure you address issues to the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues]. +It's really helpful if you can make sure to address issues on the correct channel. Usage questions should be directed to the [discussion group][google-group]. Feature requests, bug reports and other issues should be raised on the GitHub [issue tracker][issues]. Some tips on good issue reporting: @@ -26,30 +34,61 @@ Some tips on good issue reporting: * Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue. * If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one. +## Triaging issues +Getting involved in triaging incoming issues is a good way to start contributing. Every single ticket that comes into the ticket tracker needs to be reviewed in order to determine what the next steps should be. Anyone can help out with this, you just need to be willing to -* TODO: Triage +* Read through the ticket - does it make sense, is it missing any context that would help explain it better? +* Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group? +* If the ticket is a bug report, can you reproduce it? Are you able to write a failing test case that demonstrates the issue and that can be submitted as a pull request? +* If the ticket is a feature request, do you agree with it, and could the feature request instead be implemented as a third party package? # Development +To start developing on Django REST framework, clone the repo: + + git clone git@github.com:tomchristie/django-rest-framework.git + +Changes should broadly follow the [PEP 8][pep-8] style conventions, and we recommend you setup your editor to automatically indicated non-conforming styles. + +## Testing + +To run the tests, clone the repository, and then: + + # Setup the virtual environment + virtualenv env + env/bin/activate + pip install -r requirements.txt + pip install -r optionals.txt + + # Run the tests + rest_framework/runtests/runtests.py -* git clone & PYTHONPATH -* Pep8 -* Recommend editor that runs pep8 +You can also use the excellent `[tox][tox]` testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run: -### Pull requests + tox -* Make pull requests early -* Describe branching +## Pull requests -### Managing compatibility issues +It's a good idea to make pull requests early on. A pull request represents the start of a discussion, and doesn't necessarily need to be the final, finished submission. -* Describe compat module +It's also always best to make a new branch before starting work on a pull request. This means that you'll be able to later switch back to working on another seperate issue without interfering with an ongoing pull requests. -# Testing +It's also useful to remember that if you have an outstanding pull request then pushing new commits to your GitHub repo will also automatically update the pull requests. -* Running the tests -* tox +GitHub's documentation for working on pull requests is [available here][pull-requests]. + +Always run the tests before submitting pull requests, and ideally run `tox` in order to check that your modifications are compatible with both Python 2 and Python 3, and that they run properly on all supported versions of Django. + +Once you've made a pull request take a look at the travis build status in the GitHub interface and make sure the tests are runnning as you'd expect. + +![Travis status][travis-status] + +*Above: Travis build notifications* + +## Managing compatibility issues + +Sometimes, in order to ensure your code works on various different versions of Django, Python or third party libraries, you'll need to run slightly different code depending on the environment. Any code that branches in this way should be isolated into the `compat.py` module, and should provide a single common interface that the rest of the codebase can use. # Documentation @@ -77,7 +116,7 @@ Some other tips: * Keep paragraphs reasonably short. * Use double spacing after the end of sentences. -* Don't use the abbreviations such as 'e.g..' but instead use long form, such as 'For example'. +* Don't use the abbreviations such as 'e.g.' but instead use long form, such as 'For example'. ## Markdown style @@ -118,25 +157,19 @@ If you want to draw attention to a note or warning, use a pair of enclosing line --- - **Note:** Make sure you do this thing. + **Note:** A useful documentation note. --- -# Third party packages - -* Django reusable app - -# Core committers - -* Still use pull reqs -* Credits - [cite]: http://www.w3.org/People/Berners-Lee/FAQ.html -[github]: https://github.com/tomchristie/django-rest-framework -[django-packages]: https://www.djangopackages.com/grids/g/api/ +[code-of-conduct]: https://www.djangoproject.com/conduct/ [google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [so-filter]: http://stackexchange.com/filters/66475/rest-framework [issues]: https://github.com/tomchristie/django-rest-framework/issues?state=open +[pep-8]: http://www.python.org/dev/peps/pep-0008/ +[travis-status]: ../img/travis-status.png +[pull-requests]: https://help.github.com/articles/using-pull-requests +[tox]: http://tox.readthedocs.org/en/latest/ [markdown]: http://daringfireball.net/projects/markdown/basics [docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs [mou]: http://mouapp.com/ -- cgit v1.2.3 From f2682537e0fa91bb415be1a64e6bc85275129141 Mon Sep 17 00:00:00 2001 From: Drew Kowalik Date: Wed, 4 Dec 2013 16:10:05 -0800 Subject: fix broken documentation links --- docs/api-guide/views.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md index 15581e09..194a7a6b 100644 --- a/docs/api-guide/views.md +++ b/docs/api-guide/views.md @@ -168,5 +168,5 @@ Each of these decorators takes a single argument which must be a list or tuple o [cite]: http://reinout.vanrees.org/weblog/2011/08/24/class-based-views-usage.html [cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html -[settings]: api-guide/settings.md -[throttling]: api-guide/throttling.md +[settings]: settings.md +[throttling]: throttling.md -- cgit v1.2.3 From f8088bedef04c5bc487bdc764ac54d1f18f42c26 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 5 Dec 2013 09:01:00 +0000 Subject: Upgrade JSONP security warning. --- docs/api-guide/renderers.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index f30fa26a..cf200569 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -118,7 +118,13 @@ Renders the request data into `JSONP`. The `JSONP` media type provides a mechan The javascript callback function must be set by the client including a `callback` URL query parameter. For example `http://example.com/api/users?callback=jsonpCallback`. If the callback function is not explicitly set by the client it will default to `'callback'`. -**Note**: If you require cross-domain AJAX requests, you may want to consider using the more modern approach of [CORS][cors] as an alternative to `JSONP`. See the [CORS documentation][cors-docs] for more details. +--- + +**Warning**: If you require cross-domain AJAX requests, you should almost certainly be using the more modern approach of [CORS][cors] as an alternative to `JSONP`. See the [CORS documentation][cors-docs] for more details. + +The `jsonp` approach is essentially a browser hack, and is [only appropriate for globally readable API endpoints][jsonp-security], where `GET` requests are unauthenticated and do not require any user permissions. + +--- **.media_type**: `application/javascript` @@ -419,6 +425,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily [rfc4627]: http://www.ietf.org/rfc/rfc4627.txt [cors]: http://www.w3.org/TR/cors/ [cors-docs]: ../topics/ajax-csrf-cors.md +[jsonp-security]: http://stackoverflow.com/questions/613962/is-jsonp-safe-to-use [testing]: testing.md [HATEOAS]: http://timelessrepo.com/haters-gonna-hateoas [quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven -- cgit v1.2.3 From 1f8069c0a9740297e7b5d5fa0c81830c876d7240 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 5 Dec 2013 11:05:25 +0000 Subject: Boilerplate cuteness --- rest_framework/__init__.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index de82fef5..b6a4d3a0 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -1,6 +1,20 @@ +""" +______ _____ _____ _____ __ _ +| ___ \ ___/ ___|_ _| / _| | | +| |_/ / |__ \ `--. | | | |_ _ __ __ _ _ __ ___ _____ _____ _ __| | __ +| /| __| `--. \ | | | _| '__/ _` | '_ ` _ \ / _ \ \ /\ / / _ \| '__| |/ / +| |\ \| |___/\__/ / | | | | | | | (_| | | | | | | __/\ V V / (_) | | | < +\_| \_\____/\____/ \_/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_| +""" + +__title__ = 'Django REST framework' __version__ = '2.3.9' +__author__ = 'Tom Christie' +__license__ = 'BSD 2-Clause' +__copyright__ = 'Copyright 2011-2013 Tom Christie' -VERSION = __version__ # synonym +# Version synonym +VERSION = __version__ # Header encoding (see RFC5987) HTTP_HEADER_ENCODING = 'iso-8859-1' -- cgit v1.2.3 From 79596dc613bbf24aac7b5c56179cbc5c46eacdf3 Mon Sep 17 00:00:00 2001 From: Justin Davis Date: Thu, 5 Dec 2013 13:17:23 -0800 Subject: fix setup.py with new __init__.py boilerplate --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 26d07283..1a487f17 100755 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ def get_version(package): Return package version as listed in `__version__` in `init.py`. """ init_py = open(os.path.join(package, '__init__.py')).read() - return re.match("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) + return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1) def get_packages(package): -- cgit v1.2.3 From cf6c11bd4b7e7fdaa1de659d69792030e565412a Mon Sep 17 00:00:00 2001 From: Chuck Harmston Date: Fri, 6 Dec 2013 14:00:23 -0600 Subject: Raise appropriate error in serializer when making a partial update to set a required RelatedField to null (issue #1158) --- rest_framework/serializers.py | 5 ++++- rest_framework/tests/test_serializer.py | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 163abf4f..44e4b04b 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -896,7 +896,10 @@ class ModelSerializer(Serializer): # Update an existing instance... if instance is not None: for key, val in attrs.items(): - setattr(instance, key, val) + try: + setattr(instance, key, val) + except ValueError: + self._errors[key] = self.error_messages['required'] # ...or create a new instance else: diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 1f85a474..eca467ee 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -558,6 +558,29 @@ class ModelValidationTests(TestCase): self.assertFalse(second_serializer.is_valid()) self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']}) + def test_foreign_key_is_null_with_partial(self): + """ + Test ModelSerializer validation with partial=True + + Specifically test that a null foreign key does not pass validation + """ + album = Album(title='test') + album.save() + + class PhotoSerializer(serializers.ModelSerializer): + class Meta: + model = Photo + + photo_serializer = PhotoSerializer(data={'description': 'test', 'album': album.pk}) + self.assertTrue(photo_serializer.is_valid()) + photo = photo_serializer.save() + + # Updating only the album (foreign key) + photo_serializer = PhotoSerializer(instance=photo, data={'album': ''}, partial=True) + self.assertFalse(photo_serializer.is_valid()) + self.assertTrue('album' in photo_serializer.errors) + self.assertEqual(photo_serializer.errors['album'], photo_serializer.error_messages['required']) + def test_foreign_key_with_partial(self): """ Test ModelSerializer validation with partial=True -- cgit v1.2.3 From 51359e461299905c6cd359000941f9da3d561f7d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Dec 2013 21:42:52 +0000 Subject: Added @chuckharmston for kickass bug squashing in #1272 --- docs/topics/credits.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index 3395cd9e..1a838421 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -180,6 +180,7 @@ The following people have helped make REST framework great. * Rob Hudson - [robhudson] * Alex Good - [alexjg] * Ian Foote - [ian-foote] +* Chuck Harmston - [chuckharmston] Many thanks to everyone who's contributed to the project. @@ -396,3 +397,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [robhudson]: https://github.com/robhudson [alexjg]: https://github.com/alexjg [ian-foote]: https://github.com/ian-foote +[chuckharmston]: https://github.com/chuckharmston -- cgit v1.2.3 From 85d9eb0f7ed3ef66a25a443b34ead914a506462c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Dec 2013 21:47:26 +0000 Subject: Update release-notes.md --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index e6085f59..2df2cf93 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -44,6 +44,7 @@ You can determine your currently installed version using `pip freeze`: * Add in choices information for ChoiceFields in response to `OPTIONS` requests. * Added `pre_delete()` and `post_delete()` method hooks. +* Bugfix: Partial updates which erronously set a related field to `None` now correctly fail validation instead of raising an exception. * Bugfix: Responses without any content no longer include an HTTP `'Content-Type'` header. * Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400. -- cgit v1.2.3 From 910de38a9c8cd03243e738c8f4adcbade8a4d7d6 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 6 Dec 2013 22:13:50 +0000 Subject: Version 2.3.10 --- docs/api-guide/status-codes.md | 21 +++++++++++++++++++++ docs/topics/release-notes.md | 5 ++++- rest_framework/__init__.py | 2 +- rest_framework/status.py | 17 +++++++++++++++++ rest_framework/tests/test_status.py | 33 +++++++++++++++++++++++++++++++++ 5 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 rest_framework/tests/test_status.py diff --git a/docs/api-guide/status-codes.md b/docs/api-guide/status-codes.md index 409f659b..64c46434 100644 --- a/docs/api-guide/status-codes.md +++ b/docs/api-guide/status-codes.md @@ -17,6 +17,18 @@ Using bare status codes in your responses isn't recommended. REST framework inc The full set of HTTP status codes included in the `status` module is listed below. +The module also includes a set of helper functions for testing if a status code is in a given range. + + from rest_framework import status + from rest_framework.test import APITestCase + + class ExampleTestCase(APITestCase): + def test_url_root(self): + url = reverse('index') + response = self.client.get(url) + self.assertTrue(status.is_success(response.status_code)) + + For more information on proper usage of HTTP status codes see [RFC 2616][rfc2616] and [RFC 6585][rfc6585]. @@ -90,6 +102,15 @@ Response status codes beginning with the digit "5" indicate cases in which the s HTTP_505_HTTP_VERSION_NOT_SUPPORTED HTTP_511_NETWORK_AUTHENTICATION_REQUIRED +## Helper functions + +The following helper functions are available for identifying the category of the response code. + + is_informational() # 1xx + is_success() # 2xx + is_redirect() # 3xx + is_client_error() # 4xx + is_server_error() # 5xx [rfc2324]: http://www.ietf.org/rfc/rfc2324.txt [rfc2616]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 2df2cf93..b080ad43 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,10 +40,13 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series -### Master +### 2.3.10 + +**Date**: 6th December 2013 * Add in choices information for ChoiceFields in response to `OPTIONS` requests. * Added `pre_delete()` and `post_delete()` method hooks. +* Added status code category helper functions. * Bugfix: Partial updates which erronously set a related field to `None` now correctly fail validation instead of raising an exception. * Bugfix: Responses without any content no longer include an HTTP `'Content-Type'` header. * Bugfix: Correctly handle validation errors in PUT-as-create case, responding with 400. diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index b6a4d3a0..f5483b9d 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ _ """ __title__ = 'Django REST framework' -__version__ = '2.3.9' +__version__ = '2.3.10' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2013 Tom Christie' diff --git a/rest_framework/status.py b/rest_framework/status.py index b9f249f9..76435371 100644 --- a/rest_framework/status.py +++ b/rest_framework/status.py @@ -6,6 +6,23 @@ And RFC 6585 - http://tools.ietf.org/html/rfc6585 """ from __future__ import unicode_literals + +def is_informational(code): + return code >= 100 and code <= 199 + +def is_success(code): + return code >= 200 and code <= 299 + +def is_redirect(code): + return code >= 300 and code <= 399 + +def is_client_error(code): + return code >= 400 and code <= 499 + +def is_server_error(code): + return code >= 500 and code <= 599 + + HTTP_100_CONTINUE = 100 HTTP_101_SWITCHING_PROTOCOLS = 101 HTTP_200_OK = 200 diff --git a/rest_framework/tests/test_status.py b/rest_framework/tests/test_status.py new file mode 100644 index 00000000..7b1bdae3 --- /dev/null +++ b/rest_framework/tests/test_status.py @@ -0,0 +1,33 @@ +from __future__ import unicode_literals +from django.test import TestCase +from rest_framework.status import ( + is_informational, is_success, is_redirect, is_client_error, is_server_error +) + + +class TestStatus(TestCase): + def test_status_categories(self): + self.assertFalse(is_informational(99)) + self.assertTrue(is_informational(100)) + self.assertTrue(is_informational(199)) + self.assertFalse(is_informational(200)) + + self.assertFalse(is_success(199)) + self.assertTrue(is_success(200)) + self.assertTrue(is_success(299)) + self.assertFalse(is_success(300)) + + self.assertFalse(is_redirect(299)) + self.assertTrue(is_redirect(300)) + self.assertTrue(is_redirect(399)) + self.assertFalse(is_redirect(400)) + + self.assertFalse(is_client_error(399)) + self.assertTrue(is_client_error(400)) + self.assertTrue(is_client_error(499)) + self.assertFalse(is_client_error(500)) + + self.assertFalse(is_server_error(499)) + self.assertTrue(is_server_error(500)) + self.assertTrue(is_server_error(599)) + self.assertFalse(is_server_error(600)) \ No newline at end of file -- cgit v1.2.3 From db19fba50d65c1093efa25bd5ed1230b6404c8ca Mon Sep 17 00:00:00 2001 From: Andy Wilson Date: Fri, 6 Dec 2013 22:31:07 -0600 Subject: update installation example to work with django 1.6 looks like django.conf.urls.defaults was deprecated as of django 1.6--- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 3e5adbc4..badd6f60 100644 --- a/docs/index.md +++ b/docs/index.md @@ -100,7 +100,7 @@ Don't forget to make sure you've also added `rest_framework` to your `INSTALLED_ We're ready to create our API now. Here's our project's root `urls.py` module: - from django.conf.urls.defaults import url, patterns, include + from django.conf.urls import url, patterns, include from django.contrib.auth.models import User, Group from rest_framework import viewsets, routers -- cgit v1.2.3 From 3399158d62416af56201eac63cc20d8934f08de2 Mon Sep 17 00:00:00 2001 From: taras Date: Sun, 8 Dec 2013 11:40:40 -0500 Subject: RelatedField is function of serializer class --- docs/api-guide/relations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index b9d96b5e..556429bb 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -44,7 +44,7 @@ In order to explain the various types of relational fields, we'll use a couple o For example, the following serializer. class AlbumSerializer(serializers.ModelSerializer): - tracks = RelatedField(many=True) + tracks = serializers.RelatedField(many=True) class Meta: model = Album -- cgit v1.2.3 From b8732d21652cf6b6e3c3e9807594b508be6583f8 Mon Sep 17 00:00:00 2001 From: Rustam Lalkaka Date: Sun, 8 Dec 2013 19:34:24 -0500 Subject: Minor grammar fix -- 'team' is singular --- docs/template.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/template.html b/docs/template.html index 5a0bdbfd..c065237a 100644 --- a/docs/template.html +++ b/docs/template.html @@ -172,7 +172,7 @@

    -

    The team behind REST framework are launching a new API service.

    +

    The team behind REST framework is launching a new API service.

    If you want to be first in line when we start issuing invitations, please sign up here:

    -- cgit v1.2.3 From 06d8a31e132c99a9645e26b5def3a1d9b9585c24 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 07:34:08 +0000 Subject: Catch and mask ParseErrors that occur during rendering of the BrowsableAPI. --- rest_framework/renderers.py | 9 +++++++-- rest_framework/request.py | 2 +- rest_framework/tests/test_renderers.py | 12 +++++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index fe4f43d4..2fdd3337 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -20,6 +20,7 @@ from rest_framework.compat import StringIO from rest_framework.compat import six from rest_framework.compat import smart_text from rest_framework.compat import yaml +from rest_framework.exceptions import ParseError from rest_framework.settings import api_settings from rest_framework.request import is_form_media_type, override_method from rest_framework.utils import encoders @@ -420,8 +421,12 @@ 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 + try: + data = request.DATA + files = request.FILES + except ParseError: + data = None + files = None else: data = None files = None diff --git a/rest_framework/request.py b/rest_framework/request.py index 9b551aa8..fcea2508 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -362,7 +362,7 @@ class Request(object): # If we get an exception during parsing, fill in empty data and # re-raise. Ensures we don't simply repeat the error when # attempting to render the browsable renderer response, or when - # logging the request or similar. + # logging the request or similar. self._data = QueryDict('', self._request._encoding) self._files = MultiValueDict() raise diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 549e763b..10aa4248 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -69,6 +69,12 @@ class MockGETView(APIView): return Response({'foo': ['bar', 'baz']}) +class MockPOSTView(APIView): + + def post(self, request, **kwargs): + return Response({'foo': request.DATA}) + + class HTMLView(APIView): renderer_classes = (BrowsableAPIRenderer, ) @@ -88,7 +94,7 @@ urlpatterns = patterns('', url(r'^cache$', MockGETView.as_view()), url(r'^jsonp/jsonrenderer$', MockGETView.as_view(renderer_classes=[JSONRenderer, JSONPRenderer])), url(r'^jsonp/nojsonrenderer$', MockGETView.as_view(renderer_classes=[JSONPRenderer])), - url(r'^parseerror$', MockGETView.as_view(renderer_classes=[JSONRenderer, BrowsableAPIRenderer])), + url(r'^parseerror$', MockPOSTView.as_view(renderer_classes=[JSONRenderer, BrowsableAPIRenderer])), url(r'^html$', HTMLView.as_view()), url(r'^html1$', HTMLView1.as_view()), url(r'^api', include('rest_framework.urls', namespace='rest_framework')) @@ -224,8 +230,8 @@ class RendererEndToEndTests(TestCase): """Invalid data should still render the browsable API correctly.""" resp = self.client.post('/parseerror', data='foobar', content_type='application/json', HTTP_ACCEPT='text/html') self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8') - self.assertContains(resp.content, 'Mock GET View') - self.assertEqual(resp.status_code, status.HTTP_400_) + self.assertIn('Mock Post', resp.content) + self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) _flat_repr = '{"foo": ["bar", "baz"]}' _indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' -- cgit v1.2.3 From 4e9385e709bcee87456a99839841ecf6b56f337a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 07:37:13 +0000 Subject: Drop unneeded assert --- rest_framework/tests/test_renderers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index 10aa4248..f4818eef 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -230,7 +230,6 @@ class RendererEndToEndTests(TestCase): """Invalid data should still render the browsable API correctly.""" resp = self.client.post('/parseerror', data='foobar', content_type='application/json', HTTP_ACCEPT='text/html') self.assertEqual(resp['Content-Type'], 'text/html; charset=utf-8') - self.assertIn('Mock Post', resp.content) self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) _flat_repr = '{"foo": ["bar", "baz"]}' -- cgit v1.2.3 From e80b353085c460c5ab7e9b4d22249f01176c5eb1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 08:10:51 +0000 Subject: Add notes to contributing docs --- docs/topics/contributing.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index 2b18c4f6..4dd2718f 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -33,6 +33,8 @@ Some tips on good issue reporting: * When describing issues try to phrase your ticket in terms of the *behavior* you think needs changing rather than the *code* you think need changing. * Search the issue list first for related items, and make sure you're running the latest version of REST framework before reporting an issue. * If reporting a bug, then try to include a pull request with a failing test case. This will help us quickly identify if there is a valid issue, and make sure that it gets fixed more quickly if there is one. +* Feature requests will often be closed with a recommendation that they be implemented outside of the core REST framework library. Keeping new feature requests implemented as third party libraries allows us to keep down the maintainence overhead of REST framework, so that the focus can be on continued stability, bugfixes, and great documentation. +* Closing an issue doesn't necessarily mean the end of a discussion. If you believe your issue has been closed incorrectly, explain why and we'll consider if it needs to be reopened. ## Triaging issues @@ -42,6 +44,7 @@ Getting involved in triaging incoming issues is a good way to start contributing * Is the ticket reported in the correct place, would it be better suited as a discussion on the discussion group? * If the ticket is a bug report, can you reproduce it? Are you able to write a failing test case that demonstrates the issue and that can be submitted as a pull request? * If the ticket is a feature request, do you agree with it, and could the feature request instead be implemented as a third party package? +* If a ticket hasn't had much activity and it addresses something you need, then comment on the ticket and try to find out what's needed to get it moving again. # Development -- cgit v1.2.3 From 23369650e3502305ebf5d682e141c7d47db89111 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 08:14:21 +0000 Subject: Add notes to contributing docs --- docs/topics/contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index 4dd2718f..b3ab52bb 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -10,7 +10,7 @@ There are many ways you can contribute to Django REST framework. We'd like it t The most important thing you can do to help push the REST framework project forward is to be actively involved wherever possible. Code contributions are often overvalued as being the primary way to get involved in a project, we don't believe that needs to be the case. -If you use REST framework, we'd love you to be vocal about your experiances with it - you might consider writing a blog post on your experience with using REST framework, or publishing a tutorial about using the project with a particular javascript framework. Experiances from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are and aren't easy to understand and work with. +If you use REST framework, we'd love you to be vocal about your experiences with it - you might consider writing a blog post about using REST framework, or publishing a tutorial about building a project with a particularJjavascript framework. Experiences from beginners can be particularly helpful because you'll be in the best position to assess which bits of REST framework are more difficult to understand and work with. Other really great ways you can help move the community forward include helping answer questions on the [discussion group][google-group], or setting up an [email alert on StackOverflow][so-filter] so that you get notified of any new questions with the `django-rest-framework` tag. -- cgit v1.2.3 From e6f6bb5c7e3e882b0215c981e2f2b6a576820100 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 08:42:09 +0000 Subject: Add notes to contributing docs --- docs/topics/contributing.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index b3ab52bb..77c60fb4 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -164,6 +164,16 @@ If you want to draw attention to a note or warning, use a pair of enclosing line --- +## Third party packages + +New features to REST framework are generally recommended to be implemented as third party libraries that are developed outside of the core framework. Ideally third party libraries should be properly documented and packaged, and made available on PyPI. + +If you have some functionality that you would like to implement as a third party package it's worth contacting the [discussion group][google-group] as others may be willing to get involved. We strongly encourage third party package development and will always try to prioritize time spent helping their development, documentation and packaging. + +Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main documentation. + +We recommend the [`django-reusable-app`][django-reusable-app] template as a good resource for getting up and running with implementing a third party Django package. + [cite]: http://www.w3.org/People/Berners-Lee/FAQ.html [code-of-conduct]: https://www.djangoproject.com/conduct/ [google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework @@ -176,3 +186,4 @@ If you want to draw attention to a note or warning, use a pair of enclosing line [markdown]: http://daringfireball.net/projects/markdown/basics [docs]: https://github.com/tomchristie/django-rest-framework/tree/master/docs [mou]: http://mouapp.com/ +[django-reusable-app]: https://github.com/dabapps/django-reusable-app -- cgit v1.2.3 From c1be503308e755d72aae2c9695739bd33631e18b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 08:46:18 +0000 Subject: Add notes to contributing docs --- docs/topics/contributing.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index 77c60fb4..906950bb 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -164,16 +164,20 @@ If you want to draw attention to a note or warning, use a pair of enclosing line --- -## Third party packages +# Third party packages New features to REST framework are generally recommended to be implemented as third party libraries that are developed outside of the core framework. Ideally third party libraries should be properly documented and packaged, and made available on PyPI. -If you have some functionality that you would like to implement as a third party package it's worth contacting the [discussion group][google-group] as others may be willing to get involved. We strongly encourage third party package development and will always try to prioritize time spent helping their development, documentation and packaging. +## Getting started -Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main documentation. +If you have some functionality that you would like to implement as a third party package it's worth contacting the [discussion group][google-group] as others may be willing to get involved. We strongly encourage third party package development and will always try to prioritize time spent helping their development, documentation and packaging. We recommend the [`django-reusable-app`][django-reusable-app] template as a good resource for getting up and running with implementing a third party Django package. +## Linking to your package + +Once your package is decently documented and available on PyPI open a pull request or issue, and we'll add a link to it from the main REST framework documentation. + [cite]: http://www.w3.org/People/Berners-Lee/FAQ.html [code-of-conduct]: https://www.djangoproject.com/conduct/ [google-group]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework -- cgit v1.2.3 From ddd17c69e7abdd70448fa0f2f2a807d600b3391d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 09:24:10 +0000 Subject: Fix compat issues for #1231 --- rest_framework/compat.py | 7 +++++++ rest_framework/tests/test_renderers.py | 31 +++++++------------------------ rest_framework/utils/encoders.py | 3 +-- 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 581e29fc..05bd99e0 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -69,6 +69,13 @@ try: except ImportError: import urlparse +# UserDict moves in Python 3 +try: + from UserDict import UserDict + from UserDict import DictMixin +except ImportError: + from collections import UserDict + from collections import MutableMapping as DictMixin # Try to import PIL in either of the two ways it can end up installed. try: diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index d720bc51..2ae8ae18 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -15,12 +15,11 @@ from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ from rest_framework.parsers import YAMLParser, XMLParser from rest_framework.settings import api_settings from rest_framework.test import APIRequestFactory +from collections import MutableMapping import datetime +import json import pickle import re -import UserDict -import collections -import json DUMMYSTATUS = status.HTTP_200_OK @@ -277,26 +276,8 @@ class JSONRendererTests(TestCase): ret = JSONRenderer().render(_('test')) self.assertEqual(ret, b'"test"') - def test_render_userdict_obj(self): - class DictLike(UserDict.DictMixin): - def __init__(self): - self._dict = dict() - def __getitem__(self, key): - return self._dict.__getitem__(key) - def __setitem__(self, key, value): - return self._dict.__setitem__(key, value) - def __delitem__(self, key): - return self._dict.__delitem__(key) - def keys(self): - return self._dict.keys() - x = DictLike() - x['a'] = 1 - x['b'] = "string value" - ret = JSONRenderer().render(x) - self.assertEquals(json.loads(ret), {'a': 1, 'b': 'string value'}) - def test_render_dict_abc_obj(self): - class Dict(collections.MutableMapping): + class Dict(MutableMapping): def __init__(self): self._dict = dict() def __getitem__(self, key): @@ -309,13 +290,15 @@ class JSONRendererTests(TestCase): return self._dict.__iter__() def __len__(self): return self._dict.__len__() + def keys(self): + return self._dict.keys() x = Dict() x['key'] = 'string value' x[2] = 3 ret = JSONRenderer().render(x) - self.assertEquals(json.loads(ret), {'key': 'string value', '2': 3}) - + data = json.loads(ret.decode('utf-8')) + self.assertEquals(data, {'key': 'string value', '2': 3}) def test_render_obj_with_getitem(self): class DictLike(object): diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 22b1ab3d..3ac920c6 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -47,8 +47,7 @@ class JSONEncoder(json.JSONEncoder): elif hasattr(o, '__getitem__'): try: return dict(o) - except KeyError: - # Couldn't convert to a dict, fall through + except: pass elif hasattr(o, '__iter__'): return [i for i in o] -- cgit v1.2.3 From 2c898bd9018e40a8d0e4718fb2a0e3672e64782c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 9 Dec 2013 09:27:10 +0000 Subject: Update release notes --- docs/topics/release-notes.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index b080ad43..d3f85e6e 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,10 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series +### Master + +* JSON renderer now deals with objects that implement a dict-like interface. + ### 2.3.10 **Date**: 6th December 2013 -- cgit v1.2.3 From de319f3e28d27d71fffce7c8f12c23363d5c25eb Mon Sep 17 00:00:00 2001 From: Ian Date: Mon, 9 Dec 2013 09:53:16 +0000 Subject: Fix typo "Not" -> "Note" --- docs/api-guide/serializers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 4c3fb9d3..6fc25f57 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -425,7 +425,7 @@ You can change the field that is used for object lookups by setting the `lookup_ fields = ('url', 'account_name', 'users', 'created') lookup_field = 'slug' -Not that the `lookup_field` will be used as the default on *all* hyperlinked fields, including both the URL identity, and any hyperlinked relationships. +Note that the `lookup_field` will be used as the default on *all* hyperlinked fields, including both the URL identity, and any hyperlinked relationships. For more specific requirements such as specifying a different lookup for each field, you'll want to set the fields on the serializer explicitly. For example: -- cgit v1.2.3 From 9ba7be959c2a2fea989527c590ce833df5925e63 Mon Sep 17 00:00:00 2001 From: Maxim Kamenkov Date: Mon, 9 Dec 2013 20:33:06 +0200 Subject: Added REST Condition to 3rd party permissions packages list. --- docs/api-guide/permissions.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 871de84e..60624b63 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -230,6 +230,10 @@ The [DRF Any Permissions][drf-any-permissions] packages provides a different per The [Composed Permissions][composed-permissions] package provides a simple way to define complex and multi-depth (with logic operators) permission objects, using small and reusable components. +## REST Condition + +The [REST Condition][rest-condition] yet another but simple and convenient extension for complex permissions tree. The extension allows to combine permissions with logical operators rules. Logical expressions can be used along with the usual permissions classes in api views. + [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html [authentication]: authentication.md [throttling]: throttling.md @@ -243,3 +247,4 @@ The [Composed Permissions][composed-permissions] package provides a simple way t [filtering]: filtering.md [drf-any-permissions]: https://github.com/kevin-brown/drf-any-permissions [composed-permissions]: https://github.com/niwibe/djangorestframework-composed-permissions +[rest-condition]: https://github.com/caxap/rest_condition -- cgit v1.2.3 From 785a42cd5aee9e96f9b780ff144fa13c16189748 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 10 Dec 2013 08:38:43 +0000 Subject: Tweak REST condition text. --- docs/api-guide/permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 60624b63..6a0f48f4 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -232,7 +232,7 @@ The [Composed Permissions][composed-permissions] package provides a simple way t ## REST Condition -The [REST Condition][rest-condition] yet another but simple and convenient extension for complex permissions tree. The extension allows to combine permissions with logical operators rules. Logical expressions can be used along with the usual permissions classes in api views. +The [REST Condition][rest-condition] package is another extension for building complex permissions in a simple and convenient way. The extension allows you to combine permissions with logical operators. [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html [authentication]: authentication.md -- cgit v1.2.3 From 3a1c40f81488c241cb64860d6cc510f8e71c0c40 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 10 Dec 2013 08:46:44 +0000 Subject: Refine model manager behavior so as not to use the behavior in incorrect cases. Closes #1205 --- rest_framework/serializers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 44e4b04b..0d35fb32 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -412,7 +412,13 @@ class BaseSerializer(WritableField): # Set the serializer object if it exists 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 we have a model manager or similar object then we need + # to iterate through each instance. + if (self.many and + not hasattr(obj, '__iter__') and + is_simple_callable(getattr(obj, 'all', None))): + obj = obj.all() if self.source == '*': if value: -- cgit v1.2.3 From 40164fcc6241489033d7015d429cbe0afc674d37 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 10 Dec 2013 08:49:54 +0000 Subject: Update release notes --- docs/topics/release-notes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index d3f85e6e..e186893e 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -43,6 +43,7 @@ You can determine your currently installed version using `pip freeze`: ### Master * JSON renderer now deals with objects that implement a dict-like interface. +* Bugfix: Refine behavior that call's model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations. ### 2.3.10 -- cgit v1.2.3 From c09ad1bedc8559b2e6eadf0e5b8f3732af2d9d29 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 10 Dec 2013 08:53:38 +0000 Subject: Remove incorrect apostrophe --- 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 e186893e..1ddd8351 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -43,7 +43,7 @@ You can determine your currently installed version using `pip freeze`: ### Master * JSON renderer now deals with objects that implement a dict-like interface. -* Bugfix: Refine behavior that call's model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations. +* Bugfix: Refine behavior that calls model manager `all()` across nested serializer relationships, preventing erronous behavior with some non-ORM objects, and preventing unneccessary queryset re-evaluations. ### 2.3.10 -- cgit v1.2.3 From 7382f8c6adc17c9feb02d028f7791af632d6dd3b Mon Sep 17 00:00:00 2001 From: David Ray Date: Tue, 10 Dec 2013 14:56:07 -0500 Subject: Update routers.md Reference to ```DefaultRouter``` should be ```SimpleRouter```--- docs/api-guide/routers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index fb48197e..8151e60f 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -12,7 +12,7 @@ REST framework adds support for automatic URL routing to Django, and provides yo ## Usage -Here's an example of a simple URL conf, that uses `DefaultRouter`. +Here's an example of a simple URL conf, that uses `SimpleRouter`. from rest_framework import routers -- cgit v1.2.3 From 5acefd3b17e498af756fa48e27d7f8ce19322c7a Mon Sep 17 00:00:00 2001 From: OddBloke Date: Wed, 11 Dec 2013 13:55:54 +0000 Subject: Add full required imports to Generating Tokens example Previously we were missing User and post_save.--- docs/api-guide/authentication.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 1a1c68b8..ef77e02c 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -162,6 +162,8 @@ The `curl` command line tool may be useful for testing token authenticated APIs. If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal. + from django.contrib.auth.models import User + from django.db.models.signals import post_save from django.dispatch import receiver from rest_framework.authtoken.models import Token -- cgit v1.2.3 From 4f473f0b9e918f2e071da0c84bd9b584c00ac919 Mon Sep 17 00:00:00 2001 From: OddBloke Date: Wed, 11 Dec 2013 13:56:56 +0000 Subject: Use get_user_model instead of User in Generating Tokens example Because that's a better way of doing it.--- docs/api-guide/authentication.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index ef77e02c..53efc49a 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -162,12 +162,12 @@ The `curl` command line tool may be useful for testing token authenticated APIs. If you want every user to have an automatically generated Token, you can simply catch the User's `post_save` signal. - from django.contrib.auth.models import User + from django.contrib.auth import get_user_model from django.db.models.signals import post_save from django.dispatch import receiver from rest_framework.authtoken.models import Token - @receiver(post_save, sender=User) + @receiver(post_save, sender=get_user_model()) def create_auth_token(sender, instance=None, created=False, **kwargs): if created: Token.objects.create(user=instance) -- cgit v1.2.3 From df2d9034c2a5a07dc3aa5455db892ee94cbed467 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 12 Dec 2013 23:10:31 +0000 Subject: Add third party packages --- docs/api-guide/filtering.md | 9 +++++++++ docs/api-guide/routers.md | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index a0132ffc..0e02a2a7 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -360,6 +360,14 @@ For example, you might need to restrict users to only being able to see objects We could achieve the same behavior by overriding `get_queryset()` on the views, but using a filter backend allows you to more easily add this restriction to multiple views, or to apply it across the entire API. +# Third party packages + +The following third party packages provide additional filter implementations. + +## Django REST framework chain + +The [django-rest-framework-chain package][django-rest-framework-chain] works together with the `DjangoFilterBackend` class, and allows you to easily create filters across relationships, or create multiple filter lookup types for a given field. + [cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters [django-filter]: https://github.com/alex/django-filter [django-filter-docs]: https://django-filter.readthedocs.org/en/latest/index.html @@ -368,3 +376,4 @@ We could achieve the same behavior by overriding `get_queryset()` on the views, [view-permissions-blogpost]: http://blog.nyaruka.com/adding-a-view-permission-to-django-models [nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py [search-django-admin]: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields +[django-rest-framework-chain]: https://github.com/philipn/django-rest-framework-chain diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 8151e60f..9001b859 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -150,4 +150,13 @@ If you want to provide totally custom behavior, you can override `BaseRouter` an You may also want to override the `get_default_base_name(self, viewset)` method, or else always explicitly set the `base_name` argument when registering your viewsets with the router. +# Third Party Packages + +The following third party packages provide router implementations that extend the default functionality provided by REST framework. + +## DRF Nested Routers + +The [drf-nested-routers package][drf-nested-routers] provides routers and relationship fields for working with nested resources. + [cite]: http://guides.rubyonrails.org/routing.html +[drf-nested-routers]: https://github.com/alanjds/drf-nested-routers -- cgit v1.2.3 From 73e8536e0d38f6677ac30aa2b3ba80563961191f Mon Sep 17 00:00:00 2001 From: S. Andrew Sheppard Date: Thu, 12 Dec 2013 21:45:44 -0600 Subject: third-party package: wq.db --- docs/api-guide/routers.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 8151e60f..ed903114 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -150,4 +150,18 @@ If you want to provide totally custom behavior, you can override `BaseRouter` an You may also want to override the `get_default_base_name(self, viewset)` method, or else always explicitly set the `base_name` argument when registering your viewsets with the router. +# Third party packages + +The following third party packages are also available. + +## wq.db + +[wq.db] provides an advanced [Router][wq.db-router] class (and singleton instance) that extends `DefaultRouter` with a `register_model()` API. Much like Django's `admin.site.register`, the only required argument to `app.router.register_model` is a model class. Reasonable defaults for a url prefix and viewset will be inferred from the model and global configuration. + + from wq.db.rest import app + from .models import MyModel + app.router.register_model(MyModel) + [cite]: http://guides.rubyonrails.org/routing.html +[wq.db]: http://wq.io/wq.db +[wq.db-router]: http://wq.io/docs/app.py -- cgit v1.2.3 From 0453cbd56bf5c553412b61a2a5e5522a2d44a419 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 11:09:54 +0000 Subject: Clean up implementation --- rest_framework/serializers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 34f31531..40caa1f3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -949,7 +949,11 @@ class ModelSerializer(Serializer): del(obj._m2m_data) if getattr(obj, '_related_data', None): - related_fields = dict(((f.get_accessor_name(), f) for f, m in obj._meta.get_all_related_objects_with_model())) + related_fields = dict([ + (field.get_accessor_name(), field) + for field, model + in obj._meta.get_all_related_objects_with_model() + ]) for accessor_name, related in obj._related_data.items(): if isinstance(related, RelationsList): # Nested reverse fk relationship -- cgit v1.2.3 From ca244ad614e2f6fb4fef1dc9987be996d2624303 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 13 Dec 2013 15:30:59 +0000 Subject: Expanded notes in quickstart. Closes #1127. Closes #1128. --- docs/index.md | 2 +- docs/tutorial/quickstart.md | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index badd6f60..04804fa7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -112,7 +112,7 @@ Here's our project's root `urls.py` module: model = Group - # Routers provide an easy way of automatically determining the URL conf + # Routers provide an easy way of automatically determining the URL conf. router = routers.DefaultRouter() router.register(r'users', UserViewSet) router.register(r'groups', GroupViewSet) diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index 80bb9abb..8bf8c7f5 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -89,6 +89,10 @@ Rather than write multiple views we're grouping together all the common behavior We can easily break these down into individual views if we need to, but using viewsets keeps the view logic nicely organized as well as being very concise. +Notice that our viewset classes here are a little different from those in the [frontpage example][readme-example-api], as they include `queryset` and `serializer_class` attributes, instead of a `model` attribute. + +For trivial cases you can simply set a `model` attribute on the `ViewSet` class and the serializer and queryset will be automatically generated for you. Setting the `queryset` and/or `serializer_class` attributes gives you more explicit control of the API behaviour, and is the recommended style for most applications. + ## URLs Okay, now let's wire up the API URLs. On to `tutorial/urls.py`... @@ -169,6 +173,7 @@ Great, that was easy! If you want to get a more in depth understanding of how REST framework fits together head on over to [the tutorial][tutorial], or start browsing the [API guide][guide]. +[readme-example-api]: ../#example [image]: ../img/quickstart.png [tutorial]: 1-serialization.md [guide]: ../#api-guide -- cgit v1.2.3