From 1e9ece0f9353515265da9b6266dc4b39775a0257 Mon Sep 17 00:00:00 2001 From: Ben Konrath Date: Mon, 8 Oct 2012 22:00:55 +0200 Subject: First attempt at adding filter support. The filter support uses django-filter to work its magic. --- rest_framework/generics.py | 35 +++++++- rest_framework/mixins.py | 2 +- rest_framework/tests/filterset.py | 160 +++++++++++++++++++++++++++++++++++++ rest_framework/tests/models.py | 7 ++ rest_framework/tests/pagination.py | 69 +++++++++++++++- 5 files changed, 268 insertions(+), 5 deletions(-) create mode 100644 rest_framework/tests/filterset.py (limited to 'rest_framework') diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 59739d01..3b2bea3b 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -1,12 +1,12 @@ """ -Generic views that provide commmonly needed behaviour. +Generic views that provide commonly needed behaviour. """ from rest_framework import views, mixins from rest_framework.settings import api_settings from django.views.generic.detail import SingleObjectMixin from django.views.generic.list import MultipleObjectMixin - +import django_filters ### Base classes for the generic views ### @@ -58,6 +58,37 @@ class MultipleObjectBaseView(MultipleObjectMixin, BaseView): pagination_serializer_class = api_settings.PAGINATION_SERIALIZER paginate_by = api_settings.PAGINATE_BY + filter_class = None + filter_fields = None + + def get_filter_class(self): + """ + Return the django-filters `FilterSet` used to filter the queryset. + """ + if self.filter_class: + return self.filter_class + + if self.filter_fields: + class AutoFilterSet(django_filters.FilterSet): + class Meta: + model = self.model + fields = self.filter_fields + return AutoFilterSet + + return None + + def filter_queryset(self, queryset): + filter_class = self.get_filter_class() + + if filter_class: + assert issubclass(filter_class.Meta.model, self.model), \ + "%s is not a subclass of %s" % (filter_class.Meta.model, self.model) + return filter_class(self.request.GET, queryset=queryset) + + return queryset + + def get_filtered_queryset(self): + return self.filter_queryset(self.get_queryset()) def get_pagination_serializer_class(self): """ diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 29153e18..04626fb0 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -33,7 +33,7 @@ class ListModelMixin(object): empty_error = u"Empty list and '%(class_name)s.allow_empty' is False." def list(self, request, *args, **kwargs): - self.object_list = self.get_queryset() + self.object_list = self.get_filtered_queryset() # Default is to allow empty querysets. This can be altered by setting # `.allow_empty = False`, to raise 404 errors on empty querysets. diff --git a/rest_framework/tests/filterset.py b/rest_framework/tests/filterset.py new file mode 100644 index 00000000..8c857f3f --- /dev/null +++ b/rest_framework/tests/filterset.py @@ -0,0 +1,160 @@ +import datetime +from django.test import TestCase +from django.test.client import RequestFactory +from rest_framework import generics, status +from rest_framework.tests.models import FilterableItem, BasicModel +import django_filters + +factory = RequestFactory() + +# Basic filter on a list view. +class FilterFieldsRootView(generics.ListCreateAPIView): + model = FilterableItem + filter_fields = ['decimal', 'date'] + + +# These class are used to test a filter class. +class SeveralFieldsFilter(django_filters.FilterSet): + text = django_filters.CharFilter(lookup_type='icontains') + decimal = django_filters.NumberFilter(lookup_type='lt') + date = django_filters.DateFilter(lookup_type='gt') + class Meta: + model = FilterableItem + fields = ['text', 'decimal', 'date'] + + +class FilterClassRootView(generics.ListCreateAPIView): + model = FilterableItem + filter_class = SeveralFieldsFilter + + +# These classes are used to test a misconfigured filter class. +class MisconfiguredFilter(django_filters.FilterSet): + text = django_filters.CharFilter(lookup_type='icontains') + class Meta: + model = BasicModel + fields = ['text'] + + +class IncorrectlyConfiguredRootView(generics.ListCreateAPIView): + model = FilterableItem + filter_class = MisconfiguredFilter + + +class IntegrationTestFiltering(TestCase): + """ + Integration tests for filtered list views. + """ + + def setUp(self): + """ + Create 10 FilterableItem instances. + """ + base_data = ('a', 0.25, datetime.date(2012, 10, 8)) + for i in range(10): + text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc. + decimal = base_data[1] + i + date = base_data[2] - datetime.timedelta(days=i * 2) + FilterableItem(text=text, decimal=decimal, date=date).save() + + self.objects = FilterableItem.objects + self.data = [ + {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date} + for obj in self.objects.all() + ] + + def test_get_filtered_fields_root_view(self): + """ + GET requests to paginated ListCreateAPIView should return paginated results. + """ + view = FilterFieldsRootView.as_view() + + # Basic test with no filter. + request = factory.get('/') + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data, self.data) + + # Tests that the decimal filter works. + search_decimal = 2.25 + request = factory.get('/?decimal=%s' % search_decimal) + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + expected_data = [ f for f in self.data if f['decimal'] == search_decimal ] + self.assertEquals(response.data, expected_data) + + # Tests that the date filter works. + search_date = datetime.date(2012, 9, 22) + request = factory.get('/?date=%s' % search_date) # search_date str: '2012-09-22' + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + expected_data = [ f for f in self.data if f['date'] == search_date ] + self.assertEquals(response.data, expected_data) + + def test_get_filtered_class_root_view(self): + """ + GET requests to filtered ListCreateAPIView that have a filter_class set + should return filtered results. + """ + view = FilterClassRootView.as_view() + + # Basic test with no filter. + request = factory.get('/') + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data, self.data) + + # Tests that the decimal filter set with 'lt' in the filter class works. + search_decimal = 4.25 + request = factory.get('/?decimal=%s' % search_decimal) + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + expected_data = [ f for f in self.data if f['decimal'] < search_decimal ] + self.assertEquals(response.data, expected_data) + + # Tests that the date filter set with 'gt' in the filter class works. + search_date = datetime.date(2012, 10, 2) + request = factory.get('/?date=%s' % search_date) # search_date str: '2012-10-02' + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + expected_data = [ f for f in self.data if f['date'] > search_date ] + self.assertEquals(response.data, expected_data) + + # Tests that the text filter set with 'icontains' in the filter class works. + search_text = 'ff' + request = factory.get('/?text=%s' % search_text) + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + expected_data = [ f for f in self.data if search_text in f['text'].lower() ] + self.assertEquals(response.data, expected_data) + + # Tests that multiple filters works. + search_decimal = 5.25 + search_date = datetime.date(2012, 10, 2) + request = factory.get('/?decimal=%s&date=%s' % (search_decimal, search_date)) + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + expected_data = [ f for f in self.data if f['date'] > search_date and + f['decimal'] < search_decimal ] + self.assertEquals(response.data, expected_data) + + def test_incorrectly_configured_filter(self): + """ + An error should be displayed when the filter class is misconfigured. + """ + view = IncorrectlyConfiguredRootView.as_view() + + request = factory.get('/') + self.assertRaises(AssertionError, view, request) + + # TODO Return 400 filter paramater requested that hasn't been configured. + def test_bad_request(self): + """ + GET requests with filters that aren't configured should return 400. + """ + view = FilterFieldsRootView.as_view() + + search_integer = 10 + request = factory.get('/?integer=%s' % search_integer) + response = view(request).render() + self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) \ No newline at end of file diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 6a758f0c..780c9dba 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -85,6 +85,13 @@ class Bookmark(RESTFrameworkModel): tags = GenericRelation(TaggedItem) +# Model to test filtering. +class FilterableItem(RESTFrameworkModel): + text = models.CharField(max_length=100) + decimal = models.DecimalField(max_digits=4, decimal_places=2) + date = models.DateField() + + # Model for regression test for #285 class Comment(RESTFrameworkModel): diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index a939c9ef..729bbfc2 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -1,8 +1,10 @@ +import datetime from django.core.paginator import Paginator from django.test import TestCase from django.test.client import RequestFactory from rest_framework import generics, status, pagination -from rest_framework.tests.models import BasicModel +from rest_framework.tests.models import BasicModel, FilterableItem +import django_filters factory = RequestFactory() @@ -15,6 +17,19 @@ class RootView(generics.ListCreateAPIView): paginate_by = 10 +class DecimalFilter(django_filters.FilterSet): + decimal = django_filters.NumberFilter(lookup_type='lt') + class Meta: + model = FilterableItem + fields = ['text', 'decimal', 'date'] + + +class FilterFieldsRootView(generics.ListCreateAPIView): + model = FilterableItem + paginate_by = 10 + filter_class = DecimalFilter + + class IntegrationTestPagination(TestCase): """ Integration tests for paginated list views. @@ -22,7 +37,7 @@ class IntegrationTestPagination(TestCase): def setUp(self): """ - Create 26 BasicModel intances. + Create 26 BasicModel instances. """ for char in 'abcdefghijklmnopqrstuvwxyz': BasicModel(text=char * 3).save() @@ -61,6 +76,56 @@ class IntegrationTestPagination(TestCase): self.assertEquals(response.data['next'], None) self.assertNotEquals(response.data['previous'], None) +class IntegrationTestPaginationAndFiltering(TestCase): + + def setUp(self): + """ + Create 50 FilterableItem instances. + """ + base_data = ('a', 0.25, datetime.date(2012, 10, 8)) + for i in range(26): + text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc. + decimal = base_data[1] + i + date = base_data[2] - datetime.timedelta(days=i * 2) + FilterableItem(text=text, decimal=decimal, date=date).save() + + self.objects = FilterableItem.objects + self.data = [ + {'id': obj.id, 'text': obj.text, 'decimal': obj.decimal, 'date': obj.date} + for obj in self.objects.all() + ] + self.view = FilterFieldsRootView.as_view() + + def test_get_paginated_filtered_root_view(self): + """ + GET requests to paginated filtered ListCreateAPIView should return + paginated results. The next and previous links should preserve the + filtered parameters. + """ + request = factory.get('/?decimal=15.20') + response = self.view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data['count'], 15) + self.assertEquals(response.data['results'], self.data[:10]) + self.assertNotEquals(response.data['next'], None) + self.assertEquals(response.data['previous'], None) + + request = factory.get(response.data['next']) + response = self.view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data['count'], 15) + self.assertEquals(response.data['results'], self.data[10:15]) + self.assertEquals(response.data['next'], None) + self.assertNotEquals(response.data['previous'], None) + + request = factory.get(response.data['previous']) + response = self.view(request).render() + self.assertEquals(response.status_code, status.HTTP_200_OK) + self.assertEquals(response.data['count'], 15) + self.assertEquals(response.data['results'], self.data[:10]) + self.assertNotEquals(response.data['next'], None) + self.assertEquals(response.data['previous'], None) + class UnitTestPagination(TestCase): """ -- cgit v1.2.3 From 692203f933b77cc3b18e15434002169b642fbd84 Mon Sep 17 00:00:00 2001 From: Ben Konrath Date: Tue, 9 Oct 2012 08:22:00 +0200 Subject: Check for 200 status when unknown filter requested. This changes the test from the failing checking for status 400. See discussion here: https://github.com/tomchristie/django-rest-framework/pull/169#issuecomment-9240480 --- rest_framework/tests/filterset.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/filterset.py b/rest_framework/tests/filterset.py index 8c857f3f..b21abacb 100644 --- a/rest_framework/tests/filterset.py +++ b/rest_framework/tests/filterset.py @@ -147,14 +147,13 @@ class IntegrationTestFiltering(TestCase): request = factory.get('/') self.assertRaises(AssertionError, view, request) - # TODO Return 400 filter paramater requested that hasn't been configured. - def test_bad_request(self): + def test_unknown_filter(self): """ - GET requests with filters that aren't configured should return 400. + GET requests with filters that aren't configured should return 200. """ view = FilterFieldsRootView.as_view() search_integer = 10 request = factory.get('/?integer=%s' % search_integer) response = view(request).render() - self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) \ No newline at end of file + self.assertEquals(response.status_code, status.HTTP_200_OK) \ No newline at end of file -- cgit v1.2.3 From e295f616ec2cfee9c24b22d4be1a605a93d9544d Mon Sep 17 00:00:00 2001 From: Ben Konrath Date: Thu, 11 Oct 2012 11:32:51 +0200 Subject: Fix small PEP8 problem. --- rest_framework/tests/pagination.py | 1 + 1 file changed, 1 insertion(+) (limited to 'rest_framework') diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 729bbfc2..054b7ee3 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -76,6 +76,7 @@ class IntegrationTestPagination(TestCase): self.assertEquals(response.data['next'], None) self.assertNotEquals(response.data['previous'], None) + class IntegrationTestPaginationAndFiltering(TestCase): def setUp(self): -- cgit v1.2.3 From 6fbd411254089c86baca65b08a89d239e5b804a9 Mon Sep 17 00:00:00 2001 From: Ben Konrath Date: Thu, 11 Oct 2012 11:35:00 +0200 Subject: Make query filters work with pagination. --- rest_framework/pagination.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 131718fd..616c7674 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -14,6 +14,9 @@ class NextPageField(serializers.Field): request = self.context.get('request') relative_url = '?page=%d' % page if request: + for field, value in request.QUERY_PARAMS.iteritems(): + if field != 'page': + relative_url += '&%s=%s' % (field, value) return request.build_absolute_uri(relative_url) return relative_url @@ -29,7 +32,10 @@ class PreviousPageField(serializers.Field): request = self.context.get('request') relative_url = '?page=%d' % page if request: - return request.build_absolute_uri('?page=%d' % page) + for field, value in request.QUERY_PARAMS.iteritems(): + if field != 'page': + relative_url += '&%s=%s' % (field, value) + return request.build_absolute_uri(relative_url) return relative_url -- cgit v1.2.3 From 5454162b0419ab6564f37ea56b1852d686b0a11e Mon Sep 17 00:00:00 2001 From: Ben Konrath Date: Thu, 11 Oct 2012 11:39:23 +0200 Subject: Define 'page' query field name in one place. --- rest_framework/pagination.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 616c7674..c77a1005 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -3,7 +3,11 @@ from rest_framework import serializers # TODO: Support URLconf kwarg-style paging -class NextPageField(serializers.Field): +class PageField(serializers.Field): + page_field = 'page' + + +class NextPageField(PageField): """ Field that returns a link to the next page in paginated results. """ @@ -12,16 +16,16 @@ class NextPageField(serializers.Field): return None page = value.next_page_number() request = self.context.get('request') - relative_url = '?page=%d' % page + relative_url = '?%s=%d' % (self.page_field, page) if request: for field, value in request.QUERY_PARAMS.iteritems(): - if field != 'page': + if field != self.page_field: relative_url += '&%s=%s' % (field, value) return request.build_absolute_uri(relative_url) return relative_url -class PreviousPageField(serializers.Field): +class PreviousPageField(PageField): """ Field that returns a link to the previous page in paginated results. """ @@ -30,10 +34,10 @@ class PreviousPageField(serializers.Field): return None page = value.previous_page_number() request = self.context.get('request') - relative_url = '?page=%d' % page + relative_url = '?%s=%d' % (self.page_field, page) if request: for field, value in request.QUERY_PARAMS.iteritems(): - if field != 'page': + if field != self.page_field: relative_url += '&%s=%s' % (field, value) return request.build_absolute_uri(relative_url) return relative_url -- cgit v1.2.3 From 6300334acaef8fe66e03557f089fb335ac861a57 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 11 Oct 2012 11:21:50 +0100 Subject: Sanitise JSON error messages --- rest_framework/tests/views.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/views.py b/rest_framework/tests/views.py index 3746d7c8..43365e07 100644 --- a/rest_framework/tests/views.py +++ b/rest_framework/tests/views.py @@ -1,3 +1,4 @@ +import copy from django.test import TestCase from django.test.client import RequestFactory from rest_framework import status @@ -27,6 +28,17 @@ def basic_view(request): return {'method': 'PUT', 'data': request.DATA} +def sanitise_json_error(error_dict): + """ + Exact contents of JSON error messages depend on the installed version + of json. + """ + ret = copy.copy(error_dict) + chop = len('JSON parse error - No JSON object could be decoded') + ret['detail'] = ret['detail'][:chop] + return ret + + class ClassBasedViewIntegrationTests(TestCase): def setUp(self): self.view = BasicView.as_view() @@ -38,7 +50,7 @@ class ClassBasedViewIntegrationTests(TestCase): 'detail': u'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEquals(response.data, expected) + self.assertEquals(sanitise_json_error(response.data), expected) def test_400_parse_error_tunneled_content(self): content = 'f00bar' @@ -53,7 +65,7 @@ class ClassBasedViewIntegrationTests(TestCase): 'detail': u'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEquals(response.data, expected) + self.assertEquals(sanitise_json_error(response.data), expected) class FunctionBasedViewIntegrationTests(TestCase): @@ -67,7 +79,7 @@ class FunctionBasedViewIntegrationTests(TestCase): 'detail': u'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEquals(response.data, expected) + self.assertEquals(sanitise_json_error(response.data), expected) def test_400_parse_error_tunneled_content(self): content = 'f00bar' @@ -82,4 +94,4 @@ class FunctionBasedViewIntegrationTests(TestCase): 'detail': u'JSON parse error - No JSON object could be decoded' } self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEquals(response.data, expected) + self.assertEquals(sanitise_json_error(response.data), expected) -- cgit v1.2.3 From 6f736a682369e003e4ae4b8d587f9168d4196986 Mon Sep 17 00:00:00 2001 From: Ben Konrath Date: Thu, 11 Oct 2012 13:55:16 +0200 Subject: Explicitly use Decimal for creating filter test data. This fixes a Travis build failures on python 2.6: https://travis-ci.org/#!/tomchristie/django-rest-framework/builds/2746628 --- rest_framework/tests/filterset.py | 3 ++- rest_framework/tests/pagination.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/filterset.py b/rest_framework/tests/filterset.py index b21abacb..5b2721ff 100644 --- a/rest_framework/tests/filterset.py +++ b/rest_framework/tests/filterset.py @@ -1,4 +1,5 @@ import datetime +from decimal import Decimal from django.test import TestCase from django.test.client import RequestFactory from rest_framework import generics, status @@ -50,7 +51,7 @@ class IntegrationTestFiltering(TestCase): """ Create 10 FilterableItem instances. """ - base_data = ('a', 0.25, datetime.date(2012, 10, 8)) + base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8)) for i in range(10): text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc. decimal = base_data[1] + i diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 054b7ee3..8c5e6ad7 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -1,4 +1,5 @@ import datetime +from decimal import Decimal from django.core.paginator import Paginator from django.test import TestCase from django.test.client import RequestFactory @@ -83,7 +84,7 @@ class IntegrationTestPaginationAndFiltering(TestCase): """ Create 50 FilterableItem instances. """ - base_data = ('a', 0.25, datetime.date(2012, 10, 8)) + base_data = ('a', Decimal(0.25), datetime.date(2012, 10, 8)) for i in range(26): text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc. decimal = base_data[1] + i -- cgit v1.2.3 From 1d054f95725e5bec7d4ba9d23717897ef80b7388 Mon Sep 17 00:00:00 2001 From: Ben Konrath Date: Thu, 11 Oct 2012 14:19:29 +0200 Subject: Use Decimal (properly) everywhere. --- rest_framework/tests/filterset.py | 6 +++--- rest_framework/tests/pagination.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/filterset.py b/rest_framework/tests/filterset.py index 5b2721ff..5374eefc 100644 --- a/rest_framework/tests/filterset.py +++ b/rest_framework/tests/filterset.py @@ -77,7 +77,7 @@ class IntegrationTestFiltering(TestCase): self.assertEquals(response.data, self.data) # Tests that the decimal filter works. - search_decimal = 2.25 + search_decimal = Decimal('2.25') request = factory.get('/?decimal=%s' % search_decimal) response = view(request).render() self.assertEquals(response.status_code, status.HTTP_200_OK) @@ -106,7 +106,7 @@ class IntegrationTestFiltering(TestCase): self.assertEquals(response.data, self.data) # Tests that the decimal filter set with 'lt' in the filter class works. - search_decimal = 4.25 + search_decimal = Decimal('4.25') request = factory.get('/?decimal=%s' % search_decimal) response = view(request).render() self.assertEquals(response.status_code, status.HTTP_200_OK) @@ -130,7 +130,7 @@ class IntegrationTestFiltering(TestCase): self.assertEquals(response.data, expected_data) # Tests that multiple filters works. - search_decimal = 5.25 + search_decimal = Decimal('5.25') search_date = datetime.date(2012, 10, 2) request = factory.get('/?decimal=%s&date=%s' % (search_decimal, search_date)) response = view(request).render() diff --git a/rest_framework/tests/pagination.py b/rest_framework/tests/pagination.py index 8c5e6ad7..170515a7 100644 --- a/rest_framework/tests/pagination.py +++ b/rest_framework/tests/pagination.py @@ -84,7 +84,7 @@ class IntegrationTestPaginationAndFiltering(TestCase): """ Create 50 FilterableItem instances. """ - base_data = ('a', Decimal(0.25), datetime.date(2012, 10, 8)) + base_data = ('a', Decimal('0.25'), datetime.date(2012, 10, 8)) for i in range(26): text = chr(i + ord(base_data[0])) * 3 # Produces string 'aaa', 'bbb', etc. decimal = base_data[1] + i -- cgit v1.2.3 From 195006bbc36c21f0154fe1ab7c46f339b2efe559 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 09:27:59 +0100 Subject: Drop resources from codebase since implementation is only partial (Created resoorces-routers branch for future reference) --- rest_framework/resources.py | 96 --------------------------------------------- 1 file changed, 96 deletions(-) delete mode 100644 rest_framework/resources.py (limited to 'rest_framework') diff --git a/rest_framework/resources.py b/rest_framework/resources.py deleted file mode 100644 index dd8a5471..00000000 --- a/rest_framework/resources.py +++ /dev/null @@ -1,96 +0,0 @@ -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - -from functools import update_wrapper -import inspect -from django.utils.decorators import classonlymethod -from rest_framework import views, generics - - -def wrapped(source, dest): - """ - Copy public, non-method attributes from source to dest, and return dest. - """ - for attr in [attr for attr in dir(source) - if not attr.startswith('_') and not inspect.ismethod(attr)]: - setattr(dest, attr, getattr(source, attr)) - return dest - - -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - -class ResourceMixin(object): - """ - Clone Django's `View.as_view()` behaviour *except* using REST framework's - 'method -> action' binding for resources. - """ - - @classonlymethod - def as_view(cls, actions, **initkwargs): - """ - Main entry point for a request-response process. - """ - # sanitize keyword arguments - for key in initkwargs: - if key in cls.http_method_names: - raise TypeError("You tried to pass in the %s method name as a " - "keyword argument to %s(). Don't do that." - % (key, cls.__name__)) - if not hasattr(cls, key): - raise TypeError("%s() received an invalid keyword %r" % ( - cls.__name__, key)) - - def view(request, *args, **kwargs): - self = cls(**initkwargs) - - # Bind methods to actions - for method, action in actions.items(): - handler = getattr(self, action) - setattr(self, method, handler) - - # As you were, solider. - if hasattr(self, 'get') and not hasattr(self, 'head'): - self.head = self.get - return self.dispatch(request, *args, **kwargs) - - # take name and docstring from class - update_wrapper(view, cls, updated=()) - - # and possible attributes set by decorators - # like csrf_exempt from dispatch - update_wrapper(view, cls.dispatch, assigned=()) - return view - - -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - -class Resource(ResourceMixin, views.APIView): - pass - - -##### RESOURCES AND ROUTERS ARE NOT YET IMPLEMENTED - PLACEHOLDER ONLY ##### - -class ModelResource(ResourceMixin, views.APIView): - # TODO: Actually delegation won't work - root_class = generics.ListCreateAPIView - detail_class = generics.RetrieveUpdateDestroyAPIView - - def root_view(self): - return wrapped(self, self.root_class()) - - def detail_view(self): - return wrapped(self, self.detail_class()) - - def list(self, request, *args, **kwargs): - return self.root_view().list(request, args, kwargs) - - def create(self, request, *args, **kwargs): - return self.root_view().create(request, args, kwargs) - - def retrieve(self, request, *args, **kwargs): - return self.detail_view().retrieve(request, args, kwargs) - - def update(self, request, *args, **kwargs): - return self.detail_view().update(request, args, kwargs) - - def destroy(self, request, *args, **kwargs): - return self.detail_view().destroy(request, args, kwargs) -- cgit v1.2.3 From 32d602880fc88e2b3e8d8f2a82132bed224f8b49 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 12:45:52 +0100 Subject: Choice fields from ModelSerializer. --- rest_framework/serializers.py | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8ee9a0ec..d4fcddd5 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -3,6 +3,7 @@ import datetime import types from decimal import Decimal from django.db import models +from django.forms import widgets from django.utils.datastructures import SortedDict from rest_framework.compat import get_concrete_model from rest_framework.fields import * @@ -409,6 +410,15 @@ class ModelSerializer(Serializer): kwargs = {} if model_field.has_default(): kwargs['required'] = False + kwargs['default'] = model_field.default + + if model_field.__class__ == models.TextField: + kwargs['widget'] = widgets.Textarea + + # TODO: TypedChoiceField? + if model_field.flatchoices: # This ModelField contains choices + kwargs['choices'] = model_field.flatchoices + return ChoiceField(**kwargs) field_mapping = { models.FloatField: FloatField, -- cgit v1.2.3 From 2efb5f8a14ffc321a1a9e88548abfa8b0782aae4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 12:46:15 +0100 Subject: Object-level permissions respected by Browseable API --- rest_framework/renderers.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index c64fb517..1a8b1d97 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -224,7 +224,7 @@ class BrowsableAPIRenderer(BaseRenderer): return content - def show_form_for_method(self, view, method, request): + def show_form_for_method(self, view, method, request, obj): """ Returns True if a form should be shown for this method. """ @@ -236,7 +236,7 @@ class BrowsableAPIRenderer(BaseRenderer): request = clone_request(request, method) try: - if not view.has_permission(request): + if not view.has_permission(request, obj): return # Don't have permission except: return # Don't have permission and exception explicitly raise @@ -295,7 +295,8 @@ class BrowsableAPIRenderer(BaseRenderer): In the absence on of the Resource having an associated form then provide a form that can be used to submit arbitrary content. """ - if not self.show_form_for_method(view, method, request): + obj = getattr(view, 'object', None) + if not self.show_form_for_method(view, method, request, obj): return if method == 'DELETE' or method == 'OPTIONS': @@ -305,17 +306,13 @@ class BrowsableAPIRenderer(BaseRenderer): media_types = [parser.media_type for parser in view.parser_classes] return self.get_generic_content_form(media_types) - # Creating an on the fly form see: http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python - obj, data = None, None - if getattr(view, 'object', None): - obj = view.object - serializer = view.get_serializer(instance=obj) fields = self.serializer_to_form_fields(serializer) + # Creating an on the fly form see: + # http://stackoverflow.com/questions/3915024/dynamically-creating-classes-python OnTheFlyForm = type("OnTheFlyForm", (forms.Form,), fields) - if obj: - data = serializer.data + data = (obj is not None) and serializer.data or None form_instance = OnTheFlyForm(data) return form_instance -- cgit v1.2.3 From fc4614a89c5c2bbdeb3626e2f16e3a2cd6445e3e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 12:46:41 +0100 Subject: Whitespace --- rest_framework/fields.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 6ed37823..85e6ee31 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -258,7 +258,7 @@ class ManyRelatedMixin(object): def field_from_native(self, data, field_name, into): if self.readonly: return - + try: # Form data value = data.getlist(self.source or field_name) -- cgit v1.2.3 From 67f1265e493adc35239d90aeb3bfeb8492fbd741 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 26 Oct 2012 13:20:30 +0100 Subject: Fix failing 'default' on ModelSerializer --- rest_framework/serializers.py | 2 +- rest_framework/tests/models.py | 4 ++-- rest_framework/tests/serializer.py | 20 +++++++++++--------- 3 files changed, 14 insertions(+), 12 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d4fcddd5..db6f9f61 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -410,7 +410,7 @@ class ModelSerializer(Serializer): kwargs = {} if model_field.has_default(): kwargs['required'] = False - kwargs['default'] = model_field.default + kwargs['default'] = model_field.get_default() if model_field.__class__ == models.TextField: kwargs['widget'] = widgets.Textarea diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 97cd0849..0ee18c69 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -62,12 +62,12 @@ class CallableDefaultValueModel(RESTFrameworkModel): class ManyToManyModel(RESTFrameworkModel): rel = models.ManyToManyField(Anchor) - + class ReadOnlyManyToManyModel(RESTFrameworkModel): text = models.CharField(max_length=100, default='anchor') rel = models.ManyToManyField(Anchor) - + # Models to test generic relations diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 936f15aa..67c97f0f 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -7,7 +7,7 @@ from rest_framework.tests.models import * class SubComment(object): def __init__(self, sub_comment): self.sub_comment = sub_comment - + class Comment(object): def __init__(self, email, content, created): @@ -18,7 +18,7 @@ class Comment(object): def __eq__(self, other): return all([getattr(self, attr) == getattr(other, attr) for attr in ('email', 'content', 'created')]) - + def get_sub_comment(self): sub_comment = SubComment('And Merry Christmas!') return sub_comment @@ -29,7 +29,7 @@ class CommentSerializer(serializers.Serializer): content = serializers.CharField(max_length=1000) created = serializers.DateTimeField() sub_comment = serializers.Field(source='get_sub_comment.sub_comment') - + def restore_object(self, data, instance=None): if instance is None: return Comment(**data) @@ -42,6 +42,7 @@ class ActionItemSerializer(serializers.ModelSerializer): class Meta: model = ActionItem + class BasicTests(TestCase): def setUp(self): self.comment = Comment( @@ -73,7 +74,7 @@ class BasicTests(TestCase): self.assertEquals(serializer.data, expected) def test_retrieve(self): - serializer = CommentSerializer(instance=self.comment) + serializer = CommentSerializer(instance=self.comment) self.assertEquals(serializer.data, self.expected) def test_create(self): @@ -104,7 +105,7 @@ class ValidationTests(TestCase): 'email': 'tom@example.com', 'content': 'x' * 1001, 'created': datetime.datetime(2012, 1, 1) - } + } self.actionitem = ActionItem('Some to do item', ) @@ -131,7 +132,7 @@ class ValidationTests(TestCase): """Make sure that a boolean value with a 'False' value is not mistaken for not having a default.""" data = { - 'title':'Some action item', + 'title': 'Some action item', #No 'done' value. } serializer = ActionItemSerializer(data, instance=self.actionitem) @@ -295,11 +296,13 @@ class ManyToManyTests(TestCase): self.assertEquals(len(ManyToManyModel.objects.all()), 2) self.assertEquals(instance.pk, 2) self.assertEquals(list(instance.rel.all()), []) - + + class ReadOnlyManyToManyTests(TestCase): def setUp(self): class ReadOnlyManyToManySerializer(serializers.ModelSerializer): - rel = serializers.ManyRelatedField(readonly=True) + rel = serializers.ManyRelatedField(readonly=True) + class Meta: model = ReadOnlyManyToManyModel @@ -317,7 +320,6 @@ class ReadOnlyManyToManyTests(TestCase): # A serialized representation of the model instance self.data = {'rel': [self.anchor.id], 'id': 1, 'text': 'anchor'} - def test_update(self): """ Attempt to update an instance of a model with a ManyToMany -- cgit v1.2.3 From 44207a347ac765d900f15b65bdd24dbf8eb9ead2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 10:32:49 +0100 Subject: pep8 --- rest_framework/compat.py | 4 +++- rest_framework/generics.py | 2 +- rest_framework/permissions.py | 2 +- rest_framework/request.py | 4 ++-- rest_framework/serializers.py | 8 ++++---- 5 files changed, 11 insertions(+), 9 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 7664c400..b0367a32 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -1,6 +1,8 @@ """ -The :mod:`compat` module provides support for backwards compatibility with older versions of django/python. +The `compat` module provides support for backwards compatibility with older +versions of django/python, and compatbility wrappers around optional packages. """ +# flake8: noqa import django # cStringIO only if it's available, otherwise StringIO diff --git a/rest_framework/generics.py b/rest_framework/generics.py index 81014026..190a5f79 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -125,7 +125,7 @@ class RetrieveAPIView(mixins.RetrieveModelMixin, class DestroyAPIView(mixins.DestroyModelMixin, - SingleObjectAPIView): + SingleObjectAPIView): """ Concrete view for deleting a model instance. diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 6f848cee..51e96196 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -85,7 +85,7 @@ class DjangoModelPermissions(BasePermission): """ kwargs = { 'app_label': model_cls._meta.app_label, - 'model_name': model_cls._meta.module_name + 'model_name': model_cls._meta.module_name } return [perm % kwargs for perm in self.perms_map[method]] diff --git a/rest_framework/request.py b/rest_framework/request.py index 5870be82..a1827ba4 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -21,8 +21,8 @@ def is_form_media_type(media_type): Return True if the media type is a valid form media type. """ base_media_type, params = parse_header(media_type) - return base_media_type == 'application/x-www-form-urlencoded' or \ - base_media_type == 'multipart/form-data' + return (base_media_type == 'application/x-www-form-urlencoded' or + base_media_type == 'multipart/form-data') class Empty(object): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index db6f9f61..8a895343 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -34,10 +34,10 @@ def _is_protected_type(obj): """ return isinstance(obj, ( types.NoneType, - int, long, - datetime.datetime, datetime.date, datetime.time, - float, Decimal, - basestring) + int, long, + datetime.datetime, datetime.date, datetime.time, + float, Decimal, + basestring) ) -- cgit v1.2.3 From b9e576f16ef7cc98f671e9c18ff8ae1a95bfe3ad Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 18:44:23 +0100 Subject: Push tests into a seperate app namespace 'rest_framework.test' Prevents tests from running by default when rest_framework is installed as 3rd party app. Fixes #316, #185 --- rest_framework/runtests/runtests.py | 2 +- rest_framework/runtests/settings.py | 9 +-------- rest_framework/tests/__init__.py | 13 ------------- rest_framework/tests/models.py | 2 +- rest_framework/tests/tests.py | 13 +++++++++++++ 5 files changed, 16 insertions(+), 23 deletions(-) create mode 100644 rest_framework/tests/tests.py (limited to 'rest_framework') diff --git a/rest_framework/runtests/runtests.py b/rest_framework/runtests/runtests.py index b2438c9b..1bd0a5fc 100755 --- a/rest_framework/runtests/runtests.py +++ b/rest_framework/runtests/runtests.py @@ -32,7 +32,7 @@ def main(): else: print usage() sys.exit(1) - failures = test_runner.run_tests(['rest_framework' + test_case]) + failures = test_runner.run_tests(['tests' + test_case]) sys.exit(failures) diff --git a/rest_framework/runtests/settings.py b/rest_framework/runtests/settings.py index 67de82c8..951b1e72 100644 --- a/rest_framework/runtests/settings.py +++ b/rest_framework/runtests/settings.py @@ -91,6 +91,7 @@ INSTALLED_APPS = ( # 'django.contrib.admindocs', 'rest_framework', 'rest_framework.authtoken', + 'rest_framework.tests' ) STATIC_URL = '/static/' @@ -100,14 +101,6 @@ import django if django.VERSION < (1, 3): INSTALLED_APPS += ('staticfiles',) -# OAuth support is optional, so we only test oauth if it's installed. -try: - import oauth_provider -except ImportError: - pass -else: - INSTALLED_APPS += ('oauth_provider',) - # If we're running on the Jenkins server we want to archive the coverage reports as XML. import os if os.environ.get('HUDSON_URL', None): diff --git a/rest_framework/tests/__init__.py b/rest_framework/tests/__init__.py index adeaf6da..e69de29b 100644 --- a/rest_framework/tests/__init__.py +++ b/rest_framework/tests/__init__.py @@ -1,13 +0,0 @@ -""" -Force import of all modules in this package in order to get the standard test -runner to pick up the tests. Yowzers. -""" -import os - -modules = [filename.rsplit('.', 1)[0] - for filename in os.listdir(os.path.dirname(__file__)) - if filename.endswith('.py') and not filename.startswith('_')] -__test__ = dict() - -for module in modules: - exec("from rest_framework.tests.%s import *" % module) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 0ee18c69..d4ea729b 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -40,7 +40,7 @@ class RESTFrameworkModel(models.Model): Base for test models that sets app_label, so they play nicely. """ class Meta: - app_label = 'rest_framework' + app_label = 'tests' abstract = True diff --git a/rest_framework/tests/tests.py b/rest_framework/tests/tests.py new file mode 100644 index 00000000..adeaf6da --- /dev/null +++ b/rest_framework/tests/tests.py @@ -0,0 +1,13 @@ +""" +Force import of all modules in this package in order to get the standard test +runner to pick up the tests. Yowzers. +""" +import os + +modules = [filename.rsplit('.', 1)[0] + for filename in os.listdir(os.path.dirname(__file__)) + if filename.endswith('.py') and not filename.startswith('_')] +__test__ = dict() + +for module in modules: + exec("from rest_framework.tests.%s import *" % module) -- cgit v1.2.3 From d995742afc09ff8d387751a6fe47b9686845740b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 20:04:33 +0100 Subject: Add AllowAny permission --- rest_framework/settings.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 3c508294..9c40a214 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -37,11 +37,14 @@ DEFAULTS = { 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication' ), - 'DEFAULT_PERMISSION_CLASSES': (), - 'DEFAULT_THROTTLE_CLASSES': (), + 'DEFAULT_PERMISSION_CLASSES': ( + 'rest_framework.permissions.AllowAny', + ), + 'DEFAULT_THROTTLE_CLASSES': ( + ), + 'DEFAULT_CONTENT_NEGOTIATION_CLASS': 'rest_framework.negotiation.DefaultContentNegotiation', - 'DEFAULT_MODEL_SERIALIZER_CLASS': 'rest_framework.serializers.ModelSerializer', 'DEFAULT_PAGINATION_SERIALIZER_CLASS': -- cgit v1.2.3 From af96fe05d0138c34128fc3944fc2701cbad5bd01 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sat, 27 Oct 2012 20:17:49 +0100 Subject: Add AllowAny class --- rest_framework/permissions.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'rest_framework') diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 51e96196..655b78a3 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -18,6 +18,17 @@ class BasePermission(object): raise NotImplementedError(".has_permission() must be overridden.") +class AllowAny(BasePermission): + """ + Allow any access. + This isn't strictly required, since you could use an empty + permission_classes list, but it's useful because it makes the intention + more explicit. + """ + def has_permission(self, request, view, obj=None): + return True + + class IsAuthenticated(BasePermission): """ Allows access only to authenticated users. -- cgit v1.2.3 From 12c363c1fe237d0357e6020b44890926856b9191 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Oct 2012 18:12:56 +0000 Subject: TemplateHTMLRenderer, StaticHTMLRenderer --- rest_framework/renderers.py | 41 +++++++++++++++++++++++++++++++----- rest_framework/tests/htmlrenderer.py | 6 +++--- 2 files changed, 39 insertions(+), 8 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 1a8b1d97..cfe4df6d 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -139,13 +139,24 @@ class YAMLRenderer(BaseRenderer): return yaml.dump(data, stream=None, Dumper=self.encoder) -class HTMLRenderer(BaseRenderer): +class TemplateHTMLRenderer(BaseRenderer): """ - A Base class provided for convenience. + An HTML renderer for use with templates. - Render the object simply by using the given template. - To create a template renderer, subclass this class, and set - the :attr:`media_type` and :attr:`template` attributes. + The data supplied to the Response object should be a dictionary that will + be used as context for the template. + + The template name is determined by (in order of preference): + + 1. An explicit `.template_name` attribute set on the response. + 2. An explicit `.template_name` attribute set on this class. + 3. The return result of calling `view.get_template_names()`. + + For example: + data = {'users': User.objects.all()} + return Response(data, template_name='users.html') + + For pre-rendered HTML, see StaticHTMLRenderer. """ media_type = 'text/html' @@ -188,6 +199,26 @@ class HTMLRenderer(BaseRenderer): raise ConfigurationError('Returned a template response with no template_name') +class StaticHTMLRenderer(BaseRenderer): + """ + An HTML renderer class that simply returns pre-rendered HTML. + + The data supplied to the Response object should be a string representing + the pre-rendered HTML content. + + For example: + data = '
example' + return Response(data) + + For template rendered HTML, see TemplateHTMLRenderer. + """ + media_type = 'text/html' + format = 'html' + + def render(self, data, accepted_media_type=None, renderer_context=None): + return data + + class BrowsableAPIRenderer(BaseRenderer): """ HTML renderer used to self-document the API. diff --git a/rest_framework/tests/htmlrenderer.py b/rest_framework/tests/htmlrenderer.py index da2f83c3..10d7e31d 100644 --- a/rest_framework/tests/htmlrenderer.py +++ b/rest_framework/tests/htmlrenderer.py @@ -3,12 +3,12 @@ from django.test import TestCase from django.template import TemplateDoesNotExist, Template import django.template.loader from rest_framework.decorators import api_view, renderer_classes -from rest_framework.renderers import HTMLRenderer +from rest_framework.renderers import TemplateHTMLRenderer from rest_framework.response import Response @api_view(('GET',)) -@renderer_classes((HTMLRenderer,)) +@renderer_classes((TemplateHTMLRenderer,)) def example(request): """ A view that can returns an HTML representation. @@ -22,7 +22,7 @@ urlpatterns = patterns('', ) -class HTMLRendererTests(TestCase): +class TemplateHTMLRendererTests(TestCase): urls = 'rest_framework.tests.htmlrenderer' def setUp(self): -- cgit v1.2.3 From bc99142c7dc1ebf84ca0858ce32b400a537e1908 Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 28 Oct 2012 19:35:50 +0100 Subject: Added wo tests. One for PUTing on a non-existing id-based url. And another for PUTing on a non-existing slug-based url. Fix doctoring for 'test_put_cannot_set_id'. --- rest_framework/tests/generics.py | 44 ++++++++++++++++++++++++++++++++++++++-- rest_framework/tests/models.py | 5 +++++ 2 files changed, 47 insertions(+), 2 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index f4263478..151532a7 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -2,7 +2,7 @@ from django.test import TestCase from django.test.client import RequestFactory from django.utils import simplejson as json from rest_framework import generics, serializers, status -from rest_framework.tests.models import BasicModel, Comment +from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel factory = RequestFactory() @@ -22,6 +22,13 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView): model = BasicModel +class SlugBasedInstanceView(InstanceView): + """ + A model with a slug-field. + """ + model = SlugBasedModel + + class TestRootView(TestCase): def setUp(self): """ @@ -129,6 +136,7 @@ class TestInstanceView(TestCase): for obj in self.objects.all() ] self.view = InstanceView.as_view() + self.slug_based_view = SlugBasedInstanceView.as_view() def test_get_instance_view(self): """ @@ -198,7 +206,7 @@ class TestInstanceView(TestCase): def test_put_cannot_set_id(self): """ - POST requests to create a new object should not be able to set the id. + PUT requests to create a new object should not be able to set the id. """ content = {'id': 999, 'text': 'foobar'} request = factory.put('/1', json.dumps(content), @@ -224,6 +232,38 @@ class TestInstanceView(TestCase): updated = self.objects.get(id=1) self.assertEquals(updated.text, 'foobar') + def test_put_as_create_on_id_based_url(self): + """ + PUT requests to RetrieveUpdateDestroyAPIView should create an object + at the requested url if it doesn't exist, if creation is not possible, + e.g. the pk for an id-field is determined by the database, + a HTTP_403_FORBIDDEN error-response must be returned. + """ + content = {'text': 'foobar'} + # pk fields can not be created on demand, only the database can set th pk for a new object + request = factory.put('/5', json.dumps(content), + content_type='application/json') + response = self.view(request, pk=5).render() + expected = { + 'detail': u'A resource could not be created at the requested URI' + } + self.assertEquals(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEquals(response.data, expected) + + def test_put_as_create_on_slug_based_url(self): + """ + PUT requests to RetrieveUpdateDestroyAPIView should create an object + at the requested url if possible, else return HTTP_403_FORBIDDEN error-response. + """ + content = {'text': 'foobar'} + request = factory.put('/test_slug', json.dumps(content), + content_type='application/json') + response = self.slug_based_view(request, pk='test_slug').render() + self.assertEquals(response.status_code, status.HTTP_201_CREATED) + self.assertEquals(response.data, {'slug': 'test_slug', 'text': 'foobar'}) + updated = self.objects.get(slug='test_slug') + self.assertEquals(updated.text, 'foobar') + # Regression test for #285 diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index d4ea729b..ac73a4bb 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -52,6 +52,11 @@ class BasicModel(RESTFrameworkModel): text = models.CharField(max_length=100) +class SlugBasedModel(RESTFrameworkModel): + text = models.CharField(max_length=100) + slug = models.SlugField(max_length=32) + + class DefaultValueModel(RESTFrameworkModel): text = models.CharField(default='foobar', max_length=100) -- cgit v1.2.3 From 5bb66803761c0536497158d14ce0a23665a335da Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 28 Oct 2012 20:45:42 +0100 Subject: test_put_as_create_on_id_based_url should check for a created-response. --- rest_framework/tests/generics.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 151532a7..48805720 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -235,20 +235,16 @@ class TestInstanceView(TestCase): def test_put_as_create_on_id_based_url(self): """ PUT requests to RetrieveUpdateDestroyAPIView should create an object - at the requested url if it doesn't exist, if creation is not possible, - e.g. the pk for an id-field is determined by the database, - a HTTP_403_FORBIDDEN error-response must be returned. + at the requested url if it doesn't exist. """ content = {'text': 'foobar'} # pk fields can not be created on demand, only the database can set th pk for a new object request = factory.put('/5', json.dumps(content), content_type='application/json') response = self.view(request, pk=5).render() - expected = { - 'detail': u'A resource could not be created at the requested URI' - } - self.assertEquals(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEquals(response.data, expected) + self.assertEquals(response.status_code, status.HTTP_201_CREATED) + new_obj = self.objects.get(slug='test_slug') + self.assertEquals(new_obj.text, 'foobar') def test_put_as_create_on_slug_based_url(self): """ @@ -261,8 +257,8 @@ class TestInstanceView(TestCase): response = self.slug_based_view(request, pk='test_slug').render() self.assertEquals(response.status_code, status.HTTP_201_CREATED) self.assertEquals(response.data, {'slug': 'test_slug', 'text': 'foobar'}) - updated = self.objects.get(slug='test_slug') - self.assertEquals(updated.text, 'foobar') + new_obj = self.objects.get(slug='test_slug') + self.assertEquals(new_obj.text, 'foobar') # Regression test for #285 -- cgit v1.2.3 From 1a16289edeea73253826916ca230af2bf30ba39f Mon Sep 17 00:00:00 2001 From: Marko Tibold Date: Sun, 28 Oct 2012 20:56:48 +0100 Subject: Get the correct instance --- rest_framework/tests/generics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'rest_framework') diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py index 48805720..a0a4109d 100644 --- a/rest_framework/tests/generics.py +++ b/rest_framework/tests/generics.py @@ -243,7 +243,7 @@ class TestInstanceView(TestCase): content_type='application/json') response = self.view(request, pk=5).render() self.assertEquals(response.status_code, status.HTTP_201_CREATED) - new_obj = self.objects.get(slug='test_slug') + new_obj = self.objects.get(pk=5) self.assertEquals(new_obj.text, 'foobar') def test_put_as_create_on_slug_based_url(self): -- cgit v1.2.3 From 6e4ab09aae8295e4ef722d59894bc2934435ae46 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Oct 2012 20:21:45 +0000 Subject: readonly -> read_only --- rest_framework/fields.py | 14 +++++++------- rest_framework/renderers.py | 2 +- rest_framework/tests/serializer.py | 6 +++--- rest_framework/tests/validators.py | 10 +++++----- 4 files changed, 16 insertions(+), 16 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 85e6ee31..c0e527e5 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -111,17 +111,17 @@ class WritableField(Field): widget = widgets.TextInput default = None - def __init__(self, source=None, readonly=False, required=None, + def __init__(self, source=None, read_only=False, required=None, validators=[], error_messages=None, widget=None, default=None): super(WritableField, self).__init__(source=source) - self.readonly = readonly + self.read_only = read_only if required is None: - self.required = not(readonly) + self.required = not(read_only) else: - assert not readonly, "Cannot set required=True and readonly=True" + assert not read_only, "Cannot set required=True and read_only=True" self.required = required messages = {} @@ -166,7 +166,7 @@ class WritableField(Field): Given a dictionary and a field name, updates the dictionary `into`, with the field and it's deserialized value. """ - if self.readonly: + if self.read_only: return try: @@ -240,7 +240,7 @@ class RelatedField(WritableField): return self.to_native(value) def field_from_native(self, data, field_name, into): - if self.readonly: + if self.read_only: return value = data.get(field_name) @@ -256,7 +256,7 @@ class ManyRelatedMixin(object): return [self.to_native(item) for item in value.all()] def field_from_native(self, data, field_name, into): - if self.readonly: + if self.read_only: return try: diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index cfe4df6d..938e8664 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -288,7 +288,7 @@ class BrowsableAPIRenderer(BaseRenderer): fields = {} for k, v in serializer.get_fields(True).items(): - if getattr(v, 'readonly', True): + if getattr(v, 'read_only', True): continue kwargs = {} diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py index 67c97f0f..5df3bd7e 100644 --- a/rest_framework/tests/serializer.py +++ b/rest_framework/tests/serializer.py @@ -301,7 +301,7 @@ class ManyToManyTests(TestCase): class ReadOnlyManyToManyTests(TestCase): def setUp(self): class ReadOnlyManyToManySerializer(serializers.ModelSerializer): - rel = serializers.ManyRelatedField(readonly=True) + rel = serializers.ManyRelatedField(read_only=True) class Meta: model = ReadOnlyManyToManyModel @@ -323,7 +323,7 @@ class ReadOnlyManyToManyTests(TestCase): def test_update(self): """ Attempt to update an instance of a model with a ManyToMany - relationship. Not updated due to readonly=True + relationship. Not updated due to read_only=True """ new_anchor = Anchor() new_anchor.save() @@ -339,7 +339,7 @@ class ReadOnlyManyToManyTests(TestCase): def test_update_without_relationship(self): """ Attempt to update an instance of a model where many to ManyToMany - relationship is not supplied. Not updated due to readonly=True + relationship is not supplied. Not updated due to read_only=True """ new_anchor = Anchor() new_anchor.save() diff --git a/rest_framework/tests/validators.py b/rest_framework/tests/validators.py index b390c42f..c032985e 100644 --- a/rest_framework/tests/validators.py +++ b/rest_framework/tests/validators.py @@ -285,7 +285,7 @@ # uiop = models.CharField(max_length=256, blank=True) # @property -# def readonly(self): +# def read_only(self): # return 'read only' # class MockResource(ModelResource): @@ -298,7 +298,7 @@ # def test_property_fields_are_allowed_on_model_forms(self): # """Validation on ModelForms may include property fields that exist on the Model to be included in the input.""" -# content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only'} +# content = {'qwerty': 'example', 'uiop': 'example', 'read_only': 'read only'} # self.assertEqual(self.validator.validate_request(content, None), content) # def test_property_fields_are_not_required_on_model_forms(self): @@ -310,19 +310,19 @@ # """If some (otherwise valid) content includes fields that are not in the form then validation should fail. # It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up # broken clients more easily (eg submitting content with a misnamed field)""" -# content = {'qwerty': 'example', 'uiop': 'example', 'readonly': 'read only', 'extra': 'extra'} +# content = {'qwerty': 'example', 'uiop': 'example', 'read_only': 'read only', 'extra': 'extra'} # self.assertRaises(ImmediateResponse, self.validator.validate_request, content, None) # def test_validate_requires_fields_on_model_forms(self): # """If some (otherwise valid) content includes fields that are not in the form then validation should fail. # It might be okay on normal form submission, but for Web APIs we oughta get strict, as it'll help show up # broken clients more easily (eg submitting content with a misnamed field)""" -# content = {'readonly': 'read only'} +# content = {'read_only': 'read only'} # self.assertRaises(ImmediateResponse, self.validator.validate_request, content, None) # def test_validate_does_not_require_blankable_fields_on_model_forms(self): # """Test standard ModelForm validation behaviour - fields with blank=True are not required.""" -# content = {'qwerty': 'example', 'readonly': 'read only'} +# content = {'qwerty': 'example', 'read_only': 'read only'} # self.validator.validate_request(content, None) # def test_model_form_validator_uses_model_forms(self): -- cgit v1.2.3 From 351382fe35f966c989b27add5bb04d0d983a99ee Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Sun, 28 Oct 2012 20:43:43 +0000 Subject: nested -> depth --- rest_framework/serializers.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) (limited to 'rest_framework') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8a895343..31d9d7a5 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -74,7 +74,7 @@ class SerializerOptions(object): Meta class options for Serializer """ def __init__(self, meta): - self.nested = getattr(meta, 'nested', False) + self.depth = getattr(meta, 'depth', 0) self.fields = getattr(meta, 'fields', ()) self.exclude = getattr(meta, 'exclude', ()) @@ -156,10 +156,8 @@ class BaseSerializer(Field): """ super(BaseSerializer, self).initialize(parent) self.stack = parent.stack[:] - if parent.opts.nested and not isinstance(parent.opts.nested, bool): - self.opts.nested = parent.opts.nested - 1 - else: - self.opts.nested = parent.opts.nested + if parent.opts.depth: + self.opts.depth = parent.opts.depth - 1 ##### # Methods to convert or revert from objects <--> primative representations. @@ -182,14 +180,10 @@ class BaseSerializer(Field): ret = self._dict_class() ret.fields = {} - fields = self.get_fields(serialize=True, obj=obj, nested=self.opts.nested) + fields = self.get_fields(serialize=True, obj=obj, nested=bool(self.opts.depth)) for field_name, field in fields.items(): key = self.get_field_key(field_name) - try: - value = field.field_to_native(obj, field_name) - except RecursionOccured: - field = self.get_fields(serialize=True, obj=obj, nested=False)[field_name] - value = field.field_to_native(obj, field_name) + value = field.field_to_native(obj, field_name) ret[key] = value ret.fields[key] = field return ret @@ -199,7 +193,7 @@ class BaseSerializer(Field): Core of deserialization, together with `restore_object`. Converts a dictionary of data into a dictionary of deserialized fields. """ - fields = self.get_fields(serialize=False, data=data, nested=self.opts.nested) + fields = self.get_fields(serialize=False, data=data, nested=bool(self.opts.depth)) reverted_data = {} for field_name, field in fields.items(): try: @@ -213,7 +207,8 @@ class BaseSerializer(Field): """ Run `validate_{{ description }}
+ {{ description }}{{ request.method }} {{ request.get_full_path }}
--
cgit v1.2.3
From 343da8e3cf0d50761cd5b15e168a104aad4e0ac5 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 29 Oct 2012 16:13:08 +0000
Subject: PUT as create should return 200
---
rest_framework/tests/generics.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
(limited to 'rest_framework')
diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py
index a0a4109d..ef5edd58 100644
--- a/rest_framework/tests/generics.py
+++ b/rest_framework/tests/generics.py
@@ -242,7 +242,7 @@ class TestInstanceView(TestCase):
request = factory.put('/5', json.dumps(content),
content_type='application/json')
response = self.view(request, pk=5).render()
- self.assertEquals(response.status_code, status.HTTP_201_CREATED)
+ self.assertEquals(response.status_code, status.HTTP_200_OK)
new_obj = self.objects.get(pk=5)
self.assertEquals(new_obj.text, 'foobar')
@@ -255,7 +255,7 @@ class TestInstanceView(TestCase):
request = factory.put('/test_slug', json.dumps(content),
content_type='application/json')
response = self.slug_based_view(request, pk='test_slug').render()
- self.assertEquals(response.status_code, status.HTTP_201_CREATED)
+ self.assertEquals(response.status_code, status.HTTP_200_OK)
self.assertEquals(response.data, {'slug': 'test_slug', 'text': 'foobar'})
new_obj = self.objects.get(slug='test_slug')
self.assertEquals(new_obj.text, 'foobar')
--
cgit v1.2.3
From d206c686a643b7cc720743d8dab207e8ad0280f8 Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 29 Oct 2012 17:08:38 +0000
Subject: Fixes for urls with suffixes
---
rest_framework/exceptions.py | 8 --------
rest_framework/fields.py | 12 ++++++++----
rest_framework/negotiation.py | 3 ++-
rest_framework/reverse.py | 8 +++++---
4 files changed, 15 insertions(+), 16 deletions(-)
(limited to 'rest_framework')
diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py
index 572425b9..89479deb 100644
--- a/rest_framework/exceptions.py
+++ b/rest_framework/exceptions.py
@@ -31,14 +31,6 @@ class PermissionDenied(APIException):
self.detail = detail or self.default_detail
-class InvalidFormat(APIException):
- status_code = status.HTTP_404_NOT_FOUND
- default_detail = "Format suffix '.%s' not found."
-
- def __init__(self, format, detail=None):
- self.detail = (detail or self.default_detail) % format
-
-
class MethodNotAllowed(APIException):
status_code = status.HTTP_405_METHOD_NOT_ALLOWED
default_detail = "Method '%s' not allowed."
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index 03c01a88..f9de7cf9 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -331,14 +331,16 @@ class HyperlinkedRelatedField(RelatedField):
self.view_name = kwargs.pop('view_name')
except:
raise ValueError("Hyperlinked field requires 'view_name' kwarg")
+ self.format = kwargs.pop('format', None)
super(HyperlinkedRelatedField, self).__init__(*args, **kwargs)
def to_native(self, obj):
view_name = self.view_name
request = self.context.get('request', None)
+ format = self.format or self.context.get('format', None)
kwargs = {self.pk_url_kwarg: obj.pk}
try:
- return reverse(view_name, kwargs=kwargs, request=request)
+ return reverse(view_name, kwargs=kwargs, request=request, format=format)
except:
pass
@@ -349,13 +351,13 @@ class HyperlinkedRelatedField(RelatedField):
kwargs = {self.slug_url_kwarg: slug}
try:
- return reverse(self.view_name, kwargs=kwargs, request=request)
+ return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
except:
pass
kwargs = {self.pk_url_kwarg: obj.pk, self.slug_url_kwarg: slug}
try:
- return reverse(self.view_name, kwargs=kwargs, request=request)
+ return reverse(self.view_name, kwargs=kwargs, request=request, format=format)
except:
pass
@@ -405,13 +407,15 @@ class HyperlinkedIdentityField(Field):
# TODO: Make this mandatory, and have the HyperlinkedModelSerializer
# set it on-the-fly
self.view_name = kwargs.pop('view_name', None)
+ self.format = kwargs.pop('format', None)
super(HyperlinkedIdentityField, self).__init__(*args, **kwargs)
def field_to_native(self, obj, field_name):
request = self.context.get('request', None)
+ format = self.format or self.context.get('format', None)
view_name = self.view_name or self.parent.opts.view_name
view_kwargs = {'pk': obj.pk}
- return reverse(view_name, kwargs=view_kwargs, request=request)
+ return reverse(view_name, kwargs=view_kwargs, request=request, format=format)
##### Typed Fields #####
diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py
index 444f8056..dae38477 100644
--- a/rest_framework/negotiation.py
+++ b/rest_framework/negotiation.py
@@ -1,3 +1,4 @@
+from django.http import Http404
from rest_framework import exceptions
from rest_framework.settings import api_settings
from rest_framework.utils.mediatypes import order_by_precedence, media_type_matches
@@ -66,7 +67,7 @@ class DefaultContentNegotiation(BaseContentNegotiation):
renderers = [renderer for renderer in renderers
if renderer.format == format]
if not renderers:
- raise exceptions.InvalidFormat(format)
+ raise Http404
return renderers
def get_accept_list(self, request):
diff --git a/rest_framework/reverse.py b/rest_framework/reverse.py
index ba663f98..c9db02f0 100644
--- a/rest_framework/reverse.py
+++ b/rest_framework/reverse.py
@@ -5,13 +5,15 @@ from django.core.urlresolvers import reverse as django_reverse
from django.utils.functional import lazy
-def reverse(viewname, *args, **kwargs):
+def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra):
"""
Same as `django.core.urlresolvers.reverse`, but optionally takes a request
and returns a fully qualified URL, using the request to get the base URL.
"""
- request = kwargs.pop('request', None)
- url = django_reverse(viewname, *args, **kwargs)
+ if format is not None:
+ kwargs = kwargs or {}
+ kwargs['format'] = format
+ url = django_reverse(viewname, args=args, kwargs=kwargs, **extra)
if request:
return request.build_absolute_uri(url)
return url
--
cgit v1.2.3
From b6c90c0d684cd036a2ce874fd9ace7a8a2bfda2f Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Mon, 29 Oct 2012 17:20:06 +0000
Subject: Fix slug based put as create test
---
rest_framework/tests/generics.py | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
(limited to 'rest_framework')
diff --git a/rest_framework/tests/generics.py b/rest_framework/tests/generics.py
index ef5edd58..d45ea976 100644
--- a/rest_framework/tests/generics.py
+++ b/rest_framework/tests/generics.py
@@ -22,11 +22,20 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView):
model = BasicModel
+class SlugSerializer(serializers.ModelSerializer):
+ slug = serializers.Field() # read only
+
+ class Meta:
+ model = SlugBasedModel
+ exclude = ('id',)
+
+
class SlugBasedInstanceView(InstanceView):
"""
A model with a slug-field.
"""
model = SlugBasedModel
+ serializer_class = SlugSerializer
class TestRootView(TestCase):
@@ -254,10 +263,10 @@ class TestInstanceView(TestCase):
content = {'text': 'foobar'}
request = factory.put('/test_slug', json.dumps(content),
content_type='application/json')
- response = self.slug_based_view(request, pk='test_slug').render()
+ response = self.slug_based_view(request, slug='test_slug').render()
self.assertEquals(response.status_code, status.HTTP_200_OK)
self.assertEquals(response.data, {'slug': 'test_slug', 'text': 'foobar'})
- new_obj = self.objects.get(slug='test_slug')
+ new_obj = SlugBasedModel.objects.get(slug='test_slug')
self.assertEquals(new_obj.text, 'foobar')
--
cgit v1.2.3
From 6ccbdd10ad91bbc2116c6adf520096e9da95b8e0 Mon Sep 17 00:00:00 2001
From: Marko Tibold
Date: Tue, 30 Oct 2012 00:03:24 +0100
Subject: Adding failing tests that show null=True model fields are won't
validate if their value is omitted.
---
rest_framework/tests/models.py | 5 +++++
rest_framework/tests/serializer.py | 13 +++++++++++++
2 files changed, 18 insertions(+)
(limited to 'rest_framework')
diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py
index ac73a4bb..fb23e359 100644
--- a/rest_framework/tests/models.py
+++ b/rest_framework/tests/models.py
@@ -116,3 +116,8 @@ class BlogPost(RESTFrameworkModel):
class BlogPostComment(RESTFrameworkModel):
text = models.TextField()
blog_post = models.ForeignKey(BlogPost)
+
+
+class Person(RESTFrameworkModel):
+ name = models.CharField(max_length=10)
+ age = models.IntegerField(null=True, blank=True)
diff --git a/rest_framework/tests/serializer.py b/rest_framework/tests/serializer.py
index 5df3bd7e..eb21dc46 100644
--- a/rest_framework/tests/serializer.py
+++ b/rest_framework/tests/serializer.py
@@ -43,6 +43,11 @@ class ActionItemSerializer(serializers.ModelSerializer):
model = ActionItem
+class PersonSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Person
+
+
class BasicTests(TestCase):
def setUp(self):
self.comment = Comment(
@@ -188,6 +193,14 @@ class ValidationTests(TestCase):
self.assertFalse(serializer.is_valid())
self.assertEquals(serializer.errors, {'non_field_errors': [u'Email address not in content']})
+ def test_null_is_true_fields(self):
+ """
+ Omitting a value for null-field should validate.
+ """
+ serializer = PersonSerializer({'name': 'marko'})
+ self.assertEquals(serializer.is_valid(), True)
+ self.assertEquals(serializer.errors, {})
+
class MetadataTests(TestCase):
def test_empty(self):
--
cgit v1.2.3
From 206859529780adbe7f49ee1e4d466e3b4fe13099 Mon Sep 17 00:00:00 2001
From: Marko Tibold
Date: Tue, 30 Oct 2012 00:09:40 +0100
Subject: Set a ModelSerializer field to required=False if the ModelField has
null=True set.
---
rest_framework/serializers.py | 3 +++
1 file changed, 3 insertions(+)
(limited to 'rest_framework')
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index ce04b3e2..e705bf77 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -393,6 +393,9 @@ class ModelSerializer(Serializer):
Creates a default instance of a basic non-relational field.
"""
kwargs = {}
+ if model_field.null:
+ kwargs['required'] = False
+
if model_field.has_default():
kwargs['required'] = False
kwargs['default'] = model_field.get_default()
--
cgit v1.2.3
From 4d90bb4af4e565f156ca5d84eb205cbb63810b46 Mon Sep 17 00:00:00 2001
From: Marko Tibold
Date: Tue, 30 Oct 2012 00:30:52 +0100
Subject: Fix some typos.
---
rest_framework/fields.py | 4 ++--
rest_framework/generics.py | 2 +-
rest_framework/serializers.py | 2 +-
rest_framework/throttling.py | 2 +-
rest_framework/views.py | 2 +-
5 files changed, 6 insertions(+), 6 deletions(-)
(limited to 'rest_framework')
diff --git a/rest_framework/fields.py b/rest_framework/fields.py
index f9de7cf9..067b1087 100644
--- a/rest_framework/fields.py
+++ b/rest_framework/fields.py
@@ -43,7 +43,7 @@ class Field(object):
Called to set up a field prior to field_to_native or field_from_native.
parent - The parent serializer.
- model_field - The model field this field corrosponds to, if one exists.
+ model_field - The model field this field corresponds to, if one exists.
"""
self.parent = parent
self.root = parent.root or parent
@@ -197,7 +197,7 @@ class WritableField(Field):
class ModelField(WritableField):
"""
- A generic field that can be used against an arbirtrary model field.
+ A generic field that can be used against an arbitrary model field.
"""
def __init__(self, *args, **kwargs):
try:
diff --git a/rest_framework/generics.py b/rest_framework/generics.py
index 190a5f79..27540a57 100644
--- a/rest_framework/generics.py
+++ b/rest_framework/generics.py
@@ -1,5 +1,5 @@
"""
-Generic views that provide commmonly needed behaviour.
+Generic views that provide commonly needed behaviour.
"""
from rest_framework import views, mixins
diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py
index ce04b3e2..1cfcf103 100644
--- a/rest_framework/serializers.py
+++ b/rest_framework/serializers.py
@@ -279,7 +279,7 @@ class BaseSerializer(Field):
def errors(self):
"""
Run deserialization and return error data,
- setting self.object if no errors occured.
+ setting self.object if no errors occurred.
"""
if self._errors is None:
obj = self.from_native(self.init_data)
diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py
index 6860e6b9..8fe64248 100644
--- a/rest_framework/throttling.py
+++ b/rest_framework/throttling.py
@@ -16,7 +16,7 @@ class BaseThrottle(object):
def wait(self):
"""
- Optionally, return a recommeded number of seconds to wait before
+ Optionally, return a recommended number of seconds to wait before
the next request.
"""
return None
diff --git a/rest_framework/views.py b/rest_framework/views.py
index c721be3c..71e1fe6c 100644
--- a/rest_framework/views.py
+++ b/rest_framework/views.py
@@ -218,7 +218,7 @@ class APIView(View):
def get_throttles(self):
"""
- Instantiates and returns the list of thottles that this view uses.
+ Instantiates and returns the list of throttles that this view uses.
"""
return [throttle() for throttle in self.throttle_classes]
--
cgit v1.2.3
From 1b6fe9abb4a8b7a5eade9fba5915ae0c116e42ea Mon Sep 17 00:00:00 2001
From: Tom Christie
Date: Tue, 30 Oct 2012 10:35:30 +0000
Subject: Hyperlinked serializer tests
---
rest_framework/tests/hyperlinkedserializers.py | 44 +++++++++++++++++++++++++-
1 file changed, 43 insertions(+), 1 deletion(-)
(limited to 'rest_framework')
diff --git a/rest_framework/tests/hyperlinkedserializers.py b/rest_framework/tests/hyperlinkedserializers.py
index 5532a8ee..f1eddd35 100644
--- a/rest_framework/tests/hyperlinkedserializers.py
+++ b/rest_framework/tests/hyperlinkedserializers.py
@@ -2,11 +2,19 @@ from django.conf.urls.defaults import patterns, url
from django.test import TestCase
from django.test.client import RequestFactory
from rest_framework import generics, status, serializers
-from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel
+from rest_framework.tests.models import Anchor, BasicModel, ManyToManyModel, BlogPost, BlogPostComment
factory = RequestFactory()
+class BlogPostCommentSerializer(serializers.Serializer):
+ text = serializers.CharField()
+ blog_post_url = serializers.HyperlinkedRelatedField(source='blog_post', view_name='blogpost-detail', queryset=BlogPost.objects.all())
+
+ def restore_object(self, attrs, instance=None):
+ return BlogPostComment(**attrs)
+
+
class BasicList(generics.ListCreateAPIView):
model = BasicModel
model_serializer_class = serializers.HyperlinkedModelSerializer
@@ -32,12 +40,22 @@ class ManyToManyDetail(generics.RetrieveAPIView):
model_serializer_class = serializers.HyperlinkedModelSerializer
+class BlogPostCommentListCreate(generics.ListCreateAPIView):
+ model = BlogPostComment
+ model_serializer_class = BlogPostCommentSerializer
+
+
+class BlogPostDetail(generics.RetrieveAPIView):
+ model = BlogPost
+
urlpatterns = patterns('',
url(r'^basic/$', BasicList.as_view(), name='basicmodel-list'),
url(r'^basic/(?P(?:%s).*?[a-zA-Z].*?
\s*)+)' % '| trailing_empty_content_re = re.compile(r'(?:(?: |\s|
)*?
{{ request.method }} {{ request.get_full_path }}
-