From 12d05630821b126f61dff74c0193132187f4f74c Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 28 Jan 2014 07:40:13 +0100 Subject: [PATCH 001/153] 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/153] =?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 74fec7eeb4e7e2e593ed5e2213020024264681ce Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Tue, 28 Jan 2014 14:30:46 +0000 Subject: [PATCH 003/153] Import force_bytes on django >= 1.5 --- rest_framework/compat.py | 2 +- rest_framework/tests/test_compat.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 rest_framework/tests/test_compat.py diff --git a/rest_framework/compat.py b/rest_framework/compat.py index b69749feb..d283e2f5d 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -457,7 +457,7 @@ from django.test.client import RequestFactory as DjangoRequestFactory from django.test.client import FakePayload try: # In 1.5 the test client uses force_bytes - from django.utils.encoding import force_bytes_or_smart_bytes + from django.utils.encoding import force_bytes as force_bytes_or_smart_bytes except ImportError: # In 1.3 and 1.4 the test client just uses smart_str from django.utils.encoding import smart_str as force_bytes_or_smart_bytes diff --git a/rest_framework/tests/test_compat.py b/rest_framework/tests/test_compat.py new file mode 100644 index 000000000..4916d19b8 --- /dev/null +++ b/rest_framework/tests/test_compat.py @@ -0,0 +1,13 @@ +import django +from django.test import TestCase + + +class TestCompat(TestCase): + def test_force_bytes_or_smart_bytes(self): + from rest_framework.compat import force_bytes_or_smart_bytes + if django.VERSION >= (1, 5): + from django.utils.encoding import force_bytes + self.assertEqual(force_bytes_or_smart_bytes, force_bytes) + else: + from django.utils.encoding import smart_str + self.assertEqual(force_bytes_or_smart_bytes, smart_str) From 78e4468f0367cc2a3a5cc6f3570a791ad67c90d9 Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Tue, 28 Jan 2014 15:54:50 +0000 Subject: [PATCH 004/153] Add file upload test for APIRequestFactory Remove test_compat --- rest_framework/tests/test_compat.py | 13 ------------- rest_framework/tests/test_testing.py | 9 +++++++++ 2 files changed, 9 insertions(+), 13 deletions(-) delete mode 100644 rest_framework/tests/test_compat.py diff --git a/rest_framework/tests/test_compat.py b/rest_framework/tests/test_compat.py deleted file mode 100644 index 4916d19b8..000000000 --- a/rest_framework/tests/test_compat.py +++ /dev/null @@ -1,13 +0,0 @@ -import django -from django.test import TestCase - - -class TestCompat(TestCase): - def test_force_bytes_or_smart_bytes(self): - from rest_framework.compat import force_bytes_or_smart_bytes - if django.VERSION >= (1, 5): - from django.utils.encoding import force_bytes - self.assertEqual(force_bytes_or_smart_bytes, force_bytes) - else: - from django.utils.encoding import smart_str - self.assertEqual(force_bytes_or_smart_bytes, smart_str) diff --git a/rest_framework/tests/test_testing.py b/rest_framework/tests/test_testing.py index 48b8956b5..71bd8b552 100644 --- a/rest_framework/tests/test_testing.py +++ b/rest_framework/tests/test_testing.py @@ -1,6 +1,8 @@ # -- coding: utf-8 -- from __future__ import unicode_literals +from io import BytesIO + from django.contrib.auth.models import User from django.test import TestCase from rest_framework.compat import patterns, url @@ -143,3 +145,10 @@ class TestAPIRequestFactory(TestCase): force_authenticate(request, user=user) response = view(request) self.assertEqual(response.data['user'], 'example') + + def test_upload_file(self): + # This is a 1x1 black png + simple_png = BytesIO(b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\rIDATx\x9cc````\x00\x00\x00\x05\x00\x01\xa5\xf6E@\x00\x00\x00\x00IEND\xaeB`\x82') + simple_png.name = 'test.png' + factory = APIRequestFactory() + factory.post('/', data={'image': simple_png}) From 6aadd1639b11091fa1987b1608fd4f797f09bc2b Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 28 Jan 2014 18:53:24 +0100 Subject: [PATCH 005/153] 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 006/153] =?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 007/153] =?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 008/153] 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 009/153] 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 010/153] 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 011/153] 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 012/153] 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 013/153] 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 014/153] 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 015/153] 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 9f0ead95976c379957faf7d3f02eb52bf80a2e17 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 30 Jan 2014 17:32:05 +0000 Subject: [PATCH 016/153] Remove TODO note, since it hasn't been TODONE. --- rest_framework/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index e863af6dd..78010fa03 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -112,7 +112,6 @@ class APIView(View): @property def default_response_headers(self): - # TODO: deprecate? # TODO: Only vary by accept if multiple renderers return { 'Allow': ', '.join(self.allowed_methods), From 18f26ff5cc193726956d97f1a7d5ced5e6c0f4ee Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 30 Jan 2014 17:47:55 +0000 Subject: [PATCH 017/153] Only add 'Vary: Accept' header when there is more than one possible renderer. --- rest_framework/views.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 78010fa03..02a6e25a9 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -112,11 +112,13 @@ class APIView(View): @property def default_response_headers(self): - # TODO: Only vary by accept if multiple renderers - return { + headers = { 'Allow': ', '.join(self.allowed_methods), - 'Vary': 'Accept' } + if len(self.renderer_classes) > 1: + headers['Vary'] = 'Accept' + return headers + def http_method_not_allowed(self, request, *args, **kwargs): """ From 0043f30cab86f50b61ce265635d503c8212848c4 Mon Sep 17 00:00:00 2001 From: Ian Foote Date: Fri, 31 Jan 2014 09:12:45 +0000 Subject: [PATCH 018/153] Use bytes BOUNDARY on django < 1.5 Django's encode_multipart was updated in django 1.5 to work internally with unicode and convert to bytes. In django >= 1.5 we therefore need to pass the BOUNDARY as unicode. In django < 1.5 we still need to pass it as bytes. --- rest_framework/renderers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 2fdd33376..e8afc26d7 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -10,6 +10,7 @@ from __future__ import unicode_literals import copy import json +import django from django import forms from django.core.exceptions import ImproperlyConfigured from django.http.multipartparser import parse_header @@ -597,7 +598,7 @@ class MultiPartRenderer(BaseRenderer): media_type = 'multipart/form-data; boundary=BoUnDaRyStRiNg' format = 'multipart' charset = 'utf-8' - BOUNDARY = 'BoUnDaRyStRiNg' + BOUNDARY = 'BoUnDaRyStRiNg' if django.VERSION >= (1, 5) else b'BoUnDaRyStRiNg' def render(self, data, accepted_media_type=None, renderer_context=None): return encode_multipart(self.BOUNDARY, data) From 5a662dd60e79fd5efb84a5a8e7f7b00dd64e0bb0 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 31 Jan 2014 11:20:53 +0100 Subject: [PATCH 019/153] 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 77ced39e6c02cd375ac6df0c00f78846ac5cbec5 Mon Sep 17 00:00:00 2001 From: Paul Melnikow Date: Fri, 31 Jan 2014 12:26:45 -0500 Subject: [PATCH 020/153] Fix doc for custom exception sample The way to provide a default detail for APIException is to define a `default_detail` attribute on the subclass. Defining a `detail` attribute without `default_detail` will not work, and will result in empty detail instead. --- docs/api-guide/exceptions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index 221df679d..4e8b823ce 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -94,7 +94,7 @@ For example, if your API relies on a third party service that may sometimes be u class ServiceUnavailable(APIException): status_code = 503 - detail = 'Service temporarily unavailable, try again later.' + default_detail = 'Service temporarily unavailable, try again later.' ## ParseError From e437854a44249806478c40f1e36244863e5ded3d Mon Sep 17 00:00:00 2001 From: meoooh Date: Sat, 1 Feb 2014 15:02:11 +0900 Subject: [PATCH 021/153] Update fields.md --- docs/api-guide/fields.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index c136509b3..0b50aa959 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -104,6 +104,7 @@ A serializer definition that looked like this: expired = serializers.Field(source='has_expired') class Meta: + model = Account fields = ('url', 'owner', 'name', 'expired') Would produce output similar to: From a33eb4177e78a65d9e07cec18198b48fa11acca1 Mon Sep 17 00:00:00 2001 From: Jeff Fein-Worton Date: Sun, 2 Feb 2014 21:21:08 -0800 Subject: [PATCH 022/153] fixed typo (wrong "its") --- docs/api-guide/fields.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 0b50aa959..93f992e66 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -126,7 +126,7 @@ A field that supports both read and write operations. By itself `WritableField` ## ModelField -A generic field that can be tied to any arbitrary model field. The `ModelField` class delegates the task of serialization/deserialization to it's associated model field. This field can be used to create serializer fields for custom model fields, without having to create a new custom serializer field. +A generic field that can be tied to any arbitrary model field. The `ModelField` class delegates the task of serialization/deserialization to its associated model field. This field can be used to create serializer fields for custom model fields, without having to create a new custom serializer field. The `ModelField` class is generally intended for internal use, but can be used by your API if needed. In order to properly instantiate a `ModelField`, it must be passed a field that is attached to an instantiated model. For example: `ModelField(model_field=MyModel()._meta.get_field('custom_field'))` @@ -308,7 +308,7 @@ Django's regular [FILE_UPLOAD_HANDLERS] are used for handling uploaded files. If you want to create a custom field, you'll probably want to override either one or both of the `.to_native()` and `.from_native()` methods. These two methods are used to convert between the initial datatype, and a primitive, serializable datatype. Primitive datatypes may be any of a number, string, date/time/datetime or None. They may also be any list or dictionary like object that only contains other primitive objects. -The `.to_native()` method is called to convert the initial datatype into a primitive, serializable datatype. The `from_native()` method is called to restore a primitive datatype into it's initial representation. +The `.to_native()` method is called to convert the initial datatype into a primitive, serializable datatype. The `from_native()` method is called to restore a primitive datatype into its initial representation. ## Examples From 40b148a2e427ffbedbc04c47235f07bf98d7e520 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Mon, 3 Feb 2014 14:54:44 +0000 Subject: [PATCH 023/153] Viewsets docs typo The docstring in the example said "update" instead of "create". --- docs/api-guide/viewsets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 4fdd9364d..23b16575f 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -225,7 +225,7 @@ To create a base viewset class that provides `create`, `list` and `retrieve` ope mixins.RetrieveModelMixin, viewsets.GenericViewSet): """ - A viewset that provides `retrieve`, `update`, and `list` actions. + A viewset that provides `retrieve`, `create`, and `list` actions. To use it, override the class and set the `.queryset` and `.serializer_class` attributes. From b182b9e246fb0c74801bea46b0d82e7384451165 Mon Sep 17 00:00:00 2001 From: juroe Date: Tue, 4 Feb 2014 11:56:41 +0100 Subject: [PATCH 024/153] Fixes typo (Implicit instead of Implict). --- 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 536b040bc..38b5089a8 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -501,7 +501,7 @@ class BaseSerializer(WritableField): else: many = hasattr(data, '__iter__') and not isinstance(data, (Page, dict, six.text_type)) if many: - warnings.warn('Implict list/queryset serialization is deprecated. ' + warnings.warn('Implicit list/queryset serialization is deprecated. ' 'Use the `many=True` flag when instantiating the serializer.', DeprecationWarning, stacklevel=3) @@ -563,7 +563,7 @@ class BaseSerializer(WritableField): else: many = hasattr(obj, '__iter__') and not isinstance(obj, (Page, dict)) if many: - warnings.warn('Implict list/queryset serialization is deprecated. ' + warnings.warn('Implicit list/queryset serialization is deprecated. ' 'Use the `many=True` flag when instantiating the serializer.', DeprecationWarning, stacklevel=2) From ba8a0bac538adc8d6897ece14648e03ce7441b24 Mon Sep 17 00:00:00 2001 From: RicterZ Date: Tue, 4 Feb 2014 23:41:13 +0800 Subject: [PATCH 025/153] Fixed a bug backticks fix you may optionally exclude the ``self.check_object_permissions, and simply return the object from the `get_object_or_404` lookup. to you may optionally exclude the `self.check_object_permissions`, and simply return the object from the `get_object_or_404` lookup. --- 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 e23b2c74f..fb927ea8b 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -119,7 +119,7 @@ For example: self.check_object_permissions(self.request, obj) return obj -Note that if your API doesn't include any object level permissions, you may optionally exclude the ``self.check_object_permissions, and simply return the object from the `get_object_or_404` lookup. +Note that if your API doesn't include any object level permissions, you may optionally exclude the `self.check_object_permissions`, and simply return the object from the `get_object_or_404` lookup. #### `get_filter_backends(self)` From d48ba1cff76ffceb1d700e9e0c6ccf518a6382da Mon Sep 17 00:00:00 2001 From: Andrey Kaygorodov Date: Wed, 5 Feb 2014 05:47:27 +0800 Subject: [PATCH 026/153] 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 f8cda8adbd7db4cd60b1dbdcd4bb5debc64ba572 Mon Sep 17 00:00:00 2001 From: Matthew King Date: Sat, 1 Feb 2014 16:56:23 -0500 Subject: [PATCH 027/153] Generate random token directly --- rest_framework/authtoken/models.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/rest_framework/authtoken/models.py b/rest_framework/authtoken/models.py index 024f62bfe..8eac2cc49 100644 --- a/rest_framework/authtoken/models.py +++ b/rest_framework/authtoken/models.py @@ -1,5 +1,5 @@ -import uuid -import hmac +import binascii +import os from hashlib import sha1 from django.conf import settings from django.db import models @@ -34,8 +34,7 @@ class Token(models.Model): return super(Token, self).save(*args, **kwargs) def generate_key(self): - unique = uuid.uuid4() - return hmac.new(unique.bytes, digestmod=sha1).hexdigest() + return binascii.hexlify(os.urandom(20)) def __unicode__(self): return self.key From 2d20512d259f51a5a5c2b71b20f98d24e0176f16 Mon Sep 17 00:00:00 2001 From: Andrey Kaygorodov Date: Wed, 5 Feb 2014 21:10:51 +0800 Subject: [PATCH 028/153] #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 41eb313e1c18051614809e2040e6ac8584936962 Mon Sep 17 00:00:00 2001 From: Artem Mezhenin Date: Sun, 9 Feb 2014 01:01:05 +0400 Subject: [PATCH 029/153] update regex for matching URLs, fixes issue #1386 --- rest_framework/templatetags/rest_framework.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 83c046f99..7a70fd460 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -6,7 +6,7 @@ from django.utils.encoding import iri_to_uri from django.utils.html import escape from django.utils.safestring import SafeData, mark_safe from rest_framework.compat import urlparse, force_text, six, smart_urlquote -import re, string +import re register = template.Library() @@ -185,7 +185,7 @@ WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('<', '>'), ('"', '"'), ("'", "'")] word_split_re = re.compile(r'(\s+)') simple_url_re = re.compile(r'^https?://\[?\w', re.IGNORECASE) -simple_url_2_re = re.compile(r'^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net|org)$', re.IGNORECASE) +simple_url_2_re = re.compile(r'^www\.|^(?!http)\w[^@\[\]]+\.(com|edu|gov|int|mil|net|org)$', re.IGNORECASE) simple_email_re = re.compile(r'^\S+@\S+\.\S+$') @@ -211,7 +211,6 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru safe_input = isinstance(text, SafeData) words = word_split_re.split(force_text(text)) for i, word in enumerate(words): - match = None if '.' in word or '@' in word or ':' in word: # Deal with punctuation. lead, middle, trail = '', word, '' From 35f4908e48cc18e94be239f8065c95e87b2fb007 Mon Sep 17 00:00:00 2001 From: Artem Mezhenin Date: Sun, 9 Feb 2014 02:46:25 +0400 Subject: [PATCH 030/153] issue #1386 * regex for matching URLs was rewritten * added unittests --- rest_framework/templatetags/rest_framework.py | 2 +- rest_framework/tests/test_templatetags.py | 38 ++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 7a70fd460..8a0e11ba7 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -185,7 +185,7 @@ WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('<', '>'), ('"', '"'), ("'", "'")] word_split_re = re.compile(r'(\s+)') simple_url_re = re.compile(r'^https?://\[?\w', re.IGNORECASE) -simple_url_2_re = re.compile(r'^www\.|^(?!http)\w[^@\[\]]+\.(com|edu|gov|int|mil|net|org)$', re.IGNORECASE) +simple_url_2_re = re.compile(r'^\w[^@\[\]\:\/,]+\.(com|edu|gov|int|mil|net|org)(:\d{2,5})?(/(\w[^@\[\]\:\,]+)?)?$', re.IGNORECASE) simple_email_re = re.compile(r'^\S+@\S+\.\S+$') diff --git a/rest_framework/tests/test_templatetags.py b/rest_framework/tests/test_templatetags.py index 609a9e089..0c2259b99 100644 --- a/rest_framework/tests/test_templatetags.py +++ b/rest_framework/tests/test_templatetags.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from django.test import TestCase from rest_framework.test import APIRequestFactory -from rest_framework.templatetags.rest_framework import add_query_param +from rest_framework.templatetags.rest_framework import add_query_param, urlize_quoted_links factory = APIRequestFactory() @@ -17,3 +17,39 @@ class TemplateTagTests(TestCase): json_url = add_query_param(request, "format", "json") self.assertIn("q=%E6%9F%A5%E8%AF%A2", json_url) self.assertIn("format=json", json_url) + + +class Issue1386Tests(TestCase): + """ + Covers #1386 + """ + + def test_issue_1386(self): + """ + Test function urlize_quoted_links with different args + """ + correct_urls = [ + "asdf.com/zxvc", + "asdf.net", + "www.as_df.org", + "as.d8f.ghj8.gov", + "www.a-op.s.d.edu/asdf/dfff_908/", + "cd8fr.com:80/hello", + "cdfr.com:808/hello", + "cdfr.com:8080/hello", + "cdfr.com:44808/hello/asdf/", + ] + for i in correct_urls: + res = urlize_quoted_links(i) + self.assertGreater(len(res), len(i)) + self.assertIn(i, res) + + incorrect_urls = [ + "mailto://asdf@fdf.com", + "asdf://asdf.com", + "asdf.netnet", + "asdf:[/p]zxcv.com" # example from issue #1386 + ] + for i in incorrect_urls: + res = urlize_quoted_links(i) + self.assertEqual(i, res) \ No newline at end of file From 97b7c25987c3bfa096a084dc671fc24816b08f87 Mon Sep 17 00:00:00 2001 From: Hassan Shamim Date: Mon, 10 Feb 2014 12:54:56 -0600 Subject: [PATCH 031/153] Replace 'detail' with 'default_detail' in Exceptions guide and APIException class docstring. --- docs/api-guide/exceptions.md | 4 ++-- rest_framework/exceptions.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index 4e8b823ce..66e181737 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -18,7 +18,7 @@ The handled exceptions are: In each case, REST framework will return a response with an appropriate status code and content-type. The body of the response will include any additional details regarding the nature of the error. -By default all error responses will include a key `details` in the body of the response, but other keys may also be included. +By default all error responses will include a key `detail` in the body of the response, but other keys may also be included. For example, the following request: @@ -86,7 +86,7 @@ Note that the exception handler will only be called for responses generated by r The **base class** for all exceptions raised inside REST framework. -To provide a custom exception, subclass `APIException` and set the `.status_code` and `.detail` properties on the class. +To provide a custom exception, subclass `APIException` and set the `.status_code` and `.default_detail` properties on the class. For example, if your API relies on a third party service that may sometimes be unreachable, you might want to implement an exception for the "503 Service Unavailable" HTTP response code. You could do this like so: diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index 4276625af..0ac5866ef 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -12,7 +12,7 @@ import math class APIException(Exception): """ Base class for REST framework exceptions. - Subclasses should provide `.status_code` and `.detail` properties. + Subclasses should provide `.status_code` and `.default_detail` properties. """ status_code = status.HTTP_500_INTERNAL_SERVER_ERROR default_detail = '' From 95670933d7954a99e02f0e19f285d8740ab5e449 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 11 Feb 2014 14:44:56 +0100 Subject: [PATCH 032/153] Test and quick fix for #1257 --- rest_framework/serializers.py | 1 + rest_framework/tests/test_serializer.py | 29 +++++++++++++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 38b5089a8..10256d479 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -893,6 +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 not isinstance(field, Serializer): exclusions.remove(field_name) return exclusions diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index 75d6e7859..6b1e333e4 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -71,6 +71,15 @@ class ActionItemSerializer(serializers.ModelSerializer): class Meta: model = ActionItem +class ActionItemSerializerOptionalFields(serializers.ModelSerializer): + """ + Intended to test that fields with `required=False` are excluded from validation. + """ + title = serializers.CharField(required=False) + + class Meta: + model = ActionItem + fields = ('title',) class ActionItemSerializerCustomRestore(serializers.ModelSerializer): @@ -288,7 +297,13 @@ class BasicTests(TestCase): serializer.save() self.assertIsNotNone(serializer.data.get('id',None), 'Model is saved. `id` should be set.') - + def test_fields_marked_as_not_required_are_excluded_from_validation(self): + """ + Check that fields with `required=False` are included in list of exclusions. + """ + serializer = ActionItemSerializerOptionalFields(self.actionitem) + exclusions = serializer.get_validation_exclusions() + self.assertTrue('title' in exclusions, '`title` field was marked `required=False` and should be excluded') class DictStyleSerializer(serializers.Serializer): @@ -1808,14 +1823,14 @@ class SerializerDefaultTrueBoolean(TestCase): self.assertEqual(serializer.data['cat'], False) self.assertEqual(serializer.data['dog'], False) - + class BoolenFieldTypeTest(TestCase): ''' Ensure the various Boolean based model fields are rendered as the proper field type - + ''' - + def setUp(self): ''' Setup an ActionItemSerializer for BooleanTesting @@ -1831,11 +1846,11 @@ class BoolenFieldTypeTest(TestCase): ''' bfield = self.serializer.get_fields()['done'] self.assertEqual(type(bfield), fields.BooleanField) - + def test_nullbooleanfield_type(self): ''' - Test that BooleanField is infered from models.NullBooleanField - + Test that BooleanField is infered from models.NullBooleanField + https://groups.google.com/forum/#!topic/django-rest-framework/D9mXEftpuQ8 ''' bfield = self.serializer.get_fields()['started'] From f1016441f5b9f6c95530d2ec306f90aa45762831 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 11 Feb 2014 19:52:32 +0100 Subject: [PATCH 033/153] Test and fix for #1210. World's lowest hanging fruit. --- rest_framework/fields.py | 3 ++- rest_framework/tests/test_fields.py | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 2f475d6ea..05daaab76 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -477,7 +477,8 @@ class URLField(CharField): type_label = 'url' def __init__(self, **kwargs): - kwargs['validators'] = [validators.URLValidator()] + if not 'validators' in kwargs: + kwargs['validators'] = [validators.URLValidator()] super(URLField, self).__init__(**kwargs) diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index 5c96bce92..e127feef9 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -860,7 +860,9 @@ class SlugFieldTests(TestCase): class URLFieldTests(TestCase): """ - Tests for URLField attribute values + Tests for URLField attribute values. + + (Includes test for #1210, checking that validators can be overridden.) """ class URLFieldModel(RESTFrameworkModel): @@ -902,6 +904,11 @@ class URLFieldTests(TestCase): self.assertEqual(getattr(serializer.fields['url_field'], 'max_length'), 20) + def test_validators_can_be_overridden(self): + url_field = serializers.URLField(validators=[]) + validators = url_field.validators + self.assertEqual([], validators, 'Passing `validators` kwarg should have overridden default validators') + class FieldMetadata(TestCase): def setUp(self): From d18d32669ac47178f26409f149160dc2c0c5359c Mon Sep 17 00:00:00 2001 From: tuky Date: Wed, 12 Feb 2014 18:11:18 +0100 Subject: [PATCH 034/153] remove spaces from META['HTTP_X_FORWARDED_FOR'] as throttle key memcached cannot handle spaces in keys --- rest_framework/throttling.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index a946d837f..56023bdad 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -155,6 +155,8 @@ class AnonRateThrottle(SimpleRateThrottle): ident = request.META.get('HTTP_X_FORWARDED_FOR') if ident is None: ident = request.META.get('REMOTE_ADDR') + else: + ident = u''.join(ident.split()) return self.cache_format % { 'scope': self.scope, From 0cb08ac7076da05bec797144263c472f507958b6 Mon Sep 17 00:00:00 2001 From: amatellanes Date: Wed, 12 Feb 2014 23:17:05 +0100 Subject: [PATCH 035/153] Fixed Testing docs section --- 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 30d292f80..5a5d1a80b 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -60,7 +60,7 @@ To run the tests, clone the repository, and then: # Setup the virtual environment virtualenv env - env/bin/activate + source env/bin/activate pip install -r requirements.txt pip install -r optionals.txt From c61d311acbe20807e1d24e22fdac83c6301b5add Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 13 Feb 2014 12:34:50 +0100 Subject: [PATCH 036/153] 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 037/153] 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 038/153] 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 d00ea3bcac5d622c586b267d18aef4700657f269 Mon Sep 17 00:00:00 2001 From: Artem Mezhenin Date: Thu, 13 Feb 2014 18:59:05 +0400 Subject: [PATCH 039/153] change regex back, issue #1386 --- rest_framework/templatetags/rest_framework.py | 8 ++++++-- rest_framework/tests/test_templatetags.py | 12 +++--------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 8a0e11ba7..0fcc53463 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -185,7 +185,7 @@ WRAPPING_PUNCTUATION = [('(', ')'), ('<', '>'), ('[', ']'), ('<', '>'), ('"', '"'), ("'", "'")] word_split_re = re.compile(r'(\s+)') simple_url_re = re.compile(r'^https?://\[?\w', re.IGNORECASE) -simple_url_2_re = re.compile(r'^\w[^@\[\]\:\/,]+\.(com|edu|gov|int|mil|net|org)(:\d{2,5})?(/(\w[^@\[\]\:\,]+)?)?$', re.IGNORECASE) +simple_url_2_re = re.compile(r'^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net|org)$', re.IGNORECASE) simple_email_re = re.compile(r'^\S+@\S+\.\S+$') @@ -234,7 +234,11 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru if simple_url_re.match(middle): url = smart_urlquote(middle) elif simple_url_2_re.match(middle): - url = smart_urlquote('http://%s' % middle) + # ValueError("Invalid IPv6 URL") can be raised here, see issue #1386 + try: + url = smart_urlquote('http://%s' % middle) + except ValueError: + pass elif not ':' in middle and simple_email_re.match(middle): local, domain = middle.rsplit('@', 1) try: diff --git a/rest_framework/tests/test_templatetags.py b/rest_framework/tests/test_templatetags.py index 0c2259b99..999c718ac 100644 --- a/rest_framework/tests/test_templatetags.py +++ b/rest_framework/tests/test_templatetags.py @@ -29,26 +29,20 @@ class Issue1386Tests(TestCase): Test function urlize_quoted_links with different args """ correct_urls = [ - "asdf.com/zxvc", + "asdf.com", "asdf.net", "www.as_df.org", "as.d8f.ghj8.gov", - "www.a-op.s.d.edu/asdf/dfff_908/", - "cd8fr.com:80/hello", - "cdfr.com:808/hello", - "cdfr.com:8080/hello", - "cdfr.com:44808/hello/asdf/", ] for i in correct_urls: res = urlize_quoted_links(i) - self.assertGreater(len(res), len(i)) + self.assertNotEqual(res, i) self.assertIn(i, res) incorrect_urls = [ "mailto://asdf@fdf.com", - "asdf://asdf.com", "asdf.netnet", - "asdf:[/p]zxcv.com" # example from issue #1386 + "asdf:[/p]zxcv.com", # example from issue #1386 ] for i in incorrect_urls: res = urlize_quoted_links(i) From 08ec23268dbb4a40000b6c4bf877f5563a4ba57b Mon Sep 17 00:00:00 2001 From: Artem Mezhenin Date: Thu, 13 Feb 2014 19:39:53 +0400 Subject: [PATCH 040/153] (I hope) tests are fixed, issue #1386 --- rest_framework/tests/test_templatetags.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/test_templatetags.py b/rest_framework/tests/test_templatetags.py index 999c718ac..d4da0c23b 100644 --- a/rest_framework/tests/test_templatetags.py +++ b/rest_framework/tests/test_templatetags.py @@ -42,8 +42,10 @@ class Issue1386Tests(TestCase): incorrect_urls = [ "mailto://asdf@fdf.com", "asdf.netnet", - "asdf:[/p]zxcv.com", # example from issue #1386 ] for i in incorrect_urls: res = urlize_quoted_links(i) - self.assertEqual(i, res) \ No newline at end of file + self.assertEqual(i, res) + + # example from issue #1386, this shouldn't raise an exception + _ = urlize_quoted_links("asdf:[/p]zxcv.com") From dbd993d108b51bebbf9fd8d567d1c782cf941404 Mon Sep 17 00:00:00 2001 From: Artem Mezhenin Date: Thu, 13 Feb 2014 20:14:47 +0400 Subject: [PATCH 041/153] wrapper for smart_urlquote, issue #1386 --- rest_framework/templatetags/rest_framework.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 0fcc53463..beb8c5b0e 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -189,6 +189,17 @@ simple_url_2_re = re.compile(r'^www\.|^(?!http)\w[^@]+\.(com|edu|gov|int|mil|net simple_email_re = re.compile(r'^\S+@\S+\.\S+$') +def smart_urlquote_wrapper(matched_url): + """ + Simple wrapper for smart_urlquote. ValueError("Invalid IPv6 URL") can + be raised here, see issue #1386 + """ + try: + return smart_urlquote(matched_url) + except ValueError: + return None + + @register.filter def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=True): """ @@ -232,13 +243,9 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru url = None nofollow_attr = ' rel="nofollow"' if nofollow else '' if simple_url_re.match(middle): - url = smart_urlquote(middle) + url = smart_urlquote_wrapper(middle) elif simple_url_2_re.match(middle): - # ValueError("Invalid IPv6 URL") can be raised here, see issue #1386 - try: - url = smart_urlquote('http://%s' % middle) - except ValueError: - pass + url = smart_urlquote_wrapper('http://%s' % middle) elif not ':' in middle and simple_email_re.match(middle): local, domain = middle.rsplit('@', 1) try: From 723d3a509bf196b1317205eb9bb425f84bbbb22b Mon Sep 17 00:00:00 2001 From: Artem Mezhenin Date: Thu, 13 Feb 2014 21:24:29 +0400 Subject: [PATCH 042/153] credit, issue #1386, PR #1897 --- docs/topics/credits.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index d4c00bc4e..f4a3e6555 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -182,6 +182,7 @@ The following people have helped make REST framework great. * Ian Foote - [ian-foote] * Chuck Harmston - [chuckharmston] * Philip Forget - [philipforget] +* Artem Mezhenin - [amezhenin] Many thanks to everyone who's contributed to the project. From 45d89b5d110939ecbbbbc03ee51bd8ea78cc41dd Mon Sep 17 00:00:00 2001 From: Artem Mezhenin Date: Thu, 13 Feb 2014 21:25:35 +0400 Subject: [PATCH 043/153] credit, issue #1386, PR #1397 --- docs/topics/credits.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index d4c00bc4e..f4a3e6555 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -182,6 +182,7 @@ The following people have helped make REST framework great. * Ian Foote - [ian-foote] * Chuck Harmston - [chuckharmston] * Philip Forget - [philipforget] +* Artem Mezhenin - [amezhenin] Many thanks to everyone who's contributed to the project. From aaa58852326ecf98785de853a71c8a6f19a0cb7e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 13 Feb 2014 17:40:00 +0000 Subject: [PATCH 044/153] Update credits --- docs/topics/credits.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/credits.md b/docs/topics/credits.md index f4a3e6555..5f0dc7522 100644 --- a/docs/topics/credits.md +++ b/docs/topics/credits.md @@ -401,3 +401,4 @@ You can also contact [@_tomchristie][twitter] directly on twitter. [ian-foote]: https://github.com/ian-foote [chuckharmston]: https://github.com/chuckharmston [philipforget]: https://github.com/philipforget +[amezhenin]: https://github.com/amezhenin From 821f8488023cb5161eb0f69b9121f6d956c39baf Mon Sep 17 00:00:00 2001 From: Vita Smid Date: Fri, 14 Feb 2014 10:44:02 +0100 Subject: [PATCH 045/153] Minor typos fixed in api-guide/testing.md (request -> response). --- docs/api-guide/testing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 4a8a91682..72c339613 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -218,12 +218,12 @@ You can use any of REST framework's test case classes as you would for the regul When checking the validity of test responses it's often more convenient to inspect the data that the response was created with, rather than inspecting the fully rendered response. -For example, it's easier to inspect `request.data`: +For example, it's easier to inspect `response.data`: response = self.client.get('/users/4/') self.assertEqual(response.data, {'id': 4, 'username': 'lauren'}) -Instead of inspecting the result of parsing `request.content`: +Instead of inspecting the result of parsing `response.content`: response = self.client.get('/users/4/') self.assertEqual(json.loads(response.content), {'id': 4, 'username': 'lauren'}) From 5e4336845fed97a819e69669ed7aa3b9bf443edb Mon Sep 17 00:00:00 2001 From: tuky Date: Fri, 14 Feb 2014 13:47:17 +0100 Subject: [PATCH 046/153] Update throttling.py python 3 u'' gone --- rest_framework/throttling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 56023bdad..c36b58bf2 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -156,7 +156,7 @@ class AnonRateThrottle(SimpleRateThrottle): if ident is None: ident = request.META.get('REMOTE_ADDR') else: - ident = u''.join(ident.split()) + ident = ''.join(ident.split()) return self.cache_format % { 'scope': self.scope, From 6f4c2c6f0536bdf596534c295e411e17be14aab7 Mon Sep 17 00:00:00 2001 From: Bo Peng Date: Fri, 14 Feb 2014 13:47:06 -0600 Subject: [PATCH 047/153] 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 048/153] 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 049/153] 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 050/153] 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 051/153] 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 052/153] 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 053/153] 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 054/153] 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 055/153] 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 056/153] 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 057/153] 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 058/153] 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 059/153] 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 060/153] 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 061/153] 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 062/153] 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 063/153] 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 064/153] 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 065/153] 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 066/153] 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 067/153] 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 068/153] 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 069/153] 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 070/153] 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 071/153] 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 072/153] 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 073/153] 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 074/153] 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 075/153] 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 076/153] 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 077/153] 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 078/153] 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 079/153] 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 080/153] 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 081/153] 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 082/153] 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 083/153] 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 084/153] 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 085/153] 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 086/153] 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 087/153] 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 088/153] 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 089/153] 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 090/153] 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 091/153] 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 092/153] 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 093/153] 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 094/153] 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 095/153] 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 096/153] 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 097/153] 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 098/153] 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 099/153] 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 100/153] 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 04315c12af09d9b2ee1106ab31af5891833dd2f9 Mon Sep 17 00:00:00 2001 From: Emanuele Pucciarelli Date: Mon, 24 Mar 2014 19:25:28 +0100 Subject: [PATCH 101/153] Use help_text, verbose_name, editable attributes for related fields --- rest_framework/serializers.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 88972e257..46beb6ac7 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -828,6 +828,15 @@ class ModelSerializer(Serializer): if model_field: kwargs['required'] = not(model_field.null or model_field.blank) + if not model_field.editable: + kwargs['read_only'] = True + + if model_field.verbose_name is not None: + kwargs['label'] = model_field.verbose_name + + if model_field.help_text is not None: + kwargs['help_text'] = model_field.help_text + return PrimaryKeyRelatedField(**kwargs) def get_field(self, model_field): From ab5082d15c04866401c6f1bc7d77d21e695f996d Mon Sep 17 00:00:00 2001 From: Emanuele Pucciarelli Date: Fri, 28 Mar 2014 19:42:46 +0100 Subject: [PATCH 102/153] Do not check model_field's attributes if it is None --- rest_framework/serializers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 46beb6ac7..d7941df14 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -828,14 +828,14 @@ class ModelSerializer(Serializer): if model_field: kwargs['required'] = not(model_field.null or model_field.blank) - if not model_field.editable: - kwargs['read_only'] = True + if not model_field.editable: + kwargs['read_only'] = True - if model_field.verbose_name is not None: - kwargs['label'] = model_field.verbose_name + if model_field.verbose_name is not None: + kwargs['label'] = model_field.verbose_name - if model_field.help_text is not None: - kwargs['help_text'] = model_field.help_text + if model_field.help_text is not None: + kwargs['help_text'] = model_field.help_text return PrimaryKeyRelatedField(**kwargs) From d8bf8787923080a64842a12e3e476aff27bfa8fc Mon Sep 17 00:00:00 2001 From: Emanuele Pucciarelli Date: Sun, 30 Mar 2014 11:48:17 +0200 Subject: [PATCH 103/153] Metadata for related fields -- added test case. --- rest_framework/tests/models.py | 6 ++- rest_framework/tests/test_generics.py | 73 +++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index 6c8f2342b..355e070e4 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -143,14 +143,16 @@ 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', + verbose_name='Target object') # Nullable ForeignKey class NullableForeignKeySource(RESTFrameworkModel): name = models.CharField(max_length=100) target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True, - related_name='nullable_sources') + related_name='nullable_sources', + verbose_name='Optional target object') # OneToOne diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py index 996bd5b0e..0cadc5dee 100644 --- a/rest_framework/tests/test_generics.py +++ b/rest_framework/tests/test_generics.py @@ -5,6 +5,7 @@ from django.test import TestCase from rest_framework import generics, renderers, serializers, status from rest_framework.test import APIRequestFactory from rest_framework.tests.models import BasicModel, Comment, SlugBasedModel +from rest_framework.tests.models import ForeignKeySource, ForeignKeyTarget from rest_framework.compat import six factory = APIRequestFactory() @@ -28,6 +29,13 @@ class InstanceView(generics.RetrieveUpdateDestroyAPIView): return queryset.exclude(text='filtered out') +class FKInstanceView(generics.RetrieveUpdateDestroyAPIView): + """ + FK: example description for OPTIONS. + """ + model = ForeignKeySource + + class SlugSerializer(serializers.ModelSerializer): slug = serializers.Field() # read only @@ -407,6 +415,71 @@ class TestInstanceView(TestCase): self.assertFalse(self.objects.filter(id=999).exists()) +class TestFKInstanceView(TestCase): + def setUp(self): + """ + Create 3 BasicModel instances. + """ + items = ['foo', 'bar', 'baz'] + for item in items: + t = ForeignKeyTarget(name=item) + t.save() + ForeignKeySource(name='source_' + item, target=t).save() + + self.objects = ForeignKeySource.objects + self.data = [ + {'id': obj.id, 'name': obj.name} + for obj in self.objects.all() + ] + self.view = FKInstanceView.as_view() + + def test_options_root_view(self): + """ + OPTIONS requests to ListCreateAPIView should return metadata + """ + request = factory.options('/999') + with self.assertNumQueries(1): + response = self.view(request, pk=999).render() + expected = { + 'name': 'Fk Instance', + 'description': 'FK: example description for OPTIONS.', + 'renders': [ + 'application/json', + 'text/html' + ], + 'parses': [ + 'application/json', + 'application/x-www-form-urlencoded', + 'multipart/form-data' + ], + 'actions': { + 'PUT': { + 'id': { + 'type': 'integer', + 'required': False, + 'read_only': True, + 'label': u'ID' + }, + 'name': { + 'type': 'string', + 'required': True, + 'read_only': False, + 'label': 'name', + 'max_length': 100 + }, + 'target': { + 'type': 'field', + 'required': True, + 'read_only': False, + 'label': 'Target object' + } + } + } + } + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, expected) + + class TestOverriddenGetObject(TestCase): """ Test cases for a RetrieveUpdateDestroyAPIView that does NOT use the From 8904f179d1bc925d52001497e92b9cd509e65bd5 Mon Sep 17 00:00:00 2001 From: Emanuele Pucciarelli Date: Sun, 30 Mar 2014 12:06:03 +0200 Subject: [PATCH 104/153] Stray unicode string marker removed --- rest_framework/tests/test_generics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py index 0cadc5dee..b4ae20219 100644 --- a/rest_framework/tests/test_generics.py +++ b/rest_framework/tests/test_generics.py @@ -458,7 +458,7 @@ class TestFKInstanceView(TestCase): 'type': 'integer', 'required': False, 'read_only': True, - 'label': u'ID' + 'label': 'ID' }, 'name': { 'type': 'string', From 3560796bbff33917df9f8e6885328467c7f809f9 Mon Sep 17 00:00:00 2001 From: Ravi Kotecha Date: Mon, 31 Mar 2014 11:38:26 +0100 Subject: [PATCH 105/153] 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 106/153] 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 107/153] 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 108/153] 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 109/153] 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 110/153] 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 111/153] 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 112/153] 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 113/153] 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 114/153] 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 115/153] 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 116/153] 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 117/153] 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 118/153] 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 119/153] 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 120/153] 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 121/153] 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 122/153] 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 123/153] 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 124/153] 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 125/153] 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 126/153] 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 127/153] 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 128/153] 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 129/153] 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 130/153] 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 131/153] 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 132/153] 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 133/153] 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 134/153] 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**: From fc44cd8d6a358ac6b5fd5155f69d032c7656ff4b Mon Sep 17 00:00:00 2001 From: Emanuele Pucciarelli Date: Tue, 29 Apr 2014 21:45:57 +0200 Subject: [PATCH 135/153] Sync test result w/ new label --- rest_framework/tests/test_generics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py index b4ae20219..4a3b67f65 100644 --- a/rest_framework/tests/test_generics.py +++ b/rest_framework/tests/test_generics.py @@ -471,7 +471,7 @@ class TestFKInstanceView(TestCase): 'type': 'field', 'required': True, 'read_only': False, - 'label': 'Target object' + 'label': 'Target' } } } From 295a4ab62d9af9ad7f74792c6543a1cf35cee2f9 Mon Sep 17 00:00:00 2001 From: Emanuele Pucciarelli Date: Tue, 29 Apr 2014 22:16:11 +0200 Subject: [PATCH 136/153] Added help_text to expected response in test --- rest_framework/tests/test_generics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/tests/test_generics.py b/rest_framework/tests/test_generics.py index 4a3b67f65..57d327cc6 100644 --- a/rest_framework/tests/test_generics.py +++ b/rest_framework/tests/test_generics.py @@ -471,7 +471,8 @@ class TestFKInstanceView(TestCase): 'type': 'field', 'required': True, 'read_only': False, - 'label': 'Target' + 'label': 'Target', + 'help_text': 'Target' } } } From c15dab903d3759578449279cc034d766d362d41f Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Thu, 1 May 2014 10:18:16 +0100 Subject: [PATCH 137/153] Mark strings in AuthTokenSerializer as translatable --- rest_framework/authtoken/serializers.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py index 60a3740e7..995f2e646 100644 --- a/rest_framework/authtoken/serializers.py +++ b/rest_framework/authtoken/serializers.py @@ -1,4 +1,6 @@ from django.contrib.auth import authenticate +from django.utils.translation import ugettext_lazy as _ + from rest_framework import serializers @@ -15,10 +17,13 @@ class AuthTokenSerializer(serializers.Serializer): if user: if not user.is_active: - raise serializers.ValidationError('User account is disabled.') + msg = _('User account is disabled.') + raise serializers.ValidationError() attrs['user'] = user return attrs else: - raise serializers.ValidationError('Unable to login with provided credentials.') + msg = _('Unable to login with provided credentials.') + raise serializers.ValidationError(msg) else: - raise serializers.ValidationError('Must include "username" and "password"') + msg = _('Must include "username" and "password"') + raise serializers.ValidationError(msg) From ccf3c508bd6750073ea3bbaefff567b92880df73 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Fri, 2 May 2014 21:58:49 +0100 Subject: [PATCH 138/153] Fix missing message in ValidationError --- rest_framework/authtoken/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py index 995f2e646..99e99ae3d 100644 --- a/rest_framework/authtoken/serializers.py +++ b/rest_framework/authtoken/serializers.py @@ -18,7 +18,7 @@ class AuthTokenSerializer(serializers.Serializer): if user: if not user.is_active: msg = _('User account is disabled.') - raise serializers.ValidationError() + raise serializers.ValidationError(msg) attrs['user'] = user return attrs else: From 4e33ff05d9aabee0a90bfb0ef8ce58a5d274b9a2 Mon Sep 17 00:00:00 2001 From: Lucian Mocanu Date: Sun, 4 May 2014 00:12:08 +0200 Subject: [PATCH 139/153] Automatically set the field name as value for the HTML `id` attribute on the rendered widget. --- rest_framework/fields.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 8cdc55515..e67338499 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -154,7 +154,12 @@ class Field(object): def widget_html(self): if not self.widget: return '' - return self.widget.render(self._name, self._value) + + attrs = {} + if 'id' not in self.widget.attrs: + attrs['id'] = self._name + + return self.widget.render(self._name, self._value, attrs=attrs) def label_tag(self): return '' % (self._name, self.label) From cdc7d19034170e5d775166763e6df1220e131d35 Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Mon, 5 May 2014 14:41:10 +0200 Subject: [PATCH 140/153] Added missing "to" word --- docs/tutorial/1-serialization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 979c4a3e3..dbe693ed5 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -104,7 +104,7 @@ Don't forget to sync the database for the first time. ## Creating a Serializer class -The first thing we need to get started on our Web API is provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similar to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following. +The first thing we need to get started on our Web API is to provide a way of serializing and deserializing the snippet instances into representations such as `json`. We can do this by declaring serializers that work very similar to Django's forms. Create a file in the `snippets` directory named `serializers.py` and add the following. from django.forms import widgets from rest_framework import serializers From 05fc974dc961de6d4e11b7baf51f7b3791c06711 Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Mon, 5 May 2014 14:44:54 +0200 Subject: [PATCH 141/153] Added missing "the" word --- docs/tutorial/1-serialization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index dbe693ed5..55b194576 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -143,7 +143,7 @@ The first thing we need to get started on our Web API is to provide a way of ser # Create new instance return Snippet(**attrs) -The first part of serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data. +The first part of the serializer class defines the fields that get serialized/deserialized. The `restore_object` method defines how fully fledged instances get created when deserializing data. Notice that we can also use various attributes that would typically be used on form fields, such as `widget=widgets.Textarea`. These can be used to control how the serializer should render when displayed as an HTML form. This is particularly useful for controlling how the browsable API should be displayed, as we'll see later in the tutorial. From 9e3ba939e152c2eb96d3c9b4460a3d4ce76931cd Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Mon, 5 May 2014 20:27:44 +0200 Subject: [PATCH 142/153] Removed superfluous "./"s --- docs/tutorial/4-authentication-and-permissions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 432371f34..491df1608 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -44,11 +44,11 @@ When that's all done we'll need to update our database tables. Normally we'd create a database migration in order to do that, but for the purposes of this tutorial, let's just delete the database and start again. rm tmp.db - python ./manage.py syncdb + python manage.py syncdb You might also want to create a few different users, to use for testing the API. The quickest way to do this will be with the `createsuperuser` command. - python ./manage.py createsuperuser + python manage.py createsuperuser ## Adding endpoints for our User models From 708c7b3a816c3c2df7847695044ef852dc89e72c Mon Sep 17 00:00:00 2001 From: Lucian Mocanu Date: Tue, 6 May 2014 14:17:51 +0200 Subject: [PATCH 143/153] Added test case to check if the proper attributes are set on html widgets. --- rest_framework/tests/test_fields.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index e127feef9..03f79cf4d 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -4,6 +4,7 @@ General serializer field tests. from __future__ import unicode_literals import datetime +import re from decimal import Decimal from uuid import uuid4 from django.core import validators @@ -103,6 +104,16 @@ class BasicFieldTests(TestCase): keys = list(field.to_native(ret).keys()) self.assertEqual(keys, ['c', 'b', 'a', 'z']) + def test_widget_html_attributes(self): + """ + Make sure widget_html() renders the correct attributes + """ + r = re.compile('(\S+)=["\']?((?:.(?!["\']?\s+(?:\S+)=|[>"\']))+.)["\']?') + form = TimeFieldModelSerializer().data + attributes = r.findall(form.fields['clock'].widget_html()) + self.assertIn(('name', 'clock'), attributes) + self.assertIn(('id', 'clock'), attributes) + class DateFieldTest(TestCase): """ From 9dc5e15e5afaf806378bb52ea2134f8dec2af386 Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Tue, 6 May 2014 13:00:41 +0200 Subject: [PATCH 144/153] Added missing "the" word --- docs/tutorial/6-viewsets-and-routers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 870632f1b..0a7c33639 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -21,7 +21,7 @@ First of all let's refactor our `UserList` and `UserDetail` views into a single queryset = User.objects.all() serializer_class = UserSerializer -Here we've used `ReadOnlyModelViewSet` class to automatically provide the default 'read-only' operations. We're still setting the `queryset` and `serializer_class` attributes exactly as we did when we were using regular views, but we no longer need to provide the same information to two separate classes. +Here we've used the `ReadOnlyModelViewSet` class to automatically provide the default 'read-only' operations. We're still setting the `queryset` and `serializer_class` attributes exactly as we did when we were using regular views, but we no longer need to provide the same information to two separate classes. Next we're going to replace the `SnippetList`, `SnippetDetail` and `SnippetHighlight` view classes. We can remove the three views, and again replace them with a single class. From beb7253a961870a37833b7df6d1dfd3e8c1db778 Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Tue, 6 May 2014 15:06:38 +0200 Subject: [PATCH 145/153] Removed unnecessary "that" --- docs/tutorial/6-viewsets-and-routers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 0a7c33639..dad71601a 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -85,7 +85,7 @@ In the `urls.py` file we bind our `ViewSet` classes into a set of concrete views Notice how we're creating multiple views from each `ViewSet` class, by binding the http methods to the required action for each view. -Now that we've bound our resources into concrete views, that we can register the views with the URL conf as usual. +Now that we've bound our resources into concrete views, we can register the views with the URL conf as usual. urlpatterns = format_suffix_patterns(patterns('snippets.views', url(r'^$', 'api_root'), From e033a0b9a0d655666385cd9831c7f1279573b47f Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Tue, 6 May 2014 15:07:40 +0200 Subject: [PATCH 146/153] Replaced singular "is" by plural "are" --- docs/tutorial/6-viewsets-and-routers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index dad71601a..04b42f2e7 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -138,7 +138,7 @@ You can review the final [tutorial code][repo] on GitHub, or try out a live exam ## Onwards and upwards -We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here's a few places you can start: +We've reached the end of our tutorial. If you want to get more involved in the REST framework project, here are a few places you can start: * Contribute on [GitHub][github] by reviewing and submitting issues, and making pull requests. * Join the [REST framework discussion group][group], and help build the community. From 11115fde9cc8f70dfd85ce937893d67fd061f3c1 Mon Sep 17 00:00:00 2001 From: Elliott Date: Wed, 7 May 2014 11:37:20 -0700 Subject: [PATCH 147/153] Add colon to time zone offset in readable_datetime_formats --- 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..9f53a0000 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -62,7 +62,7 @@ def get_component(obj, attr_name): def readable_datetime_formats(formats): format = ', '.join(formats).replace(ISO_8601, - 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]') + 'YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]') return humanize_strptime(format) From 0ff474d7c4882a31f8fb133caa82d0368b0406c2 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 8 May 2014 11:20:03 +0200 Subject: [PATCH 148/153] Updated failing test from #1575 --- rest_framework/tests/test_fields.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index e127feef9..894b5b3cf 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -312,7 +312,7 @@ class DateTimeFieldTest(TestCase): f.from_native('04:61:59') except validators.ValidationError as e: self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: " - "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]"]) + "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]"]) else: self.fail("ValidationError was not properly raised") @@ -326,7 +326,7 @@ class DateTimeFieldTest(TestCase): f.from_native('04 -- 31') except validators.ValidationError as e: self.assertEqual(e.messages, ["Datetime has wrong format. Use one of these formats instead: " - "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HHMM|-HHMM|Z]"]) + "YYYY-MM-DDThh:mm[:ss[.uuuuuu]][+HH:MM|-HH:MM|Z]"]) else: self.fail("ValidationError was not properly raised") From 8ecb778cd23d5d561f2e9f4a3561bb1647257a89 Mon Sep 17 00:00:00 2001 From: Corey Farwell Date: Sun, 11 May 2014 20:29:01 -0700 Subject: [PATCH 149/153] Enable testing on Python 3.4 --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bd6d2539a..0c9b44553 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ python: - "2.7" - "3.2" - "3.3" + - "3.4" env: - DJANGO="https://www.djangoproject.com/download/1.7b2/tarball/" @@ -41,4 +42,7 @@ matrix: env: DJANGO="django==1.4.11" - python: "3.3" env: DJANGO="django==1.3.7" - + - python: "3.4" + env: DJANGO="django==1.4.11" + - python: "3.4" + env: DJANGO="django==1.3.7" From 768f537dcbb5d4f7429a74556559047bfd6f3078 Mon Sep 17 00:00:00 2001 From: Giorgos Logiotatidis Date: Thu, 15 May 2014 15:34:31 +0300 Subject: [PATCH 150/153] Typo fix. --- docs/api-guide/serializers.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 7ee060af4..0044f0701 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -464,7 +464,7 @@ For more specific requirements such as specifying a different lookup for each fi model = Account fields = ('url', 'account_name', 'users', 'created') -## Overiding the URL field behavior +## Overriding the URL field behavior The name of the URL field defaults to 'url'. You can override this globally, by using the `URL_FIELD_NAME` setting. @@ -478,7 +478,7 @@ You can also override this on a per-serializer basis by using the `url_field_nam **Note**: The generic view implementations normally generate a `Location` header in response to successful `POST` requests. Serializers using `url_field_name` option will not have this header automatically included by the view. If you need to do so you will ned to also override the view's `get_success_headers()` method. -You can also overide the URL field's view name and lookup field without overriding the field explicitly, by using the `view_name` and `lookup_field` options, like so: +You can also override the URL field's view name and lookup field without overriding the field explicitly, by using the `view_name` and `lookup_field` options, like so: class AccountSerializer(serializers.HyperlinkedModelSerializer): class Meta: From e5556079fc2559916d62b766dc9776b03dc4256b Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 16 May 2014 00:50:16 +0200 Subject: [PATCH 151/153] Updated tox with Python 2.4 --- tox.ini | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index e21210058..35a108e5e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,10 +1,22 @@ [tox] downloadcache = {toxworkdir}/cache/ -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 +envlist = + py3.4-django1.7,py3.3-django1.7,py3.2-django1.7,py2.7-django1.7, + py3.4-django1.6,py3.3-django1.6,py3.2-django1.6,py2.7-django1.6,py2.6-django1.6, + py3.4-django1.5,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.4-django1.7] +basepython = python3.4 +deps = https://www.djangoproject.com/download/1.7b2/tarball/ + django-filter==0.7 + defusedxml==0.3 + Pillow==2.3.0 + [testenv:py3.3-django1.7] basepython = python3.3 deps = https://www.djangoproject.com/download/1.7b2/tarball/ @@ -30,6 +42,13 @@ deps = https://www.djangoproject.com/download/1.7b2/tarball/ django-guardian==1.1.1 Pillow==2.3.0 +[testenv:py3.4-django1.6] +basepython = python3.3 +deps = Django==1.6.3 + django-filter==0.7 + defusedxml==0.3 + Pillow==2.3.0 + [testenv:py3.3-django1.6] basepython = python3.3 deps = Django==1.6.3 @@ -66,6 +85,13 @@ deps = Django==1.6.3 django-guardian==1.1.1 Pillow==2.3.0 +[testenv:py3.4-django1.5] +basepython = python3.3 +deps = django==1.5.6 + django-filter==0.7 + defusedxml==0.3 + Pillow==2.3.0 + [testenv:py3.3-django1.5] basepython = python3.3 deps = django==1.5.6 From b370fb40b6bc0fd3f597fb8c2db59f0ca57a7ccd Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 16 May 2014 01:06:34 +0200 Subject: [PATCH 152/153] Typo in the Python version. --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 35a108e5e..279f79cc4 100644 --- a/tox.ini +++ b/tox.ini @@ -43,7 +43,7 @@ deps = https://www.djangoproject.com/download/1.7b2/tarball/ Pillow==2.3.0 [testenv:py3.4-django1.6] -basepython = python3.3 +basepython = python3.4 deps = Django==1.6.3 django-filter==0.7 defusedxml==0.3 @@ -86,7 +86,7 @@ deps = Django==1.6.3 Pillow==2.3.0 [testenv:py3.4-django1.5] -basepython = python3.3 +basepython = python3.4 deps = django==1.5.6 django-filter==0.7 defusedxml==0.3 From a704d5a206238c65765c1f02eb053e461675dda2 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 16 May 2014 01:20:40 +0200 Subject: [PATCH 153/153] Fixed tests for python 3.4 --- rest_framework/tests/test_views.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/rest_framework/tests/test_views.py b/rest_framework/tests/test_views.py index 65c7e50ea..77b113ee5 100644 --- a/rest_framework/tests/test_views.py +++ b/rest_framework/tests/test_views.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import sys import copy from django.test import TestCase from rest_framework import status @@ -11,6 +12,11 @@ from rest_framework.views import APIView factory = APIRequestFactory() +if sys.version_info[:2] >= (3, 4): + JSON_ERROR = 'JSON parse error - Expecting value:' +else: + JSON_ERROR = 'JSON parse error - No JSON object could be decoded' + class BasicView(APIView): def get(self, request, *args, **kwargs): @@ -48,7 +54,7 @@ def sanitise_json_error(error_dict): of json. """ ret = copy.copy(error_dict) - chop = len('JSON parse error - No JSON object could be decoded') + chop = len(JSON_ERROR) ret['detail'] = ret['detail'][:chop] return ret @@ -61,7 +67,7 @@ class ClassBasedViewIntegrationTests(TestCase): request = factory.post('/', 'f00bar', content_type='application/json') response = self.view(request) expected = { - 'detail': 'JSON parse error - No JSON object could be decoded' + 'detail': JSON_ERROR } self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(sanitise_json_error(response.data), expected) @@ -76,7 +82,7 @@ class ClassBasedViewIntegrationTests(TestCase): request = factory.post('/', form_data) response = self.view(request) expected = { - 'detail': 'JSON parse error - No JSON object could be decoded' + 'detail': JSON_ERROR } self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(sanitise_json_error(response.data), expected) @@ -90,7 +96,7 @@ class FunctionBasedViewIntegrationTests(TestCase): request = factory.post('/', 'f00bar', content_type='application/json') response = self.view(request) expected = { - 'detail': 'JSON parse error - No JSON object could be decoded' + 'detail': JSON_ERROR } self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(sanitise_json_error(response.data), expected) @@ -105,7 +111,7 @@ class FunctionBasedViewIntegrationTests(TestCase): request = factory.post('/', form_data) response = self.view(request) expected = { - 'detail': 'JSON parse error - No JSON object could be decoded' + 'detail': JSON_ERROR } self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(sanitise_json_error(response.data), expected)