aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml13
-rwxr-xr-xdocs/api-guide/authentication.md20
-rw-r--r--docs/topics/contributing.md18
-rw-r--r--docs/tutorial/4-authentication-and-permissions.md2
-rw-r--r--rest_framework/compat.py20
-rw-r--r--rest_framework/relations.py7
-rw-r--r--rest_framework/renderers.py11
-rwxr-xr-xrest_framework/runtests/runtests.py4
-rw-r--r--rest_framework/templates/rest_framework/base.html2
-rw-r--r--rest_framework/test.py8
-rw-r--r--rest_framework/tests/models.py7
-rw-r--r--rest_framework/tests/serializers.py8
-rw-r--r--rest_framework/tests/test_filters.py58
-rw-r--r--rest_framework/tests/test_genericrelations.py10
-rw-r--r--rest_framework/tests/test_htmlrenderer.py8
-rw-r--r--rest_framework/tests/test_nullable_fields.py30
-rw-r--r--rest_framework/tests/test_pagination.py32
-rw-r--r--rest_framework/tests/test_relations_nested.py4
-rw-r--r--rest_framework/tests/test_renderers.py16
-rw-r--r--rest_framework/tests/test_serializer.py37
-rw-r--r--rest_framework/tests/views.py8
-rw-r--r--rest_framework/throttling.py2
-rw-r--r--rest_framework/views.py2
23 files changed, 235 insertions, 92 deletions
diff --git a/.travis.yml b/.travis.yml
index 18fe66ab..c165d8d5 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,20 +7,23 @@ python:
- "3.3"
env:
- - DJANGO="django==1.6"
+ - DJANGO="https://www.djangoproject.com/download/1.7a2/tarball/"
+ - DJANGO="django==1.6.2"
- DJANGO="django==1.5.5"
- DJANGO="django==1.4.10"
- DJANGO="django==1.3.7"
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-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"
- - "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"
+ - "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:
@@ -28,6 +31,8 @@ script:
matrix:
exclude:
+ - python: "2.6"
+ env: DJANGO="https://www.djangoproject.com/download/1.7a2/tarball/"
- python: "3.2"
env: DJANGO="django==1.4.10"
- python: "3.2"
diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md
index f992b0ce..88a7a011 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)
---
@@ -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
diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md
index 5a5d1a80..18a05050 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?
@@ -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
@@ -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
diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md
index bdc6b579..432371f3 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:
diff --git a/rest_framework/compat.py b/rest_framework/compat.py
index d283e2f5..3089b7fb 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/relations.py b/rest_framework/relations.py
index 19dc9d6e..308545ce 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
@@ -176,9 +177,9 @@ class RelatedField(WritableField):
return
value = self.get_default_value()
- 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/renderers.py b/rest_framework/renderers.py
index e8afc26d..7cf1c051 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/runtests/runtests.py b/rest_framework/runtests/runtests.py
index da36d23f..2daaae4e 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()
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html
index d19d5a2b..7067ee2f 100644
--- a/rest_framework/templates/rest_framework/base.html
+++ b/rest_framework/templates/rest_framework/base.html
@@ -118,7 +118,7 @@
</div>
<div class="response-info">
<pre class="prettyprint"><div class="meta nocode"><b>HTTP {{ response.status_code }} {{ response.status_text }}</b>{% autoescape off %}
-{% for key, val in response.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>
+{% for key, val in response_headers.items %}<b>{{ key }}:</b> <span class="lit">{{ val|break_long_headers|urlize_quoted_links }}</span>
{% endfor %}
</div>{{ content|urlize_quoted_links }}</pre>{% endautoescape %}
</div>
diff --git a/rest_framework/test.py b/rest_framework/test.py
index 234d10a4..75cb4d0b 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,6 +72,13 @@ class APIRequestFactory(DjangoRequestFactory):
return ret, content_type
+ 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)
return self.generic('POST', path, data, content_type, **extra)
diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py
index 32a726c0..bf988312 100644
--- a/rest_framework/tests/models.py
+++ b/rest_framework/tests/models.py
@@ -168,3 +168,10 @@ class NullableOneToOneSource(RESTFrameworkModel):
class BasicModelSerializer(serializers.ModelSerializer):
class Meta:
model = BasicModel
+
+
+# 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/serializers.py b/rest_framework/tests/serializers.py
new file mode 100644
index 00000000..cc943c7d
--- /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_filters.py b/rest_framework/tests/test_filters.py
index 18188186..dd5d8e42 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):
@@ -128,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]
@@ -136,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]
@@ -151,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]
@@ -184,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]
@@ -192,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]
@@ -200,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()]
@@ -209,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
@@ -234,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)
@@ -265,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)
@@ -281,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)
@@ -315,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,
@@ -332,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,
@@ -348,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,
@@ -396,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,
@@ -415,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,
@@ -434,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,
@@ -503,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,
@@ -566,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:
@@ -596,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_genericrelations.py b/rest_framework/tests/test_genericrelations.py
index 2d341344..fa09c9e6 100644
--- a/rest_framework/tests/test_genericrelations.py
+++ b/rest_framework/tests/test_genericrelations.py
@@ -4,8 +4,10 @@ from django.contrib.contenttypes.generic import GenericRelation, GenericForeignK
from django.db import models
from django.test import TestCase
from rest_framework import serializers
+from rest_framework.compat import python_2_unicode_compatible
+@python_2_unicode_compatible
class Tag(models.Model):
"""
Tags have a descriptive slug, and are attached to an arbitrary object.
@@ -15,10 +17,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 +29,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 +41,7 @@ class Note(models.Model):
text = models.TextField()
tags = GenericRelation(Tag)
- def __unicode__(self):
+ def __str__(self):
return 'Note: %s' % self.text
diff --git a/rest_framework/tests/test_htmlrenderer.py b/rest_framework/tests/test_htmlrenderer.py
index 8957a43c..514d9e2b 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)
@@ -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')
diff --git a/rest_framework/tests/test_nullable_fields.py b/rest_framework/tests/test_nullable_fields.py
new file mode 100644
index 00000000..6ee55c00
--- /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<pk>\d+)/$', NullableFKSourceDetail.as_view(), name='object-detail'),
+)
+
+
+class NullableForeignKeyTests(APITestCase):
+ """
+ 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_fk(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/test_pagination.py b/rest_framework/tests/test_pagination.py
index cadb515f..24c1ba39 100644
--- a/rest_framework/tests/test_pagination.py
+++ b/rest_framework/tests/test_pagination.py
@@ -9,14 +9,18 @@ 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()
+# Helper function to split arguments out of an url
+def split_arguments_from_url(url):
+ if '?' not in url:
+ return url
-class FilterableItem(models.Model):
- text = models.CharField(max_length=100)
- decimal = models.DecimalField(max_digits=4, decimal_places=2)
- date = models.DateField()
+ path, args = url.split('?')
+ args = dict(r.split('=') for r in args.split('&'))
+ return path, args
class RootView(generics.ListCreateAPIView):
@@ -84,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)
@@ -93,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)
@@ -146,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)
@@ -155,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)
@@ -164,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)
@@ -191,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)
@@ -200,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)
@@ -209,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)
@@ -317,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])
@@ -345,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])
diff --git a/rest_framework/tests/test_relations_nested.py b/rest_framework/tests/test_relations_nested.py
index d393b0c3..4d9da489 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_renderers.py b/rest_framework/tests/test_renderers.py
index fb33df2c..c7bf772e 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}'
@@ -601,6 +613,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):
diff --git a/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py
index a2013749..198c269f 100644
--- a/rest_framework/tests/test_serializer.py
+++ b/rest_framework/tests/test_serializer.py
@@ -14,6 +14,26 @@ 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
@@ -1548,15 +1568,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:
@@ -1629,14 +1640,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:
diff --git a/rest_framework/tests/views.py b/rest_framework/tests/views.py
new file mode 100644
index 00000000..3917b74a
--- /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
diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py
index a946d837..efa9fb94 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)
diff --git a/rest_framework/views.py b/rest_framework/views.py
index 02a6e25a..2cf9b220 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()