From 12d05630821b126f61dff74c0193132187f4f74c Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 28 Jan 2014 07:40:13 +0100 Subject: [PATCH 001/103] Updated the django-filter version and added tests against Django 1.7a1 --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 18fe66abd..84a15b15a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ python: - "3.3" env: + - DJANGO="https://www.djangoproject.com/download/1.7a1/tarball/" - DJANGO="django==1.6" - DJANGO="django==1.5.5" - DJANGO="django==1.4.10" @@ -20,7 +21,7 @@ install: - "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" + - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.7; fi" - export PYTHONPATH=. script: @@ -28,6 +29,8 @@ script: matrix: exclude: + - python: "2.6" + env: DJANGO="https://www.djangoproject.com/download/1.7a1/tarball/" - python: "3.2" env: DJANGO="django==1.4.10" - python: "3.2" From 7713ddc0a83b41921992fc31909483903975f795 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 28 Jan 2014 13:20:51 +0100 Subject: [PATCH 002/103] =?UTF-8?q?Don=E2=80=99t=20forget=20to=20setup=20d?= =?UTF-8?q?jango=20for=201.7+=20as=20it=E2=80=99s=20not=20a=20regular=20dj?= =?UTF-8?q?ango=20command.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rest_framework/runtests/runtests.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rest_framework/runtests/runtests.py b/rest_framework/runtests/runtests.py index da36d23fc..2daaae4ee 100755 --- a/rest_framework/runtests/runtests.py +++ b/rest_framework/runtests/runtests.py @@ -26,6 +26,10 @@ def usage(): def main(): + try: + django.setup() + except AttributeError: + pass TestRunner = get_runner(settings) test_runner = TestRunner() From 6aadd1639b11091fa1987b1608fd4f797f09bc2b Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 28 Jan 2014 18:53:24 +0100 Subject: [PATCH 003/103] Moved all the reused models to the models files to avoid conflicts with the refactored apps. --- rest_framework/tests/models.py | 27 +++++++++++++++++++ rest_framework/tests/test_filters.py | 7 +---- rest_framework/tests/test_pagination.py | 7 +---- rest_framework/tests/test_relations_nested.py | 4 +-- rest_framework/tests/test_serializer.py | 19 +------------ 5 files changed, 31 insertions(+), 33 deletions(-) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 32a726c0b..ae664fc81 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -168,3 +168,30 @@ class NullableOneToOneSource(RESTFrameworkModel): class BasicModelSerializer(serializers.ModelSerializer): class Meta: model = BasicModel + + +# Models to test the serializers +class AMOAFModel(RESTFrameworkModel): + char_field = models.CharField(max_length=1024, blank=True) + comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True) + decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True) + email_field = models.EmailField(max_length=1024, blank=True) + file_field = models.FileField(max_length=1024, blank=True) + image_field = models.ImageField(max_length=1024, blank=True) + slug_field = models.SlugField(max_length=1024, blank=True) + url_field = models.URLField(max_length=1024, blank=True) + +class DVOAFModel(RESTFrameworkModel): + positive_integer_field = models.PositiveIntegerField(blank=True) + positive_small_integer_field = models.PositiveSmallIntegerField(blank=True) + email_field = models.EmailField(blank=True) + file_field = models.FileField(blank=True) + image_field = models.ImageField(blank=True) + slug_field = models.SlugField(blank=True) + url_field = models.URLField(blank=True) + +# Models to test filters +class FilterableItem(models.Model): + text = models.CharField(max_length=100) + decimal = models.DecimalField(max_digits=4, decimal_places=2) + date = models.DateField() diff --git a/rest_framework/tests/test_filters.py b/rest_framework/tests/test_filters.py index 181881865..1b84bca09 100644 --- a/rest_framework/tests/test_filters.py +++ b/rest_framework/tests/test_filters.py @@ -9,16 +9,11 @@ from rest_framework import generics, serializers, status, filters from rest_framework.compat import django_filters, patterns, url from rest_framework.test import APIRequestFactory from rest_framework.tests.models import BasicModel +from .models import FilterableItem factory = APIRequestFactory() -class FilterableItem(models.Model): - text = models.CharField(max_length=100) - decimal = models.DecimalField(max_digits=4, decimal_places=2) - date = models.DateField() - - if django_filters: # Basic filter on a list view. class FilterFieldsRootView(generics.ListCreateAPIView): diff --git a/rest_framework/tests/test_pagination.py b/rest_framework/tests/test_pagination.py index cadb515fa..f67e6df2d 100644 --- a/rest_framework/tests/test_pagination.py +++ b/rest_framework/tests/test_pagination.py @@ -9,16 +9,11 @@ from rest_framework import generics, status, pagination, filters, serializers from rest_framework.compat import django_filters from rest_framework.test import APIRequestFactory from rest_framework.tests.models import BasicModel +from .models import FilterableItem factory = APIRequestFactory() -class FilterableItem(models.Model): - text = models.CharField(max_length=100) - decimal = models.DecimalField(max_digits=4, decimal_places=2) - date = models.DateField() - - class RootView(generics.ListCreateAPIView): """ Example description for OPTIONS. diff --git a/rest_framework/tests/test_relations_nested.py b/rest_framework/tests/test_relations_nested.py index d393b0c35..4d9da4896 100644 --- a/rest_framework/tests/test_relations_nested.py +++ b/rest_framework/tests/test_relations_nested.py @@ -3,9 +3,7 @@ from django.db import models from django.test import TestCase from rest_framework import serializers - -class OneToOneTarget(models.Model): - name = models.CharField(max_length=100) +from .models import OneToOneTarget class OneToOneSource(models.Model): diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 75d6e7859..af9fa68e0 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -8,7 +8,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers, fields, relations from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel, BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel, - ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel) + ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel, AMOAFModel, DVOAFModel) from rest_framework.tests.models import BasicModelSerializer import datetime import pickle @@ -1481,15 +1481,6 @@ class ManyFieldHelpTextTest(TestCase): class AttributeMappingOnAutogeneratedFieldsTests(TestCase): def setUp(self): - class AMOAFModel(RESTFrameworkModel): - char_field = models.CharField(max_length=1024, blank=True) - comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True) - decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True) - email_field = models.EmailField(max_length=1024, blank=True) - file_field = models.FileField(max_length=1024, blank=True) - image_field = models.ImageField(max_length=1024, blank=True) - slug_field = models.SlugField(max_length=1024, blank=True) - url_field = models.URLField(max_length=1024, blank=True) class AMOAFSerializer(serializers.ModelSerializer): class Meta: @@ -1562,14 +1553,6 @@ class AttributeMappingOnAutogeneratedFieldsTests(TestCase): class DefaultValuesOnAutogeneratedFieldsTests(TestCase): def setUp(self): - class DVOAFModel(RESTFrameworkModel): - positive_integer_field = models.PositiveIntegerField(blank=True) - positive_small_integer_field = models.PositiveSmallIntegerField(blank=True) - email_field = models.EmailField(blank=True) - file_field = models.FileField(blank=True) - image_field = models.ImageField(blank=True) - slug_field = models.SlugField(blank=True) - url_field = models.URLField(blank=True) class DVOAFSerializer(serializers.ModelSerializer): class Meta: From da89cfc9373c03115b1bc2f62a7f974363c6c050 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 28 Jan 2014 18:53:58 +0100 Subject: [PATCH 004/103] =?UTF-8?q?Don=E2=80=99t=20rely=20on=20Django?= =?UTF-8?q?=E2=80=99s=20get=20as=20it=20breaks=20for=201.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rest_framework/test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rest_framework/test.py b/rest_framework/test.py index 234d10a4a..23a31ee4c 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -71,6 +71,10 @@ class APIRequestFactory(DjangoRequestFactory): return ret, content_type + def get(self, path, data=None, format=None, content_type=None, **extra): + data, content_type = self._encode_data(data, format, content_type) + return self.generic('GET', path, data, content_type, **extra) + def post(self, path, data=None, format=None, content_type=None, **extra): data, content_type = self._encode_data(data, format, content_type) return self.generic('POST', path, data, content_type, **extra) From 23a20160c4db0466c15f1a4d1edfda5004ebb551 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 28 Jan 2014 18:54:49 +0100 Subject: [PATCH 005/103] =?UTF-8?q?Mock=20updated=20as=20Django=E2=80=99s?= =?UTF-8?q?=20get=5Ftemplate=20now=20has=20two=20arguments.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rest_framework/tests/test_htmlrenderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_htmlrenderer.py b/rest_framework/tests/test_htmlrenderer.py index 8957a43c7..bb87272d2 100644 --- a/rest_framework/tests/test_htmlrenderer.py +++ b/rest_framework/tests/test_htmlrenderer.py @@ -50,7 +50,7 @@ class TemplateHTMLRendererTests(TestCase): """ self.get_template = django.template.loader.get_template - def get_template(template_name): + def get_template(template_name, dirs=None): if template_name == 'example.html': return Template("example: {{ object }}") raise TemplateDoesNotExist(template_name) From 656117814c8efb277c4284888bfb6741036c88e3 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 28 Jan 2014 18:55:29 +0100 Subject: [PATCH 006/103] Django 1.7 has new 404 and 403 body messages. --- rest_framework/tests/test_htmlrenderer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/test_htmlrenderer.py b/rest_framework/tests/test_htmlrenderer.py index bb87272d2..514d9e2b6 100644 --- a/rest_framework/tests/test_htmlrenderer.py +++ b/rest_framework/tests/test_htmlrenderer.py @@ -108,11 +108,13 @@ class TemplateHTMLRendererExceptionTests(TestCase): def test_not_found_html_view_with_template(self): response = self.client.get('/not_found') self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertEqual(response.content, six.b("404: Not found")) + self.assertTrue(response.content in ( + six.b("404: Not found"), six.b("404 Not Found"))) self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') def test_permission_denied_html_view_with_template(self): response = self.client.get('/permission_denied') self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - self.assertEqual(response.content, six.b("403: Permission denied")) + self.assertTrue(response.content in ( + six.b("403: Permission denied"), six.b("403 Forbidden"))) self.assertEqual(response['Content-Type'], 'text/html; charset=utf-8') From e4c25e0ff20c9b3d18dd05b2afa7f2fdf637b279 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 28 Jan 2014 18:57:36 +0100 Subject: [PATCH 007/103] wsgi_request is now added to the response so we have to remove it before pickling the response. --- rest_framework/tests/test_renderers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index fb33df2cf..cddd00e7c 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -601,6 +601,10 @@ class CacheRenderTest(TestCase): method = getattr(self.client, http_method) resp = method(url) del resp.client, resp.request + try: + del resp.wsgi_request + except AttributeError: + pass return resp def test_obj_pickling(self): From c6d89afdf7473c6b9f2af41a4bbc9bdaa83d39d1 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 28 Jan 2014 23:43:09 +0100 Subject: [PATCH 008/103] Define the upload_to for file fields. --- rest_framework/tests/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index ae664fc81..19461e2a0 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -176,8 +176,8 @@ class AMOAFModel(RESTFrameworkModel): comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True) decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True) email_field = models.EmailField(max_length=1024, blank=True) - file_field = models.FileField(max_length=1024, blank=True) - image_field = models.ImageField(max_length=1024, blank=True) + file_field = models.FileField(upload_to='test', max_length=1024, blank=True) + image_field = models.ImageField(upload_to='test', max_length=1024, blank=True) slug_field = models.SlugField(max_length=1024, blank=True) url_field = models.URLField(max_length=1024, blank=True) @@ -185,8 +185,8 @@ class DVOAFModel(RESTFrameworkModel): positive_integer_field = models.PositiveIntegerField(blank=True) positive_small_integer_field = models.PositiveSmallIntegerField(blank=True) email_field = models.EmailField(blank=True) - file_field = models.FileField(blank=True) - image_field = models.ImageField(blank=True) + file_field = models.FileField(upload_to='test', blank=True) + image_field = models.ImageField(upload_to='test', blank=True) slug_field = models.SlugField(blank=True) url_field = models.URLField(blank=True) From 9c910a3f6d3328c067a5454174691f0b7155904f Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 28 Jan 2014 23:43:37 +0100 Subject: [PATCH 009/103] Install Pillow for ImageField to work. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 84a15b15a..017dd5816 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ env: install: - pip install $DJANGO - - pip install defusedxml==0.3 + - pip install defusedxml==0.3 Pillow - "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.2.1; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4; fi" From 1319da59ce8e62d2b2d9fa938de8ac5b5ccfaf20 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 30 Jan 2014 14:26:21 +0100 Subject: [PATCH 010/103] Make factory.get work with Django 1.7 --- rest_framework/test.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/rest_framework/test.py b/rest_framework/test.py index 23a31ee4c..75cb4d0b9 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -8,6 +8,7 @@ from django.conf import settings from django.test.client import Client as DjangoClient from django.test.client import ClientHandler from django.test import testcases +from django.utils.http import urlencode from rest_framework.settings import api_settings from rest_framework.compat import RequestFactory as DjangoRequestFactory from rest_framework.compat import force_bytes_or_smart_bytes, six @@ -71,9 +72,12 @@ class APIRequestFactory(DjangoRequestFactory): return ret, content_type - def get(self, path, data=None, format=None, content_type=None, **extra): - data, content_type = self._encode_data(data, format, content_type) - return self.generic('GET', path, data, content_type, **extra) + def get(self, path, data=None, **extra): + r = { + 'QUERY_STRING': urlencode(data or {}, doseq=True), + } + r.update(extra) + return self.generic('GET', path, **r) def post(self, path, data=None, format=None, content_type=None, **extra): data, content_type = self._encode_data(data, format, content_type) From c2ee52239d995c676628777edda596706f5905d0 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 30 Jan 2014 14:27:09 +0100 Subject: [PATCH 011/103] Refactoring due to the previous commit. --- rest_framework/tests/test_filters.py | 51 +++++++++++++++---------- rest_framework/tests/test_pagination.py | 29 +++++++++----- 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/rest_framework/tests/test_filters.py b/rest_framework/tests/test_filters.py index 1b84bca09..dd5d8e428 100644 --- a/rest_framework/tests/test_filters.py +++ b/rest_framework/tests/test_filters.py @@ -123,7 +123,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase): # Tests that the decimal filter works. search_decimal = Decimal('2.25') - request = factory.get('/?decimal=%s' % search_decimal) + request = factory.get('/', {'decimal': '%s' % search_decimal}) response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) expected_data = [f for f in self.data if f['decimal'] == search_decimal] @@ -131,7 +131,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase): # 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' + request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-09-22' response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) expected_data = [f for f in self.data if f['date'] == search_date] @@ -146,7 +146,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase): # Tests that the decimal filter works. search_decimal = Decimal('2.25') - request = factory.get('/?decimal=%s' % search_decimal) + request = factory.get('/', {'decimal': '%s' % search_decimal}) response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) expected_data = [f for f in self.data if f['decimal'] == search_decimal] @@ -179,7 +179,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase): # Tests that the decimal filter set with 'lt' in the filter class works. search_decimal = Decimal('4.25') - request = factory.get('/?decimal=%s' % search_decimal) + request = factory.get('/', {'decimal': '%s' % search_decimal}) response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) expected_data = [f for f in self.data if f['decimal'] < search_decimal] @@ -187,7 +187,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase): # 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' + request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-10-02' response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) expected_data = [f for f in self.data if f['date'] > search_date] @@ -195,7 +195,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase): # Tests that the text filter set with 'icontains' in the filter class works. search_text = 'ff' - request = factory.get('/?text=%s' % search_text) + request = factory.get('/', {'text': '%s' % search_text}) response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) expected_data = [f for f in self.data if search_text in f['text'].lower()] @@ -204,7 +204,10 @@ class IntegrationTestFiltering(CommonFilteringTestCase): # Tests that multiple filters works. search_decimal = Decimal('5.25') search_date = datetime.date(2012, 10, 2) - request = factory.get('/?decimal=%s&date=%s' % (search_decimal, search_date)) + request = factory.get('/', { + 'decimal': '%s' % (search_decimal,), + 'date': '%s' % (search_date,) + }) response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) expected_data = [f for f in self.data if f['date'] > search_date and @@ -229,7 +232,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase): view = FilterFieldsRootView.as_view() search_integer = 10 - request = factory.get('/?integer=%s' % search_integer) + request = factory.get('/', {'integer': '%s' % search_integer}) response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -260,14 +263,18 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase): # Tests that the decimal filter set that should fail. search_decimal = Decimal('4.25') high_item = self.objects.filter(decimal__gt=search_decimal)[0] - response = self.client.get('{url}?decimal={param}'.format(url=self._get_url(high_item), param=search_decimal)) + response = self.client.get( + '{url}'.format(url=self._get_url(high_item)), + {'decimal': '{param}'.format(param=search_decimal)}) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) # Tests that the decimal filter set that should succeed. search_decimal = Decimal('4.25') low_item = self.objects.filter(decimal__lt=search_decimal)[0] low_item_data = self._serialize_object(low_item) - response = self.client.get('{url}?decimal={param}'.format(url=self._get_url(low_item), param=search_decimal)) + response = self.client.get( + '{url}'.format(url=self._get_url(low_item)), + {'decimal': '{param}'.format(param=search_decimal)}) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, low_item_data) @@ -276,7 +283,11 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase): search_date = datetime.date(2012, 10, 2) valid_item = self.objects.filter(decimal__lt=search_decimal, date__gt=search_date)[0] valid_item_data = self._serialize_object(valid_item) - response = self.client.get('{url}?decimal={decimal}&date={date}'.format(url=self._get_url(valid_item), decimal=search_decimal, date=search_date)) + response = self.client.get( + '{url}'.format(url=self._get_url(valid_item)), { + 'decimal': '{decimal}'.format(decimal=search_decimal), + 'date': '{date}'.format(date=search_date) + }) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data, valid_item_data) @@ -310,7 +321,7 @@ class SearchFilterTests(TestCase): search_fields = ('title', 'text') view = SearchListView.as_view() - request = factory.get('?search=b') + request = factory.get('/', {'search': 'b'}) response = view(request) self.assertEqual( response.data, @@ -327,7 +338,7 @@ class SearchFilterTests(TestCase): search_fields = ('=title', 'text') view = SearchListView.as_view() - request = factory.get('?search=zzz') + request = factory.get('/', {'search': 'zzz'}) response = view(request) self.assertEqual( response.data, @@ -343,7 +354,7 @@ class SearchFilterTests(TestCase): search_fields = ('title', '^text') view = SearchListView.as_view() - request = factory.get('?search=b') + request = factory.get('/', {'search': 'b'}) response = view(request) self.assertEqual( response.data, @@ -391,7 +402,7 @@ class OrderingFilterTests(TestCase): ordering_fields = ('text',) view = OrderingListView.as_view() - request = factory.get('?ordering=text') + request = factory.get('/', {'ordering': 'text'}) response = view(request) self.assertEqual( response.data, @@ -410,7 +421,7 @@ class OrderingFilterTests(TestCase): ordering_fields = ('text',) view = OrderingListView.as_view() - request = factory.get('?ordering=-text') + request = factory.get('/', {'ordering': '-text'}) response = view(request) self.assertEqual( response.data, @@ -429,7 +440,7 @@ class OrderingFilterTests(TestCase): ordering_fields = ('text',) view = OrderingListView.as_view() - request = factory.get('?ordering=foobar') + request = factory.get('/', {'ordering': 'foobar'}) response = view(request) self.assertEqual( response.data, @@ -498,7 +509,7 @@ class OrderingFilterTests(TestCase): models.Count("relateds")) view = OrderingListView.as_view() - request = factory.get('?ordering=relateds__count') + request = factory.get('/', {'ordering': 'relateds__count'}) response = view(request) self.assertEqual( response.data, @@ -561,7 +572,7 @@ class SensitiveOrderingFilterTests(TestCase): serializer_class = serializer_cls view = OrderingListView.as_view() - request = factory.get('?ordering=-username') + request = factory.get('/', {'ordering': '-username'}) response = view(request) if serializer_cls == SensitiveDataSerializer3: @@ -591,7 +602,7 @@ class SensitiveOrderingFilterTests(TestCase): serializer_class = serializer_cls view = OrderingListView.as_view() - request = factory.get('?ordering=password') + request = factory.get('/', {'ordering': 'password'}) response = view(request) if serializer_cls == SensitiveDataSerializer3: diff --git a/rest_framework/tests/test_pagination.py b/rest_framework/tests/test_pagination.py index f67e6df2d..24c1ba39f 100644 --- a/rest_framework/tests/test_pagination.py +++ b/rest_framework/tests/test_pagination.py @@ -13,6 +13,15 @@ from .models import FilterableItem factory = APIRequestFactory() +# Helper function to split arguments out of an url +def split_arguments_from_url(url): + if '?' not in url: + return url + + path, args = url.split('?') + args = dict(r.split('=') for r in args.split('&')) + return path, args + class RootView(generics.ListCreateAPIView): """ @@ -79,7 +88,7 @@ class IntegrationTestPagination(TestCase): self.assertNotEqual(response.data['next'], None) self.assertEqual(response.data['previous'], None) - request = factory.get(response.data['next']) + request = factory.get(*split_arguments_from_url(response.data['next'])) with self.assertNumQueries(2): response = self.view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -88,7 +97,7 @@ class IntegrationTestPagination(TestCase): self.assertNotEqual(response.data['next'], None) self.assertNotEqual(response.data['previous'], None) - request = factory.get(response.data['next']) + request = factory.get(*split_arguments_from_url(response.data['next'])) with self.assertNumQueries(2): response = self.view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -141,7 +150,7 @@ class IntegrationTestPaginationAndFiltering(TestCase): EXPECTED_NUM_QUERIES = 2 - request = factory.get('/?decimal=15.20') + request = factory.get('/', {'decimal': '15.20'}) with self.assertNumQueries(EXPECTED_NUM_QUERIES): response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -150,7 +159,7 @@ class IntegrationTestPaginationAndFiltering(TestCase): self.assertNotEqual(response.data['next'], None) self.assertEqual(response.data['previous'], None) - request = factory.get(response.data['next']) + request = factory.get(*split_arguments_from_url(response.data['next'])) with self.assertNumQueries(EXPECTED_NUM_QUERIES): response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -159,7 +168,7 @@ class IntegrationTestPaginationAndFiltering(TestCase): self.assertEqual(response.data['next'], None) self.assertNotEqual(response.data['previous'], None) - request = factory.get(response.data['previous']) + request = factory.get(*split_arguments_from_url(response.data['previous'])) with self.assertNumQueries(EXPECTED_NUM_QUERIES): response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -186,7 +195,7 @@ class IntegrationTestPaginationAndFiltering(TestCase): view = BasicFilterFieldsRootView.as_view() - request = factory.get('/?decimal=15.20') + request = factory.get('/', {'decimal': '15.20'}) with self.assertNumQueries(2): response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -195,7 +204,7 @@ class IntegrationTestPaginationAndFiltering(TestCase): self.assertNotEqual(response.data['next'], None) self.assertEqual(response.data['previous'], None) - request = factory.get(response.data['next']) + request = factory.get(*split_arguments_from_url(response.data['next'])) with self.assertNumQueries(2): response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -204,7 +213,7 @@ class IntegrationTestPaginationAndFiltering(TestCase): self.assertEqual(response.data['next'], None) self.assertNotEqual(response.data['previous'], None) - request = factory.get(response.data['previous']) + request = factory.get(*split_arguments_from_url(response.data['previous'])) with self.assertNumQueries(2): response = view(request).render() self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -312,7 +321,7 @@ class TestCustomPaginateByParam(TestCase): """ If paginate_by_param is set, the new kwarg should limit per view requests. """ - request = factory.get('/?page_size=5') + request = factory.get('/', {'page_size': 5}) response = self.view(request).render() self.assertEqual(response.data['count'], 13) self.assertEqual(response.data['results'], self.data[:5]) @@ -340,7 +349,7 @@ class TestMaxPaginateByParam(TestCase): """ If max_paginate_by is set, it should limit page size for the view. """ - request = factory.get('/?page_size=10') + request = factory.get('/', data={'page_size': 10}) response = self.view(request).render() self.assertEqual(response.data['count'], 13) self.assertEqual(response.data['results'], self.data[:5]) From 73e5b7e4b21baf87129509610fffe474a9d4ecaa Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 30 Jan 2014 14:27:46 +0100 Subject: [PATCH 012/103] Fixed the object representation in order to pass the tests. --- rest_framework/tests/test_genericrelations.py | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/rest_framework/tests/test_genericrelations.py b/rest_framework/tests/test_genericrelations.py index 2d3413444..95810de72 100644 --- a/rest_framework/tests/test_genericrelations.py +++ b/rest_framework/tests/test_genericrelations.py @@ -5,7 +5,27 @@ from django.db import models from django.test import TestCase from rest_framework import serializers +try: + from django.utils.encoding import python_2_unicode_compatible +except ImportError: + def python_2_unicode_compatible(klass): + """ + A decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if '__str__' not in klass.__dict__: + raise ValueError("@python_2_unicode_compatible cannot be applied " + "to %s because it doesn't define __str__()." % + klass.__name__) + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass + + +@python_2_unicode_compatible class Tag(models.Model): """ Tags have a descriptive slug, and are attached to an arbitrary object. @@ -15,10 +35,11 @@ class Tag(models.Model): object_id = models.PositiveIntegerField() tagged_item = GenericForeignKey('content_type', 'object_id') - def __unicode__(self): + def __str__(self): return self.tag +@python_2_unicode_compatible class Bookmark(models.Model): """ A URL bookmark that may have multiple tags attached. @@ -26,10 +47,11 @@ class Bookmark(models.Model): url = models.URLField() tags = GenericRelation(Tag) - def __unicode__(self): + def __str__(self): return 'Bookmark: %s' % self.url +@python_2_unicode_compatible class Note(models.Model): """ A textual note that may have multiple tags attached. @@ -37,7 +59,7 @@ class Note(models.Model): text = models.TextField() tags = GenericRelation(Tag) - def __unicode__(self): + def __str__(self): return 'Note: %s' % self.text From ef742f2d46f4800490fb82122e8e809f1bbd8153 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 30 Jan 2014 17:18:03 +0100 Subject: [PATCH 013/103] Upgraded django-oauth-plus --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 017dd5816..001534b26 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,7 @@ install: - pip install $DJANGO - pip install defusedxml==0.3 Pillow - "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.2.1; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.2.4; 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" From 5a662dd60e79fd5efb84a5a8e7f7b00dd64e0bb0 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 31 Jan 2014 11:20:53 +0100 Subject: [PATCH 014/103] Moving models back to test_serializer. --- rest_framework/tests/models.py | 20 -------------------- rest_framework/tests/test_serializer.py | 22 +++++++++++++++++++++- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 19461e2a0..bf9883123 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -170,26 +170,6 @@ class BasicModelSerializer(serializers.ModelSerializer): model = BasicModel -# Models to test the serializers -class AMOAFModel(RESTFrameworkModel): - char_field = models.CharField(max_length=1024, blank=True) - comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True) - decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True) - email_field = models.EmailField(max_length=1024, blank=True) - file_field = models.FileField(upload_to='test', max_length=1024, blank=True) - image_field = models.ImageField(upload_to='test', max_length=1024, blank=True) - slug_field = models.SlugField(max_length=1024, blank=True) - url_field = models.URLField(max_length=1024, blank=True) - -class DVOAFModel(RESTFrameworkModel): - positive_integer_field = models.PositiveIntegerField(blank=True) - positive_small_integer_field = models.PositiveSmallIntegerField(blank=True) - email_field = models.EmailField(blank=True) - file_field = models.FileField(upload_to='test', blank=True) - image_field = models.ImageField(upload_to='test', blank=True) - slug_field = models.SlugField(blank=True) - url_field = models.URLField(blank=True) - # Models to test filters class FilterableItem(models.Model): text = models.CharField(max_length=100) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index af9fa68e0..dbbb9a8f5 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -8,12 +8,32 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers, fields, relations from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel, BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel, - ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel, AMOAFModel, DVOAFModel) + ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel) from rest_framework.tests.models import BasicModelSerializer import datetime import pickle +class AMOAFModel(RESTFrameworkModel): + char_field = models.CharField(max_length=1024, blank=True) + comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True) + decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True) + email_field = models.EmailField(max_length=1024, blank=True) + file_field = models.FileField(upload_to='test', max_length=1024, blank=True) + image_field = models.ImageField(upload_to='test', max_length=1024, blank=True) + slug_field = models.SlugField(max_length=1024, blank=True) + url_field = models.URLField(max_length=1024, blank=True) + +class DVOAFModel(RESTFrameworkModel): + positive_integer_field = models.PositiveIntegerField(blank=True) + positive_small_integer_field = models.PositiveSmallIntegerField(blank=True) + email_field = models.EmailField(blank=True) + file_field = models.FileField(upload_to='test', blank=True) + image_field = models.ImageField(upload_to='test', blank=True) + slug_field = models.SlugField(blank=True) + url_field = models.URLField(blank=True) + + class SubComment(object): def __init__(self, sub_comment): self.sub_comment = sub_comment From d48ba1cff76ffceb1d700e9e0c6ccf518a6382da Mon Sep 17 00:00:00 2001 From: Andrey Kaygorodov Date: Wed, 5 Feb 2014 05:47:27 +0800 Subject: [PATCH 015/103] turn of pagination --- docs/api-guide/pagination.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 0829589f8..f86e6ce11 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -102,7 +102,7 @@ You can also set the pagination style on a per-view basis, using the `ListAPIVie paginate_by_param = 'page_size' max_paginate_by = 100 -Note that using a `paginate_by` value of `None` will turn off pagination for the view. +Note that using a `paginate_by` value of `None` will turn off pagination for the view. But if you specified `PAGINATE_BY` and `PAGINATE_BY_PARAM` in your settings file then you have to set both `paginate_by` and `paginate_by_param` to a `None` value in order to turn off pagination for the view. For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods. From 2d20512d259f51a5a5c2b71b20f98d24e0176f16 Mon Sep 17 00:00:00 2001 From: Andrey Kaygorodov Date: Wed, 5 Feb 2014 21:10:51 +0800 Subject: [PATCH 016/103] #1390, docs, turning of pagination --- docs/api-guide/pagination.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index f86e6ce11..047a09883 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -102,7 +102,8 @@ You can also set the pagination style on a per-view basis, using the `ListAPIVie paginate_by_param = 'page_size' max_paginate_by = 100 -Note that using a `paginate_by` value of `None` will turn off pagination for the view. But if you specified `PAGINATE_BY` and `PAGINATE_BY_PARAM` in your settings file then you have to set both `paginate_by` and `paginate_by_param` to a `None` value in order to turn off pagination for the view. +Note that using a `paginate_by` value of `None` will turn off pagination for the view. +Note if you use the `PAGINATE_BY_PARAM` settings, you also have to set the `paginate_by_param` attribute in your view to `None` in order to turn off pagination for those requests that contain the `paginate_by_param` parameter. For more complex requirements such as serialization that differs depending on the requested media type you can override the `.get_paginate_by()` and `.get_pagination_serializer_class()` methods. From c61d311acbe20807e1d24e22fdac83c6301b5add Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 13 Feb 2014 12:34:50 +0100 Subject: [PATCH 017/103] Use 1.7a2 for django 1.7 line and added django-guardian PR for python 3.x or django 1.7 tests. --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 001534b26..01955e7de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: - "3.3" env: - - DJANGO="https://www.djangoproject.com/download/1.7a1/tarball/" + - DJANGO="https://www.djangoproject.com/download/1.7a2/tarball/" - DJANGO="django==1.6" - DJANGO="django==1.5.5" - DJANGO="django==1.4.10" @@ -22,6 +22,8 @@ install: - "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.7; fi" + - "if [[ ${TRAVIS_PYTHON_VERSION::1} == '3' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" + - "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7a2/tarball/'' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" - export PYTHONPATH=. script: From 57103587e0335dd2a01048fe11b44f81564c6c2a Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 13 Feb 2014 12:50:04 +0100 Subject: [PATCH 018/103] Fixed a typo. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 01955e7de..73b142300 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ install: - "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.7; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} == '3' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" - - "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7a2/tarball/'' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" + - "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7a2/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" - export PYTHONPATH=. script: From d49846025311d5b1679f656414eb685bede132c0 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 13 Feb 2014 13:10:48 +0100 Subject: [PATCH 019/103] Skip Python 2.6 build for Django 1.7 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 73b142300..146bf4cca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,7 +32,7 @@ script: matrix: exclude: - python: "2.6" - env: DJANGO="https://www.djangoproject.com/download/1.7a1/tarball/" + env: DJANGO="https://www.djangoproject.com/download/1.7a2/tarball/" - python: "3.2" env: DJANGO="django==1.4.10" - python: "3.2" From 6f4c2c6f0536bdf596534c295e411e17be14aab7 Mon Sep 17 00:00:00 2001 From: Bo Peng Date: Fri, 14 Feb 2014 13:47:06 -0600 Subject: [PATCH 020/103] Update throttling.md Added comma to make DEFAULT_THROTTLE_CLASSES a tuple in example, for copy&paste to work nicely. --- 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 fc1525df6..b7c320f01 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -150,7 +150,7 @@ For example, given the following views... REST_FRAMEWORK = { 'DEFAULT_THROTTLE_CLASSES': ( - 'rest_framework.throttling.ScopedRateThrottle' + 'rest_framework.throttling.ScopedRateThrottle', ), 'DEFAULT_THROTTLE_RATES': { 'contacts': '1000/day', From 5ae94547bc08ade94c3f1df2223c0b8261cae59f Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 18 Feb 2014 11:42:17 +0100 Subject: [PATCH 021/103] Moved the python_2_unicode_compatible into compat module. --- rest_framework/compat.py | 20 +++++++++++++++++++ rest_framework/tests/test_genericrelations.py | 20 +------------------ 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index b69749feb..36f5653a0 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -584,3 +584,23 @@ if six.PY3: else: def is_non_str_iterable(obj): return hasattr(obj, '__iter__') + + +try: + from django.utils.encoding import python_2_unicode_compatible +except ImportError: + def python_2_unicode_compatible(klass): + """ + A decorator that defines __unicode__ and __str__ methods under Python 2. + Under Python 3 it does nothing. + + To support Python 2 and 3 with a single code base, define a __str__ method + returning text and apply this decorator to the class. + """ + if '__str__' not in klass.__dict__: + raise ValueError("@python_2_unicode_compatible cannot be applied " + "to %s because it doesn't define __str__()." % + klass.__name__) + klass.__unicode__ = klass.__str__ + klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + return klass diff --git a/rest_framework/tests/test_genericrelations.py b/rest_framework/tests/test_genericrelations.py index 95810de72..fa09c9e6c 100644 --- a/rest_framework/tests/test_genericrelations.py +++ b/rest_framework/tests/test_genericrelations.py @@ -4,25 +4,7 @@ from django.contrib.contenttypes.generic import GenericRelation, GenericForeignK from django.db import models from django.test import TestCase from rest_framework import serializers - -try: - from django.utils.encoding import python_2_unicode_compatible -except ImportError: - def python_2_unicode_compatible(klass): - """ - A decorator that defines __unicode__ and __str__ methods under Python 2. - Under Python 3 it does nothing. - - To support Python 2 and 3 with a single code base, define a __str__ method - returning text and apply this decorator to the class. - """ - if '__str__' not in klass.__dict__: - raise ValueError("@python_2_unicode_compatible cannot be applied " - "to %s because it doesn't define __str__()." % - klass.__name__) - klass.__unicode__ = klass.__str__ - klass.__str__ = lambda self: self.__unicode__().encode('utf-8') - return klass +from rest_framework.compat import python_2_unicode_compatible @python_2_unicode_compatible From f22aeeb0a3e12f377b4523865b0c519f1d01f1b5 Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Tue, 18 Feb 2014 12:08:12 +0000 Subject: [PATCH 022/103] Refactor UpdateModelMixin.update Reduce nesting, return early in error cases. --- rest_framework/mixins.py | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 5fbcf700f..536303433 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -116,30 +116,27 @@ class UpdateModelMixin(object): partial = kwargs.pop('partial', False) self.object = self.get_object_or_none() - if self.object is None: - created = True - save_kwargs = {'force_insert': True} - success_status_code = status.HTTP_201_CREATED - else: - created = False - save_kwargs = {'force_update': True} - success_status_code = status.HTTP_200_OK - serializer = self.get_serializer(self.object, data=request.DATA, files=request.FILES, partial=partial) - if serializer.is_valid(): - 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) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + 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) + + if self.object is None: + self.object = serializer.save(force_insert=True) + self.post_save(self.object, created=True) + return Response(serializer.data, status=status.HTTP_201_CREATED) + else: + self.object = serializer.save(force_update=True) + self.post_save(self.object, created=False) + return Response(serializer.data, status=status.HTTP_200_OK) def partial_update(self, request, *args, **kwargs): kwargs['partial'] = True From dca8b983568e1ca7534ad7244e6eb57e1b87cc68 Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Tue, 18 Feb 2014 12:28:02 +0000 Subject: [PATCH 023/103] Remove unnecessary else --- rest_framework/mixins.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 536303433..7722d5bed 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -133,10 +133,10 @@ class UpdateModelMixin(object): self.object = serializer.save(force_insert=True) self.post_save(self.object, created=True) return Response(serializer.data, status=status.HTTP_201_CREATED) - else: - self.object = serializer.save(force_update=True) - self.post_save(self.object, created=False) - return Response(serializer.data, status=status.HTTP_200_OK) + + self.object = serializer.save(force_update=True) + self.post_save(self.object, created=False) + return Response(serializer.data, status=status.HTTP_200_OK) def partial_update(self, request, *args, **kwargs): kwargs['partial'] = True From d328f1827dcb53a92b11f3f146bad4db3ca83d89 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 18 Feb 2014 12:30:55 +0000 Subject: [PATCH 024/103] Tweak comment wrapping. --- rest_framework/mixins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 7722d5bed..e1a24dc7e 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -125,8 +125,8 @@ class UpdateModelMixin(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. + # 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) if self.object is None: From b3aa512d35b7f3e432bb41bf9de9ac7db7a1ed4c Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Date: Tue, 18 Feb 2014 20:39:39 +0530 Subject: [PATCH 025/103] fix(docs): fix code indentation in serializers.md It fixes the indentation of `restore_object()` in the section "Specifying which fields should be write-only" serializers documentation. --- docs/api-guide/serializers.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index e8369c20f..39fe61eb9 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -383,14 +383,14 @@ You may wish to specify multiple fields as write-only. Instead of adding each f fields = ('email', 'username', 'password') write_only_fields = ('password',) # Note: Password field is write-only - def restore_object(self, attrs, instance=None): - """ - Instantiate a new User instance. - """ - assert instance is None, 'Cannot update users with CreateUserSerializer' - user = User(email=attrs['email'], username=attrs['username']) - user.set_password(attrs['password']) - return user + def restore_object(self, attrs, instance=None): + """ + Instantiate a new User instance. + """ + assert instance is None, 'Cannot update users with CreateUserSerializer' + user = User(email=attrs['email'], username=attrs['username']) + user.set_password(attrs['password']) + return user ## Specifying fields explicitly From 1a4c6f075b33661e9b919c95575a5439aa924dcd Mon Sep 17 00:00:00 2001 From: Elvio Toccalino Date: Thu, 20 Feb 2014 01:07:39 -0300 Subject: [PATCH 026/103] include djangorestframework-httpsignature in the docs --- docs/api-guide/authentication.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index dc8e20995..f992b0ce8 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -393,6 +393,10 @@ The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is a JSON Web Token is a fairly new standard which can be used for token-based authentication. Unlike the built-in TokenAuthentication scheme, JWT Authentication doesn't need to use a database to validate a token. [Blimp][blimp] maintains the [djangorestframework-jwt][djangorestframework-jwt] package which provides a JWT Authentication class as well as a mechanism for clients to obtain a JWT given the username and password. +## HTTP Signature Authentication + +HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a way to achieve origin authentication and message integrity for HTTP messages. Similar to [Amazon's HTTP Signature scheme][amazon-http-signature], used by many of its services, it permits stateless, per-request authentication. [Elvio Toccalino][etoccalino] maintains the [djangorestframework-httpsignature][djangorestframework-httpsignature] package which provides an easy to use HTTP Signature Authentication mechanism. + [cite]: http://jacobian.org/writing/rest-worst-practices/ [http401]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.2 [http403]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4 @@ -419,3 +423,7 @@ JSON Web Token is a fairly new standard which can be used for token-based authen [doac-rest-framework]: https://github.com/Rediker-Software/doac/blob/master/docs/integrations.md# [blimp]: https://github.com/GetBlimp [djangorestframework-jwt]: https://github.com/GetBlimp/django-rest-framework-jwt +[etoccalino]: https://github.com/etoccalino/ +[djangorestframework-httpsignature]: https://github.com/etoccalino/django-rest-framework-httpsignature +[amazon-http-signature]: http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html +[http-signature-ietf-draft]: https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ From 98410693738aa81528bef2016a69e87560ae4ae3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 20 Feb 2014 14:54:17 +0000 Subject: [PATCH 027/103] Link to DRF-extensions. Refs #1430 --- docs/api-guide/pagination.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 0829589f8..efc4ae7f4 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -147,4 +147,14 @@ Alternatively, to set your custom pagination serializer on a per-view basis, use pagination_serializer_class = CustomPaginationSerializer paginate_by = 10 +# Third party packages + +The following third party packages are also available. + +## DRF-extensions + +The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin` mixin class][paginate-by-max-mixin] that allows your API clients to specify `?page_size=max` to obtain the maximum allowed page size. + [cite]: https://docs.djangoproject.com/en/dev/topics/pagination/ +[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/ +[paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin \ No newline at end of file From c3967c08ab48a8129b7d7fdc00175fd203aea6c8 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Fri, 21 Feb 2014 14:23:33 +0000 Subject: [PATCH 028/103] Minor docs example fix Dates are smaller when they happen before. --- 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 39fe61eb9..7ee060af4 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -161,7 +161,7 @@ To do any other validation that requires access to multiple fields, add a method """ Check that the start is before the stop. """ - if attrs['start'] < attrs['finish']: + if attrs['start'] > attrs['finish']: raise serializers.ValidationError("finish must occur after start") return attrs From 505f1173d0c5a20ea804cad3b503c4bf55cd04d8 Mon Sep 17 00:00:00 2001 From: Dustin Farris Date: Wed, 26 Feb 2014 12:08:26 -0500 Subject: [PATCH 029/103] Demonstrate problem post/put'ing serialized data that contains 'None' --- rest_framework/tests/serializers.py | 8 ++++++ rest_framework/tests/test_nullable_fields.py | 30 ++++++++++++++++++++ rest_framework/tests/views.py | 8 ++++++ 3 files changed, 46 insertions(+) create mode 100644 rest_framework/tests/serializers.py create mode 100644 rest_framework/tests/test_nullable_fields.py create mode 100644 rest_framework/tests/views.py diff --git a/rest_framework/tests/serializers.py b/rest_framework/tests/serializers.py new file mode 100644 index 000000000..cc943c7d0 --- /dev/null +++ b/rest_framework/tests/serializers.py @@ -0,0 +1,8 @@ +from rest_framework import serializers + +from rest_framework.tests.models import NullableForeignKeySource + + +class NullableFKSourceSerializer(serializers.ModelSerializer): + class Meta: + model = NullableForeignKeySource diff --git a/rest_framework/tests/test_nullable_fields.py b/rest_framework/tests/test_nullable_fields.py new file mode 100644 index 000000000..556543d99 --- /dev/null +++ b/rest_framework/tests/test_nullable_fields.py @@ -0,0 +1,30 @@ +from django.core.urlresolvers import reverse + +from rest_framework.compat import patterns, url +from rest_framework.test import APITestCase +from rest_framework.tests.models import NullableForeignKeySource +from rest_framework.tests.serializers import NullableFKSourceSerializer +from rest_framework.tests.views import NullableFKSourceDetail + + +urlpatterns = patterns( + '', + url(r'^objects/(?P\d+)/$', NullableFKSourceDetail.as_view(), name='object-detail'), +) + + +class NullableForeignKeyTests(APITestCase): + """ + DRF should be able to handle nullable fields when a TestClient + POST/PUT request is made with its own serialized object. + """ + urls = 'rest_framework.tests.test_nullable_fields' + + def test_updating_object_with_null_field_value(self): + obj = NullableForeignKeySource(name='example', target=None) + obj.save() + serialized_data = NullableFKSourceSerializer(obj).data + + response = self.client.put(reverse('object-detail', args=[obj.pk]), serialized_data) + + self.assertEqual(response.data, serialized_data) diff --git a/rest_framework/tests/views.py b/rest_framework/tests/views.py new file mode 100644 index 000000000..3917b74a9 --- /dev/null +++ b/rest_framework/tests/views.py @@ -0,0 +1,8 @@ +from rest_framework import generics +from rest_framework.tests.models import NullableForeignKeySource +from rest_framework.tests.serializers import NullableFKSourceSerializer + + +class NullableFKSourceDetail(generics.RetrieveUpdateDestroyAPIView): + model = NullableForeignKeySource + model_serializer_class = NullableFKSourceSerializer From 6cd0394e20c16828d14257a7360e9abef2c3e674 Mon Sep 17 00:00:00 2001 From: Keats Date: Fri, 21 Feb 2014 17:12:41 +0000 Subject: [PATCH 030/103] Display the media type of the API response on the browsable API --- rest_framework/renderers.py | 11 ++++++++++- rest_framework/templates/rest_framework/base.html | 2 +- rest_framework/tests/test_renderers.py | 12 ++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index e8afc26d7..7cf1c0518 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -427,7 +427,7 @@ class BrowsableAPIRenderer(BaseRenderer): files = request.FILES except ParseError: data = None - files = None + files = None else: data = None files = None @@ -544,6 +544,14 @@ class BrowsableAPIRenderer(BaseRenderer): 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 + response_headers = dict(response.items()) + renderer_content_type = '' + if renderer: + renderer_content_type = '%s' % renderer.media_type + if renderer.charset: + renderer_content_type += ' ;%s' % renderer.charset + response_headers['Content-Type'] = renderer_content_type + context = { 'content': self.get_content(renderer, data, accepted_media_type, renderer_context), 'view': view, @@ -555,6 +563,7 @@ class BrowsableAPIRenderer(BaseRenderer): 'breadcrumblist': self.get_breadcrumbs(request), 'allowed_methods': view.allowed_methods, 'available_formats': [renderer.format for renderer in view.renderer_classes], + 'response_headers': response_headers, 'put_form': self.get_rendered_html_form(view, 'PUT', request), 'post_form': self.get_rendered_html_form(view, 'POST', request), diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index d19d5a2be..7067ee2f0 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -118,7 +118,7 @@
HTTP {{ response.status_code }} {{ response.status_text }}{% autoescape off %} -{% for key, val in response.items %}{{ key }}: {{ val|break_long_headers|urlize_quoted_links }} +{% for key, val in response_headers.items %}{{ key }}: {{ val|break_long_headers|urlize_quoted_links }} {% endfor %}
{{ content|urlize_quoted_links }}
{% endautoescape %}
diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index fb33df2cf..0f3432c99 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -256,6 +256,18 @@ class RendererEndToEndTests(TestCase): self.assertEqual(resp.get('Content-Type', None), None) self.assertEqual(resp.status_code, status.HTTP_204_NO_CONTENT) + def test_contains_headers_of_api_response(self): + """ + Issue #1437 + + Test we display the headers of the API response and not those from the + HTML response + """ + resp = self.client.get('/html1') + self.assertContains(resp, '>GET, HEAD, OPTIONS<') + self.assertContains(resp, '>application/json<') + self.assertNotContains(resp, '>text/html; charset=utf-8<') + _flat_repr = '{"foo": ["bar", "baz"]}' _indented_repr = '{\n "foo": [\n "bar",\n "baz"\n ]\n}' From 1addd09e2b0e26507aada864123f610ead62d8da Mon Sep 17 00:00:00 2001 From: Anton Shutik Date: Thu, 27 Feb 2014 18:34:36 +0300 Subject: [PATCH 031/103] RelatedField default value handling fixed --- rest_framework/fields.py | 10 ++++++---- rest_framework/relations.py | 9 ++++++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 05daaab76..68b956822 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -301,6 +301,11 @@ class WritableField(Field): result.validators = self.validators[:] return result + def get_default_value(self): + if is_simple_callable(self.default): + return self.default() + return self.default + def validate(self, value): if value in validators.EMPTY_VALUES and self.required: raise ValidationError(self.error_messages['required']) @@ -349,10 +354,7 @@ class WritableField(Field): except KeyError: if self.default is not None and not self.partial: # Note: partial updates shouldn't set defaults - if is_simple_callable(self.default): - native = self.default() - else: - native = self.default + native = self.get_default_value() else: if self.required: raise ValidationError(self.error_messages['required']) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 02185c2ff..626454aca 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -118,6 +118,13 @@ class RelatedField(WritableField): choices = property(_get_choices, _set_choices) + ### Default value handling + + def get_default_value(self): + default = super(RelatedField, self).get_default_value() + return default or \ + [] if self.many else None + ### Regular serializer stuff... def field_to_native(self, obj, field_name): @@ -166,7 +173,7 @@ class RelatedField(WritableField): except KeyError: if self.partial: return - value = [] if self.many else None + value = self.get_default_value() if value in (None, '') and self.required: raise ValidationError(self.error_messages['required']) From 0620263d2423e0329c328301c2ceb1ba12024df5 Mon Sep 17 00:00:00 2001 From: Adam McKerlie Date: Thu, 27 Feb 2014 11:47:21 -0500 Subject: [PATCH 032/103] Update Travis to test Django 1.6.2 We should be testing the latest version of Django 1.6 as we're doing in the other 1.x releases. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 18fe66abd..2e6ed46a2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: - "3.3" env: - - DJANGO="django==1.6" + - DJANGO="django==1.6.2" - DJANGO="django==1.5.5" - DJANGO="django==1.4.10" - DJANGO="django==1.3.7" From f126856f65aa86de2c4cc1b1e3bb6a52cebb34b8 Mon Sep 17 00:00:00 2001 From: Dustin Farris Date: Thu, 27 Feb 2014 12:17:32 -0500 Subject: [PATCH 033/103] Allow 'None' to pass as a null value in RelatedFields --- rest_framework/relations.py | 7 ++++--- rest_framework/tests/test_nullable_fields.py | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 02185c2ff..163a89840 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -33,6 +33,7 @@ class RelatedField(WritableField): many_widget = widgets.SelectMultiple form_field_class = forms.ChoiceField many_form_field_class = forms.MultipleChoiceField + null_values = (None, '', 'None') cache_choices = False empty_label = None @@ -168,9 +169,9 @@ class RelatedField(WritableField): return value = [] if self.many else None - if value in (None, '') and self.required: - raise ValidationError(self.error_messages['required']) - elif value in (None, ''): + if value in self.null_values: + if self.required: + raise ValidationError(self.error_messages['required']) into[(self.source or field_name)] = None elif self.many: into[(self.source or field_name)] = [self.from_native(item) for item in value] diff --git a/rest_framework/tests/test_nullable_fields.py b/rest_framework/tests/test_nullable_fields.py index 556543d99..6ee55c005 100644 --- a/rest_framework/tests/test_nullable_fields.py +++ b/rest_framework/tests/test_nullable_fields.py @@ -15,12 +15,12 @@ urlpatterns = patterns( class NullableForeignKeyTests(APITestCase): """ - DRF should be able to handle nullable fields when a TestClient - POST/PUT request is made with its own serialized object. + DRF should be able to handle nullable foreign keys when a test + Client POST/PUT request is made with its own serialized object. """ urls = 'rest_framework.tests.test_nullable_fields' - def test_updating_object_with_null_field_value(self): + def test_updating_object_with_null_fk(self): obj = NullableForeignKeySource(name='example', target=None) obj.save() serialized_data = NullableFKSourceSerializer(obj).data From 818b4bf8b354d43360e3fd9d0b10440636a25212 Mon Sep 17 00:00:00 2001 From: Val Neekman Date: Thu, 27 Feb 2014 12:27:54 -0800 Subject: [PATCH 034/103] handle negative time value and prevent a divide by zero --- rest_framework/throttling.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index a946d837f..efa9fb949 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -136,6 +136,8 @@ class SimpleRateThrottle(BaseThrottle): remaining_duration = self.duration available_requests = self.num_requests - len(self.history) + 1 + if available_requests <= 0: + return None return remaining_duration / float(available_requests) From 3c62f0efc3cff7c1d7da9f13e0b0629d963069cb Mon Sep 17 00:00:00 2001 From: Anton Shutik Date: Fri, 28 Feb 2014 13:59:21 +0300 Subject: [PATCH 035/103] RelatedField.get_default_value: return empty list if self.many==True --- rest_framework/relations.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 626454aca..19dc9d6e5 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -122,8 +122,9 @@ class RelatedField(WritableField): def get_default_value(self): default = super(RelatedField, self).get_default_value() - return default or \ - [] if self.many else None + if self.many and default is None: + return [] + return default ### Regular serializer stuff... From abb240648c971af83c17cb6f77b274533d40c7f3 Mon Sep 17 00:00:00 2001 From: DanSears Date: Fri, 28 Feb 2014 08:40:45 -0800 Subject: [PATCH 036/103] clarified which urls.py to edit --- docs/tutorial/4-authentication-and-permissions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index bdc6b5791..432371f34 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -129,7 +129,7 @@ Then, add the following property to **both** the `SnippetList` and `SnippetDetai If you open a browser and navigate to the browsable API at the moment, you'll find that you're no longer able to create new code snippets. In order to do so we'd need to be able to login as a user. -We can add a login view for use with the browsable API, by editing our URLconf once more. +We can add a login view for use with the browsable API, by editing the URLconf in our project-level urls.py file. Add the following import at the top of the file: From f5f4c1a837f3ead760742bb8d783eaf78ce389e0 Mon Sep 17 00:00:00 2001 From: Kumar McMillan Date: Fri, 28 Feb 2014 16:34:42 -0600 Subject: [PATCH 037/103] strip trailing whitespace my editor does this by default --- docs/topics/contributing.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index 5a5d1a80b..0255c4846 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -14,7 +14,7 @@ If you use REST framework, we'd love you to be vocal about your experiences 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. +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 @@ -38,7 +38,7 @@ Some tips on good issue reporting: ## 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 +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 * 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? @@ -130,8 +130,8 @@ There are a couple of conventions you should follow when working on the document Headers should use the hash style. For example: ### Some important topic - -The underline style should not be used. **Don't do this:** + +The underline style should not be used. **Don't do this:** Some important topic ==================== @@ -141,9 +141,9 @@ The underline style should not be used. **Don't do this:** Links should always use the reference style, with the referenced hyperlinks kept at the end of the document. Here is a link to [some other thing][other-thing]. - + More text... - + [other-thing]: http://example.com/other/thing This style helps keep the documentation source consistent and readable. @@ -159,9 +159,9 @@ Linking in this style means you'll be able to click the hyperlink in your markdo If you want to draw attention to a note or warning, use a pair of enclosing lines, like so: --- - + **Note:** A useful documentation note. - + --- # Third party packages From e2857bdcffce612814e4cc35bef6f97ce5f39e77 Mon Sep 17 00:00:00 2001 From: Kumar McMillan Date: Fri, 28 Feb 2014 16:35:03 -0600 Subject: [PATCH 038/103] Fix link to tox --- 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 0255c4846..18a05050c 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -67,7 +67,7 @@ To run the tests, clone the repository, and then: # Run the tests rest_framework/runtests/runtests.py -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: +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: tox From a4470c42765004825ed6c09ae434dfd7dd969d1e Mon Sep 17 00:00:00 2001 From: Kumar McMillan Date: Fri, 28 Feb 2014 16:51:32 -0600 Subject: [PATCH 039/103] Fix trailing whitespace Sorry, my editor does this automatically --- docs/api-guide/authentication.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index f992b0ce8..59bea7336 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -93,7 +93,7 @@ Note that if deploying to [Apache using mod_wsgi][mod_wsgi_official], the author If you are deploying to Apache, and using any non-session based authentication, you will need to explicitly configure mod_wsgi to pass the required headers through to the application. This can be done by specifying the `WSGIPassAuthorization` directive in the appropriate context and setting it to `'On'`. - # this can go in either server config, virtual host, directory or .htaccess + # this can go in either server config, virtual host, directory or .htaccess WSGIPassAuthorization On --- @@ -117,7 +117,7 @@ Unauthenticated responses that are denied permission will result in an `HTTP 401 ## TokenAuthentication -This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients. +This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients. To use the `TokenAuthentication` scheme, include `rest_framework.authtoken` in your `INSTALLED_APPS` setting: @@ -125,7 +125,7 @@ To use the `TokenAuthentication` scheme, include `rest_framework.authtoken` in y ... 'rest_framework.authtoken' ) - + Make sure to run `manage.py syncdb` after changing your settings. The `authtoken` database tables are managed by south (see [Schema migrations](#schema-migrations) below). You'll also need to create tokens for your users. @@ -209,7 +209,7 @@ You can do so by inserting a `needed_by` attribute in your user migration: needed_by = ( ('authtoken', '0001_initial'), ) - + def forwards(self): ... @@ -282,7 +282,7 @@ Note that the `namespace='oauth2'` argument is required. Finally, sync your database. python manage.py syncdb - python manage.py migrate + python manage.py migrate --- @@ -368,7 +368,7 @@ The following example will authenticate any incoming request as the user given b user = User.objects.get(username=username) except User.DoesNotExist: raise exceptions.AuthenticationFailed('No such user') - + return (user, None) --- From c2cd610b15e97979be23e4c7e713028ef7f2e23a Mon Sep 17 00:00:00 2001 From: Kumar McMillan Date: Fri, 28 Feb 2014 16:51:58 -0600 Subject: [PATCH 040/103] Adds HawkREST to 3rd party auth docs --- docs/api-guide/authentication.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 59bea7336..88a7a0119 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -393,6 +393,10 @@ The [Django OAuth2 Consumer][doac] library from [Rediker Software][rediker] is a JSON Web Token is a fairly new standard which can be used for token-based authentication. Unlike the built-in TokenAuthentication scheme, JWT Authentication doesn't need to use a database to validate a token. [Blimp][blimp] maintains the [djangorestframework-jwt][djangorestframework-jwt] package which provides a JWT Authentication class as well as a mechanism for clients to obtain a JWT given the username and password. +## Hawk HTTP Authentication + +The [HawkREST][hawkrest] library builds on the [Mohawk][mohawk] library to let you work with [Hawk][hawk] signed requests and responses in your API. [Hawk][hawk] lets two parties securely communicate with each other using messages signed by a shared key. It is based on [HTTP MAC access authentication][mac] (which was based on parts of [OAuth 1.0][oauth-1.0a]). + ## HTTP Signature Authentication HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a way to achieve origin authentication and message integrity for HTTP messages. Similar to [Amazon's HTTP Signature scheme][amazon-http-signature], used by many of its services, it permits stateless, per-request authentication. [Elvio Toccalino][etoccalino] maintains the [djangorestframework-httpsignature][djangorestframework-httpsignature] package which provides an easy to use HTTP Signature Authentication mechanism. @@ -427,3 +431,7 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a [djangorestframework-httpsignature]: https://github.com/etoccalino/django-rest-framework-httpsignature [amazon-http-signature]: http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html [http-signature-ietf-draft]: https://datatracker.ietf.org/doc/draft-cavage-http-signatures/ +[hawkrest]: http://hawkrest.readthedocs.org/en/latest/ +[hawk]: https://github.com/hueniverse/hawk +[mohawk]: http://mohawk.readthedocs.org/en/latest/ +[mac]: http://tools.ietf.org/html/draft-hammer-oauth-v2-mac-token-05 From 693d9d9c630338379a1fb5271eb67eaddb60d525 Mon Sep 17 00:00:00 2001 From: hongfeiZhang Date: Sat, 1 Mar 2014 11:37:31 +0800 Subject: [PATCH 041/103] In the method permission_denied, did not use the request parameter. --- rest_framework/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 02a6e25a9..2cf9b220c 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -131,7 +131,7 @@ class APIView(View): """ If request is not permitted, determine what kind of exception to raise. """ - if not self.request.successful_authenticator: + if not request.successful_authenticator: raise exceptions.NotAuthenticated() raise exceptions.PermissionDenied() From dea2766abac5ef55fa226f413711cfd49af2a745 Mon Sep 17 00:00:00 2001 From: Anton Shutik Date: Tue, 4 Mar 2014 13:11:54 +0300 Subject: [PATCH 042/103] Added tests for "get_default_value" function --- rest_framework/tests/test_serializer.py | 52 +++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 6b1e333e4..a20137494 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -880,6 +880,58 @@ class DefaultValueTests(TestCase): self.assertEqual(instance.text, 'overridden') +class WritableFieldDefaultValueTests(TestCase): + + def setUp(self): + self.expected = {'default': 'value'} + self.create_field = fields.WritableField + + def test_get_default_value_with_noncallable(self): + field = self.create_field(default=self.expected) + got = field.get_default_value() + self.assertEqual(got, self.expected) + + def test_get_default_value_with_callable(self): + field = self.create_field(default=lambda : self.expected) + got = field.get_default_value() + self.assertEqual(got, self.expected) + + def test_get_default_value_when_not_required(self): + field = self.create_field(default=self.expected, required=False) + got = field.get_default_value() + self.assertEqual(got, self.expected) + + def test_get_default_value_returns_None(self): + field = self.create_field() + got = field.get_default_value() + self.assertIsNone(got) + + def test_get_default_value_returns_non_True_values(self): + values = [None, '', False, 0, [], (), {}] # values that assumed as 'False' in the 'if' clause + for expected in values: + field = self.create_field(default=expected) + got = field.get_default_value() + self.assertEqual(got, expected) + + +class RelatedFieldDefaultValueTests(WritableFieldDefaultValueTests): + + def setUp(self): + self.expected = {'foo': 'bar'} + self.create_field = relations.RelatedField + + def test_get_default_value_returns_empty_list(self): + field = self.create_field(many=True) + got = field.get_default_value() + self.assertListEqual(got, []) + + def test_get_default_value_returns_expected(self): + expected = [1, 2, 3] + field = self.create_field(many=True, default=expected) + got = field.get_default_value() + self.assertListEqual(got, expected) + + class CallableDefaultValueTests(TestCase): def setUp(self): class CallableDefaultValueSerializer(serializers.ModelSerializer): From 07cb436d610b0e9b6e60b84412c8d6552c0c55a0 Mon Sep 17 00:00:00 2001 From: David Larlet Date: Tue, 4 Mar 2014 16:32:34 +0100 Subject: [PATCH 043/103] Typo in keywords arguments name Because it matters ;) --- rest_framework/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 2cf9b220c..a2668f2c0 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -295,7 +295,7 @@ class APIView(View): # Dispatch methods - def initialize_request(self, request, *args, **kargs): + def initialize_request(self, request, *args, **kwargs): """ Returns the initial request object. """ From 84e7bf6796f0ab84f6797d0e9670b9bba4773e08 Mon Sep 17 00:00:00 2001 From: David Larlet Date: Tue, 4 Mar 2014 17:21:07 +0100 Subject: [PATCH 044/103] Update documentation of render methods (obj to data) --- rest_framework/renderers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 7cf1c0518..7a7da5610 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -146,7 +146,7 @@ class XMLRenderer(BaseRenderer): def render(self, data, accepted_media_type=None, renderer_context=None): """ - Renders *obj* into serialized XML. + Renders `data` into serialized XML. """ if data is None: return '' @@ -196,7 +196,7 @@ class YAMLRenderer(BaseRenderer): def render(self, data, accepted_media_type=None, renderer_context=None): """ - Renders *obj* into serialized YAML. + Renders `data` into serialized YAML. """ assert yaml, 'YAMLRenderer requires pyyaml to be installed' From 94fe03779b8e193a4cfd67be28ab9276e36f4179 Mon Sep 17 00:00:00 2001 From: Rodolfo Carvalho Date: Wed, 5 Mar 2014 17:01:54 +0100 Subject: [PATCH 045/103] Fix typo --- 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 10256d479..c95b0593f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -758,7 +758,7 @@ class ModelSerializer(Serializer): ret[accessor_name] = field - # Add the `read_only` flag to any fields that have bee specified + # Add the `read_only` flag to any fields that have been specified # in the `read_only_fields` option for field_name in self.opts.read_only_fields: assert field_name not in self.base_fields.keys(), ( From e0682e9298092721c0d3eb358ce4be8039e7ccf6 Mon Sep 17 00:00:00 2001 From: Eric Buehl Date: Wed, 5 Mar 2014 17:15:52 +0000 Subject: [PATCH 046/103] don't implicitly import provider.oauth2 --- rest_framework/authentication.py | 4 ++-- rest_framework/compat.py | 13 ++----------- rest_framework/permissions.py | 7 +++---- rest_framework/tests/test_authentication.py | 12 ++++++------ 4 files changed, 13 insertions(+), 23 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index e491ce5f9..b0e88d88b 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -326,11 +326,11 @@ class OAuth2Authentication(BaseAuthentication): """ try: - token = oauth2_provider.models.AccessToken.objects.select_related('user') + token = oauth2_provider.oauth2.models.AccessToken.objects.select_related('user') # provider_now switches to timezone aware datetime when # the oauth2_provider version supports to it. token = token.get(token=access_token, expires__gt=provider_now()) - except oauth2_provider.models.AccessToken.DoesNotExist: + except oauth2_provider.oauth2.models.AccessToken.DoesNotExist: raise exceptions.AuthenticationFailed('Invalid token') user = token.user diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 3089b7fbb..f60a180df 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -550,13 +550,8 @@ except (ImportError, ImproperlyConfigured): # OAuth 2 support is optional try: - import provider.oauth2 as oauth2_provider - from provider.oauth2 import models as oauth2_provider_models - from provider.oauth2 import forms as oauth2_provider_forms - from provider import scope as oauth2_provider_scope - from provider import constants as oauth2_constants - from provider import __version__ as provider_version - if provider_version in ('0.2.3', '0.2.4'): + import provider as oauth2_provider + if oauth2_provider.__version__ in ('0.2.3', '0.2.4'): # 0.2.3 and 0.2.4 are supported version that do not support # timezone aware datetimes import datetime @@ -566,10 +561,6 @@ try: from django.utils.timezone import now as provider_now except ImportError: oauth2_provider = None - oauth2_provider_models = None - oauth2_provider_forms = None - oauth2_provider_scope = None - oauth2_constants = None provider_now = None # Handle lazy strings diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index f24a51235..6460056af 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -8,8 +8,7 @@ import warnings SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS'] from django.http import Http404 -from rest_framework.compat import (get_model_name, oauth2_provider_scope, - oauth2_constants) +from rest_framework.compat import (get_model_name, oauth2_provider) class BasePermission(object): @@ -219,8 +218,8 @@ class TokenHasReadWriteScope(BasePermission): if hasattr(token, 'resource'): # OAuth 1 return read_only or not request.auth.resource.is_readonly elif hasattr(token, 'scope'): # OAuth 2 - required = oauth2_constants.READ if read_only else oauth2_constants.WRITE - return oauth2_provider_scope.check(required, request.auth.scope) + required = oauth2_provider.constants.READ if read_only else oauth2_provider.constants.WRITE + return oauth2_provider.scope.check(required, request.auth.scope) assert False, ('TokenHasReadWriteScope requires either the' '`OAuthAuthentication` or `OAuth2Authentication` authentication ' diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index f072b81b7..90383eefd 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -19,7 +19,7 @@ from rest_framework.authentication import ( ) from rest_framework.authtoken.models import Token from rest_framework.compat import patterns, url, include -from rest_framework.compat import oauth2_provider, oauth2_provider_models, oauth2_provider_scope +from rest_framework.compat import oauth2_provider from rest_framework.compat import oauth, oauth_provider from rest_framework.test import APIRequestFactory, APIClient from rest_framework.views import APIView @@ -488,7 +488,7 @@ class OAuth2Tests(TestCase): self.ACCESS_TOKEN = "access_token" self.REFRESH_TOKEN = "refresh_token" - self.oauth2_client = oauth2_provider_models.Client.objects.create( + self.oauth2_client = oauth2_provider.oauth2.models.Client.objects.create( client_id=self.CLIENT_ID, client_secret=self.CLIENT_SECRET, redirect_uri='', @@ -497,12 +497,12 @@ class OAuth2Tests(TestCase): user=None, ) - self.access_token = oauth2_provider_models.AccessToken.objects.create( + self.access_token = oauth2_provider.oauth2.models.AccessToken.objects.create( token=self.ACCESS_TOKEN, client=self.oauth2_client, user=self.user, ) - self.refresh_token = oauth2_provider_models.RefreshToken.objects.create( + self.refresh_token = oauth2_provider.oauth2.models.RefreshToken.objects.create( user=self.user, access_token=self.access_token, client=self.oauth2_client @@ -581,7 +581,7 @@ class OAuth2Tests(TestCase): def test_post_form_with_invalid_scope_failing_auth(self): """Ensure POSTing with a readonly scope instead of a write scope fails""" read_only_access_token = self.access_token - read_only_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['read'] + read_only_access_token.scope = oauth2_provider.scope.SCOPE_NAME_DICT['read'] read_only_access_token.save() auth = self._create_authorization_header(token=read_only_access_token.token) response = self.csrf_client.get('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth) @@ -593,7 +593,7 @@ class OAuth2Tests(TestCase): def test_post_form_with_valid_scope_passing_auth(self): """Ensure POSTing with a write scope succeed""" read_write_access_token = self.access_token - read_write_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['write'] + read_write_access_token.scope = oauth2_provider.scope.SCOPE_NAME_DICT['write'] read_write_access_token.save() auth = self._create_authorization_header(token=read_write_access_token.token) response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth) From c1148241eee3df1139f9855ee3220c82f60726d5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 6 Mar 2014 09:01:05 +0000 Subject: [PATCH 047/103] Version 2.3.13 --- docs/topics/release-notes.md | 11 +++++++++++ rest_framework/__init__.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 14503148c..0010f6878 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,17 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series +### 2.3.13 + +**Date**: 6th March 2014 + +* Django 1.7 Support. +* Fix `default` argument when used with serializer relation fields. +* Display the media type of the content that is being displayed in the browsable API, rather than 'text/html'. +* Bugfix for `urlize` template failure when URL regex is matched, but value does not `urlparse`. +* Use `urandom` for token generation. +* Only use `Vary: Accept` when more than one renderer exists. + ### 2.3.12 **Date**: 15th January 2014 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 6759680b7..d6689a871 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ _ """ __title__ = 'Django REST framework' -__version__ = '2.3.12' +__version__ = '2.3.13' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2013 Tom Christie' From ef94861c2d31592c3760a0c0758beb084f452c03 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 6 Mar 2014 09:25:18 +0000 Subject: [PATCH 048/103] It's 2014 now, dontchaknow --- 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 d6689a871..2d76b55d5 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -11,7 +11,7 @@ __title__ = 'Django REST framework' __version__ = '2.3.13' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' -__copyright__ = 'Copyright 2011-2013 Tom Christie' +__copyright__ = 'Copyright 2011-2014 Tom Christie' # Version synonym VERSION = __version__ From 9e291879d1705dea18131fc66be31e422afa1e62 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 6 Mar 2014 15:24:07 +0100 Subject: [PATCH 049/103] Added an optional unique field to Album and checked that duplicates are detected. --- rest_framework/tests/models.py | 2 +- rest_framework/tests/test_serializer.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index bf9883123..6c8f2342b 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -103,7 +103,7 @@ class BlogPostComment(RESTFrameworkModel): class Album(RESTFrameworkModel): title = models.CharField(max_length=100, unique=True) - + ref = models.CharField(max_length=10, unique=True, null=True, blank=True) class Photo(RESTFrameworkModel): description = models.TextField() diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 198c269f0..17ef191a1 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -611,12 +611,15 @@ class ModelValidationTests(TestCase): """ Just check if serializers.ModelSerializer handles unique checks via .full_clean() """ - serializer = AlbumsSerializer(data={'title': 'a'}) + serializer = AlbumsSerializer(data={'title': 'a', 'ref': '1'}) serializer.is_valid() serializer.save() second_serializer = AlbumsSerializer(data={'title': 'a'}) self.assertFalse(second_serializer.is_valid()) - self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.']}) + self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.'],}) + third_serializer = AlbumsSerializer(data={'title': 'b', 'ref': '1'}) + self.assertFalse(third_serializer.is_valid()) + self.assertEqual(third_serializer.errors, {'ref': ['Album with this Ref already exists.'],}) def test_foreign_key_is_null_with_partial(self): """ From de899824b8352912d2a0d2fa030b8e5a053a3ae5 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 6 Mar 2014 16:43:30 +0100 Subject: [PATCH 050/103] Forgot to add the ref field to the field list. --- 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 17ef191a1..56714d1e3 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -161,7 +161,7 @@ class AlbumsSerializer(serializers.ModelSerializer): class Meta: model = Album - fields = ['title'] # lists are also valid options + fields = ['title', 'ref'] # lists are also valid options class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer): From caf4d36cb3484313a36453a229bfc002a075f811 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 6 Mar 2014 21:17:41 +0100 Subject: [PATCH 051/103] More complex test case. --- rest_framework/tests/test_serializer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 56714d1e3..441630113 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -617,9 +617,9 @@ class ModelValidationTests(TestCase): second_serializer = AlbumsSerializer(data={'title': 'a'}) self.assertFalse(second_serializer.is_valid()) self.assertEqual(second_serializer.errors, {'title': ['Album with this Title already exists.'],}) - third_serializer = AlbumsSerializer(data={'title': 'b', 'ref': '1'}) + third_serializer = AlbumsSerializer(data=[{'title': 'b', 'ref': '1'}, {'title': 'c'}]) self.assertFalse(third_serializer.is_valid()) - self.assertEqual(third_serializer.errors, {'ref': ['Album with this Ref already exists.'],}) + self.assertEqual(third_serializer.errors, [{'ref': ['Album with this Ref already exists.']}, {}]) def test_foreign_key_is_null_with_partial(self): """ From 51e6982397cc032d6b3fd66f452713d448eb9084 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 6 Mar 2014 21:18:37 +0100 Subject: [PATCH 052/103] Fixed the validation for optional fields that have a value. --- rest_framework/serializers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index c95b0593f..5c726dfcd 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -881,7 +881,7 @@ class ModelSerializer(Serializer): except KeyError: return ModelField(model_field=model_field, **kwargs) - def get_validation_exclusions(self): + def get_validation_exclusions(self, instance=None): """ Return a list of field names to exclude from model validation. """ @@ -893,7 +893,7 @@ class ModelSerializer(Serializer): field_name = field.source or field_name if field_name in exclusions \ and not field.read_only \ - and field.required \ + and (field.required or hasattr(instance, field_name)) \ and not isinstance(field, Serializer): exclusions.remove(field_name) return exclusions @@ -908,7 +908,7 @@ class ModelSerializer(Serializer): the full_clean validation checking. """ try: - instance.full_clean(exclude=self.get_validation_exclusions()) + instance.full_clean(exclude=self.get_validation_exclusions(instance)) except ValidationError as err: self._errors = err.message_dict return None From 34887ed75625a58d00c986b3ea5526877f4724b2 Mon Sep 17 00:00:00 2001 From: Eric Buehl Date: Thu, 6 Mar 2014 20:19:21 +0000 Subject: [PATCH 053/103] it's safe to import scope and constants --- rest_framework/compat.py | 4 ++++ rest_framework/permissions.py | 7 ++++--- rest_framework/tests/test_authentication.py | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index f60a180df..d155f5542 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -551,6 +551,8 @@ except (ImportError, ImproperlyConfigured): # OAuth 2 support is optional try: import provider as oauth2_provider + from provider import scope as oauth2_provider_scope + from provider import constants as oauth2_constants if oauth2_provider.__version__ in ('0.2.3', '0.2.4'): # 0.2.3 and 0.2.4 are supported version that do not support # timezone aware datetimes @@ -561,6 +563,8 @@ try: from django.utils.timezone import now as provider_now except ImportError: oauth2_provider = None + oauth2_provider_scope = None + oauth2_constants = None provider_now = None # Handle lazy strings diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 6460056af..f24a51235 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 (get_model_name, oauth2_provider) +from rest_framework.compat import (get_model_name, oauth2_provider_scope, + oauth2_constants) class BasePermission(object): @@ -218,8 +219,8 @@ class TokenHasReadWriteScope(BasePermission): if hasattr(token, 'resource'): # OAuth 1 return read_only or not request.auth.resource.is_readonly elif hasattr(token, 'scope'): # OAuth 2 - required = oauth2_provider.constants.READ if read_only else oauth2_provider.constants.WRITE - return oauth2_provider.scope.check(required, request.auth.scope) + required = oauth2_constants.READ if read_only else oauth2_constants.WRITE + return oauth2_provider_scope.check(required, request.auth.scope) assert False, ('TokenHasReadWriteScope requires either the' '`OAuthAuthentication` or `OAuth2Authentication` authentication ' diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index 90383eefd..8caeb0812 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -19,7 +19,7 @@ from rest_framework.authentication import ( ) from rest_framework.authtoken.models import Token from rest_framework.compat import patterns, url, include -from rest_framework.compat import oauth2_provider +from rest_framework.compat import oauth2_provider, oauth2_provider_scope from rest_framework.compat import oauth, oauth_provider from rest_framework.test import APIRequestFactory, APIClient from rest_framework.views import APIView @@ -581,7 +581,7 @@ class OAuth2Tests(TestCase): def test_post_form_with_invalid_scope_failing_auth(self): """Ensure POSTing with a readonly scope instead of a write scope fails""" read_only_access_token = self.access_token - read_only_access_token.scope = oauth2_provider.scope.SCOPE_NAME_DICT['read'] + read_only_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['read'] read_only_access_token.save() auth = self._create_authorization_header(token=read_only_access_token.token) response = self.csrf_client.get('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth) @@ -593,7 +593,7 @@ class OAuth2Tests(TestCase): def test_post_form_with_valid_scope_passing_auth(self): """Ensure POSTing with a write scope succeed""" read_write_access_token = self.access_token - read_write_access_token.scope = oauth2_provider.scope.SCOPE_NAME_DICT['write'] + read_write_access_token.scope = oauth2_provider_scope.SCOPE_NAME_DICT['write'] read_write_access_token.save() auth = self._create_authorization_header(token=read_write_access_token.token) response = self.csrf_client.post('/oauth2-with-scope-test/', HTTP_AUTHORIZATION=auth) From 2353878951b0607a95d539c27c362d0353c53119 Mon Sep 17 00:00:00 2001 From: Peter Inglesby Date: Thu, 6 Mar 2014 21:39:44 +0000 Subject: [PATCH 054/103] Add SEARCH_PARAM and ORDERING_PARAM to settings Fixes #1434 --- docs/api-guide/filtering.md | 6 +++- docs/api-guide/settings.md | 12 ++++++++ rest_framework/filters.py | 7 +++-- rest_framework/settings.py | 4 +++ rest_framework/tests/test_filters.py | 42 +++++++++++++++++++++++++++- rest_framework/tests/utils.py | 24 ++++++++++++++++ 6 files changed, 91 insertions(+), 4 deletions(-) create mode 100644 rest_framework/tests/utils.py diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 07420d842..d6c4b1c1b 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -264,13 +264,17 @@ For example: search_fields = ('=username', '=email') +By default, the search parameter is named `'search`', but this may be overridden with the `SEARCH_PARAM` setting. + For more details, see the [Django documentation][search-django-admin]. --- ## OrderingFilter -The `OrderingFilter` class supports simple query parameter controlled ordering of results. To specify the result order, set a query parameter named `'ordering'` to the required field name. For example: +The `OrderingFilter` class supports simple query parameter controlled ordering of results. By default, the query parameter is named `'ordering'`, but this may by overridden with the `ORDERING_PARAM` setting. + +For example, to order users by username: http://example.com/api/users?ordering=username diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 5aee52aa1..c979019f8 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -158,6 +158,18 @@ A client request like the following would return a paginated list of up to 100 i Default: `None` +### SEARCH_PARAM + +The name of a query paramater, which can be used to specify the search term used by `SearchFilter`. + +Default: `search` + +#### ORDERING_PARAM + +The name of a query paramater, which can be used to specify the ordering of results returned by `OrderingFilter`. + +Default: `ordering` + --- ## Authentication settings diff --git a/rest_framework/filters.py b/rest_framework/filters.py index de91caedc..96d15eb9d 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals from django.core.exceptions import ImproperlyConfigured from django.db import models from rest_framework.compat import django_filters, six, guardian, get_model_name +from rest_framework.settings import api_settings from functools import reduce import operator @@ -69,7 +70,8 @@ class DjangoFilterBackend(BaseFilterBackend): class SearchFilter(BaseFilterBackend): - search_param = 'search' # The URL query parameter used for the search. + # The URL query parameter used for the search. + search_param = api_settings.SEARCH_PARAM def get_search_terms(self, request): """ @@ -107,7 +109,8 @@ class SearchFilter(BaseFilterBackend): class OrderingFilter(BaseFilterBackend): - ordering_param = 'ordering' # The URL query parameter used for the ordering. + # The URL query parameter used for the ordering. + ordering_param = api_settings.ORDERING_PARAM ordering_fields = None def get_ordering(self, request): diff --git a/rest_framework/settings.py b/rest_framework/settings.py index ce171d6d4..38753c968 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -69,6 +69,10 @@ DEFAULTS = { 'PAGINATE_BY_PARAM': None, 'MAX_PAGINATE_BY': None, + # Filtering + 'SEARCH_PARAM': 'search', + 'ORDERING_PARAM': 'ordering', + # Authentication 'UNAUTHENTICATED_USER': 'django.contrib.auth.models.AnonymousUser', 'UNAUTHENTICATED_TOKEN': None, diff --git a/rest_framework/tests/test_filters.py b/rest_framework/tests/test_filters.py index dd5d8e428..23226bbcf 100644 --- a/rest_framework/tests/test_filters.py +++ b/rest_framework/tests/test_filters.py @@ -7,9 +7,11 @@ from django.test import TestCase from django.utils import unittest from rest_framework import generics, serializers, status, filters from rest_framework.compat import django_filters, patterns, url +from rest_framework.settings import api_settings from rest_framework.test import APIRequestFactory from rest_framework.tests.models import BasicModel from .models import FilterableItem +from .utils import temporary_setting factory = APIRequestFactory() @@ -363,6 +365,24 @@ class SearchFilterTests(TestCase): ] ) + def test_search_with_nonstandard_search_param(self): + with temporary_setting('SEARCH_PARAM', 'query', module=filters): + class SearchListView(generics.ListAPIView): + model = SearchFilterModel + filter_backends = (filters.SearchFilter,) + search_fields = ('title', 'text') + + view = SearchListView.as_view() + request = factory.get('/', {'query': 'b'}) + response = view(request) + self.assertEqual( + response.data, + [ + {'id': 1, 'title': 'z', 'text': 'abc'}, + {'id': 2, 'title': 'zz', 'text': 'bcd'} + ] + ) + class OrdringFilterModel(models.Model): title = models.CharField(max_length=20) @@ -520,6 +540,26 @@ class OrderingFilterTests(TestCase): ] ) + def test_ordering_with_nonstandard_ordering_param(self): + with temporary_setting('ORDERING_PARAM', 'order', filters): + class OrderingListView(generics.ListAPIView): + model = OrdringFilterModel + filter_backends = (filters.OrderingFilter,) + ordering = ('title',) + ordering_fields = ('text',) + + view = OrderingListView.as_view() + request = factory.get('/', {'order': 'text'}) + response = view(request) + self.assertEqual( + response.data, + [ + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + ] + ) + class SensitiveOrderingFilterModel(models.Model): username = models.CharField(max_length=20) @@ -618,4 +658,4 @@ class SensitiveOrderingFilterTests(TestCase): {'id': 2, username_field: 'userB'}, # PassC {'id': 3, username_field: 'userC'}, # PassA ] - ) \ No newline at end of file + ) diff --git a/rest_framework/tests/utils.py b/rest_framework/tests/utils.py new file mode 100644 index 000000000..ee157824b --- /dev/null +++ b/rest_framework/tests/utils.py @@ -0,0 +1,24 @@ +from contextlib import contextmanager +from rest_framework.settings import api_settings + + +@contextmanager +def temporary_setting(setting, value, module=None): + """ + Temporarily change value of setting for test. + + Optionally reload given module, useful when module uses value of setting on + import. + """ + original_value = getattr(api_settings, setting) + setattr(api_settings, setting, value) + + if module is not None: + reload(module) + + yield + + setattr(api_settings, setting, original_value) + + if module is not None: + reload(module) From 29f5ce7aeb57abde3924527f63bb761e0c2342d3 Mon Sep 17 00:00:00 2001 From: Peter Inglesby Date: Thu, 6 Mar 2014 23:51:02 +0000 Subject: [PATCH 055/103] Use six to reload module --- rest_framework/tests/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/utils.py b/rest_framework/tests/utils.py index ee157824b..a8f2eb0b0 100644 --- a/rest_framework/tests/utils.py +++ b/rest_framework/tests/utils.py @@ -1,4 +1,5 @@ from contextlib import contextmanager +from rest_framework.compat import six from rest_framework.settings import api_settings @@ -14,11 +15,11 @@ def temporary_setting(setting, value, module=None): setattr(api_settings, setting, value) if module is not None: - reload(module) + six.moves.reload_module(module) yield setattr(api_settings, setting, original_value) if module is not None: - reload(module) + six.moves.reload_module(module) From 4001cd74ed46dd8bf56518e2e8f64e6152d4d480 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 7 Mar 2014 14:15:56 +0000 Subject: [PATCH 056/103] Pin pillow to 2.3.0 --- .travis.yml | 2 +- optionals.txt | 1 + tox.ini | 11 +++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c165d8d5a..f6b4753d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,7 @@ env: install: - pip install $DJANGO - - pip install defusedxml==0.3 Pillow + - pip install defusedxml==0.3 Pillow==2.3.0 - "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.2.4; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4; fi" diff --git a/optionals.txt b/optionals.txt index 96f4b2f44..262e76443 100644 --- a/optionals.txt +++ b/optionals.txt @@ -5,3 +5,4 @@ django-filter>=0.5.4 django-oauth-plus>=2.2.1 oauth2>=1.5.211 django-oauth2-provider>=0.2.4 +Pillow==2.3.0 diff --git a/tox.ini b/tox.ini index 77766d20b..d25e3140a 100644 --- a/tox.ini +++ b/tox.ini @@ -10,12 +10,14 @@ basepython = python3.3 deps = Django==1.6 django-filter==0.6a1 defusedxml==0.3 + Pillow==2.3.0 [testenv:py3.2-django1.6] basepython = python3.2 deps = Django==1.6 django-filter==0.6a1 defusedxml==0.3 + Pillow==2.3.0 [testenv:py2.7-django1.6] basepython = python2.7 @@ -26,6 +28,7 @@ deps = Django==1.6 oauth2==1.5.211 django-oauth2-provider==0.2.4 django-guardian==1.1.1 + Pillow==2.3.0 [testenv:py2.6-django1.6] basepython = python2.6 @@ -36,18 +39,21 @@ deps = Django==1.6 oauth2==1.5.211 django-oauth2-provider==0.2.4 django-guardian==1.1.1 + Pillow==2.3.0 [testenv:py3.3-django1.5] basepython = python3.3 deps = django==1.5.5 django-filter==0.6a1 defusedxml==0.3 + Pillow==2.3.0 [testenv:py3.2-django1.5] basepython = python3.2 deps = django==1.5.5 django-filter==0.6a1 defusedxml==0.3 + Pillow==2.3.0 [testenv:py2.7-django1.5] basepython = python2.7 @@ -58,6 +64,7 @@ deps = django==1.5.5 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 + Pillow==2.3.0 [testenv:py2.6-django1.5] basepython = python2.6 @@ -68,6 +75,7 @@ deps = django==1.5.5 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 + Pillow==2.3.0 [testenv:py2.7-django1.4] basepython = python2.7 @@ -78,6 +86,7 @@ deps = django==1.4.10 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 + Pillow==2.3.0 [testenv:py2.6-django1.4] basepython = python2.6 @@ -88,6 +97,7 @@ deps = django==1.4.10 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 + Pillow==2.3.0 [testenv:py2.7-django1.3] basepython = python2.7 @@ -98,6 +108,7 @@ deps = django==1.3.5 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 + Pillow==2.3.0 [testenv:py2.6-django1.3] basepython = python2.6 From 3fa95132d8fced3e45f5175912672163cb71933b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 7 Mar 2014 14:16:14 +0000 Subject: [PATCH 057/103] Don't barf if PIL is not installed. --- rest_framework/tests/test_serializer.py | 42 +++++++++++++++---------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 441630113..85a899c53 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from django.db import models from django.db.models.fields import BLANK_CHOICE_DASH from django.test import TestCase +from django.utils import unittest from django.utils.datastructures import MultiValueDict from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers, fields, relations @@ -12,26 +13,31 @@ from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, Acti from rest_framework.tests.models import BasicModelSerializer import datetime import pickle +try: + import PIL +except: + PIL = None -class AMOAFModel(RESTFrameworkModel): - char_field = models.CharField(max_length=1024, blank=True) - comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True) - decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True) - email_field = models.EmailField(max_length=1024, blank=True) - file_field = models.FileField(upload_to='test', max_length=1024, blank=True) - image_field = models.ImageField(upload_to='test', max_length=1024, blank=True) - slug_field = models.SlugField(max_length=1024, blank=True) - url_field = models.URLField(max_length=1024, blank=True) +if PIL is not None: + class AMOAFModel(RESTFrameworkModel): + char_field = models.CharField(max_length=1024, blank=True) + comma_separated_integer_field = models.CommaSeparatedIntegerField(max_length=1024, blank=True) + decimal_field = models.DecimalField(max_digits=64, decimal_places=32, blank=True) + email_field = models.EmailField(max_length=1024, blank=True) + file_field = models.FileField(upload_to='test', max_length=1024, blank=True) + image_field = models.ImageField(upload_to='test', max_length=1024, blank=True) + slug_field = models.SlugField(max_length=1024, blank=True) + url_field = models.URLField(max_length=1024, blank=True) -class DVOAFModel(RESTFrameworkModel): - positive_integer_field = models.PositiveIntegerField(blank=True) - positive_small_integer_field = models.PositiveSmallIntegerField(blank=True) - email_field = models.EmailField(blank=True) - file_field = models.FileField(upload_to='test', blank=True) - image_field = models.ImageField(upload_to='test', blank=True) - slug_field = models.SlugField(blank=True) - url_field = models.URLField(blank=True) + class DVOAFModel(RESTFrameworkModel): + positive_integer_field = models.PositiveIntegerField(blank=True) + positive_small_integer_field = models.PositiveSmallIntegerField(blank=True) + email_field = models.EmailField(blank=True) + file_field = models.FileField(upload_to='test', blank=True) + image_field = models.ImageField(upload_to='test', blank=True) + slug_field = models.SlugField(blank=True) + url_field = models.URLField(blank=True) class SubComment(object): @@ -1568,6 +1574,7 @@ class ManyFieldHelpTextTest(TestCase): self.assertEqual('Some help text.', rel_field.help_text) +@unittest.skipUnless(PIL is not None, 'PIL is not installed') class AttributeMappingOnAutogeneratedFieldsTests(TestCase): def setUp(self): @@ -1640,6 +1647,7 @@ class AttributeMappingOnAutogeneratedFieldsTests(TestCase): self.field_test('url_field') +@unittest.skipUnless(PIL is not None, 'PIL is not installed') class DefaultValuesOnAutogeneratedFieldsTests(TestCase): def setUp(self): From 6cf19fa4ef1942c84546322aedd371ba87c0ed5f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 7 Mar 2014 14:16:31 +0000 Subject: [PATCH 058/103] Add Django 1.7 to tox --- tox.ini | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index d25e3140a..47fdf67ff 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,35 @@ [tox] downloadcache = {toxworkdir}/cache/ -envlist = py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,py2.7-django1.4,py2.6-django1.4,py2.7-django1.3,py2.6-django1.3 +envlist = py3.3-django1.7,py3.2-django1.7,py2.7-django1.7,py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6,py3.3-django1.5,py3.2-django1.5,py2.7-django1.5,py2.6-django1.5,py2.7-django1.4,py2.6-django1.4,py2.7-django1.3,py2.6-django1.3 [testenv] commands = {envpython} rest_framework/runtests/runtests.py +[testenv:py3.3-django1.7] +basepython = python3.3 +deps = https://www.djangoproject.com/download/1.7a2/tarball/ + django-filter==0.6a1 + defusedxml==0.3 + Pillow==2.3.0 + +[testenv:py3.2-django1.7] +basepython = python3.2 +deps = https://www.djangoproject.com/download/1.7a2/tarball/ + django-filter==0.6a1 + defusedxml==0.3 + Pillow==2.3.0 + +[testenv:py2.7-django1.7] +basepython = python2.7 +deps = https://www.djangoproject.com/download/1.7a2/tarball/ + django-filter==0.6a1 + defusedxml==0.3 + django-oauth-plus==2.2.1 + oauth2==1.5.211 + django-oauth2-provider==0.2.4 + django-guardian==1.1.1 + Pillow==2.3.0 + [testenv:py3.3-django1.6] basepython = python3.3 deps = Django==1.6 @@ -119,3 +144,4 @@ deps = django==1.3.5 oauth2==1.5.211 django-oauth2-provider==0.2.3 django-guardian==1.1.1 + Pillow==2.3.0 From 0e677e9dd178ae7a0250829729a666b54f4eac61 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 7 Mar 2014 16:11:51 +0100 Subject: [PATCH 059/103] Reintroduced url arguments in the urls for the tests. --- rest_framework/test.py | 4 ++++ rest_framework/tests/test_testing.py | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/rest_framework/test.py b/rest_framework/test.py index 75cb4d0b9..df5a5b3b3 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -76,6 +76,10 @@ class APIRequestFactory(DjangoRequestFactory): r = { 'QUERY_STRING': urlencode(data or {}, doseq=True), } + # Fix to support old behavior where you have the arguments in the url + # See #1461 + if not data and '?' in path: + r['QUERY_STRING'] = path.split('?')[1] r.update(extra) return self.generic('GET', path, **r) diff --git a/rest_framework/tests/test_testing.py b/rest_framework/tests/test_testing.py index 71bd8b552..a55d4b225 100644 --- a/rest_framework/tests/test_testing.py +++ b/rest_framework/tests/test_testing.py @@ -152,3 +152,13 @@ class TestAPIRequestFactory(TestCase): simple_png.name = 'test.png' factory = APIRequestFactory() factory.post('/', data={'image': simple_png}) + + def test_request_factory_url_arguments(self): + """ + This is a non regression test against #1461 + """ + factory = APIRequestFactory() + request = factory.get('/view/?demo=test') + self.assertEqual(dict(request.GET), {'demo': ['test']}) + request = factory.get('/view/', {'demo': 'test'}) + self.assertEqual(dict(request.GET), {'demo': ['test']}) From c779dce3e4bba8fc453e0abe51f6fb5b2f005721 Mon Sep 17 00:00:00 2001 From: Steven Cummings Date: Sun, 16 Mar 2014 18:55:21 -0500 Subject: [PATCH 060/103] Serializer fields section for 3rd-party packages * Add new section to serializer fields page where we can list and link 3rd-party packages that provide more field types * Add an entry for drf-compound-fields --- docs/api-guide/fields.md | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 93f992e66..89606798c 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -2,7 +2,7 @@ # Serializer fields -> Each field in a Form class is responsible not only for validating data, but also for "cleaning" it — normalizing it to a consistent format. +> Each field in a Form class is responsible not only for validating data, but also for "cleaning" it — normalizing it to a consistent format. > > — [Django documentation][cite] @@ -47,7 +47,7 @@ Defaults to `True`. ### `default` -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. +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. @@ -92,7 +92,7 @@ For example, using the following model. name = models.CharField(max_length=100) created = models.DateTimeField(auto_now_add=True) payment_expiry = models.DateTimeField() - + def has_expired(self): return now() > self.payment_expiry @@ -102,7 +102,7 @@ A serializer definition that looked like this: class AccountSerializer(serializers.HyperlinkedModelSerializer): expired = serializers.Field(source='has_expired') - + class Meta: model = Account fields = ('url', 'owner', 'name', 'expired') @@ -112,7 +112,7 @@ Would produce output similar to: { 'url': 'http://example.com/api/accounts/3/', 'owner': 'http://example.com/api/users/12/', - 'name': 'FooCorp business account', + 'name': 'FooCorp business account', 'expired': True } @@ -224,7 +224,7 @@ In the case of JSON this means the default datetime representation uses the [ECM **Signature:** `DateTimeField(format=None, input_formats=None)` -* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that Python `datetime` objects should be returned by `to_native`. In this case the datetime encoding will be determined by the renderer. +* `format` - A string representing the output format. If not specified, this defaults to `None`, which indicates that Python `datetime` objects should be returned by `to_native`. In this case the datetime encoding will be determined by the renderer. * `input_formats` - A list of strings representing the input formats which may be used to parse the date. If not specified, the `DATETIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`. DateTime format strings may either be [Python strftime formats][strftime] which explicitly specify the format, or the special string `'iso-8601'`, which indicates that [ISO 8601][iso8601] style datetimes should be used. (eg `'2013-01-29T12:34:56.000000Z'`) @@ -284,7 +284,7 @@ Corresponds to `django.forms.fields.FileField`. **Signature:** `FileField(max_length=None, allow_empty_file=False)` - `max_length` designates the maximum length for the file name. - + - `allow_empty_file` designates if empty files are allowed. ## ImageField @@ -329,12 +329,12 @@ Let's look at an example of serializing a class that represents an RGB color val """ def to_native(self, obj): return "rgb(%d, %d, %d)" % (obj.red, obj.green, obj.blue) - + def from_native(self, data): data = data.strip('rgb(').rstrip(')') red, green, blue = [int(col) for col in data.split(',')] return Color(red, green, blue) - + By default field values are treated as mapping to an attribute on the object. If you need to customize how the field value is accessed and set you need to override `.field_to_native()` and/or `.field_from_native()`. @@ -347,6 +347,14 @@ As an example, let's create a field that can be used represent the class name of """ return obj.__class__ +# More fields from 3rd-party packages + +## [drf-compound-fields](http://drf-compound-fields.readthedocs.org) + +Provides "compound" serializer fields, such as lists of simple values, which can be described by +other fields rather than serializers with the `many=True` option. Also provided are fields for +typed dictionaries and values that can be either a specific type or a list of items of that type. + [cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data [FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS [ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 From dddbff59319bdefbf235f8a37af5b6ac20c4fec1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 17 Mar 2014 08:33:18 +0000 Subject: [PATCH 061/103] Tweak DRF compound fields docs --- docs/api-guide/fields.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 89606798c..b3d5b55a2 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -347,16 +347,15 @@ As an example, let's create a field that can be used represent the class name of """ return obj.__class__ -# More fields from 3rd-party packages +# Third party packages -## [drf-compound-fields](http://drf-compound-fields.readthedocs.org) +## DRF Compound Fields -Provides "compound" serializer fields, such as lists of simple values, which can be described by -other fields rather than serializers with the `many=True` option. Also provided are fields for -typed dictionaries and values that can be either a specific type or a list of items of that type. +The [drf-compound-fields][drf-compound-fields] package provides "compound" serializer fields, such as lists of simple values, which can be described by other fields rather than serializers with the `many=True` option. Also provided are fields for typed dictionaries and values that can be either a specific type or a list of items of that type. [cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data [FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS [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 +[drf-compound-fields]: http://drf-compound-fields.readthedocs.org From abe14c06f78de3b1ab20ed73f0ee5691f5224f94 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 17 Mar 2014 08:36:13 +0000 Subject: [PATCH 062/103] Minor docs tweak --- docs/api-guide/fields.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index b3d5b55a2..67fa65d2d 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -349,6 +349,8 @@ As an example, let's create a field that can be used represent the class name of # Third party packages +The following third party packages are also available. + ## DRF Compound Fields The [drf-compound-fields][drf-compound-fields] package provides "compound" serializer fields, such as lists of simple values, which can be described by other fields rather than serializers with the `many=True` option. Also provided are fields for typed dictionaries and values that can be either a specific type or a list of items of that type. From 1909472aa27907190467b81a10fc4ee496bb8889 Mon Sep 17 00:00:00 2001 From: Benjamin Dauvergne Date: Thu, 13 Mar 2014 23:53:53 +0100 Subject: [PATCH 063/103] authentication: allow all transport modes of access token in OAuth2Authentication RFC6750 describe three transport modes for access tokens when accessing a protected resource: - Auhthorization header with the Bearer authentication type - form-encoded body parameter - URI query parameter This patch add support for last two transport modes. --- rest_framework/authentication.py | 12 ++++++++-- rest_framework/tests/test_authentication.py | 26 +++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index b0e88d88b..da9ca510e 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -6,6 +6,7 @@ import base64 from django.contrib.auth import authenticate from django.core.exceptions import ImproperlyConfigured +from django.conf import settings from rest_framework import exceptions, HTTP_HEADER_ENCODING from rest_framework.compat import CsrfViewMiddleware from rest_framework.compat import oauth, oauth_provider, oauth_provider_store @@ -291,6 +292,7 @@ class OAuth2Authentication(BaseAuthentication): OAuth 2 authentication backend using `django-oauth2-provider` """ www_authenticate_realm = 'api' + allow_query_params_token = settings.DEBUG def __init__(self, *args, **kwargs): super(OAuth2Authentication, self).__init__(*args, **kwargs) @@ -308,7 +310,13 @@ class OAuth2Authentication(BaseAuthentication): auth = get_authorization_header(request).split() - if not auth or auth[0].lower() != b'bearer': + if auth and auth[0].lower() == b'bearer': + access_token = auth[1] + elif 'access_token' in request.POST: + access_token = request.POST['access_token'] + elif 'access_token' in request.GET and self.allow_query_params_token: + access_token = request.GET['access_token'] + else: return None if len(auth) == 1: @@ -318,7 +326,7 @@ class OAuth2Authentication(BaseAuthentication): msg = 'Invalid bearer header. Token string should not contain spaces.' raise exceptions.AuthenticationFailed(msg) - return self.authenticate_credentials(request, auth[1]) + return self.authenticate_credentials(request, access_token) def authenticate_credentials(self, request, access_token): """ diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index 8caeb0812..c37d2a512 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -3,6 +3,7 @@ from django.contrib.auth.models import User from django.http import HttpResponse from django.test import TestCase from django.utils import unittest +from django.utils.http import urlencode from rest_framework import HTTP_HEADER_ENCODING from rest_framework import exceptions from rest_framework import permissions @@ -53,10 +54,14 @@ urlpatterns = patterns('', permission_classes=[permissions.TokenHasReadWriteScope])) ) +class OAuth2AuthenticationDebug(OAuth2Authentication): + allow_query_params_token = True + if oauth2_provider is not None: urlpatterns += patterns('', url(r'^oauth2/', include('provider.oauth2.urls', namespace='oauth2')), url(r'^oauth2-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication])), + url(r'^oauth2-test-debug/$', MockView.as_view(authentication_classes=[OAuth2AuthenticationDebug])), url(r'^oauth2-with-scope-test/$', MockView.as_view(authentication_classes=[OAuth2Authentication], permission_classes=[permissions.TokenHasReadWriteScope])), ) @@ -545,6 +550,27 @@ class OAuth2Tests(TestCase): response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 200) + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') + def test_post_form_passing_auth_url_transport(self): + """Ensure GETing form over OAuth with correct client credentials in form data succeed""" + response = self.csrf_client.post('/oauth2-test/', + data={'access_token': self.access_token.token}) + self.assertEqual(response.status_code, 200) + + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') + def test_get_form_passing_auth_url_transport(self): + """Ensure GETing form over OAuth with correct client credentials in query succeed when DEBUG is True""" + query = urlencode({'access_token': self.access_token.token}) + response = self.csrf_client.get('/oauth2-test-debug/?%s' % query) + self.assertEqual(response.status_code, 200) + + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') + def test_get_form_failing_auth_url_transport(self): + """Ensure GETing form over OAuth with correct client credentials in query fails when DEBUG is False""" + query = urlencode({'access_token': self.access_token.token}) + response = self.csrf_client.get('/oauth2-test/?%s' % query) + self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)) + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') def test_post_form_passing_auth(self): """Ensure POSTing form over OAuth with correct credentials passes and does not require CSRF""" From 5c87db96c54aeb8ee62213b3ab2a054546d9756c Mon Sep 17 00:00:00 2001 From: elmkarami Date: Wed, 19 Mar 2014 15:41:25 +0000 Subject: [PATCH 064/103] Update serializers.py Prevent iterating over a string that is supposed to be an iterable <==> Prevent read_only_fields = ('some_string) --- 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 10256d479..62ef5eedc 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -757,6 +757,9 @@ class ModelSerializer(Serializer): field.read_only = True ret[accessor_name] = field + + #Ensure that 'read_only_fields is an iterable + assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' # Add the `read_only` flag to any fields that have bee specified # in the `read_only_fields` option @@ -771,7 +774,10 @@ class ModelSerializer(Serializer): "on serializer '%s'." % (field_name, self.__class__.__name__)) ret[field_name].read_only = True - + + # Ensure that 'write_only_fields' is an iterabe + assert isinstance(self.opts.write_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' + for field_name in self.opts.write_only_fields: assert field_name not in self.base_fields.keys(), ( "field '%s' on serializer '%s' specified in " From 03f96988baa6b7fa3a94fd49a5a1631f92b19b4a Mon Sep 17 00:00:00 2001 From: elmkarami Date: Wed, 19 Mar 2014 17:11:44 +0000 Subject: [PATCH 065/103] Update serializers.py Prevent iterating over a string that is supposed to be an iterable <==> Prevent read_only_fields = ('some_string) --- rest_framework/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 62ef5eedc..03db418dc 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -758,7 +758,7 @@ class ModelSerializer(Serializer): ret[accessor_name] = field - #Ensure that 'read_only_fields is an iterable + # Ensure that 'read_only_fields' is an iterable assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' # Add the `read_only` flag to any fields that have bee specified @@ -775,7 +775,7 @@ class ModelSerializer(Serializer): (field_name, self.__class__.__name__)) ret[field_name].read_only = True - # Ensure that 'write_only_fields' is an iterabe + # Ensure that 'write_only_fields' is an iterable assert isinstance(self.opts.write_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' for field_name in self.opts.write_only_fields: From 499d3cb8f0cb2f8327050e4fe775ee4bdf288285 Mon Sep 17 00:00:00 2001 From: elmkarami Date: Wed, 19 Mar 2014 17:23:15 +0000 Subject: [PATCH 066/103] Update serializers.py --- 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 03db418dc..4cb2d81c8 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -776,7 +776,7 @@ class ModelSerializer(Serializer): ret[field_name].read_only = True # Ensure that 'write_only_fields' is an iterable - assert isinstance(self.opts.write_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' + assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple' for field_name in self.opts.write_only_fields: assert field_name not in self.base_fields.keys(), ( From 19c03f4a60f339397b8ed03c9e6f20b3c604ffc3 Mon Sep 17 00:00:00 2001 From: Vladislav Vlastovskiy Date: Thu, 20 Mar 2014 01:49:30 +0400 Subject: [PATCH 067/103] Added test writable star source Uses nested serializer with parent object --- rest_framework/tests/test_serializer.py | 26 +++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 85a899c53..b78ceaa66 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -508,6 +508,32 @@ class ValidationTests(TestCase): ) self.assertEqual(serializer.is_valid(), True) + def test_writable_star_source_on_nested_serializer_with_parent_object(self): + class Serializer(serializers.Serializer): + title = serializers.WritableField(source='title') + + class AlbumSerializer(serializers.ModelSerializer): + nested = Serializer(source='*') + + class Meta: + model = Album + fields = ('nested',) + + class PhotoSerializer(serializers.ModelSerializer): + album = AlbumSerializer(source='album') + + class Meta: + model = Photo + fields = ('album', ) + + photo = Photo(album=Album()) + + data = {'album': {'nested': {'title': 'test'}}} + + serializer = PhotoSerializer(photo, data=data) + self.assertEqual(serializer.is_valid(), True) + self.assertEqual(serializer.data, data) + def test_writable_star_source_with_inner_source_fields(self): """ Tests that a serializer with source="*" correctly expands the From c3aa10e589cb524dc3bb39a4fccee8238763d25a Mon Sep 17 00:00:00 2001 From: Vladislav Vlastovskiy Date: Thu, 20 Mar 2014 01:50:40 +0400 Subject: [PATCH 068/103] Moved get component from object after test source is star --- rest_framework/serializers.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 5c726dfcd..cc0e027f7 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -438,25 +438,26 @@ class BaseSerializer(WritableField): raise ValidationError(self.error_messages['required']) return - # Set the serializer object if it exists - obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None - - # 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: - reverted_data = self.restore_fields(value, {}) - if not self._errors: - into.update(reverted_data) else: if value in (None, ''): into[(self.source or field_name)] = None else: + # Set the serializer object if it exists + obj = get_component(self.parent.object, self.source or field_name) if self.parent.object else None + + # 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: + reverted_data = self.restore_fields(value, {}) + if not self._errors: + into.update(reverted_data) + kwargs = { 'instance': obj, 'data': value, From e8167f96e6c1a112e76b647ac32164be931b09a8 Mon Sep 17 00:00:00 2001 From: Vladislav Vlastovskiy Date: Thu, 20 Mar 2014 08:53:41 +0400 Subject: [PATCH 069/103] Fixed copy-paste --- rest_framework/serializers.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index cc0e027f7..01606e9c0 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -438,6 +438,11 @@ class BaseSerializer(WritableField): raise ValidationError(self.error_messages['required']) return + if self.source == '*': + if value: + reverted_data = self.restore_fields(value, {}) + if not self._errors: + into.update(reverted_data) else: if value in (None, ''): into[(self.source or field_name)] = None @@ -452,12 +457,6 @@ class BaseSerializer(WritableField): is_simple_callable(getattr(obj, 'all', None))): obj = obj.all() - if self.source == '*': - if value: - reverted_data = self.restore_fields(value, {}) - if not self._errors: - into.update(reverted_data) - kwargs = { 'instance': obj, 'data': value, From f5fc6937ece8c2bc70088979cc19c2c0a660c7a0 Mon Sep 17 00:00:00 2001 From: Vladislav Vlastovskiy Date: Thu, 20 Mar 2014 20:27:07 +0400 Subject: [PATCH 070/103] Change serializer name for removing confusion --- rest_framework/tests/test_serializer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index b78ceaa66..3ee2b38a7 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -509,11 +509,11 @@ class ValidationTests(TestCase): self.assertEqual(serializer.is_valid(), True) def test_writable_star_source_on_nested_serializer_with_parent_object(self): - class Serializer(serializers.Serializer): + class TitleSerializer(serializers.Serializer): title = serializers.WritableField(source='title') class AlbumSerializer(serializers.ModelSerializer): - nested = Serializer(source='*') + nested = TitleSerializer(source='*') class Meta: model = Album From b04cd570504df0415d5f52b6e5cee490f9219cf2 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 21 Mar 2014 16:37:27 +0100 Subject: [PATCH 071/103] Bumped tests against Django from 1.7a2 to 1.7b1 --- .travis.yml | 6 +++--- tox.ini | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index f6b4753d8..60b48cbaf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ python: - "3.3" env: - - DJANGO="https://www.djangoproject.com/download/1.7a2/tarball/" + - DJANGO="https://www.djangoproject.com/download/1.7b1/tarball/" - DJANGO="django==1.6.2" - DJANGO="django==1.5.5" - DJANGO="django==1.4.10" @@ -23,7 +23,7 @@ install: - "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.7; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} == '3' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" - - "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7a2/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" + - "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7b1/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" - export PYTHONPATH=. script: @@ -32,7 +32,7 @@ script: matrix: exclude: - python: "2.6" - env: DJANGO="https://www.djangoproject.com/download/1.7a2/tarball/" + env: DJANGO="https://www.djangoproject.com/download/1.7b1/tarball/" - python: "3.2" env: DJANGO="django==1.4.10" - python: "3.2" diff --git a/tox.ini b/tox.ini index 47fdf67ff..eac17ede5 100644 --- a/tox.ini +++ b/tox.ini @@ -7,22 +7,22 @@ commands = {envpython} rest_framework/runtests/runtests.py [testenv:py3.3-django1.7] basepython = python3.3 -deps = https://www.djangoproject.com/download/1.7a2/tarball/ django-filter==0.6a1 +deps = https://www.djangoproject.com/download/1.7b1/tarball/ defusedxml==0.3 Pillow==2.3.0 [testenv:py3.2-django1.7] basepython = python3.2 -deps = https://www.djangoproject.com/download/1.7a2/tarball/ django-filter==0.6a1 +deps = https://www.djangoproject.com/download/1.7b1/tarball/ defusedxml==0.3 Pillow==2.3.0 [testenv:py2.7-django1.7] basepython = python2.7 -deps = https://www.djangoproject.com/download/1.7a2/tarball/ django-filter==0.6a1 +deps = https://www.djangoproject.com/download/1.7b1/tarball/ defusedxml==0.3 django-oauth-plus==2.2.1 oauth2==1.5.211 From 3b71be725a727be802eb2e43d4d155b734320023 Mon Sep 17 00:00:00 2001 From: Daniel Kontsek Date: Sat, 22 Mar 2014 10:22:08 +0100 Subject: [PATCH 072/103] Fixed encoding parameter in QueryDict --- rest_framework/request.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index ca70b49e7..40467c03d 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -346,7 +346,7 @@ class Request(object): media_type = self.content_type if stream is None or media_type is None: - empty_data = QueryDict('', self._request._encoding) + empty_data = QueryDict('', encoding=self._request._encoding) empty_files = MultiValueDict() return (empty_data, empty_files) @@ -362,7 +362,7 @@ class Request(object): # 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._data = QueryDict('', encoding=self._request._encoding) self._files = MultiValueDict() raise From 2a27674a7971a60050d4530a0852a864b2065adb Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Sun, 23 Mar 2014 15:39:57 +0100 Subject: [PATCH 073/103] Aligned the django-filter version with travis builds. --- tox.ini | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/tox.ini b/tox.ini index eac17ede5..855ab0ceb 100644 --- a/tox.ini +++ b/tox.ini @@ -7,22 +7,22 @@ commands = {envpython} rest_framework/runtests/runtests.py [testenv:py3.3-django1.7] basepython = python3.3 - django-filter==0.6a1 deps = https://www.djangoproject.com/download/1.7b1/tarball/ + django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 [testenv:py3.2-django1.7] basepython = python3.2 - django-filter==0.6a1 deps = https://www.djangoproject.com/download/1.7b1/tarball/ + django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 [testenv:py2.7-django1.7] basepython = python2.7 - django-filter==0.6a1 deps = https://www.djangoproject.com/download/1.7b1/tarball/ + django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 oauth2==1.5.211 @@ -33,21 +33,21 @@ deps = https://www.djangoproject.com/download/1.7b1/tarball/ [testenv:py3.3-django1.6] basepython = python3.3 deps = Django==1.6 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 [testenv:py3.2-django1.6] basepython = python3.2 deps = Django==1.6 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 [testenv:py2.7-django1.6] basepython = python2.7 deps = Django==1.6 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 oauth2==1.5.211 @@ -58,7 +58,7 @@ deps = Django==1.6 [testenv:py2.6-django1.6] basepython = python2.6 deps = Django==1.6 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 oauth2==1.5.211 @@ -69,21 +69,21 @@ deps = Django==1.6 [testenv:py3.3-django1.5] basepython = python3.3 deps = django==1.5.5 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 [testenv:py3.2-django1.5] basepython = python3.2 deps = django==1.5.5 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 [testenv:py2.7-django1.5] basepython = python2.7 deps = django==1.5.5 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 oauth2==1.5.211 @@ -94,7 +94,7 @@ deps = django==1.5.5 [testenv:py2.6-django1.5] basepython = python2.6 deps = django==1.5.5 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 oauth2==1.5.211 @@ -105,7 +105,7 @@ deps = django==1.5.5 [testenv:py2.7-django1.4] basepython = python2.7 deps = django==1.4.10 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 oauth2==1.5.211 @@ -116,7 +116,7 @@ deps = django==1.4.10 [testenv:py2.6-django1.4] basepython = python2.6 deps = django==1.4.10 - django-filter==0.6a1 + django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 oauth2==1.5.211 From 3560796bbff33917df9f8e6885328467c7f809f9 Mon Sep 17 00:00:00 2001 From: Ravi Kotecha Date: Mon, 31 Mar 2014 11:38:26 +0100 Subject: [PATCH 074/103] add regression tests for field Validators pep8 and add issue no fix formatting for python 2.6 and strings for python 3.2 --- rest_framework/tests/test_validation.py | 44 +++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py index 124c874d7..31549df85 100644 --- a/rest_framework/tests/test_validation.py +++ b/rest_framework/tests/test_validation.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from django.core.validators import MaxValueValidator from django.db import models from django.test import TestCase from rest_framework import generics, serializers, status @@ -102,3 +103,46 @@ class TestAvoidValidation(TestCase): self.assertFalse(serializer.is_valid()) self.assertDictEqual(serializer.errors, {'non_field_errors': ['Invalid data']}) + + +# regression tests for issue: 1493 + +class ValidationMaxValueValidatorModel(models.Model): + number_value = models.PositiveIntegerField(validators=[MaxValueValidator(100)]) + + +class ValidationMaxValueValidatorModelSerializer(serializers.ModelSerializer): + class Meta: + model = ValidationMaxValueValidatorModel + + +class UpdateMaxValueValidationModel(generics.RetrieveUpdateDestroyAPIView): + model = ValidationMaxValueValidatorModel + serializer_class = ValidationMaxValueValidatorModelSerializer + + +class TestMaxValueValidatorValidation(TestCase): + + def test_max_value_validation_serializer_success(self): + serializer = ValidationMaxValueValidatorModelSerializer(data={'number_value': 99}) + self.assertTrue(serializer.is_valid()) + + def test_max_value_validation_serializer_fails(self): + serializer = ValidationMaxValueValidatorModelSerializer(data={'number_value': 101}) + self.assertFalse(serializer.is_valid()) + self.assertDictEqual({'number_value': ['Ensure this value is less than or equal to 100.']}, serializer.errors) + + def test_max_value_validation_success(self): + obj = ValidationMaxValueValidatorModel.objects.create(number_value=100) + request = factory.patch('/{0}'.format(obj.pk), {'number_value': 98}, format='json') + view = UpdateMaxValueValidationModel().as_view() + response = view(request, pk=obj.pk).render() + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_max_value_validation_fail(self): + obj = ValidationMaxValueValidatorModel.objects.create(number_value=100) + request = factory.patch('/{0}'.format(obj.pk), {'number_value': 101}, format='json') + view = UpdateMaxValueValidationModel().as_view() + response = view(request, pk=obj.pk).render() + self.assertEqual(response.content, '{"number_value": ["Ensure this value is less than or equal to 100."]}') + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) From 591cf8a48c6e5ce37d205c4b7e418fb7d2c31b0f Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 31 Mar 2014 13:17:31 +0200 Subject: [PATCH 075/103] Content is a binary string. --- rest_framework/tests/test_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_validation.py b/rest_framework/tests/test_validation.py index 31549df85..e13e4078c 100644 --- a/rest_framework/tests/test_validation.py +++ b/rest_framework/tests/test_validation.py @@ -144,5 +144,5 @@ class TestMaxValueValidatorValidation(TestCase): request = factory.patch('/{0}'.format(obj.pk), {'number_value': 101}, format='json') view = UpdateMaxValueValidationModel().as_view() response = view(request, pk=obj.pk).render() - self.assertEqual(response.content, '{"number_value": ["Ensure this value is less than or equal to 100."]}') + self.assertEqual(response.content, b'{"number_value": ["Ensure this value is less than or equal to 100."]}') self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) From 6322feb32dceb7be67b2117686f0a7570a615294 Mon Sep 17 00:00:00 2001 From: jacobg Date: Fri, 4 Apr 2014 10:22:02 -0400 Subject: [PATCH 076/103] add a __str__ implementation to APIException Add a __str__ implementation to rest_framework.exceptions.APIException. This helps for logging raised exceptions. Thanks. --- rest_framework/exceptions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index 0ac5866ef..5f774a9f3 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -20,6 +20,8 @@ class APIException(Exception): def __init__(self, detail=None): self.detail = detail or self.default_detail + def __str__(self): + return self.detail class ParseError(APIException): status_code = status.HTTP_400_BAD_REQUEST From e45e52a255c0dfbecfc5048697534ffbe0e2648e Mon Sep 17 00:00:00 2001 From: Dmitry Mukhin Date: Mon, 7 Apr 2014 20:39:45 +0400 Subject: [PATCH 077/103] replace page with page_size to avoide confusion --- docs/topics/release-notes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 0010f6878..2bc8b2d6a 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -112,11 +112,11 @@ You can determine your currently installed version using `pip freeze`: * Bugfix: `client.force_authenticate(None)` should also clear session info if it exists. * Bugfix: Client sending empty string instead of file now clears `FileField`. * Bugfix: Empty values on ChoiceFields with `required=False` now consistently return `None`. -* Bugfix: Clients setting `page=0` now simply returns the default page size, instead of disabling pagination. [*] +* Bugfix: Clients setting `page_size=0` now simply returns the default page size, instead of disabling pagination. [*] --- -[*] Note that the change in `page=0` behaviour fixes what is considered to be a bug in how clients can effect the pagination size. However if you were relying on this behavior you will need to add the following mixin to your list views in order to preserve the existing behavior. +[*] Note that the change in `page_size=0` behaviour fixes what is considered to be a bug in how clients can effect the pagination size. However if you were relying on this behavior you will need to add the following mixin to your list views in order to preserve the existing behavior. class DisablePaginationMixin(object): def get_paginate_by(self, queryset=None): From 2a1571b3bf36ff153af68401f7aefa0620f80807 Mon Sep 17 00:00:00 2001 From: Mauro de Carvalho Date: Mon, 7 Apr 2014 18:27:59 -0300 Subject: [PATCH 078/103] Fixed comment. --- 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 68b956822..946a59545 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -164,7 +164,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 corresponds to, if one exists. + field_name - The name of the field being initialized. """ self.parent = parent self.root = parent.root or parent From 3234a5dd6b0c090dd25a716e7b1c2567d8fee89b Mon Sep 17 00:00:00 2001 From: Craig Date: Tue, 8 Apr 2014 22:56:07 -0400 Subject: [PATCH 079/103] Fix python syntax in filtering docs --- docs/api-guide/filtering.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index d6c4b1c1b..6a8a267b2 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -24,7 +24,7 @@ For example: from myapp.serializers import PurchaseSerializer from rest_framework import generics - class PurchaseList(generics.ListAPIView) + class PurchaseList(generics.ListAPIView): serializer_class = PurchaseSerializer def get_queryset(self): @@ -46,7 +46,7 @@ For example if your URL config contained an entry like this: You could then write a view that returned a purchase queryset filtered by the username portion of the URL: - class PurchaseList(generics.ListAPIView) + class PurchaseList(generics.ListAPIView): serializer_class = PurchaseSerializer def get_queryset(self): @@ -63,7 +63,7 @@ A final example of filtering the initial queryset would be to determine the init We can override `.get_queryset()` to deal with URLs such as `http://example.com/api/purchases?username=denvercoder9`, and filter the queryset only if the `username` parameter is included in the URL: - class PurchaseList(generics.ListAPIView) + class PurchaseList(generics.ListAPIView): serializer_class = PurchaseSerializer def get_queryset(self): From c1ac65edce1bcfff4c87df3bb9c4df14fe8e9d6c Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 9 Apr 2014 15:51:00 +0200 Subject: [PATCH 080/103] Adds test that blank option is added when required=False on RelatedFields --- rest_framework/relations.py | 2 ++ rest_framework/tests/test_relations.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 308545ce9..3463954dc 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -59,6 +59,8 @@ class RelatedField(WritableField): super(RelatedField, self).__init__(*args, **kwargs) if not self.required: + # Accessed in ModelChoiceIterator django/forms/models.py:1034 + # If set adds empty choice. self.empty_label = BLANK_CHOICE_DASH[0][1] self.queryset = queryset diff --git a/rest_framework/tests/test_relations.py b/rest_framework/tests/test_relations.py index f52e0e1e5..c421096ab 100644 --- a/rest_framework/tests/test_relations.py +++ b/rest_framework/tests/test_relations.py @@ -118,3 +118,25 @@ class RelatedFieldSourceTests(TestCase): (serializers.ModelSerializer,), attrs) with self.assertRaises(AttributeError): TestSerializer(data={'name': 'foo'}) + + +class RelatedFieldChoicesTests(TestCase): + """ + Tests for #1408 "Web browseable API doesn't have blank option on drop down list box" + https://github.com/tomchristie/django-rest-framework/issues/1408 + """ + def test_blank_option_is_added_to_choice_if_required_equals_false(self): + """ + + """ + post = BlogPost(title="Checking blank option is added") + post.save() + + queryset = BlogPost.objects.all() + field = serializers.RelatedField(required=False, queryset=queryset) + + choice_count = BlogPost.objects.count() + widget_count = len(field.widget.choices) + + self.assertEqual(widget_count, choice_count + 1, 'BLANK_CHOICE_DASH option should have been added') + From a73498d7974b15a25902fbdd1024742b95a166d4 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 9 Apr 2014 19:54:13 +0200 Subject: [PATCH 081/103] Skip new test for Django < 1.6 --- rest_framework/tests/test_relations.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rest_framework/tests/test_relations.py b/rest_framework/tests/test_relations.py index c421096ab..37ac826b2 100644 --- a/rest_framework/tests/test_relations.py +++ b/rest_framework/tests/test_relations.py @@ -2,8 +2,10 @@ General tests for relational fields. """ from __future__ import unicode_literals +from django import get_version from django.db import models from django.test import TestCase +from django.utils import unittest from rest_framework import serializers from rest_framework.tests.models import BlogPost @@ -119,7 +121,7 @@ class RelatedFieldSourceTests(TestCase): with self.assertRaises(AttributeError): TestSerializer(data={'name': 'foo'}) - +@unittest.skipIf(get_version() < '1.6.0', 'Upstream behaviour changed in v1.6') class RelatedFieldChoicesTests(TestCase): """ Tests for #1408 "Web browseable API doesn't have blank option on drop down list box" From a23059b6f73aaff9709f611826bac892e56663dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Wed, 9 Apr 2014 23:35:41 +0200 Subject: [PATCH 082/103] Add more TRAILING_PUNCTUATION to work with YAML. Fixes #1517 --- rest_framework/templatetags/rest_framework.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index beb8c5b0e..dff176d62 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -180,7 +180,7 @@ def add_class(value, css_class): # Bunch of stuff cloned from urlize -TRAILING_PUNCTUATION = ['.', ',', ':', ';', '.)', '"', "'"] +TRAILING_PUNCTUATION = ['.', ',', ':', ';', '.)', '"', "']", "'}", "'"] WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('<', '>'), ('"', '"'), ("'", "'")] word_split_re = re.compile(r'(\s+)') From 7ae8409370635ccec7d3c160ea87281f21c9ae11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Thu, 10 Apr 2014 01:35:45 +0200 Subject: [PATCH 083/103] Allow unicode YAML dump with UnicodeYAMLRenderer Fixes #1519 --- rest_framework/renderers.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 7a7da5610..484961add 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -193,6 +193,7 @@ class YAMLRenderer(BaseRenderer): format = 'yaml' encoder = encoders.SafeDumper charset = 'utf-8' + ensure_ascii = True def render(self, data, accepted_media_type=None, renderer_context=None): """ @@ -203,7 +204,15 @@ class YAMLRenderer(BaseRenderer): if data is None: return '' - return yaml.dump(data, stream=None, encoding=self.charset, Dumper=self.encoder) + return yaml.dump(data, stream=None, encoding=self.charset, Dumper=self.encoder, allow_unicode=not self.ensure_ascii) + + +class UnicodeYAMLRenderer(YAMLRenderer): + """ + Renderer which serializes to YAML. + Does *not* apply character escaping for non-ascii characters. + """ + ensure_ascii = False class TemplateHTMLRenderer(BaseRenderer): From f68596a7326777f971d9551ff1bfc7176389ea22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Thu, 10 Apr 2014 01:58:06 +0200 Subject: [PATCH 084/103] Document new UnicodeYAMLRenderer --- docs/api-guide/renderers.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 7798827bc..7a3429bfd 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -138,6 +138,26 @@ Renders the request data into `YAML`. Requires the `pyyaml` package to be installed. +Note that non-ascii characters will be rendered using `\uXXXX` character escape. For example: + + unicode black star: "\u2605" + +**.media_type**: `application/yaml` + +**.format**: `'.yaml'` + +**.charset**: `utf-8` + +## UnicodeYAMLRenderer + +Renders the request data into `YAML`. + +Requires the `pyyaml` package to be installed. + +Note that non-ascii characters will not be character escaped. For example: + + unicode black star: ★ + **.media_type**: `application/yaml` **.format**: `'.yaml'` From 613df5c6501f715c0775229f34fcba9f4291c05d Mon Sep 17 00:00:00 2001 From: Ian Leith Date: Fri, 11 Apr 2014 05:49:49 +0100 Subject: [PATCH 085/103] Fix dict_keys equality test for python 3. --- rest_framework/utils/mediatypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/utils/mediatypes.py b/rest_framework/utils/mediatypes.py index c09c29338..92f99efd2 100644 --- a/rest_framework/utils/mediatypes.py +++ b/rest_framework/utils/mediatypes.py @@ -74,7 +74,7 @@ class _MediaType(object): return 0 elif self.sub_type == '*': return 1 - elif not self.params or self.params.keys() == ['q']: + elif not self.params or list(self.params.keys()) == ['q']: return 2 return 3 From 0a0e4f22e72badd1d8700a2b253cb27450a5319f Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Sat, 12 Apr 2014 17:51:02 +0100 Subject: [PATCH 086/103] Set GenericForeignKey fields on object before save * A model with a required GenericForeignKey can be saved if the field is set --- rest_framework/serializers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index cb7539e0b..1d6097edd 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -16,6 +16,7 @@ import datetime import inspect import types from decimal import Decimal +from django.contrib.contenttypes.generic import GenericForeignKey from django.core.paginator import Page from django.db import models from django.forms import widgets @@ -943,6 +944,8 @@ class ModelSerializer(Serializer): # Forward m2m relations for field in meta.many_to_many + meta.virtual_fields: + if isinstance(field, GenericForeignKey): + continue if field.name in attrs: m2m_data[field.name] = attrs.pop(field.name) From 853c7a16c15c7291561bc4b3dfbcad88ea262a18 Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Sun, 13 Apr 2014 17:26:15 +0100 Subject: [PATCH 087/103] Use setattr for adding fields to a new instance Add test for restoring a GenericForeignKey --- rest_framework/serializers.py | 18 ++++++++---------- rest_framework/tests/test_genericrelations.py | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 1d6097edd..ea9509bf9 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -955,17 +955,15 @@ class ModelSerializer(Serializer): if isinstance(self.fields.get(field_name, None), Serializer): nested_forward_relations[field_name] = attrs[field_name] - # Update an existing instance... - if instance is not None: - for key, val in attrs.items(): - try: - setattr(instance, key, val) - except ValueError: - self._errors[key] = self.error_messages['required'] + # Create an empty instance of the model + if instance is None: + instance = self.opts.model() - # ...or create a new instance - else: - instance = self.opts.model(**attrs) + for key, val in attrs.items(): + try: + setattr(instance, key, val) + except ValueError: + self._errors[key] = self.error_messages['required'] # Any relations that cannot be set until we've # saved the model get hidden away on these diff --git a/rest_framework/tests/test_genericrelations.py b/rest_framework/tests/test_genericrelations.py index fa09c9e6c..46a2d863f 100644 --- a/rest_framework/tests/test_genericrelations.py +++ b/rest_framework/tests/test_genericrelations.py @@ -131,3 +131,21 @@ class TestGenericRelations(TestCase): } ] self.assertEqual(serializer.data, expected) + + def test_restore_object_generic_fk(self): + """ + Ensure an object with a generic foreign key can be restored. + """ + + class TagSerializer(serializers.ModelSerializer): + class Meta: + model = Tag + exclude = ('content_type', 'object_id') + + serializer = TagSerializer() + + bookmark = Bookmark(url='http://example.com') + attrs = {'tagged_item': bookmark, 'tag': 'example'} + + tag = serializer.restore_object(attrs) + self.assertEqual(tag.tagged_item, bookmark) From 4b3eb6e0b0e6412693de126ac92482a276ca9a78 Mon Sep 17 00:00:00 2001 From: Vladislav Vlastovskiy Date: Mon, 14 Apr 2014 12:21:38 +0400 Subject: [PATCH 088/103] Fixed parse file name --- rest_framework/parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index f1b3e38d4..703cefca8 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -288,7 +288,7 @@ class FileUploadParser(BaseParser): try: meta = parser_context['request'].META - disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION']) + disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode('utf-8')) return disposition[1]['filename'] except (AttributeError, KeyError): pass From 063addabfeb716f54c5784917e92ab6abb635ff5 Mon Sep 17 00:00:00 2001 From: Vladislav Vlastovskiy Date: Mon, 14 Apr 2014 12:28:41 +0400 Subject: [PATCH 089/103] Removed encode from test Django does not produce such a decoding by default, this test was not honest. --- rest_framework/tests/test_parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_parsers.py b/rest_framework/tests/test_parsers.py index 7699e10c9..ffd6b360f 100644 --- a/rest_framework/tests/test_parsers.py +++ b/rest_framework/tests/test_parsers.py @@ -96,7 +96,7 @@ class TestFileUploadParser(TestCase): request = MockRequest() request.upload_handlers = (MemoryFileUploadHandler(),) request.META = { - 'HTTP_CONTENT_DISPOSITION': 'Content-Disposition: inline; filename=file.txt'.encode('utf-8'), + 'HTTP_CONTENT_DISPOSITION': 'Content-Disposition: inline; filename=file.txt', 'HTTP_CONTENT_LENGTH': 14, } self.parser_context = {'request': request, 'kwargs': {}} From d474934d365291c28d5741898257cbdd5d0aa9ec Mon Sep 17 00:00:00 2001 From: Vladislav Vlastovskiy Date: Mon, 14 Apr 2014 13:01:24 +0400 Subject: [PATCH 090/103] Fixed return type From bytes to str --- rest_framework/parsers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 703cefca8..d49b17a4a 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -10,6 +10,7 @@ from django.core.files.uploadhandler import StopFutureHandlers from django.http import QueryDict from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter +from django.utils.encoding import force_str from rest_framework.compat import etree, six, yaml from rest_framework.exceptions import ParseError from rest_framework import renderers @@ -289,6 +290,6 @@ class FileUploadParser(BaseParser): try: meta = parser_context['request'].META disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode('utf-8')) - return disposition[1]['filename'] + return force_str(disposition[1]['filename']) except (AttributeError, KeyError): pass From d1f4dfca2061cb552158ac7ea6f2de609989797b Mon Sep 17 00:00:00 2001 From: Vladislav Vlastovskiy Date: Mon, 14 Apr 2014 13:04:18 +0400 Subject: [PATCH 091/103] Removed decode from test filename --- rest_framework/tests/test_parsers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_parsers.py b/rest_framework/tests/test_parsers.py index ffd6b360f..8af906772 100644 --- a/rest_framework/tests/test_parsers.py +++ b/rest_framework/tests/test_parsers.py @@ -112,4 +112,4 @@ class TestFileUploadParser(TestCase): def test_get_filename(self): parser = FileUploadParser() filename = parser.get_filename(self.stream, None, self.parser_context) - self.assertEqual(filename, 'file.txt'.encode('utf-8')) + self.assertEqual(filename, 'file.txt') From 3fe038357267f947eba467f2b7714a782fa93c33 Mon Sep 17 00:00:00 2001 From: Vladislav Vlastovskiy Date: Mon, 14 Apr 2014 13:21:24 +0400 Subject: [PATCH 092/103] Fixed convert bytes to str Use compact function for convert --- rest_framework/parsers.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index d49b17a4a..4990971b8 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -10,8 +10,7 @@ from django.core.files.uploadhandler import StopFutureHandlers from django.http import QueryDict from django.http.multipartparser import MultiPartParser as DjangoMultiPartParser from django.http.multipartparser import MultiPartParserError, parse_header, ChunkIter -from django.utils.encoding import force_str -from rest_framework.compat import etree, six, yaml +from rest_framework.compat import etree, six, yaml, force_text from rest_framework.exceptions import ParseError from rest_framework import renderers import json @@ -290,6 +289,6 @@ class FileUploadParser(BaseParser): try: meta = parser_context['request'].META disposition = parse_header(meta['HTTP_CONTENT_DISPOSITION'].encode('utf-8')) - return force_str(disposition[1]['filename']) + return force_text(disposition[1]['filename']) except (AttributeError, KeyError): pass From 617c9825913cfc0cdeaa4405df0b885db0a9ff60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Tue, 15 Apr 2014 14:12:09 +0200 Subject: [PATCH 093/103] Add test for UnicodeYAMLRenderer --- rest_framework/tests/test_renderers.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/rest_framework/tests/test_renderers.py b/rest_framework/tests/test_renderers.py index c7bf772ef..7cb7d0f93 100644 --- a/rest_framework/tests/test_renderers.py +++ b/rest_framework/tests/test_renderers.py @@ -12,7 +12,7 @@ from rest_framework.compat import yaml, etree, patterns, url, include, six, Stri from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.renderers import BaseRenderer, JSONRenderer, YAMLRenderer, \ - XMLRenderer, JSONPRenderer, BrowsableAPIRenderer, UnicodeJSONRenderer + XMLRenderer, JSONPRenderer, BrowsableAPIRenderer, UnicodeJSONRenderer, UnicodeYAMLRenderer from rest_framework.parsers import YAMLParser, XMLParser from rest_framework.settings import api_settings from rest_framework.test import APIRequestFactory @@ -467,6 +467,17 @@ if yaml: self.assertTrue(string in content, '%r not in %r' % (string, content)) + class UnicodeYAMLRendererTests(TestCase): + """ + Tests specific for the Unicode YAML Renderer + """ + def test_proper_encoding(self): + obj = {'countries': ['United Kingdom', 'France', 'España']} + renderer = UnicodeYAMLRenderer() + content = renderer.render(obj, 'application/yaml') + self.assertEqual(content.strip(), 'countries: [United Kingdom, France, España]'.encode('utf-8')) + + class XMLRendererTestCase(TestCase): """ Tests specific to the XML Renderer From ef1d65282771c806f68d717d57172597184db26c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Tue, 15 Apr 2014 14:02:11 +0200 Subject: [PATCH 094/103] Introduce tests for urlize_quoted_links() function --- rest_framework/tests/test_urlizer.py | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 rest_framework/tests/test_urlizer.py diff --git a/rest_framework/tests/test_urlizer.py b/rest_framework/tests/test_urlizer.py new file mode 100644 index 000000000..3dc8e8fe5 --- /dev/null +++ b/rest_framework/tests/test_urlizer.py @@ -0,0 +1,38 @@ +from __future__ import unicode_literals +from django.test import TestCase +from rest_framework.templatetags.rest_framework import urlize_quoted_links +import sys + + +class URLizerTests(TestCase): + """ + Test if both JSON and YAML URLs are transformed into links well + """ + def _urlize_dict_check(self, data): + """ + For all items in dict test assert that the value is urlized key + """ + for original, urlized in data.items(): + assert urlize_quoted_links(original, nofollow=False) == urlized + + def test_json_with_url(self): + """ + Test if JSON URLs are transformed into links well + """ + data = {} + data['"url": "http://api/users/1/", '] = \ + '"url": "http://api/users/1/", ' + data['"foo_set": [\n "http://api/foos/1/"\n], '] = \ + '"foo_set": [\n "http://api/foos/1/"\n], ' + self._urlize_dict_check(data) + + def test_yaml_with_url(self): + """ + Test if YAML URLs are transformed into links well + """ + data = {} + data['''{users: 'http://api/users/'}'''] = \ + '''{users: 'http://api/users/'}''' + data['''foo_set: ['http://api/foos/1/']'''] = \ + '''foo_set: ['http://api/foos/1/']''' + self._urlize_dict_check(data) From a6e525cf3a22a01a4f9924e975ea7288d80ac5ef Mon Sep 17 00:00:00 2001 From: Sergey Sinitsyn Date: Thu, 24 Apr 2014 15:58:53 +0600 Subject: [PATCH 095/103] Add help_text and verbose_name attribute mapping for related field --- rest_framework/serializers.py | 8 ++++++++ rest_framework/tests/models.py | 3 ++- rest_framework/tests/test_serializer.py | 26 ++++++++++++++++++++++++- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index ea9509bf9..9cb548a51 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -828,6 +828,10 @@ class ModelSerializer(Serializer): if model_field: kwargs['required'] = not(model_field.null or model_field.blank) + if model_field.help_text is not None: + kwargs['help_text'] = model_field.help_text + if model_field.verbose_name is not None: + kwargs['label'] = model_field.verbose_name return PrimaryKeyRelatedField(**kwargs) @@ -1088,6 +1092,10 @@ class HyperlinkedModelSerializer(ModelSerializer): if model_field: kwargs['required'] = not(model_field.null or model_field.blank) + if model_field.help_text is not None: + kwargs['help_text'] = model_field.help_text + if model_field.verbose_name is not None: + kwargs['label'] = model_field.verbose_name if self.opts.lookup_field: kwargs['lookup_field'] = self.opts.lookup_field diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 6c8f2342b..0256697a1 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -143,7 +143,8 @@ class ForeignKeyTarget(RESTFrameworkModel): class ForeignKeySource(RESTFrameworkModel): name = models.CharField(max_length=100) - target = models.ForeignKey(ForeignKeyTarget, related_name='sources') + target = models.ForeignKey(ForeignKeyTarget, related_name='sources', + help_text='Target', verbose_name='Target') # Nullable ForeignKey diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 3ee2b38a7..e688c8239 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -9,7 +9,8 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers, fields, relations from rest_framework.tests.models import (HasPositiveIntegerAsChoice, Album, ActionItem, Anchor, BasicModel, BlankFieldModel, BlogPost, BlogPostComment, Book, CallableDefaultValueModel, DefaultValueModel, - ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel) + ManyToManyModel, Person, ReadOnlyManyToManyModel, Photo, RESTFrameworkModel, + ForeignKeySource, ManyToManySource) from rest_framework.tests.models import BasicModelSerializer import datetime import pickle @@ -176,6 +177,16 @@ class PositiveIntegerAsChoiceSerializer(serializers.ModelSerializer): fields = ['some_integer'] +class ForeignKeySourceSerializer(serializers.ModelSerializer): + class Meta: + model = ForeignKeySource + + +class HyperlinkedForeignKeySourceSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = ForeignKeySource + + class BasicTests(TestCase): def setUp(self): self.comment = Comment( @@ -1600,6 +1611,19 @@ class ManyFieldHelpTextTest(TestCase): self.assertEqual('Some help text.', rel_field.help_text) +class AttributeMappingOnAutogeneratedRelatedFields(TestCase): + + def test_primary_key_related_field(self): + serializer = ForeignKeySourceSerializer() + self.assertEqual(serializer.fields['target'].help_text, 'Target') + self.assertEqual(serializer.fields['target'].label, 'Target') + + def test_hyperlinked_related_field(self): + serializer = HyperlinkedForeignKeySourceSerializer() + self.assertEqual(serializer.fields['target'].help_text, 'Target') + self.assertEqual(serializer.fields['target'].label, 'Target') + + @unittest.skipUnless(PIL is not None, 'PIL is not installed') class AttributeMappingOnAutogeneratedFieldsTests(TestCase): From f4a82dd5dadf95908c96c402f7f68b8e74c7de7a Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 24 Apr 2014 14:33:36 +0200 Subject: [PATCH 096/103] Updated the release notes. --- docs/topics/release-notes.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 2bc8b2d6a..335497eec 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,25 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series +### 2.3.x + +**Date**: April 2014 + +* Fix nested serializers linked through a backward foreign key relation +* Fix bad links for the `BrowsableAPIRenderer` with `YAMLRenderer` +* Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode +* Fix `parse_header` argument convertion +* Fix mediatype detection under Python3 +* Web browseable API now offers blank option on dropdown when the field is not required +* `APIException` representation improved for logging purposes +* Allow source="*" within nested serializers +* Better support for custom oauth2 provider backends +* Fix field validation if it's optional and has no value +* Add `SEARCH_PARAM` and `ORDERING_PARAM` +* Fix `APIRequestFactory` to support arguments within the url string for GET +* Allow three transport modes for access tokens when accessing a protected resource +* Fix `Request`'s `QueryDict` encoding + ### 2.3.13 **Date**: 6th March 2014 From 82094554e5d267bcb550d3f7be26552befd7a1fe Mon Sep 17 00:00:00 2001 From: Kamil Niski Date: Sun, 27 Apr 2014 02:54:47 +0200 Subject: [PATCH 097/103] Minor typo --- 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 946a59545..8cdc55515 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -289,7 +289,7 @@ class WritableField(Field): self.validators = self.default_validators + validators self.default = default if default is not None else self.default - # Widgets are ony used for HTML forms. + # Widgets are only used for HTML forms. widget = widget or self.widget if isinstance(widget, type): widget = widget() From 4a1ef6d4b15c504881662a2667564394cb333b6b Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Sun, 27 Apr 2014 11:52:33 +0200 Subject: [PATCH 098/103] Updated Django's versions. --- .travis.yml | 16 ++++++++-------- tox.ini | 26 +++++++++++++------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.travis.yml b/.travis.yml index 60b48cbaf..bd6d2539a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,10 @@ python: - "3.3" env: - - DJANGO="https://www.djangoproject.com/download/1.7b1/tarball/" - - DJANGO="django==1.6.2" - - DJANGO="django==1.5.5" - - DJANGO="django==1.4.10" + - DJANGO="https://www.djangoproject.com/download/1.7b2/tarball/" + - DJANGO="django==1.6.3" + - DJANGO="django==1.5.6" + - DJANGO="django==1.4.11" - DJANGO="django==1.3.7" install: @@ -23,7 +23,7 @@ install: - "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.7; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} == '3' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" - - "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7b1/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" + - "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7b2/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" - export PYTHONPATH=. script: @@ -32,13 +32,13 @@ script: matrix: exclude: - python: "2.6" - env: DJANGO="https://www.djangoproject.com/download/1.7b1/tarball/" + env: DJANGO="https://www.djangoproject.com/download/1.7b2/tarball/" - python: "3.2" - env: DJANGO="django==1.4.10" + env: DJANGO="django==1.4.11" - python: "3.2" env: DJANGO="django==1.3.7" - python: "3.3" - env: DJANGO="django==1.4.10" + env: DJANGO="django==1.4.11" - python: "3.3" env: DJANGO="django==1.3.7" diff --git a/tox.ini b/tox.ini index 855ab0ceb..e21210058 100644 --- a/tox.ini +++ b/tox.ini @@ -7,21 +7,21 @@ commands = {envpython} rest_framework/runtests/runtests.py [testenv:py3.3-django1.7] basepython = python3.3 -deps = https://www.djangoproject.com/download/1.7b1/tarball/ +deps = https://www.djangoproject.com/download/1.7b2/tarball/ django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 [testenv:py3.2-django1.7] basepython = python3.2 -deps = https://www.djangoproject.com/download/1.7b1/tarball/ +deps = https://www.djangoproject.com/download/1.7b2/tarball/ django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 [testenv:py2.7-django1.7] basepython = python2.7 -deps = https://www.djangoproject.com/download/1.7b1/tarball/ +deps = https://www.djangoproject.com/download/1.7b2/tarball/ django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 @@ -32,21 +32,21 @@ deps = https://www.djangoproject.com/download/1.7b1/tarball/ [testenv:py3.3-django1.6] basepython = python3.3 -deps = Django==1.6 +deps = Django==1.6.3 django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 [testenv:py3.2-django1.6] basepython = python3.2 -deps = Django==1.6 +deps = Django==1.6.3 django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 [testenv:py2.7-django1.6] basepython = python2.7 -deps = Django==1.6 +deps = Django==1.6.3 django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 @@ -57,7 +57,7 @@ deps = Django==1.6 [testenv:py2.6-django1.6] basepython = python2.6 -deps = Django==1.6 +deps = Django==1.6.3 django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 @@ -68,21 +68,21 @@ deps = Django==1.6 [testenv:py3.3-django1.5] basepython = python3.3 -deps = django==1.5.5 +deps = django==1.5.6 django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 [testenv:py3.2-django1.5] basepython = python3.2 -deps = django==1.5.5 +deps = django==1.5.6 django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 [testenv:py2.7-django1.5] basepython = python2.7 -deps = django==1.5.5 +deps = django==1.5.6 django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 @@ -93,7 +93,7 @@ deps = django==1.5.5 [testenv:py2.6-django1.5] basepython = python2.6 -deps = django==1.5.5 +deps = django==1.5.6 django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 @@ -104,7 +104,7 @@ deps = django==1.5.5 [testenv:py2.7-django1.4] basepython = python2.7 -deps = django==1.4.10 +deps = django==1.4.11 django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 @@ -115,7 +115,7 @@ deps = django==1.4.10 [testenv:py2.6-django1.4] basepython = python2.6 -deps = django==1.4.10 +deps = django==1.4.11 django-filter==0.7 defusedxml==0.3 django-oauth-plus==2.2.1 From 1c777ffe8b67c342bc1b27fefe67d1094a2f6b07 Mon Sep 17 00:00:00 2001 From: Max Peterson Date: Mon, 28 Apr 2014 12:35:55 +0100 Subject: [PATCH 099/103] Ensure Token.generate_key returns a string. --- rest_framework/authtoken/models.py | 2 +- rest_framework/tests/test_authentication.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 8eac2cc49..167fa5314 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -34,7 +34,7 @@ class Token(models.Model): return super(Token, self).save(*args, **kwargs) def generate_key(self): - return binascii.hexlify(os.urandom(20)) + return binascii.hexlify(os.urandom(20)).decode() def __unicode__(self): return self.key diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index c37d2a512..8773f580b 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -195,6 +195,12 @@ class TokenAuthTests(TestCase): token = Token.objects.create(user=self.user) self.assertTrue(bool(token.key)) + def test_generate_key_returns_string(self): + """Ensure generate_key returns a string""" + token = Token() + key = token.generate_key() + self.assertTrue(isinstance(key, str)) + def test_token_login_json(self): """Ensure token login view using JSON POST works.""" client = APIClient(enforce_csrf_checks=True) From 170fa10ae0f2b531a8011be33cc9417b9f71e698 Mon Sep 17 00:00:00 2001 From: Max Peterson Date: Mon, 28 Apr 2014 13:10:34 +0100 Subject: [PATCH 100/103] Python < 3 compatibility. --- rest_framework/tests/test_authentication.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index 8773f580b..34ce1b7ac 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -199,7 +199,13 @@ class TokenAuthTests(TestCase): """Ensure generate_key returns a string""" token = Token() key = token.generate_key() - self.assertTrue(isinstance(key, str)) + try: + # added in Python < 3 + base = unicode + except NameError: + # added in Python >= 3 + base = str + self.assertTrue(isinstance(key, base)) def test_token_login_json(self): """Ensure token login view using JSON POST works.""" From 73597a16a2a6a388a08af923a1da8aa71d2f2848 Mon Sep 17 00:00:00 2001 From: Max Peterson Date: Mon, 28 Apr 2014 13:13:51 +0100 Subject: [PATCH 101/103] Better Python < 3 compatibility. --- rest_framework/tests/test_authentication.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index 34ce1b7ac..a1c43d9ce 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -19,7 +19,7 @@ from rest_framework.authentication import ( OAuth2Authentication ) from rest_framework.authtoken.models import Token -from rest_framework.compat import patterns, url, include +from rest_framework.compat import patterns, url, include, six from rest_framework.compat import oauth2_provider, oauth2_provider_scope from rest_framework.compat import oauth, oauth_provider from rest_framework.test import APIRequestFactory, APIClient @@ -199,13 +199,7 @@ class TokenAuthTests(TestCase): """Ensure generate_key returns a string""" token = Token() key = token.generate_key() - try: - # added in Python < 3 - base = unicode - except NameError: - # added in Python >= 3 - base = str - self.assertTrue(isinstance(key, base)) + self.assertTrue(isinstance(key, six.string_types)) def test_token_login_json(self): """Ensure token login view using JSON POST works.""" From 5e8f05a8de410125d6df7a8e27f61e94176a8897 Mon Sep 17 00:00:00 2001 From: dpetzel Date: Mon, 28 Apr 2014 13:51:50 -0400 Subject: [PATCH 102/103] very minor typo in code example --- 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 6a0f48f44..50f669a2d 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -56,7 +56,7 @@ You can also set the authentication policy on a per-view, or per-viewset basis, using the `APIView` class based views. from rest_framework.permissions import IsAuthenticated - from rest_framework.responses import Response + from rest_framework.response import Response from rest_framework.views import APIView class ExampleView(APIView): From d8cb85ef8fb0a0804d9b2c09d909ad99f69301c8 Mon Sep 17 00:00:00 2001 From: Laurent Bristiel Date: Mon, 28 Apr 2014 22:00:36 +0200 Subject: [PATCH 103/103] typo --- 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 fb927ea8b..7d06f246c 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -70,7 +70,7 @@ The following attributes control the basic view behavior. **Shortcuts**: -* `model` - This shortcut may be used instead of setting either (or both) of the `queryset`/`serializer_class` attributes, although using the explicit style is generally preferred. If used instead of `serializer_class`, then then `DEFAULT_MODEL_SERIALIZER_CLASS` setting will determine the base serializer class. Note that `model` is only ever used for generating a default queryset or serializer class - the `queryset` and `serializer_class` attributes are always preferred if provided. +* `model` - This shortcut may be used instead of setting either (or both) of the `queryset`/`serializer_class` attributes, although using the explicit style is generally preferred. If used instead of `serializer_class`, then `DEFAULT_MODEL_SERIALIZER_CLASS` setting will determine the base serializer class. Note that `model` is only ever used for generating a default queryset or serializer class - the `queryset` and `serializer_class` attributes are always preferred if provided. **Pagination**: