From 3d0292e1cdcf56c490c7ee4777724620e76cad11 Mon Sep 17 00:00:00 2001 From: Ollie Walsh Date: Fri, 14 Aug 2015 12:16:57 +0100 Subject: [PATCH 001/457] Do not ignore overridden View.get_view_name() in breadcrumbs --- rest_framework/utils/breadcrumbs.py | 3 +-- tests/test_utils.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/rest_framework/utils/breadcrumbs.py b/rest_framework/utils/breadcrumbs.py index 950e9695b..5d8a18cf2 100644 --- a/rest_framework/utils/breadcrumbs.py +++ b/rest_framework/utils/breadcrumbs.py @@ -32,8 +32,7 @@ def get_breadcrumbs(url, request=None): # Don't list the same view twice in a row. # Probably an optional trailing slash. if not seen or seen[-1] != view: - suffix = getattr(view, 'suffix', None) - name = view_name_func(cls, suffix) + name = cls().get_view_name() insert_url = preserve_builtin_query_params(prefix + url, request) breadcrumbs_list.insert(0, (name, insert_url)) seen.append(view) diff --git a/tests/test_utils.py b/tests/test_utils.py index 062f78e11..5e2823b96 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -31,10 +31,15 @@ class NestedResourceRoot(APIView): class NestedResourceInstance(APIView): pass +class CustomNameResourceInstance(APIView): + def get_view_name(self): + return "Foo" + urlpatterns = [ url(r'^$', Root.as_view()), url(r'^resource/$', ResourceRoot.as_view()), + url(r'^resource/customname$', CustomNameResourceInstance.as_view()), url(r'^resource/(?P[0-9]+)$', ResourceInstance.as_view()), url(r'^resource/(?P[0-9]+)/$', NestedResourceRoot.as_view()), url(r'^resource/(?P[0-9]+)/(?P[A-Za-z]+)$', NestedResourceInstance.as_view()), @@ -75,6 +80,17 @@ class BreadcrumbTests(TestCase): ] ) + def test_resource_instance_customname_breadcrumbs(self): + url = '/resource/customname' + self.assertEqual( + get_breadcrumbs(url), + [ + ('Root', '/'), + ('Resource Root', '/resource/'), + ('Foo', '/resource/customname') + ] + ) + def test_nested_resource_breadcrumbs(self): url = '/resource/123/' self.assertEqual( From 332c30afb9f710653843c6d6746be613a9fd638d Mon Sep 17 00:00:00 2001 From: Ollie Walsh Date: Fri, 14 Aug 2015 12:20:25 +0100 Subject: [PATCH 002/457] Lint --- rest_framework/utils/breadcrumbs.py | 3 --- tests/test_utils.py | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/rest_framework/utils/breadcrumbs.py b/rest_framework/utils/breadcrumbs.py index 5d8a18cf2..8f68d9300 100644 --- a/rest_framework/utils/breadcrumbs.py +++ b/rest_framework/utils/breadcrumbs.py @@ -9,11 +9,8 @@ def get_breadcrumbs(url, request=None): tuple of (name, url). """ from rest_framework.reverse import preserve_builtin_query_params - from rest_framework.settings import api_settings from rest_framework.views import APIView - view_name_func = api_settings.VIEW_NAME_FUNCTION - def breadcrumbs_recursive(url, breadcrumbs_list, prefix, seen): """ Add tuples of (name, url) to the breadcrumbs list, diff --git a/tests/test_utils.py b/tests/test_utils.py index 5e2823b96..781aedb84 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -31,6 +31,7 @@ class NestedResourceRoot(APIView): class NestedResourceInstance(APIView): pass + class CustomNameResourceInstance(APIView): def get_view_name(self): return "Foo" @@ -89,7 +90,7 @@ class BreadcrumbTests(TestCase): ('Resource Root', '/resource/'), ('Foo', '/resource/customname') ] - ) + ) def test_nested_resource_breadcrumbs(self): url = '/resource/123/' From c8ca3a1c0571ab12a5c4e07307998fafba577ec4 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 11 Feb 2016 07:14:45 +0100 Subject: [PATCH 003/457] Release notes for 3.3.3 --- docs/topics/release-notes.md | 66 ++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index d21f8e51b..d339e0f70 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,36 @@ You can determine your currently installed version using `pip freeze`: ## 3.3.x series +### 3.3.3 + +**Date**: [12th February 2016][3.3.3-milestone]. + +* Remove version string from templates. Thanks to @blag for the report and fixes. ([#3878][gh3878], [#3913][gh3913], [#3912][gh3912]) +* Fixes vertical html layout for `BooleanField`. Thanks to Mikalai Radchuk for the fix. ([#3910][gh3910]) +* Silenced deprecation warnings on Django 1.8. Thanks to Simon Charette for the fix. ([#3903][gh3903]) +* Internationalization for authtoken. Thanks to Michael Nacharov for the fix. ([#3887][gh3887]) +* Fix `Token` model as `abstract` when the authtoken application isn't declared. Thanks to Adam Thomas for the report. ([#3860][gh3860], [#3858][gh3858]) +* Improve Markdown version compatibility. Thanks to Michael J. Schultz for the fix. ([#3604][gh3604], [#3842][gh3842]) +* `QueryParameterVersioning` does not use `DEFAULT_VERSION` setting. Thanks to Brad Montgomery for the fix. ([#3833][gh3833]) +* Add an explicit `on_delete` on the models. Thanks to Mads Jensen for the fix. ([#3832][gh3832]) +* Fix `DateField.to_representation` to work with Python 2 unicode. Thanks to Mikalai Radchuk for the fix. ([#3819][gh3819]) +* Fixed `TimeField` not handling string times. Thanks to Areski Belaid for the fix. ([#3809][gh3809]) +* Avoid updates of `Meta.extra_kwargs`. Thanks to Kevin Massey for the report and fix. ([#3805][gh3805], [#3804][gh3804]) +* Fix nested validation error being rendered incorrectly. Thanks to Craig de Stigter for the fix. ([#3801][gh3801]) +* Document how to avoid CSRF and missing button issues with `django-crispy-forms`. Thanks to Emmanuelle Delescolle, José Padilla and Luis San Pablo for the report, analysis and fix. ([#3787][gh3787], [#3636][gh3636], [#3637][gh3637]) +* Improve Rest Framework Settings file setup time. Thanks to Miles Hutson for the report and Mads Jensen for the fix. ([#3786][gh3786], [#3815][gh3815]) +* Improve authtoken compatibility with Django 1.9. Thanks to S. Andrew Sheppard for the fix. ([#3785][gh3785]) +* Fix `Min/MaxValueValidator` transfer from a model's `DecimalField`. Thanks to Kevin Brown for the fix. ([#3774][gh3774]) +* Improve HTML title in the Browsable API. Thanks to Mike Lissner for the report and fix. ([#3769][gh3769]) +* Fix `AutoFilterSet` to inherit from `default_filter_set`. Thanks to Tom Linford for the fix. ([#3753][gh3753]) +* Fix transifex config to handle the new Chinese language codes. Thanks to @nypisces for the report and fix. ([#3739][gh3739]) +* `DateTimeField` does not handle empty values correctly. Thanks to Mick Parker for the report and fix. ([#3731][gh3731], [#3726][gh3728]) +* Raise error when setting a removed rest_framework setting. Thanks to Luis San Pablo for the fix. ([#3715][gh3715]) +* Add missing csrf_token in AdminRenderer post form. Thanks to Piotr Śniegowski for the fix. ([#3703][gh3703]) +* Refactored `_get_reverse_relationships()` to use correct `to_field`. Thanks to Benjamin Phillips for the fix. ([#3696][gh3696]) +* Document the use of `get_queryset` for `RelatedField`. Thanks to Ryan Hiebert for the fix. ([#3605][gh3605]) + + ### 3.3.2 **Date**: [14th December 2015][3.3.2-milestone]. @@ -370,6 +400,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [3.3.0-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.3.0+Release%22 [3.3.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.3.1+Release%22 [3.3.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.3.2+Release%22 +[3.3.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.3.3+Release%22 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 @@ -649,3 +680,38 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh3714]: https://github.com/tomchristie/django-rest-framework/issues/3714 [gh3718]: https://github.com/tomchristie/django-rest-framework/issues/3718 [gh3723]: https://github.com/tomchristie/django-rest-framework/issues/3723 + + +[gh3913]: https://github.com/tomchristie/django-rest-framework/issues/3913 +[gh3912]: https://github.com/tomchristie/django-rest-framework/issues/3912 +[gh3910]: https://github.com/tomchristie/django-rest-framework/issues/3910 +[gh3903]: https://github.com/tomchristie/django-rest-framework/issues/3903 +[gh3887]: https://github.com/tomchristie/django-rest-framework/issues/3887 +[gh3878]: https://github.com/tomchristie/django-rest-framework/issues/3878 +[gh3860]: https://github.com/tomchristie/django-rest-framework/issues/3860 +[gh3858]: https://github.com/tomchristie/django-rest-framework/issues/3858 +[gh3842]: https://github.com/tomchristie/django-rest-framework/issues/3842 +[gh3833]: https://github.com/tomchristie/django-rest-framework/issues/3833 +[gh3832]: https://github.com/tomchristie/django-rest-framework/issues/3832 +[gh3819]: https://github.com/tomchristie/django-rest-framework/issues/3819 +[gh3815]: https://github.com/tomchristie/django-rest-framework/issues/3815 +[gh3809]: https://github.com/tomchristie/django-rest-framework/issues/3809 +[gh3805]: https://github.com/tomchristie/django-rest-framework/issues/3805 +[gh3804]: https://github.com/tomchristie/django-rest-framework/issues/3804 +[gh3801]: https://github.com/tomchristie/django-rest-framework/issues/3801 +[gh3787]: https://github.com/tomchristie/django-rest-framework/issues/3787 +[gh3786]: https://github.com/tomchristie/django-rest-framework/issues/3786 +[gh3785]: https://github.com/tomchristie/django-rest-framework/issues/3785 +[gh3774]: https://github.com/tomchristie/django-rest-framework/issues/3774 +[gh3769]: https://github.com/tomchristie/django-rest-framework/issues/3769 +[gh3753]: https://github.com/tomchristie/django-rest-framework/issues/3753 +[gh3739]: https://github.com/tomchristie/django-rest-framework/issues/3739 +[gh3731]: https://github.com/tomchristie/django-rest-framework/issues/3731 +[gh3728]: https://github.com/tomchristie/django-rest-framework/issues/3726 +[gh3715]: https://github.com/tomchristie/django-rest-framework/issues/3715 +[gh3703]: https://github.com/tomchristie/django-rest-framework/issues/3703 +[gh3696]: https://github.com/tomchristie/django-rest-framework/issues/3696 +[gh3637]: https://github.com/tomchristie/django-rest-framework/issues/3637 +[gh3636]: https://github.com/tomchristie/django-rest-framework/issues/3636 +[gh3605]: https://github.com/tomchristie/django-rest-framework/issues/3605 +[gh3604]: https://github.com/tomchristie/django-rest-framework/issues/3604 From 72ecd32c6b891395d0df98517195e01e1239234e Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 12 Feb 2016 13:15:20 +0100 Subject: [PATCH 004/457] Use pandoc to generate a nice PyPI information page. --- requirements/requirements-packaging.txt | 3 +++ setup.py | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/requirements/requirements-packaging.txt b/requirements/requirements-packaging.txt index 8f62ee365..89819da20 100644 --- a/requirements/requirements-packaging.txt +++ b/requirements/requirements-packaging.txt @@ -6,3 +6,6 @@ twine==1.4.0 # Transifex client for managing translation resources. transifex-client==0.11 + +# Pandoc to have a nice pypi page +pypandoc diff --git a/setup.py b/setup.py index 8e9d0a9ab..364863137 100755 --- a/setup.py +++ b/setup.py @@ -7,6 +7,17 @@ import sys from setuptools import setup +try: + from pypandoc import convert + + def read_md(f): + return convert(f, 'rst') +except ImportError: + print("warning: pypandoc module not found, could not convert Markdown to RST") + + def read_md(f): + return open(f, 'r').read() + def get_version(package): """ @@ -68,6 +79,7 @@ setup( url='http://www.django-rest-framework.org', license='BSD', description='Web APIs for Django, made easy.', + long_description=read_md('README.md'), author='Tom Christie', author_email='tom@tomchristie.com', # SEE NOTE BELOW (*) packages=get_packages('rest_framework'), From f5822e7b7138622fb10d75c00cdf60a706a98d96 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 12 Feb 2016 13:23:08 +0100 Subject: [PATCH 005/457] Bump version to 3.3.3 --- 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 74c317f35..6f02b8039 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.3.2' +__version__ = '3.3.3' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2016 Tom Christie' From f48ccad581cbcc0466a9418c61d73f34a3721898 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 12 Feb 2016 13:43:18 +0100 Subject: [PATCH 006/457] Fail hard during publish if pandoc isn't available. --- setup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.py b/setup.py index 364863137..0fef70f1f 100755 --- a/setup.py +++ b/setup.py @@ -56,6 +56,10 @@ version = get_version('rest_framework') if sys.argv[-1] == 'publish': + try: + import pypandoc + except ImportError: + print("pypandoc not installed.\nUse `pip install pypandoc`.\nExiting.") if os.system("pip freeze | grep wheel"): print("wheel not installed.\nUse `pip install wheel`.\nExiting.") sys.exit() From 20d1fdba697a590307ef65626bfb574a0c92bc96 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 16 Feb 2016 09:29:48 +0100 Subject: [PATCH 007/457] Fix None UUID ForeignKey serialization --- docs/topics/release-notes.md | 1 + rest_framework/fields.py | 2 ++ tests/models.py | 15 +++++++++++++++ tests/test_relations_pk.py | 21 ++++++++++++++++++++- 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index b6794a817..9c8d9e978 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -45,6 +45,7 @@ You can determine your currently installed version using `pip freeze`: **Unreleased** * Dropped support for EOL Django 1.7 ([#3933][gh3933]) +* Fixed null foreign keys targeting UUIDField primary keys. ([#3936][gh3936]) ### 3.3.2 diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 6d5962c8e..c700b85e8 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -778,6 +778,8 @@ class UUIDField(Field): return data def to_representation(self, value): + if value is None: + return None if self.uuid_format == 'hex_verbose': return str(value) else: diff --git a/tests/models.py b/tests/models.py index 8ec274d8b..5d5d40968 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,5 +1,7 @@ from __future__ import unicode_literals +import uuid + from django.db import models from django.utils.translation import ugettext_lazy as _ @@ -46,6 +48,11 @@ class ForeignKeyTarget(RESTFrameworkModel): name = models.CharField(max_length=100) +class UUIDForeignKeyTarget(RESTFrameworkModel): + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4) + name = models.CharField(max_length=100) + + class ForeignKeySource(RESTFrameworkModel): name = models.CharField(max_length=100) target = models.ForeignKey(ForeignKeyTarget, related_name='sources', @@ -62,6 +69,14 @@ class NullableForeignKeySource(RESTFrameworkModel): on_delete=models.CASCADE) +class NullableUUIDForeignKeySource(RESTFrameworkModel): + name = models.CharField(max_length=100) + target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True, + related_name='nullable_sources', + verbose_name='Optional target object', + on_delete=models.CASCADE) + + # OneToOne class OneToOneTarget(RESTFrameworkModel): name = models.CharField(max_length=100) diff --git a/tests/test_relations_pk.py b/tests/test_relations_pk.py index 169f7d9c5..658357b2f 100644 --- a/tests/test_relations_pk.py +++ b/tests/test_relations_pk.py @@ -6,7 +6,8 @@ from django.utils import six from rest_framework import serializers from tests.models import ( ForeignKeySource, ForeignKeyTarget, ManyToManySource, ManyToManyTarget, - NullableForeignKeySource, NullableOneToOneSource, OneToOneTarget + NullableForeignKeySource, NullableOneToOneSource, + NullableUUIDForeignKeySource, OneToOneTarget, UUIDForeignKeyTarget ) @@ -43,6 +44,18 @@ class NullableForeignKeySourceSerializer(serializers.ModelSerializer): fields = ('id', 'name', 'target') +# Nullable UUIDForeignKey +class NullableUUIDForeignKeySourceSerializer(serializers.ModelSerializer): + target = serializers.PrimaryKeyRelatedField( + pk_field=serializers.UUIDField(), + queryset=UUIDForeignKeyTarget.objects.all(), + allow_empty=True) + + class Meta: + model = NullableUUIDForeignKeySource + fields = ('id', 'name', 'target') + + # Nullable OneToOne class NullableOneToOneTargetSerializer(serializers.ModelSerializer): class Meta: @@ -432,6 +445,12 @@ class PKNullableForeignKeyTests(TestCase): ] self.assertEqual(serializer.data, expected) + def test_null_uuid_foreign_key_serializes_as_none(self): + source = NullableUUIDForeignKeySource(name='Source') + serializer = NullableUUIDForeignKeySourceSerializer(source) + data = serializer.data + self.assertEqual(data["target"], None) + class PKNullableOneToOneTests(TestCase): def setUp(self): From cbb8d8d2546f1673dcd26c4a7f0836a4c10312f3 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 10 Feb 2016 10:07:11 +0100 Subject: [PATCH 008/457] Test deserialising data including `None` fk --- tests/test_relations_pk.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_relations_pk.py b/tests/test_relations_pk.py index 658357b2f..ba75bd94f 100644 --- a/tests/test_relations_pk.py +++ b/tests/test_relations_pk.py @@ -49,7 +49,7 @@ class NullableUUIDForeignKeySourceSerializer(serializers.ModelSerializer): target = serializers.PrimaryKeyRelatedField( pk_field=serializers.UUIDField(), queryset=UUIDForeignKeyTarget.objects.all(), - allow_empty=True) + allow_null=True) class Meta: model = NullableUUIDForeignKeySource @@ -451,6 +451,11 @@ class PKNullableForeignKeyTests(TestCase): data = serializer.data self.assertEqual(data["target"], None) + def test_nullable_uuid_foreign_key_is_valid_when_none(self): + data = {"name": "Source", "target": None} + serializer = NullableUUIDForeignKeySourceSerializer(data=data) + self.assertTrue(serializer.is_valid(), serializer.errors) + class PKNullableOneToOneTests(TestCase): def setUp(self): From 5e082314535d2f18c7596b7451de6b800ac37f71 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 17 Feb 2016 18:18:19 +0100 Subject: [PATCH 009/457] Remove informations about why the pagination didn't work. We remove a couple of informations to lower the exposition of our internals. --- rest_framework/pagination.py | 2 +- tests/test_pagination.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 1051dc5b2..de8777b96 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -186,7 +186,7 @@ class PageNumberPagination(BasePagination): template = 'rest_framework/pagination/numbers.html' - invalid_page_message = _('Invalid page "{page_number}": {message}.') + invalid_page_message = _('Invalid page.') def paginate_queryset(self, queryset, request, view=None): """ diff --git a/tests/test_pagination.py b/tests/test_pagination.py index c6caaf641..1c74e837c 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -113,7 +113,7 @@ class TestPaginationIntegration: response = self.view(request) assert response.status_code == status.HTTP_404_NOT_FOUND assert response.data == { - 'detail': 'Invalid page "0": That page number is less than 1.' + 'detail': 'Invalid page.' } def test_404_not_found_for_invalid_page(self): @@ -121,7 +121,7 @@ class TestPaginationIntegration: response = self.view(request) assert response.status_code == status.HTTP_404_NOT_FOUND assert response.data == { - 'detail': 'Invalid page "invalid": That page number is not an integer.' + 'detail': 'Invalid page.' } From c03c6c6e783b2d7c25c79bdadf2b12433dd88116 Mon Sep 17 00:00:00 2001 From: Luke Murphy Date: Wed, 24 Feb 2016 14:22:24 +0100 Subject: [PATCH 010/457] fix typo in relations docs --- docs/api-guide/relations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index ba6870bcf..b2a7d8e21 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -507,7 +507,7 @@ For example, given the following model for a tag, which has a generic relationsh def __unicode__(self): return self.tag_name -And the following two models, which may be have associated tags: +And the following two models, which may have associated tags: class Bookmark(models.Model): """ From a609e4e1ca98228bef7537d7770d7919ac25a047 Mon Sep 17 00:00:00 2001 From: Taranjeet Date: Fri, 26 Feb 2016 00:02:38 +0530 Subject: [PATCH 011/457] Docs: Fix repetitive word in the tutorial --- 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 239d54204..fd0783f07 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -325,7 +325,7 @@ Quit out of the shell... In another terminal window, we can test the server. -We can test our API using using [curl][curl] or [httpie][httpie]. Httpie is a user friendly http client that's written in Python. Let's install that. +We can test our API using [curl][curl] or [httpie][httpie]. Httpie is a user friendly http client that's written in Python. Let's install that. You can install httpie using pip: From 6ea6e37ac999025c8ee71fbc52764c1fd877c736 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Sat, 27 Feb 2016 00:51:35 +0100 Subject: [PATCH 012/457] Add missing migration file for #3887 --- .../migrations/0002_auto_20160226_1747.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 rest_framework/authtoken/migrations/0002_auto_20160226_1747.py diff --git a/rest_framework/authtoken/migrations/0002_auto_20160226_1747.py b/rest_framework/authtoken/migrations/0002_auto_20160226_1747.py new file mode 100644 index 000000000..5673f332f --- /dev/null +++ b/rest_framework/authtoken/migrations/0002_auto_20160226_1747.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +from django.conf import settings + + +class Migration(migrations.Migration): + + dependencies = [ + ('authtoken', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='token', + options={'verbose_name_plural': 'Tokens', 'verbose_name': 'Token'}, + ), + migrations.AlterField( + model_name='token', + name='created', + field=models.DateTimeField(verbose_name='Created', auto_now_add=True), + ), + migrations.AlterField( + model_name='token', + name='key', + field=models.CharField(verbose_name='Key', max_length=40, primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='token', + name='user', + field=models.OneToOneField(to=settings.AUTH_USER_MODEL, verbose_name='User', related_name='auth_token'), + ), + ] From 753f4dc4771b07fcfdfd438655a78c089551d019 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Sat, 27 Feb 2016 01:46:59 +0100 Subject: [PATCH 013/457] Fix sorting order. --- rest_framework/authtoken/migrations/0002_auto_20160226_1747.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/authtoken/migrations/0002_auto_20160226_1747.py b/rest_framework/authtoken/migrations/0002_auto_20160226_1747.py index 5673f332f..98e0bb670 100644 --- a/rest_framework/authtoken/migrations/0002_auto_20160226_1747.py +++ b/rest_framework/authtoken/migrations/0002_auto_20160226_1747.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -from django.db import migrations, models from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): From 05204333a58ef1cb7c5809e5b04875b6161d6e8c Mon Sep 17 00:00:00 2001 From: Liping Wang <1325266543@qq.com> Date: Mon, 29 Feb 2016 19:51:33 +0800 Subject: [PATCH 014/457] [FIX] "@api_view" calling error. "@api_view" usage error. --- docs/tutorial/5-relationships-and-hyperlinked-apis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index feef1fcf0..fb5f62fd3 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -11,7 +11,7 @@ Right now we have endpoints for 'snippets' and 'users', but we don't have a sing from rest_framework.reverse import reverse - @api_view(('GET',)) + @api_view(['GET',]) def api_root(request, format=None): return Response({ 'users': reverse('user-list', request=request, format=format), From 239815887dacb29bfafd7dae6bc39c113bf13789 Mon Sep 17 00:00:00 2001 From: Liping Wang <1325266543@qq.com> Date: Tue, 1 Mar 2016 15:46:20 +0800 Subject: [PATCH 015/457] remove comma remove comma --- docs/tutorial/5-relationships-and-hyperlinked-apis.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index fb5f62fd3..4b9347bfa 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -11,7 +11,7 @@ Right now we have endpoints for 'snippets' and 'users', but we don't have a sing from rest_framework.reverse import reverse - @api_view(['GET',]) + @api_view(['GET']) def api_root(request, format=None): return Response({ 'users': reverse('user-list', request=request, format=format), From f3b4cb59515f10fd93b5d66718d40a2e8975f19f Mon Sep 17 00:00:00 2001 From: Rex Kerr <(none)> Date: Sat, 27 Feb 2016 12:41:56 -0800 Subject: [PATCH 016/457] Fixes incorrect references to URLPathVersioning --- docs/api-guide/versioning.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/versioning.md b/docs/api-guide/versioning.md index bd63c974b..df5d5dfbc 100644 --- a/docs/api-guide/versioning.md +++ b/docs/api-guide/versioning.md @@ -143,7 +143,7 @@ Your URL conf must include a pattern that matches the version with a `'version'` ## NamespaceVersioning -To the client, this scheme is the same as `URLParameterVersioning`. The only difference is how it is configured in your Django application, as it uses URL namespacing, instead of URL keyword arguments. +To the client, this scheme is the same as `URLPathVersioning`. The only difference is how it is configured in your Django application, as it uses URL namespacing, instead of URL keyword arguments. GET /v1/something/ HTTP/1.1 Host: example.com @@ -165,7 +165,7 @@ In the following example we're giving a set of views two different possible URL url(r'^v2/bookings/', include('bookings.urls', namespace='v2')) ] -Both `URLParameterVersioning` and `NamespaceVersioning` are reasonable if you just need a simple versioning scheme. The `URLParameterVersioning` approach might be better suitable for small ad-hoc projects, and the `NamespaceVersioning` is probably easier to manage for larger projects. +Both `URLPathVersioning` and `NamespaceVersioning` are reasonable if you just need a simple versioning scheme. The `URLPathVersioning` approach might be better suitable for small ad-hoc projects, and the `NamespaceVersioning` is probably easier to manage for larger projects. ## HostNameVersioning From 03244291070e7f83c9ddbe0e965eeb11b1b56cb2 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 1 Mar 2016 10:39:13 +0100 Subject: [PATCH 017/457] Add #3968 to the release notes. --- docs/topics/release-notes.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index d339e0f70..12f4360b3 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -47,7 +47,7 @@ You can determine your currently installed version using `pip freeze`: * Remove version string from templates. Thanks to @blag for the report and fixes. ([#3878][gh3878], [#3913][gh3913], [#3912][gh3912]) * Fixes vertical html layout for `BooleanField`. Thanks to Mikalai Radchuk for the fix. ([#3910][gh3910]) * Silenced deprecation warnings on Django 1.8. Thanks to Simon Charette for the fix. ([#3903][gh3903]) -* Internationalization for authtoken. Thanks to Michael Nacharov for the fix. ([#3887][gh3887]) +* Internationalization for authtoken. Thanks to Michael Nacharov for the fix. ([#3887][gh3887], [#3968][gh3968]) * Fix `Token` model as `abstract` when the authtoken application isn't declared. Thanks to Adam Thomas for the report. ([#3860][gh3860], [#3858][gh3858]) * Improve Markdown version compatibility. Thanks to Michael J. Schultz for the fix. ([#3604][gh3604], [#3842][gh3842]) * `QueryParameterVersioning` does not use `DEFAULT_VERSION` setting. Thanks to Brad Montgomery for the fix. ([#3833][gh3833]) @@ -682,6 +682,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh3723]: https://github.com/tomchristie/django-rest-framework/issues/3723 +[gh3968]: https://github.com/tomchristie/django-rest-framework/issues/3968 [gh3913]: https://github.com/tomchristie/django-rest-framework/issues/3913 [gh3912]: https://github.com/tomchristie/django-rest-framework/issues/3912 [gh3910]: https://github.com/tomchristie/django-rest-framework/issues/3910 From ef8e7f168f9b88b6a59e0ddd44dea988f7030368 Mon Sep 17 00:00:00 2001 From: Luke Murphy Date: Tue, 1 Mar 2016 18:37:54 +0100 Subject: [PATCH 018/457] add rest-framework-generic-relations link to docs --- docs/api-guide/relations.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index b2a7d8e21..31a78f28f 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -580,9 +580,14 @@ The following third party packages are also available. The [drf-nested-routers package][drf-nested-routers] provides routers and relationship fields for working with nested resources. +## Rest Framework Generic Relations + +The [rest-framework-generic-relations][drf-nested-relations] library provides read/write serialization for generic foreign keys. + [cite]: http://lwn.net/Articles/193245/ [reverse-relationships]: https://docs.djangoproject.com/en/dev/topics/db/queries/#following-relationships-backward [routers]: http://www.django-rest-framework.org/api-guide/routers#defaultrouter [generic-relations]: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#id1 [2.2-announcement]: ../topics/2.2-announcement.md [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers +[drf-nested-relations]: https://github.com/Ian-Foote/rest-framework-generic-relations From e34a34e90b870424fc9a59952bfb80452d169306 Mon Sep 17 00:00:00 2001 From: Jared Lang Date: Wed, 24 Feb 2016 13:36:39 -0800 Subject: [PATCH 019/457] Fix empty pk detection in HyperlinkRelatedField.get_url This implementation allows detection of empty values that are non-nullable, allowing the field to return None values for such cases --- rest_framework/relations.py | 2 +- tests/test_relations.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 7e04e7e47..2b7906a59 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -280,7 +280,7 @@ class HyperlinkedRelatedField(RelatedField): attributes are not configured to correctly match the URL conf. """ # Unsaved objects will not yet have a valid URL. - if hasattr(obj, 'pk') and obj.pk is None: + if hasattr(obj, 'pk') and obj.pk in (None, ''): return None lookup_value = getattr(obj, self.lookup_field) diff --git a/tests/test_relations.py b/tests/test_relations.py index 240878394..a070ad6de 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -87,6 +87,18 @@ class TestProxiedPrimaryKeyRelatedField(APISimpleTestCase): assert representation == self.instance.pk.int +class TestHyperlinkedRelatedField(APISimpleTestCase): + def setUp(self): + self.field = serializers.HyperlinkedRelatedField( + view_name='example', read_only=True) + self.field.reverse = mock_reverse + self.field._context = {'request': True} + + def test_representation_unsaved_object_with_non_nullable_pk(self): + representation = self.field.to_representation(MockObject(pk='')) + assert representation is None + + class TestHyperlinkedIdentityField(APISimpleTestCase): def setUp(self): self.instance = MockObject(pk=1, name='foo') From 173c2f1e539026998ee6c515c119420fc0d2999a Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 7 Mar 2016 20:22:47 +0100 Subject: [PATCH 020/457] Release date update. --- docs/topics/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 12f4360b3..7834448c1 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -42,7 +42,7 @@ You can determine your currently installed version using `pip freeze`: ### 3.3.3 -**Date**: [12th February 2016][3.3.3-milestone]. +**Date**: [7th March 2016][3.3.3-milestone]. * Remove version string from templates. Thanks to @blag for the report and fixes. ([#3878][gh3878], [#3913][gh3913], [#3912][gh3912]) * Fixes vertical html layout for `BooleanField`. Thanks to Mikalai Radchuk for the fix. ([#3910][gh3910]) From 4399d601c5ca53e187aa80cbf7d1f0d4c9949070 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 7 Mar 2016 20:25:10 +0100 Subject: [PATCH 021/457] Translation updates. --- .../locale/ach/LC_MESSAGES/django.mo | Bin 513 -> 513 bytes .../locale/ach/LC_MESSAGES/django.po | 115 +++-- .../locale/ar/LC_MESSAGES/django.mo | Bin 4888 -> 4766 bytes .../locale/ar/LC_MESSAGES/django.po | 117 +++-- .../locale/be/LC_MESSAGES/django.mo | Bin 655 -> 655 bytes .../locale/be/LC_MESSAGES/django.po | 115 +++-- .../locale/ca/LC_MESSAGES/django.mo | Bin 9749 -> 9646 bytes .../locale/ca/LC_MESSAGES/django.po | 117 +++-- .../locale/ca_ES/LC_MESSAGES/django.mo | Bin 528 -> 528 bytes .../locale/ca_ES/LC_MESSAGES/django.po | 115 +++-- .../locale/cs/LC_MESSAGES/django.mo | Bin 9140 -> 9031 bytes .../locale/cs/LC_MESSAGES/django.po | 117 +++-- .../locale/da/LC_MESSAGES/django.mo | Bin 9682 -> 9881 bytes .../locale/da/LC_MESSAGES/django.po | 131 +++-- .../locale/da_DK/LC_MESSAGES/django.mo | Bin 529 -> 529 bytes .../locale/da_DK/LC_MESSAGES/django.po | 115 +++-- .../locale/de/LC_MESSAGES/django.mo | Bin 10465 -> 10364 bytes .../locale/de/LC_MESSAGES/django.po | 119 +++-- .../locale/el/LC_MESSAGES/django.mo | Bin 0 -> 512 bytes .../locale/el/LC_MESSAGES/django.po | 457 ++++++++++++++++++ .../locale/el_GR/LC_MESSAGES/django.mo | Bin 0 -> 527 bytes .../locale/el_GR/LC_MESSAGES/django.po | 457 ++++++++++++++++++ .../locale/en/LC_MESSAGES/django.mo | Bin 9770 -> 10002 bytes .../locale/en/LC_MESSAGES/django.po | 117 +++-- .../locale/en_AU/LC_MESSAGES/django.mo | Bin 532 -> 532 bytes .../locale/en_AU/LC_MESSAGES/django.po | 115 +++-- .../locale/en_CA/LC_MESSAGES/django.mo | Bin 529 -> 529 bytes .../locale/en_CA/LC_MESSAGES/django.po | 115 +++-- .../locale/en_US/LC_MESSAGES/django.mo | Bin 378 -> 378 bytes .../locale/en_US/LC_MESSAGES/django.po | 113 +++-- .../locale/es/LC_MESSAGES/django.mo | Bin 10509 -> 10782 bytes .../locale/es/LC_MESSAGES/django.po | 119 +++-- .../locale/et/LC_MESSAGES/django.mo | Bin 8836 -> 8732 bytes .../locale/et/LC_MESSAGES/django.po | 117 +++-- .../locale/fa/LC_MESSAGES/django.mo | Bin 0 -> 507 bytes .../locale/fa/LC_MESSAGES/django.po | 457 ++++++++++++++++++ .../locale/fa_IR/LC_MESSAGES/django.mo | Bin 0 -> 520 bytes .../locale/fa_IR/LC_MESSAGES/django.po | 457 ++++++++++++++++++ .../locale/fi/LC_MESSAGES/django.mo | Bin 10068 -> 9993 bytes .../locale/fi/LC_MESSAGES/django.po | 119 +++-- .../locale/fr/LC_MESSAGES/django.mo | Bin 10501 -> 10770 bytes .../locale/fr/LC_MESSAGES/django.po | 119 +++-- .../locale/fr_CA/LC_MESSAGES/django.mo | Bin 527 -> 527 bytes .../locale/fr_CA/LC_MESSAGES/django.po | 115 +++-- .../locale/gl/LC_MESSAGES/django.mo | Bin 515 -> 515 bytes .../locale/gl/LC_MESSAGES/django.po | 115 +++-- .../locale/gl_ES/LC_MESSAGES/django.mo | Bin 669 -> 669 bytes .../locale/gl_ES/LC_MESSAGES/django.po | 115 +++-- .../locale/he_IL/LC_MESSAGES/django.mo | Bin 528 -> 528 bytes .../locale/he_IL/LC_MESSAGES/django.po | 115 +++-- .../locale/hu/LC_MESSAGES/django.mo | Bin 9228 -> 9123 bytes .../locale/hu/LC_MESSAGES/django.po | 117 +++-- .../locale/id/LC_MESSAGES/django.mo | Bin 510 -> 510 bytes .../locale/id/LC_MESSAGES/django.po | 115 +++-- .../locale/it/LC_MESSAGES/django.mo | Bin 9512 -> 10359 bytes .../locale/it/LC_MESSAGES/django.po | 141 +++--- .../locale/ja/LC_MESSAGES/django.mo | Bin 11341 -> 11941 bytes .../locale/ja/LC_MESSAGES/django.po | 132 +++-- .../locale/ko_KR/LC_MESSAGES/django.mo | Bin 10148 -> 10022 bytes .../locale/ko_KR/LC_MESSAGES/django.po | 117 +++-- .../locale/mk/LC_MESSAGES/django.mo | Bin 10744 -> 10623 bytes .../locale/mk/LC_MESSAGES/django.po | 117 +++-- .../locale/nb/LC_MESSAGES/django.mo | Bin 9902 -> 9803 bytes .../locale/nb/LC_MESSAGES/django.po | 119 +++-- .../locale/nl/LC_MESSAGES/django.mo | Bin 9267 -> 9165 bytes .../locale/nl/LC_MESSAGES/django.po | 117 +++-- .../locale/nn/LC_MESSAGES/django.mo | Bin 524 -> 524 bytes .../locale/nn/LC_MESSAGES/django.po | 115 +++-- .../locale/no/LC_MESSAGES/django.mo | Bin 516 -> 516 bytes .../locale/no/LC_MESSAGES/django.po | 115 +++-- .../locale/pl/LC_MESSAGES/django.mo | Bin 10528 -> 10432 bytes .../locale/pl/LC_MESSAGES/django.po | 119 +++-- .../locale/pt/LC_MESSAGES/django.mo | Bin 517 -> 517 bytes .../locale/pt/LC_MESSAGES/django.po | 115 +++-- .../locale/pt_BR/LC_MESSAGES/django.mo | Bin 10343 -> 10238 bytes .../locale/pt_BR/LC_MESSAGES/django.po | 119 +++-- .../locale/pt_PT/LC_MESSAGES/django.mo | Bin 534 -> 534 bytes .../locale/pt_PT/LC_MESSAGES/django.po | 115 +++-- .../locale/ro/LC_MESSAGES/django.mo | Bin 556 -> 556 bytes .../locale/ro/LC_MESSAGES/django.po | 115 +++-- .../locale/ru/LC_MESSAGES/django.mo | Bin 11487 -> 11360 bytes .../locale/ru/LC_MESSAGES/django.po | 117 +++-- .../locale/sk/LC_MESSAGES/django.mo | Bin 9524 -> 9411 bytes .../locale/sk/LC_MESSAGES/django.po | 117 +++-- .../locale/sv/LC_MESSAGES/django.mo | Bin 10151 -> 10073 bytes .../locale/sv/LC_MESSAGES/django.po | 119 +++-- .../locale/tr/LC_MESSAGES/django.mo | Bin 10269 -> 10172 bytes .../locale/tr/LC_MESSAGES/django.po | 119 +++-- .../locale/tr_TR/LC_MESSAGES/django.mo | Bin 1933 -> 1933 bytes .../locale/tr_TR/LC_MESSAGES/django.po | 115 +++-- .../locale/uk/LC_MESSAGES/django.mo | Bin 590 -> 590 bytes .../locale/uk/LC_MESSAGES/django.po | 115 +++-- .../locale/vi/LC_MESSAGES/django.mo | Bin 510 -> 510 bytes .../locale/vi/LC_MESSAGES/django.po | 115 +++-- .../locale/zh-Hans/LC_MESSAGES/django.mo | Bin 9894 -> 9796 bytes .../locale/zh-Hans/LC_MESSAGES/django.po | 119 +++-- .../locale/zh-Hant/LC_MESSAGES/django.mo | Bin 529 -> 529 bytes .../locale/zh-Hant/LC_MESSAGES/django.po | 115 +++-- .../locale/zh_CN/LC_MESSAGES/django.mo | Bin 9895 -> 10007 bytes .../locale/zh_CN/LC_MESSAGES/django.po | 121 +++-- .../locale/zh_TW/LC_MESSAGES/django.mo | Bin 522 -> 522 bytes .../locale/zh_TW/LC_MESSAGES/django.po | 115 +++-- 102 files changed, 5322 insertions(+), 2036 deletions(-) create mode 100644 rest_framework/locale/el/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/el/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/el_GR/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/el_GR/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/fa/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/fa/LC_MESSAGES/django.po create mode 100644 rest_framework/locale/fa_IR/LC_MESSAGES/django.mo create mode 100644 rest_framework/locale/fa_IR/LC_MESSAGES/django.po diff --git a/rest_framework/locale/ach/LC_MESSAGES/django.mo b/rest_framework/locale/ach/LC_MESSAGES/django.mo index a1e8e6bc3273c26f9056a9b7973654150c7fb481..a91bc8b323901c1ec2e6b2cbbaba7c19085ab063 100644 GIT binary patch delta 41 ncmZom&`8(7T*1)7%G7w`Txo=Wxs|Eu#-lcj0M@b!N&o-= diff --git a/rest_framework/locale/ach/LC_MESSAGES/django.po b/rest_framework/locale/ach/LC_MESSAGES/django.po index 0a6f2cce5..bfb8b75cc 100644 --- a/rest_framework/locale/ach/LC_MESSAGES/django.po +++ b/rest_framework/locale/ach/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Acoli (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/ach/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: ach\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -212,137 +244,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" diff --git a/rest_framework/locale/ar/LC_MESSAGES/django.mo b/rest_framework/locale/ar/LC_MESSAGES/django.mo index 6a32487ecfca6f79807a3851b1e2e62bcf32fc0e..719bb0b318cd7893e394a3c335855cd27d5af81e 100644 GIT binary patch delta 834 zcmX}q&r4KM6u|K_^K8Z@b^JkTW9D;)vDA6=o>mT%M*d(V42p?aB|!@r2BRd!oeT|x zJ6#M#EB}BNQVeZGDB87fp=*iOMuHnj1oM0D8@zDmbI;?>d-t4kKlZLCOTSv{-U}nn zyv9rgL~h|okRQfB?7?HSvC|Uiz!B8_dF;eD=-?`j;}&M|KR&>0oyah*;640?(;_7~ zVUgj%)CGS-8C!`rQ9rPaN!&*ln?oWo9K$A@!5cV-SFnN+{Dk`duh@>ewRj)(z~Qha zS`taH&=Wt!0sMeh@kh-)93(!%7**)2_dhtvS0;G=5^em99`4{xv@ZE^3f~ex$2I(o z(>UKi5`15_Sm*&xaRtX3{gVB}`@|8_)RH|!t<*bA;djjAU(|gQQ9rXXJ|bSn1pYy- zXtddHZ5+FZbC@4xG0#E|uz~ubLtI9;#edNT_7k7rZS3W3y8jsl&}PzR)0Q5LR(67I znwcrIO3BQ)&M##&gHoDXNB`wAvztjKLe}SlQVD%GjntlSFmwB;lo@R`6>YMnspF!) zl12+BHn%}_OK%-#5&@>Op-vR4qimSZ76RQgr|T delta 923 zcmZ9~&r8!`9Ki99o6YHT*2L1S>~TvS*_boOWc^wRLbOYV>=H!67Y|{C;zb$5QAAM$ z4Y5N=hawC-$k-*QKOhF4g75cGmrfOQ>(Gxw@6R)-=mXF9^*j&H_xbU8o{gc$!{rZ= z)-@p#l!KIs7Lha-eSCugE?Hs6S#vH zM9LD{Ba)}%GU|Xw7{Rxw3%ti6{DDa{0g*90k5OF4Fs|VUKEwUkzz+O~I?q=;fT6Z# zJApCQmjV?%@fFma-^Wqh!G8R?Yj03woOT+cWM>vfXqQ-J3ZJ9FPnf}uP;eH`yJPNAl*f|{{6cnrT|0f)OqF5w*N_}8dc z^9zgEx3_uC4r+$)VmVFa36(?Gz*G1cbtB1r&55pKiS|p>iIV(%6qj%UpQ4U`hb=fr z;ri|3ab$fPy}K$23fD4YyU3)C!+goM4Ikeg3h%_;r$G)XB;`+#N#R-8&FfoW-}cHn zs~3GeNA#JpBOC2}c>ux*?5oh1FhHqB{n1*)-{v2V`+ZYSt!NxW@X-4 zT%@a13kMc_UD;G-JT;axnaO\n" "Language-Team: Arabic (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/ar/)\n" "MIME-Version: 1.0\n" @@ -18,43 +18,75 @@ msgstr "" "Language: ar\n" "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "اسم المستخدم/كلمة السر غير صحيحين." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "المستخدم غير مفعل او تم حذفه." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "حساب المستخدم غير مفعل." @@ -109,7 +141,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "هذا الحقل مطلوب." @@ -127,7 +159,7 @@ msgstr "\"{input}\" ليس قيمة منطقية." msgid "This field may not be blank." msgstr "لا يمكن لهذا الحقل ان يكون فارغاً." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "تأكد ان الحقل لا يزيد عن {max_length} محرف." @@ -213,137 +245,136 @@ msgstr "صيغة التاريخ و الوقت غير صحيحة. عليك أن msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "صيغة التاريخ غير صحيحة. عليك أن تستخدم واحدة من هذه الصيغ التالية: {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "صيغة الوقت غير صحيحة. عليك أن تستخدم واحدة من هذه الصيغ التالية: {format}." -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\" ليست واحدة من الخيارات الصالحة." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "لم يتم إرسال أي ملف." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "الملف الذي تم إرساله فارغ." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "تأكد ان اسم الملف لا يحوي أكثر من {max_length} محرف (الإسم المرسل يحوي {length} محرف)." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "رقم الصفحة \"{page_number}\" غير صالح : {message}." +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "معرف العنصر \"{pk_value}\" غير صالح - العنصر غير موجود." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "قيمة غير صالحة." diff --git a/rest_framework/locale/be/LC_MESSAGES/django.mo b/rest_framework/locale/be/LC_MESSAGES/django.mo index 457e2947968bbc7dc1ebf857a6b8239a04ebc03f..cf959f6d9fb4bf89764a8ab67ec323c64e3cfbc5 100644 GIT binary patch delta 41 ncmeBY?Pr}Zf!9pez*yJ7P{Gi`%GhG!Txo=WIZ$BZ(Nl~7+y)Bp delta 41 ocmeBY?Pr}Zf!9>m&`8(7T*1)7%G7w`Txo=Wxs|Eu#-pbg0o;5F_5c6? diff --git a/rest_framework/locale/be/LC_MESSAGES/django.po b/rest_framework/locale/be/LC_MESSAGES/django.po index 8dd5a6e9e..6fe59735f 100644 --- a/rest_framework/locale/be/LC_MESSAGES/django.po +++ b/rest_framework/locale/be/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Belarusian (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/be/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: be\n" "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -212,137 +244,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" diff --git a/rest_framework/locale/ca/LC_MESSAGES/django.mo b/rest_framework/locale/ca/LC_MESSAGES/django.mo index 06136491b8455aeedb64d6da2b84718255d46f0f..e9d5355e1f5641ec5e75b8c3d454e0b0b9fd0858 100644 GIT binary patch delta 1778 zcmYM!Sx8h-9LMp0>5QpqsikSwjP04r=#);HX_}VJlr2_N4?fw8Li(m}V z1|``;Py{xSJyx{$P((chEkw|Q?L@s)^!?3T>fqncId|@I&j0-HJsi3a48O=sJZ!X8 zL_Tq8xY-sAx%fvLon%&k8R$hnPQ_N7h&yl`9z$J!5y#^LEXE-$!rT#N_1J_>xG!$l z9@1IFft+NsI&8x=*oT$)6RWW_#Vm*&ScQYA-@jo93scP|V;5%P3DkrxILGfhen#Cd zYoys63!Amj(Tet+ z@e9^q)hO=6^R1gs9`4lvJdIk(0BQwK9KSioM~#lJ(2v@JCY+A#n2kN?#WR?NH!%^P zBFVBBSc>m4tW@P@#4jwu9L5dk!wsmdIf$CbW9Rp;&N!Kiwb#?I0_#y*vfUXEpceKH zbzTCq@uCMcfx0a6ucGMWz$}bn8NS4Im@$UQVTa=$EN6TQ?+jxLP!l`BvZvq`^x|{J zKRAuChgIqKCRD~cP|w?+L;fr1T;{+M{D{jiKuL#iFY5Qlj;YK>=hdV3um|VjeO!$$ zmRpG%(2o&p#oJhf`J_P;ZbsF}#xR`{I)@yuVjbhpsMo~D*($Cstj0cU#Amn)CryZ- zzYXUx9z?d)zN5}_v0Obkh$`}K)XGnyG8w){M^*eAbwPzEzM`E-z1d~t&u%&6H>ex^ zK;~%q-grs_s0Xce#=9N+Q46?^B*BJI6L1&Az8hgHr<1^eAf{joDkJMrE7^z2#BtOO zUSTc%LT%N&N%52(Kuzc@@@F@k@mtguCGbjVD{D|&v=#G}!}D}>!ClnKKA={TNyTWd zYf-7)k1D3yxE?>D9@xx9npis)2LoHj-FK@CwAf4w^cz&%dVkOHFrB z(-%VT-U=c}C|49^ECmqTE9J9J16mSMMevo3HO-J>w59B=EvGY^NGH^W6I$AQ4QR?Y z`)+QchM=g-MR*BShe}v`p{DAa6YItQ6#`$rSWZzd>rMlmGw# delta 1891 zcmaLXTWAzl9LMp0G#5=w)WjrOYdT51CF{o7=(=`utwznwOQR8cu^}~!Y%z(ND##iJ zv{2|nUo@Z;HBc!?L8Ad*T0%5vZSk?tmY^*PEoh;p4~4c+`~A&Mee8k%e$JVl%Q^q^ zpSjvG(&;@c%p5V=9^z%<%UNbe@a-&qXiknvyB6*=9)F*AnwI6jA2f$SsgZE2c|HNU!ne=m1ow8aje7- zu@FB;O=u!GJ{|ZRb-&7aW>NFZdgy3HBd8mHfgAA;Y{IqqtQrTg2FI}gAEC~h!PQvJ zL1k(;R^u_$@lkZ~YplV)unnu`n>Fx!8=}K8yN)m6WH6pat>_7AB?Sx8hET^_Q7b)! z+6oU>;YBRLyXfEpEW&><6AKrbkvuC!Z!Mh)I=WF8>cRw;;z@MzENYAHpeB;f$@;$< zHQt2U^B&xQDb&{72*yuP3oB<9Ia@{~ju1mrKdN9yrAT&HOiP$CZ@y4m^UL zIEnf{pIOKj)Ojgvz`OV=X0ps~Y{VFz#dUZe_n?-)2D(w1JLS<)v0OqG$2Wn0Vl(5_ zoTb+#i7KWstivC03l=Y<;P5SM#%oxQ&yel5Fw4|=ji?8IfGYOusD*pq(FxO;{YpBe z5mZ(8p)S0DTG?Zy4sDrZM%Tiq@pjY=_ak$*)5tCD66%2y!T4!lZh3kkRY+1i>!PEX z^dnie^EexCU@qQ7W#$%YMbA(f$*4#dRVy|zK7iV?Pm$!<@2CmQ=T)I=A=G#WYC-Se z20K6z(9vE^qb@9>LUe(PTG?*YN=~5m{4=b_8B|f#aMGLDje5WsvNF4m%Iq&^+OjIW z*XePA%_FuDyb}J`q8WLuUbNPrr#q!Ez^fnXQi9Di&=paa*?ILy(EyNr`Z;&#qf>(E6HZ0yF!T%@>whLmV0&|G40j>}WI`sSUd^C$cdf zT^DjAuA94SXSgeIJUNsc90<4czMqIYwQl6ad9ipjI{r>^|J=~j=)vSbuag`&J~etM O+1LAj&$?IkbN0VBQ_DaA diff --git a/rest_framework/locale/ca/LC_MESSAGES/django.po b/rest_framework/locale/ca/LC_MESSAGES/django.po index ce74a5f45..f23310d2c 100644 --- a/rest_framework/locale/ca/LC_MESSAGES/django.po +++ b/rest_framework/locale/ca/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Catalan (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/ca/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: ca\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "Header Basic invàlid. No hi ha disponibles les credencials." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "Header Basic invàlid. Les credencials no poden contenir espais." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "Header Basic invàlid. Les credencials no estan codificades correctament en base64." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Usuari/Contrasenya incorrectes." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "Usuari inactiu o esborrat." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "Token header invàlid. No s'han indicat les credencials." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "Token header invàlid. El token no ha de contenir espais." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "Token header invàlid. El token no pot contenir caràcters invàlids." -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Token invàlid." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "Compte d'usuari desactivat." @@ -108,7 +140,7 @@ msgstr "Media type \"{media_type}\" no suportat." msgid "Request was throttled." msgstr "La petició ha estat limitada pel número màxim de peticions definit." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "Aquest camp és obligatori." @@ -126,7 +158,7 @@ msgstr "\"{input}\" no és un booleà." msgid "This field may not be blank." msgstr "Aquest camp no pot estar en blanc." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Aquest camp no pot tenir més de {max_length} caràcters." @@ -212,137 +244,136 @@ msgstr "El Datetime té un format incorrecte. Utilitzi un d'aquests formats: {fo msgid "Expected a datetime but got a date." msgstr "S'espera un Datetime però s'ha rebut un Date." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "El Date té un format incorrecte. Utilitzi un d'aquests formats: {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "S'espera un Date però s'ha rebut un Datetime." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "El Time té un format incorrecte. Utilitzi un d'aquests formats: {format}." -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "La durada té un format incorrecte. Utilitzi un d'aquests formats: {format}." -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\" no és una opció vàlida." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "S'espera una llista d'ítems però s'ha rebut el tipus \"{input_type}\"." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "Aquesta selecció no pot estar buida." -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "\"{input}\" no és un path vàlid." -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "No s'ha enviat cap fitxer." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "Les dades enviades no són un fitxer. Comproveu l'encoding type del formulari." -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "No s'ha pogut determinar el nom del fitxer." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "El fitxer enviat està buit." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "El nom del fitxer ha de tenir com a màxim {max_length} caràcters (en té {length})." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Envieu una imatge vàlida. El fitxer enviat no és una imatge o és una imatge corrompuda." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "Aquesta llista no pot estar buida." -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "S'espera un diccionari però s'ha rebut el tipus \"{input_type}\"." -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Pàgina invàlida \"{page_number}\": {message}." +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "Cursor invàlid." -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "PK invàlida \"{pk_value}\" - l'objecte no existeix." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Tipus incorrecte. S'espera el valor d'una PK, s'ha rebut {data_type}." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Hyperlink invàlid - Cap match d'URL." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Hyperlink invàlid - Match d'URL incorrecta." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Hyperlink invàlid - L'objecte no existeix." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Tipus incorrecte. S'espera una URL, s'ha rebut {data_type}." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "L'objecte amb {slug_name}={value} no existeix." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Valor invàlid." diff --git a/rest_framework/locale/ca_ES/LC_MESSAGES/django.mo b/rest_framework/locale/ca_ES/LC_MESSAGES/django.mo index 5c2dd5baaa80fcaa53006459b11bf6172a69b04e..96a0b1ebb8a981b8d762b8c0e8319ee1e54c53a6 100644 GIT binary patch delta 41 ncmbQhGJ$2n1YR>;17lqSLj^+%D`Sg^bEOdi=0JgsN4*&V+X@PC delta 41 pcmbQhGJ$2n1YT2JLnB=Sa|J^SD^uf%bEOdi=2oVr8;^Q30sz~93U>ei diff --git a/rest_framework/locale/ca_ES/LC_MESSAGES/django.po b/rest_framework/locale/ca_ES/LC_MESSAGES/django.po index fad1d9024..c4f9df6cb 100644 --- a/rest_framework/locale/ca_ES/LC_MESSAGES/django.po +++ b/rest_framework/locale/ca_ES/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Catalan (Spain) (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/ca_ES/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: ca_ES\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -212,137 +244,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" diff --git a/rest_framework/locale/cs/LC_MESSAGES/django.mo b/rest_framework/locale/cs/LC_MESSAGES/django.mo index f0dc3dfcf4d6a44d050affcc7d5be2e8bd3c53e5..459b3f01f444090391c6d45f9f44edd71036c949 100644 GIT binary patch delta 1558 zcmYM!OGs2v9LMp$=4e)9zS2sk#ACcwc(VbVPT-5ylW3NQz39ea+<;@a7Q?9PUSke^!BTW2nU$ayTk$Bi;S{#xBDRW9N$K1`cCB$7f?sqSkqi)%XWDW7W#& zdWW!<``b8!W=_08Kc=iQE5kla!4a&*tGE?kp(3(`MVRR{tH5T|b*HfrZ=s;S#zrd9@i2Wkpzy)u{6Wn2qOA8yiD~ zd_Of5BG%&+w%`ZU!fwJu`t2mD z7%w2%u{*d8pP)AM5mlsd6p*SuADOZa)chbSWx>k~_}Ejv6ykTti(qlAq8m7|7k#LO zFY&b<@8UlEgj(3+j@C#w^08xlsc5fYBZjdVf1)1D!!8Q+{-0n_$HX;M?&eUD$RG?p z^kW~MMNZiqR^fN#V+G_T4z-3-r25bX)wEs{T@m)u)hL9eQV%E@HFTaWQo1FmZL5kLbTw^TP47TF zJ)Pb{kNxFF5?sx(S_a)0`s^r7Q@ZrFsm`bd^M_oC*{&Uo)D&wq#nw&N>(LqRO-xD( ok2n_`PG4z-x3t3J_SDvSYeUB~a{kZMFcS`CbSH!i4mQij z3RHjF(9igGo|946m8n{X%|M?ap%srVEXktCPdbj(AwhtY$3Q15qQ5TBs> z8OdQR=A*95IOH z8fs$qP@(^Y8m}^)`0IhqoT#A%mBW)b8?T~5{xNZ#JG^fNwK$XZU8vli#{ztZUd&|H zYq1i?;OT@nv54zleAUk^Era+ggzq!^R-Bn-Hi2tD>c!Qll{cX_TN_Tsi|EGtSb;yW z8bd656}I6*e2v=7(^#b@+J;JT52}6SXyUIN?Wgy7coTQvSL9vW!Yp#I1vT>yRB9fe z-usN&WW^+QA~vFSc^fXnN4N&_2&>xnqsHk*vOxTa`(jMT8CZ)7?P1iwH?SCApax1O zJY`so8sI3hE$lgJ6MjIlWf_ER8s?!UR)gBKQPfv(40+UAuP1KwqH^~I`Po$7QmCtt z55uCU0ruevJd6D7Glw~t_I&mMEo=KY32beK)4zW?={Xk{IkhtDv8 zzfn015C%o$AkM}sxDwwYk63_w%))g@lO4x?s6Le{MOFQ&C z_#cIuwQ|kAjH*&jWsh32E-0igzHt|!RfD z!Qxw3OHhJSs47~5ioT~LDxZoipeFu~@teEe(-ajHUx! zLD#z!pU>?ra(hY~uRq``F7S9go~gguG)<7 ez`{=F_R;#-jz%ZeytDgEbo<`_4R&Vyh15S5YQBH~ diff --git a/rest_framework/locale/cs/LC_MESSAGES/django.po b/rest_framework/locale/cs/LC_MESSAGES/django.po index 15cb86c80..43c571130 100644 --- a/rest_framework/locale/cs/LC_MESSAGES/django.po +++ b/rest_framework/locale/cs/LC_MESSAGES/django.po @@ -9,8 +9,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Czech (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/cs/)\n" "MIME-Version: 1.0\n" @@ -19,43 +19,75 @@ msgstr "" "Language: cs\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "Chybná hlavička. Nebyly poskytnuty přihlašovací údaje." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "Chybná hlavička. Přihlašovací údaje by neměly obsahovat mezery." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "Chybná hlavička. Přihlašovací údaje nebyly správně zakódovány pomocí base64." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Chybné uživatelské jméno nebo heslo." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "Uživatelský účet je neaktivní nebo byl smazán." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "Chybná hlavička tokenu. Nebyly zadány přihlašovací údaje." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "Chybná hlavička tokenu. Přihlašovací údaje by neměly obsahovat mezery." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Chybný token." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "Uživatelský účet je uzamčen." @@ -110,7 +142,7 @@ msgstr "Nepodporovaný media type \"{media_type}\" v požadavku." msgid "Request was throttled." msgstr "Požadavek byl limitován kvůli omezení počtu požadavků za časovou periodu." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "Toto pole je vyžadováno." @@ -128,7 +160,7 @@ msgstr "\"{input}\" nelze použít jako typ boolean." msgid "This field may not be blank." msgstr "Toto pole nesmí být prázdné." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Zkontrolujte, že toto pole není delší než {max_length} znaků." @@ -214,137 +246,136 @@ msgstr "Chybný formát data a času. Použijte jeden z těchto formátů: {form msgid "Expected a datetime but got a date." msgstr "Bylo zadáno pouze datum bez času." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "Chybný formát data. Použijte jeden z těchto formátů: {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "Bylo zadáno datum a čas, místo samotného data." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "Chybný formát času. Použijte jeden z těchto formátů: {format}." -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\" není platnou možností." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "Byl očekáván seznam položek ale nalezen \"{input_type}\"." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "Nebyl zaslán žádný soubor." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "Zaslaná data neobsahují soubor. Zkontrolujte typ kódování ve formuláři." -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "Nebylo možné zjistit jméno souboru." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "Zaslaný soubor je prázdný." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "Zajistěte, aby jméno souboru obsahovalo maximálně {max_length} znaků (teď má {length} znaků)." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Nahrajte platný obrázek. Nahraný soubor buď není obrázkem nebo je poškozen." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "Byl očekáván slovník položek ale nalezen \"{input_type}\"." -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Chybné čislo stránky \"{page_number}\": {message}." +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "Chybný kurzor." -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "Chybný primární klíč \"{pk_value}\" - objekt neexistuje." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Chybný typ. Byl přijat typ {data_type} místo hodnoty primárního klíče." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Chybný odkaz - nebyla nalezena žádní shoda." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Chybný odkaz - byla nalezena neplatná shoda." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Chybný odkaz - objekt neexistuje." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Chybný typ. Byl přijat typ {data_type} místo očekávaného odkazu." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "Objekt s {slug_name}={value} neexistuje." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Chybná hodnota." diff --git a/rest_framework/locale/da/LC_MESSAGES/django.mo b/rest_framework/locale/da/LC_MESSAGES/django.mo index f6ac0f7d591fd6f98faae73be353464e85743a97..7820bdd3e93b02293df5e92d506df9ccfcbbc40e 100644 GIT binary patch delta 2273 zcmYk+TWl0n9LMp0FSLNP*p_lDwbPbb(QfIDQd)sRFHj1tb+Jl~DaD;Ro@ocIh|F?*p|KW@kMcnn+cGInFZ9cIlqi2D5q?!;Lv z$C5>6`Pha^s2_EH-1kk?{XW5U8tR{PaySv-i$PfnYJ_d52Su<6r*SiWi)~n(YgUaz z$j1)xrR$F2J$MB*b7_ms?!|Jf!5*x}1a@hvPt)0me>eTOK}(-d=3lnC?+!Ld`O4cwX?VuXOJ@4@2DI9jk-ZD z2U%Ew%dpBnZpRvqyHTY)h)VQx)OA<=r(_ zmb4OSM+XP77@x;_Jcyf^69kHiRnK2(H06HsMj+f{Dv? zl=%wox*dm6Q~Ew?@m)pT@K@A>s!CG3V>c?XXHkj2i_EHhkKOnO>H%9F%7-r?x!LRf z@flpLSIq@F`r$`pu$EDpdSR3xvuit1tKUU(wHHy_>m;%~>=KfjT|+j2Wl%r8yOtn3 z+g74#U5UJ6EQm_zF)Y#kAEBdN@Cqt{*HEkfG-}bDMa|I9NI7ji*~mU*khb6VCDf`u zg_`0yT!%#rQv>WnaUIbldA^;Zqgv0Q9$3rUXDfy@g&R>L)(%l)BD_^jzFzC| z-Ha*;FGz1@{L>y)d~5w^*J#J6u|PHcRLg=+LNlSZhiD}Rh)jan^hT@2#elsTVz*i^ zaeKR;1J${WP&w2zTPj66p&3-u8v5_~s`u?p3-4BYn9der2cd<@ogZs6M2LdjoLioAz~Bp2%+W>dLL*()g%Mi z2j&%aM%-w~8IDBbZfx8-QzoYu6{aWWa^Fs$-x~|LvB+q6>R?{gYIi91_!Ab4>x45o z6rFIL_}G{e9g2nBY70*68;Qh|pXPOBEbsSz9GMu8JNsN+8VNZ&g1tRcX@xK6GzMzw z12wfyZBui7)6|i|qW{mV=S*^@@Mczi@Z!0!tGr^amsZS8e!QwVJy~3GB7H${EJnUc zYo@fWk}H*zI~tz6cp?^xxTB66bB4!a&Ukz*R&8CQp>cOKl*}o6Bcr(AyTlpaKNNLz Sx$A_ViH0KKRC>oNYX1c!r3SnJ delta 2036 zcmaLXUu;ul7{~Ev?fx?7SYazN*v={-f7Y=wYq!p^vJDWn0SY4;V+3Io%;vw!g&E7a zm|Qp$W^yz{A$oyA76Kvkf-I0I8NsNDk_j=I$PF>kU;-*3APM^YwH3KAzRAo7H0rcHj%q17 zFz&|yo<>dR>)i3HIm;i&-meLx=9;}sM=SaSb>pwF84JtI+OZuw(ZvRw#0T+r)OmRf z7GfNgscpCz_vVhDz(&U3V*^erH(P@(*v9j1gbx4NS)7d*a^tJ075#}?NzL@Et*GNq zqgJ{HwH0Hi>yP6j_ytzsZ|Gogh1m>Tg!$NpE?Kk=I!mz^b;DuQh3{Yxk6XB}bWN}abzX7?`FH4ya6l9I7*!+}aR~-zQh3;j z&){x+2*1hs3oc{quQdBSkB6eJyUXlDxR}|6u@^NSLQUX9bZ{a_{^!#9fdfr=3m?UL zN?Log6?^a~_UeCB239eP81BO^Jf9nva(C5CC#uRvFpghh3l^~Kb=Z!ok$o;5rFsIj zhZk@GUPJ9+6?skIa%@Ex7vX7a#-DHn*02gqbPGO#$55HOiK?xI++5ekQ1?xtirzgy zM>9E#s^VW!Gp?*9a07PZ4%CBABK2rDkY`)IlO5OLJjM~!@y*B@EQR$rh9u9(Z7spKDbw=#_3jkSPEQp*TafN7 zo9V0Bn(TK%L$AQ|FYO+Dq5rkvP}~_B>`$dQG@R}!KkW1FR4nnrfnd5j(B=2WD@V(s z;mERZW6X&($D>VNqN@7eBe8fin%-G;yRf7$`NqKZT}fxa`)F2e`rzCOUu|mF_Wym_ Zq1qEZ@3~OK^Fr0>Q=xbL-ir&C{tZXh){g)H diff --git a/rest_framework/locale/da/LC_MESSAGES/django.po b/rest_framework/locale/da/LC_MESSAGES/django.po index 95bd0cb96..3d4e0ada2 100644 --- a/rest_framework/locale/da/LC_MESSAGES/django.po +++ b/rest_framework/locale/da/LC_MESSAGES/django.po @@ -3,14 +3,14 @@ # This file is distributed under the same license as the PACKAGE package. # # Translators: -# Mads Jensen , 2015 +# Mads Jensen , 2015-2016 # Mikkel Munch Mortensen <3xm@detfalskested.dk>, 2015 msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Danish (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/da/)\n" "MIME-Version: 1.0\n" @@ -19,43 +19,75 @@ msgstr "" "Language: da\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "Ugyldig basic header. Ingen legitimation angivet." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "Ugyldig basic header. Legitimationsstrenge må ikke indeholde mellemrum." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "Ugyldig basic header. Legitimationen er ikke base64 encoded på korrekt vis." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Ugyldigt brugernavn/kodeord." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "Inaktiv eller slettet bruger." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "Ugyldig token header." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "Ugyldig token header. Token-strenge må ikke indeholde mellemrum." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "Ugyldig token header. Token streng bør ikke indeholde ugyldige karakterer." -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Ugyldigt token." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "Brugerkontoen er deaktiveret." @@ -110,7 +142,7 @@ msgstr "Forespørgslens media type, \"{media_type}\", er ikke understøttet." msgid "Request was throttled." msgstr "Forespørgslen blev neddroslet." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "Dette felt er påkrævet." @@ -128,7 +160,7 @@ msgstr "\"{input}\" er ikke en tilladt boolsk værdi." msgid "This field may not be blank." msgstr "Dette felt må ikke være tomt." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Tjek at dette felt ikke indeholder flere end {max_length} tegn." @@ -214,137 +246,136 @@ msgstr "Datotid har et forkert format. Brug i stedet et af disse formater: {form msgid "Expected a datetime but got a date." msgstr "Forventede en datotid, men fik en dato." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "Dato har et forkert format. Brug i stedet et af disse formater: {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "Forventede en dato men fik en datotid." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "Klokkeslæt har forkert format. Brug i stedet et af disse formater: {format}. " -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "Varighed har forkert format. Brug istedet et af følgende formater: {format}." -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\" er ikke et gyldigt valg." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "Flere end {count} objekter..." -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "Forventede en liste, men fik noget af typen \"{input_type}\"." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "Dette valg kan være tomt." -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "\"{input}\" er ikke et gyldigt valg af adresse." -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "Ingen medsendt fil." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "Det medsendte data var ikke en fil. Tjek typen af indkodning på formularen." -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "Filnavnet kunne ikke afgøres." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "Den medsendte fil er tom." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "Sørg for at filnavnet er højst {max_length} langt (det er {length})." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Medsend et gyldigt billede. Den medsendte fil var enten ikke et billede eller billedfilen var ødelagt." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "Denne liste er muligvis ikke tom." -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "Forventede en dictionary, men fik noget af typen \"{input_type}\"." -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." -msgstr "" +msgstr "Værdi skal være gyldig JSON." -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" -msgstr "" +msgstr "Indsend." #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Ugyldig side \"{page_number}\": {message}." +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "Ugyldig cursor" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "Ugyldig primærnøgle \"{pk_value}\" - objektet findes ikke." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Ugyldig type. Forventet værdi er primærnøgle, fik {data_type}." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Ugyldigt hyperlink - intet URL match." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Ugyldigt hyperlink - forkert URL match." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Ugyldigt hyperlink - objektet findes ikke." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Forkert type. Forventede en URL-streng, fik {data_type}." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "Object med {slug_name}={value} findes ikke." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Ugyldig værdi." @@ -356,20 +387,20 @@ msgstr "Ugyldig data. Forventede en dictionary, men fik {datatype}." #: templates/rest_framework/admin.html:118 #: templates/rest_framework/base.html:128 msgid "Filters" -msgstr "" +msgstr "Filtre" #: templates/rest_framework/filters/django_filter.html:2 #: templates/rest_framework/filters/django_filter_crispyforms.html:4 msgid "Field filters" -msgstr "" +msgstr "Søgefiltre" #: templates/rest_framework/filters/ordering.html:3 msgid "Ordering" -msgstr "" +msgstr "Sortering" #: templates/rest_framework/filters/search.html:2 msgid "Search" -msgstr "" +msgstr "Søg" #: templates/rest_framework/horizontal/radio.html:2 #: templates/rest_framework/inline/radio.html:2 diff --git a/rest_framework/locale/da_DK/LC_MESSAGES/django.mo b/rest_framework/locale/da_DK/LC_MESSAGES/django.mo index 78170eeb6040287c2812b6f909fbc876fbac9462..0e1cc36efce4bf67188b4092a8d23fca1d107dfb 100644 GIT binary patch delta 41 ncmbQpGLdD%1YR>;17lqSLj^+%D`Sg^bEOdi=0JgsM|~Ip+gu8C delta 41 pcmbQpGLdD%1YT2JLnB=Sa|J^SD^uf%bEOdi=2oVr8;|-h0s!063VHwl diff --git a/rest_framework/locale/da_DK/LC_MESSAGES/django.po b/rest_framework/locale/da_DK/LC_MESSAGES/django.po index c198cfdf6..7ec27dee2 100644 --- a/rest_framework/locale/da_DK/LC_MESSAGES/django.po +++ b/rest_framework/locale/da_DK/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Danish (Denmark) (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/da_DK/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: da_DK\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -212,137 +244,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" diff --git a/rest_framework/locale/de/LC_MESSAGES/django.mo b/rest_framework/locale/de/LC_MESSAGES/django.mo index 0c1034bb047c2a0899cf7fd5ae0b9e8388647a6f..ce4458e0019ecbb7a953ea7b87b69c1cbad72051 100644 GIT binary patch delta 2010 zcmYM#TWnNC9LMqh!n(`Fw%Y1eS_-Ev<)YoXrL>enVb?C#rG+Zgwm#5?OWY`Iz<`iI zPSo_pkcioUkq`-y2MGxg_d&cQL@@S+2I5OeiUfSJfsgu>^RVa*h?moSC;eLq&> z`xwVO*5gn38s5WI*f`bGwf!_!GjI`;7@cO;gKKaxp2Vd%iWw}MZq|ldP=7y;>+x5t z#o5oAm0|~KLOrPQ_X0md-R}o%)>0qSC}JSa7mKo`s1xH|)TwVzUMu zL_T(eFI{&M=i_zM&V@?MUdCEXU>7!^i)*#j=V-L!pU7{ln|fHz=kP z!8$C*`k=#TEQ>40sqAT?Bwa2@EmT&$*iUe`>+?k!nGJ;b)A@TX$;W#99QDs zn8qagryC5RRyc;*nW8FxBJ*(>{m#ITaUK0()bT1M5fsusRLHg=i?(+#g&$xmx|eBa zCKIR|tefp0tD{J`Yy^{d3$=ySj-S1oP`UCEHe&%5%5l{Fs@Xo>FNdU$eUE(XO3=TD zaqm^besgbY2<$>-?Jm?-A4C1{EV4N^g7r9#d@RP%P=5)s2HSyJ@ep#L>@aGgCr~?b z1xYI##VVcuJ2dpNn@WJSMJ1>U>u@5rm= zGw1r(wWB7!1)F)keMmz|a{-&Mh~>9q3g1Q-wemmE!7v%AmDZuQJdGjzzvvz+TD=ye zIBR07sm#R-BY4m3+xlrk&CaL|@FqLq4@5ct{|6jh0+^RBXw&iy3J>V?cpYvPEceA^4`?7-#TXQ=LW3h^9 Kg_`P%(fDFqW_Hgx zGynO|;g8;PeVK1p<{mZ5F6x!k-xir=@bqFnD5rvE<@hN&IFDE3(j{hVFoGr6hr0e= zEW<+>!fC9+GuVn3u^n43b=U8e+|3v zJg&nHOU+hd95tamsPTioFQe`^i%~5#xXg?{YvO~&Stn|RanyqjU_HKtoAGD72}8@x zs__Bj&tBj|zdM1~;g6`u737<($6D0*ZmhvWSglaMOXFso!zK)`@NPJYrSu>6`^Qi# zc?Gqy)4mt{@j!vM^7W{#X+m9p2VRR0Vi7)%4!(n#LKD#D@{)zfsDGyUWg36JeLgIe|jr$nTULHsFzedfx z;BxPWRp`*)ikesol}yL562HMPE?H$ZfHAxRpZ0y-_Y_7LKaZc}n5`-z{_YK!-c`B* zsjNK;VFmW1_V9o|{tW7OCy{5`1=NHuVh=`n>L5jYn`NeuvufO&Q{_ zoyI{N#53525#qE3A41*WL(~cjS9_6ZLrr858*$3_Biu&+FVr#HMj~iSkD#{fS!D4x zjV+jYk48O>1=K|9)_OO16tyLvA=_t*9q)LSph7r+)%YZ8OWwySoX04Z5H?-kjk@pS zsEM6L(#qBnUYZ#T)6hU1LzqOKX<6SHRQ7&}3i$$Zp{?L3D2XDd2X`Z1*@WMJ4q21U zqE`MB=HsuZ3Fi>LJe~hA4f4$zk*HZO>g6|r3e`9&gop7)Jc_sBS=2HC{45g5^Blj?>W0PohFMhf1E+EV~xd*n}_PUi==l^6g}*gDKQX z52HfbgvLfu8prLte{hIJv4ao@eg z*h~FyQHb~-;>K8EP$4V<_kgx#&jo5TI(n_tPO5T7Maju&bPI26_XVZ{q2yKxdbp=d z+a9O(P?ZuYeI9l_DnBowYIkB(9dYG0hgW5}n^*m`Hd6<(F9r*9!?dys!P1#8f>XgY ziF=Yx#l-z^*U0{TL&@}HMa-GlmmC{oXmX}2e=e{r8VW~3H5;99eJom=Z7wS7@J8x2 z5{XvUQ1ZGGW8BPv`aAJHk9dp_fL#f1w)B5oTckdlerk%z;?m+X<{-irlof@5) UE-K9{cjC#^`2Wu0O=mXvH`B1{d;kCd diff --git a/rest_framework/locale/de/LC_MESSAGES/django.po b/rest_framework/locale/de/LC_MESSAGES/django.po index 3fc38afb0..c30dd7555 100644 --- a/rest_framework/locale/de/LC_MESSAGES/django.po +++ b/rest_framework/locale/de/LC_MESSAGES/django.po @@ -13,9 +13,9 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-08 18:25+0000\n" -"Last-Translator: Fabian Büchler \n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" +"Last-Translator: Xavier Ordoquy \n" "Language-Team: German (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/de/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -23,43 +23,75 @@ msgstr "" "Language: de\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "Ungültiger basic header. Keine Zugangsdaten angegeben." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "Ungültiger basic header. Zugangsdaten sollen keine Leerzeichen enthalten." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "Ungültiger basic header. Zugangsdaten sind nicht korrekt mit base64 kodiert." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Ungültiger Benutzername/Passwort" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "Benutzer inaktiv oder gelöscht." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "Ungültiger token header. Keine Zugangsdaten angegeben." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "Ungültiger token header. Zugangsdaten sollen keine Leerzeichen enthalten." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "Ungültiger Token Header. Tokens dürfen keine ungültigen Zeichen enthalten." -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Ungültiges Token" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "Benutzerkonto ist gesperrt." @@ -114,7 +146,7 @@ msgstr "Nicht unterstützter Medientyp \"{media_type}\" in der Anfrage." msgid "Request was throttled." msgstr "Die Anfrage wurde gedrosselt." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "Dieses Feld ist erforderlich." @@ -132,7 +164,7 @@ msgstr "\"{input}\" ist kein gültiger Wahrheitswert." msgid "This field may not be blank." msgstr "Dieses Feld darf nicht leer sein." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Stelle sicher, dass dieses Feld nicht mehr als {max_length} Zeichen lang ist." @@ -218,137 +250,136 @@ msgstr "Datums- und Zeitangabe hat das falsche Format. Nutze stattdessen eines d msgid "Expected a datetime but got a date." msgstr "Erwarte eine Datums- und Zeitangabe, erhielt aber ein Datum." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "Datum hat das falsche Format. Nutze stattdessen eines dieser Formate: {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "Erwarte ein Datum, erhielt aber eine Datums- und Zeitangabe." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "Zeitangabe hat das falsche Format. Nutze stattdessen eines dieser Formate: {format}." -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "Laufzeit hat das falsche Format. Benutze stattdessen eines dieser Formate {format}." -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\" ist keine gültige Option." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "Mehr als {count} Ergebnisse" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "Erwarte eine Liste von Elementen, erhielt aber den Typ \"{input_type}\"." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "Diese Auswahl darf nicht leer sein" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "\"{input}\" ist ein ungültiger Pfad Wahl." -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "Es wurde keine Datei übermittelt." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "Die übermittelten Daten stellen keine Datei dar. Prüfe den Kodierungstyp im Formular." -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "Der Dateiname konnte nicht ermittelt werden." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "Die übermittelte Datei ist leer." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "Stelle sicher, dass dieser Dateiname höchstens {max_length} Zeichen lang ist (er hat {length})." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Lade ein gültiges Bild hoch. Die hochgeladene Datei ist entweder kein Bild oder ein beschädigtes Bild." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "Diese Liste darf nicht leer sein." -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "Erwarte ein Dictionary mit Elementen, erhielt aber den Typ \"{input_type}\"." -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "Wert muss gültiges JSON sein." -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "Abschicken" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Ungültige Seite \"{page_number}\": {message}." +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "Ungültiger Zeiger" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "Ungültiger pk \"{pk_value}\" - Object existiert nicht." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Falscher Typ. Erwarte pk Wert, erhielt aber {data_type}." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Ungültiger Hyperlink - entspricht keiner URL." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Ungültiger Hyperlink - URL stimmt nicht überein." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Ungültiger Hyperlink - Objekt existiert nicht." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Falscher Typ. Erwarte URL Zeichenkette, erhielt aber {data_type}." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "Objekt mit {slug_name}={value} existiert nicht." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Ungültiger Wert." diff --git a/rest_framework/locale/el/LC_MESSAGES/django.mo b/rest_framework/locale/el/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..fc2120f7f371fbb381747daa830f56dca521fb92 GIT binary patch literal 512 zcmZvY%}xR_5XUuo+M{O=W8wj#!|p<&YY}7Q!;nZIxEimO9hSn!b{Gt+;6&rZLOY#dviTU}b6SY2Bc5m^1$%R9Rk%q|96O=)DX8{s?YOeo2q zHRUo^Fl@huFxGrdOQmPdkW!@$_Oe(+_>{*PhKhkR=!e+U#EmV*7B|G8>iIY5)zI_7 z4_Ga@@_f&;AP#9EiV+96+YRSg?uBOtL(OHDaHBMX555qjp|2zLm9OD`rMB5)BBWXf zUKy$RgDsPB&SS!m_?*GBCYnKMVob_hcez|vOwm$|X;tuUwDE|V?693j^beoA$$xW~ z61VL1y1n-PxBQCdG?g?Y?X%aZ^>1ip+%(}r)?qZ^IwR9~3p*I>x#BHj(%6\n" +"Language-Team: Greek (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/el/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: el\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: authentication.py:71 +msgid "Invalid basic header. No credentials provided." +msgstr "" + +#: authentication.py:74 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "" + +#: authentication.py:80 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "" + +#: authentication.py:97 +msgid "Invalid username/password." +msgstr "" + +#: authentication.py:100 authentication.py:195 +msgid "User inactive or deleted." +msgstr "" + +#: authentication.py:173 +msgid "Invalid token header. No credentials provided." +msgstr "" + +#: authentication.py:176 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "" + +#: authentication.py:182 +msgid "" +"Invalid token header. Token string should not contain invalid characters." +msgstr "" + +#: authentication.py:192 +msgid "Invalid token." +msgstr "" + +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "" + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "" + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "" + +#: exceptions.py:49 +msgid "A server error occurred." +msgstr "" + +#: exceptions.py:84 +msgid "Malformed request." +msgstr "" + +#: exceptions.py:89 +msgid "Incorrect authentication credentials." +msgstr "" + +#: exceptions.py:94 +msgid "Authentication credentials were not provided." +msgstr "" + +#: exceptions.py:99 +msgid "You do not have permission to perform this action." +msgstr "" + +#: exceptions.py:104 views.py:81 +msgid "Not found." +msgstr "" + +#: exceptions.py:109 +#, python-brace-format +msgid "Method \"{method}\" not allowed." +msgstr "" + +#: exceptions.py:120 +msgid "Could not satisfy the request Accept header." +msgstr "" + +#: exceptions.py:132 +#, python-brace-format +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "" + +#: exceptions.py:145 +msgid "Request was throttled." +msgstr "" + +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 +#: validators.py:162 +msgid "This field is required." +msgstr "" + +#: fields.py:267 +msgid "This field may not be null." +msgstr "" + +#: fields.py:603 fields.py:634 +#, python-brace-format +msgid "\"{input}\" is not a valid boolean." +msgstr "" + +#: fields.py:669 +msgid "This field may not be blank." +msgstr "" + +#: fields.py:670 fields.py:1664 +#, python-brace-format +msgid "Ensure this field has no more than {max_length} characters." +msgstr "" + +#: fields.py:671 +#, python-brace-format +msgid "Ensure this field has at least {min_length} characters." +msgstr "" + +#: fields.py:708 +msgid "Enter a valid email address." +msgstr "" + +#: fields.py:719 +msgid "This value does not match the required pattern." +msgstr "" + +#: fields.py:730 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "" + +#: fields.py:742 +msgid "Enter a valid URL." +msgstr "" + +#: fields.py:755 +#, python-brace-format +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:791 +msgid "Enter a valid IPv4 or IPv6 address." +msgstr "" + +#: fields.py:816 +msgid "A valid integer is required." +msgstr "" + +#: fields.py:817 fields.py:852 fields.py:885 +#, python-brace-format +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "" + +#: fields.py:818 fields.py:853 fields.py:886 +#, python-brace-format +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "" + +#: fields.py:819 fields.py:854 fields.py:890 +msgid "String value too large." +msgstr "" + +#: fields.py:851 fields.py:884 +msgid "A valid number is required." +msgstr "" + +#: fields.py:887 +#, python-brace-format +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "" + +#: fields.py:888 +#, python-brace-format +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "" + +#: fields.py:889 +#, python-brace-format +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "" + +#: fields.py:1004 +#, python-brace-format +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:1005 +msgid "Expected a datetime but got a date." +msgstr "" + +#: fields.py:1082 +#, python-brace-format +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:1083 +msgid "Expected a date but got a datetime." +msgstr "" + +#: fields.py:1151 +#, python-brace-format +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:1215 +#, python-brace-format +msgid "Duration has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:1240 fields.py:1289 +#, python-brace-format +msgid "\"{input}\" is not a valid choice." +msgstr "" + +#: fields.py:1243 relations.py:71 relations.py:442 +#, python-brace-format +msgid "More than {count} items..." +msgstr "" + +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 +#, python-brace-format +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "" + +#: fields.py:1291 +msgid "This selection may not be empty." +msgstr "" + +#: fields.py:1328 +#, python-brace-format +msgid "\"{input}\" is not a valid path choice." +msgstr "" + +#: fields.py:1347 +msgid "No file was submitted." +msgstr "" + +#: fields.py:1348 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "" + +#: fields.py:1349 +msgid "No filename could be determined." +msgstr "" + +#: fields.py:1350 +msgid "The submitted file is empty." +msgstr "" + +#: fields.py:1351 +#, python-brace-format +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "" + +#: fields.py:1399 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: fields.py:1438 relations.py:439 serializers.py:521 +msgid "This list may not be empty." +msgstr "" + +#: fields.py:1491 +#, python-brace-format +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: fields.py:1538 +msgid "Value must be valid JSON." +msgstr "" + +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 +msgid "Submit" +msgstr "" + +#: pagination.py:189 +msgid "Invalid page." +msgstr "" + +#: pagination.py:407 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:207 +#, python-brace-format +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "" + +#: relations.py:208 +#, python-brace-format +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "" + +#: relations.py:240 +msgid "Invalid hyperlink - No URL match." +msgstr "" + +#: relations.py:241 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "" + +#: relations.py:242 +msgid "Invalid hyperlink - Object does not exist." +msgstr "" + +#: relations.py:243 +#, python-brace-format +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "" + +#: relations.py:402 +#, python-brace-format +msgid "Object with {slug_name}={value} does not exist." +msgstr "" + +#: relations.py:403 +msgid "Invalid value." +msgstr "" + +#: serializers.py:326 +#, python-brace-format +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "" + +#: templates/rest_framework/admin.html:118 +#: templates/rest_framework/base.html:128 +msgid "Filters" +msgstr "" + +#: templates/rest_framework/filters/django_filter.html:2 +#: templates/rest_framework/filters/django_filter_crispyforms.html:4 +msgid "Field filters" +msgstr "" + +#: templates/rest_framework/filters/ordering.html:3 +msgid "Ordering" +msgstr "" + +#: templates/rest_framework/filters/search.html:2 +msgid "Search" +msgstr "" + +#: templates/rest_framework/horizontal/radio.html:2 +#: templates/rest_framework/inline/radio.html:2 +#: templates/rest_framework/vertical/radio.html:2 +msgid "None" +msgstr "" + +#: templates/rest_framework/horizontal/select_multiple.html:2 +#: templates/rest_framework/inline/select_multiple.html:2 +#: templates/rest_framework/vertical/select_multiple.html:2 +msgid "No items to select." +msgstr "" + +#: validators.py:24 +msgid "This field must be unique." +msgstr "" + +#: validators.py:78 +#, python-brace-format +msgid "The fields {field_names} must make a unique set." +msgstr "" + +#: validators.py:226 +#, python-brace-format +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "" + +#: validators.py:241 +#, python-brace-format +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "" + +#: validators.py:254 +#, python-brace-format +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "" + +#: versioning.py:42 +msgid "Invalid version in \"Accept\" header." +msgstr "" + +#: versioning.py:73 versioning.py:115 +msgid "Invalid version in URL path." +msgstr "" + +#: versioning.py:144 +msgid "Invalid version in hostname." +msgstr "" + +#: versioning.py:166 +msgid "Invalid version in query parameter." +msgstr "" + +#: views.py:88 +msgid "Permission denied." +msgstr "" diff --git a/rest_framework/locale/el_GR/LC_MESSAGES/django.mo b/rest_framework/locale/el_GR/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..67022a7f764708dd90e4ebb5696030a81fb70082 GIT binary patch literal 527 zcmZut!A`mCdOpUb<47Bm}<5d)4XbQRLM9A?cVX6s7%a7%VOnUO;=R(-oPo@i$GqKa!K!T6BpkKDueY3?KVV#+CIA2c literal 0 HcmV?d00001 diff --git a/rest_framework/locale/el_GR/LC_MESSAGES/django.po b/rest_framework/locale/el_GR/LC_MESSAGES/django.po new file mode 100644 index 000000000..645a1a8c6 --- /dev/null +++ b/rest_framework/locale/el_GR/LC_MESSAGES/django.po @@ -0,0 +1,457 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# +# Translators: +msgid "" +msgstr "" +"Project-Id-Version: Django REST framework\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" +"Last-Translator: Xavier Ordoquy \n" +"Language-Team: Greek (Greece) (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/el_GR/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: el_GR\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: authentication.py:71 +msgid "Invalid basic header. No credentials provided." +msgstr "" + +#: authentication.py:74 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "" + +#: authentication.py:80 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "" + +#: authentication.py:97 +msgid "Invalid username/password." +msgstr "" + +#: authentication.py:100 authentication.py:195 +msgid "User inactive or deleted." +msgstr "" + +#: authentication.py:173 +msgid "Invalid token header. No credentials provided." +msgstr "" + +#: authentication.py:176 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "" + +#: authentication.py:182 +msgid "" +"Invalid token header. Token string should not contain invalid characters." +msgstr "" + +#: authentication.py:192 +msgid "Invalid token." +msgstr "" + +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "" + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "" + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "" + +#: exceptions.py:49 +msgid "A server error occurred." +msgstr "" + +#: exceptions.py:84 +msgid "Malformed request." +msgstr "" + +#: exceptions.py:89 +msgid "Incorrect authentication credentials." +msgstr "" + +#: exceptions.py:94 +msgid "Authentication credentials were not provided." +msgstr "" + +#: exceptions.py:99 +msgid "You do not have permission to perform this action." +msgstr "" + +#: exceptions.py:104 views.py:81 +msgid "Not found." +msgstr "" + +#: exceptions.py:109 +#, python-brace-format +msgid "Method \"{method}\" not allowed." +msgstr "" + +#: exceptions.py:120 +msgid "Could not satisfy the request Accept header." +msgstr "" + +#: exceptions.py:132 +#, python-brace-format +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "" + +#: exceptions.py:145 +msgid "Request was throttled." +msgstr "" + +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 +#: validators.py:162 +msgid "This field is required." +msgstr "" + +#: fields.py:267 +msgid "This field may not be null." +msgstr "" + +#: fields.py:603 fields.py:634 +#, python-brace-format +msgid "\"{input}\" is not a valid boolean." +msgstr "" + +#: fields.py:669 +msgid "This field may not be blank." +msgstr "" + +#: fields.py:670 fields.py:1664 +#, python-brace-format +msgid "Ensure this field has no more than {max_length} characters." +msgstr "" + +#: fields.py:671 +#, python-brace-format +msgid "Ensure this field has at least {min_length} characters." +msgstr "" + +#: fields.py:708 +msgid "Enter a valid email address." +msgstr "" + +#: fields.py:719 +msgid "This value does not match the required pattern." +msgstr "" + +#: fields.py:730 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "" + +#: fields.py:742 +msgid "Enter a valid URL." +msgstr "" + +#: fields.py:755 +#, python-brace-format +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:791 +msgid "Enter a valid IPv4 or IPv6 address." +msgstr "" + +#: fields.py:816 +msgid "A valid integer is required." +msgstr "" + +#: fields.py:817 fields.py:852 fields.py:885 +#, python-brace-format +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "" + +#: fields.py:818 fields.py:853 fields.py:886 +#, python-brace-format +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "" + +#: fields.py:819 fields.py:854 fields.py:890 +msgid "String value too large." +msgstr "" + +#: fields.py:851 fields.py:884 +msgid "A valid number is required." +msgstr "" + +#: fields.py:887 +#, python-brace-format +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "" + +#: fields.py:888 +#, python-brace-format +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "" + +#: fields.py:889 +#, python-brace-format +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "" + +#: fields.py:1004 +#, python-brace-format +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:1005 +msgid "Expected a datetime but got a date." +msgstr "" + +#: fields.py:1082 +#, python-brace-format +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:1083 +msgid "Expected a date but got a datetime." +msgstr "" + +#: fields.py:1151 +#, python-brace-format +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:1215 +#, python-brace-format +msgid "Duration has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:1240 fields.py:1289 +#, python-brace-format +msgid "\"{input}\" is not a valid choice." +msgstr "" + +#: fields.py:1243 relations.py:71 relations.py:442 +#, python-brace-format +msgid "More than {count} items..." +msgstr "" + +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 +#, python-brace-format +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "" + +#: fields.py:1291 +msgid "This selection may not be empty." +msgstr "" + +#: fields.py:1328 +#, python-brace-format +msgid "\"{input}\" is not a valid path choice." +msgstr "" + +#: fields.py:1347 +msgid "No file was submitted." +msgstr "" + +#: fields.py:1348 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "" + +#: fields.py:1349 +msgid "No filename could be determined." +msgstr "" + +#: fields.py:1350 +msgid "The submitted file is empty." +msgstr "" + +#: fields.py:1351 +#, python-brace-format +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "" + +#: fields.py:1399 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: fields.py:1438 relations.py:439 serializers.py:521 +msgid "This list may not be empty." +msgstr "" + +#: fields.py:1491 +#, python-brace-format +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: fields.py:1538 +msgid "Value must be valid JSON." +msgstr "" + +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 +msgid "Submit" +msgstr "" + +#: pagination.py:189 +msgid "Invalid page." +msgstr "" + +#: pagination.py:407 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:207 +#, python-brace-format +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "" + +#: relations.py:208 +#, python-brace-format +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "" + +#: relations.py:240 +msgid "Invalid hyperlink - No URL match." +msgstr "" + +#: relations.py:241 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "" + +#: relations.py:242 +msgid "Invalid hyperlink - Object does not exist." +msgstr "" + +#: relations.py:243 +#, python-brace-format +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "" + +#: relations.py:402 +#, python-brace-format +msgid "Object with {slug_name}={value} does not exist." +msgstr "" + +#: relations.py:403 +msgid "Invalid value." +msgstr "" + +#: serializers.py:326 +#, python-brace-format +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "" + +#: templates/rest_framework/admin.html:118 +#: templates/rest_framework/base.html:128 +msgid "Filters" +msgstr "" + +#: templates/rest_framework/filters/django_filter.html:2 +#: templates/rest_framework/filters/django_filter_crispyforms.html:4 +msgid "Field filters" +msgstr "" + +#: templates/rest_framework/filters/ordering.html:3 +msgid "Ordering" +msgstr "" + +#: templates/rest_framework/filters/search.html:2 +msgid "Search" +msgstr "" + +#: templates/rest_framework/horizontal/radio.html:2 +#: templates/rest_framework/inline/radio.html:2 +#: templates/rest_framework/vertical/radio.html:2 +msgid "None" +msgstr "" + +#: templates/rest_framework/horizontal/select_multiple.html:2 +#: templates/rest_framework/inline/select_multiple.html:2 +#: templates/rest_framework/vertical/select_multiple.html:2 +msgid "No items to select." +msgstr "" + +#: validators.py:24 +msgid "This field must be unique." +msgstr "" + +#: validators.py:78 +#, python-brace-format +msgid "The fields {field_names} must make a unique set." +msgstr "" + +#: validators.py:226 +#, python-brace-format +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "" + +#: validators.py:241 +#, python-brace-format +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "" + +#: validators.py:254 +#, python-brace-format +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "" + +#: versioning.py:42 +msgid "Invalid version in \"Accept\" header." +msgstr "" + +#: versioning.py:73 versioning.py:115 +msgid "Invalid version in URL path." +msgstr "" + +#: versioning.py:144 +msgid "Invalid version in hostname." +msgstr "" + +#: versioning.py:166 +msgid "Invalid version in query parameter." +msgstr "" + +#: views.py:88 +msgid "Permission denied." +msgstr "" diff --git a/rest_framework/locale/en/LC_MESSAGES/django.mo b/rest_framework/locale/en/LC_MESSAGES/django.mo index 6e464e9fd9a61820309b12be2e61b93eb2c72253..06dc754a8d9fca02a5d8c0e2c534ad0ba349e006 100644 GIT binary patch delta 2413 zcmcK4|4-Fb9LMqZ!?g0}4b7#ji;qw=D}OXiMkXZOcX2 z8m^nVxizMhGk&VkFD|6b>9Q}Y<=V0@%KW8OKe)DR)JCi4>+|`nFMmL1_x*XCbMC$8 z^Ev0d&u6Oh)vnm}ti)qR*+<+-)CA4Ou``(q<*8J&B77PhjAIGDhs*IB%){SN|IfP3 ztN>Tz?bv~p*n<^#7*p_F++Y^7b5vUCSn!STuQu98uo?WUdIM3 zPxHQyVkhm_u^4~CEX+vv9!+dtU&LCxfPCyXE_%}B3~$C6s2kN_Jr3h~ zJc|+h0asx~CO5;~sNe0!yYUUwN`8vvcoWmPzvVABtHM&$06Xvj9K&_^1!};|JIt2i za#VXQY9{TdrGL_Q-0wewLHa*Nt-xofExUmlN1d@!ET9s?2s*e6v+)on;smk__8P9n zNo2e2bKk4T$8K=ZIJZ#gvxq@*F$>i$$0`h?wzxl={nwLE`9J&+)t*Bo(E`?C-V$$b zH>28Ps2R+ne)lyx_$%rGLpg48St};d9!EXE1m1(kF^m^-SpS_=ZqcEccJcW6*zG%r zn(3I|K8p9!p2Eukvumh5J;M=j@G|Oj{D@lWOa`NBt56ebLp|^SYDJ%qQK_WzGFIR@ zRMLHq91pvRIvruAD-WQS?gMPb6h`a8cGT&Z#76uIbvm-SYXkP8zCVdN9am7f6DwTi z?Og=vvmVqJqrN9l1HO;7IFC9Vl}t|$+K8HQ1a+gwu^!(#_7G|!&!HxE%6Hc9 z{}zMX-~Obc>OI^lw7#?P0$?gPOoENE%oM z`{Q6a>H#}&10KRy5|vphdZLS{2Y>GE)wUb9yz^RPX* z*{^E_wah9Gp{3tNBoHdvnr)gtC&Vp5uG;4&Vu#yfwitP-*`vfhLPt)eh~PE;pOQ#x zFOf#*j4OFJ5e-BiQA2DcRM;VVh=|>%7DY!%d;RaSo`y2E*zbGG3=4-O^x!Re-=v_MMV=;*V^Zw^vN8>-QJRd(>Qk%MM xG&(ZWHyDk7SJD;F32jfWtE{T7tO`5f`iAQI_`y)YKRs*cnLZUNNcbP0;V+hT|7rjL delta 2186 zcmb`|OKenC9LMp0$36xMLR)Bs0lY0z6gq8Z%Gf$s+bPHtTc~Zd@)94h2x zkN=q-SbwzBKVO>vrcpK%3y42MWt>jhVlQey+feapA8Y2q8DZS5SMuno+QtwjDK~Auj5F%Xtb{(!PKRT(B@m?jBTfKZ=^*cGP?P*Qhj6 zIfKgX5Ytw6w_z>5iX^g~K;3s5mBrUkD_4DMklpR5{y|g@97KKp3siDn#b#XN1b&R2A@(D6{yXd@*>W}a?&AbJ9xAh=rMN^YwPlDhzv zL$fig^WREE4_=FUz!p??40#UaT$bMF{#1O!E!qMtbF1sygehV(kx#ISxz*C?s3uks+J^gy8;g>T ze=W9}&^D+jf3!9CYX3?6Tv7H;mGD%>$?^WDY@nv?(Q(z;S6N4BPgQhUlqWhZIsqz0 z0d_Y&MD!9}#B?pIN_T+Wf&7@+O@xvtN$B|SOPotCumYlwXd#pjDjuQatt4WZf$(Ie zdr^62*R)jj*LlwrM!c$_U5`E4|H!k?4?N!g+;COW8ye^z9HeVFyU9D8m;JJ&I4?U{ zdL)#YSaK?}zq&s2dikN5iD)byt#9;VP02(\n" "Language-Team: English (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/en/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: en\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "Invalid basic header. No credentials provided." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "Invalid basic header. Credentials string should not contain spaces." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "Invalid basic header. Credentials not correctly base64 encoded." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Invalid username/password." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "User inactive or deleted." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "Invalid token header. No credentials provided." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "Invalid token header. Token string should not contain spaces." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "Invalid token header. Token string should not contain invalid characters." -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Invalid token." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "Auth Token" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "Key" + +#: authtoken/models.py:23 +msgid "User" +msgstr "User" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "Created" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "Token" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "Tokens" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "Username" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "Password" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "User account is disabled." @@ -108,7 +140,7 @@ msgstr "Unsupported media type \"{media_type}\" in request." msgid "Request was throttled." msgstr "Request was throttled." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "This field is required." @@ -126,7 +158,7 @@ msgstr "\"{input}\" is not a valid boolean." msgid "This field may not be blank." msgstr "This field may not be blank." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Ensure this field has no more than {max_length} characters." @@ -212,137 +244,136 @@ msgstr "Datetime has wrong format. Use one of these formats instead: {format}." msgid "Expected a datetime but got a date." msgstr "Expected a datetime but got a date." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "Date has wrong format. Use one of these formats instead: {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "Expected a date but got a datetime." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "Time has wrong format. Use one of these formats instead: {format}." -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "Duration has wrong format. Use one of these formats instead: {format}." -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\" is not a valid choice." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "More than {count} items..." -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "Expected a list of items but got type \"{input_type}\"." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "This selection may not be empty." -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "\"{input}\" is not a valid path choice." -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "No file was submitted." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "The submitted data was not a file. Check the encoding type on the form." -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "No filename could be determined." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "The submitted file is empty." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "Ensure this filename has at most {max_length} characters (it has {length})." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Upload a valid image. The file you uploaded was either not an image or a corrupted image." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "This list may not be empty." -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "Expected a dictionary of items but got type \"{input_type}\"." -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "Value must be valid JSON." -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "Submit" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." +msgstr "Invalid page." #: pagination.py:407 msgid "Invalid cursor" msgstr "Invalid cursor" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "Invalid pk \"{pk_value}\" - object does not exist." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Incorrect type. Expected pk value, received {data_type}." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Invalid hyperlink - No URL match." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Invalid hyperlink - Incorrect URL match." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Invalid hyperlink - Object does not exist." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Incorrect type. Expected URL string, received {data_type}." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "Object with {slug_name}={value} does not exist." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Invalid value." diff --git a/rest_framework/locale/en_AU/LC_MESSAGES/django.mo b/rest_framework/locale/en_AU/LC_MESSAGES/django.mo index ffaa53bd68f864439f560a728412f0a50afe9bdb..288595123478d0817f3b1c44d2884635461fd3ca 100644 GIT binary patch delta 41 ncmbQjGKFQr1YR>;17lqSLj^+%D`Sg^bEOdi=0JgsNBtQA+)@gC delta 41 pcmbQjGKFQr1YT2JLnB=Sa|J^SD^uf%bEOdi=2oVr8;|-k0s!2|3WERu diff --git a/rest_framework/locale/en_AU/LC_MESSAGES/django.po b/rest_framework/locale/en_AU/LC_MESSAGES/django.po index 696396b2b..9ee758d64 100644 --- a/rest_framework/locale/en_AU/LC_MESSAGES/django.po +++ b/rest_framework/locale/en_AU/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: English (Australia) (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/en_AU/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: en_AU\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -212,137 +244,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" diff --git a/rest_framework/locale/en_CA/LC_MESSAGES/django.mo b/rest_framework/locale/en_CA/LC_MESSAGES/django.mo index bdf76870d71a111c67706885ed6ced967314d70e..aee511aa1352bcb292c4d565d7c35a2259d0e06f 100644 GIT binary patch delta 41 ncmbQpGLdD%1YR>;17lqSLj^+%D`Sg^bEOdi=0JgsM|~Ip+gu8C delta 41 pcmbQpGLdD%1YT2JLnB=Sa|J^SD^uf%bEOdi=2oVr8;|-h0s!063VHwl diff --git a/rest_framework/locale/en_CA/LC_MESSAGES/django.po b/rest_framework/locale/en_CA/LC_MESSAGES/django.po index f23a59fb6..2bb5b573b 100644 --- a/rest_framework/locale/en_CA/LC_MESSAGES/django.po +++ b/rest_framework/locale/en_CA/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: English (Canada) (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/en_CA/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: en_CA\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -212,137 +244,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" diff --git a/rest_framework/locale/en_US/LC_MESSAGES/django.mo b/rest_framework/locale/en_US/LC_MESSAGES/django.mo index c99892e4bc9bddbab7066c13c43ad5dbcc12944a..1f28ebba03d951523b3e9fa42d14ec34fc1aa2c0 100644 GIT binary patch delta 23 ecmeyx^owai7q6MFfw8WEp@N}>m9fRd>5lhExq_jEm8tQ>>5l\n" "Language-Team: LANGUAGE \n" @@ -17,43 +17,75 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -211,136 +243,135 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" diff --git a/rest_framework/locale/es/LC_MESSAGES/django.mo b/rest_framework/locale/es/LC_MESSAGES/django.mo index 287a794806c6cd9280e7195b9cca8e2ea602bb37..fa87eb02f6d6bb983902834ff2fb54091386e228 100644 GIT binary patch delta 2444 zcmY+_TWnNC9LMq5-gawSY8P54RoGszPz!YJg;J!nU0T3J5NIp+x@|YH*lx93ss!S` zCGN`0V}ed z&!f1S_86ApALz#^InDr!QT>g!31hmc=!Sc-7SAJp=5H<!W505{`Q+>L2?5Xpi$gbVRw zB;DqF*PoF;Gs#8wxr5q1S=?kg`cdr)tim8F#obfMzeax2{o*B5`vz(g-N!ndG0n;C zT2y-!HG^}g@BM-Syo(xOLB74YOdF=t-j5ob&6I5!bT){^4 zFfA=*6L#PR+>9r16Q(iUX58+27}wFhiOaEuz&*%*GGoZI%@?QvjicU( z_plCw`|^ z<+_T{;Pv1Nf&*fjh-U~Dts?YxzDXLVY_IxwEI(x_9lnMiT_w1OR)KPSsBsa*9^Xe2h;J8RM9)5(5^ZE{nk?tyX}>j>(;eTC>~>F`DR)-y%&9f_Q4mT zG1F{q&ndLd`kK9=p=e|{+ME2xH;|EhJ@=R=H5}cQoR^pF@kZkDmj{P>lhytcDcR2N z>}%W_pLW)=@@st^@#s)2G7z;cNQD z2=qn+k)6ZQ*l=G@q^IvfGG^VLu{)dI9!6t#B)bZ~^=9_PMlKxb@9T{u!^QrT^iY3f zB%0h>+TzIy4aSCtBJt>jQ<3CYSz7A<$9K5=nztjqGcwdSXpPKWZpBOTtTS`l{{B`}0>j8Sv zR^8R~MWj8MZE4$4R;^yu3ma6cwc4oFqBd*xxadu-*7tYrTgSR~{^xVu*E#3Duj_yP zuQR^m@$J#Cvy+}N+HT@nVm84nic|A=qg_lfE5r}b!OK{R8L4L1VKrWZ+fcv18w+q0 zgZLtr<0rTQ|HX|MNsAk`F*+>_e2nWcZ@$@1?7}s84%gx(Y{lYqvoH>!K7S3{a0ZL9 zGQ%tzJ5Uq43pM_b?`hQiCb3RSO<7>Z%NlrNan_1jVF&6#_hUVt$94EKHezt0Sp^@+xX& zQ@%g?tgbM1Dyj5XfIztT~DKCp0&jL zVmUfoZ$eG1A5}~za5a8~AxzCN+l66VickBV^}UGIjL+a?z$_=1{FgBBEz`RmlPTp2 z45EWQs687&)x;~f9H+1rf5Mxwkf+^-cVIV8;4aK!6%jn(`#Nsp`d4hiNR&M2I6R8I z_#xhemE=hd;7&Y)nwTx~GE#{*a@mT?$RnuFFQ8KYJt{-x6awX8L#X0@5S6hpR53@- z)7eUA7CW$s-Vt?|HL3JFZ3q11$9bBQ7fKAt$10H zcfWne@1i#B4?KsO*;!P|r+pJCc|E8cxrgmR2ct+*>?CSy-bEH|-=XdkU|+d~1yL(+ zM^2=5p(eZ^Gj#r+q(h3 zZ$+(MZM#SOZxuFoY<7$PN&1{`B-3vp*85}K7$J5ONrbjamET4v z8;yjvtDTtJl)*HjiBS60sP*`kG;9C0S88PKYD>M^^E%q!E3J#p2BMWH_XoEiM>^hA zWZK4hLPt_9#Up;CJL&7B>D0|vTGez!)Kr;Eh(tnrt9{>0s6bWiYV!#FC#)m%C!^*N zxkMomatBjq-0rl_*js4>$puc)(R+IL_uYQ*zQKKc_l^{Wouh+&!@~@X#7Y;v9Eg3C zl^%%AWS>rOpUrv4?aZxmpU8b_L0vFZ9jsd8gzCd}HEtxYz#pl1LUrM~+E{;HYl1tq z^Z|EaL3`{>{-KnT|2@a)KX~}PQv>~X_Rf7`aZz?cY;8#-5F0B^OmvU0{M, 2015 # José Padilla , 2015 # Miguel González , 2015 -# Miguel González , 2015 +# Miguel González , 2015-2016 # Sergio Infante , 2015 msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-08 15:54+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 18:16+0000\n" "Last-Translator: Miguel González \n" "Language-Team: Spanish (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/es/)\n" "MIME-Version: 1.0\n" @@ -22,43 +22,75 @@ msgstr "" "Language: es\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "Cabecera básica inválida. Las credenciales no fueron suministradas." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "Cabecera básica inválida. La cadena con las credenciales no debe contener espacios." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "Cabecera básica inválida. Las credenciales incorrectamente codificadas en base64." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Nombre de usuario/contraseña inválidos." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "Usuario inactivo o borrado." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "Cabecera token inválida. Las credenciales no fueron suministradas." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "Cabecera token inválida. La cadena token no debe contener espacios." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "Cabecera token inválida. La cadena token no debe contener caracteres inválidos." -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Token inválido." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "Token de autenticación" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "Clave" + +#: authtoken/models.py:23 +msgid "User" +msgstr "Usuario" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "Fecha de creación" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "Token" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "Tokens" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "Nombre de usuario" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "Contraseña" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "Cuenta de usuario está deshabilitada." @@ -113,7 +145,7 @@ msgstr "Tipo de medio \"{media_type}\" incompatible en la solicitud." msgid "Request was throttled." msgstr "Solicitud fue regulada (throttled)." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "Este campo es requerido." @@ -131,7 +163,7 @@ msgstr "\"{input}\" no es un booleano válido." msgid "This field may not be blank." msgstr "Este campo no puede estar en blanco." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Asegúrese de que este campo no tenga más de {max_length} caracteres." @@ -217,137 +249,136 @@ msgstr "Fecha/hora con formato erróneo. Use uno de los siguientes formatos en s msgid "Expected a datetime but got a date." msgstr "Se esperaba un fecha/hora en vez de una fecha." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "Fecha con formato erróneo. Use uno de los siguientes formatos en su lugar: {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "Se esperaba una fecha en vez de una fecha/hora." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "Hora con formato erróneo. Use uno de los siguientes formatos en su lugar: {format}." -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "Duración con formato erróneo. Use uno de los siguientes formatos en su lugar: {format}." -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\" no es una elección válida." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "Más de {count} elementos..." -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "Se esperaba una lista de elementos en vez del tipo \"{input_type}\"." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "Esta selección no puede estar vacía." -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "\"{input}\" no es una elección de ruta válida." -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "No se envió ningún archivo." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "La información enviada no era un archivo. Compruebe el tipo de codificación del formulario." -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "No se pudo determinar un nombre de archivo." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "El archivo enviado está vació." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "Asegúrese de que el nombre de archivo no tenga más de {max_length} caracteres (tiene {length})." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Adjunte una imagen válida. El archivo adjunto o bien no es una imagen o bien está dañado." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "Esta lista no puede estar vacía." -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "Se esperaba un diccionario de elementos en vez del tipo \"{input_type}\"." -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "El valor debe ser JSON válido." -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "Enviar" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Página \"{page_number}\" inválida: {message}." +msgid "Invalid page." +msgstr "Página inválida." #: pagination.py:407 msgid "Invalid cursor" msgstr "Cursor inválido" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "Clave primaria \"{pk_value}\" inválida - objeto no existe." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Tipo incorrecto. Se esperaba valor de clave primaria y se recibió {data_type}." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Hiperenlace inválido - No hay URL coincidentes." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Hiperenlace inválido - Coincidencia incorrecta de la URL." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Hiperenlace inválido - Objeto no existe." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Tipo incorrecto. Se esperaba una URL y se recibió {data_type}." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "Objeto con {slug_name}={value} no existe." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Valor inválido." diff --git a/rest_framework/locale/et/LC_MESSAGES/django.mo b/rest_framework/locale/et/LC_MESSAGES/django.mo index d0d064b1814c573673e8f9b7145b8226e7040ec9..ee9c40c2b63ff58a7f819ddf82980a809985b465 100644 GIT binary patch delta 1558 zcmYM!OGs2v9LMp$<~U`IX-;aXHJW8*HPm;LMI+D%uu@ikbfD7;fW@8lfyH}WlU$7KiZnNd+#}?d!Yw#AX#R+UO z3tQE6vu)fMz&d=1^_V!rY&|w$5YM2VKgSLD2j^gWN<4s_xQO{N$7`r{#;^we;$p0x z8DDQZ26(>>Gic<&Yg~n?v&_n{3sZ3j19%>n;tNzprmz^j90Nx>+vnkZh6XwPj@}cHIT!y{afG1Hq8$+c$h3zZw zHq`tC>PVlWjyRFD?7|jYk2kZ(|0V|CxzU3mN?KJt%tdE>AGN?+M=#r|W!{5Y@Ej_z zH>hI!feNgcbg9}8p#JV5R^TL(Ov@+TA*>2BSi;~Cmf}TRj?Zuf{zUDlgfundKFq+| zSb(F>`~#|}CNU4aTx1m&)lLVJR6B|b@hp~O_#Oj-vT^6ZZ{$O;Y__H9E<}=Jn^6nw zM+G#9O*n$e*e~Q1ES1$+!U~aOSPORJ4(z}Y>_V3>w$ZTlFi-|gV*}nr71dW%QDt!s zTW}p}!Kuc@nNwR$*P3N?KV3~h>N9rJt7Fx}KuuX_jE&;et=&#@ z=A1&T@KkI8x&l@(N?{v46xrzVM_OHJTXnk1Pa$1-(P0u{to~i7rh`*ceiP|@4zXN1 z|8irWKn?fQblA0#&#t0$B}0i(U2)2`Dw3C+mDkKjO({{Ur~Bxw^!8|HvfCXU@{GGY jwWSsQ(uzu7WgzGeL=I-;{J&GjooFQE#`Ngk+=AqPf7g>* delta 1702 zcmaLXe`w5c9LMpu+3jYWU)#)&ZNB^A=NxyNv$<`|cH1ya6t?nX*5%hW%WX@Ij`&Od zz*3U5WQC$M@*{sJ{vf|13Q7J@NK#5d#wyGnW;PQy;&42SCD@Bo@dYX)2_CcQn2(yTM<4D)ef}(# z<8#zHV|W^mlThOdp7Qv9tEZzAMB*OO3yjaBPWTv=^0znz2ajMeEInAY-FQYd00G0aRsPC&8N&fYQW;&YaM%C~b&cREllz)tm^F|Gvpb=*=--)Vi9~R(y z^kW*kZonEGhrMyHVUG$iLH_ILykue-PUcFl!A|614|!6p-=P*5 z!Y<@eoP&o^3*JF(G?@bH(oI2atQmFdPoduT0ev`)Q<79`jM6EivkfQX8T8^KoPob^ zCQhe5bFl$+vL0N3_b?SbS!TJIgBpiWcW4druw8MFqwdgEB-s{yLuVqLuc(@)a|||V z)u{Om$PdHzplq3^cgGiF>9InROxLm*gOvh{m z6RoHd-$Z5N6VAgl@~_La5_P$vxDqd-7IblJooFJKVJ!~AQbHL}_JV|(N->|%L1z=O zt!S6JOl~4TsA&VrLIt6wn(MTKHK0+IST+`-%AyTuGpg+(qP(ZvRTR~B7ZW;$nkrsG z^|0X}1_J$! ZslCbhE#b}KZMSZ;Z2I4qe4gM<`U73xwo?EA diff --git a/rest_framework/locale/et/LC_MESSAGES/django.po b/rest_framework/locale/et/LC_MESSAGES/django.po index da2db4967..5ccd9226c 100644 --- a/rest_framework/locale/et/LC_MESSAGES/django.po +++ b/rest_framework/locale/et/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Estonian (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/et/)\n" "MIME-Version: 1.0\n" @@ -18,43 +18,75 @@ msgstr "" "Language: et\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "Sobimatu lihtpäis. Kasutajatunnus on esitamata." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "Sobimatu lihtpäis. Kasutajatunnus ei tohi sisaldada tühikuid." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "Sobimatu lihtpäis. Kasutajatunnus pole korrektselt base64-kodeeritud." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Sobimatu kasutajatunnus/salasõna." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "Kasutaja on inaktiivne või kustutatud." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "Sobimatu lubakaardi päis. Kasutajatunnus on esitamata." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "Sobimatu lubakaardi päis. Loa sõne ei tohi sisaldada tühikuid." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Sobimatu lubakaart." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "Kasutajakonto on suletud." @@ -109,7 +141,7 @@ msgstr "Meedia tüüpi {media_type} päringus ei toetata." msgid "Request was throttled." msgstr "Liiga palju päringuid." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "Väli on kohustuslik." @@ -127,7 +159,7 @@ msgstr "\"{input}\" pole kehtiv kahendarv." msgid "This field may not be blank." msgstr "See väli ei tohi olla tühi." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Veendu, et see väli poleks pikem kui {max_length} tähemärki." @@ -213,137 +245,136 @@ msgstr "Valesti formaaditud kuupäev-kellaaeg. Kasutage mõnda neist: {format}." msgid "Expected a datetime but got a date." msgstr "Ootasin kuupäev-kellaaeg andmetüüpi, kuid sain hoopis kuupäeva." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "Valesti formaaditud kuupäev. Kasutage mõnda neist: {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "Ootasin kuupäeva andmetüüpi, kuid sain hoopis kuupäev-kellaaja." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "Valesti formaaditud kellaaeg. Kasutage mõnda neist: {format}." -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\" on sobimatu valik." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "Ootasin kirjete järjendit, kuid sain \"{input_type}\" - tüübi." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "Ühtegi faili ei esitatud." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "Esitatud andmetes ei olnud faili. Kontrollige vormi kodeeringut." -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "Ei suutnud tuvastada failinime." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "Esitatud fail oli tühi." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "Veenduge, et failinimi oleks maksimaalselt {max_length} tähemärki pikk (praegu on {length})." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Laadige üles kehtiv pildifail. Üles laetud fail ei olnud pilt või oli see katki." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "Ootasin kirjete sõnastikku, kuid sain \"{input_type}\"." -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Sobimatu lehekülg \"{page_number}\": {message}." +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "Sobimatu kursor." -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "Sobimatu primaarvõti \"{pk_value}\" - objekti pole olemas." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Sobimatu andmetüüp. Ootasin primaarvõtit, sain {data_type}." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Sobimatu hüperlink - ei leidnud URLi vastet." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Sobimatu hüperlink - vale URLi vaste." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Sobimatu hüperlink - objekti ei eksisteeri." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Sobimatu andmetüüp. Ootasin URLi sõne, sain {data_type}." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "Objekti {slug_name}={value} ei eksisteeri." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Sobimatu väärtus." diff --git a/rest_framework/locale/fa/LC_MESSAGES/django.mo b/rest_framework/locale/fa/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..4b0b24bc2f1de50cd2d90d779dc1bba70ac760f6 GIT binary patch literal 507 zcmZut%TB{E5Cp*~N6s94-~fVc94bL@DMIuiRibKC0^+(PPU@1_!Eu`QL--(mgm1xV zQG4l1Bd_h5-JShCIsQJdacFUBab|I3ab?kjZ}DS4@9f!dwh?GG=aGR?3~yYQLP$+~4rl?5xvQfD4+9hai+ihKfxBuPE{>!@w zx);uPG#>2s<+Z5OQ_^s02VWI-1BNe+TVyC?heR{fB{$vIa0vd6*UXY|4FegeSjdzS j|3jFq4Mq}lf~?Y*!BFYEWJDJ0YuWWWWRrGj$2s@|e\n" +"Language-Team: Persian (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/fa/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fa\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: authentication.py:71 +msgid "Invalid basic header. No credentials provided." +msgstr "" + +#: authentication.py:74 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "" + +#: authentication.py:80 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "" + +#: authentication.py:97 +msgid "Invalid username/password." +msgstr "" + +#: authentication.py:100 authentication.py:195 +msgid "User inactive or deleted." +msgstr "" + +#: authentication.py:173 +msgid "Invalid token header. No credentials provided." +msgstr "" + +#: authentication.py:176 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "" + +#: authentication.py:182 +msgid "" +"Invalid token header. Token string should not contain invalid characters." +msgstr "" + +#: authentication.py:192 +msgid "Invalid token." +msgstr "" + +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "" + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "" + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "" + +#: exceptions.py:49 +msgid "A server error occurred." +msgstr "" + +#: exceptions.py:84 +msgid "Malformed request." +msgstr "" + +#: exceptions.py:89 +msgid "Incorrect authentication credentials." +msgstr "" + +#: exceptions.py:94 +msgid "Authentication credentials were not provided." +msgstr "" + +#: exceptions.py:99 +msgid "You do not have permission to perform this action." +msgstr "" + +#: exceptions.py:104 views.py:81 +msgid "Not found." +msgstr "" + +#: exceptions.py:109 +#, python-brace-format +msgid "Method \"{method}\" not allowed." +msgstr "" + +#: exceptions.py:120 +msgid "Could not satisfy the request Accept header." +msgstr "" + +#: exceptions.py:132 +#, python-brace-format +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "" + +#: exceptions.py:145 +msgid "Request was throttled." +msgstr "" + +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 +#: validators.py:162 +msgid "This field is required." +msgstr "" + +#: fields.py:267 +msgid "This field may not be null." +msgstr "" + +#: fields.py:603 fields.py:634 +#, python-brace-format +msgid "\"{input}\" is not a valid boolean." +msgstr "" + +#: fields.py:669 +msgid "This field may not be blank." +msgstr "" + +#: fields.py:670 fields.py:1664 +#, python-brace-format +msgid "Ensure this field has no more than {max_length} characters." +msgstr "" + +#: fields.py:671 +#, python-brace-format +msgid "Ensure this field has at least {min_length} characters." +msgstr "" + +#: fields.py:708 +msgid "Enter a valid email address." +msgstr "" + +#: fields.py:719 +msgid "This value does not match the required pattern." +msgstr "" + +#: fields.py:730 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "" + +#: fields.py:742 +msgid "Enter a valid URL." +msgstr "" + +#: fields.py:755 +#, python-brace-format +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:791 +msgid "Enter a valid IPv4 or IPv6 address." +msgstr "" + +#: fields.py:816 +msgid "A valid integer is required." +msgstr "" + +#: fields.py:817 fields.py:852 fields.py:885 +#, python-brace-format +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "" + +#: fields.py:818 fields.py:853 fields.py:886 +#, python-brace-format +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "" + +#: fields.py:819 fields.py:854 fields.py:890 +msgid "String value too large." +msgstr "" + +#: fields.py:851 fields.py:884 +msgid "A valid number is required." +msgstr "" + +#: fields.py:887 +#, python-brace-format +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "" + +#: fields.py:888 +#, python-brace-format +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "" + +#: fields.py:889 +#, python-brace-format +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "" + +#: fields.py:1004 +#, python-brace-format +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:1005 +msgid "Expected a datetime but got a date." +msgstr "" + +#: fields.py:1082 +#, python-brace-format +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:1083 +msgid "Expected a date but got a datetime." +msgstr "" + +#: fields.py:1151 +#, python-brace-format +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:1215 +#, python-brace-format +msgid "Duration has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:1240 fields.py:1289 +#, python-brace-format +msgid "\"{input}\" is not a valid choice." +msgstr "" + +#: fields.py:1243 relations.py:71 relations.py:442 +#, python-brace-format +msgid "More than {count} items..." +msgstr "" + +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 +#, python-brace-format +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "" + +#: fields.py:1291 +msgid "This selection may not be empty." +msgstr "" + +#: fields.py:1328 +#, python-brace-format +msgid "\"{input}\" is not a valid path choice." +msgstr "" + +#: fields.py:1347 +msgid "No file was submitted." +msgstr "" + +#: fields.py:1348 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "" + +#: fields.py:1349 +msgid "No filename could be determined." +msgstr "" + +#: fields.py:1350 +msgid "The submitted file is empty." +msgstr "" + +#: fields.py:1351 +#, python-brace-format +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "" + +#: fields.py:1399 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: fields.py:1438 relations.py:439 serializers.py:521 +msgid "This list may not be empty." +msgstr "" + +#: fields.py:1491 +#, python-brace-format +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: fields.py:1538 +msgid "Value must be valid JSON." +msgstr "" + +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 +msgid "Submit" +msgstr "" + +#: pagination.py:189 +msgid "Invalid page." +msgstr "" + +#: pagination.py:407 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:207 +#, python-brace-format +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "" + +#: relations.py:208 +#, python-brace-format +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "" + +#: relations.py:240 +msgid "Invalid hyperlink - No URL match." +msgstr "" + +#: relations.py:241 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "" + +#: relations.py:242 +msgid "Invalid hyperlink - Object does not exist." +msgstr "" + +#: relations.py:243 +#, python-brace-format +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "" + +#: relations.py:402 +#, python-brace-format +msgid "Object with {slug_name}={value} does not exist." +msgstr "" + +#: relations.py:403 +msgid "Invalid value." +msgstr "" + +#: serializers.py:326 +#, python-brace-format +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "" + +#: templates/rest_framework/admin.html:118 +#: templates/rest_framework/base.html:128 +msgid "Filters" +msgstr "" + +#: templates/rest_framework/filters/django_filter.html:2 +#: templates/rest_framework/filters/django_filter_crispyforms.html:4 +msgid "Field filters" +msgstr "" + +#: templates/rest_framework/filters/ordering.html:3 +msgid "Ordering" +msgstr "" + +#: templates/rest_framework/filters/search.html:2 +msgid "Search" +msgstr "" + +#: templates/rest_framework/horizontal/radio.html:2 +#: templates/rest_framework/inline/radio.html:2 +#: templates/rest_framework/vertical/radio.html:2 +msgid "None" +msgstr "" + +#: templates/rest_framework/horizontal/select_multiple.html:2 +#: templates/rest_framework/inline/select_multiple.html:2 +#: templates/rest_framework/vertical/select_multiple.html:2 +msgid "No items to select." +msgstr "" + +#: validators.py:24 +msgid "This field must be unique." +msgstr "" + +#: validators.py:78 +#, python-brace-format +msgid "The fields {field_names} must make a unique set." +msgstr "" + +#: validators.py:226 +#, python-brace-format +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "" + +#: validators.py:241 +#, python-brace-format +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "" + +#: validators.py:254 +#, python-brace-format +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "" + +#: versioning.py:42 +msgid "Invalid version in \"Accept\" header." +msgstr "" + +#: versioning.py:73 versioning.py:115 +msgid "Invalid version in URL path." +msgstr "" + +#: versioning.py:144 +msgid "Invalid version in hostname." +msgstr "" + +#: versioning.py:166 +msgid "Invalid version in query parameter." +msgstr "" + +#: views.py:88 +msgid "Permission denied." +msgstr "" diff --git a/rest_framework/locale/fa_IR/LC_MESSAGES/django.mo b/rest_framework/locale/fa_IR/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..05d849e0849e8f2a4697719b5a11a60ab4f1ffb9 GIT binary patch literal 520 zcmZutO-}+b5XIdA(23EHQr2R%Tiq0wOhV^h=0qU;BRpj z3|yGxr8DiDd42PHdh%VOI3k=8&I!kaOF|JL;fEe?=`1+A7+5ovBZu7>U!~2J(HvS+ zp%Vkc_HzVdD`s+O?58)BnXwLgd7@%`oF_R(hJ!cgN7%GdIBLa~aFW9n3$BsXkOdHg zycS-tfH5NCP%cyWbNFELTONA8*W}m87~ei^1W0 zKmAwn$Kt&^^ebMk+iUMP7gU+oG}=i`7S\n" +"Language-Team: Persian (Iran) (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/fa_IR/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fa_IR\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: authentication.py:71 +msgid "Invalid basic header. No credentials provided." +msgstr "" + +#: authentication.py:74 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "" + +#: authentication.py:80 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "" + +#: authentication.py:97 +msgid "Invalid username/password." +msgstr "" + +#: authentication.py:100 authentication.py:195 +msgid "User inactive or deleted." +msgstr "" + +#: authentication.py:173 +msgid "Invalid token header. No credentials provided." +msgstr "" + +#: authentication.py:176 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "" + +#: authentication.py:182 +msgid "" +"Invalid token header. Token string should not contain invalid characters." +msgstr "" + +#: authentication.py:192 +msgid "Invalid token." +msgstr "" + +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + +#: authtoken/serializers.py:20 +msgid "User account is disabled." +msgstr "" + +#: authtoken/serializers.py:23 +msgid "Unable to log in with provided credentials." +msgstr "" + +#: authtoken/serializers.py:26 +msgid "Must include \"username\" and \"password\"." +msgstr "" + +#: exceptions.py:49 +msgid "A server error occurred." +msgstr "" + +#: exceptions.py:84 +msgid "Malformed request." +msgstr "" + +#: exceptions.py:89 +msgid "Incorrect authentication credentials." +msgstr "" + +#: exceptions.py:94 +msgid "Authentication credentials were not provided." +msgstr "" + +#: exceptions.py:99 +msgid "You do not have permission to perform this action." +msgstr "" + +#: exceptions.py:104 views.py:81 +msgid "Not found." +msgstr "" + +#: exceptions.py:109 +#, python-brace-format +msgid "Method \"{method}\" not allowed." +msgstr "" + +#: exceptions.py:120 +msgid "Could not satisfy the request Accept header." +msgstr "" + +#: exceptions.py:132 +#, python-brace-format +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "" + +#: exceptions.py:145 +msgid "Request was throttled." +msgstr "" + +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 +#: validators.py:162 +msgid "This field is required." +msgstr "" + +#: fields.py:267 +msgid "This field may not be null." +msgstr "" + +#: fields.py:603 fields.py:634 +#, python-brace-format +msgid "\"{input}\" is not a valid boolean." +msgstr "" + +#: fields.py:669 +msgid "This field may not be blank." +msgstr "" + +#: fields.py:670 fields.py:1664 +#, python-brace-format +msgid "Ensure this field has no more than {max_length} characters." +msgstr "" + +#: fields.py:671 +#, python-brace-format +msgid "Ensure this field has at least {min_length} characters." +msgstr "" + +#: fields.py:708 +msgid "Enter a valid email address." +msgstr "" + +#: fields.py:719 +msgid "This value does not match the required pattern." +msgstr "" + +#: fields.py:730 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "" + +#: fields.py:742 +msgid "Enter a valid URL." +msgstr "" + +#: fields.py:755 +#, python-brace-format +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:791 +msgid "Enter a valid IPv4 or IPv6 address." +msgstr "" + +#: fields.py:816 +msgid "A valid integer is required." +msgstr "" + +#: fields.py:817 fields.py:852 fields.py:885 +#, python-brace-format +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "" + +#: fields.py:818 fields.py:853 fields.py:886 +#, python-brace-format +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "" + +#: fields.py:819 fields.py:854 fields.py:890 +msgid "String value too large." +msgstr "" + +#: fields.py:851 fields.py:884 +msgid "A valid number is required." +msgstr "" + +#: fields.py:887 +#, python-brace-format +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "" + +#: fields.py:888 +#, python-brace-format +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "" + +#: fields.py:889 +#, python-brace-format +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "" + +#: fields.py:1004 +#, python-brace-format +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:1005 +msgid "Expected a datetime but got a date." +msgstr "" + +#: fields.py:1082 +#, python-brace-format +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:1083 +msgid "Expected a date but got a datetime." +msgstr "" + +#: fields.py:1151 +#, python-brace-format +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:1215 +#, python-brace-format +msgid "Duration has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:1240 fields.py:1289 +#, python-brace-format +msgid "\"{input}\" is not a valid choice." +msgstr "" + +#: fields.py:1243 relations.py:71 relations.py:442 +#, python-brace-format +msgid "More than {count} items..." +msgstr "" + +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 +#, python-brace-format +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "" + +#: fields.py:1291 +msgid "This selection may not be empty." +msgstr "" + +#: fields.py:1328 +#, python-brace-format +msgid "\"{input}\" is not a valid path choice." +msgstr "" + +#: fields.py:1347 +msgid "No file was submitted." +msgstr "" + +#: fields.py:1348 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "" + +#: fields.py:1349 +msgid "No filename could be determined." +msgstr "" + +#: fields.py:1350 +msgid "The submitted file is empty." +msgstr "" + +#: fields.py:1351 +#, python-brace-format +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "" + +#: fields.py:1399 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: fields.py:1438 relations.py:439 serializers.py:521 +msgid "This list may not be empty." +msgstr "" + +#: fields.py:1491 +#, python-brace-format +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: fields.py:1538 +msgid "Value must be valid JSON." +msgstr "" + +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 +msgid "Submit" +msgstr "" + +#: pagination.py:189 +msgid "Invalid page." +msgstr "" + +#: pagination.py:407 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:207 +#, python-brace-format +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "" + +#: relations.py:208 +#, python-brace-format +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "" + +#: relations.py:240 +msgid "Invalid hyperlink - No URL match." +msgstr "" + +#: relations.py:241 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "" + +#: relations.py:242 +msgid "Invalid hyperlink - Object does not exist." +msgstr "" + +#: relations.py:243 +#, python-brace-format +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "" + +#: relations.py:402 +#, python-brace-format +msgid "Object with {slug_name}={value} does not exist." +msgstr "" + +#: relations.py:403 +msgid "Invalid value." +msgstr "" + +#: serializers.py:326 +#, python-brace-format +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "" + +#: templates/rest_framework/admin.html:118 +#: templates/rest_framework/base.html:128 +msgid "Filters" +msgstr "" + +#: templates/rest_framework/filters/django_filter.html:2 +#: templates/rest_framework/filters/django_filter_crispyforms.html:4 +msgid "Field filters" +msgstr "" + +#: templates/rest_framework/filters/ordering.html:3 +msgid "Ordering" +msgstr "" + +#: templates/rest_framework/filters/search.html:2 +msgid "Search" +msgstr "" + +#: templates/rest_framework/horizontal/radio.html:2 +#: templates/rest_framework/inline/radio.html:2 +#: templates/rest_framework/vertical/radio.html:2 +msgid "None" +msgstr "" + +#: templates/rest_framework/horizontal/select_multiple.html:2 +#: templates/rest_framework/inline/select_multiple.html:2 +#: templates/rest_framework/vertical/select_multiple.html:2 +msgid "No items to select." +msgstr "" + +#: validators.py:24 +msgid "This field must be unique." +msgstr "" + +#: validators.py:78 +#, python-brace-format +msgid "The fields {field_names} must make a unique set." +msgstr "" + +#: validators.py:226 +#, python-brace-format +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "" + +#: validators.py:241 +#, python-brace-format +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "" + +#: validators.py:254 +#, python-brace-format +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "" + +#: versioning.py:42 +msgid "Invalid version in \"Accept\" header." +msgstr "" + +#: versioning.py:73 versioning.py:115 +msgid "Invalid version in URL path." +msgstr "" + +#: versioning.py:144 +msgid "Invalid version in hostname." +msgstr "" + +#: versioning.py:166 +msgid "Invalid version in query parameter." +msgstr "" + +#: views.py:88 +msgid "Permission denied." +msgstr "" diff --git a/rest_framework/locale/fi/LC_MESSAGES/django.mo b/rest_framework/locale/fi/LC_MESSAGES/django.mo index 2b7144816b59ad8a52ee9d5312b1dee78ee06bca..cd904e35cd5ce2b2f7c8b2ff4128363545f3de61 100644 GIT binary patch delta 2011 zcmYk-eN0t#9LMo5cDWZap}2WLP&`7YDVHu>L!RV8P*DW52SRESmey`Fui7MSxwE!f zI(4$$nwy(XtNw6p4dsuZST0tZ|7e@Gh11;9KN`)onM`x`NAJ(M$JKBA&g*x6_nh;4 z__r$*_c&ZXX-Vdmm^CI`xB$ZP?Qp@Y}34FADmEXgt}z!uc~eOQF= zViZ$Yj^E=-{1aDWWwxhlduXho<2*KF=y9_iY{%tz66^67?7;9$vpU?2`u!NL$IDoX z^Pe!w!&cOUdQkoE_#Q<)?^}FcOTABH79CLzEXwLpD{Ms#l)xJN9Gmc0Y{hvwW)(Pq z{Mir(U3U@}<0NY5GIGt9U@6A13oFpYwc6^hXsp1$k$bHANwY#+g*xv?tsse7QObAN z@4tvz@f}o10#BvyFU3WiH(~_eKnM3>7*C)ZpmB}{yK7(Lb2x#7$^Jq;_#x^6Ih+Ks z4D+$vKX1br=N+g}zK@#d52)*Y^UoikawEd%)mRl~{}sc8-*FnX@=4T%chSMzr_&RQ zp^~T{m*Qtwi9g{c%$RL92VeEw?Rx~P=s$xOGR&@{Li9yG`>$AEVw&3GX>_oFl;#?% z#Rb@nn$S)x!NaH(4C9OVJN9Bdqp!tJunljZa;212_28RWhu@*@f8hE*#CW<^@-Zp| zV>p0QsO0HjKee(!R7{7k5l2x2-N!gK%uCCZ6q>y@0j29uFaTX;)GA|ACrV9<$*& zu9eVWGPVX4immA%>>z5ZhkdW02E2z+EaXRB-;SElc2u&ZkWYbqiC$9ULe3}r^C{GX zrcwE4nLIT>#~ft0tr)dsb*LD%ViUfHY>It@3dL3A8Fm+yOc7?$f=#Fxe}GEvan$wG zzB$ZV<1EH9#A&YyspN_(fc@Q1=@BMty&XiL0;MD+mzZEEON^^ zQ>2$s^)9F=AdgCr#%gMp-?0gsseRM{RZCR{DjQjum#zwIH}(HTSHzr=HnXSdo(ZQ>D2d*qItHNe1$3E2epTJ5y zjsbiV8*m=C;6Jz>V=JhJGj4_?7q z+_=(gHTI(>^cbrDdDnMP@B0!%TB@(mjE_aQusDmOR@jdk=oxIs_c4mU;#Lf-GHb*q zk&nH`MbAy+I{X>6bHznw8?Xh{Ka5Ry92>ROAJVuN7cqjtn{zLmzzXi4b??ujR&oxt zvN_jZ+)7*(Z5m8RWO+Dn^8HkvxNO`qj7)^#qtf*{Q_#{#kb@h zY(R(m9jJ+oppxk<*5eNt#DY?@K@8)qc-r;6>qTs){|a7MW>#9p{wpS*F+IinchnY_ zlF|-#AkW%fT#E-$Ge3njcpf$3dF;mmM%{z^Q6ZhhZTK%LXST7b-FO1S_+y%eUQoeX z^@l#xO3tBHx_}Avvu{eGQRH3r3M!`W;b#0EHBkBOBpME(zW*{7;U}n2&Z8ExfZDU~rd1>}11(&(mkxgC2^`?injp=zmGzOqNl+)7nowG}s( z2siu>$sPmkvkLL?W`lN5<|l7UT1RsWRSDieRnbm!2(yJ>?CcJ diff --git a/rest_framework/locale/fi/LC_MESSAGES/django.po b/rest_framework/locale/fi/LC_MESSAGES/django.po index c6c24c647..39315af6b 100644 --- a/rest_framework/locale/fi/LC_MESSAGES/django.po +++ b/rest_framework/locale/fi/LC_MESSAGES/django.po @@ -9,9 +9,9 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-08 16:26+0000\n" -"Last-Translator: Aarni Koskela\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" +"Last-Translator: Xavier Ordoquy \n" "Language-Team: Finnish (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/fi/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -19,43 +19,75 @@ msgstr "" "Language: fi\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "Epäkelpo perusotsake. Ei annettuja tunnuksia." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "Epäkelpo perusotsake. Tunnusmerkkijono ei saa sisältää välilyöntejä." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "Epäkelpo perusotsake. Tunnukset eivät ole base64-koodattu." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Epäkelpo käyttäjänimi tai salasana." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "Käyttäjä ei-aktiivinen tai poistettu." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "Epäkelpo Token-otsake. Ei annettuja tunnuksia." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "Epäkelpo Token-otsake. Tunnusmerkkijono ei saa sisältää välilyöntejä." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "Epäkelpo Token-otsake. Tunnusmerkkijono ei saa sisältää epäkelpoja merkkejä." -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Epäkelpo Token." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "Käyttäjätili ei ole käytössä." @@ -110,7 +142,7 @@ msgstr "Pyynnön mediatyyppiä \"{media_type}\" ei tueta." msgid "Request was throttled." msgstr "Pyyntö hidastettu." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "Tämä kenttä vaaditaan." @@ -128,7 +160,7 @@ msgstr "\"{input}\" ei ole kelvollinen totuusarvo." msgid "This field may not be blank." msgstr "Tämä kenttä ei voi olla tyhjä." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Arvo saa olla enintään {max_length} merkkiä pitkä." @@ -214,137 +246,136 @@ msgstr "Virheellinen päivämäärän/ajan muotoilu. Käytä jotain näistä muo msgid "Expected a datetime but got a date." msgstr "Odotettiin päivämäärää ja aikaa, saatiin vain päivämäärä." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "Virheellinen päivämäärän muotoilu. Käytä jotain näistä muodoista: {format}" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "Odotettiin päivämäärää, saatiin päivämäärä ja aika." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "Virheellinen kellonajan muotoilu. Käytä jotain näistä muodoista: {format}" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "Virheellinen keston muotoilu. Käytä jotain näistä muodoista: {format}" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\" ei ole kelvollinen valinta." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "Enemmän kuin {count} kappaletta..." -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "Odotettiin listaa, saatiin tyyppi {input_type}." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "Valinta ei saa olla tyhjä." -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "\"{input}\" ei ole kelvollinen polku." -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "Yhtään tiedostoa ei ole lähetetty." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "Tiedostoa ei lähetetty. Tarkista lomakkeen koodaus (encoding)." -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "Tiedostonimeä ei voitu päätellä." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "Lähetetty tiedosto on tyhjä." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "Varmista että tiedostonimi on enintään {max_length} merkkiä pitkä (nyt {length})." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Kuva ei kelpaa. Lähettämäsi tiedosto ei ole kuva, tai tiedosto on vioittunut." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "Lista ei saa olla tyhjä." -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "Odotettiin sanakirjaa, saatiin tyyppi {input_type}." -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "Arvon pitää olla kelvollista JSONia." -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "Lähetä" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Epäkelpo sivu ({page_number}): {message}" +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "Epäkelpo kursori" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "Epäkelpo pääavain {pk_value} - objektia ei ole olemassa." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Väärä tyyppi. Odotettiin pääavainarvoa, saatiin {data_type}." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Epäkelpo linkki - URL ei täsmää." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Epäkelpo linkki - epäkelpo URL-osuma." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Epäkelpo linkki - objektia ei ole." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Epäkelpo tyyppi. Odotettiin URL-merkkijonoa, saatiin {data_type}." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "Objektia ({slug_name}={value}) ei ole." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Epäkelpo arvo." diff --git a/rest_framework/locale/fr/LC_MESSAGES/django.mo b/rest_framework/locale/fr/LC_MESSAGES/django.mo index 03bcfa949ca022d3279d332353a6188676be4fe5..531cc46b5dec5a0acc874182e1be8111759ecb4b 100644 GIT binary patch delta 2472 zcmYk+S!`5Q9LMqhw52oHmO@KQ3yM<*Ck)GF~-G7jZvPlWsPG?U`ICdwgak_$2xm|V1@*gbEXTJ|D|rED;V+oO^R0NiStXXCZm6hynEDX3WQZn1Kh8U9eYh9=?fe zw_S9-jC|}m7v1L;Dt)rJ$t27}wP#@^R-v|dXFmI{k)L#bcplaM4wXc=u@;LaruKF< zs(k=8gEOe#eS?1d12w?H0w=kw1;@}HK@H#_&c@fU3eOg>{+p=WqC+!X#qf)9t!p1@ zrU%^iA$*wjN&MVn_AP1!-{A;M!waY_{2jG5er_h0xb8+x@MYvDb|Oxtg34K}!QYUB zVAGkNvU?t`#eKL6FJdQ7W!g(Hh8=hsmtr<|SAPdSfk#kFeaE%+0kiG252BJV{wo!p zX_Lte&7cOgG@D#s!T{}2R5E%VG$S9a5xuwu=io-v@f^lx9L2482gA74pE{Q3ko2(e zCGPnzrJ|(Rh1#1Vs3kpz%JQ422mg&a*L4r^FdRmml50ppTRH1ojrFJpL{VGy95Qx0 zg}U!0xBnl^ApdMG(gBsXj)D|DdQl4)gP|;Fe!bNx$*WsiYsqF4WZP5$JHrg?_{}a@bUvq4!- zv#6OJMLpmn)PSy_2iFq|39YXF|9?^Dk0p49=AU}GI2A71LzVdiTk909$_j#UIAsIZ z<%9;Wtx+a(FszAKL#SvAmpHZ53EM(_JJCYSCX{o|^Oa;Af9IXQ6j^MT_0KAGP$Gi8Mk*ThyufD?3%viAh8Qp$u2iQPUFd zAi4-Ip`zti*56w)Xlx^tjO_&5Y%7U+B1qH_$}1I8#vUW$kElh_5!>X{?p_+H@`7`c zNWEJL<%#xMrI}Dds%YO;bYhgODoVx9TG9uThrH&?5k0r8vlM_F9KlAz5_4kCM z)|}XuQ=Ay}HG2aC;b1HrO8)EX%S;aE9`~fTh4&_BPRRCngM))l_YZ`U)p;kBzAp-m4-A|;;k2wh=nn;BiKeoatJHI?8mImSfn>H c$P|YL65Ztu6Ib>3=^wpiL-%~~Uir5F0Dqwyc>n+a delta 2189 zcmZA1T~JhI9LMoL8|?CC2*Q%k%K=11SzvilT-yXg(XdP)LGfkCHX>Lk9BUe)y{ObK znsM4{qKl%^(HbW&R?B9(u^PuFv*ISFahhT>Y@BAg@S>UQ<@?(`k<&B#KcDA3yL-;_ z{Gb0hFLgiP8T-B<{S~A25w{Z8JZ3RGpTU84&SzGJpQD3Uu@ZAL&2Gnfyal(Q{{JYJ z;sFfcJ6Mh1;9C3(*JEo|(wH5j)6T$G*n-8`W?QirSK!1 z;N!^0ra0(#CvYiVL1ivK&ukespvHG$9Uj0MrTR3TjW~;849-toa1={8f5ttZM6Ki) zYGvnLe{sjX`KgsJLv2kM_5U5X1fN7dzKITgfUyO1zNM3nSCK5+Z&-_eAlqfj$){4< z$UzrqM=$QeVvM-w6IjRjbEqvng_`J}sNa=vGo9C?YGm^Q^1p)45ChuFw^8Tcp=O?6 znEGKgI-GAnO>7WVOp~}AFJTZfi_ErR2p8hZuJ5~^!+OSN@NAk{k)Ql4HJ6y)B21^0 zYp@EH>R#8UU8hhh_zVO1BQ|0lck945?7~BMAO3_7U<0dZ!xyjXi;)L?F@OVj z9F@94?jm>KZaj=CqV(HRE2~9Kcq3}%Bd$}Z;+)1X`Y8m;!@ALj5v;~2D&w)&>9o?B z#+_Jr2S36w+=LgA?XyZJmGWlP9*?6^e;k+KMbw2=^15*V_5UI4#nY%F&o3vi4;%EV ziPF&x-a_roNn|cIi@LCveb9IfQWn;ad~6>F7HN~HE&C9etNo0cn3wc$Ppd~2Yd7X% zAF75P!3;hBuhF4?>cuTiPHhO6;9c4IRy33kg~M7G)9Mb*Zq?)VR= z6km63T%3A845R*c1S`0|ouR`u?J{a+*(_Vd6ms2;lj zq1KrqnJsM@FU;iDxWh^G^stT??_jfg+;4JRg_me2S_rl6L@Uupq!Zd2WlKdy;*wR* zQ*8GWH#QZcwzG{;cGOf~de5xW{;P=8$l6@Xoa?1JJmzyv)x4I_7HM15?j^J(YP|N7 zPe?PWFx7l1>`v?-6Q*=4@Sl&%0te0G_rRuLlcS0`A5*9y}hvzi~g7tyAl}@lZ)YK4fEiV0UWECTc!QvKA{F8-y6162~e2f2g u4`*a_#Cf8Bc<`Z!bL0JfFI(YBG%O0IB_=Abc;aJAFUR*(h2y8HcK!oBu;+jP diff --git a/rest_framework/locale/fr/LC_MESSAGES/django.po b/rest_framework/locale/fr/LC_MESSAGES/django.po index 17bff26b9..3824b1a2e 100644 --- a/rest_framework/locale/fr/LC_MESSAGES/django.po +++ b/rest_framework/locale/fr/LC_MESSAGES/django.po @@ -6,13 +6,13 @@ # Etienne Desgagné , 2015 # Martin Maillard , 2015 # Martin Maillard , 2015 -# Xavier Ordoquy , 2015 +# Xavier Ordoquy , 2015-2016 msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 19:53+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:40+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: French (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/fr/)\n" "MIME-Version: 1.0\n" @@ -21,43 +21,75 @@ msgstr "" "Language: fr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "En-tête « basic » non valide. Informations d'identification non fournies." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "En-tête « basic » non valide. Les informations d'identification ne doivent pas contenir d'espaces." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "En-tête « basic » non valide. Encodage base64 des informations d'identification incorrect." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Nom d'utilisateur et/ou mot de passe non valide(s)." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "Utilisateur inactif ou supprimé." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "En-tête « token » non valide. Informations d'identification non fournies." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "En-tête « token » non valide. Un token ne doit pas contenir d'espaces." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "En-tête « token » non valide. Un token ne doit pas contenir de caractères invalides." -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Token non valide." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "Jeton d'authentification" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "Clef" + +#: authtoken/models.py:23 +msgid "User" +msgstr "Utilisateur" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "Création" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "Jeton" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "Jetons" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "Nom de l'utilisateur" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "Mot de passe" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "Ce compte est désactivé." @@ -112,7 +144,7 @@ msgstr "Type de média \"{media_type}\" non supporté." msgid "Request was throttled." msgstr "Requête ralentie." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "Ce champ est obligatoire." @@ -130,7 +162,7 @@ msgstr "\"{input}\" n'est pas un booléen valide." msgid "This field may not be blank." msgstr "Ce champ ne peut être vide." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Assurez-vous que ce champ comporte au plus {max_length} caractères." @@ -216,137 +248,136 @@ msgstr "La date + heure n'a pas le bon format. Utilisez un des formats suivants msgid "Expected a datetime but got a date." msgstr "Attendait une date + heure mais a reçu une date." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "La date n'a pas le bon format. Utilisez un des formats suivants : {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "Attendait une date mais a reçu une date + heure." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "L'heure n'a pas le bon format. Utilisez un des formats suivants : {format}." -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "La durée n'a pas le bon format. Utilisez l'un des formats suivants: {format}." -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\" n'est pas un choix valide." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "Plus de {count} éléments..." -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "Attendait une liste d'éléments mais a reçu \"{input_type}\"." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "Cette sélection ne peut être vide." -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "\"{input}\" n'est pas un choix de chemin valide." -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "Aucun fichier n'a été soumis." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "La donnée soumise n'est pas un fichier. Vérifiez le type d'encodage du formulaire." -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "Le nom de fichier n'a pu être déterminé." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "Le fichier soumis est vide." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "Assurez-vous que le nom de fichier comporte au plus {max_length} caractères (il en comporte {length})." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Transférez une image valide. Le fichier que vous avez transféré n'est pas une image, ou il est corrompu." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "Cette liste ne peut pas être vide." -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "Attendait un dictionnaire d'éléments mais a reçu \"{input_type}\"." -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "La valeur doit être un JSON valide." -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "Envoyer" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Page \"{page_number}\" non valide : {message}." +msgid "Invalid page." +msgstr "Page invalide." #: pagination.py:407 msgid "Invalid cursor" msgstr "Curseur non valide" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "Clé primaire \"{pk_value}\" non valide - l'objet n'existe pas." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Type incorrect. Attendait une clé primaire, a reçu {data_type}." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Lien non valide : pas d'URL correspondante." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Lien non valide : URL correspondante incorrecte." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Lien non valide : l'objet n'existe pas." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Type incorrect. Attendait une URL, a reçu {data_type}." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "L'object avec {slug_name}={value} n'existe pas." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Valeur non valide." diff --git a/rest_framework/locale/fr_CA/LC_MESSAGES/django.mo b/rest_framework/locale/fr_CA/LC_MESSAGES/django.mo index eb0b496dbe39cfc698a45415b04692e5e6e72255..cd0a91340e7cd76179136ab12b81d454e887b7cd 100644 GIT binary patch delta 41 ncmeBY>1UZRf!9pez*yJ7P{Gi`%GhG!Txo=WIZ$BZQ7=XS+PDgC delta 41 ocmeBY>1UZRf!9>m&`8(7T*1)7%G7w`Txo=Wxs|Eu#-m=00NXAKbN~PV diff --git a/rest_framework/locale/fr_CA/LC_MESSAGES/django.po b/rest_framework/locale/fr_CA/LC_MESSAGES/django.po index 62a559dc5..f26ed453b 100644 --- a/rest_framework/locale/fr_CA/LC_MESSAGES/django.po +++ b/rest_framework/locale/fr_CA/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: French (Canada) (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/fr_CA/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: fr_CA\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -212,137 +244,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" diff --git a/rest_framework/locale/gl/LC_MESSAGES/django.mo b/rest_framework/locale/gl/LC_MESSAGES/django.mo index b8c3db3045fc5b4dda9854bbe77e7ce09310e5c9..761037249231da752320dce282cd550027a2b9c3 100644 GIT binary patch delta 41 ncmZo>X=a%)f!9pez*yJ7P{Gi`%GhG!Txo=WIZ$BZQ9DKe*5C?C delta 41 ocmZo>X=a%)f!9>m&`8(7T*1)7%G7w`Txo=Wxs|Eu#-nzO0M}LuPyhe` diff --git a/rest_framework/locale/gl/LC_MESSAGES/django.po b/rest_framework/locale/gl/LC_MESSAGES/django.po index f7e11d4d1..ba0b788fc 100644 --- a/rest_framework/locale/gl/LC_MESSAGES/django.po +++ b/rest_framework/locale/gl/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Galician (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/gl/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: gl\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -212,137 +244,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" diff --git a/rest_framework/locale/gl_ES/LC_MESSAGES/django.mo b/rest_framework/locale/gl_ES/LC_MESSAGES/django.mo index 0e5a00ac0b310b974fe6c52fac423d96ee92527f..281b6e66b7e66e840451489ae54e0fd19a7deba4 100644 GIT binary patch delta 42 ocmbQsI+u0AdtNhL17lqSLj^+%D`Sg^f20ut=0JhXDvXtk0R0pS+W-In delta 42 pcmbQsI+u0AdtOsrLnB=Sa|J^SD^uf%f20ut=2oVrn^hPq83Fx}3gG|% diff --git a/rest_framework/locale/gl_ES/LC_MESSAGES/django.po b/rest_framework/locale/gl_ES/LC_MESSAGES/django.po index 363d49463..07ee4061f 100644 --- a/rest_framework/locale/gl_ES/LC_MESSAGES/django.po +++ b/rest_framework/locale/gl_ES/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Galician (Spain) (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/gl_ES/)\n" "MIME-Version: 1.0\n" @@ -18,43 +18,75 @@ msgstr "" "Language: gl_ES\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -109,7 +141,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -127,7 +159,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -213,137 +245,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Valor non válido." diff --git a/rest_framework/locale/he_IL/LC_MESSAGES/django.mo b/rest_framework/locale/he_IL/LC_MESSAGES/django.mo index 53ecccd1e9183668dd55954698190ac1a59165e1..feef64e1b36c9be38b32bf05c413c471116f8199 100644 GIT binary patch delta 41 ncmbQhGJ$2n1YR>;17lqSLj^+%D`Sg^bEOdi=0JgsN4*&V+X@PC delta 41 pcmbQhGJ$2n1YT2JLnB=Sa|J^SD^uf%bEOdi=2oVr8;^Q30sz~93U>ei diff --git a/rest_framework/locale/he_IL/LC_MESSAGES/django.po b/rest_framework/locale/he_IL/LC_MESSAGES/django.po index b2162a4dc..cee35fedc 100644 --- a/rest_framework/locale/he_IL/LC_MESSAGES/django.po +++ b/rest_framework/locale/he_IL/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Hebrew (Israel) (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/he_IL/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: he_IL\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -212,137 +244,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" diff --git a/rest_framework/locale/hu/LC_MESSAGES/django.mo b/rest_framework/locale/hu/LC_MESSAGES/django.mo index a35f73215aa61525aa091d4b18b5df07ae899836..9053ad62fefb8c069bc5eaafbfcfbb5be1cf8948 100644 GIT binary patch delta 1546 zcmYk+OGs2v9LMp$k5MZvolLXIyk=#kqfS$5KJu9g6(mU!nNd>F!;)IGa4`}=kS;2d zbR(3|A_xMBpfD>ULfTY95mXxsN+?{kX%T&Y_fFPsvX{LiDmWwbf+(H}o( zlxFI3YFn(?LF|v`hmx9LmWyfVU>KKT8|wKk4B#aU;w>!1DXhnsM6)JrM17+VYw->4 zHj7w#l35uYC$JJnaT|WZ3M`oC-9LicX^)^EXHWtB#ykwn_pHH9v^#M%4x`>Pf#vuM zYcRaPtby;_SuTp`xQ}|^3$DeSWN+jxxSsZLEWtt4^Utvwf8s(6rXTBvj#|_R&S5cLMa|(;EI?nHSqMY87dtTv$D_W*4YX%*GRABb z^Hf6n$6{|PoOExXO{nJ%r;~rZc!&-MpJ5e##vQnFiDx&Sr~L?7G*(A?)E>fJ7+{!f zcpA^(Gi<;zR%#>mU^_m>9Ly$aWx6_x{Oga0>5valsaZrCh{2jrDcy&)IEwn!XDg=w^7ee;~vb9WP2ZyT6EWtLD@JeMN_C3B(gxXnoDsb zwjMILZb&BZ%ZL>Z2^zfYk8x`_J2 zU0j7PQ7@W}8qD=}%`sF;Z${gbxSe(!QG{_PD!>caffLAkBesG)qWxNd8gVzqpr%1* zkkYUFbyNky+HwyRaEr3?e<`EEdUMYRcBUJwMjA`4r8-oO7WWWaXh+Sq(D+-FO1|$F zcI8}A%C!Y+s9G0H|6Jj?a06#RZmHr|BUK>@?&#MJbouhqv;tHTsah@712=q`Sz4eW zs!BaoJ18o-)8(!-tuhvqdvYoN>&KQP3@41eP5$6ZDGio{ fgC!v+R9+D-A9&&q{Oc*BXY8v#Y5rJl_DsSbHz1Km delta 1656 zcmaLXOKeP09LMp$$Fw!|YU@>xx@|p5O*_*Xt*O$Qq6tE1P|s9MgbYe)Letz}ArT8w zZ%r*k!bT(&N$n&=kf1gq5gQ^l9tj~d4d36Lo5spX&V24UbMHOp|3By4Ys*g6yKl3H zo-;~4buxAHV6!Id9Lf*HNi)mGd~`656L1IW{&t*%mobEQaSp!6#h8_DwiFw18D7B} ze244IT$`3*RzXJ_R$(_T$1fPes$r@BTX6;LTbPY~s0oY=niXQP?<$PaZo%nz6ZM|w zSc$<*vqc!kIOE%KE=uTlgu3Ae7Gde|ROB0QHti#*{_7aVXQ=1?U^SMEU^1A*iP+}1 z&!Z-I9W~*{z8}=j_%?*66wyq~!x+xQMjVGn(ZP#I5Y~gG_`v`FJC3KFIVv@=LR6|7 zPy=?N{_pYIy{IMojP5)xf}>L@u0^#EqawJCrT7%JmVH=^3$x53xEmX>6Lay0?`Wba zqdgm+3^Ln-VcJDwQcKc^3e+7#{&iy)9eQyuI@pg3FmJ5cO5Ego6OYmUiEJJ_LVDEh z#kJT#m<@OX4`4sWv6&rPhj(x@{=%uaE|>f()vZil4|e%}LuI0#G%y!)Q7Ju(HTVWK z;1u$>75Aeu^a7V)4tZC^jW`QWVhP?sW%M1+#~gP`>cN$`o{rO~0Y9QPTR#$<%^+XO zR5j{F&8Xdd73bnZWI63O&cGny#V~@}yh$v;3&<+kJ*-0aDHlxI(jB6~LafGG)P!6l zTlNYyp|7a>bMwunVHEYEjlSoxlJ-kfW-}R8?Q&d!Ni4@6WP+}J;$kZuMVt}6_yk7r z8Y<$~I0!4K`X<#=wKR*Vx}QDgEe&`RQS|>74W2{IrgBofeWgGLQ)_kp9WFAcijtG$ zeH_3mia=4Sa9jt9Ca9xWL)CJz9R>=Y4=+OE@`|!a3|=8A-g0SAsEq4K1SVx^p;XeT z`c^LNI1?D3t9_&dsKlw-Pg?rAj{AX}h<1@SK!~aZRhdI&IlZ%~MQ4+FyG-j=+#N`} zl$O`DGvO4pHN&>OhxTkw9B40yIc<9qEiH7lcXtf`7>Gtgk+M*@!iiMIqUFWmNH`o^ wy(+XO(YmW;*S@`>x~7APm{S&x4BS@{i$=R|XJ-VReaRh7$^Q*-V{%{GUyW|H>Hq)$ diff --git a/rest_framework/locale/hu/LC_MESSAGES/django.po b/rest_framework/locale/hu/LC_MESSAGES/django.po index d37e10426..669a7bffa 100644 --- a/rest_framework/locale/hu/LC_MESSAGES/django.po +++ b/rest_framework/locale/hu/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Hungarian (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/hu/)\n" "MIME-Version: 1.0\n" @@ -18,43 +18,75 @@ msgstr "" "Language: hu\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "Érvénytelen basic fejlécmező. Nem voltak megadva azonosítók." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "Érvénytelen basic fejlécmező. Az azonosító karakterlánc nem tartalmazhat szóközöket." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "Érvénytelen basic fejlécmező. Az azonosítók base64 kódolása nem megfelelő." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Érvénytelen felhasználónév/jelszó." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "A felhasználó nincs aktiválva vagy törölve lett." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "Érvénytelen token fejlécmező. Nem voltak megadva azonosítók." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "Érvénytelen token fejlécmező. A token karakterlánc nem tartalmazhat szóközöket." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Érvénytelen token." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "A felhasználó tiltva van." @@ -109,7 +141,7 @@ msgstr "Nem támogatott média típus \"{media_type}\" a kérésben." msgid "Request was throttled." msgstr "A kérés korlátozva lett." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "Ennek a mezőnek a megadása kötelező." @@ -127,7 +159,7 @@ msgstr "Az \"{input}\" nem egy érvényes logikai érték." msgid "This field may not be blank." msgstr "Ez a mező nem lehet üres." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Bizonyosodjon meg arról, hogy ez a mező legfeljebb {max_length} karakterből áll." @@ -213,137 +245,136 @@ msgstr "A dátum formátuma hibás. Használja ezek valamelyikét helyette: {for msgid "Expected a datetime but got a date." msgstr "Időt is tartalmazó dátum helyett egy időt nem tartalmazó dátum lett elküldve." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "A dátum formátuma hibás. Használja ezek valamelyikét helyette: {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "Időt nem tartalmazó dátum helyett egy időt is tartalmazó dátum lett elküldve." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "Az idő formátuma hibás. Használja ezek valamelyikét helyette: {format}." -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "Az \"{input}\" nem egy érvényes elem." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "Elemek listája helyett \"{input_type}\" lett elküldve." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "Semmilyen fájl sem került feltöltésre." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "Az elküldött adat nem egy fájl volt. Ellenőrizze a kódolás típusát az űrlapon!" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "A fájlnév nem megállapítható." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "A küldött fájl üres." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "Bizonyosodjon meg arról, hogy a fájlnév legfeljebb {max_length} karakterből áll (jelenlegi hossza: {length})." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Töltsön fel egy érvényes képfájlt! A feltöltött fájl nem kép volt, vagy megsérült." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Érvénytelen oldal \"{page_number}\": {message}." +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "Érvénytelen pk \"{pk_value}\" - az objektum nem létezik." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Helytelen típus. pk érték helyett {data_type} lett elküldve." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Érvénytelen link - Nem illeszkedő URL." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Érvénytelen link. - Eltérő URL illeszkedés." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Érvénytelen link - Az objektum nem létezik." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Helytelen típus. URL karakterlánc helyett {data_type} lett elküldve." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "Nem létezik olyan objektum, amelynél {slug_name}={value}." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Érvénytelen érték." diff --git a/rest_framework/locale/id/LC_MESSAGES/django.mo b/rest_framework/locale/id/LC_MESSAGES/django.mo index ac3fe2c0fc77def64903095adf7d6c8acb8a178e..350d64a3b89e2d42301c885b4d4dc018ea2b21e3 100644 GIT binary patch delta 41 ncmeyz{EvCU1YR>;17lqSLj^+%D`Sg^bEOdi=0JgsM=cov{P+tw delta 41 pcmeyz{EvCU1YT2JLnB=Sa|J^SD^uf%bEOdi=2oVr8;@Et0s#GP3qJq= diff --git a/rest_framework/locale/id/LC_MESSAGES/django.po b/rest_framework/locale/id/LC_MESSAGES/django.po index 5e02437e8..1137755c8 100644 --- a/rest_framework/locale/id/LC_MESSAGES/django.po +++ b/rest_framework/locale/id/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Indonesian (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/id/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: id\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -212,137 +244,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" diff --git a/rest_framework/locale/it/LC_MESSAGES/django.mo b/rest_framework/locale/it/LC_MESSAGES/django.mo index fde0bde4669aec8382323aaa8ffc27630d30b4b0..8af8bc74694ef0ffc80d50e8c594e49bc6636d0c 100644 GIT binary patch delta 2657 zcmZ|Pdu$X%9Ki9p+7|_B=>tlkW!i!i+n!e*rM6NYh0@Z}mICFKDBIrBZoIo~?)Hil z$o=788Vx2JLP7%dMS}kj;~A8wF^VRdV37Wy_yB^57!y&EAR0~Z`@37KiE+~DXJ&79 zcjot-+0&cu_9dC*f7|QR* zu^T6`8W+q^su0^yCNzL@J?41~<#})63R&u18uxRdo_{P#wVF-5Z zfsL{v*Yj2H`X?wWzJ-#K%vtICt8p>?R-BFdP~%}N!jqWDr12UJqO0D(hwyzQP3jkv z2mgWcfSGi%aS_hJI^?SG$J6M(c{xq(^8@K@% zCs<~XMi^xeOkxWb5Wg+hjk0-8qJ;8el!;!)PRuS(+lT%1k7GC9M1rT9nYE;>2bbV> z?85|Z7C)u2j7G(Rbng05LV6q}0vAwLat&8ujZQ~kAIe^tKnd+dT!9&t>2FCB$^u+u zU#oACN2og}&n>A+O(>yiXvi+@_Z&w#9xozkQkPKUcPM)!mwcs;6Uy}-tVbK=1t%~E z-$72c`q2A*68S`=!pA{TGYDg?od0qfSzPEud10^DH}PTmkK-VoLei;n*$@)iD&&%C zM0xQZY{rA0XHo9I<^5j!KzgBFD4Q>ajl5q?&}hfcP;MwE95P@BZpSB)XQ(fbw5xkw ze}0Wp%jvH}AO2k=ol+$1vLM;88>kzp@*GK*lx@_6ghIk8Me;5sgSwR}d6E!H-eu(y zW+`MMwRvRqQY6n(R#PQ1d>~U1fvHkQi+QIeEE`t}>5%oOOEyRpdc6KFTtnSW&7_jP z)V}CK0+l*`aw2-E|1OWvnMQ4+%11^Ddpxx{*CPibRR|izdw*`K^Upty^;FqGYb{mI zwWLLg95LC9QsfgYCt)R3@-HPPP3kBQ(C+v8d7g3vr7V%0@bOajQ91gl{CD6is^nTq z7FG5^D|IJTil)jZxsn>7x?g6Oxs%y-?v?C0nWKg?lDwT=oLSV_H?~~cQH_n-2nM5O z%>6a*W=XqchJt#Z6>`jIOtq&?elxu!!__mNEwmzIM#u{4p%Ei$49S>ee5T)5YDZ1& zj2IDpaLA5FoWt63%y7)_cc0Hco7)==no%n<>~1fp%QB7V(1@ETn47&dJ{Y!~6jTZhaAg}{AWP2wb1aD4j7@hsl#!ez@6#& zZQ9z~9eZBSl=9m@ZiS_FZ$86S10}WH-Rx&R6 zXz>wWr4<>CJBO>7QbeCWt>Y0rYDS0bXv|K(#7=%+;%3gbB0(!^jgQ+BOJbT1X=)mF zGF%q(l}Qw%mfWqakRCF^qjoB?Q7d_E-k}Wlsfq)|USMs%?lyFc;gMm(En84kI7FZw z0&Q_`$Q&m6$)76r`^x&P^XGKXnhLl*JZw6S)t0y^BP0dm#LN8uu}P;o d;j8WpNo-^{1kEwhn0^?clZ2$h=Kj2}=`VAsj`RQk delta 1951 zcmZA1ZERCj9LMok1YiUQO|iC6F7&B_#-A6-xT|e>E^^X?7>+~<7XH{tIU|?7)SlD2b=LI zDl%{4I{dtFei7sJgWRP1cj9Jz3i+82INXL`sL%N38V${O5w(Ky6?xlH=Lb+LoI-{E zS-c(J!fN~)!?=JI=viru7psw=m|ASdCe(9=QTI(?E=c1TjX0h{_QZUJnn)nPDlmrX zZ%6I%!?+Q1sIB_2&|gHYtdUVPU<$SOQ`m|QcHlL900R}oe-(`bOj{nu_4o!}w2Zlo zy76>XJ`!JInEubmGmM{QXfM~GBGZT3qA}FKuj5Xf$5CuyHM{UQ>inf3@!v{g6{)O0 zjA9Bu!an>1$8Za?(;mK!N~Q%I!qqHS5juno_#EDa=L+7yz4TLrZ2)a-!Fk++B{||c zNMjT`@hoZ!uA)NKxH`Ym`%yD~3^fr4yYV_I)azJ&6h}}Id>)nUpQ0vu9Z3(9UPH9- z9uA6NZkmQ>bOM!B=L>#|G5U%((KhYK&-8Inwm*XW%!?c}@cYOuW*&=BuLduW@u99) zA!%jOs4cx0d2Y@eqp^t-ui`$ughbbLM)KJ_hze~6HNXi>;yVQwQ1^#;RdjtAH{uc0 z2WJj-{WsLWF&=gwj$otG_dE@vZLZ)3{1q+KDwvBK6>uLWwJihog=G~vzDVw)!>G;c zrQS{TQJbkswNh#uwUXLTT`GJ&Zx$wTv&3oLN!5(VTClto*qt&DF1G*sM7 z$v53JEh)4;n5HUal?}I0RkTsOTJF0;X1d>9rQ>p{_JFK*(_5*n>{I6Gee_ZXsU|Wr zk$uTmWnV68a~gcbUVF&@y^}7h^weeckB1{OQ}9ss$;o}=hi4RomaN+1}j{bPhKBWI1P=j#_qabK)Nt_uGB| diff --git a/rest_framework/locale/it/LC_MESSAGES/django.po b/rest_framework/locale/it/LC_MESSAGES/django.po index 4889f61b7..24101d88e 100644 --- a/rest_framework/locale/it/LC_MESSAGES/django.po +++ b/rest_framework/locale/it/LC_MESSAGES/django.po @@ -11,8 +11,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Italian (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/it/)\n" "MIME-Version: 1.0\n" @@ -21,43 +21,75 @@ msgstr "" "Language: it\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "Header di base invalido. Credenziali non fornite." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "Header di base invalido. Le credenziali non dovrebbero contenere spazi." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "Credenziali non correttamente codificate in base64." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Nome utente/password non validi" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "Utente inattivo o eliminato." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "Header del token non valido. Credenziali non fornite." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "Header del token non valido. Il contenuto del token non dovrebbe contenere spazi." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." -msgstr "" +msgstr "Header del token invalido. La stringa del token non dovrebbe contenere caratteri illegali." -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Token invalido." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "L'account dell'utente è disabilitato" @@ -112,7 +144,7 @@ msgstr "Tipo di media \"{media_type}\"non supportato." msgid "Request was throttled." msgstr "La richiesta è stata limitata (throttled)." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "Campo obbligatorio." @@ -130,7 +162,7 @@ msgstr "\"{input}\" non è un valido valore booleano." msgid "This field may not be blank." msgstr "Questo campo non può essere omesso." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Assicurati che questo campo non abbia più di {max_length} caratteri." @@ -165,7 +197,7 @@ msgstr "\"{value}\" non è un UUID valido." #: fields.py:791 msgid "Enter a valid IPv4 or IPv6 address." -msgstr "" +msgstr "Inserisci un indirizzo IPv4 o IPv6 valido." #: fields.py:816 msgid "A valid integer is required." @@ -216,137 +248,136 @@ msgstr "L'oggetto di tipo datetime è in un formato errato. Usa uno dei seguenti msgid "Expected a datetime but got a date." msgstr "Atteso un oggetto di tipo datetime ma l'oggetto ricevuto è di tipo date." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "La data è in un formato errato. Usa uno dei seguenti formati: {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "Atteso un oggetto di tipo date ma l'oggetto ricevuto è di tipo datetime." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "L'orario ha un formato errato. Usa uno dei seguenti formati: {format}." -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "La durata è in un formato errato. Usa uno dei seguenti formati: {format}." -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\" non è una scelta valida." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." -msgstr "" +msgstr "Più di {count} oggetti..." -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "Attesa una lista di oggetti ma l'oggetto ricevuto è di tipo \"{input_type}\"." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." -msgstr "" +msgstr "Questa selezione potrebbe non essere vuota." -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." -msgstr "" +msgstr "\"{input}\" non è un percorso valido." -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "Non è stato inviato alcun file." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "I dati inviati non corrispondono ad un file. Si prega di controllare il tipo di codifica nel form." -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "Il nome del file non può essere determinato." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "Il file inviato è vuoto." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "Assicurati che il nome del file abbia, al più, {max_length} caratteri (attualmente ne ha {length})." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Invia un'immagine valida. Il file che hai inviato non era un'immagine o era corrotto." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." -msgstr "" +msgstr "Questa lista potrebbe non essere vuota." -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "Era atteso un dizionario di oggetti ma il dato ricevuto è di tipo \"{input_type}\"." -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." -msgstr "" +msgstr "Il valore deve essere un JSON valido." -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" -msgstr "" +msgstr "Invia" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Pagina \"{page_number}\" non valida: {message}." +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "Cursore non valido" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "Pk \"{pk_value}\" non valido - l'oggetto non esiste." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Tipo non corretto. Era atteso un valore pk, ma è stato ricevuto {data_type}." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Collegamento non valido - Nessuna corrispondenza di URL." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Collegamento non valido - Corrispondenza di URL non corretta." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Collegamento non valido - L'oggetto non esiste." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Tipo non corretto. Era attesa una stringa URL, ma è stato ricevuto {data_type}." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "L'oggetto con {slug_name}={value} non esiste." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Valore non valido." @@ -358,20 +389,20 @@ msgstr "Dati non validi. Era atteso un dizionario, ma si è ricevuto {datatype}. #: templates/rest_framework/admin.html:118 #: templates/rest_framework/base.html:128 msgid "Filters" -msgstr "" +msgstr "Filtri" #: templates/rest_framework/filters/django_filter.html:2 #: templates/rest_framework/filters/django_filter_crispyforms.html:4 msgid "Field filters" -msgstr "" +msgstr "Filtri per il campo" #: templates/rest_framework/filters/ordering.html:3 msgid "Ordering" -msgstr "" +msgstr "Ordinamento" #: templates/rest_framework/filters/search.html:2 msgid "Search" -msgstr "" +msgstr "Cerca" #: templates/rest_framework/horizontal/radio.html:2 #: templates/rest_framework/inline/radio.html:2 diff --git a/rest_framework/locale/ja/LC_MESSAGES/django.mo b/rest_framework/locale/ja/LC_MESSAGES/django.mo index 4a583925ea55167ec2eb31cbe8b3ef97729c5aee..048da56fd3776f05337b9498f6444bf9546a7a85 100644 GIT binary patch delta 2633 zcmYk-dr(wW9Ki7d%S%}lLN*b@>0Oi=51_orI!vhD(*m8$zGg{S1^nBtA}LdixZ+*W-P@ycnqbZ+xQ&*Zk8=Eftig! znNhB3nOXljvWuz{W#&gvwyGbcpKtJa^r3qcnJ8LmI1yv92t%+OS#?#3Q_z8IuR3aa z9QjlIJfxrVD97d&#^Xn}4+VEBnR#xxIQrln?i z9Zsd}!b?F)eTcF}n>qTq_z}uk_!*_$I69L(UWpmF)3hI11a;f|{7*NTQYzw^7RNw2 zuoAbK{$QHNbStQT8|BphjjM1T>$nU%@ntmd6`VCDaF`F9Ud8RyTQ~@XxC>=~?#pCm zllco}CTZgWR-trw6xZQH=CmHTK|o3>nWPunITewV88&JSnW1KFF-DvDIl3)0)-9K-f>=aM zC)f-B1(SJ?A|%O;XN{GOke!}I$oeD@D+rC?w^pqtIPEHhs3MjUa#AEMA@Ye~1Sj18 z41K@kx_$VN$$jmah!5}|~oDFhc?y($F~`>9s?bAh8Rr%KXG0sK$d*g|3) z!Ikm4U?TUD8i7R&4YiqQPj#`)9*{F&GdONLg!QStNbp=_L*th9hqNgP^IPIEi zldWFcQQgo~@7k$3T=u%g%uKyI`gF{4XN}$IsDDkZur)SrZE)7;r7`KDcAK+$lfEM+ zF|4eqs?Oo^>M`ZP*1)Lz&qn=0>^c3!$m!!(H`<-`wmQ37W29NXKX2-sP5=M^ delta 2111 zcmZY9c}!eI9Ki8mxuFYXITk3=2UPBRyMkLOrG*v&kG2HC9#oL3Nh?-LtfkEpY0_9j zOf#Zw(-$Ql6n5fbChfUP)@9nELI>~Q8GqdlQ`OVCm z*!DrE`K2dfOi|j2EMnHB)E?X#$q&VdRw@T`(7<(gHQtWdcm&gN66OB0xD3C-GQ5PP zSQMkwTI|5}cs!)3zNWH)j)GXF)?hz&;561?bevMFupV1+2m|;o%Kxr-rP?rnd3YE- zIE6Bx(@y_+$G=dXm$yVIzcQ8TrXn*MLwWEiti|849xL3;8vC&vElj{~QLg(13$d6^ z$<$^n#zB<+C$SRW!*cusug9W9rRsRU8lXa-dJ!}6EvJ1RWk!oAGf7w)vIM2S8D*xs zP?o~P0(=6~@EtVpA|~UX7=fN7rAVIgqFF&Dmx?@S6UvQ!=*0tAiI1Wz(K(cXxVc#V zFGgvvMOpK1yatC*mgXg=y@)cg97ZA6U55sCCX;^|!2NW{=9t71{1K}#Jw>T5Y{3kC z+VM12)1Je#VM@iPl1kbiGI|-n5=JLWw+`jH5tJo)70d7&$Ha86Xm@T=2fJFN(IWu zcR7xuJn##=6{}fbxnUH0aR#?x)^erp!u_}d=TQdOWQ6v_QS72UjnW^-4sF5?l(u=2 zN*$FSum$sTLz&ov{j}f4o#G&08gl$Ek8%9uGG=;K67jQdfFdb>{LutQ)^5T>drLM)N zQ0}{ck^y%qlfiJ}e}(tkMK&K9Qhaml*>Ga)kI5QLA}*I^r^0@-OAEhM#3rJR;9d67 z+<;PKK+H=uNP`khtRiBiL6I!lM@x1w6SQ}&e2k>H2uY>PLP{JVDUBjl6KrA?Nf?AI z;Yvb|vlPjYWb<+%8Olx65Q$DFTgSA|y_`{5|D zcHvX-PhmyuPxcKMPoxp81Y1kpOx#2?5;5{7ImfcGHWN|;#8SH!`c5Z1^$64t$7JfB zm~;9!cbaaFZLp5U)<;+i?i5${ZFlq;xkL9r_uYfLd;0D@m>V#LcJ~bo&~?!IDRCl9 zA4~G-g(OQ)c}ny}kJq~7>2&EGsk1sKty1qzJL2}2RaKW&))-Z_0l!bjdzby&Qxov} zEuZ%}mu}C^*XfxXqZS^Ym_Kz)CzWX=S4;C8wWMQ^-rf@Rc RI#lWl(`U+h_0ID8zX6b-2G#%o diff --git a/rest_framework/locale/ja/LC_MESSAGES/django.po b/rest_framework/locale/ja/LC_MESSAGES/django.po index ea9ba1728..85c2fa5ae 100644 --- a/rest_framework/locale/ja/LC_MESSAGES/django.po +++ b/rest_framework/locale/ja/LC_MESSAGES/django.po @@ -3,12 +3,13 @@ # This file is distributed under the same license as the PACKAGE package. # # Translators: +# Hiroaki Nakamura , 2016 msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Japanese (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/ja/)\n" "MIME-Version: 1.0\n" @@ -17,43 +18,75 @@ msgstr "" "Language: ja\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "不正な基本ヘッダです。認証情報が含まれていません。" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "不正な基本ヘッダです。認証情報文字列に空白を含めてはいけません。" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "不正な基本ヘッダです。認証情報がBASE64で正しくエンコードされていません。" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "ユーザ名かパスワードが違います。" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "ユーザが無効か削除されています。" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "不正なトークンヘッダです。認証情報が含まれていません。" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "不正なトークンヘッダです。トークン文字列に空白を含めてはいけません。" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "不正なトークンヘッダです。トークン文字列に不正な文字を含めてはいけません。" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "不正なトークンです。" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "認証トークン" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "キー" + +#: authtoken/models.py:23 +msgid "User" +msgstr "ユーザ" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "作成された" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "トークン" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "トークン" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "ユーザ名" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "パスワード" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "ユーザアカウントが無効化されています。" @@ -108,7 +141,7 @@ msgstr "リクエストのメディアタイプ \"{media_type}\" はサポート msgid "Request was throttled." msgstr "リクエストの処理は絞られました。" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "この項目は必須です。" @@ -126,7 +159,7 @@ msgstr "\"{input}\" は有効なブーリアンではありません。" msgid "This field may not be blank." msgstr "この項目は空にできません。" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "この項目が{max_length}文字より長くならないようにしてください。" @@ -212,137 +245,136 @@ msgstr "日時の形式が違います。以下のどれかの形式にしてく msgid "Expected a datetime but got a date." msgstr "日付ではなく日時を入力してください。" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "日付の形式が違います。以下のどれかの形式にしてください: {format}。" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "日時ではなく日付を入力してください。" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "時刻の形式が違います。以下のどれかの形式にしてください: {format}。" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "機関の形式が違います。以下のどれかの形式にしてください: {format}。" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\"は有効な選択肢ではありません。" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." -msgstr "" +msgstr " {count} 個より多い..." -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "\"{input_type}\" 型のデータではなく項目のリストを入力してください。" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "空でない項目を選択してください。" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "\"{input}\"は有効なパスの選択肢ではありません。" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "ファイルが添付されていません。" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "添付されたデータはファイルではありません。フォームのエンコーディングタイプを確認してください。" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "ファイル名が取得できませんでした。" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "添付ファイルの中身が空でした。" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "ファイル名は最大{max_length}文字にしてください({length}文字でした)。" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "有効な画像をアップロードしてください。アップロードされたファイルは画像でないか壊れた画像です。" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "リストは空ではいけません。" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "\"{input_type}\" 型のデータではなく項目の辞書を入力してください。" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." -msgstr "" +msgstr "値は有効なJSONでなければなりません。" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" -msgstr "" +msgstr "提出" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "ページ番号 \"{page_number}\" は不正です: {message}。" +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "カーソルが不正です。" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "主キー \"{pk_value}\" は不正です - データが存在しません。" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "不正な型です。{data_type} 型ではなく主キーの値を入力してください。" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "ハイパーリンクが不正です - URLにマッチしません。" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "ハイパーリンクが不正です - 不正なURLにマッチします。" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "ハイパーリンクが不正です - リンク先が存在しません。" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "不正なデータ型です。{data_type} 型ではなくURL文字列を入力してください。" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "{slug_name}={value} のデータが存在しません。" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "不正な値です。" @@ -354,20 +386,20 @@ msgstr "不正なデータです。{datatype} 型ではなく辞書を入力し #: templates/rest_framework/admin.html:118 #: templates/rest_framework/base.html:128 msgid "Filters" -msgstr "" +msgstr "フィルタ" #: templates/rest_framework/filters/django_filter.html:2 #: templates/rest_framework/filters/django_filter_crispyforms.html:4 msgid "Field filters" -msgstr "" +msgstr "フィールドフィルタ" #: templates/rest_framework/filters/ordering.html:3 msgid "Ordering" -msgstr "" +msgstr "順序" #: templates/rest_framework/filters/search.html:2 msgid "Search" -msgstr "" +msgstr "検索" #: templates/rest_framework/horizontal/radio.html:2 #: templates/rest_framework/inline/radio.html:2 diff --git a/rest_framework/locale/ko_KR/LC_MESSAGES/django.mo b/rest_framework/locale/ko_KR/LC_MESSAGES/django.mo index f9a7261f8b404f226268ff099443cefbc8bf6c49..ac88a062dc30575bd46c86f2479fcd819a420a1f 100644 GIT binary patch delta 1672 zcmY+^-%rhP9LMpu^aGV2g$|-qNIFiPQ&N4CqU2Ym8^%TW6odEyS7CaR*#@k}Iy{Y=@EKN{ zMJ#QaSqlw2u@vuP1&-r3Tr%Cv!NaKU?_xc^#if`(!z>LOQ3Gj5wV(97jrzaGSb}lM zX3Mb*9iDIdxhbRJI#%H*)?iwSSpl}-OgxL~cLnqCIcjAlF&}-YtQOXwe%FUv@D5hu z7u5d*xGY2mXYzd8#f?VXiJC#b=UuOT1U18tsHOjbE6_K~Y(7?_A9rIqo-sk!6ZiXz;&}@U$mm?y{Nssh#KiI7UC1sUVg^_I&;ViJcN625EtNA≦o zL_HT@OrZ~IEA!@>mE(cAtbd4`TQn@g7Z}8EUOjsrc|pAni}40_s2?7}Fw<(lK~!>m z#C;fKn%nU_s{Ji$rI(ShwK(MY*+**+4FwEadwUZ#kk=T-AE+4wSO&QtTksOr<0ov! z5|*nK&*5eq$Ez4%Ij`Xe9>=|lW2a&i+o=DFaHG9wV`JqIZp8`D(k!!X>ZedMn804_ z@y7=G4p&o8AQSnR!=)74P%C@I^AT21pG3xNMLDsph%|7cnfIfT>LK#6&s?+;DP$(A zWnommt*E^}jq3LRyKofsdxszK4C_E0voomwxzA-4zVzCEVu$9GL$7UoaTYc6SE!}S z%#9^o6t(y7k;JzQo-zeh5{Q+AW>!P!bdY227JyrHH2*DYsgPdoNlDe3R#Wj4al{5f zqvhS=o&ZjW#S?#uX0BPPXfS_@!%Z@=o*)N}UA1+D3a+GtKwEq?Ds|qhhH&=9@s0(E(!J#ltrkn9=P^Feoekgf25-Q}PB@yJdm1}AB zHdD59Y~7;7VLRQ|We$>aZqjJ~^d6`rvZD=&SxM1>)Xs#GHNjAEFckKO%ben}Kqwpv p&1h~4?l^d?ySMx3kzj3m-$BP;6bk?CS4zKVL3&q8^jX%Mq~FftpjH3? delta 1750 zcmaLXTWkzb9LMp0s~5DTR@J4ntL~R=yIX5(OIvLftxH7+ac!j%F2~iIcj}qVC%!u&dWdHLy=gjQPobx|tE-vX=8h_(U zIcb!7Vl=TW*{lU`r*NP&rkRb!W$4Ek=Hm$*feF_$E6%~0+)Vdt=P-)rTbI{y7WL*=P$PNf`OWLk80?O;7?px*oPuFK9g8;+AOu3S`7(E|^ACwjd0ZB$NQ;wb!qx>5EJH>XuNjdm9X@hYyy zcQ_Ku$%|ZpA>4salFZJdQo1XL`JYAQa*jJ?pKv_w;ltcTG#l04ip%jhmg0MC)^!YL zGj`%)e1}?O#SCv9cHlC6j_RMtiq(v_VJ-HJpruNP*W8ACQ91sAdhi&At&vruM$qng z30Km7g^MuAG&SM@T!T+>0ZwCDPhmTr#*8sM125oo%!`xAwNw_MQqk+#hxN3>1!gnw zAgX->x1bWuyW3Wb;TO~kuV+Q-`Yz;WH#tP`6_%i{(6t83X~)~CFbKPbQGA4&fiza4 z7FQ+GWv!?O97Sf&uA;7cfLa4doYZvzY{f>@|8JsRsDQXJOpcY?{yJ`(IAsfR^ViKz8#u}xznMw^Ymrx345-PcbUO1hIICDsa zdpIi{oKnS+wP^jEmb*KYuldAmLi=2^rZu5FR}%jgzE8$Ra0>5kX@sBPGhms72C+~% z)W@TmV0Sq=(QL5(oY87Z!$c~f!fZPwz%iL%CpjgB<2*t$GRx_4zX3{VhSwG>pTEj# zz6s99K#Qh`s3TaM#tw6ea;=YuN`FE*SDE5s6{!5y5?Y|YOMW7dR*;_G)Y{@N+_w`p zY~Rz?+_HOrVbs5`ttA$tYky*H){fMya3B~8ltlc&vS_%p`;IT~PfsKo4ktePS~LCs RzfqBO@65?bG!(o}{{=QWukio? diff --git a/rest_framework/locale/ko_KR/LC_MESSAGES/django.po b/rest_framework/locale/ko_KR/LC_MESSAGES/django.po index 901907c86..f0d56067f 100644 --- a/rest_framework/locale/ko_KR/LC_MESSAGES/django.po +++ b/rest_framework/locale/ko_KR/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Korean (Korea) (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/ko_KR/)\n" "MIME-Version: 1.0\n" @@ -18,43 +18,75 @@ msgstr "" "Language: ko_KR\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "기본 헤더(basic header)가 유효하지 않습니다. 인증데이터(credentials)가 제공되지 않았습니다." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "기본 헤더(basic header)가 유효하지 않습니다. 인증데이터(credentials) 문자열은 빈칸(spaces)을 포함하지 않아야 합니다." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "기본 헤더(basic header)가 유효하지 않습니다. 인증데이터(credentials)가 base64로 적절히 부호화(encode)되지 않았습니다." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "아이디/비밀번호가 유효하지 않습니다." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "계정이 중지되었거나 삭제되었습니다." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "토큰 헤더가 유효하지 않습니다. 인증데이터(credentials)가 제공되지 않았습니다." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "토큰 헤더가 유효하지 않습니다. 토큰 문자열은 빈칸(spaces)를 포함하지 않아야 합니다." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "토큰 헤더가 유효하지 않습니다. 토큰 문자열은 유효하지 않은 문자를 포함하지 않아야 합니다." -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "토큰이 유효하지 않습니다." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "사용자 계정을 사용할 수 없습니다." @@ -109,7 +141,7 @@ msgstr "요청된 \"{media_type}\"가 지원되지 않는 미디어 형태입니 msgid "Request was throttled." msgstr "요청이 지연(throttled)되었습니다." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "이 항목을 채워주십시오." @@ -127,7 +159,7 @@ msgstr "\"{input}\"이 유효하지 않은 부울(boolean)입니다." msgid "This field may not be blank." msgstr "이 칸은 blank일 수 없습니다." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "이 칸이 글자 수가 {max_length} 이하인지 확인하십시오." @@ -213,137 +245,136 @@ msgstr "Datetime의 포멧이 잘못되었습니다. 이 형식들 중 한가지 msgid "Expected a datetime but got a date." msgstr "예상된 datatime 대신 date를 받았습니다." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "Date의 포멧이 잘못되었습니다. 이 형식들 중 한가지를 사용하세요: {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "예상된 date 대신 datetime을 받았습니다." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "Time의 포멧이 잘못되었습니다. 이 형식들 중 한가지를 사용하세요: {format}." -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\"이 유효하지 않은 선택(choice)입니다." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "아이템 리스트가 예상되었으나 \"{input_type}\"를 받았습니다." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "파일이 제출되지 않았습니다." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "제출된 데이터는 파일이 아닙니다. 제출된 서식의 인코딩 형식을 확인하세요." -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "파일명을 알 수 없습니다." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "제출된 파일이 비어있습니다." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "이 파일명의 글자수가 최대 {max_length}를 넘지 않는지 확인하십시오. (이것은 {length}가 있습니다)." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "유효한 이미지 파일을 업로드 하십시오. 업로드 하신 파일은 이미지 파일이 아니거나 손상된 이미지 파일입니다." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "아이템 딕셔너리가 예상되었으나 \"{input_type}\" 타입을 받았습니다." -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "유효하지 않은 page \"{page_number}\": {message}." +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "커서(cursor)가 유효하지 않습니다." -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "유효하지 않은 pk \"{pk_value}\" - 객체가 존재하지 않습니다." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "잘못된 형식입니다. pk 값 대신 {data_type}를 받았습니다." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "유효하지 않은 하이퍼링크 - 일치하는 URL이 없습니다." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "유효하지 않은 하이퍼링크 - URL이 일치하지 않습니다." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "유효하지 않은 하이퍼링크 - 객체가 존재하지 않습니다." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "잘못된 형식입니다. URL 문자열을 예상했으나 {data_type}을 받았습니다." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "{slug_name}={value} 객체가 존재하지 않습니다." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "값이 유효하지 않습니다." diff --git a/rest_framework/locale/mk/LC_MESSAGES/django.mo b/rest_framework/locale/mk/LC_MESSAGES/django.mo index 64ef673cf5ab76c91f523b4cc26f5a4d3bd7ccd6..e2a518f71325e547319530bac20437003629b347 100644 GIT binary patch delta 1546 zcmY+^TS!zv7{Kvw#Z@z}tL8ObZ+X9LYr1LPR!u=6E0YAp%u-5?Aj=+tHG&>AA`(Lh zA`5y5>Y*hf!fr&A)Pq*|RJ{pi1QAgfRR8ast!Uu$H#2)YGxN>N`M&el4$nKA|D;eF zi6o-UPoxLW`16Mn5g?L^k!VLJ#$g-k^Fx@3V_1OKFdtu|8!dq%Td^K>jB%{NSJ*D% zk?0_iGCBsZ0>9OeP9W*F(t%Yc@yT+K8VFQiTeCgti-PvhR#rt1azaeyHF=Kh&s`8 zK5zK?=g`LZ@`*|^TKKOlwBtH#LOXUNla^5|!g1g4_b`_B0_wy*pr$x6+#IkM_4iR< zdkS@{ZeuCVqeoL86k&GMpsrv9i|{<^9zMoAv_^_J(19J;i}5(?a}hVv{*22Okqq`J zhjv)BIYAfddk>&)^>j4(FQM{=jx87*BhreEIEYg?idNFmji+%tF5yvhvAia{iEa1| z4`T}_bRW**9!z7FBX|na@CQ0DIf49lP-$h^J$N14F@!wnN_ue@Uhw%5$7s9R(6jgw zhp}_LNFUClPSC}E*5Vj8;|pxTxD=7SxF7ZZo_VO~-dEa1s<0C?@FLdWL)6SzQq5Cc zh4r+Dk+I}1mf<2+V>rvJz-_4S8%I6NbExn6f_g@ZIhndOp5s*dsa(Nh7|iJU;7QEK z`?wibumLlTF#*#7-o=o& l*3i;|VrN0I!|rgEJ6#ixY>EGR%INVf+eSjXms55J{{~KunX3Q* delta 1673 zcmY+^Uu;WJ9Ki7(+M$MR#;~!wnLktA+HFiLV`$GC0;!s{yZQO7U6;K@7`MCB)@*{@AmedbAIQX{#pNXL-ciy z?Yz*|5Cz1x(IRb_u<=1NGDM272n`J4bZkd`z7O;9GP>|4F2Z+Mi`ipER%0{P;}u+q zZ*h}IR7%E)__z_lWthYU{EB{DHs1Qa7aPfMU=IF99l#`q$Q*R0U5j4wFwVv6sPB1! zOVBYvWCaE>NdI!2i!yFJMtxuy7hrj&HS;aFko*Yh{Q-32bJXAc!D=j<$Y8J&XJRCs zUqBt;0P29BrhTvX>0ie1E1l7NoQ-~*k1aS2kD`GWkx9sHEXPOb@4sUn`Gm>Vz~-Qq zx*4^@1nT#<(|HPY$-ZE8F&B<3tHd>^`C-%<3}QJxLtV??=)^#_hzIvz3np*|4yVmx zHWlOx@zW@g?N~})pKA@U2lc(za;d+r`Acq8;iRb|%Q1*e7{g9XVGIM5qZRMtIxJ*b z2eAj&;0J6*H#>A6p1^h-#vQnoQ69p(Sd3LOslSJd%}iHkavIz4EpEb9)JbQOz>WAK z?Of`5hCG62F=Lj<0X&bp(7`(D0DG_oA7TUA3$4w!4jakOMY-6?#cR|T)EL$txQPMs zS6G6%Mb@!didvx~=*3%Dho6zYrGjbsunAY-Dbxx-L4BW%ov52Qi29yrCl|U|uA{Ee zH{6Z+^xBKZQ6Kz;i_piJT8izs3j1+8zQb0moo7|>8tQ4Xv6FRas<9hSqE_+;R_pn1 zU^rK~aT@E<$tct?YR7kQ6zZ&OguZMsQA@BI!k#mq2Xm(M`(M*ROd;5V!bvsXsg)W{ z)ady)xEM#UN6ho9W&76{q*;E=Ij$p3ORq<9C6P<$Y5CXq`;bb4ikppDn&)2=R>|Dx zT4`<5;vsu}w%Vu{4{mPa&Z@ zTK};M2wf{GE6$`n<5I@#w%sA4II<5~yAO8l4DIhL_8XC|P&mx3zGOV}quuLuc`96P zpW#{J_f|UH9=F@kwAQsg)Y}p6*xT)@Yda9~8x?NP$a6lw*P9&7Ig}aCDzcTPj;4;K fVnc&N_l5?IH?jWzdV3Q~jPw@|6yznHh0Tt?og&0K diff --git a/rest_framework/locale/mk/LC_MESSAGES/django.po b/rest_framework/locale/mk/LC_MESSAGES/django.po index 554460c61..5818124ae 100644 --- a/rest_framework/locale/mk/LC_MESSAGES/django.po +++ b/rest_framework/locale/mk/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Macedonian (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/mk/)\n" "MIME-Version: 1.0\n" @@ -18,43 +18,75 @@ msgstr "" "Language: mk\n" "Plural-Forms: nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "Невалиден основен header. Не се внесени податоци за автентикација." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "Невалиден основен header. Автентикационата низа не треба да содржи празни места." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "Невалиден основен header. Податоците за автентикација не се енкодирани со base64." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Невалидно корисничко име/лозинка." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "Корисникот е деактивиран или избришан." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "Невалиден токен header. Не се внесени податоци за најава." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "Невалиден токен во header. Токенот не треба да содржи празни места." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Невалиден токен." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "Сметката на корисникот е деактивирана." @@ -109,7 +141,7 @@ msgstr "Media типот „{media_type}“ не е поддржан." msgid "Request was throttled." msgstr "Request-от е забранет заради ограничувања." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "Ова поле е задолжително." @@ -127,7 +159,7 @@ msgstr "\"{input}\" не е валиден boolean." msgid "This field may not be blank." msgstr "Ова поле не смее да биде празно." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Ова поле не смее да има повеќе од {max_length} знаци." @@ -213,137 +245,136 @@ msgstr "Датата и времето се со погрешен формат. msgid "Expected a datetime but got a date." msgstr "Очекувано беше дата и време, а внесено беше само дата." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "Датата е со погрешен формат. Користете го овој формат: {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "Очекувана беше дата, а внесени беа и дата и време." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "Времето е со погрешен формат. Користете го овој формат: {format}." -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "„{input}“ не е валиден избор." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "Очекувана беше листа, а внесено беше „{input_type}“." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "Ниеден фајл не е качен (upload-иран)." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "Испратените податоци не се фајл. Проверете го encoding-от на формата." -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "Не може да се открие име на фајлот." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "Качениот (upload-иран) фајл е празен." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "Името на фајлот треба да има највеќе {max_length} знаци (а има {length})." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Качете (upload-ирајте) валидна слика. Фајлот што го качивте не е валидна слика или е расипан." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Невалидна страна „{page_number}“: {message}." +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "Невалиден pk „{pk_value}“ - објектот не постои." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Неточен тип. Очекувано беше pk, а внесено {data_type}." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Невалиден хиперлинк - не е внесен URL." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Невалиден хиперлинк - внесен е неправилен URL." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Невалиден хиперлинк - Објектот не постои." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Неточен тип. Очекувано беше URL, a внесено {data_type}." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "Објектот со {slug_name}={value} не постои." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Невалидна вредност." diff --git a/rest_framework/locale/nb/LC_MESSAGES/django.mo b/rest_framework/locale/nb/LC_MESSAGES/django.mo index 56b7a969b10a76a0c3873834588c5530d9f93a4e..a0bdb3a49184449bd82e8037f9830fba15338b75 100644 GIT binary patch delta 2010 zcmYk-TWl0n9LMqh;<~#Q*+Pq3r6L1`VzrCgR(gY$t=rN!DvVmVqyuDL?3)2CJQ9>7UTCfI|?T~^Eq>NX3pjR zpXqS-4_)r9qTokH8K6E%y*&BZU#!5g>$|Hh|qUc~HiY(d>WfTg$# ziN#aR8gV1)_rutQ*RTxd zJZe^iDb$4eQT=cGeu8@5SGZV9ouo01jyMMvWlK;iOrZu!V?Cb0WjKK;oEuZd=qK~8PtlhzGwXY ztEd&{P$3D<$lqUv3psDb7`}=QzK6wl6x|?=&uOr`b{?O@Z;&wA9n^#WLOmeLNeCBU z30C^&t(f4v6BWvLQ4<|SUH7wp{s$^IVvJsk)y3?;VwmV)%kfXt`CL+3D^8(< z12_lY!TERuwc@X_6?52uwTv!ZY{BnPp)Ful+L;b)z#&u)eecrfrV(Yi`r&1)$B%I> zj$tJ(WIq|h`cN@`1GRz=ea~SV=eID4^JnKnx&;-oZ8#5ip?2ahY60$58qGBRLCvs< zrz<=6Bl&1w;8Gk#ZFzug&zV*@w((xKqj(RPV92mnk<6=C3b$Ah<$9t%W zSFl_M+fmnV#RkT=4``%t6e}>o*GDn0##eA7k|uTu^`PrWG;9(zK#a@{;D1HoP*M0u zbT6OUP~qhkFaBVC)b8mV^mS6vtE4i8`Vy7b)!V`*)QaO&6*j_4TITDORlduzt3su& zm#*|^YAgI-_%{Dvl#a@f9=~TjCaD9|AeHU%GN}_;nfJmgF1^$Ti?UJy)z;~2qQbU> zydbn@bD3s#0pmHVHM^&F3tIO_&?rnhW&dbe-RytQMGvvhYR-%7roelxQ(Jveebb~}|r2NwvKgOUF|mLfWdG@z)c35&eT|!u{T&}2qCla zEoefFNx_5*GnEU>OdvWjdSOhwP-Mgx(3luw0vFC@qF!(%k@)>ReMr3Wr04mZbJjl3 zIsfxN&!!XSc4dAk@|`kD5A`1E%{;RVUcQS9@6<(-g&$qe?vvCaH-h_45P+-u@;YGjY9n~jh#4y%@|siyWt3y(tpnDpGK|Z zUDV1hd(L{}{=(eKH=wqr8TI?eu@aA92~MDcA7Q4L#*Z|7IEO^p{=$uT3)wEKBAyCq zJr~_z2l}xWS7OTRk7F(Umrz@J0X5NoP~R)%Vd~eRa%6Wg@qduU00Y{~cToMGP%|%F zp8H}oI`rF66YEDM(`l^2pD~2_E6fgH4DZI1p65I-Vjbhx@#;LY6(z)9dos)Pv{yx> z@_MXC^?Oh&9!CdHVmV&Gb@&5n#ed^Atm3I{c*ye;Y@wgWYP7Z6P?327AHqu+8cHS~ z@z{lJs6V`cQT!IWuzS4>87?&d#FAt;dA4jKvw34M*Fgd z`rpz_hyNk&IaIt=2#eo+qqJtP`NNt03~iydQx#DaojOHRMTx0o*2z=ysswVl$=^l0 zgSwllQ>C&uhr3hDY0sxBEn-x?)p>`y`3y_kMBGPfGxb3B?Le_ludVE@KoGMikNe3D3wk#G(MGBI^$m)35M!|wGB=v8jFOp@shIE+(^_3 zMPlLls#?l|L^3@V>>N!Frw5Z`Bcm~APikx|HR`k\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" +"Last-Translator: Xavier Ordoquy \n" "Language-Team: Norwegian Bokmål (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/nb/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -18,43 +18,75 @@ msgstr "" "Language: nb\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "Ugyldig basic header. Ingen legitimasjon gitt." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "Ugylid basic header. Legitimasjonsstreng bør ikke inneholde mellomrom." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "Ugyldig basic header. Legitimasjonen ikke riktig Base64 kodet." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Ugyldig brukernavn eller passord." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "Bruker inaktiv eller slettet." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "Ugyldig token header. Ingen legitimasjon gitt." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "Ugyldig token header. Token streng skal ikke inneholde mellomrom." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "Ugyldig token header. Tokenstrengen skal ikke inneholde ugyldige tegn." -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Ugyldig token." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "Brukerkonto er deaktivert." @@ -109,7 +141,7 @@ msgstr "Ugyldig media type \"{media_type}\" i request." msgid "Request was throttled." msgstr "Forespørselen ble strupet." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "Dette feltet er påkrevd." @@ -127,7 +159,7 @@ msgstr "\"{input}\" er ikke en gyldig bolsk verdi." msgid "This field may not be blank." msgstr "Dette feltet må ikke være blankt." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Forsikre deg om at dette feltet ikke har mer enn {max_length} tegn." @@ -213,137 +245,136 @@ msgstr "Datetime har feil format. Bruk et av disse formatene i stedet: {format}. msgid "Expected a datetime but got a date." msgstr "Forventet en datetime, men fikk en date." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "Dato har feil format. Bruk et av disse formatene i stedet: {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "Forventet en date, men fikk en datetime." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "Tid har feil format. Bruk et av disse formatene i stedet: {format}." -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "Varighet har feil format. Bruk et av disse formatene i stedet: {format}." -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\" er ikke et gyldig valg." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "Mer enn {count} elementer ..." -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "Forventet en liste over elementer, men fikk type \"{input_type}\"." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "Dette valget kan ikke være tomt." -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "\"{input}\" er ikke en gyldig bane valg." -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "Ingen fil ble sendt." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "De innsendte data var ikke en fil. Kontroller kodingstypen på skjemaet." -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "Kunne ikke finne filnavn." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "Den innsendte filen er tom." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "Sikre dette filnavnet har på det meste {max_length} tegn (det har {length})." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Last opp et gyldig bilde. Filen du lastet opp var enten ikke et bilde eller en ødelagt bilde." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "Denne listen kan ikke være tom." -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "Forventet en dictionary av flere ting, men fikk typen \"{input_type}\"." -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "Verdien må være gyldig JSON." -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "Send inn" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Ugyldig side \"{page_number}\": {message}." +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "Ugyldig markør" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "Ugyldig pk \"{pk_value}\" - objektet eksisterer ikke." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Feil type. Forventet pk verdi, fikk {data_type}." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Ugyldig hyperkobling - No URL match." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Ugyldig hyperkobling - Incorrect URL match." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Ugyldig hyperkobling - Objektet eksisterer ikke." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Feil type. Forventet URL streng, fikk {data_type}." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "Objekt med {slug_name}={value} finnes ikke." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Ugyldig verdi." diff --git a/rest_framework/locale/nl/LC_MESSAGES/django.mo b/rest_framework/locale/nl/LC_MESSAGES/django.mo index 7cb2a85ef4ecd0d9b1aed5e976094840cccca922..8b70ddbd68f7ef3424197c35c3c064fbb5abc4f5 100644 GIT binary patch delta 1740 zcmX}sS!_&E9LMp$rBiLSW!fsGT+~)>-D$Nm25l9srIsL~p|;p2QjJ&=LMEP&@jyzN z1`l3@2M;o7(pMhD5@LBlQW2gcwmuL`AAEmvFDG;6bI#m*&+jrcM)owomh+4 za2meEIP=>t8ZmyDH`1&DJ8%i!#wr}Z0(A2I`{v?I`ukClxrmjRMi1Yjo-5>`MOcq| z{v=;zmI(OpkGwz?<|T6`AVFD2^@nL(ZQQogbz`t){g|i-s4>S zjEYeCDE~PROX;^@QY+q0gR`&;sD->j-S9o==a2T!wi4_3y%}{xr-FVjYGDb!%qnFMYT!4>C97iF zT0jeG=ljsZZd`>AP!kpN*HEr(!CE|pO3HMSMk|f4sEHS{tvWo7MAQ1P9v`7{BB$I> zE*BM{t*8mlpa$;47=A%c&t{M~6&OQ}*BJ*ap(4V0emul+CCGcB78uF7Cqt)I4qMibw1?MwB&IXbjhlW8ygSrZ(S}swR5B&=Z)!mHl=Ta zl`P8mt~a5w^o!86?6gym6RNFqy&BhxIFVSqCN|rPcwX+hweI>o#}bDVod?~eJCE#% XJJnv~|9w&JOK&Ug$VvY!Ul95WP=cp2 delta 1798 zcmaLXZAesE9LMqhn2uRm>6lYZ>DA5Bwy`lCAEpP*%%)YhM~a9xEKH;|Y+I=i86kRE zP*YGukI=gcVo2JBXtyUS2$rH(Eh`c9ss!HzMZtc5cdqoV!<_k?bLY-E=YP)s-sfvm z@x*@xnP-f$o;sI$I>T%q=4SClx#Kr0#Xr!&*I0~Sa2AHL)Bmf*5dDo9#y(t(=dlH! zU>i=IoOTngHA~nz8qNIh2Df5aj#)h(!DV<0+wlX&uyIQI_usLD{&g(CPpANLr<(nM zm7blbaSmVb@qdz{9AWxq^%Efp>ij zBlLs(N&k=I3Os~->^5I>@S*z5Z=*D{;xSYN1=G_mL0#`aMc9wp`lGlIuVN8CLkHhr z05fKoWnvMsDHg&dxBxZIR@DD`F%hIOOe2D4ku$NUsD@$0T+S+q7(_8%z9s2LQ zel}rfVU<{dZK#R%qQ*IeoAD8D!r4T#1%LJYJIMYkD~m|w4Y(7P12?f1$8aasu)1bE z>G=XRaDZ^R#d=T+7)C{Y8MUMTU_1KQHqFzC%9+!sWE@G*SVrSLc3=hJXyO5^!#|N- zwQ*dDljfy!q6Mqy??vs<8C;Bip(g%-nxL43X}~SWsoHUzj~7wnCLVYfULkYXXXIl+ z!qJU&sK|EkrDPn$8oY$eVK1=~$5A^Ga?;6Hi{!I4q81iM{XT%4hMhp>OV~9U8t^{q zj6Nb`**f+~H};|yHtcx;-JS6qM@3pumd>GHP~#m&9qDz{0>7e;W+74Z;5IDL-ru9a z7&eN^P9I_WFq^9Ddel^Oek!_0N3}j#niU8)(sHPpvW5CHl{cCDhWH72%ee6uVtcYH zE11v|Gf{L@%c%-b!M0K-3d?a{RO<6eagSeZ6&<;XQisfOlf4;<(R~HPP^YI7rfP%M zsbjX=7VxbdRMF$EqPIP{>K1a_E!wr|)KyeHRmv9?<=sSy)0pCQGH^9@MRL2pB+^8y zfT|>@r7BsJi8_OsRAu8DY9_UlntbM;le+IO&o1uj>2}Hn`(fAqfxg|{zYUeeoWZ{C zg9o`Zl=?TX(4Q9#S1%1m>YeI_ShObjH4yrCr9KvorpgQZa-6Q7-u+$Q8|v7+xc`4Z CPq!Wb diff --git a/rest_framework/locale/nl/LC_MESSAGES/django.po b/rest_framework/locale/nl/LC_MESSAGES/django.po index f53b6097c..b89af0606 100644 --- a/rest_framework/locale/nl/LC_MESSAGES/django.po +++ b/rest_framework/locale/nl/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Dutch (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/nl/)\n" "MIME-Version: 1.0\n" @@ -18,43 +18,75 @@ msgstr "" "Language: nl\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "Ongeldige basic header. Geen login gegevens opgegeven." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "Ongeldige basic header. login gegevens kunnen geen spaties bevatten." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "Ongeldige basic header. login gegevens zijn niet correct base64 versleuteld." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Ongeldige gebruikersnaam/wachtwoord." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "Gebruiker inactief of verwijderd." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "Ongeldige token header. Geen login gegevens opgegeven" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "Ongeldige token header. Token kan geen spaties bevatten." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Ongeldige token." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "Gebruikers account is inactief." @@ -109,7 +141,7 @@ msgstr "Ongeldig media type \"{media_type}\" in aanvraag." msgid "Request was throttled." msgstr "Aanvraag was verstikt." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "Dit veld is verplicht." @@ -127,7 +159,7 @@ msgstr "\"{input}\" is een ongeldige boolean waarde." msgid "This field may not be blank." msgstr "Dit veld mag niet leeg zijn." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Zorg ervoor dat dit veld niet meer dan {max_length} karakters bevat." @@ -213,137 +245,136 @@ msgstr "Datetime heeft een ongeldig formaat, gebruik 1 van de volgende formaten: msgid "Expected a datetime but got a date." msgstr "Verwacht een datetime, in plaats kreeg een date." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "Date heeft het verkeerde formaat, gebruik 1 van de onderstaande formaten: {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "Verwacht een date, in plaats kreeg een datetime" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "Time heeft het verkeerde formaat, gebruik 1 van onderstaande formaten: {format}." -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "Tijdsduur heeft een verkeerd formaat, gebruik 1 van onderstaande formaten: {format}." -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\" is een ongeldige keuze." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "Verwacht een lijst met items, kreeg een type \"{input_type}\" in plaats." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "Er is geen bestand opgestuurd" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "De verstuurde data was geen bestand. Controleer de encoding type op het formulier." -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "Bestandsnaam kan niet vastgesteld worden." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "Het verstuurde bestand is leeg." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "Zorg ervoor dat deze bestandsnaam minstens {max_length} karakters heeft (momenteel {length})." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Upload een geldige afbeelding, de geüploade afbeelding is geen afbeelding of mogelijk corrupt geraakt," -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "Verwacht een dictionary van items, in plaats kreeg een type \"{input_type}\"." -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Ongeldige pagina \"{page_number}\": {message}." +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "Ongeldige cursor." -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "Ongeldige pk \"{pk_value}\" - object bestaat niet." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Ongeldig type. Verwacht een pk waarde, ontving {data_type}." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Ongeldige hyperlink - Geen overeenkomende URL." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Ongeldige hyperlink - Ongeldige URL" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Ongeldige hyperlink - Object bestaat niet." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Ongeldig type. Verwacht een URL, ontving {data_type}." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "Object met {slug_name}={value} bestaat niet." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Ongeldige waarde." diff --git a/rest_framework/locale/nn/LC_MESSAGES/django.mo b/rest_framework/locale/nn/LC_MESSAGES/django.mo index fd5f5ec26a9568288476ae9a57dea07605386ff3..43d080aa67cdb810998a30d1065253dcdb605a7d 100644 GIT binary patch delta 41 ncmeBS>0y~Lf!9pez*yJ7P{Gi`%GhG!Txo=WIZ$BZQFlfF*}@8C delta 41 ocmeBS>0y~Lf!9>m&`8(7T*1)7%G7w`Txo=Wxs|Eu#-r|x0NOYTYXATM diff --git a/rest_framework/locale/nn/LC_MESSAGES/django.po b/rest_framework/locale/nn/LC_MESSAGES/django.po index a23d8d25b..26a625b6d 100644 --- a/rest_framework/locale/nn/LC_MESSAGES/django.po +++ b/rest_framework/locale/nn/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Norwegian Nynorsk (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/nn/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: nn\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -212,137 +244,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" diff --git a/rest_framework/locale/no/LC_MESSAGES/django.mo b/rest_framework/locale/no/LC_MESSAGES/django.mo index 00f5db6b594908affee647eec4ca29a8464fd10b..730768a272bbc0c161dc6435e89bf8d700c44b64 100644 GIT binary patch delta 41 ncmZo+Xm&`8(7T*1)7%G7w`Txo=Wxs|Eu#-sL(0N1DrQvd(} diff --git a/rest_framework/locale/no/LC_MESSAGES/django.po b/rest_framework/locale/no/LC_MESSAGES/django.po index 4342b2bdb..4a3d609ca 100644 --- a/rest_framework/locale/no/LC_MESSAGES/django.po +++ b/rest_framework/locale/no/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Norwegian (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/no/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: no\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -212,137 +244,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" diff --git a/rest_framework/locale/pl/LC_MESSAGES/django.mo b/rest_framework/locale/pl/LC_MESSAGES/django.mo index 30e6022558e4409c426bb205e867ca23c1fc9ef3..232de88ffe95f84a46cafd9c534cbc980579a248 100644 GIT binary patch delta 2010 zcmYk-Z){Ul7{~FaSXwq@TQ}*HIk@`+0Au9S_9&W$k`8xF@DLxBcIQWYC9gDoQW zjV5YHAU4KC6DNT;Bxr=ifWZVc#5YO|!~_yx3BFe2(D?s_-{0*edcx`FoYVE(d!FaH zmk+jnKIqO=2H!T?DE&eD4|B|1{A?~CwDV!JI{XwJyn##cA6$&}^UUtYPSo#5u?7!f z3=7zVU*Kc-6ZT-D#M8A21{+woh#3r*nhj$wuEr^B!)w@wRrAeSaTn_QcX2ab#Rgn* zuURE_p>Ajxwf>UtJE;9GV2h6WJA(x*#Q5M))`mJ^7wSSeOyfyhhqtf`Ys<`ojeECZrz=kO8y5=oQYL2djuYJ)N+AzX^p z*yPWJNWG2g~m(-dG$} zM7!}}Jb?-P7Pn(yq1gkt!*|^GD6V4t6b2~po0w!iQ%(Gp>q<&kIZIGitDmLO2 zYU4|&kj|hs46)dOYj6Z7a0_0+ZCJ-9s+ohh70;oHuawiNhI(8Edl|fm-8k!;B7TRN zzl^%zUs#Lvd?>UTR0N(zMc@s56fdA6^fOYYmZTt*vQAVn_F^YKi%+0?n!!#6w#4i? z*oC^0uaPKOg;PA?GHhW!hHdyBuE1;lJizW5=Iyu{52Lpal6I>gtlm?As-*#3rdQ1v z1BZnp$TjUlR4Ohb(X!wC^$3puLrb6{GK9LpL;m_(sEt3tAbx{9P<91X)VGmWghd*; z8LY*{dj6XkC`A28T5Sl!xDQn%qt=SB*wVDeZ`;$f5moe<9KPDM5X2|a`P4;EbM75s2kgXdhCv30Pi+^r>4^@ zLfUIReFI&;SK+D+(fbt&eOn3MQmMRFjV6< zG2@QCa@^>Dqrl&7AqG0`fWNREGxSkpwuBr{agIeio;UZp<4>MK@)%1c?d%TEO{JeJT z9=g)HhTcn$(Dj7U%4{y5+vpwik;0j9RZs~l%%8Vt`up%};pt=LH$w|jv3N2TPdJHm zYcgG!jMn_OvRW(0qt%Tg*#o(;{mz#C&*WbmpLEtvbhW`a9TBdBW;oJ)YL}!W?>=@hHZXrlU zO_1OfQ7a~ds3;erQMlMJM!e7%6O1#yB*tivL~b?%Mq~8C;M@22v?gA8((`=IbJm{c zIsfxNXYco&>`z@P$vtM29n_ntzvh^waBeXd$_1}k6@H8kUcuXNX`a~%Y``0_5B2*I zti&Ve$5U8~U*HD(3pZkTiEGN9rqM~qXBfie`DR174O{Rmw&6T>V@-is5XVt}e*=5) zGS=XlrDi2KfO?@_sQyD)Uqg-e2{vo0-a<1z*3N~=SvP8i1E>e>$5x!h_4p%ppnsWJ z9mbK5o#dkLoy9wF0TsF8BD2-li0U84dOU)43iUfQ?!$R($H4WO0mrd|`-9p0krv}PjP1Et5HkSj{5yWcsuUHa(o#bd>d1xG`^;hi&u~++t0WLuOZ82KH{m6 zHgVAao#??~T#nJ~{WR8d{}gIT&!JxQPt^A+c$n@RP&v|DO8oDlv4;+=<*TUsZ&7bv zd_(4owdio)g?h0VDw&R>4=-W>^UBP&V-Rn`=d+&4dI1~gzl^^xGAk=5{*81Lk-}Q@ z4%8aOuo|C3y~sISiJzke{tXqXbjN!4u>&tE6ah;pkCk)Bw7}BGBezY&D_6$n!z02iFu4#hYhIy2!`-Q?7>T@ z@qC1pbg&6zSETF^4JFkXT!kN@gIAGfTOld0CGjIsvmsP}9Qlepi;B!F>P0VR`+q@= zTedQj+_lICvj8fG!dRgFAE&_%vS}m;b`%w=w~%GFS@hzks9gC9J8%gJ!)n=P)PrJ3 z)a^;+EA|p@#t(5T7Bf4oeGIi^?_nj+w*?wR$8wp5A6MZ<9K?1!fvxxrYKck+2a~fn zGPWH4RSTvp zB2sSbb(%at{clmI_#fhCnGO>b*3;vjDEFqn_cW%oH8)VZskKxUEs^p{g%jJ2prWm0 zR`F)Ab*N?3X6dEspj7G0;BEqrA-j&Mv\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" +"Last-Translator: Xavier Ordoquy \n" "Language-Team: Polish (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/pl/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -20,43 +20,75 @@ msgstr "" "Language: pl\n" "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "Niepoprawny podstawowy nagłówek. Brak danych uwierzytelniających." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "Niepoprawny podstawowy nagłówek. Ciąg znaków danych uwierzytelniających nie powinien zawierać spacji." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "Niepoprawny podstawowy nagłówek. Niewłaściwe kodowanie base64 danych uwierzytelniających." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Niepoprawna nazwa użytkownika lub hasło." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "Użytkownik nieaktywny lub usunięty." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "Niepoprawny nagłówek tokena. Brak danych uwierzytelniających." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "Niepoprawny nagłówek tokena. Token nie może zawierać odstępów." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "Błędny nagłówek z tokenem. Token nie może zawierać błędnych znaków." -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Niepoprawny token." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "Konto użytkownika jest nieaktywne." @@ -111,7 +143,7 @@ msgstr "Brak wsparcia dla żądanego typu danych \"{media_type}\"." msgid "Request was throttled." msgstr "Żądanie zostało zdławione." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "To pole jest wymagane." @@ -129,7 +161,7 @@ msgstr "\"{input}\" nie jest poprawną wartością logiczną." msgid "This field may not be blank." msgstr "To pole nie może być puste." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Upewnij się, że to pole ma nie więcej niż {max_length} znaków." @@ -215,137 +247,136 @@ msgstr "Wartość daty z czasem ma zły format. Użyj jednego z dostępnych form msgid "Expected a datetime but got a date." msgstr "Oczekiwano datę z czasem, otrzymano tylko datę." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "Data ma zły format. Użyj jednego z tych formatów: {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "Oczekiwano daty a otrzymano datę z czasem." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "Błędny format czasu. Użyj jednego z dostępnych formatów: {format}" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "Czas trwania ma zły format. Użyj w zamian jednego z tych formatów: {format}." -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\" nie jest poprawnym wyborem." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "Więcej niż {count} elementów..." -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "Oczekiwano listy elementów, a otrzymano dane typu \"{input_type}\"." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "Zaznaczenie nie może być puste." -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "\"{input}\" nie jest poprawną ścieżką." -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "Nie przesłano pliku." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "Przesłane dane nie były plikiem. Sprawdź typ kodowania formatki." -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "Nie można określić nazwy pliku." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "Przesłany plik jest pusty." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "Upewnij się, że nazwa pliku ma długość co najwyżej {max_length} znaków (aktualnie ma {length})." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Prześlij poprawny plik graficzny. Przesłany plik albo nie jest grafiką lub jest uszkodzony." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "Lista nie może być pusta." -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "Oczekiwano słownika, ale otrzymano \"{input_type}\"." -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "Wartość musi być poprawnym ciągiem znaków JSON" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "Wyślij" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Niepoprawna strona \"{page_number}\": {message}." +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "Niepoprawny wskaźnik" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "Błędny klucz główny \"{pk_value}\" - obiekt nie istnieje." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Błędny typ danych. Oczekiwano wartość klucza głównego, otrzymano {data_type}." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Błędny hyperlink - nie znaleziono pasującego adresu URL." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Błędny hyperlink - błędne dopasowanie adresu URL." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Błędny hyperlink - obiekt nie istnieje." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Błędny typ danych. Oczekiwano adresu URL, otrzymano {data_type}" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "Obiekt z polem {slug_name}={value} nie istnieje" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Niepoprawna wartość." diff --git a/rest_framework/locale/pt/LC_MESSAGES/django.mo b/rest_framework/locale/pt/LC_MESSAGES/django.mo index 476e41490ac1dbe53c9500f7e5ede967a600905a..9553b8f7ae0e46b54d4aed4f473515eb364357ea 100644 GIT binary patch delta 41 ncmZo=X=Rx(f!9pez*yJ7P{Gi`%GhG!Txo=WIZ$BZQ3pl<*MtgC delta 41 ocmZo=X=Rx(f!9>m&`8(7T*1)7%G7w`Txo=Wxs|Eu#-k350N45oRsaA1 diff --git a/rest_framework/locale/pt/LC_MESSAGES/django.po b/rest_framework/locale/pt/LC_MESSAGES/django.po index 31cf0c2dd..c1217b2d6 100644 --- a/rest_framework/locale/pt/LC_MESSAGES/django.po +++ b/rest_framework/locale/pt/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Portuguese (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/pt/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: pt\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -212,137 +244,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" diff --git a/rest_framework/locale/pt_BR/LC_MESSAGES/django.mo b/rest_framework/locale/pt_BR/LC_MESSAGES/django.mo index 0f2dac6156a0b7c2706b41f35d99b96bc6e84094..20a41eb1f7bbc11f247fe8dd8755893c878296d4 100644 GIT binary patch delta 2006 zcmYM#eP~s69LMqRUDtCr*S6g3E?sM9PR(l5J6GG<<6V2uIp>_((hbVlgIo%48k&M} z{3HM9pArrch-jh+f=D^=A*Cq#qXi-eGSev}Nc|Ck^@l}EdVkLSRX_GUuix+7^YDB4 z{?2|{`(P-zT^j$`D5J#V#4mYf0bZKLfpR`!R*e_X$D6naXYdJJkZ<-FcA@Sc#R_~E z>oCMx{2E`tU$GDC3nByCO=SfqzQQynW}A)RN^HU-*osp)fMtbdE%*xR_k*|^uVM{W zJ#1Er-KY+Ypw7SJ`~>y9i};*|dY?)WC+avbC~HNHup9NF3^wC&?8IBxjg`e_OK}|e zvjZG--4R@Z*HJSUD=~W-YcPdta481Zuc9)^|Al%$F%1tF zVL8^ib`Pd#51^KEFRG*8qOSYVwf{urMv}KT;_@=)Uu&3gCyt^} zs3h8q&)_kv$1AuAV~?86!!hSh=SR4l^C$6#nAtSy{^+KxK@|r`Uxv*oz&! zeI34!N}egy6696pIy?_G@=oV=)cwaWjdziBvc)8XmaGwvjP(BUh0@+TX5S!4_fitPk1IwgvU#ENZRyVF8{( zJ@+im)&8HPqA9wKKHhb0k6tNhif|0mNMG#`>P257m)Z?f#~)w=R?=HJjJkh6YUJlp z_g_Q)>^_HOyx%IxuwEQQC1HrsRN_|J9-Eliw{55qe~5bEDSQ?uF^2yZyPohU3 zB|U!yp%<%DDjSGFO_8QkMeD1QN316_O`6Jff`pE~=Me3XL1e&D8FqGt!(K&DON$;w zUF&zh6gq`R^bu>+pll-2g!V_A(46Y~sQtkVMA^sojItE|D{a)YpxuO~O@(#v_^PuZ z!+TysphWB`GDtWaNTB0+#e*vm$1xAQ;)^?z|eS@XsV+p_7_YHPM^HoYO{Y}MMjkX-N2@BG@j>Y3l)>-RjI^ZPx| z_xV1@dtGxok~ebF-!|Gl;z8oi#b!yon!$y3*>6^iU!#M!@nKxH#B2=)@qX+^eg6O! z;Ry`jIV{I-u?g>CD>i1jChZMw+8Fo-BUq4SwhMP-4bEU4&S3|ZE;S2d0`>O~u@i4$ zDQ;M1mV-U03H77KU-Ntq^}H)st)=>xoAJ*YxUe|uK&`L`^`bGX#ZPey-oa)JtT3y< zgUCNS!$qH)!AJ2IROYf*nytqWYJ5Ld;t8x!s=wf78_r<^R;@}sFoA{iU-kMYQ7d^L zwX&<8^WL~GJGJulsI6&0eg7F;hp%8ho<;}HV=|YUYuu#cZ6wR~8*ae6$aYy7`BX|b za?u0Y(1-i60QVxIz&~HagERHIslURm7VHGaPGuwkHQ{vLqKVYwR!m|Ceu>)BRjfk&y{M{x7x&^Fuiu^|5Bk8H zxDT&mBi8WN797K!co9`ZIjd7!(14n7FKXrEo*$#We*+`vPzaQjMNwOJ5Ff*%SdYoG z+-M~~V;2^zO&P;5{m+s5vOiEOO>X?P_Y5EC#0>45% zFQ>%KY|=`((Le+K^60U+sqAP_B<+e$FT*^ql$3>>oCM2j$s1X4ZDU~@n5J1=JD&Y z5reo0|7{vmQyozF_Bhc>v=h(j#hRtsQ-o5abgF5#YKw_pLRnHOHxm@Nn?`U?%623n zZrkZuH`VAXN~$WDAwRaM3eX>y>f$vYwht?d7=I4&uM9 zflel|l~6^iu@62shb>b#eWB!ThMEYSxN<`6NrKbpHh!_)AFx`iNDaCFCFoYCMTOi$ zbP-vET6YS!c2z!UgvvpOXB)w9sJlC`!ZNt-BDN5Frl$S5={meqcm0L4-}ztnuZhO` zoRW!`;kn_%Lof7=OqPV5iJ`vHQHCaGJ6F#6mRAR=f`Q5!r>Ztw9h!>d7qzBFYMrW0 z;b3K1B`vEnI(jJ3I}#lp9gH4IjD(%$k!U>TGz~^a$6p*D8*u7}_(b2xf#|^D!ANW< k8XufJoxhM-c4;~mAC5Zl;Ukx(2jeeA|8F|)IQ{;A0ErvxQUCw| diff --git a/rest_framework/locale/pt_BR/LC_MESSAGES/django.po b/rest_framework/locale/pt_BR/LC_MESSAGES/django.po index ce372f321..8b55e9ed9 100644 --- a/rest_framework/locale/pt_BR/LC_MESSAGES/django.po +++ b/rest_framework/locale/pt_BR/LC_MESSAGES/django.po @@ -11,9 +11,9 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-08 16:20+0000\n" -"Last-Translator: Craig Blaszczyk \n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" +"Last-Translator: Xavier Ordoquy \n" "Language-Team: Portuguese (Brazil) (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/pt_BR/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -21,43 +21,75 @@ msgstr "" "Language: pt_BR\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "Cabeçalho básico inválido. Credenciais não fornecidas." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "Cabeçalho básico inválido. String de credenciais não deve incluir espaços." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "Cabeçalho básico inválido. Credenciais codificadas em base64 incorretamente." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Usuário ou senha inválido." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "Usuário inativo ou removido." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "Cabeçalho de token inválido. Credenciais não fornecidas." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "Cabeçalho de token inválido. String de token não deve incluir espaços." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "Cabeçalho de token inválido. String de token não deve possuir caracteres inválidos." -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Token inválido." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "Conta de usuário desabilitada." @@ -112,7 +144,7 @@ msgstr "Tipo de mídia \"{media_type}\" no pedido não é suportado." msgid "Request was throttled." msgstr "Pedido foi limitado." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "Este campo é obrigatório." @@ -130,7 +162,7 @@ msgstr "\"{input}\" não é um valor boleano válido." msgid "This field may not be blank." msgstr "Este campo não pode ser em branco." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Certifique-se de que este campo não tenha mais de {max_length} caracteres." @@ -216,137 +248,136 @@ msgstr "Formato inválido para data e hora. Use um dos formatos a seguir: {forma msgid "Expected a datetime but got a date." msgstr "Necessário uma data e hora mas recebeu uma data." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "Formato inválido para data. Use um dos formatos a seguir: {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "Necessário uma data mas recebeu uma data e hora." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "Formato inválido para Tempo. Use um dos formatos a seguir: {format}." -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "Formato inválido para Duração. Use um dos formatos a seguir: {format}." -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\" não é um escolha válido." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "Mais de {count} itens..." -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "Necessário uma lista de itens, mas recebeu tipo \"{input_type}\"." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "Esta seleção não pode estar vazia." -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "\"{input}\" não é uma escolha válida para um caminho." -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "Nenhum arquivo foi submetido." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "O dado submetido não é um arquivo. Certifique-se do tipo de codificação no formulário." -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "Nome do arquivo não pode ser determinado." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "O arquivo submetido está vázio." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "Certifique-se de que o nome do arquivo tem menos de {max_length} caracteres (tem {length})." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Fazer upload de uma imagem válida. O arquivo enviado não é um arquivo de imagem ou está corrompido." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "Esta lista não pode estar vazia." -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "Esperado um dicionário de itens mas recebeu tipo \"{input_type}\"." -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "Valor devo ser JSON válido." -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "Enviar" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Página inválida \"{page_number}\": {message}." +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "Cursor inválido" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "Pk inválido \"{pk_value}\" - objeto não existe." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Tipo incorreto. Esperado valor pk, recebeu {data_type}." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Hyperlink inválido - Sem combinação para a URL." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Hyperlink inválido - Combinação URL incorreta." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Hyperlink inválido - Objeto não existe." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Tipo incorreto. Necessário string URL, recebeu {data_type}." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "Objeto com {slug_name}={value} não existe." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Valor inválido." diff --git a/rest_framework/locale/pt_PT/LC_MESSAGES/django.mo b/rest_framework/locale/pt_PT/LC_MESSAGES/django.mo index e548456051a5ec9e9b922612cb0df728e89ccd07..82403c3cad8a9408909e20cbf276b8692e5b2af6 100644 GIT binary patch delta 41 ncmbQnGL2=z1YR>;17lqSLj^+%D`Sg^bEOdi=0JgsM*|rF-1Z8C delta 41 pcmbQnGL2=z1YT2JLnB=Sa|J^SD^uf%bEOdi=2oVr8;=Gu0s!4?3W)#! diff --git a/rest_framework/locale/pt_PT/LC_MESSAGES/django.po b/rest_framework/locale/pt_PT/LC_MESSAGES/django.po index 32cdd5c47..1727fc42b 100644 --- a/rest_framework/locale/pt_PT/LC_MESSAGES/django.po +++ b/rest_framework/locale/pt_PT/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Portuguese (Portugal) (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/pt_PT/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: pt_PT\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -212,137 +244,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" diff --git a/rest_framework/locale/ro/LC_MESSAGES/django.mo b/rest_framework/locale/ro/LC_MESSAGES/django.mo index 11567edc3997d7f53610509d959deeedacc7a2ae..77dc7d0f23fce6a90c9cf8c60fa31fd568372e46 100644 GIT binary patch delta 41 ncmZ3(vW8{C1YR>;17lqSLj^+%D`Sg^bEOdi=0JgsN7ESr\n" "Language-Team: Romanian (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/ro/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: ro\n" "Plural-Forms: nplurals=3; plural=(n==1?0:(((n%100>19)||((n%100==0)&&(n!=0)))?2:1));\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -212,137 +244,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" diff --git a/rest_framework/locale/ru/LC_MESSAGES/django.mo b/rest_framework/locale/ru/LC_MESSAGES/django.mo index 063bf0b74ce2591559b1b8838125b25d00533c66..ac67f09a3be91baa792881c22e91fe1d9dac66f8 100644 GIT binary patch delta 1558 zcmYM!Ur19?9Ki9@a$8#S-~3bRYMN%5n}4QrO>;K2i6*I(e^N0>3DY#NKd8alg9SYW z(-Mk$h=TAza#CRRkl{lTKJ^x6FBMr&QUrm~_jfnwz}e@Vdw1_W=lA>FyVmik-TyH$ zYD&@S=^6Avol<@HGKwEslR>FWY(^`N;BK76ofty7?j5G%H?*PNs8l{W(2J+>C_Y3V zu49!_epS3psa6h*U>PoBIqIU7YQRc#<4u(FOLz=7F%j!y!VBoez0AiV-bcC5B9`J` z%)yfF;rpFJ7tdEy3_P4z!D@`%p_Cn4Fct&o!dti>-=aih1M@J+q*Nh#P_Daxhw%Z* z^{beMKO%D@2W6otn8EXvTL#z`IdKtXXVWO5p21vP#uVH@D@MmD#impq7GNpL`94g= zt0)VbL<#v4%5zNd;q!Uumjhk~lDi&s;y6mE=OXiUlpSOygdbFalG8yvh_hIPAFu;W ztbP}sjTpdU=6A7$`dYy@=9$UFzmCCZayTc8SipP}B_%etBMUi?wfG1-@jG5ZAKM(m zS2&6;D)I!*;RqU8#TgvO9$dp=^st*&oJ}MCGBB|H+t`hg ziH(+UaXrBn=4%+hV)8bD^QfhGk5c{27jPeDTa`M84Soha44z>lrchbOu@?t$9xtOc zi(TLy%tkX6B>#7z6ybH02t7q7{zebl`EvsfA(yHJ%*R!f$ogY)!YlG(1qb>u2_Iu6 zzCg+CAC%POv-~UAg&p`F51^e2l_G8DM;34m?f4wq@e6jLhw#e$9v+h5{9@3?fdcO0 z#~av-Mz+<8gZLWXU>#0SaaFj2O&G^^wxFBNrnQd`H`ZD$zp@Egh(kM~6s4u5*oYvIz%3+!3@ic)^6I%qMZT8=Q1MtHR* y)oP{J(Cb6Z2BR?)Fs$a=9I@;D9;(|B*6|go5Uw*pM%4I_4i)T%N)J delta 1716 zcmZ|PTTEO<9LMn~u&h!SrSyWOtfyRTD_z(Hm&+mr7K$k1R@%@)X(-@ouz^+=5F})& ztt9&3rNpLbqVgb3jiDiKjmE?#jj!75HX1K&&@?=d5YmSxCbo(C{T;TxImw*QnK`$a z|9|HEap?EX=;DU74-KW2x{W%VVoVsnPvby2nr_T?>_rPtp&LKN9Q+RT+)d2IztDr5 zoW|_NMhxN!Y{nQqjd!u$n5YRpU`z)cr?3)#!y4T1;M$;`#{li~sOvxBbLh(89_&I5 zU=R!Nourpg@41eC%v^6w5jLUT_X<|=eKW^N9UY6#1F6vXK^R~ii${z z%a|vy5Y-<-FOH#}{|syK7u0(;bJ&VSsCGSv0={oToM;A-r0?qj?RnG;6R40c;SNlF zn3rKOS{OtoY5K7ghfvo)#x3|IYG6O0LjO1Fd(Dp!f8Ef}i8>}wIXs6Ico`M)m1Mgh zYi$NySW5p0Dz|f3jLYc5jf}bzoAFVcP5KR%(f$$JS!d2{;(w5iu1&_Y;1{Ue+(Uh! zf@vrzjOKY~;E2L6fT_$n*&0IpyJ>lsBqUdA5GArHrKY%B5a;AEK& zS;zFx;#pKqJ%r^2?7$klm~;uXORT)LflQ;H_APA39P)J%kKtRmib{3lF=Gbs4nB@Y zq82NWlhfFPx3LXFtgsM%gqrax9>el{V@B~4oWRX2ltMg%dhRRKqIA%y=e$^fqgaRY z*ov#jD@?SyXlPYGhaODO|>lSVZR@44`s6j!Mzz_&VOePTbE5-G%e0#riV` z4d5Rv!3x592*cQ|pj=6I>^}*yxBs0UB(J8N>Y%D9DJt4eDO9$KX`rT2Rrm+k z@zi-XRXJO)MzXNb?w6U{4p)};8xgTLK8w~g&A8IHdA%9?LZ0^U z=s;xP_>iZmcQ_od%Dle+?yC&={jsj>q0Ex_LgGCuaWe5{Vk*86|2F=0VmiL?zaN}g MaJysIwx7xP7fIXBh5!Hn diff --git a/rest_framework/locale/ru/LC_MESSAGES/django.po b/rest_framework/locale/ru/LC_MESSAGES/django.po index eae0fe286..980a9e5b4 100644 --- a/rest_framework/locale/ru/LC_MESSAGES/django.po +++ b/rest_framework/locale/ru/LC_MESSAGES/django.po @@ -10,8 +10,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Russian (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/ru/)\n" "MIME-Version: 1.0\n" @@ -20,43 +20,75 @@ msgstr "" "Language: ru\n" "Plural-Forms: nplurals=4; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : n%10==0 || (n%10>=5 && n%10<=9) || (n%100>=11 && n%100<=14)? 2 : 3);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "Недопустимый заголовок. Не предоставлены учетные данные." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "Недопустимый заголовок. Учетные данные не должны содержать пробелов." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "Недопустимый заголовок. Учетные данные некорректно закодированны в base64." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Недопустимые имя пользователя или пароль." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "Пользователь неактивен или удален." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "Недопустимый заголовок токена. Не предоставлены учетные данные." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "Недопустимый заголовок токена. Токен не должен содержать пробелов." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Недопустимый токен." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "Учетная запись пользователя отключена." @@ -111,7 +143,7 @@ msgstr "Неподдерживаемый тип данных \"{media_type}\" в msgid "Request was throttled." msgstr "Запрос был проигнорирован." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "Это поле обязательно." @@ -129,7 +161,7 @@ msgstr "\"{input}\" не является корректным булевым з msgid "This field may not be blank." msgstr "Это поле не может быть пустым." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Убедитесь что в этом поле не больше {max_length} символов." @@ -215,137 +247,136 @@ msgstr "Неправильный формат datetime. Используйте msgid "Expected a datetime but got a date." msgstr "Ожидался datetime, но был получен date." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "Неправильный формат date. Используйте один из этих форматов: {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "Ожидался date, но был получен datetime." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "Неправильный формат времени. Используйте один из этих форматов: {format}." -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\" не является корректным значением." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "Ожидался list со значениями, но был получен \"{input_type}\"." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "Не был загружен файл." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "Загруженный файл не является корректным файлом. " -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "Невозможно определить имя файла." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "Загруженный файл пуст." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "Убедитесь что имя файла меньше {max_length} символов (сейчас {length})." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Загрузите корректное изображение. Загруженный файл не является изображением, либо является испорченным." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "Ожидался словарь со значениями, но был получен \"{input_type}\"." -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Недопустимая страница \"{page_number}\": {message}." +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "Не корректный курсор" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "Недопустимый первичный ключ \"{pk_value}\" - объект не существует." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Некорректный тип. Ожилалось значение первичного ключа, получен {data_type}." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Недопустимая ссылка - нет совпадения по URL." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Недопустимая ссылка - некорректное совпадение по URL," -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Недопустимая ссылка - объект не существует." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Некорректный тип. Ожидался URL, получен {data_type}." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "Объект с {slug_name}={value} не существует." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Недопустимое значение." diff --git a/rest_framework/locale/sk/LC_MESSAGES/django.mo b/rest_framework/locale/sk/LC_MESSAGES/django.mo index 89c3f462f467c716641e1049cc4135cb2654d59a..83237889833cddac18370ba584b677f8c4fbdcf3 100644 GIT binary patch delta 1558 zcmYMzOKeP09LMp0tJ6^o)eeddQ*V9Hb~-ht)s8;UB2iRT&?eGUD3R1Fh{H^z%ulB%}OzZ4cLwA@D{Ge5saC| zt#YE-4hH&h6~4k6%*-*{h;oFpT547^^0y z-?s}ReBUn9sAu90HsaJNW-D+jPQ?U9@B%KwAyh=huo(S5vvRCQJ$D4x;0@IC!#D>= z-M*JWEp#Rp@_mb{fgSF|QPj?^qC$Nam*Oj&iDMYVoM~ok%8GF%hEel9I1A697B+wi z`3ux{e7WiQVvK8`friem3#;)oD%20${s?LZMbpzCszse?AC}-ftiX@hi9S|88+%<7 zSV{jHe$61#sEytC6aOk2Z~f^3&kVCg^cSK2(1J>`{qA@Il?!)pCw{?hjI!-^yoNFS ziCtJp7?cB-P)G6x58xDDT8AeJh<`1OM-1%4U#LiQu>1wsk1cowWB3`jVG-eI!($l4 zm#Bz*M}@kCmvm!4HsgEL2j>uOHgBD%=lc}@G8#{DC61z!u`pn^8+T(9K1N-iaU{yt zOn8FWf%$k6l_TeIK0b7Pk4nF)a@!VW%pd)Vk7;Z7{M@M4dY>CA#uA+LpvYB9ry=hxMg8F5*P6} z{dcI9Z{SDCbO`yY-NX#kBKeCGP3=@gq1P@~P(xG|QpUN)UaWEsOQRHy&I!HR4ODW{ zCa6PEPAsSDY_*FeR8qlN_y(*^ZuW$dO`hC#u8MPav{aJGi5XjWmh3?lEn7tglSx%Z z#Hd+R74EkaBeiw7)yZ$3rPC{D>9#2i$P#Nz2C`=b*3weZ;j7e8gVZK!YpN~V>rEwm m!yaFCS$U|eydqc;iH0J{gZXp+kE~)OmCTq;F{1P zVWHBOLcvyq(1)TB0|gTbnm~LjhESSFX-S|h#X#vpnmqUzT1e^lx4X$>hMn`7nLTID z%ztLide`ca_~!16c_Vd`UnWnbn?>?fiK}d=)ruK zSrxWo7$>j;SFjWR#gJLt!Y`Peq+tf@@OP}o-Py@O9mOE^uTlTs!?)3$!#x;54PX?@ z@bi>cQT_aae%!I$tQ^}={S9M)_uCSMW*RoJ6^maqtHyrZfnQ<(7w|Rw4Hc1ew^;=q zM78&z7e7Wl|1CD+uc&_Za@mjNsCtM?8Sl3q3YtMI<*fdoeg!qdpHU(I3tz=;JLwE7 z(ZMh>NgKe!IEMQF8{CK2Py^dQh5kR(`#N?Jf88)ZK@HQWHN1##;3_KQk5cur+~f=* zc$oHcsI^_fN_>Dm+{38f#||vSg_LVpL;WuPnP%q7BmSD%!#!rNV|IRWrcHQ|`k7Sy zQ`BZ#Otr6}*6==_#{EpU7h`w=?_vlmnB7?%LG6Xxs3mdH*)X;j5dS6$-_g*3TR4DK z3|olr7jwy&nTQ z|CcHFX}FIXNPbx|0zo`UeHcUdJvL)HH=V~Y>i!#Cw5c|cE-aTro`&^g9!VTU4OmGL zQy+@3_M6!$UI>TCF0uxq^a`0JO}tq3Rx*3kwy8p5ITK-g3l&C%nyryIX4)r>bB&p` zaUZ24WW`m9W0d%kXxMDD#DWtht0iZWl{5tWJ uXwa$g`kvia7xepAB6-_$JW=Q77wcDIAB{V)smb-lv5`-n_qIsUkn4Z**1LEB diff --git a/rest_framework/locale/sk/LC_MESSAGES/django.po b/rest_framework/locale/sk/LC_MESSAGES/django.po index cdd82c107..208c063ac 100644 --- a/rest_framework/locale/sk/LC_MESSAGES/django.po +++ b/rest_framework/locale/sk/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Slovak (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/sk/)\n" "MIME-Version: 1.0\n" @@ -18,43 +18,75 @@ msgstr "" "Language: sk\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "Nesprávna hlavička. Neboli poskytnuté prihlasovacie údaje." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "Nesprávna hlavička. Prihlasovacie údaje nesmú obsahovať medzery." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "Nesprávna hlavička. Prihlasovacie údaje nie sú správne zakódované pomocou metódy base64." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Nesprávne prihlasovacie údaje." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "Daný používateľ je neaktívny, alebo zmazaný." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "Nesprávna token hlavička. Neboli poskytnuté prihlasovacie údaje." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "Nesprávna token hlavička. Token hlavička nesmie obsahovať medzery." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Nesprávny token." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "Daný používateľ je zablokovaný." @@ -109,7 +141,7 @@ msgstr "Požiadavok obsahuje nepodporovaný media type: \"{media_type}\"." msgid "Request was throttled." msgstr "Požiadavok bol obmedzený, z dôvodu prekročenia limitu." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "Toto pole je povinné." @@ -127,7 +159,7 @@ msgstr "\"{input}\" je validný boolean." msgid "This field may not be blank." msgstr "Toto pole nemože byť prázdne." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Uistite sa, že toto pole nemá viac ako {max_length} znakov." @@ -213,137 +245,136 @@ msgstr "Nesprávny formát dátumu a času. Prosím použite jeden z nasledujúc msgid "Expected a datetime but got a date." msgstr "Vložený len dátum - date namiesto dátumu a času - datetime." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "Nesprávny formát dátumu. Prosím použite jeden z nasledujúcich formátov: {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "Vložený dátum a čas - datetime namiesto jednoduchého dátumu - date." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "Nesprávny formát času. Prosím použite jeden z nasledujúcich formátov: {format}." -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\" je nesprávny výber z daných možností." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "Bol očakávaný zoznam položiek, no namiesto toho bol nájdený \"{input_type}\"." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "Nebol odoslaný žiadny súbor." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "Odoslané dáta neobsahujú súbor. Prosím skontrolujte kódovanie - encoding type daného formuláru." -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "Nebolo možné určiť meno súboru." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "Odoslaný súbor je prázdny." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "Uistite sa, že meno súboru neobsahuje viac ako {max_length} znakov. (V skutočnosti ich má {length})." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Uploadujte prosím obrázok. Súbor, ktorý ste uploadovali buď nie je obrázok, alebo daný obrázok je poškodený." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "Bol očakávaný slovník položiek, no namiesto toho bol nájdený \"{input_type}\"." -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Nesprávne číslo stránky \"{page_number}\": {message}." +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "Nesprávny kurzor." -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "Nesprávny primárny kľúč \"{pk_value}\" - objekt s daným primárnym kľúčom neexistuje." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Nesprávny typ. Bol prijatý {data_type} namiesto primárneho kľúča." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Nesprávny hypertextový odkaz - žiadna zhoda." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Nesprávny hypertextový odkaz - chybná URL." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Nesprávny hypertextový odkaz - požadovný objekt neexistuje." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Nesprávny typ {data_type}. Požadovaný typ: hypertextový odkaz." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "Objekt, ktorého atribút \"{slug_name}\" je \"{value}\" neexistuje." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Nesprávna hodnota." diff --git a/rest_framework/locale/sv/LC_MESSAGES/django.mo b/rest_framework/locale/sv/LC_MESSAGES/django.mo index a6c04ef4bde5b3070f8f7ab5b52f16f3f4f2616d..d560de6e148ec09886153f5ccc95854ea689da9d 100644 GIT binary patch delta 2011 zcmYk-e`r-@9LMqR)!RKzb4zdDm1}w2T%~QdJ8y3Ds&{wIIqmL8E}c5T9J%O{cFEEd zibJ5#AA!Q{7a^KBtq8&#qGA!1fA~*iLF#Y`2L2-={s>Am=>0k8NImR(Ue9y9=g0GX zzR&Tq-q9ZK;?(E|MjId=A+C-y^YEMdIM9weW;5{wx_ANW@Gd@zH51Gp!Zy_P16YM` zVgr`29>2$D@ER_`#xh^ew$bV2#CMoOXQEj@F2!cthYRo=<}p6WEQ2qg{{0i|!e6l# zXWVZ#728oS)Q>vud3G##v0@OX%X;7{|TnMd=)&Lw4;LK7~IZ%ValEH@<_qK{qhvUVVVw^`Ux82v6)s&HOy-hquthiU)%i zOQDKrJwAyCun~X8H5i#@_Assu-4ePB=W_lFJRLC`LrwHxCHYrsf8#Zu#@ncIl9JYo z^o4E;eHZIFe*ov>8C-@Fd1@Y4pvIq~YUB#$Fv-nUU=cO*Q<%j&9-ZZMGE8?dzK6Q- z7wpGrA|Vpj0I$YLww4Vc4WT!fcV zzpv$^B0EE0!8)FAyXYuoM=^tU@iA=XunyOuGV?8RFB?M_BRpNjRD%(`*V17}v!u}d zk5UJA65QI~3E&rGx7I@GQPXVI#u3jGY**vs=+A;e_P^&~B+R-Ay^NZ&qt;ql9;@g=V`jab@SMX!x`mQYQpsn}IVYN`(vuXchmujT~!|3_#C_Jw18Al04=@QYvdH=a;g zXjd&IrVuJ4TDf0{E9qs4)upeUcr-cOprq0MgZ<_*QywrqD7ZYu`8+{VJI#jS5tn_c4vNBQh3~GyNTP0YjeypIDHET+F8HZDm;e{UcozX;XJcSY{V7VgS!4n ztiWR!z<017zrjuT4{pI&QQnNb$VmqS=P`=Q=bP=qer&}lT#qvt$JGnWBA7yb{~mVX zC0vbb7n&`>KGcLBLybT0c@lNMud!LL>R)8W$J#mYau!FuVIS&2&tMyVgd6csycYw- zW(~L>`Pc~#`rQ=Xg_lv8D_Lw-i(%CGAO`UmHYn9+IN63X*p8uF-5aK`ocZta_Wa!&_m#MBUW;0qcGUHIa19>9GJF#q`~)+loczE^AzneUY=7Zeyn!s2)sau7 zw26am(1AW2#O0Xu`V$zW{{m`BKSoV-7WKPw9;SXHsz$m?$$twcdl}GLzK!btgqnHD zQul}T=+NJan%FR^m?p6ff5s5bTV^(Z5xfmw_k7>;EH*NJ33CNz%gV^VZg`IADYe(J z30G3m>i421@|5Rmo~N;%@h@>5UPCQm15b_P)2RLzs9GuJCQ{Z5lO^8PBCF-PFf0%J@sj#;~tYOZGih;YHNM zGqap1^{rG)7p8DGeuhz8!LO-H8$eBT7?tun9BRLRjxj7+ zWk$VP9QkByk2i1xmGW1Rdsz;dgMEv7;8kRqt+LwPR$){IcA_RQhWh>u)b&$d|2*oy z>^Ial{R6cfucKf4zlNvk4eK$28&JF95Rz4U7L~e5RFR!PE!72Nb*!ewop=?nspJdMiC0-nY_tri{Z zMitc<7U0dMf>zT@Quz6G--26-hxNcPp|+FI+9*A0+A3;u2vsfH*!b7X*8zo{UluSj zY1;`+My=Py)@NhBicEkWGvd$Itke5OC3&;?IO!mw-oQf`BUFWjgmR@ls_Nv;^V@=L zY7Y?qZOUyCv6)astC1#OJ}38OfAxhk{S0j);)HgM+Wmy;NKN$-B-)4uLe1}D8}LD* zkLV`!N2#V#P*V-6{^k-Y3zgC~qJ&Tx(TekhsO_w}7|6cmFD+C>WpDV)a~J$a{gsKw zlTP*FF?eM3;K-xN@rmk)b9f|~PBS!->smbHThtr~H3ovMPN*%?9L`3|Dw^DpHYd~) z3ANM(Y4f`h=>vfs\n" "Language-Team: Swedish (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/sv/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -19,43 +19,75 @@ msgstr "" "Language: sv\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "Ogiltig \"basic\"-header. Inga användaruppgifter tillhandahölls." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "Ogiltig \"basic\"-header. Strängen för användaruppgifterna ska inte innehålla mellanslag." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "Ogiltig \"basic\"-header. Användaruppgifterna är inte korrekt base64-kodade." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Ogiltigt användarnamn/lösenord." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "Användaren borttagen eller inaktiv." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "Ogiltig \"token\"-header. Inga användaruppgifter tillhandahölls." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "Ogiltig \"token\"-header. Strängen ska inte innehålla mellanslag." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "Ogiltig \"token\"-header. Strängen ska inte innehålla ogiltiga tecken." -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Ogiltig \"token\"." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "Användarkontot är borttaget." @@ -110,7 +142,7 @@ msgstr "Medietypen \"{media_type}\" stöds inte." msgid "Request was throttled." msgstr "Förfrågan stoppades eftersom du har skickat för många." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "Det här fältet är obligatoriskt." @@ -128,7 +160,7 @@ msgstr "\"{input}\" är inte ett giltigt booleskt värde." msgid "This field may not be blank." msgstr "Det här fältet får inte vara blankt." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Se till att detta fält inte har fler än {max_length} tecken." @@ -214,137 +246,136 @@ msgstr "Datumtiden har fel format. Använd ett av dessa format istället: {forma msgid "Expected a datetime but got a date." msgstr "Förväntade en datumtid men fick ett datum." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "Datumet har fel format. Använde ett av dessa format istället: {format}." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "Förväntade ett datum men fick en datumtid." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "Tiden har fel format. Använd ett av dessa format istället: {format}." -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "Perioden har fel format. Använd ett av dessa format istället: {format}." -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\" är inte ett giltigt val." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "Fler än {count} objekt..." -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "Förväntade en lista med element men fick typen \"{input_type}\"." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "Det här valet får inte vara tomt." -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "\"{input}\" är inte ett giltigt val för en sökväg." -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "Ingen fil skickades." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "Den skickade informationen var inte en fil. Kontrollera formulärets kodningstyp." -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "Inget filnamn kunde bestämmas." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "Den skickade filen var tom." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "Se till att det här filnamnet har högst {max_length} tecken (det har {length})." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Ladda upp en giltig bild. Filen du laddade upp var antingen inte en bild eller en skadad bild." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "Den här listan får inte vara tom." -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "Förväntade en \"dictionary\" med element men fick typen \"{input_type}\"." -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "Värdet måste vara giltig JSON." -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "Skicka" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Ogiltigt sida \"{page_number}\": {message}." +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "Ogiltig cursor." -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "Ogiltigt pk \"{pk_value}\" - Objektet finns inte." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Felaktig typ. Förväntade pk-värde, fick {data_type}." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Ogiltig hyperlänk - Ingen URL matchade." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Ogiltig hyperlänk - Felaktig URL-matching." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Ogiltig hyperlänk - Objektet finns inte." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Felaktig typ. Förväntade URL-sträng, fick {data_type}." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "Objekt med {slug_name}={value} finns inte." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Ogiltigt värde." diff --git a/rest_framework/locale/tr/LC_MESSAGES/django.mo b/rest_framework/locale/tr/LC_MESSAGES/django.mo index c3412b330afad6bfa535bb4507c7190eb378dae1..70fe0331421c76d4006c21108add5a475071f389 100644 GIT binary patch delta 2010 zcmYk+Z){Ul7{~FaSlii+4OZxcf!r}R6;^CF_lFHe=l(O;hQiop;#i_1jmE|iCNt_y zNO;X+v-k&*5a9(RC@>N<5{+Oqh7b~nafx7%gclM+oF<}?7k+=Y*XT)~e$F}Vz4x5w zoO2KLpYL;jEDlZ^Wtdn-{4&qX#q$sHMfoIbR)(LVgFj#e-p5C9WuDo?*n+x#7)$Y0 zjAIt7@JoCO?_vigAM$iF9 z1!l$AiW<-Ws(-id+o=0}hP9gNJu36*i1WpytO+&4R@8&i*nmfIBi_JPTwZ9l7Dthf zP4T7w9mdCS7L~a`k=bf2#{~A`T6D2nss4z{2K*Db#_ATBEyZ?JdnalJ8PtrjzNh^D z%cvR8p_U}LFn4`9K1#b8V>pHmzJXEv0No&!GgQc~ox>;a0nOI4HL9`P)qqbYM@`C{`ZsL{tLA?Vm!SLtE1#!Ynb*sj-X~fi~8g5=wQ*J+`tm3 zO|%o&;D?yRD>#IK#b!(JS>HXr2e6v{U$>^fG`K3YQlwbqvyrq*Z)U)mYuV;A{iJK1fl z(yQj4|HCS#c+c}lW9}93J;+MbO9g7TS)e;fc4@QR)L&M>%=_Vj=KIi)J$JQ zW#SmJt?Xm0)cLIN)Et!Y1M38qmqI*BCBKKH$WwY2YJ4OX+U z)E+}!_daUCXVJmSSkLqAE|nBkvL&_FyHGcH3$+)%M(y$_hgmakM+Y;gQ!s@A{I}>@ z6|MhbLd(!jbP_zuI|*Rhh%K6l9;%{;sLUgtAvmsH3Y7(AM(eCX=4}hXM7^@rcVl*I zsMKXuz1NO*L%07+zOM)o9Yn7hlp!KT=sX1p((k<=dytuX2VVK>BmQ63)6jyp5_+4c zupS{V>22A$P>s8Tt`=ewp-ibLqntjk=qer$>ALiRkad} zhzAG`t+&UzP@9`Ws#56fD;-Onpuz0va5R{pmd(#wI&&+$KRh#4bUie`E}p1~Cz4LG zp|PeRI~gneucux;M`9(;;OIp9g>k2E{Q1l)dnTRrdo#IO>ZSC{nTho1%y+ToNcKQc LY-U^8x1oOk9pK1k delta 2100 zcmZYAe@xVM9LMoDMt6XUB7z1bUxA{8LyjLpr{IAE(qa5E0xjLy4n~OJ9o3A{{gH-$ z_0Q#+Tg`rK>6*(L=KQ0rf7+tSGWw^DwO0N^jkV=!{o`~!U*G$-+WL(5JwBh$-F-j5 z-k;<1o_BhaKW@rAXOzRlgT$X}%#yfxKQEL8zu9*D8Xa84hjCq&**2`jt=Nb9{tH-) za~Qx6unL#34ewzGwr0B~?JSi}I&NYZx8#@&;t)3C72J(07{Ri&W+9wJz5fw*<1H-1 zo$Jgt;Q(qvBdGq@JTIc2cLN)=RR4N2{;Zi77H1LE3I|XFox&!(ihJ=l+=qc&vuZqv z{Moy_=yO-_QTzq9b9o!gc3>T<{|MIL99CP@f#`FUxv55AoUi&<1C6`bu zTlD-XiQSH1&vG|i~*KZ_OkG8W4m8s*Fss(lr;L%B(w@-&q} zjN@gDUu^vY~-$p%e2_MG_W=mRG6uDq_5}A|DU=t?K zQPBs#!4b?VF*}Nr*o!xjKXV+j77U}#IEgxnPf%I?6KaCl+tc^H9yPIJsH1udHQ+Lm zua?j3sV1$Q3VCK7ScOCBH|z{Lv@fCtSU|3<-NtgfhniR!=kyrvLr%dakO$lA$W66N zs3g9I`u-QF1utT@?!S+xk#DvUl?&ylAHN7{tNKvC3**>~32ettQAhC?YDKvu5SwTv zo-Now`#G$`l-K?W^}V~emGP~BQ5~$tM(n{b&Y`w+0rh}CP&rY-WmdLNqgMVQI`}Q> zHr&Px{HN#`RFnrKzPtJkR9 z;g){S-KkbzaWX)Q8M%A$r1#ERPvIjvi7=s}`_xJtCNc@m$;~t^ogHxRk@l~b_;1nK zD8<_eZKDe3;d6gco=E-dt4j{i)kf%sRS_x&2yUZW_{DaUTZvko?sAV_)jh;B!~oGl z\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" +"Last-Translator: Xavier Ordoquy \n" "Language-Team: Turkish (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/tr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -24,43 +24,75 @@ msgstr "" "Language: tr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "Geçersiz yetkilendirme başlığı. Gerekli uygunluk kriterleri sağlanmamış." -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "Geçersiz yetkilendirme başlığı. Uygunluk kriterine ait veri boşluk karakteri içermemeli." -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "Geçersiz yetkilendirme başlığı. Uygunluk kriterleri base64 formatına uygun olarak kodlanmamış." -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Geçersiz kullanıcı adı/parola" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "Kullanıcı aktif değil ya da silinmiş." -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "Geçersiz token başlığı. Kimlik bilgileri eksik." -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "Geçersiz token başlığı. Token'da boşluk olmamalı." -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "Geçersiz token başlığı. Token geçersiz karakter içermemeli." -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Geçersiz token." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "Kullanıcı hesabı devre dışı bırakılmış." @@ -115,7 +147,7 @@ msgstr "İstekte desteklenmeyen medya tipi: \"{media_type}\"." msgid "Request was throttled." msgstr "Üst üste çok fazla istek yapıldı." -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "Bu alan zorunlu." @@ -133,7 +165,7 @@ msgstr "\"{input}\" geçerli bir boolean değil." msgid "This field may not be blank." msgstr "Bu alan boş bırakılmamalı." -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "Bu alanın {max_length} karakterden fazla karakter barındırmadığından emin olun." @@ -219,137 +251,136 @@ msgstr "Datetime alanı yanlış biçimde. {format} biçimlerinden birini kullan msgid "Expected a datetime but got a date." msgstr "Datetime değeri bekleniyor, ama date değeri geldi." -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "Tarih biçimi yanlış. {format} biçimlerinden birini kullanın." -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "Date tipi beklenmekteydi, fakat datetime tipi geldi." -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "Time biçimi yanlış. {format} biçimlerinden birini kullanın." -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "Duration biçimi yanlış. {format} biçimlerinden birini kullanın." -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "\"{input}\" geçerli bir seçim değil." -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "{count} elemandan daha fazla..." -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "Elemanların listesi beklenirken \"{input_type}\" alındı." -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "Bu seçim boş bırakılmamalı." -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "\"{input}\" geçerli bir yol seçimi değil." -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "Hiçbir dosya verilmedi." -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "Gönderilen veri dosya değil. Formdaki kodlama tipini kontrol edin." -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "Hiçbir dosya adı belirlenemedi." -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "Gönderilen dosya boş." -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "Bu dosya adının en fazla {max_length} karakter uzunluğunda olduğundan emin olun. (şu anda {length} karakter)." -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "Geçerli bir resim yükleyin. Yüklediğiniz dosya resim değil ya da bozuk." -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "Bu liste boş olmamalı." -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "Sözlük tipi bir değişken beklenirken \"{input_type}\" tipi bir değişken alındı." -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "Değer geçerli bir JSON olmalı" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "Gönder" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "Geçersiz sayfa \"{page_number}\":{message}." +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "Sayfalandırma imleci geçersiz" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "Geçersiz pk \"{pk_value}\" - obje bulunamadı." -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "Hatalı tip. Pk değeri beklenirken, alınan {data_type}." -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Geçersiz bağlantı - Hiçbir URL eşleşmedi." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Geçersiz bağlantı - Yanlış URL eşleşmesi." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Geçersiz bağlantı - Obje bulunamadı." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "Hatalı tip. URL metni bekleniyor, {data_type} alındı." -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "{slug_name}={value} değerini taşıyan obje bulunamadı." -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Geçersiz değer." diff --git a/rest_framework/locale/tr_TR/LC_MESSAGES/django.mo b/rest_framework/locale/tr_TR/LC_MESSAGES/django.mo index 1d3475bb273c9669249a5d57c620a3730379cb67..5d4511a4f2b3e5ef48ae9d8a5ada9739c4ac5e34 100644 GIT binary patch delta 44 qcmeC>@8#d{ftlA#*T7iUz)-=^!phiU@?U0Ygors%WV0$u1~UK(xC-t7 delta 44 rcmeC>@8#d{ftlA-*U(7Uz+A!5!phWm@?U0YgowG7sp)1_mJDV93pNV$ diff --git a/rest_framework/locale/tr_TR/LC_MESSAGES/django.po b/rest_framework/locale/tr_TR/LC_MESSAGES/django.po index a5f7d420b..9ead5da4a 100644 --- a/rest_framework/locale/tr_TR/LC_MESSAGES/django.po +++ b/rest_framework/locale/tr_TR/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Turkish (Turkey) (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/tr_TR/)\n" "MIME-Version: 1.0\n" @@ -18,43 +18,75 @@ msgstr "" "Language: tr_TR\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "Geçersiz kullanıcı adı / şifre." -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "Geçersiz simge." +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -109,7 +141,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -127,7 +159,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -213,137 +245,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "Geçersiz imleç." -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "Geçersiz hyper link - URL maçı yok." -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "Geçersiz hyper link - Yanlış URL maçı." -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "Geçersiz hyper link - Nesne yok.." -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "Geçersiz değer." diff --git a/rest_framework/locale/uk/LC_MESSAGES/django.mo b/rest_framework/locale/uk/LC_MESSAGES/django.mo index 7af39663903d2da845740b0073b6508f19ca6236..79396f543536eb6d2a2bb3842c62d7733c575cad 100644 GIT binary patch delta 41 ncmX@da*k!f1YR>;17lqSLj^+%D`Sg^bEOdi=0JgsM>`n-?&b?C delta 41 pcmX@da*k!f1YT2JLnB=Sa|J^SD^uf%bEOdi=2oVr8;^D}0s!w^3o!ry diff --git a/rest_framework/locale/uk/LC_MESSAGES/django.po b/rest_framework/locale/uk/LC_MESSAGES/django.po index cbf1b0f2f..0e3c82802 100644 --- a/rest_framework/locale/uk/LC_MESSAGES/django.po +++ b/rest_framework/locale/uk/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Ukrainian (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/uk/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: uk\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -212,137 +244,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" diff --git a/rest_framework/locale/vi/LC_MESSAGES/django.mo b/rest_framework/locale/vi/LC_MESSAGES/django.mo index 0be6ed259ca6319ac2213a93cc2cff7c1d55bcb0..a055d763a364964c42cb63ea396a2ada7557adaf 100644 GIT binary patch delta 41 ncmeyz{EvCU1YR>;17lqSLj^+%D`Sg^bEOdi=0JgsM=cov{P+tw delta 41 pcmeyz{EvCU1YT2JLnB=Sa|J^SD^uf%bEOdi=2oVr8;@Et0s#GP3qJq= diff --git a/rest_framework/locale/vi/LC_MESSAGES/django.po b/rest_framework/locale/vi/LC_MESSAGES/django.po index 8d0e5d3d3..0f8415b51 100644 --- a/rest_framework/locale/vi/LC_MESSAGES/django.po +++ b/rest_framework/locale/vi/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Vietnamese (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/vi/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: vi\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -212,137 +244,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" diff --git a/rest_framework/locale/zh-Hans/LC_MESSAGES/django.mo b/rest_framework/locale/zh-Hans/LC_MESSAGES/django.mo index 892ed14940ab9937a1ee281502a20c3cdc19317d..cffdae362001044baa09a3a24b87ea50bab071ad 100644 GIT binary patch delta 2006 zcmYM!Z%EZw9LMpmcDpw%wX|I;*Zh^*w!F8lTIMv(Qd^~_&SmC^61L!$bg}3k6h9b? zJ&0Iv8G|iqt?18_XuG37Bt#L>lZXW?w^dLK_M|PU2W_X9box{0!1=%KhihA%%)C2PA1hE2( zG2;5I7^B~g+RELiiC#rr_rUf4LFGo6x7T8I5%Jd^#@)aX)XGOu7d}G|3nphK7DFY` zdaT5wScNxn4Q9PzHWfc|ZgYNt)r_CQ+gWCJ@m>0RQ?eW9;dfY%cd!u)J+m(CMxt%sB9E}MsOSESY=LhNY3Lsl zn4YY}a$JGrizQGm`V@(xokT_G3KA51<}8?*xxNy|Fy4fE(Gt`IyHLsd87eaSaEi|V zH#Btae?-l61a;vptifNf4f9A0Z9%Vd$hjBUJv)So+y&=j)ZSOH8r|2Adj2qK;-6zV z@3*5gn(#U*2}3Mbr=l7^#Eqy2TtXIWqsXQ9!dW_t#HIg#(WF(hO1-d-+D6sUB(S=l0Cr^KSEER9R+>4`w!pd zS4FX1N>!Swtf4kjmF@v5VM~7>+mV%}vybDM&L((O7Sd8`w@~#pQ6YhX=}l-&J_*+N zs~K8M)sfWqLZySM?NZU#buLxWR`RMS6;v9iebf)sq2y+;ZYRlWlc{5<+Rpc=I$4vb z972_R8_Vx1s!OJx!%Fxe-W9)M*si- delta 2103 zcmY+^e@vBC9LMpmc)1q{MMSO&1y?jn;Nrbp2)KxX;jh4qP|_saZMzu-+ytC!j;>{n z{&B5c`@<&qt66JJS43;A=0E*mi|MRZ%eA&xMa)tC(I0Kj*8B6^r(b8B`#R_FJkL4j zd%n-}N&AKE$?q0>UNy>I>YdczGt81WmB|m~iqC8Xeu@FSiFademRSi_<1*|(-TxF8 z<0uC4JXYd$+=zeTW^BoJOxjB{wlHuFn{jE5Sr_icI=qC9ID>7t@(!~oCQ$!>3)^uT zSK``Sv&GnnTF?`y@uRM9pq}>`)@rN11!nwN6F+Ru+E6>}L{0Q8*5iA)0q5|33@$XQ z!UM>ko#98NiKj-#OqIU8o zYG+ffv+lSzKfUvE)X_Ad?(fEX@DTd(GzRc}Ocv1inuZ5&B2l*Aa4p_Kj>{^Dr$So8 zj~=iEz1V|GG4A$9Fhu`F)R9i27J3_XT@jP1UyaI<9Rfb=EJby|0 z!b%L#--=pTFDjW%Vg-JSVazHt+k;VDjwf6%x?aI*#;5VxJhMVS@n6Hhk1S82%_Eha zvqJTIP%C`_71D9H|FPTu23Irw2d>9bW^KhDd;rg*j`SDQ#Fgwy*X=_k;ds(*+`wH7 z_?b;1>~kH%N9kY1N?b$yn9I6Q$(6t$zT$cr^?J?WgIIGHo5G{W3uY1+uz{^;RUm$#%IOKwWQt7GBk7A&ALWOVjKgCN+UyEjUMCsl_-AnaQ38#};?a0oYEY;WKA?kmNvP>!7 zN>#X2$S|+-McI`4-Wy5oW@sa|jao@n*-mwi3;AL@Z(luDQB{-*D(k3^P&=tg zM;a%0m(ZS1)oDhl+o*g)osS+Yv`l`xs2iwzQfGYy9(}e_w|qqtpZkvaN@DxtfwIHT zz`lWl{g1~7N6Mms!~OB0A%;dK+Vf_-3u=Sm>R_lY5U!8bMpDiG;wEP#91esc(NLr! zM9FE74Gjl(4#ozC`eMV0!D!&gg9F3y!4UVYI}{%njwR|En)mm|diy3$`?IrG&z_x} b9Xl78K62`Rvriux`}L#O{$0R$=>_j!^~2`) diff --git a/rest_framework/locale/zh-Hans/LC_MESSAGES/django.po b/rest_framework/locale/zh-Hans/LC_MESSAGES/django.po index 983ff1ba0..8a0f67382 100644 --- a/rest_framework/locale/zh-Hans/LC_MESSAGES/django.po +++ b/rest_framework/locale/zh-Hans/LC_MESSAGES/django.po @@ -10,9 +10,9 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-11 03:03+0000\n" -"Last-Translator: hunter007 \n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" +"Last-Translator: Xavier Ordoquy \n" "Language-Team: Chinese Simplified (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/zh-Hans/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -20,43 +20,75 @@ msgstr "" "Language: zh-Hans\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "无效的Basic认证头,没有提供认证信息。" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "认证字符串不应该包含空格(基本认证HTTP头无效)。" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "认证字符串base64编码错误(基本认证HTTP头无效)。" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "用户名或者密码错误。" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "用户未激活或者已删除。" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "没有提供认证信息(认证令牌HTTP头无效)。" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "认证令牌字符串不应该包含空格(无效的认证令牌HTTP头)。" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "无效的Token。Token字符串不能包含非法的字符。" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "认证令牌无效。" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "用户账户已禁用。" @@ -111,7 +143,7 @@ msgstr "不支持请求中的媒体类型 “{media_type}”。" msgid "Request was throttled." msgstr "请求超过了限速。" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "该字段是必填项。" @@ -129,7 +161,7 @@ msgstr "“{input}” 不是合法的布尔值。" msgid "This field may not be blank." msgstr "该字段不能为空。" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "请确保这个字段不能超过 {max_length} 个字符。" @@ -215,137 +247,136 @@ msgstr "日期时间格式错误。请从这些格式中选择:{format}。" msgid "Expected a datetime but got a date." msgstr "期望为日期时间,获得的是日期。" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "日期格式错误。请从这些格式中选择:{format}。" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "期望为日期,获得的是日期时间。" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "时间格式错误。请从这些格式中选择:{format}。" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "持续时间的格式错误。使用这些格式中的一个:{format}。" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "“{input}” 不是合法选项。" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "多于{count}条记录。" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "期望为一个包含物件的列表,得到的类型是“{input_type}”。" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "这项选择不能为空。" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "\"{input}\"不是一个有效路径选项。" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "没有提交任何文件。" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "提交的数据不是一个文件。请检查表单的编码类型。" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "无法检测到文件名。" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "提交的是空文件。" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "确保该文件名最多包含 {max_length} 个字符 ( 当前长度为{length} ) 。" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "请上传有效图片。您上传的该文件不是图片或者图片已经损坏。" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "列表不能为空。" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "期望是包含类目的字典,得到类型为 “{input_type}”。" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "值必须是有效的 JSON 数据。" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "提交" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "无效页面 “{page_number}”:{message}。" +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "无效游标" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "无效主键 “{pk_value}” - 对象不存在。" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "类型错误。期望为主键,获得的类型为 {data_type}。" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "无效超链接 -没有匹配的URL。" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "无效超链接 -错误的URL匹配。" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "无效超链接 -对象不存在。" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "类型错误。期望为URL字符串,实际的类型是 {data_type}。" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "属性 {slug_name} 为 {value} 的对象不存在。" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "无效值。" diff --git a/rest_framework/locale/zh-Hant/LC_MESSAGES/django.mo b/rest_framework/locale/zh-Hant/LC_MESSAGES/django.mo index 0e9d3a3370df531ef82d539da31f1d2f99af8d03..d33604524b1bd71b84486a858d72aeb44b7f192c 100644 GIT binary patch delta 41 ncmbQpGLdD%1YR>;17lqSLj^+%D`Sg^bEOdi=0JgsM|~Ip+gu8C delta 41 pcmbQpGLdD%1YT2JLnB=Sa|J^SD^uf%bEOdi=2oVr8;|-h0s!063VHwl diff --git a/rest_framework/locale/zh-Hant/LC_MESSAGES/django.po b/rest_framework/locale/zh-Hant/LC_MESSAGES/django.po index a04ed527a..ea6a45312 100644 --- a/rest_framework/locale/zh-Hant/LC_MESSAGES/django.po +++ b/rest_framework/locale/zh-Hant/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Chinese Traditional (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/zh-Hant/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: zh-Hant\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -212,137 +244,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" diff --git a/rest_framework/locale/zh_CN/LC_MESSAGES/django.mo b/rest_framework/locale/zh_CN/LC_MESSAGES/django.mo index c1552b71f9b251bbe8dc876caf4308d059b54060..ecd7a91eb16f364fa0360fc7fbd39b25a49c22be 100644 GIT binary patch delta 2331 zcmYk-drXye9LMpm2Lud3MDA)FG(ZxNOI+e2pdt{UVwobu>=_xxfwC&XEDf{Ji;b9$-FPn!V-k*{?*AN@;w;{SAq&iMFdDP5 z0@q;|ZZz}RTTaD($2xu}=^FO=BM(KGzvtlYmT+oO7*#!>aco{X)G1QBuu@qCI&8n~k*W)S7#jlY+i(KSf zmx8NkZ$-`EAl{FsF%QQvm-pLWR2ncV#;g+iQ4bo$6#UR_-$0G*2h@n77dx&-ov%lY zxEr-JKGglEF%#d%1pE%Yco%(3s3db&DCQuuZ3VanE0JZi{jLMZpZPfGLBpstxrmAQ zj@$kW^Jq_?mehEOI$V#suG4KFS;G7)Sw=WfjF(Ys`IFmDk8?)Qin{I~deM*S;Jc_~ zn#TE<8Siu`2UpX60Q0d8ci}KN9d+TC zSdNQ%s%*yXcnmew-@3*y&yUis!EAgD>64A4lJF+xVCZs3UjY^Np|#;AJdfm~{fX>I z3!~e*F$U|g05#=@QQ1F+y5E!P?4kzLh<&K*j-z(LgzH_@u1fQ=JX~gVRLVFph?RH+ zH{&13GFdHe=N+~i_28#aOYwqxe$;gm@8kS0NZwi83g<+*5-S!bIr2Q<`;(Ms;|8`BJw@JL; z@~E)9)__IW;W~oKfl1VjAcL;Q{)Y2thZ4;MyYt_ooM3YVOYrMQO^K-`RRXG%5?!YT!fzZ;a zln0NR|NoUH>e@c~YH972xw{A@rq)(P9~4dN<3t$oAfa{EmrXk;m53p>5}H+J-P#Ck zZ!NP*15rgp>HXwGa8G)WnGRdKNmD1dsjx8*$hC{`jJZ?fsp7J?;BD{h!94 ziz?2^E6mBu_vV+D6_)xBB`p2#Oo`5%Nl5gzv=4N3_IWq=b@V=Q;E=cCNuIA-b$3@! z??6|3U@F1miG2I)?Mp+`SI^EKJ06%y@`MLMyth1IGnby9ePbvvv*I~Vl=GuG_`A|S Y@TX-|ESP=i!uLn7%$z*oZ_Q}@2W##F761SM delta 2228 zcmY+^e@xVM9LMnw2OKA91jwP7^bLYa?(mKufv1Ov_>*55Dwt)vwhKjp0-S4v$I7W| zt$!|MG+CuJnyqY+jz3)MpAu8%sz2IV$;KZNIah7f+MKQD>w8}+KfBL8KA+Eh@B96H z-k7t{Q}3t#?lLBZS5x_-Or{z0AYMibZ(lq5j{A z%kUWb@eKO#Yplb+aU-rh^XX>4NP8rEWNx-mPk9jovhuEtqxz`}cs31JWF_p{iD zGgyc#7aEg=t*8k-hZ=vyaSV08D_E(erY$mtj|ua|;!Fc-g{`Ouy@=KLDXzs^xE}rY z8dHidARqHKU%KubK7v1?BA1zA%nB?=jqkw#9>Y?FdV?d54y|9jNT zGZ!Z=^r1z6GiqY{P{}lcCHMmd(UW7$E)3yPe8cf0$4M+>duZ@}c@|sF@C+LOSg9FF5^gu$b}Rum+d#)Ou{gt#}HxrL(997qKc`*N#fU;h59- z7I!d^!P6ANsN+d|lKv(1VFB^uS*8V*Tzk=vuR2bkj@J!rM*ni|i7z1s%)EyB-$~qz zu?ZRqWfIF%R@b9$a2zY~8gf!h>H~@EvQSyP!|^ccw2Yy)W(q6t4u-JIN<^RwTj+m) zMBm&-ZXGk3Oh*q0ptd56{9rm9kK#l0-$U}qe1&@8k4RKaazP?u%aI_NX2(v{^+z!o zM^MihMNRN5x^@1iX(&WDP@(aPugC(eB>_nZ86Sy6xP&X(b^9X`jjr!lyjxS>p z{rRHRs%Vi~&?@Ris=fc*%U%`O3CO#|eyIAIt%{4P2P>Qkp(dx$>*%QvFgw{a36&=j z*cr}Aw%@L#!(By5%S$O{hek(5g;)5$#Z5~uky>X&r(hj*H#LPybnS#|L>6o(uU;~b zQ~zJWbd>q^R7Fum`K9;F2JJrwz%I(_`Qn*xI4a!prJY6{wSnq$2AfcOuEJ~IKEl;l zssTz`0<#9&sI63;hIDmQwk5Dnz+&3TROLj7x`oP{*nV|_Nu}1@@V_;OqTT(Go~l)~9bJ)q2gYl1U9K}tOVf(y-Z?)vdfJ*9I(65S jW`;(8x%lRPvs!Q6a*bCN9&yDl7kw6gy|^+yRlM~dMrrit diff --git a/rest_framework/locale/zh_CN/LC_MESSAGES/django.po b/rest_framework/locale/zh_CN/LC_MESSAGES/django.po index a5e6cde2f..7eca9d517 100644 --- a/rest_framework/locale/zh_CN/LC_MESSAGES/django.po +++ b/rest_framework/locale/zh_CN/LC_MESSAGES/django.po @@ -5,14 +5,14 @@ # Translators: # hunter007 , 2015 # Lele Long , 2015 -# Ming Chen , 2015 +# Ming Chen , 2015-2016 msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-11 03:00+0000\n" -"Last-Translator: hunter007 \n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" +"Last-Translator: Xavier Ordoquy \n" "Language-Team: Chinese (China) (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/zh_CN/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -20,43 +20,75 @@ msgstr "" "Language: zh_CN\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "无效的Basic认证头,没有提供认证信息。" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "认证字符串不应该包含空格(基本认证HTTP头无效)。" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "认证字符串base64编码错误(基本认证HTTP头无效)。" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "用户名或者密码错误。" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "用户未激活或者已删除。" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "没有提供认证信息(认证令牌HTTP头无效)。" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "认证令牌字符串不应该包含空格(无效的认证令牌HTTP头)。" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "无效的Token。Token字符串不能包含非法的字符。" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "认证令牌无效。" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "认证令牌" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "用户" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "令牌" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "令牌" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "用户名" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "密码" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "用户账户已禁用。" @@ -111,7 +143,7 @@ msgstr "不支持请求中的媒体类型 “{media_type}”。" msgid "Request was throttled." msgstr "请求超过了限速。" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "该字段是必填项。" @@ -129,7 +161,7 @@ msgstr "“{input}” 不是合法的布尔值。" msgid "This field may not be blank." msgstr "该字段不能为空。" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "请确保这个字段不能超过 {max_length} 个字符。" @@ -215,137 +247,136 @@ msgstr "日期时间格式错误。请从这些格式中选择:{format}。" msgid "Expected a datetime but got a date." msgstr "期望为日期时间,得到的是日期。" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "日期格式错误。请从这些格式中选择:{format}。" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "期望为日期,得到的是日期时间。" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "时间格式错误。请从这些格式中选择:{format}。" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "持续时间的格式错误。使用这些格式中的一个:{format}。" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "“{input}” 不是合法选项。" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "多于{count}条记录。" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "期望为一个包含物件的列表,得到的类型是“{input_type}”。" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "这项选择不能为空。" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "“{input}” 不是有效路径选项。" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "没有提交任何文件。" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "提交的数据不是一个文件。请检查表单的编码类型。" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "无法检测到文件名。" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "提交的是空文件。" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "确保该文件名最多包含 {max_length} 个字符 ( 当前长度为{length} ) 。" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "请上传有效图片。您上传的该文件不是图片或者图片已经损坏。" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "列表字段不能为空值。" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "期望是包含类目的字典,得到类型为 “{input_type}”。" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "值必须是有效的 JSON 数据。" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "保存" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." -msgstr "无效页面 “{page_number}”:{message}。" +msgid "Invalid page." +msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "无效游标" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "无效主键 “{pk_value}” - 对象不存在。" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "类型错误。期望为主键,得到的类型为 {data_type}。" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "无效超链接 -没有匹配的URL。" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "无效超链接 -错误的URL匹配。" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "无效超链接 -对象不存在。" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "类型错误。期望为URL字符串,实际的类型是 {data_type}。" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "属性 {slug_name} 为 {value} 的对象不存在。" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "无效值。" diff --git a/rest_framework/locale/zh_TW/LC_MESSAGES/django.mo b/rest_framework/locale/zh_TW/LC_MESSAGES/django.mo index e202136e7681b20ca558f9b9d0109c9d62dcf020..6371fcea7fd84e256c8240ad704f5cdbe3ced0c9 100644 GIT binary patch delta 41 ncmeBT>0+5Mf!9pez*yJ7P{Gi`%GhG!Txo=WIZ$BZQCCI)*&YgC delta 41 ocmeBT>0+5Mf!9>m&`8(7T*1)7%G7w`Txo=Wxs|Eu#-px`0NIoZWdHyG diff --git a/rest_framework/locale/zh_TW/LC_MESSAGES/django.po b/rest_framework/locale/zh_TW/LC_MESSAGES/django.po index 5ca07eeaa..8858ce17c 100644 --- a/rest_framework/locale/zh_TW/LC_MESSAGES/django.po +++ b/rest_framework/locale/zh_TW/LC_MESSAGES/django.po @@ -7,8 +7,8 @@ msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-12-07 18:53+0100\n" -"PO-Revision-Date: 2015-12-07 17:55+0000\n" +"POT-Creation-Date: 2016-03-01 18:38+0100\n" +"PO-Revision-Date: 2016-03-01 17:38+0000\n" "Last-Translator: Xavier Ordoquy \n" "Language-Team: Chinese (Taiwan) (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/zh_TW/)\n" "MIME-Version: 1.0\n" @@ -17,43 +17,75 @@ msgstr "" "Language: zh_TW\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: authentication.py:72 +#: authentication.py:71 msgid "Invalid basic header. No credentials provided." msgstr "" -#: authentication.py:75 +#: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." msgstr "" -#: authentication.py:81 +#: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." msgstr "" -#: authentication.py:98 +#: authentication.py:97 msgid "Invalid username/password." msgstr "" -#: authentication.py:101 authentication.py:188 +#: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." msgstr "" -#: authentication.py:167 +#: authentication.py:173 msgid "Invalid token header. No credentials provided." msgstr "" -#: authentication.py:170 +#: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." msgstr "" -#: authentication.py:176 +#: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." msgstr "" -#: authentication.py:185 +#: authentication.py:192 msgid "Invalid token." msgstr "" +#: authtoken/apps.py:7 +msgid "Auth Token" +msgstr "" + +#: authtoken/models.py:21 +msgid "Key" +msgstr "" + +#: authtoken/models.py:23 +msgid "User" +msgstr "" + +#: authtoken/models.py:24 +msgid "Created" +msgstr "" + +#: authtoken/models.py:33 +msgid "Token" +msgstr "" + +#: authtoken/models.py:34 +msgid "Tokens" +msgstr "" + +#: authtoken/serializers.py:8 +msgid "Username" +msgstr "" + +#: authtoken/serializers.py:9 +msgid "Password" +msgstr "" + #: authtoken/serializers.py:20 msgid "User account is disabled." msgstr "" @@ -108,7 +140,7 @@ msgstr "" msgid "Request was throttled." msgstr "" -#: fields.py:266 relations.py:195 relations.py:228 validators.py:79 +#: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." msgstr "" @@ -126,7 +158,7 @@ msgstr "" msgid "This field may not be blank." msgstr "" -#: fields.py:670 fields.py:1656 +#: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." msgstr "" @@ -212,137 +244,136 @@ msgstr "" msgid "Expected a datetime but got a date." msgstr "" -#: fields.py:1079 +#: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1080 +#: fields.py:1083 msgid "Expected a date but got a datetime." msgstr "" -#: fields.py:1148 +#: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1207 +#: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." msgstr "" -#: fields.py:1232 fields.py:1281 +#: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." msgstr "" -#: fields.py:1235 relations.py:62 relations.py:431 +#: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." msgstr "" -#: fields.py:1282 fields.py:1429 relations.py:427 serializers.py:520 +#: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1283 +#: fields.py:1291 msgid "This selection may not be empty." msgstr "" -#: fields.py:1320 +#: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." msgstr "" -#: fields.py:1339 +#: fields.py:1347 msgid "No file was submitted." msgstr "" -#: fields.py:1340 +#: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." msgstr "" -#: fields.py:1341 +#: fields.py:1349 msgid "No filename could be determined." msgstr "" -#: fields.py:1342 +#: fields.py:1350 msgid "The submitted file is empty." msgstr "" -#: fields.py:1343 +#: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." msgstr "" -#: fields.py:1391 +#: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." msgstr "" -#: fields.py:1430 relations.py:428 serializers.py:521 +#: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." msgstr "" -#: fields.py:1483 +#: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." msgstr "" -#: fields.py:1530 +#: fields.py:1538 msgid "Value must be valid JSON." msgstr "" -#: filters.py:35 templates/rest_framework/filters/django_filter.html:5 +#: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" msgstr "" #: pagination.py:189 -#, python-brace-format -msgid "Invalid page \"{page_number}\": {message}." +msgid "Invalid page." msgstr "" #: pagination.py:407 msgid "Invalid cursor" msgstr "" -#: relations.py:196 +#: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." msgstr "" -#: relations.py:197 +#: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." msgstr "" -#: relations.py:229 +#: relations.py:240 msgid "Invalid hyperlink - No URL match." msgstr "" -#: relations.py:230 +#: relations.py:241 msgid "Invalid hyperlink - Incorrect URL match." msgstr "" -#: relations.py:231 +#: relations.py:242 msgid "Invalid hyperlink - Object does not exist." msgstr "" -#: relations.py:232 +#: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." msgstr "" -#: relations.py:391 +#: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." msgstr "" -#: relations.py:392 +#: relations.py:403 msgid "Invalid value." msgstr "" From 6ea9a414080e651750516dba5aca802b21583aa8 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 7 Mar 2016 20:39:34 +0100 Subject: [PATCH 022/457] Add #3962 in the last minute fixes. --- docs/topics/release-notes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 7834448c1..9fa399670 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -68,6 +68,7 @@ You can determine your currently installed version using `pip freeze`: * Add missing csrf_token in AdminRenderer post form. Thanks to Piotr Śniegowski for the fix. ([#3703][gh3703]) * Refactored `_get_reverse_relationships()` to use correct `to_field`. Thanks to Benjamin Phillips for the fix. ([#3696][gh3696]) * Document the use of `get_queryset` for `RelatedField`. Thanks to Ryan Hiebert for the fix. ([#3605][gh3605]) +* Fix empty pk detection in HyperlinkRelatedField.get_url. Thanks to @jslang for the fix ([#3962][gh3962]) ### 3.3.2 @@ -683,6 +684,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh3968]: https://github.com/tomchristie/django-rest-framework/issues/3968 +[gh3962]: https://github.com/tomchristie/django-rest-framework/issues/3962 [gh3913]: https://github.com/tomchristie/django-rest-framework/issues/3913 [gh3912]: https://github.com/tomchristie/django-rest-framework/issues/3912 [gh3910]: https://github.com/tomchristie/django-rest-framework/issues/3910 From b572cdb068c0f14e101749a6c7affe4f1a5acaf3 Mon Sep 17 00:00:00 2001 From: Kin Date: Tue, 16 Feb 2016 23:08:45 -0800 Subject: [PATCH 023/457] fix typo --- docs/tutorial/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index 910eb4cff..5e3b522cc 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -157,7 +157,7 @@ We can now access our API, both from the command-line, using tools like `curl`.. Or using the [httpie][httpie], command line tool... - bash: http -a username:password123 http://127.0.0.1:8000/users/ + bash: http -a admin:password123 http://127.0.0.1:8000/users/ HTTP/1.1 200 OK ... From 0e1dcb7323c9ad610d085ee6cbab0182d68edc84 Mon Sep 17 00:00:00 2001 From: Wes Date: Wed, 17 Feb 2016 16:35:53 +0800 Subject: [PATCH 024/457] fix typo --- 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 6aa34da3d..2e6207805 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -788,7 +788,7 @@ Here's an example of how you might choose to implement multiple updates: # Perform creations and updates. ret = [] for book_id, data in data_mapping.items(): - book = book_mapping.get(book_id, None): + book = book_mapping.get(book_id, None) if book is None: ret.append(self.child.create(data)) else: From 0a2a01d05a48de0850ed5f614ea1e5ff08aecd21 Mon Sep 17 00:00:00 2001 From: meoooh Date: Thu, 18 Feb 2016 14:43:41 +0900 Subject: [PATCH 025/457] fix typo fix typo --- 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 d6c80d93a..f704a3247 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -317,4 +317,4 @@ The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin` [link-header]: ../img/link-header-pagination.png [drf-extensions]: http://chibisov.github.io/drf-extensions/docs/ [paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin -[disqus-cursor-api]: http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api/ +[disqus-cursor-api]: http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api From 2e2abbc811f536c8266e74f7955dafb4260d9b1f Mon Sep 17 00:00:00 2001 From: ildoc Date: Thu, 25 Feb 2016 14:27:57 +0100 Subject: [PATCH 026/457] updated tutorial for django 1.9 --- docs/tutorial/1-serialization.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index fd0783f07..87856e037 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -45,7 +45,7 @@ We'll need to add our new `snippets` app and the `rest_framework` app to `INSTAL INSTALLED_APPS = ( ... 'rest_framework', - 'snippets', + 'snippets.apps.SnippetsConfig', ) Okay, we're ready to roll. @@ -293,11 +293,11 @@ Finally we need to wire these views up. Create the `snippets/urls.py` file: url(r'^snippets/$', views.snippet_list), url(r'^snippets/(?P[0-9]+)/$', views.snippet_detail), ] - + We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet app's URLs. - + from django.conf.urls import url, include - + urlpatterns = [ url(r'^', include('snippets.urls')), ] From cfd681dc73e49c7b9878b1adbfa34b0c3bf4eba0 Mon Sep 17 00:00:00 2001 From: Krzysztof Szularz Date: Tue, 1 Mar 2016 14:52:07 +0100 Subject: [PATCH 027/457] Add `/en` to Heroku guidelines link --- docs/api-guide/versioning.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/versioning.md b/docs/api-guide/versioning.md index df5d5dfbc..54aa0170d 100644 --- a/docs/api-guide/versioning.md +++ b/docs/api-guide/versioning.md @@ -214,7 +214,7 @@ If your versioning scheme is based on the request URL, you will also want to alt [cite]: http://www.slideshare.net/evolve_conference/201308-fielding-evolve/31 [roy-fielding-on-versioning]: http://www.infoq.com/articles/roy-fielding-on-versioning [klabnik-guidelines]: http://blog.steveklabnik.com/posts/2011-07-03-nobody-understands-rest-or-http#i_want_my_api_to_be_versioned -[heroku-guidelines]: https://github.com/interagent/http-api-design/blob/master/foundations/require-versioning-in-the-accepts-header.md +[heroku-guidelines]: https://github.com/interagent/http-api-design/blob/master/en/foundations/require-versioning-in-the-accepts-header.md [json-parameters]: http://tools.ietf.org/html/rfc4627#section-6 [vendor-media-type]: http://en.wikipedia.org/wiki/Internet_media_type#Vendor_tree [lvh]: https://reinteractive.net/posts/199-developing-and-testing-rails-applications-with-subdomains From cac1ecb2502bf5de4455d40516b132bb657d7a9c Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 8 Mar 2016 06:37:46 +0100 Subject: [PATCH 028/457] paginate_by removed in 3.3 --- docs/api-guide/generic-views.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 4a9581212..0b155f000 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -26,7 +26,6 @@ Typically when using the generic views, you'll override the view, and set severa queryset = User.objects.all() serializer_class = UserSerializer permission_classes = (IsAdminUser,) - paginate_by = 100 For more complex cases you might also want to override various methods on the view class. For example. From 920861936255ad7f9057df339685e6f059948a4e Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 8 Mar 2016 06:38:03 +0100 Subject: [PATCH 029/457] Those are now fully removed. --- docs/api-guide/generic-views.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 0b155f000..e6e300561 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -71,8 +71,6 @@ The following attributes are used to control pagination when used with list view * `pagination_class` - The pagination class that should be used when paginating list results. Defaults to the same value as the `DEFAULT_PAGINATION_CLASS` setting, which is `'rest_framework.pagination.PageNumberPagination'`. -Note that usage of the `paginate_by`, `paginate_by_param` and `page_kwarg` attributes are now pending deprecation. The `pagination_serializer_class` attribute and `DEFAULT_PAGINATION_SERIALIZER_CLASS` setting have been removed completely. Pagination settings should instead be controlled by overriding a pagination class and setting any configuration attributes there. See the pagination documentation for more details. - **Filtering**: * `filter_backends` - A list of filter backend classes that should be used for filtering the queryset. Defaults to the same value as the `DEFAULT_FILTER_BACKENDS` setting. From a0aac166bc23b82c4b93f255faafc8fa7766f558 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 8 Mar 2016 06:39:08 +0100 Subject: [PATCH 030/457] DEFAULT_PAGINATION_SERIALIZER_CLASS removed in 3.1 --- docs/api-guide/settings.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 23691dec1..bc351f321 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -102,9 +102,15 @@ Default: `'rest_framework.negotiation.DefaultContentNegotiation'` #### DEFAULT_PAGINATION_SERIALIZER_CLASS -A class the determines the default serialization style for paginated responses. +--- -Default: `rest_framework.pagination.PaginationSerializer` +**This setting has been removed.** + +The pagination API does not use serializers to determine the output format, and +you'll need to instead override the `get_paginated_response method on a +pagination class in order to specify how the output format is controlled. + +--- #### DEFAULT_FILTER_BACKENDS From 180137300f3bb1f67a1003ff940b2b9b0c2da5e1 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 8 Mar 2016 06:39:58 +0100 Subject: [PATCH 031/457] PAGINATE_BY removed in 3.3 --- docs/api-guide/settings.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index bc351f321..4de23b1fd 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -119,15 +119,9 @@ If set to `None` then generic filtering is disabled. #### PAGINATE_BY -The default page size to use for pagination. If set to `None`, pagination is disabled by default. - -Default: `None` - -#### PAGINATE_BY_PARAM - --- -**This setting is pending deprecation.** +**This setting has been removed.** See the pagination documentation for further guidance on [setting the pagination style](pagination.md#modifying-the-pagination-style). From c90cf828aeda3ebb1a599064ee6c39c03eb04242 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 8 Mar 2016 06:40:33 +0100 Subject: [PATCH 032/457] PAGE_SIZE addition in 3.1 --- docs/api-guide/settings.md | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 4de23b1fd..142cd7ebc 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -127,18 +127,9 @@ See the pagination documentation for further guidance on [setting the pagination --- -The name of a query parameter, which can be used by the client to override the default page size to use for pagination. If set to `None`, clients may not override the default page size. +#### PAGE_SIZE -For example, given the following settings: - - REST_FRAMEWORK = { - 'PAGINATE_BY': 10, - 'PAGINATE_BY_PARAM': 'page_size', - } - -A client would be able to modify the pagination size by using the `page_size` query parameter. For example: - - GET http://example.com/api/accounts?page_size=25 +The default page size to use for pagination. If set to `None`, pagination is disabled by default. Default: `None` From c91229aaab0e730b0e7f19ce47d0023edb343533 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 8 Mar 2016 06:41:08 +0100 Subject: [PATCH 033/457] PAGINATE_BY_PARAM and MAX_PAGINATE_BY removed in 3.3 --- docs/api-guide/settings.md | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 142cd7ebc..f218d00ad 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -133,6 +133,16 @@ The default page size to use for pagination. If set to `None`, pagination is di Default: `None` +#### PAGINATE_BY_PARAM + +--- + +**This setting has been removed.** + +See the pagination documentation for further guidance on [setting the pagination style](pagination.md#modifying-the-pagination-style). + +--- + #### MAX_PAGINATE_BY --- @@ -143,22 +153,6 @@ See the pagination documentation for further guidance on [setting the pagination --- -The maximum page size to allow when the page size is specified by the client. If set to `None`, then no maximum limit is applied. - -For example, given the following settings: - - REST_FRAMEWORK = { - 'PAGINATE_BY': 10, - 'PAGINATE_BY_PARAM': 'page_size', - 'MAX_PAGINATE_BY': 100 - } - -A client request like the following would return a paginated list of up to 100 items. - - GET http://example.com/api/accounts?page_size=999 - -Default: `None` - ### SEARCH_PARAM The name of a query parameter, which can be used to specify the search term used by `SearchFilter`. From 2ef74cfa61d6f52a2a5c32151a052488d5d7e9e2 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Sun, 13 Mar 2016 20:39:19 +0100 Subject: [PATCH 034/457] Bring check for null fk to `BaseSerializer.to_representation` --- rest_framework/fields.py | 2 -- rest_framework/serializers.py | 10 +++++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index c700b85e8..6d5962c8e 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -778,8 +778,6 @@ class UUIDField(Field): return data def to_representation(self, value): - if value is None: - return None if self.uuid_format == 'hex_verbose': return str(value) else: diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index b95bb7fa6..625d32644 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -464,9 +464,13 @@ class Serializer(BaseSerializer): except SkipField: continue - if attribute is None: - # We skip `to_representation` for `None` values so that - # fields do not have to explicitly deal with that case. + # We skip `to_representation` for `None` values so that fields do + # not have to explicitly deal with that case. + # + # For related fields with `use_pk_only_optimization` we need to + # resolve the pk value. + check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute + if check_for_none is None: ret[field.field_name] = None else: ret[field.field_name] = field.to_representation(attribute) From 265db86590baf57ae2c9ee9bf0bd66172ebe27e8 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 14 Mar 2016 08:31:27 +0100 Subject: [PATCH 035/457] Translation update. --- .../locale/pl/LC_MESSAGES/django.mo | Bin 10432 -> 10799 bytes .../locale/pl/LC_MESSAGES/django.po | 24 +-- .../locale/tr/LC_MESSAGES/django.mo | Bin 10172 -> 10548 bytes .../locale/tr/LC_MESSAGES/django.po | 25 +-- .../locale/tr_TR/LC_MESSAGES/django.mo | Bin 1933 -> 10520 bytes .../locale/tr_TR/LC_MESSAGES/django.po | 168 +++++++++--------- 6 files changed, 109 insertions(+), 108 deletions(-) diff --git a/rest_framework/locale/pl/LC_MESSAGES/django.mo b/rest_framework/locale/pl/LC_MESSAGES/django.mo index 232de88ffe95f84a46cafd9c534cbc980579a248..f228dfc7c72f6d5c1a7c86d8cd61ebe20b6f29e7 100644 GIT binary patch delta 2501 zcmY+_Yiv|S7{>8;yU<-OEznYIxo!caLV;4Lm2zt-mSQi6ZIKp4PL~CHUD;jIv?gp& zutAMr)=ShN2}Gi(QL`jsBF3P^hz28mP)v#;e$W^(8bL*g_j3HNMO>Zq`mi4}28M@FenQe{#`GGqY1G&PKgxA>M&uT#m0` zHU5C}uy_nF!wsn4bz%`7M(yNhI2(V)EZ%PgW6er%7U}`(aWxL&Qv3q-z%k>@CgN07 ze-UaWwWzJ{PP;cfegwUYpGNJ#8C1wFpq`___$-`6V**xV0NXGZAHrd{4~c?3jq~v! z5^noE?bpbkUEreUTtcPKC?0YX=Ainsu>=>OLfoE9{5A8V=^uWK>VJz$qN{j2=8sRs zb`7dOh+4t>sNbE#0A5B-a6+D&Tvmg_>F-5NU?0xGXK?|Z%wzvsX~Jui-)*Le8t@OiP`P7SxsoQCZ!IRrmxp;Thb7(*tH*xC=Fr zi^yhMDK{#=szI1t;)Uyn=+^X0VSMFULxZqR#g*)B`;n0rJgiP{*njiHhyU zTku5;;7Mdo_8lrj-kHjcI9o=8UA0ow4;qkXTMufB_M>KeJUxB}^@3k<82Sl3N7=Ga z$u|Z0CRqb&hgy*nY@3lywC(Bf{g}`D?Kv7gd<&IK$FUlJLc(e#EJH7>M};bgyxVqR z10KP9@e(Q*>Y24dco22}r^sg7_c#|vu>2Y<$M|p>J85L%Zd{6wqrP09pkkOoJhUPQ z**)t)=4MZ!w*DQ|#80En_dn>tb<{;vg;DokFUtH8)MCY7MIV>zMGsO@PACj2ivL|y z=HnKvb~RO#SI881g|L!(H&w+=>QvKR^#C54KYs?R0I!aeig zCJrAp+e{5oeN+|2lh647ijq;`;y*n1gb;2wKPu3n>KHduRY(jl7mjR6 z{x$B^jM1rk-D@;)X#9ypTV9#JB^rwKIz6GplX+h*DG!t`s3z>dC-P-LJ#7V7Tp za5_KQ6YNh6Q zM{po{d3up2v(|~8dN`b%Jad}&zboiZRLuHnRLiON`eR+;zTS?m#Mq*`oJMD$&k5|f LG3q4Rinjg>NU|#) delta 2174 zcmYk-TWl0n9LMqhmhBaVUZmAZ+vyFcv~}Cs3oR|JEv3*>KucSSN;guHRocZ~phRSS z@Ij3U#EF_6y$*y;KTSMc4JMtXUt|9Y+~UfY{%3Lvq9{^Mx4VIyoSA)v(&5^_neute{u*|F)!^oc< z=b-P+;TpV-id9m~DOiqq zSn1EZFvPqUmC7Ti8@+`3-VJ~L7pgXLxq1WE<`947Fyb$~h1&Ub)E9q22bbTIxUmqb zh(>WOp28aZ8h2q}h1q?$+jqwI7}l~rj{yq&TdZTgm`D7T>ugF`Icvfa45M!31uVlk z)QK;lLb`}LVG4_FxDJPL7PsLQ+=<1UL^X2=AHlPz;>%!ns-f-}gMAEM#xA_&+d%w| zGJh6z!N0KpOE@UB?WhPmj*7rb_z+$|Md&A_POXlDP|7+`#n^)#_!K^hv3D3e!N7{l zo`yZB8~Fl>l4UxH9j?N9=404`Z(;>r^XCE1-p;%g`|v1w=RwkLnS|AQ3Q)DwkE`^m z8DrqE@CtHGdl!|8%Sg2BcYi&LM}VQ#pdvDWy1^s<`m3lDe~3Z+5_zEP3aY4YAg>6^ zD&uCb01Nf}S2IwEwjyb@0ZhelRFTYJC(a|;u^&(uN~ds$vN_0-wc%EL5{GaBmGgAA zsZ>3N`u&SYQ0z2Taee!YK{x)2t=Pi$n(#0xHD{2Uw=BZKIjtUbW4lq0-7yT{?WWJv zw0lKJ=h{f$MAz?CxM~CRm_nhvsHu?DlISW9<&Br4cT*I!_dW+N4ce)8t+pe9SLr#7 zRd`C}ovo4y`|yf%v;P6<(EbxG1r&vTf4&Rb=_B+YT@lc0vzJbwyeikrMUUH^Z9NnI zm3S#@@IRo&z2facrA&>W(fsSDd6WMSs)O52x$K~Cp(~GSs&EyjnqJW=Full{>1wG7 zyeDOlvFbogZ&|hN39LeQ#LLT;&{Z7k=sol-x}I~jWcp5e8-19r=FnAyDx{D*o_5n6 zNgs-zPmiR;LzyQ6@vpL$2I4of-%fI0UvbeL$_=^4bFa45I5myUbxmtSw2Z#+)O6Lr zWO!m~e|S1N+3XC54@Sl&oo$nQqtDD7cGk~E6QlP1k%{QRNH~5nZ$nZ>e`L%#5ZxP| zh`I~TN;g!T>=x&Tf?eV1u)C079DhFlRBF7j, 2015 # Piotr Jakimiak , 2015 -# Maciek Olko , 2015 +# Maciek Olko , 2015-2016 msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-03-01 18:38+0100\n" -"PO-Revision-Date: 2016-03-01 17:38+0000\n" -"Last-Translator: Xavier Ordoquy \n" +"PO-Revision-Date: 2016-03-07 21:25+0000\n" +"Last-Translator: Maciek Olko \n" "Language-Team: Polish (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/pl/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -59,35 +59,35 @@ msgstr "Niepoprawny token." #: authtoken/apps.py:7 msgid "Auth Token" -msgstr "" +msgstr "Token uwierzytelniający" #: authtoken/models.py:21 msgid "Key" -msgstr "" +msgstr "Klucz" #: authtoken/models.py:23 msgid "User" -msgstr "" +msgstr "Użytkownik" #: authtoken/models.py:24 msgid "Created" -msgstr "" +msgstr "Stworzono" #: authtoken/models.py:33 msgid "Token" -msgstr "" +msgstr "Token" #: authtoken/models.py:34 msgid "Tokens" -msgstr "" +msgstr "Tokeny" #: authtoken/serializers.py:8 msgid "Username" -msgstr "" +msgstr "Nazwa użytkownika" #: authtoken/serializers.py:9 msgid "Password" -msgstr "" +msgstr "Hasło" #: authtoken/serializers.py:20 msgid "User account is disabled." @@ -338,7 +338,7 @@ msgstr "Wyślij" #: pagination.py:189 msgid "Invalid page." -msgstr "" +msgstr "Niepoprawna strona." #: pagination.py:407 msgid "Invalid cursor" diff --git a/rest_framework/locale/tr/LC_MESSAGES/django.mo b/rest_framework/locale/tr/LC_MESSAGES/django.mo index 70fe0331421c76d4006c21108add5a475071f389..14f5dc981f0819346e1355b1e01fbf482e93b10f 100644 GIT binary patch delta 2496 zcmYk+TWl0n9LMpqEp%5}ZiOOCwN9x}1q!9K1quqKh=mr4+*_-G$w)iazX% zvEl_)4MZZSNL?Nz@d|B>glMQSQ4=sEh8T@XB6xv5`2Ece(Ubn?bIx@3%(?u} zeAskmQ|e0om{$#@ow$dn%r@o#Hs)}l>6ygFL zhYeVU+i)JffI%F@RmP;uSt@I|G3r~#uO6lSGOolC+=f$fjaiJ{xD1bC175)zoRgRN zzk`jmU&j*s8S`<%_{;!{QTHGAOc~QgMIF9~Rd^2hGk?*1s-At8YA(d%ZhZeTr6nq}XU_Y`8<`6Ex z0c5+)SH9mOf95I|^>YK2KDl&qH|C?-bFds2qPDng3j42-pY$L69M%2-l|-Xhjnk)Q z_O=Pt?nBMsW7Kosp@lb51DsaiC6`%)V`(2i4WJ+A;$d8f=L%T=W-2$hp_y)A_+i}a z+k={EpWl83D`=m@;eauhaW?H^908q*3#h%lj@p_+I@1cQz5P73wjLM^L9^GH+KutyqsIQ3Fl= zK}91hVpub=8Y{8c_aLgn^SBhl_mPOW89A}09T~IfL1lX%>bXy`6K~-zv@K&c;%CS$ zlV8jc)cLQVqNLb|+PkBu<1&IIzPXKMn8)Zeuo_&2G1Lp+M?Loo@~9a_65JHA{`0UL zb-xuYOriRJ1B1NZd_hG^ehKf#E2t4>(|8a|knJ?ixDETN(g%_e*ZTW~eLhT59Ts2ScwcERM(i=2g3xEdGX zbAJ02>UTe)20n@w7I4-VV>PbCD5f-n_o(m{H6NgoVhGjY@2I3HVZHR+UevMKk6M|x zQ3Lx1wKCUG$1B7?1GtUQifH!g|4z}Mv_fo;=AZevcoi;sv5IEKR(fR_*L4K_du2P< zwZvFLTctsBFuYl9L>2PdTSe8CKRbzC#2P|_EGNA8>r+}lXipy@SY@x&aotR`5q!`x z#|t+QD%C{Dt7TS0)yIh@|DINAm0!0|$=yO^5h|QmvsLrgUaDjh+Bc4c*+HmidHJZB zCy90gbF?4DUq)DSkI)3b$8AtlUXL~6NO6eV7>SNrd>f+`<4 z?@Ok;twat%B73ElE8iEdD6v%L6Jf8GIi8!SH~8&()VD)LUr|cHxVM8d+>zjL$l4Oy z<3vrZyJ39TeK%AatcyE#(&Fbm70zo^GcqSI_NZ*=# zA}cTRTko21k52u_Z7HY53YgfbEgt<;%?{cbe0`;^GjPNHS|X4Z)mW6Xwb4dh6deeX>nk_ I+h1DtFCR}Xg#Z8m delta 2151 zcmYk+|4&tQ9LMqZC6KS7f_{^_yFF(F04uSjM)(?-JJLgTQN1mY#4j60Z(BQeusUSn`zdF6R7LQaVO4U2^MCV zaJ?|5&(@>YF+{uY*4h+hgP$O(dy(o-Z@igwhYuJt(vduPQ2>IDG z2mS99mg78X<^n6t?!^)eVn1%i81`zaKcLc#ecRh_9*|9A1(sp~ zR{8A?4ASmHrSdscN3Wp%_p{&r2emfxd3!z9<}&}vVc0+MCTisKs6YON4z662=vWZ7 zi1uJ5zJ)dTHI88*&ulF|>U+rd7}j$BUHmd&_5+sDK3%~4E7!AhQ#o2hP4#+~w0ho( z8*mggwNt2$&EPnGgl(9`TX$kVcHjvd!=F%VrIAso{Q_!+=5Yv}7*C5*nZiE&4fUWp z=C21|M0IQy)uEeMjM?<+eys7GL_P2XZo@@nIawvWW@A|s(iiK*tvG@DU+h&XlT_w# z0=E^K?ZVfPpIztBf*DRC$GxaE@+@kVzlYWMEviF-qC|@7Q7<}*tV=tK{OmFZmXrO0 zRr=H{`4=`aEQbqSs0Z&wcBW0^MtlR+p-WhS3&`wS7Wv{4R*LLQ>&7%3Lf!uuYNSu0 zX5wvRS=onJru~1JiavJNQB(8>>N}9ZTbnTlmD63Q6wIJT^gc3a_NnhRtfPGkw_q&` zOYKS2eXpZBd=4GFiVeKqZcu5(GM1!ry%+U>mr!fr3)CvlWixB!o#w7D;2en1m>Rw6VI7Db0j(TSj`PV18hzPn*r!BbS z=_BzA>ERXe;Hu++_*bhl1M!8Ncaq$f^Deq0`9XI&f3Bs*scC53vb8cu$=De>aIktX z8rpwgU+7>Y+USgirovA|oq_1%k!KDab+#XgBx, 2015 # Emrah BİLBAY , 2015 # Ertaç Paprat , 2015 +# José Alaguna , 2016 # Mesut Can Gürle , 2015 # Murat Çorlu , 2015 # Recep KIRMIZI , 2015 @@ -15,8 +16,8 @@ msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-03-01 18:38+0100\n" -"PO-Revision-Date: 2016-03-01 17:38+0000\n" -"Last-Translator: Xavier Ordoquy \n" +"PO-Revision-Date: 2016-03-09 23:45+0000\n" +"Last-Translator: José Alaguna \n" "Language-Team: Turkish (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/tr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -63,35 +64,35 @@ msgstr "Geçersiz token." #: authtoken/apps.py:7 msgid "Auth Token" -msgstr "" +msgstr "Kimlik doğrulama belirteci" #: authtoken/models.py:21 msgid "Key" -msgstr "" +msgstr "Anahtar" #: authtoken/models.py:23 msgid "User" -msgstr "" +msgstr "Kullanan" #: authtoken/models.py:24 msgid "Created" -msgstr "" +msgstr "Oluşturulan" #: authtoken/models.py:33 msgid "Token" -msgstr "" +msgstr "İşaret" #: authtoken/models.py:34 msgid "Tokens" -msgstr "" +msgstr "İşaretler" #: authtoken/serializers.py:8 msgid "Username" -msgstr "" +msgstr "Kullanıcı adı" #: authtoken/serializers.py:9 msgid "Password" -msgstr "" +msgstr "Şifre" #: authtoken/serializers.py:20 msgid "User account is disabled." @@ -334,7 +335,7 @@ msgstr "Sözlük tipi bir değişken beklenirken \"{input_type}\" tipi bir deği #: fields.py:1538 msgid "Value must be valid JSON." -msgstr "Değer geçerli bir JSON olmalı" +msgstr "Değer geçerli bir JSON olmalı." #: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" @@ -342,7 +343,7 @@ msgstr "Gönder" #: pagination.py:189 msgid "Invalid page." -msgstr "" +msgstr "Geçersiz sayfa." #: pagination.py:407 msgid "Invalid cursor" diff --git a/rest_framework/locale/tr_TR/LC_MESSAGES/django.mo b/rest_framework/locale/tr_TR/LC_MESSAGES/django.mo index 5d4511a4f2b3e5ef48ae9d8a5ada9739c4ac5e34..5fa743b50e80374a743a42495f008818753c0db2 100644 GIT binary patch literal 10520 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 M+{s%Ni7F-j9~)mM761SM delta 610 zcmYk&%WD%+6u{vV?ZZTE9)=dQ)k(C$CYUBH{%L zSFSDvvvH{|q_ntlCmL`g*nL4naN(ceuD_Fp;(;^Yeat=g%)Gw$D%ty)8QT^jL(Y;P z$T_kR<3a4=6z*XiKjT&WhdIoj5GmqK)b~O>k1s~^K3-=29<~1Wk$;cYdopoSWQ>g> zCU6n885=8j2Zsf5pG*6gX8qwfP2zJb;U_HMAI#&K36T^o;uTy*J?CK=ALE?#MBWYu zA_FY2@f)>~_#{Ozi+V7JHrDVW21q}{;7;L~(3`L^|BZgiX7z-c+|V6QBL-DiGaMV{+=uHSV1va#nI=1bwy EA+5(+0{{R3 diff --git a/rest_framework/locale/tr_TR/LC_MESSAGES/django.po b/rest_framework/locale/tr_TR/LC_MESSAGES/django.po index 9ead5da4a..efbd689e6 100644 --- a/rest_framework/locale/tr_TR/LC_MESSAGES/django.po +++ b/rest_framework/locale/tr_TR/LC_MESSAGES/django.po @@ -3,14 +3,14 @@ # This file is distributed under the same license as the PACKAGE package. # # Translators: -# José Alaguna , 2015 +# José Alaguna , 2015-2016 msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-03-01 18:38+0100\n" -"PO-Revision-Date: 2016-03-01 17:38+0000\n" -"Last-Translator: Xavier Ordoquy \n" +"PO-Revision-Date: 2016-03-09 23:48+0000\n" +"Last-Translator: José Alaguna \n" "Language-Team: Turkish (Turkey) (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/tr_TR/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -20,15 +20,15 @@ msgstr "" #: authentication.py:71 msgid "Invalid basic header. No credentials provided." -msgstr "" +msgstr "Geçersiz yetkilendirme başlığı. Gerekli uygunluk kriterleri sağlanmamış." #: authentication.py:74 msgid "Invalid basic header. Credentials string should not contain spaces." -msgstr "" +msgstr "Geçersiz yetkilendirme başlığı. Uygunluk kriterine ait veri boşluk karakteri içermemeli." #: authentication.py:80 msgid "Invalid basic header. Credentials not correctly base64 encoded." -msgstr "" +msgstr "Geçersiz yetkilendirme başlığı. Uygunluk kriterleri base64 formatına uygun olarak kodlanmamış." #: authentication.py:97 msgid "Invalid username/password." @@ -36,20 +36,20 @@ msgstr "Geçersiz kullanıcı adı / şifre." #: authentication.py:100 authentication.py:195 msgid "User inactive or deleted." -msgstr "" +msgstr "Kullanıcı aktif değil ya da silinmiş" #: authentication.py:173 msgid "Invalid token header. No credentials provided." -msgstr "" +msgstr "Geçersiz token başlığı. Kimlik bilgileri eksik." #: authentication.py:176 msgid "Invalid token header. Token string should not contain spaces." -msgstr "" +msgstr "Geçersiz token başlığı. Token'da boşluk olmamalı." #: authentication.py:182 msgid "" "Invalid token header. Token string should not contain invalid characters." -msgstr "" +msgstr "Geçersiz token başlığı. Token geçersiz karakter içermemeli." #: authentication.py:192 msgid "Invalid token." @@ -57,67 +57,67 @@ msgstr "Geçersiz simge." #: authtoken/apps.py:7 msgid "Auth Token" -msgstr "" +msgstr "Kimlik doğrulama belirteci" #: authtoken/models.py:21 msgid "Key" -msgstr "" +msgstr "Anahtar" #: authtoken/models.py:23 msgid "User" -msgstr "" +msgstr "Kullanan" #: authtoken/models.py:24 msgid "Created" -msgstr "" +msgstr "Oluşturulan" #: authtoken/models.py:33 msgid "Token" -msgstr "" +msgstr "İşaret" #: authtoken/models.py:34 msgid "Tokens" -msgstr "" +msgstr "İşaretler" #: authtoken/serializers.py:8 msgid "Username" -msgstr "" +msgstr "Kullanıcı adı" #: authtoken/serializers.py:9 msgid "Password" -msgstr "" +msgstr "Şifre" #: authtoken/serializers.py:20 msgid "User account is disabled." -msgstr "" +msgstr "Kullanıcı hesabı devre dışı bırakılmış." #: authtoken/serializers.py:23 msgid "Unable to log in with provided credentials." -msgstr "" +msgstr "Verilen bilgiler ile giriş sağlanamadı." #: authtoken/serializers.py:26 msgid "Must include \"username\" and \"password\"." -msgstr "" +msgstr "\"Kullanıcı Adı\" ve \"Parola\" eklenmeli." #: exceptions.py:49 msgid "A server error occurred." -msgstr "" +msgstr "Sunucu hatası oluştu." #: exceptions.py:84 msgid "Malformed request." -msgstr "" +msgstr "Bozuk istek." #: exceptions.py:89 msgid "Incorrect authentication credentials." -msgstr "" +msgstr "Giriş bilgileri hatalı." #: exceptions.py:94 msgid "Authentication credentials were not provided." -msgstr "" +msgstr "Giriş bilgileri verilmedi." #: exceptions.py:99 msgid "You do not have permission to perform this action." -msgstr "" +msgstr "Bu işlemi yapmak için izniniz bulunmuyor." #: exceptions.py:104 views.py:81 msgid "Not found." @@ -126,217 +126,217 @@ msgstr "Bulunamadı." #: exceptions.py:109 #, python-brace-format msgid "Method \"{method}\" not allowed." -msgstr "" +msgstr "\"{method}\" metoduna izin verilmiyor." #: exceptions.py:120 msgid "Could not satisfy the request Accept header." -msgstr "" +msgstr "İsteğe ait Accept başlık bilgisi yanıt verilecek başlık bilgileri arasında değil." #: exceptions.py:132 #, python-brace-format msgid "Unsupported media type \"{media_type}\" in request." -msgstr "" +msgstr "İstekte desteklenmeyen medya tipi: \"{media_type}\"." #: exceptions.py:145 msgid "Request was throttled." -msgstr "" +msgstr "Üst üste çok fazla istek yapıldı." #: fields.py:266 relations.py:206 relations.py:239 validators.py:79 #: validators.py:162 msgid "This field is required." -msgstr "" +msgstr "Bu alan zorunlu." #: fields.py:267 msgid "This field may not be null." -msgstr "" +msgstr "Bu alan boş bırakılmamalı." #: fields.py:603 fields.py:634 #, python-brace-format msgid "\"{input}\" is not a valid boolean." -msgstr "" +msgstr "\"{input}\" geçerli bir boolean değil." #: fields.py:669 msgid "This field may not be blank." -msgstr "" +msgstr "Bu alan boş bırakılmamalı." #: fields.py:670 fields.py:1664 #, python-brace-format msgid "Ensure this field has no more than {max_length} characters." -msgstr "" +msgstr "Bu alanın {max_length} karakterden fazla karakter barındırmadığından emin olun." #: fields.py:671 #, python-brace-format msgid "Ensure this field has at least {min_length} characters." -msgstr "" +msgstr "Bu alanın en az {min_length} karakter barındırdığından emin olun." #: fields.py:708 msgid "Enter a valid email address." -msgstr "" +msgstr "Geçerli bir e-posta adresi girin." #: fields.py:719 msgid "This value does not match the required pattern." -msgstr "" +msgstr "Bu değer gereken düzenli ifade deseni ile uyuşmuyor." #: fields.py:730 msgid "" "Enter a valid \"slug\" consisting of letters, numbers, underscores or " "hyphens." -msgstr "" +msgstr "Harf, rakam, altçizgi veya tireden oluşan geçerli bir \"slug\" giriniz." #: fields.py:742 msgid "Enter a valid URL." -msgstr "" +msgstr "Geçerli bir URL girin." #: fields.py:755 #, python-brace-format msgid "\"{value}\" is not a valid UUID." -msgstr "" +msgstr "\"{value}\" geçerli bir UUID değil." #: fields.py:791 msgid "Enter a valid IPv4 or IPv6 address." -msgstr "" +msgstr "Geçerli bir IPv4 ya da IPv6 adresi girin." #: fields.py:816 msgid "A valid integer is required." -msgstr "" +msgstr "Geçerli bir tam sayı girin." #: fields.py:817 fields.py:852 fields.py:885 #, python-brace-format msgid "Ensure this value is less than or equal to {max_value}." -msgstr "" +msgstr "Değerin {max_value} değerinden küçük ya da eşit olduğundan emin olun." #: fields.py:818 fields.py:853 fields.py:886 #, python-brace-format msgid "Ensure this value is greater than or equal to {min_value}." -msgstr "" +msgstr "Değerin {min_value} değerinden büyük ya da eşit olduğundan emin olun." #: fields.py:819 fields.py:854 fields.py:890 msgid "String value too large." -msgstr "" +msgstr "String değeri çok uzun." #: fields.py:851 fields.py:884 msgid "A valid number is required." -msgstr "" +msgstr "Geçerli bir numara gerekiyor." #: fields.py:887 #, python-brace-format msgid "Ensure that there are no more than {max_digits} digits in total." -msgstr "" +msgstr "Toplamda {max_digits} haneden fazla hane olmadığından emin olun." #: fields.py:888 #, python-brace-format msgid "" "Ensure that there are no more than {max_decimal_places} decimal places." -msgstr "" +msgstr "Ondalık basamak değerinin {max_decimal_places} haneden fazla olmadığından emin olun." #: fields.py:889 #, python-brace-format msgid "" "Ensure that there are no more than {max_whole_digits} digits before the " "decimal point." -msgstr "" +msgstr "Ondalık ayracından önce {max_whole_digits} basamaktan fazla olmadığından emin olun." #: fields.py:1004 #, python-brace-format msgid "Datetime has wrong format. Use one of these formats instead: {format}." -msgstr "" +msgstr "Datetime alanı yanlış biçimde. {format} biçimlerinden birini kullanın." #: fields.py:1005 msgid "Expected a datetime but got a date." -msgstr "" +msgstr "Datetime değeri bekleniyor, ama date değeri geldi." #: fields.py:1082 #, python-brace-format msgid "Date has wrong format. Use one of these formats instead: {format}." -msgstr "" +msgstr "Tarih biçimi yanlış. {format} biçimlerinden birini kullanın." #: fields.py:1083 msgid "Expected a date but got a datetime." -msgstr "" +msgstr "Date tipi beklenmekteydi, fakat datetime tipi geldi." #: fields.py:1151 #, python-brace-format msgid "Time has wrong format. Use one of these formats instead: {format}." -msgstr "" +msgstr "Time biçimi yanlış. {format} biçimlerinden birini kullanın." #: fields.py:1215 #, python-brace-format msgid "Duration has wrong format. Use one of these formats instead: {format}." -msgstr "" +msgstr "Duration biçimi yanlış. {format} biçimlerinden birini kullanın." #: fields.py:1240 fields.py:1289 #, python-brace-format msgid "\"{input}\" is not a valid choice." -msgstr "" +msgstr "\"{input}\" geçerli bir seçim değil." #: fields.py:1243 relations.py:71 relations.py:442 #, python-brace-format msgid "More than {count} items..." -msgstr "" +msgstr "{count} elemandan daha fazla..." #: fields.py:1290 fields.py:1437 relations.py:438 serializers.py:520 #, python-brace-format msgid "Expected a list of items but got type \"{input_type}\"." -msgstr "" +msgstr "Elemanların listesi beklenirken \"{input_type}\" alındı." #: fields.py:1291 msgid "This selection may not be empty." -msgstr "" +msgstr "Bu seçim boş bırakılmamalı." #: fields.py:1328 #, python-brace-format msgid "\"{input}\" is not a valid path choice." -msgstr "" +msgstr "\"{input}\" geçerli bir yol seçimi değil." #: fields.py:1347 msgid "No file was submitted." -msgstr "" +msgstr "Hiçbir dosya verilmedi." #: fields.py:1348 msgid "" "The submitted data was not a file. Check the encoding type on the form." -msgstr "" +msgstr "Gönderilen veri dosya değil. Formdaki kodlama tipini kontrol edin." #: fields.py:1349 msgid "No filename could be determined." -msgstr "" +msgstr "Hiçbir dosya adı belirlenemedi." #: fields.py:1350 msgid "The submitted file is empty." -msgstr "" +msgstr "Gönderilen dosya boş." #: fields.py:1351 #, python-brace-format msgid "" "Ensure this filename has at most {max_length} characters (it has {length})." -msgstr "" +msgstr "Bu dosya adının en fazla {max_length} karakter uzunluğunda olduğundan emin olun. (şu anda {length} karakter)." #: fields.py:1399 msgid "" "Upload a valid image. The file you uploaded was either not an image or a " "corrupted image." -msgstr "" +msgstr "Geçerli bir resim yükleyin. Yüklediğiniz dosya resim değil ya da bozuk." #: fields.py:1438 relations.py:439 serializers.py:521 msgid "This list may not be empty." -msgstr "" +msgstr "Bu liste boş olmamalı." #: fields.py:1491 #, python-brace-format msgid "Expected a dictionary of items but got type \"{input_type}\"." -msgstr "" +msgstr "Sözlük tipi bir değişken beklenirken \"{input_type}\" tipi bir değişken alındı." #: fields.py:1538 msgid "Value must be valid JSON." -msgstr "" +msgstr "Değer geçerli bir JSON olmalı." #: filters.py:35 templates/rest_framework/filters/django_filter.html.py:5 msgid "Submit" -msgstr "" +msgstr "Gönder" #: pagination.py:189 msgid "Invalid page." -msgstr "" +msgstr "Geçersiz sayfa." #: pagination.py:407 msgid "Invalid cursor" @@ -345,12 +345,12 @@ msgstr "Geçersiz imleç." #: relations.py:207 #, python-brace-format msgid "Invalid pk \"{pk_value}\" - object does not exist." -msgstr "" +msgstr "Geçersiz pk \"{pk_value}\" - obje bulunamadı." #: relations.py:208 #, python-brace-format msgid "Incorrect type. Expected pk value, received {data_type}." -msgstr "" +msgstr "Hatalı tip. Pk değeri beklenirken, alınan {data_type}." #: relations.py:240 msgid "Invalid hyperlink - No URL match." @@ -367,12 +367,12 @@ msgstr "Geçersiz hyper link - Nesne yok.." #: relations.py:243 #, python-brace-format msgid "Incorrect type. Expected URL string, received {data_type}." -msgstr "" +msgstr "Hatalı tip. URL metni bekleniyor, {data_type} alındı." #: relations.py:402 #, python-brace-format msgid "Object with {slug_name}={value} does not exist." -msgstr "" +msgstr "{slug_name}={value} değerini taşıyan obje bulunamadı." #: relations.py:403 msgid "Invalid value." @@ -386,20 +386,20 @@ msgstr "Geçersiz veri. Bir sözlük bekleniyor, ama var {datatype}." #: templates/rest_framework/admin.html:118 #: templates/rest_framework/base.html:128 msgid "Filters" -msgstr "" +msgstr "Filtreler" #: templates/rest_framework/filters/django_filter.html:2 #: templates/rest_framework/filters/django_filter_crispyforms.html:4 msgid "Field filters" -msgstr "" +msgstr "Alan filtreleri" #: templates/rest_framework/filters/ordering.html:3 msgid "Ordering" -msgstr "" +msgstr "Sıralama" #: templates/rest_framework/filters/search.html:2 msgid "Search" -msgstr "" +msgstr "Arama" #: templates/rest_framework/horizontal/radio.html:2 #: templates/rest_framework/inline/radio.html:2 @@ -411,7 +411,7 @@ msgstr "Hiç kimse" #: templates/rest_framework/inline/select_multiple.html:2 #: templates/rest_framework/vertical/select_multiple.html:2 msgid "No items to select." -msgstr "" +msgstr "Seçenek yok." #: validators.py:24 msgid "This field must be unique." @@ -425,17 +425,17 @@ msgstr "{field_names} alanları benzersiz bir set yapmak gerekir." #: validators.py:226 #, python-brace-format msgid "This field must be unique for the \"{date_field}\" date." -msgstr "" +msgstr "Bu alan \"{date_field}\" tarihine göre eşsiz olmalı." #: validators.py:241 #, python-brace-format msgid "This field must be unique for the \"{date_field}\" month." -msgstr "" +msgstr "Bu alan \"{date_field}\" ayına göre eşsiz olmalı." #: validators.py:254 #, python-brace-format msgid "This field must be unique for the \"{date_field}\" year." -msgstr "" +msgstr "Bu alan \"{date_field}\" yılına göre eşsiz olmalı." #: versioning.py:42 msgid "Invalid version in \"Accept\" header." From f0fc339278fa8a8b61423c9d60c23dbd372af0ed Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Mon, 14 Mar 2016 08:40:00 +0100 Subject: [PATCH 036/457] Release date update. --- docs/topics/release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 9fa399670..f2187a35c 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -42,7 +42,7 @@ You can determine your currently installed version using `pip freeze`: ### 3.3.3 -**Date**: [7th March 2016][3.3.3-milestone]. +**Date**: [14th March 2016][3.3.3-milestone]. * Remove version string from templates. Thanks to @blag for the report and fixes. ([#3878][gh3878], [#3913][gh3913], [#3912][gh3912]) * Fixes vertical html layout for `BooleanField`. Thanks to Mikalai Radchuk for the fix. ([#3910][gh3910]) From 03270431edc8ce88938a2d8495fe125867807cca Mon Sep 17 00:00:00 2001 From: Mohamad Nour Chawich Date: Sun, 20 Mar 2016 21:46:37 +0100 Subject: [PATCH 037/457] Reorder initializing the view Determining the version and performing content negotiation should be done before ensuring the permission of the request. The reason is that these information can be used in handling the exceptions. For example different versions may return different error scheme. Also, the rendering class can be used to determine how to exception handler response should be rendered. --- rest_framework/views.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 56e3c4e49..c13e74447 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -372,11 +372,6 @@ class APIView(View): """ self.format_kwarg = self.get_format_suffix(**kwargs) - # Ensure that the incoming request is permitted - self.perform_authentication(request) - self.check_permissions(request) - self.check_throttles(request) - # Perform content negotiation and store the accepted info on the request neg = self.perform_content_negotiation(request) request.accepted_renderer, request.accepted_media_type = neg @@ -385,6 +380,11 @@ class APIView(View): version, scheme = self.determine_version(request, *args, **kwargs) request.version, request.versioning_scheme = version, scheme + # Ensure that the incoming request is permitted + self.perform_authentication(request) + self.check_permissions(request) + self.check_throttles(request) + def finalize_response(self, request, response, *args, **kwargs): """ Returns the final response object. From 91e869750ebd2a614a76f74954676ef433e05cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Mon, 21 Mar 2016 08:57:43 +0000 Subject: [PATCH 038/457] Fix typo --- docs/topics/3.1-announcement.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/3.1-announcement.md b/docs/topics/3.1-announcement.md index 80d4007eb..46a67e3fc 100644 --- a/docs/topics/3.1-announcement.md +++ b/docs/topics/3.1-announcement.md @@ -110,7 +110,7 @@ When per-request internationalization is enabled, client requests will respect t "detail": "No se ha podido satisfacer la solicitud de cabecera de Accept." } -Note that the structure of the error responses is still the same. We still have a `details` key in the response. If needed you can modify this behavior too, by using a [custom exception handler][custom-exception-handler]. +Note that the structure of the error responses is still the same. We still have a `detail` key in the response. If needed you can modify this behavior too, by using a [custom exception handler][custom-exception-handler]. We include built-in translations both for standard exception cases, and for serializer validation errors. From 3785281d4c0830cdeac901e222e667eff42cfa54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Mon, 21 Mar 2016 10:07:47 +0000 Subject: [PATCH 039/457] Add missing paginator in docs --- docs/topics/3.2-announcement.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/3.2-announcement.md b/docs/topics/3.2-announcement.md index f0cc09a46..dc439f110 100644 --- a/docs/topics/3.2-announcement.md +++ b/docs/topics/3.2-announcement.md @@ -54,7 +54,7 @@ The following pagination view attributes and settings have been moved into attri * `view.max_paginate_by` - Use `paginator.max_page_size` instead. * `settings.PAGINATE_BY` - Use `paginator.page_size` instead. * `settings.PAGINATE_BY_PARAM` - Use `paginator.page_size_query_param` instead. -* `settings.MAX_PAGINATE_BY` - Use `max_page_size` instead. +* `settings.MAX_PAGINATE_BY` - Use `paginator.max_page_size` instead. ## Modifications to list behaviors From 0056703fe8afd5f2dfb8fe5eed4fb66c1ea9aa46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20Gro=C3=9F?= Date: Mon, 21 Mar 2016 10:23:34 +0000 Subject: [PATCH 040/457] Fix code sample indention --- 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 f704a3247..65ea2b3e6 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -54,7 +54,7 @@ Or apply the style globally, using the `DEFAULT_PAGINATION_CLASS` settings key. REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'apps.core.pagination.StandardResultsSetPagination' - } + } --- From 101c178b8ccfd133581036c22ae7a1d3b42e4546 Mon Sep 17 00:00:00 2001 From: "S. Andrew Sheppard" Date: Thu, 24 Mar 2016 11:55:19 -0500 Subject: [PATCH 041/457] links to html-json-forms package --- docs/api-guide/serializers.md | 5 +++++ docs/topics/third-party-resources.md | 2 ++ 2 files changed, 7 insertions(+) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 6a5519930..78d43a535 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -1080,6 +1080,9 @@ The [django-rest-framework-hstore][django-rest-framework-hstore] package provide The [dynamic-rest][dynamic-rest] package extends the ModelSerializer and ModelViewSet interfaces, adding API query parameters for filtering, sorting, and including / excluding all fields and relationships defined by your serializers. +## HTML JSON Forms +The [html-json-forms][html-json-forms] package provides an algorithm and serializer for processing `
` submissions per the (inactive) [HTML JSON Form specification][json-form-spec]. The serializer facilitates processing of arbitrarily nested JSON structures within HTML. For example, `` will be interpreted as `{"items": [{"id": "5"}]}`. + [cite]: https://groups.google.com/d/topic/django-users/sVFaOfQi4wY/discussion [relations]: relations.md [model-managers]: https://docs.djangoproject.com/en/dev/topics/db/managers/ @@ -1092,3 +1095,5 @@ The [dynamic-rest][dynamic-rest] package extends the ModelSerializer and ModelVi [django-rest-framework-hstore]: https://github.com/djangonauts/django-rest-framework-hstore [django-hstore]: https://github.com/djangonauts/django-hstore [dynamic-rest]: https://github.com/AltSchool/dynamic-rest +[html-json-forms]: https://github.com/wq/html-json-forms +[json-form-spec]: https://www.w3.org/TR/html-json-forms/ diff --git a/docs/topics/third-party-resources.md b/docs/topics/third-party-resources.md index ed15269ed..c4ac88255 100644 --- a/docs/topics/third-party-resources.md +++ b/docs/topics/third-party-resources.md @@ -203,6 +203,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [djangorestframework-gis][djangorestframework-gis] - Geographic add-ons * [djangorestframework-hstore][djangorestframework-hstore] - Serializer class to support django-hstore DictionaryField model field and its schema-mode feature. * [djangorestframework-jsonapi][djangorestframework-jsonapi] - Provides a parser, renderer, serializers, and other tools to help build an API that is compliant with the jsonapi.org spec. +* [html-json-forms][html-json-forms]: Provides an algorithm and serializer to process HTML JSON Form submissions per the (inactive) spec. ### Serializer fields @@ -355,3 +356,4 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [drf-haystack]: http://drf-haystack.readthedocs.org/en/latest/ [django-rest-framework-version-transforms]: https://github.com/mrhwick/django-rest-framework-version-transforms [djangorestframework-jsonapi]: https://github.com/django-json-api/django-rest-framework-json-api +[html-json-forms]: https://github.com/wq/html-json-forms From 3e5a1397d72045bf9fe84fbbefa426ee31f5febc Mon Sep 17 00:00:00 2001 From: Tom Viner Date: Thu, 24 Mar 2016 20:37:38 +0000 Subject: [PATCH 042/457] remove trailing slash from cramer cursor link --- docs/topics/3.1-announcement.md | 2 +- rest_framework/pagination.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/topics/3.1-announcement.md b/docs/topics/3.1-announcement.md index 46a67e3fc..9eb93fcd7 100644 --- a/docs/topics/3.1-announcement.md +++ b/docs/topics/3.1-announcement.md @@ -30,7 +30,7 @@ Note that as a result of this work a number of settings keys and generic view at Until now, there has only been a single built-in pagination style in REST framework. We now have page, limit/offset and cursor based schemes included by default. -The cursor based pagination scheme is particularly smart, and is a better approach for clients iterating through large or frequently changing result sets. The scheme supports paging against non-unique indexes, by using both cursor and limit/offset information. It also allows for both forward and reverse cursor pagination. Much credit goes to David Cramer for [this blog post](http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api/) on the subject. +The cursor based pagination scheme is particularly smart, and is a better approach for clients iterating through large or frequently changing result sets. The scheme supports paging against non-unique indexes, by using both cursor and limit/offset information. It also allows for both forward and reverse cursor pagination. Much credit goes to David Cramer for [this blog post](http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api) on the subject. #### Pagination controls in the browsable API. diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 1051dc5b2..851ddd809 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -400,7 +400,7 @@ class CursorPagination(BasePagination): """ The cursor pagination implementation is neccessarily complex. For an overview of the position/offset style we use, see this post: - http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api/ + http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api """ cursor_query_param = 'cursor' page_size = api_settings.PAGE_SIZE From 3fdd6e1db584c472795d9cfdd7edc764228f33e2 Mon Sep 17 00:00:00 2001 From: Anthony Lukach Date: Tue, 29 Mar 2016 10:50:12 -0600 Subject: [PATCH 043/457] Add .partial_update to ModelViewSet documentation The ModelViewSet inherits from the UpdateModelMixin, which provides the `.partial_update` method. This should be reflected in the documentation. --- 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 0452a0f7b..e6df17f51 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -179,7 +179,7 @@ In order to use a `GenericViewSet` class you'll override the class and either mi The `ModelViewSet` class inherits from `GenericAPIView` and includes implementations for various actions, by mixing in the behavior of the various mixin classes. -The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`, `.create()`, `.update()`, and `.destroy()`. +The actions provided by the `ModelViewSet` class are `.list()`, `.retrieve()`, `.create()`, `.update()`, `.partial_update()`, and `.destroy()`. #### Example From 5cc6c254d5d2dfe6a024d0ec64617aac693ccfac Mon Sep 17 00:00:00 2001 From: auvipy Date: Sat, 2 Apr 2016 23:02:44 +0600 Subject: [PATCH 044/457] updated minor django versions on tox --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 9af1048d9..39742d2b3 100644 --- a/tox.ini +++ b/tox.ini @@ -13,8 +13,8 @@ setenv = PYTHONDONTWRITEBYTECODE=1 PYTHONWARNINGS=once deps = - django18: Django==1.8.11 - django19: Django==1.9.4 + django18: Django==1.8.12 + django19: Django==1.9.5 -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From c22b92a66ca3bb740b8561f660883149953026e0 Mon Sep 17 00:00:00 2001 From: Nitesh Lohchab Date: Sun, 3 Apr 2016 00:07:45 +0530 Subject: [PATCH 045/457] type('') to str --- rest_framework/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 0ca90873e..0cd2dd0db 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -19,7 +19,7 @@ def get_authorization_header(request): Hide some test client ickyness where the header can be unicode. """ auth = request.META.get('HTTP_AUTHORIZATION', b'') - if isinstance(auth, type('')): + if isinstance(auth, str): # Work around django test client oddness auth = auth.encode(HTTP_HEADER_ENCODING) return auth From 09aa8f76c44197550e86dfd864fee61425346db8 Mon Sep 17 00:00:00 2001 From: Nitesh Lohchab Date: Sun, 3 Apr 2016 18:39:32 +0530 Subject: [PATCH 046/457] python2.x and 3.x compatible --- rest_framework/authentication.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 0cd2dd0db..23ef49d69 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -7,6 +7,7 @@ import base64 from django.contrib.auth import authenticate, get_user_model from django.middleware.csrf import CsrfViewMiddleware +from django.utils.six import string_types from django.utils.translation import ugettext_lazy as _ from rest_framework import HTTP_HEADER_ENCODING, exceptions @@ -19,7 +20,7 @@ def get_authorization_header(request): Hide some test client ickyness where the header can be unicode. """ auth = request.META.get('HTTP_AUTHORIZATION', b'') - if isinstance(auth, str): + if isinstance(auth, string_types): # Work around django test client oddness auth = auth.encode(HTTP_HEADER_ENCODING) return auth From 763aab6b457370897d32c36d895a70cb0e980c28 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 5 Apr 2016 16:29:16 +0200 Subject: [PATCH 047/457] Fix the string_types / text_types confusion introduced in #4025 --- rest_framework/authentication.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 23ef49d69..eb8140643 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -7,7 +7,7 @@ import base64 from django.contrib.auth import authenticate, get_user_model from django.middleware.csrf import CsrfViewMiddleware -from django.utils.six import string_types +from django.utils.six import text_types from django.utils.translation import ugettext_lazy as _ from rest_framework import HTTP_HEADER_ENCODING, exceptions @@ -20,7 +20,7 @@ def get_authorization_header(request): Hide some test client ickyness where the header can be unicode. """ auth = request.META.get('HTTP_AUTHORIZATION', b'') - if isinstance(auth, string_types): + if isinstance(auth, text_types): # Work around django test client oddness auth = auth.encode(HTTP_HEADER_ENCODING) return auth From f1a384b61bdfe61bc45e71b25089609043c3d069 Mon Sep 17 00:00:00 2001 From: Raphael Gyory Date: Wed, 6 Apr 2016 16:58:15 +0200 Subject: [PATCH 048/457] Add Django Rest Messaging in Third party packages --- docs/topics/third-party-resources.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/topics/third-party-resources.md b/docs/topics/third-party-resources.md index c4ac88255..a12a24616 100644 --- a/docs/topics/third-party-resources.md +++ b/docs/topics/third-party-resources.md @@ -253,6 +253,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [django-rest-framework-braces][django-rest-framework-braces] - Collection of utilities for working with Django Rest Framework. The most notable ones are [FormSerializer](https://django-rest-framework-braces.readthedocs.org/en/latest/overview.html#formserializer) and [SerializerForm](https://django-rest-framework-braces.readthedocs.org/en/latest/overview.html#serializerform), which are adapters between DRF serializers and Django forms. * [drf-haystack][drf-haystack] - Haystack search for Django Rest Framework * [django-rest-framework-version-transforms][django-rest-framework-version-transforms] - Enables the use of delta transformations for versioning of DRF resource representations. +* [django-rest-messaging][django-rest-messaging], [django-rest-messaging-centrifugo][django-rest-messaging-centrifugo] and [django-rest-messaging-js][django-rest-messaging-js] - A real-time pluggable messaging service using DRM. ## Other Resources From b8701015818494109441310a5a88ebea901d2086 Mon Sep 17 00:00:00 2001 From: Raphael Gyory Date: Wed, 6 Apr 2016 17:00:26 +0200 Subject: [PATCH 049/457] Update third-party-resources.md --- docs/topics/third-party-resources.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/topics/third-party-resources.md b/docs/topics/third-party-resources.md index a12a24616..cc29c4334 100644 --- a/docs/topics/third-party-resources.md +++ b/docs/topics/third-party-resources.md @@ -358,3 +358,6 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [django-rest-framework-version-transforms]: https://github.com/mrhwick/django-rest-framework-version-transforms [djangorestframework-jsonapi]: https://github.com/django-json-api/django-rest-framework-json-api [html-json-forms]: https://github.com/wq/html-json-forms +[django-rest-messaging]: https://github.com/raphaelgyory/django-rest-messaging +[django-rest-messaging-centrifugo]: https://github.com/raphaelgyory/django-rest-messaging-centrifugo +[django-rest-messaging-js]: https://github.com/raphaelgyory/django-rest-messaging-js From 78e4ea0d6e88a7b355ec3e790ef0e8f95859ca2d Mon Sep 17 00:00:00 2001 From: Jonathan Liuti Date: Thu, 7 Apr 2016 17:24:26 +0200 Subject: [PATCH 050/457] No auth view failing permission should raise 403 A view with no `authentication_classes` set and that fails a permission check should raise a 403 with the message from the failing permission. --- rest_framework/views.py | 2 +- tests/test_authentication.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index c13e74447..41d108e53 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -162,7 +162,7 @@ class APIView(View): """ If request is not permitted, determine what kind of exception to raise. """ - if not request.successful_authenticator: + if request.authenticators and not request.successful_authenticator: raise exceptions.NotAuthenticated() raise exceptions.PermissionDenied(detail=message) diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 285a3210c..70eea3132 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -321,3 +321,28 @@ class FailingAuthAccessedInRenderer(TestCase): response = self.view(request) content = response.render().content self.assertEqual(content, b'not authenticated') + + +class NoAuthenticationClassesTests(TestCase): + def test_permission_message_with_no_authentication_classes(self): + """ + An unauthenticated request made against a view that containes no + `authentication_classes` but do contain `permissions_classes` the error + code returned should be 403 with the exception's message. + """ + + class DummyPermission(permissions.BasePermission): + message = 'Dummy permission message' + + def has_permission(self, request, view): + return False + + request = factory.get('/') + view = MockView.as_view( + authentication_classes=(), + permission_classes=(DummyPermission,), + ) + response = view(request) + self.assertEqual(response.status_code, + status.HTTP_403_FORBIDDEN) + self.assertEqual(response.data, {'detail': 'Dummy permission message'}) From 019c6db759ed5aa765c26a895bbcc6953e62f423 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 7 Apr 2016 17:34:27 +0200 Subject: [PATCH 051/457] Fix the string_types / text_types confusion introduced in #4025 (#4035) --- rest_framework/authentication.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 23ef49d69..eb8140643 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -7,7 +7,7 @@ import base64 from django.contrib.auth import authenticate, get_user_model from django.middleware.csrf import CsrfViewMiddleware -from django.utils.six import string_types +from django.utils.six import text_types from django.utils.translation import ugettext_lazy as _ from rest_framework import HTTP_HEADER_ENCODING, exceptions @@ -20,7 +20,7 @@ def get_authorization_header(request): Hide some test client ickyness where the header can be unicode. """ auth = request.META.get('HTTP_AUTHORIZATION', b'') - if isinstance(auth, string_types): + if isinstance(auth, text_types): # Work around django test client oddness auth = auth.encode(HTTP_HEADER_ENCODING) return auth From 2622588b30c14a2bfab2838058138c7750cd8fd9 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Thu, 7 Apr 2016 18:00:17 +0200 Subject: [PATCH 052/457] Typo correction. --- rest_framework/authentication.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index eb8140643..63d302bc2 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -7,7 +7,7 @@ import base64 from django.contrib.auth import authenticate, get_user_model from django.middleware.csrf import CsrfViewMiddleware -from django.utils.six import text_types +from django.utils.six import text_type from django.utils.translation import ugettext_lazy as _ from rest_framework import HTTP_HEADER_ENCODING, exceptions @@ -20,7 +20,7 @@ def get_authorization_header(request): Hide some test client ickyness where the header can be unicode. """ auth = request.META.get('HTTP_AUTHORIZATION', b'') - if isinstance(auth, text_types): + if isinstance(auth, text_type): # Work around django test client oddness auth = auth.encode(HTTP_HEADER_ENCODING) return auth From 95418eb8ac9b3efd57ccca5c7159ee0bfa8effb3 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 8 Apr 2016 15:43:10 +0200 Subject: [PATCH 053/457] Add the medium collection related to Django REST framework. (#4043) --- docs/topics/third-party-resources.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/third-party-resources.md b/docs/topics/third-party-resources.md index cc29c4334..003366ffe 100644 --- a/docs/topics/third-party-resources.md +++ b/docs/topics/third-party-resources.md @@ -279,6 +279,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [Web API performance: profiling Django REST framework][web-api-performance-profiling-django-rest-framework] * [API Development with Django and Django REST Framework][api-development-with-django-and-django-rest-framework] +* [Blog posts about Django REST framework][medium-django-rest-framework] ### Documentations * [Classy Django REST Framework][cdrf.co] @@ -361,3 +362,4 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [django-rest-messaging]: https://github.com/raphaelgyory/django-rest-messaging [django-rest-messaging-centrifugo]: https://github.com/raphaelgyory/django-rest-messaging-centrifugo [django-rest-messaging-js]: https://github.com/raphaelgyory/django-rest-messaging-js +[medium-django-rest-framework]: https://medium.com/django-rest-framework From d87f2bc7b67c89bf556dfbadf1fb48aa0dc2ca6c Mon Sep 17 00:00:00 2001 From: Simon Charette Date: Fri, 8 Apr 2016 10:37:23 -0400 Subject: [PATCH 054/457] OrderingFilter adjustements (#3983) * Made sure the OrderingFilter relies on Field.verbose_name. * Marked OrderingFilter's order labels for translation. --- rest_framework/filters.py | 7 +++---- tests/test_filters.py | 15 ++++++++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index a4542df10..2a25378a0 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -240,8 +240,7 @@ class OrderingFilter(BaseFilterBackend): elif valid_fields == '__all__': # View explicitly allows filtering on any model field valid_fields = [ - (field.name, getattr(field, 'label', field.name.title())) - for field in queryset.model._meta.fields + (field.name, field.verbose_name) for field in queryset.model._meta.fields ] valid_fields += [ (key, key.title().split('__')) @@ -272,8 +271,8 @@ class OrderingFilter(BaseFilterBackend): current = None if current is None else current[0] options = [] for key, label in self.get_valid_fields(queryset, view): - options.append((key, '%s - ascending' % label)) - options.append(('-' + key, '%s - descending' % label)) + options.append((key, '%s - %s' % (label, _('ascending')))) + options.append(('-' + key, '%s - %s' % (label, _('descending')))) return { 'request': request, 'current': current, diff --git a/tests/test_filters.py b/tests/test_filters.py index 729a7b75b..b72d95691 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -499,7 +499,7 @@ class SearchFilterM2MTests(TestCase): class OrderingFilterModel(models.Model): - title = models.CharField(max_length=20) + title = models.CharField(max_length=20, verbose_name='verbose title') text = models.CharField(max_length=100) @@ -741,6 +741,19 @@ class OrderingFilterTests(TestCase): reload_module(filters) + def test_get_template_context(self): + class OrderingListView(generics.ListAPIView): + ordering_fields = '__all__' + serializer_class = OrderingFilterSerializer + queryset = OrderingFilterModel.objects.all() + filter_backends = (filters.OrderingFilter,) + + request = factory.get('/', {'ordering': 'title'}, HTTP_ACCEPT='text/html') + view = OrderingListView.as_view() + response = view(request) + + self.assertContains(response, 'verbose title') + class SensitiveOrderingFilterModel(models.Model): username = models.CharField(max_length=20) From 08dad04b1989c69295a3536e186e480fde42e460 Mon Sep 17 00:00:00 2001 From: Phivos Stylianides Date: Mon, 11 Apr 2016 16:13:11 +0300 Subject: [PATCH 055/457] Fix warnings when running tests (#4047) --- .gitignore | 1 + tests/browsable_api/test_browsable_nested_api.py | 4 ++-- tests/test_model_serializer.py | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index e9222c2da..41768084c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ /*.egg-info/ /env/ MANIFEST +coverage.* !.gitignore !.travis.yml diff --git a/tests/browsable_api/test_browsable_nested_api.py b/tests/browsable_api/test_browsable_nested_api.py index dbbd11eea..9e5538efc 100644 --- a/tests/browsable_api/test_browsable_nested_api.py +++ b/tests/browsable_api/test_browsable_nested_api.py @@ -14,13 +14,13 @@ class NestedSerializer(serializers.Serializer): two = serializers.IntegerField(max_value=10) -class TestNestedSerializerSerializer(serializers.Serializer): +class NestedSerializerTestSerializer(serializers.Serializer): nested = NestedSerializer() class NestedSerializersView(ListCreateAPIView): renderer_classes = (BrowsableAPIRenderer, ) - serializer_class = TestNestedSerializerSerializer + serializer_class = NestedSerializerTestSerializer queryset = [{'nested': {'one': 1, 'two': 2}}] diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 0179abc44..185130778 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -784,7 +784,7 @@ class TestBulkCreate(TestCase): assert serializer.data == data -class TestMetaClassModel(models.Model): +class MetaClassTestModel(models.Model): text = models.CharField(max_length=100) @@ -792,7 +792,7 @@ class TestSerializerMetaClass(TestCase): def test_meta_class_fields_option(self): class ExampleSerializer(serializers.ModelSerializer): class Meta: - model = TestMetaClassModel + model = MetaClassTestModel fields = 'text' with self.assertRaises(TypeError) as result: @@ -806,7 +806,7 @@ class TestSerializerMetaClass(TestCase): def test_meta_class_exclude_option(self): class ExampleSerializer(serializers.ModelSerializer): class Meta: - model = TestMetaClassModel + model = MetaClassTestModel exclude = 'text' with self.assertRaises(TypeError) as result: @@ -820,7 +820,7 @@ class TestSerializerMetaClass(TestCase): def test_meta_class_fields_and_exclude_options(self): class ExampleSerializer(serializers.ModelSerializer): class Meta: - model = TestMetaClassModel + model = MetaClassTestModel fields = ('text',) exclude = ('text',) From 9d9658f128476e72aedd62ee60ca2506c5e4bae1 Mon Sep 17 00:00:00 2001 From: Clinton Blackburn Date: Mon, 11 Apr 2016 23:04:20 -0400 Subject: [PATCH 056/457] Added support for custom CSRF cookie names Instead of hardcoding the CSRF cookie name, the value is passed to the template as a context variable, rendered as a JavaScript variable, and read by csrf.js. Fixes #4048 --- rest_framework/renderers.py | 4 +++- rest_framework/static/rest_framework/js/csrf.js | 2 +- rest_framework/templates/rest_framework/admin.html | 5 +++++ rest_framework/templates/rest_framework/base.html | 5 +++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 68af417da..53bbb1390 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -12,6 +12,7 @@ import json from collections import OrderedDict from django import forms +from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.core.paginator import Page from django.http.multipartparser import parse_header @@ -657,7 +658,8 @@ class BrowsableAPIRenderer(BaseRenderer): 'display_edit_forms': bool(response.status_code != 403), - 'api_settings': api_settings + 'api_settings': api_settings, + 'csrf_cookie_name': settings.CSRF_COOKIE_NAME, } return context diff --git a/rest_framework/static/rest_framework/js/csrf.js b/rest_framework/static/rest_framework/js/csrf.js index 4e8da0de5..73d1ef67d 100644 --- a/rest_framework/static/rest_framework/js/csrf.js +++ b/rest_framework/static/rest_framework/js/csrf.js @@ -33,7 +33,7 @@ function sameOrigin(url) { !(/^(\/\/|http:|https:).*/.test(url)); } -var csrftoken = getCookie('csrftoken'); +var csrftoken = getCookie(window.drf.csrfCookieName); $.ajaxSetup({ beforeSend: function(xhr, settings) { diff --git a/rest_framework/templates/rest_framework/admin.html b/rest_framework/templates/rest_framework/admin.html index a86adbc60..a21ea57be 100644 --- a/rest_framework/templates/rest_framework/admin.html +++ b/rest_framework/templates/rest_framework/admin.html @@ -230,6 +230,11 @@ {% if filter_form %}{{ filter_form }}{% endif %} {% block script %} + diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index eccadc3cc..21431b70c 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -258,6 +258,11 @@ {% block script %} + From 888e5c78bd0fafd261ad86441a52887a803ec165 Mon Sep 17 00:00:00 2001 From: Puneet Aggarwal Date: Tue, 19 Apr 2016 19:39:16 +0530 Subject: [PATCH 057/457] Update README.md (#4055) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9025e91b4..79b43416f 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![build-status-image]][travis] [![coverage-status-image]][codecov] [![pypi-version]][pypi] +[![Gitter](https://badges.gitter.im/tomchristie/django-rest-framework.svg)](https://gitter.im/tomchristie/django-rest-framework?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) **Awesome web-browsable Web APIs.** From 88c80fe2e901cf6607125b7bf57b4b2e084a4c7b Mon Sep 17 00:00:00 2001 From: Daniele Varrazzo Date: Wed, 27 Apr 2016 17:04:01 +0100 Subject: [PATCH 058/457] Fixed DecimalField arbitrary precision support (#4075) --- rest_framework/fields.py | 3 +++ tests/test_fields.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 2a08e09ff..5dcd546c0 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -992,6 +992,9 @@ class DecimalField(Field): """ Quantize the decimal value to the configured precision. """ + if self.decimal_places is None: + return value + context = decimal.getcontext().copy() context.prec = self.max_digits return value.quantize( diff --git a/tests/test_fields.py b/tests/test_fields.py index cefb45605..304de11ee 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -894,6 +894,21 @@ class TestNoStringCoercionDecimalField(FieldValues): ) +class TestNoDecimalPlaces(FieldValues): + valid_inputs = { + '0.12345': Decimal('0.12345'), + } + invalid_inputs = { + '0.1234567': ['Ensure that there are no more than 6 digits in total.'] + } + outputs = { + '1.2345': '1.2345', + '0': '0', + '1.1': '1.1', + } + field = serializers.DecimalField(max_digits=6, decimal_places=None) + + # Date & time serializers... class TestDateField(FieldValues): From a9bbb502cb0fb516deb444e1b58762cd3edb0c6d Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Fri, 29 Apr 2016 15:16:03 +0200 Subject: [PATCH 059/457] Remove references to South as we don't need it any longer. (#4085) * Remove references to South as we don't need it any longer. * Add a note about auth_token including Django migrations. --- docs/api-guide/authentication.md | 37 +------------------------------- 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 3d1cfd1e8..06d043b6c 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -128,11 +128,10 @@ To use the `TokenAuthentication` scheme you'll need to [configure the authentica --- -**Note:** Make sure to run `manage.py syncdb` after changing your settings. The `rest_framework.authtoken` app provides both Django (from v1.7) and South database migrations. See [Schema migrations](#schema-migrations) below. +**Note:** Make sure to run `manage.py migrate` after changing your settings. The `rest_framework.authtoken` app provides Django database migrations. --- - You'll also need to create tokens for your users. from rest_framework.authtoken.models import Token @@ -217,38 +216,6 @@ It is also possible to create Tokens manually through admin interface. In case y TokenAdmin.raw_id_fields = ('user',) -#### Schema migrations - -The `rest_framework.authtoken` app includes both Django native migrations (for Django versions >1.7) and South migrations (for Django versions <1.7) that will create the authtoken table. - ----- - -**Note**: From REST Framework v2.4.0 using South with Django <1.7 requires upgrading South v1.0+ - ----- - - -If you're using a [custom user model][custom-user-model] you'll need to make sure that any initial migration that creates the user table runs before the authtoken table is created. - -You can do so by inserting a `needed_by` attribute in your user migration: - - class Migration: - - needed_by = ( - ('authtoken', '0001_initial'), - ) - - def forwards(self): - ... - -For more details, see the [south documentation on dependencies][south-dependencies]. - -Also note that if you're using a `post_save` signal to create tokens, then the first time you create the database tables, you'll need to ensure any migrations are run prior to creating any superusers. For example: - - python manage.py syncdb --noinput # Won't create a superuser just yet, due to `--noinput`. - python manage.py migrate - python manage.py createsuperuser - ## SessionAuthentication This authentication scheme uses Django's default session backend for authentication. Session authentication is appropriate for AJAX clients that are running in the same session context as your website. @@ -392,8 +359,6 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a [throttling]: throttling.md [csrf-ajax]: https://docs.djangoproject.com/en/dev/ref/csrf/#ajax [mod_wsgi_official]: http://code.google.com/p/modwsgi/wiki/ConfigurationDirectives#WSGIPassAuthorization -[custom-user-model]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#specifying-a-custom-user-model -[south-dependencies]: http://south.readthedocs.org/en/latest/dependencies.html [django-oauth-toolkit-getting-started]: https://django-oauth-toolkit.readthedocs.org/en/latest/rest-framework/getting_started.html [django-rest-framework-oauth]: http://jpadilla.github.io/django-rest-framework-oauth/ [django-rest-framework-oauth-authentication]: http://jpadilla.github.io/django-rest-framework-oauth/authentication/ From 04ebb1ef04a788485ca10007dd9c91895d7c0792 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Tue, 3 May 2016 09:50:52 +0600 Subject: [PATCH 060/457] upgraded minor django version n tox --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 39742d2b3..8721af601 100644 --- a/tox.ini +++ b/tox.ini @@ -13,8 +13,8 @@ setenv = PYTHONDONTWRITEBYTECODE=1 PYTHONWARNINGS=once deps = - django18: Django==1.8.12 - django19: Django==1.9.5 + django18: Django==1.8.13 + django19: Django==1.9.6 -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From e19b21ecc547bc3318354802aeadb5d45c65d475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 3 May 2016 05:24:55 -0300 Subject: [PATCH 061/457] Handle incorrectly padded HTTP basic auth header (#4090) --- rest_framework/authentication.py | 3 ++- tests/test_authentication.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 63d302bc2..120be6165 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -4,6 +4,7 @@ Provides various authentication policies. from __future__ import unicode_literals import base64 +import binascii from django.contrib.auth import authenticate, get_user_model from django.middleware.csrf import CsrfViewMiddleware @@ -77,7 +78,7 @@ class BasicAuthentication(BaseAuthentication): try: auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':') - except (TypeError, UnicodeDecodeError): + except (TypeError, UnicodeDecodeError, binascii.Error): msg = _('Invalid basic header. Credentials not correctly base64 encoded.') raise exceptions.AuthenticationFailed(msg) diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 70eea3132..9aff7280b 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -85,6 +85,14 @@ class BasicAuthTests(TestCase): response = self.csrf_client.post('/basic/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, status.HTTP_200_OK) + def test_regression_handle_bad_base64_basic_auth_header(self): + """Ensure POSTing JSON over basic auth with incorrectly padded Base64 string is handled correctly""" + # regression test for issue in 'rest_framework.authentication.BasicAuthentication.authenticate' + # https://github.com/tomchristie/django-rest-framework/issues/4089 + auth = 'Basic =a=' + response = self.csrf_client.post('/basic/', {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + def test_post_form_failing_basic_auth(self): """Ensure POSTing form over basic auth without correct credentials fails""" response = self.csrf_client.post('/basic/', {'example': 'example'}) From 28c6d96af85dae4678f4cfc5932b0022c3017d1e Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Tue, 3 May 2016 14:25:27 +0600 Subject: [PATCH 062/457] upgraded minor django version n tox (#4091) --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 39742d2b3..8721af601 100644 --- a/tox.ini +++ b/tox.ini @@ -13,8 +13,8 @@ setenv = PYTHONDONTWRITEBYTECODE=1 PYTHONWARNINGS=once deps = - django18: Django==1.8.12 - django19: Django==1.9.5 + django18: Django==1.8.13 + django19: Django==1.9.6 -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From 5f52c4ff3eafdd90987c789d6f7b5b26aadfce2b Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Tue, 3 May 2016 15:52:05 +0600 Subject: [PATCH 063/457] added python3.5 for django 1.8 checks --- .travis.yml | 1 + tox.ini | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cf3cddfec..4c7c09dc0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ env: - TOX_ENV=py35-django19 - TOX_ENV=py34-django19 - TOX_ENV=py27-django19 + - TOX_ENV=py35-django18 - TOX_ENV=py34-django18 - TOX_ENV=py33-django18 - TOX_ENV=py32-django18 diff --git a/tox.ini b/tox.ini index 8721af601..120d2f340 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ addopts=--tb=short [tox] envlist = py27-{lint,docs}, - {py27,py32,py33,py34}-django18, + {py27,py32,py33,py34,py35}-django18, {py27,py34,py35}-django{19} [testenv] From 399e1c1dcfd9eb69794e63dd60d74c16f66e06ba Mon Sep 17 00:00:00 2001 From: Kyle Hornberg Date: Tue, 3 May 2016 09:53:55 -0500 Subject: [PATCH 064/457] Typo fix (#4094) --- docs/api-guide/relations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 31a78f28f..8695b2c1e 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -489,7 +489,7 @@ See the Django documentation on [reverse relationships][reverse-relationships] f ## Generic relationships -If you want to serialize a generic foreign key, you need to define a custom field, to determine explicitly how you want serialize the targets of the relationship. +If you want to serialize a generic foreign key, you need to define a custom field, to determine explicitly how you want to serialize the targets of the relationship. For example, given the following model for a tag, which has a generic relationship with other arbitrary models: From c355cdc585efd36302445ab99c6e7e5887383a4e Mon Sep 17 00:00:00 2001 From: Taylor Edmiston Date: Tue, 3 May 2016 14:20:45 -0400 Subject: [PATCH 065/457] Fix typo in permissions docs --- 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 bbac824da..5386e4df6 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -144,7 +144,7 @@ To use custom model permissions, override `DjangoModelPermissions` and set the ` #### Using with views that do not include a `queryset` attribute. -If you're using this permission with a view that uses an overridden `get_queryset()` method there may not be a `queryset` attribute on the view. In this case we suggest also marking the view with a sential queryset, so that this class can determine the required permissions. For example: +If you're using this permission with a view that uses an overridden `get_queryset()` method there may not be a `queryset` attribute on the view. In this case we suggest also marking the view with a sentinel queryset, so that this class can determine the required permissions. For example: queryset = User.objects.none() # Required for DjangoModelPermissions From ffdac0d93619b7ec6039b94ce0e563f0330faeb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Wed, 4 May 2016 11:53:34 +0200 Subject: [PATCH 066/457] TokenAuthentication: Allow custom keyword in the header (#4097) This allows subclassing TokenAuthentication and setting custom keyword, thus allowing the Authorization header to be for example: Bearer 956e252a-513c-48c5-92dd-bfddc364e812 It doesn't change the behavior of TokenAuthentication itself, it simply allows to reuse the logic of TokenAuthentication without the need of copy pasting the class and changing one hardcoded string. Related: #4080 --- docs/api-guide/authentication.md | 2 ++ rest_framework/authentication.py | 5 +++-- tests/test_authentication.py | 22 +++++++++++++++++----- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 06d043b6c..5568ed0c9 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -143,6 +143,8 @@ For clients to authenticate, the token key should be included in the `Authorizat Authorization: Token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b +**Note:** If you want to use a different keyword in the header, such as `Bearer`, simply subclass `TokenAuthentication` and set the `keyword` class variable. + If successfully authenticated, `TokenAuthentication` provides the following credentials. * `request.user` will be a Django `User` instance. diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index 120be6165..cb9608a3c 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -150,6 +150,7 @@ class TokenAuthentication(BaseAuthentication): Authorization: Token 401f7ac837da42b97f613d789819ff93537bee6a """ + keyword = 'Token' model = None def get_model(self): @@ -168,7 +169,7 @@ class TokenAuthentication(BaseAuthentication): def authenticate(self, request): auth = get_authorization_header(request).split() - if not auth or auth[0].lower() != b'token': + if not auth or auth[0].lower() != self.keyword.lower().encode(): return None if len(auth) == 1: @@ -199,4 +200,4 @@ class TokenAuthentication(BaseAuthentication): return (token.user, token) def authenticate_header(self, request): - return 'Token' + return self.keyword diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 9aff7280b..9784087d8 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -35,6 +35,10 @@ class CustomTokenAuthentication(TokenAuthentication): model = CustomToken +class CustomKeywordTokenAuthentication(TokenAuthentication): + keyword = 'Bearer' + + class MockView(APIView): permission_classes = (permissions.IsAuthenticated,) @@ -53,6 +57,7 @@ urlpatterns = [ url(r'^basic/$', MockView.as_view(authentication_classes=[BasicAuthentication])), url(r'^token/$', MockView.as_view(authentication_classes=[TokenAuthentication])), url(r'^customtoken/$', MockView.as_view(authentication_classes=[CustomTokenAuthentication])), + url(r'^customkeywordtoken/$', MockView.as_view(authentication_classes=[CustomKeywordTokenAuthentication])), url(r'^auth-token/$', 'rest_framework.authtoken.views.obtain_auth_token'), url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')), ] @@ -166,6 +171,7 @@ class BaseTokenAuthTests(object): urls = 'tests.test_authentication' model = None path = None + header_prefix = 'Token ' def setUp(self): self.csrf_client = APIClient(enforce_csrf_checks=True) @@ -179,31 +185,31 @@ class BaseTokenAuthTests(object): def test_post_form_passing_token_auth(self): """Ensure POSTing json over token auth with correct credentials passes and does not require CSRF""" - auth = 'Token ' + self.key + auth = self.header_prefix + self.key response = self.csrf_client.post(self.path, {'example': 'example'}, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_fail_post_form_passing_nonexistent_token_auth(self): # use a nonexistent token key - auth = 'Token wxyz6789' + auth = self.header_prefix + 'wxyz6789' response = self.csrf_client.post(self.path, {'example': 'example'}, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_fail_post_form_passing_invalid_token_auth(self): # add an 'invalid' unicode character - auth = 'Token ' + self.key + "¸" + auth = self.header_prefix + self.key + "¸" response = self.csrf_client.post(self.path, {'example': 'example'}, HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) def test_post_json_passing_token_auth(self): """Ensure POSTing form over token auth with correct credentials passes and does not require CSRF""" - auth = "Token " + self.key + auth = self.header_prefix + self.key response = self.csrf_client.post(self.path, {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, status.HTTP_200_OK) def test_post_json_makes_one_db_query(self): """Ensure that authenticating a user using a token performs only one DB query""" - auth = "Token " + self.key + auth = self.header_prefix + self.key def func_to_test(): return self.csrf_client.post(self.path, {'example': 'example'}, format='json', HTTP_AUTHORIZATION=auth) @@ -273,6 +279,12 @@ class CustomTokenAuthTests(BaseTokenAuthTests, TestCase): path = '/customtoken/' +class CustomKeywordTokenAuthTests(BaseTokenAuthTests, TestCase): + model = Token + path = '/customkeywordtoken/' + header_prefix = 'Bearer ' + + class IncorrectCredentialsTests(TestCase): def test_incorrect_credentials(self): """ From d39af5335cba57c0e36ab6949c5cef9b02a20e26 Mon Sep 17 00:00:00 2001 From: Hongxia Zhong Date: Fri, 6 May 2016 01:22:33 -0700 Subject: [PATCH 067/457] Fix incorrect zh-hans and zh-hant locale directory path --- .../{zh-Hans => zh_Hans}/LC_MESSAGES/django.mo | Bin .../{zh-Hans => zh_Hans}/LC_MESSAGES/django.po | 0 .../{zh-Hant => zh_Hant}/LC_MESSAGES/django.mo | Bin .../{zh-Hant => zh_Hant}/LC_MESSAGES/django.po | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename rest_framework/locale/{zh-Hans => zh_Hans}/LC_MESSAGES/django.mo (100%) rename rest_framework/locale/{zh-Hans => zh_Hans}/LC_MESSAGES/django.po (100%) rename rest_framework/locale/{zh-Hant => zh_Hant}/LC_MESSAGES/django.mo (100%) rename rest_framework/locale/{zh-Hant => zh_Hant}/LC_MESSAGES/django.po (100%) diff --git a/rest_framework/locale/zh-Hans/LC_MESSAGES/django.mo b/rest_framework/locale/zh_Hans/LC_MESSAGES/django.mo similarity index 100% rename from rest_framework/locale/zh-Hans/LC_MESSAGES/django.mo rename to rest_framework/locale/zh_Hans/LC_MESSAGES/django.mo diff --git a/rest_framework/locale/zh-Hans/LC_MESSAGES/django.po b/rest_framework/locale/zh_Hans/LC_MESSAGES/django.po similarity index 100% rename from rest_framework/locale/zh-Hans/LC_MESSAGES/django.po rename to rest_framework/locale/zh_Hans/LC_MESSAGES/django.po diff --git a/rest_framework/locale/zh-Hant/LC_MESSAGES/django.mo b/rest_framework/locale/zh_Hant/LC_MESSAGES/django.mo similarity index 100% rename from rest_framework/locale/zh-Hant/LC_MESSAGES/django.mo rename to rest_framework/locale/zh_Hant/LC_MESSAGES/django.mo diff --git a/rest_framework/locale/zh-Hant/LC_MESSAGES/django.po b/rest_framework/locale/zh_Hant/LC_MESSAGES/django.po similarity index 100% rename from rest_framework/locale/zh-Hant/LC_MESSAGES/django.po rename to rest_framework/locale/zh_Hant/LC_MESSAGES/django.po From dbbf79be6496c66070fcf24d17b9c19e06925bd4 Mon Sep 17 00:00:00 2001 From: "Graham R. Jeffries" Date: Fri, 6 May 2016 06:58:58 -0400 Subject: [PATCH 068/457] minor docs indentation fix (#4101) Fixes a minor indentation typo. --- docs/api-guide/routers.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index d73606da2..10a86bdf0 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -226,9 +226,9 @@ The following example will only route to the `list` and `retrieve` actions, and ), Route( url=r'^{prefix}/{lookup}$', - mapping={'get': 'retrieve'}, - name='{basename}-detail', - initkwargs={'suffix': 'Detail'} + mapping={'get': 'retrieve'}, + name='{basename}-detail', + initkwargs={'suffix': 'Detail'} ), DynamicDetailRoute( url=r'^{prefix}/{lookup}/{methodnamehyphen}$', From 0795f7394c3b452c10efd8db2b668bcccc717d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Tue, 10 May 2016 05:58:24 -0400 Subject: [PATCH 069/457] Prevent raising exception when limit is 0 (#4098) --- rest_framework/pagination.py | 32 ++++++++++++++++++++------------ tests/test_pagination.py | 25 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index b2255a212..153a77d33 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -36,10 +36,11 @@ def _positive_int(integer_string, strict=False, cutoff=None): def _divide_with_ceil(a, b): """ - Returns 'a' divded by 'b', with any remainder rounded up. + Returns 'a' divided by 'b', with any remainder rounded up. """ if a % b: return (a // b) + 1 + return a // b @@ -358,17 +359,24 @@ class LimitOffsetPagination(BasePagination): def get_html_context(self): base_url = self.request.build_absolute_uri() - current = _divide_with_ceil(self.offset, self.limit) + 1 - # The number of pages is a little bit fiddly. - # We need to sum both the number of pages from current offset to end - # plus the number of pages up to the current offset. - # When offset is not strictly divisible by the limit then we may - # end up introducing an extra page as an artifact. - final = ( - _divide_with_ceil(self.count - self.offset, self.limit) + - _divide_with_ceil(self.offset, self.limit) - ) - if final < 1: + + if self.limit: + current = _divide_with_ceil(self.offset, self.limit) + 1 + + # The number of pages is a little bit fiddly. + # We need to sum both the number of pages from current offset to end + # plus the number of pages up to the current offset. + # When offset is not strictly divisible by the limit then we may + # end up introducing an extra page as an artifact. + final = ( + _divide_with_ceil(self.count - self.offset, self.limit) + + _divide_with_ceil(self.offset, self.limit) + ) + + if final < 1: + final = 1 + else: + current = 1 final = 1 if current > final: diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 1c74e837c..ba3cc7037 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -505,6 +505,31 @@ class TestLimitOffset: assert content.get('next') == next_url assert content.get('previous') == prev_url + def test_limit_zero(self): + """ + A limit of 0 should return empty results. + """ + request = Request(factory.get('/', {'limit': 0, 'offset': 10})) + queryset = self.paginate_queryset(request) + context = self.get_html_context() + content = self.get_paginated_content(queryset) + + assert context == { + 'previous_url': 'http://testserver/?limit=0&offset=10', + 'page_links': [ + PageLink( + url='http://testserver/?limit=0', + number=1, + is_active=True, + is_break=False + ) + ], + 'next_url': 'http://testserver/?limit=0&offset=10' + } + + assert queryset == [] + assert content.get('results') == [] + class TestCursorPagination: """ From ebb4070467e34d20c6eccb78b4ec5a8f8780a4e9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 10 May 2016 11:56:36 +0100 Subject: [PATCH 070/457] Resolve TimeField representation for midnight value. (#4107) --- rest_framework/fields.py | 2 +- tests/test_fields.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 5dcd546c0..04dea89a2 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1191,7 +1191,7 @@ class TimeField(Field): self.fail('invalid', format=humanized_format) def to_representation(self, value): - if not value: + if value in (None, ''): return None output_format = getattr(self, 'format', api_settings.TIME_FORMAT) diff --git a/tests/test_fields.py b/tests/test_fields.py index 304de11ee..8b187ecd4 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1063,7 +1063,8 @@ class TestTimeField(FieldValues): '99:99': ['Time has wrong format. Use one of these formats instead: hh:mm[:ss[.uuuuuu]].'], } outputs = { - datetime.time(13, 00): '13:00:00', + datetime.time(13, 0): '13:00:00', + datetime.time(0, 0): '00:00:00', '00:00:00': '00:00:00', None: None, '': None, From 788603e1532cd5d7cd10d1fa341626809ad1afd8 Mon Sep 17 00:00:00 2001 From: Petros Moisiadis Date: Fri, 13 May 2016 17:55:31 +0300 Subject: [PATCH 071/457] Document allow_empty argument (#4117) ListSerializer fields or serializers that are passed many=True may also take an allow_empty=False argument to disallow empty lists as valid input. Information about this was part of the 3.2 release announcement, but had not been part of the API docs until now. --- docs/api-guide/serializers.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 78d43a535..bc79406d7 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -731,9 +731,17 @@ The `ListSerializer` class provides the behavior for serializing and validating When a serializer is instantiated and `many=True` is passed, a `ListSerializer` instance will be created. The serializer class then becomes a child of the parent `ListSerializer` +The following argument can also be passed to a `ListSerializer` field or a serializer that is passed `many=True`: + +### `allow_empty` + +This is `True` by default, but can be set to `False` if you want to disallow empty lists as valid input. + +### Customizing `ListSerializer` behavior + There *are* a few use cases when you might want to customize the `ListSerializer` behavior. For example: -* You want to provide particular validation of the lists, such as always ensuring that there is at least one element in a list. +* You want to provide particular validation of the lists, such as checking that one element does not conflict with another element in a list. * You want to customize the create or update behavior of multiple objects. For these cases you can modify the class that is used when `many=True` is passed, by using the `list_serializer_class` option on the serializer `Meta` class. From 1328982de3ffda5df3339af0be50447baa38affb Mon Sep 17 00:00:00 2001 From: Alexander Gaevsky Date: Mon, 16 May 2016 11:22:28 +0300 Subject: [PATCH 072/457] Set proper status code in AdminRenderer for the redirection after POST/DELETE requests. (#4106) --- 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 53bbb1390..63e0d836f 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -712,12 +712,12 @@ class AdminRenderer(BrowsableAPIRenderer): # Creation and deletion should use redirects in the admin style. if (response.status_code == status.HTTP_201_CREATED) and ('Location' in response): - response.status_code = status.HTTP_302_FOUND + response.status_code = status.HTTP_303_SEE_OTHER response['Location'] = request.build_absolute_uri() ret = '' if response.status_code == status.HTTP_204_NO_CONTENT: - response.status_code = status.HTTP_302_FOUND + response.status_code = status.HTTP_303_SEE_OTHER try: # Attempt to get the parent breadcrumb URL. response['Location'] = self.get_breadcrumbs(request)[-2][1] From 5392be4ddba37da3e50c3feabc8b3b1c944481a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Mon, 16 May 2016 04:27:10 -0400 Subject: [PATCH 073/457] Spring cleaning template styles (#4124) --- .../templates/rest_framework/admin.html | 480 +++++++++--------- .../rest_framework/admin/detail.html | 12 +- .../templates/rest_framework/admin/list.html | 34 +- .../rest_framework/admin/list_value.html | 16 +- .../templates/rest_framework/base.html | 461 +++++++++-------- 5 files changed, 501 insertions(+), 502 deletions(-) diff --git a/rest_framework/templates/rest_framework/admin.html b/rest_framework/templates/rest_framework/admin.html index a21ea57be..cec8f426f 100644 --- a/rest_framework/templates/rest_framework/admin.html +++ b/rest_framework/templates/rest_framework/admin.html @@ -1,252 +1,252 @@ {% load staticfiles %} {% load i18n %} {% load rest_framework %} + - - {% block head %} + + {% block head %} - {% block meta %} - - - {% endblock %} + {% block meta %} + + + {% endblock %} - {% block title %}Django REST framework{% endblock %} - - {% block style %} - {% block bootstrap_theme %} - - - {% endblock %} - - - {% endblock %} + {% block title %}Django REST framework{% endblock %} + {% block style %} + {% block bootstrap_theme %} + + {% endblock %} - + + + {% endblock %} - {% block body %} - - -
- - {% block navbar %} - - {% endblock %} - -
- {% block breadcrumbs %} - - {% endblock %} - - -
- - {% if 'GET' in allowed_methods %} - -
-
- - -
-
- - {% endif %} - - {% if post_form %} - - {% endif %} - - {% if put_form %} - - {% endif %} - - {% if delete_form %} -
- -
- {% endif %} - - {% if filter_form %} - - {% endif %} - -
- -
- {% block description %} - {{ description }} - {% endblock %} -
- - {% if paginator %} - - {% endif %} - -
- {% if style == 'list' %} - {% include "rest_framework/admin/list.html" %} - {% else %} - {% include "rest_framework/admin/detail.html" %} - {% endif %} -
- - {% if paginator %} - - {% endif %} -
- -
- -
-
- - - - - - - -{% if error_form %} - - -{% endif %} - -{% if filter_form %}{{ filter_form }}{% endif %} - - {% block script %} - - - - - - - - - {% endblock %} - {% endblock %} + + + {% block body %} + +
+ {% block navbar %} + + {% endblock %} + +
+ {% block breadcrumbs %} + + {% endblock %} + + +
+ {% if 'GET' in allowed_methods %} +
+
+
+ + +
+
+
+ {% endif %} + + {% if post_form %} + + {% endif %} + + {% if put_form %} + + {% endif %} + + {% if delete_form %} +
+ +
+ {% endif %} + + {% if filter_form %} + + {% endif %} + +
+ + +
+ {% block description %} + {{ description }} + {% endblock %} +
+ + {% if paginator %} + + {% endif %} + +
+ {% if style == 'list' %} + {% include "rest_framework/admin/list.html" %} + {% else %} + {% include "rest_framework/admin/detail.html" %} + {% endif %} +
+ + {% if paginator %} + + {% endif %} +
+
+ +
+
+ + + + + + + + {% if error_form %} + + + {% endif %} + + {% if filter_form %} + {{ filter_form }} + {% endif %} + + {% block script %} + + + + + + + + + {% endblock %} + + {% endblock %} diff --git a/rest_framework/templates/rest_framework/admin/detail.html b/rest_framework/templates/rest_framework/admin/detail.html index beac0a708..759fa163d 100644 --- a/rest_framework/templates/rest_framework/admin/detail.html +++ b/rest_framework/templates/rest_framework/admin/detail.html @@ -1,10 +1,10 @@ {% load rest_framework %} - - {% for key, value in results.items %} - {% if key in details %} + + {% for key, value in results.items %} + {% if key in details %} - {% endif %} - {% endfor %} - + {% endif %} + {% endfor %} +
{{ key|capfirst }}{{ value|format_value }}
diff --git a/rest_framework/templates/rest_framework/admin/list.html b/rest_framework/templates/rest_framework/admin/list.html index 048843bb7..6044ebb38 100644 --- a/rest_framework/templates/rest_framework/admin/list.html +++ b/rest_framework/templates/rest_framework/admin/list.html @@ -1,22 +1,22 @@ {% load rest_framework %} - - {% for column in columns%}{% endfor %} - - - {% for row in results %} - - {% for key, value in row.items %} - {% if key in columns %} - - {% endif %} - {% endfor %} - + {% for column in columns%}{% endfor %} + + + {% for row in results %} + + {% for key, value in row.items %} + {% if key in columns %} + - + {% endif %} {% endfor %} - + + + {% endfor %} +
{{ column|capfirst }}
- {{ value|format_value }} - - +
{{ column|capfirst }}
+ {{ value|format_value }}
+ +
diff --git a/rest_framework/templates/rest_framework/admin/list_value.html b/rest_framework/templates/rest_framework/admin/list_value.html index 267bbaa39..5bab6b57a 100644 --- a/rest_framework/templates/rest_framework/admin/list_value.html +++ b/rest_framework/templates/rest_framework/admin/list_value.html @@ -1,11 +1,11 @@ {% load rest_framework %} - - {% for item in value %} - - - - - {% endfor %} - + + {% for item in value %} + + + + + {% endfor %} +
{{ forloop.counter0 }}{{ item|format_value }}
{{ forloop.counter0 }}{{ item|format_value }}
diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 21431b70c..a75e642e6 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -1,185 +1,198 @@ {% load staticfiles %} -{% load rest_framework %} {% load i18n %} +{% load rest_framework %} - - {% block head %} + + {% block head %} - {% block meta %} - - - {% endblock %} - - {% block title %}{% if name %}{{ name }} – {% endif %}Django REST framework{% endblock %} - - {% block style %} - {% block bootstrap_theme %} - - + {% block meta %} + + + {% endblock %} + + {% block title %}{% if name %}{{ name }} – {% endif %}Django REST framework{% endblock %} + + {% block style %} + {% block bootstrap_theme %} + + + {% endblock %} + + + {% endblock %} - - {% endblock %} + - {% endblock %} - + {% block body %} + -{% block body %} - - -
- {% block navbar %} - ',trigger:"hover focus",title:"",delay:0,html:!1,container:!1,viewport:{selector:"body",padding:0}},c.prototype.init=function(b,c,d){if(this.enabled=!0,this.type=b,this.$element=a(c),this.options=this.getOptions(d),this.$viewport=this.options.viewport&&a(a.isFunction(this.options.viewport)?this.options.viewport.call(this,this.$element):this.options.viewport.selector||this.options.viewport),this.inState={click:!1,hover:!1,focus:!1},this.$element[0]instanceof document.constructor&&!this.options.selector)throw new Error("`selector` option must be specified when initializing "+this.type+" on the window.document object!");for(var e=this.options.trigger.split(" "),f=e.length;f--;){var g=e[f];if("click"==g)this.$element.on("click."+this.type,this.options.selector,a.proxy(this.toggle,this));else if("manual"!=g){var h="hover"==g?"mouseenter":"focusin",i="hover"==g?"mouseleave":"focusout";this.$element.on(h+"."+this.type,this.options.selector,a.proxy(this.enter,this)),this.$element.on(i+"."+this.type,this.options.selector,a.proxy(this.leave,this))}}this.options.selector?this._options=a.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.getOptions=function(b){return b=a.extend({},this.getDefaults(),this.$element.data(),b),b.delay&&"number"==typeof b.delay&&(b.delay={show:b.delay,hide:b.delay}),b},c.prototype.getDelegateOptions=function(){var b={},c=this.getDefaults();return this._options&&a.each(this._options,function(a,d){c[a]!=d&&(b[a]=d)}),b},c.prototype.enter=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);return c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusin"==b.type?"focus":"hover"]=!0),c.tip().hasClass("in")||"in"==c.hoverState?void(c.hoverState="in"):(clearTimeout(c.timeout),c.hoverState="in",c.options.delay&&c.options.delay.show?void(c.timeout=setTimeout(function(){"in"==c.hoverState&&c.show()},c.options.delay.show)):c.show())},c.prototype.isInStateTrue=function(){for(var a in this.inState)if(this.inState[a])return!0;return!1},c.prototype.leave=function(b){var c=b instanceof this.constructor?b:a(b.currentTarget).data("bs."+this.type);if(c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c)),b instanceof a.Event&&(c.inState["focusout"==b.type?"focus":"hover"]=!1),!c.isInStateTrue())return clearTimeout(c.timeout),c.hoverState="out",c.options.delay&&c.options.delay.hide?void(c.timeout=setTimeout(function(){"out"==c.hoverState&&c.hide()},c.options.delay.hide)):c.hide()},c.prototype.show=function(){var b=a.Event("show.bs."+this.type);if(this.hasContent()&&this.enabled){this.$element.trigger(b);var d=a.contains(this.$element[0].ownerDocument.documentElement,this.$element[0]);if(b.isDefaultPrevented()||!d)return;var e=this,f=this.tip(),g=this.getUID(this.type);this.setContent(),f.attr("id",g),this.$element.attr("aria-describedby",g),this.options.animation&&f.addClass("fade");var h="function"==typeof this.options.placement?this.options.placement.call(this,f[0],this.$element[0]):this.options.placement,i=/\s?auto?\s?/i,j=i.test(h);j&&(h=h.replace(i,"")||"top"),f.detach().css({top:0,left:0,display:"block"}).addClass(h).data("bs."+this.type,this),this.options.container?f.appendTo(this.options.container):f.insertAfter(this.$element),this.$element.trigger("inserted.bs."+this.type);var k=this.getPosition(),l=f[0].offsetWidth,m=f[0].offsetHeight;if(j){var n=h,o=this.getPosition(this.$viewport);h="bottom"==h&&k.bottom+m>o.bottom?"top":"top"==h&&k.top-mo.width?"left":"left"==h&&k.left-lg.top+g.height&&(e.top=g.top+g.height-i)}else{var j=b.left-f,k=b.left+f+c;jg.right&&(e.left=g.left+g.width-k)}return e},c.prototype.getTitle=function(){var a,b=this.$element,c=this.options;return a=b.attr("data-original-title")||("function"==typeof c.title?c.title.call(b[0]):c.title)},c.prototype.getUID=function(a){do a+=~~(1e6*Math.random());while(document.getElementById(a));return a},c.prototype.tip=function(){if(!this.$tip&&(this.$tip=a(this.options.template),1!=this.$tip.length))throw new Error(this.type+" `template` option must consist of exactly 1 top-level element!");return this.$tip},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},c.prototype.enable=function(){this.enabled=!0},c.prototype.disable=function(){this.enabled=!1},c.prototype.toggleEnabled=function(){this.enabled=!this.enabled},c.prototype.toggle=function(b){var c=this;b&&(c=a(b.currentTarget).data("bs."+this.type),c||(c=new this.constructor(b.currentTarget,this.getDelegateOptions()),a(b.currentTarget).data("bs."+this.type,c))),b?(c.inState.click=!c.inState.click,c.isInStateTrue()?c.enter(c):c.leave(c)):c.tip().hasClass("in")?c.leave(c):c.enter(c)},c.prototype.destroy=function(){var a=this;clearTimeout(this.timeout),this.hide(function(){a.$element.off("."+a.type).removeData("bs."+a.type),a.$tip&&a.$tip.detach(),a.$tip=null,a.$arrow=null,a.$viewport=null,a.$element=null})};var d=a.fn.tooltip;a.fn.tooltip=b,a.fn.tooltip.Constructor=c,a.fn.tooltip.noConflict=function(){return a.fn.tooltip=d,this}}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.popover"),f="object"==typeof b&&b;!e&&/destroy|hide/.test(b)||(e||d.data("bs.popover",e=new c(this,f)),"string"==typeof b&&e[b]())})}var c=function(a,b){this.init("popover",a,b)};if(!a.fn.tooltip)throw new Error("Popover requires tooltip.js");c.VERSION="3.3.7",c.DEFAULTS=a.extend({},a.fn.tooltip.Constructor.DEFAULTS,{placement:"right",trigger:"click",content:"",template:''}),c.prototype=a.extend({},a.fn.tooltip.Constructor.prototype),c.prototype.constructor=c,c.prototype.getDefaults=function(){return c.DEFAULTS},c.prototype.setContent=function(){var a=this.tip(),b=this.getTitle(),c=this.getContent();a.find(".popover-title")[this.options.html?"html":"text"](b),a.find(".popover-content").children().detach().end()[this.options.html?"string"==typeof c?"html":"append":"text"](c),a.removeClass("fade top bottom left right in"),a.find(".popover-title").html()||a.find(".popover-title").hide()},c.prototype.hasContent=function(){return this.getTitle()||this.getContent()},c.prototype.getContent=function(){var a=this.$element,b=this.options;return a.attr("data-content")||("function"==typeof b.content?b.content.call(a[0]):b.content)},c.prototype.arrow=function(){return this.$arrow=this.$arrow||this.tip().find(".arrow")};var d=a.fn.popover;a.fn.popover=b,a.fn.popover.Constructor=c,a.fn.popover.noConflict=function(){return a.fn.popover=d,this}}(jQuery),+function(a){"use strict";function b(c,d){this.$body=a(document.body),this.$scrollElement=a(a(c).is(document.body)?window:c),this.options=a.extend({},b.DEFAULTS,d),this.selector=(this.options.target||"")+" .nav li > a",this.offsets=[],this.targets=[],this.activeTarget=null,this.scrollHeight=0,this.$scrollElement.on("scroll.bs.scrollspy",a.proxy(this.process,this)),this.refresh(),this.process()}function c(c){return this.each(function(){var d=a(this),e=d.data("bs.scrollspy"),f="object"==typeof c&&c;e||d.data("bs.scrollspy",e=new b(this,f)),"string"==typeof c&&e[c]()})}b.VERSION="3.3.7",b.DEFAULTS={offset:10},b.prototype.getScrollHeight=function(){return this.$scrollElement[0].scrollHeight||Math.max(this.$body[0].scrollHeight,document.documentElement.scrollHeight)},b.prototype.refresh=function(){var b=this,c="offset",d=0;this.offsets=[],this.targets=[],this.scrollHeight=this.getScrollHeight(),a.isWindow(this.$scrollElement[0])||(c="position",d=this.$scrollElement.scrollTop()),this.$body.find(this.selector).map(function(){var b=a(this),e=b.data("target")||b.attr("href"),f=/^#./.test(e)&&a(e);return f&&f.length&&f.is(":visible")&&[[f[c]().top+d,e]]||null}).sort(function(a,b){return a[0]-b[0]}).each(function(){b.offsets.push(this[0]),b.targets.push(this[1])})},b.prototype.process=function(){var a,b=this.$scrollElement.scrollTop()+this.options.offset,c=this.getScrollHeight(),d=this.options.offset+c-this.$scrollElement.height(),e=this.offsets,f=this.targets,g=this.activeTarget;if(this.scrollHeight!=c&&this.refresh(),b>=d)return g!=(a=f[f.length-1])&&this.activate(a);if(g&&b=e[a]&&(void 0===e[a+1]||b .dropdown-menu > .active").removeClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!1),b.addClass("active").find('[data-toggle="tab"]').attr("aria-expanded",!0),h?(b[0].offsetWidth,b.addClass("in")):b.removeClass("fade"),b.parent(".dropdown-menu").length&&b.closest("li.dropdown").addClass("active").end().find('[data-toggle="tab"]').attr("aria-expanded",!0),e&&e()}var g=d.find("> .active"),h=e&&a.support.transition&&(g.length&&g.hasClass("fade")||!!d.find("> .fade").length);g.length&&h?g.one("bsTransitionEnd",f).emulateTransitionEnd(c.TRANSITION_DURATION):f(),g.removeClass("in")};var d=a.fn.tab;a.fn.tab=b,a.fn.tab.Constructor=c,a.fn.tab.noConflict=function(){return a.fn.tab=d,this};var e=function(c){c.preventDefault(),b.call(a(this),"show")};a(document).on("click.bs.tab.data-api",'[data-toggle="tab"]',e).on("click.bs.tab.data-api",'[data-toggle="pill"]',e)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.affix"),f="object"==typeof b&&b;e||d.data("bs.affix",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.options=a.extend({},c.DEFAULTS,d),this.$target=a(this.options.target).on("scroll.bs.affix.data-api",a.proxy(this.checkPosition,this)).on("click.bs.affix.data-api",a.proxy(this.checkPositionWithEventLoop,this)),this.$element=a(b),this.affixed=null,this.unpin=null,this.pinnedOffset=null,this.checkPosition()};c.VERSION="3.3.7",c.RESET="affix affix-top affix-bottom",c.DEFAULTS={offset:0,target:window},c.prototype.getState=function(a,b,c,d){var e=this.$target.scrollTop(),f=this.$element.offset(),g=this.$target.height();if(null!=c&&"top"==this.affixed)return e=a-d&&"bottom"},c.prototype.getPinnedOffset=function(){if(this.pinnedOffset)return this.pinnedOffset;this.$element.removeClass(c.RESET).addClass("affix");var a=this.$target.scrollTop(),b=this.$element.offset();return this.pinnedOffset=b.top-a},c.prototype.checkPositionWithEventLoop=function(){setTimeout(a.proxy(this.checkPosition,this),1)},c.prototype.checkPosition=function(){if(this.$element.is(":visible")){var b=this.$element.height(),d=this.options.offset,e=d.top,f=d.bottom,g=Math.max(a(document).height(),a(document.body).height());"object"!=typeof d&&(f=e=d),"function"==typeof e&&(e=d.top(this.$element)),"function"==typeof f&&(f=d.bottom(this.$element));var h=this.getState(g,b,e,f);if(this.affixed!=h){null!=this.unpin&&this.$element.css("top","");var i="affix"+(h?"-"+h:""),j=a.Event(i+".bs.affix");if(this.$element.trigger(j),j.isDefaultPrevented())return;this.affixed=h,this.unpin="bottom"==h?this.getPinnedOffset():null,this.$element.removeClass(c.RESET).addClass(i).trigger(i.replace("affix","affixed")+".bs.affix")}"bottom"==h&&this.$element.offset({top:g-b-f})}};var d=a.fn.affix;a.fn.affix=b,a.fn.affix.Constructor=c,a.fn.affix.noConflict=function(){return a.fn.affix=d,this},a(window).on("load",function(){a('[data-spy="affix"]').each(function(){var c=a(this),d=c.data();d.offset=d.offset||{},null!=d.offsetBottom&&(d.offset.bottom=d.offsetBottom),null!=d.offsetTop&&(d.offset.top=d.offsetTop),b.call(c,d)})})}(jQuery); \ No newline at end of file From f0f61aa077ba49f3f107ec012f021cb8fa27ac07 Mon Sep 17 00:00:00 2001 From: Sassan Haradji Date: Tue, 26 Jul 2016 18:42:51 +0430 Subject: [PATCH 170/457] use verbose_name instead of object_name in field_mapping (#4299) * use verbose_name instead of object_name in error messages --- rest_framework/utils/field_mapping.py | 2 +- tests/test_validators.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index af7ab0231..311d58317 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -220,7 +220,7 @@ def get_field_kwargs(field_name, model_field): unique_error_message = model_field.error_messages.get('unique', None) if unique_error_message: unique_error_message = unique_error_message % { - 'model_name': model_field.model._meta.object_name, + 'model_name': model_field.model._meta.verbose_name, 'field_label': model_field.verbose_name } validator = UniqueValidator( diff --git a/tests/test_validators.py b/tests/test_validators.py index 32d41f2ba..ebc8b899f 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -77,7 +77,7 @@ class TestUniquenessValidation(TestCase): data = {'username': 'existing'} serializer = UniquenessSerializer(data=data) assert not serializer.is_valid() - assert serializer.errors == {'username': ['UniquenessModel with this username already exists.']} + assert serializer.errors == {'username': ['uniqueness model with this username already exists.']} def test_is_unique(self): data = {'username': 'other'} From 5d3b56f957d5ba6d5cb6e680c853a2e0da026fbb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 26 Jul 2016 16:28:10 +0100 Subject: [PATCH 171/457] Test case for #4272 (#4310) * Test case for #4272 --- tests/test_model_serializer.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index f24d1d515..2cf6cb04c 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -955,3 +955,24 @@ class TestMetaInheritance(TestCase): self.assertEqual(unicode_repr(ChildSerializer()), child_expected) self.assertEqual(unicode_repr(TestSerializer()), test_expected) self.assertEqual(unicode_repr(ChildSerializer()), child_expected) + + +class OneToOneTargetTestModel(models.Model): + text = models.CharField(max_length=100) + + +class OneToOneSourceTestModel(models.Model): + target = models.OneToOneField(OneToOneTargetTestModel, primary_key=True) + + +class TestModelFieldValues(TestCase): + def test_model_field(self): + class ExampleSerializer(serializers.ModelSerializer): + class Meta: + model = OneToOneSourceTestModel + fields = ('target',) + + target = OneToOneTargetTestModel(id=1, text='abc') + source = OneToOneSourceTestModel(target=target) + serializer = ExampleSerializer(source) + self.assertEqual(serializer.data, {'target': 1}) From 19b415ec25089d16d7b78c99712522adf6a5ea02 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 26 Jul 2016 16:45:27 +0100 Subject: [PATCH 172/457] Improve pagination docs. Refs #4304 [ci skip] --- docs/api-guide/pagination.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 0dd935ba3..088e07184 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -21,12 +21,15 @@ Pagination can be turned off by setting the pagination class to `None`. ## Setting the pagination style -The default pagination style may be set globally, using the `DEFAULT_PAGINATION_CLASS` settings key. For example, to use the built-in limit/offset pagination, you would do: +The default pagination style may be set globally, using the `DEFAULT_PAGINATION_CLASS` and `PAGE_SIZE` setting keys. For example, to use the built-in limit/offset pagination, you would do something like this: REST_FRAMEWORK = { - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination' + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', + 'PAGE_SIZE': 100 } +Note that you need to set both the pagination class, and the page size that should be used. + You can also set the pagination class on an individual view by using the `pagination_class` attribute. Typically you'll want to use the same pagination style throughout your API, although you might want to vary individual aspects of the pagination, such as default or maximum page size, on a per-view basis. ## Modifying the pagination style @@ -109,7 +112,7 @@ To set these attributes you should override the `PageNumberPagination` class, an ## LimitOffsetPagination -This pagination style mirrors the syntax used when looking up multiple database records. The client includes both a "limit" and an +This pagination style mirrors the syntax used when looking up multiple database records. The client includes both a "limit" and an "offset" query parameter. The limit indicates the maximum number of items to return, and is equivalent to the `page_size` in other styles. The offset indicates the starting position of the query in relation to the complete set of unpaginated items. **Request**: From 351e0a4a99a6505b01718aa8926b6e466e43dcb0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 27 Jul 2016 11:49:01 +0100 Subject: [PATCH 173/457] Fix json indent parameter. Closes #4281 (#4313) --- docs/api-guide/schemas.md | 2 +- rest_framework/response.py | 4 +++- rest_framework/routers.py | 2 ++ tests/test_negotiation.py | 13 ++++++++++++- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index 6585801e7..8abd74646 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -241,7 +241,7 @@ to the Open API ("Swagger") format: from openapi_codec import OpenAPICodec class SwaggerRenderer(renderers.BaseRenderer): - media_type = 'application/openapi+json;version=2.0' + media_type = 'application/openapi+json' format = 'swagger' def render(self, data, media_type=None, renderer_context=None): diff --git a/rest_framework/response.py b/rest_framework/response.py index ab8aff114..4b863cb99 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -51,9 +51,11 @@ class Response(SimpleTemplateResponse): @property def rendered_content(self): renderer = getattr(self, 'accepted_renderer', None) + accepted_media_type = getattr(self, 'accepted_media_type', None) context = getattr(self, 'renderer_context', None) assert renderer, ".accepted_renderer not set on Response" + assert accepted_media_type, ".accepted_media_type not set on Response" assert context, ".renderer_context not set on Response" context['response'] = self @@ -67,7 +69,7 @@ class Response(SimpleTemplateResponse): content_type = media_type self['Content-Type'] = content_type - ret = renderer.render(self.data, media_type, context) + ret = renderer.render(self.data, accepted_media_type, context) if isinstance(ret, six.text_type): assert charset, ( 'renderer returned unicode, and did not specify ' diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 69d73e842..9561fa4db 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -276,6 +276,8 @@ class DefaultRouter(SimpleRouter): default_schema_renderers = [renderers.CoreJSONRenderer] def __init__(self, *args, **kwargs): + if 'schema_renderers' in kwargs: + assert 'schema_title' in kwargs, 'Missing "schema_title" argument.' self.schema_title = kwargs.pop('schema_title', None) self.schema_renderers = kwargs.pop('schema_renderers', self.default_schema_renderers) super(DefaultRouter, self).__init__(*args, **kwargs) diff --git a/tests/test_negotiation.py b/tests/test_negotiation.py index 434fba496..96ef83eab 100644 --- a/tests/test_negotiation.py +++ b/tests/test_negotiation.py @@ -10,6 +10,11 @@ from rest_framework.test import APIRequestFactory factory = APIRequestFactory() +class MockOpenAPIRenderer(BaseRenderer): + media_type = 'application/openapi+json;version=2.0' + format = 'swagger' + + class MockJSONRenderer(BaseRenderer): media_type = 'application/json' @@ -24,7 +29,7 @@ class NoCharsetSpecifiedRenderer(BaseRenderer): class TestAcceptedMediaType(TestCase): def setUp(self): - self.renderers = [MockJSONRenderer(), MockHTMLRenderer()] + self.renderers = [MockJSONRenderer(), MockHTMLRenderer(), MockOpenAPIRenderer()] self.negotiator = DefaultContentNegotiation() def select_renderer(self, request): @@ -44,3 +49,9 @@ class TestAcceptedMediaType(TestCase): request = Request(factory.get('/', HTTP_ACCEPT='application/json; indent=8')) accepted_renderer, accepted_media_type = self.select_renderer(request) self.assertEqual(accepted_media_type, 'application/json; indent=8') + + def test_client_specifies_parameter(self): + request = Request(factory.get('/', HTTP_ACCEPT='application/openapi+json;version=2.0')) + accepted_renderer, accepted_media_type = self.select_renderer(request) + self.assertEqual(accepted_media_type, 'application/openapi+json;version=2.0') + self.assertEqual(accepted_renderer.format, 'swagger') From 8ebf81b1502bee067fe842fa23442aa4bc176473 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 27 Jul 2016 13:02:48 +0100 Subject: [PATCH 174/457] Schema support should function when 'pagination_class = None' (#4314) --- rest_framework/schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index cf84aca74..523b2f1ec 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -278,7 +278,7 @@ class SchemaGenerator(object): if hasattr(callback, 'actions') and ('list' not in callback.actions.values()): return [] - if not hasattr(view, 'pagination_class'): + if not getattr(view, 'pagination_class', None): return [] paginator = view.pagination_class() From 3586c8a61ae0dd3424c49b50f22a7699e999bae7 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 27 Jul 2016 14:43:45 +0100 Subject: [PATCH 175/457] Set view.format_kwarg in schema generator (#4315) --- rest_framework/schemas.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 523b2f1ec..91fdc76a0 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -84,6 +84,7 @@ class SchemaGenerator(object): method = link.action.upper() view = callback.cls() view.request = clone_request(request, method) + view.format_kwarg = None try: view.check_permissions(view.request) except exceptions.APIException: From 46a870c002ff7b079fd70a7730b666d16b2ba9e1 Mon Sep 17 00:00:00 2001 From: Alexander Gaevsky Date: Wed, 27 Jul 2016 17:36:36 +0300 Subject: [PATCH 176/457] Fix schema generation for APIView, since it does not have get_serializer_class method. (#4285) --- rest_framework/schemas.py | 4 +++- tests/test_schemas.py | 42 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 91fdc76a0..cdf2d5f3e 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -112,7 +112,6 @@ class SchemaGenerator(object): for pattern in patterns: path_regex = prefix + pattern.regex.pattern - if isinstance(pattern, RegexURLPattern): path = self.get_path(path_regex) callback = pattern.callback @@ -254,6 +253,9 @@ class SchemaGenerator(object): fields = [] + if not (hasattr(view, 'get_serializer_class') and callable(getattr(view, 'get_serializer_class'))): + return [] + serializer_class = view.get_serializer_class() serializer = serializer_class() diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 7d3308ed9..a32b8a117 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -5,8 +5,11 @@ from django.test import TestCase, override_settings from rest_framework import filters, pagination, permissions, serializers from rest_framework.compat import coreapi +from rest_framework.response import Response from rest_framework.routers import DefaultRouter +from rest_framework.schemas import SchemaGenerator from rest_framework.test import APIClient +from rest_framework.views import APIView from rest_framework.viewsets import ModelViewSet @@ -31,11 +34,24 @@ class ExampleViewSet(ModelViewSet): serializer_class = ExampleSerializer +class ExampleView(APIView): + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def get(self, request, *args, **kwargs): + return Response() + + def post(self, request, *args, **kwargs): + return Response() + + router = DefaultRouter(schema_title='Example API' if coreapi else None) router.register('example', ExampleViewSet, base_name='example') urlpatterns = [ url(r'^', include(router.urls)) ] +urlpatterns2 = [ + url(r'^example-view/$', ExampleView.as_view(), name='example-view') +] @unittest.skipUnless(coreapi, 'coreapi is not installed') @@ -135,3 +151,29 @@ class TestRouterGeneratedSchema(TestCase): } ) self.assertEqual(response.data, expected) + + +@unittest.skipUnless(coreapi, 'coreapi is not installed') +class TestSchemaGenerator(TestCase): + def test_view(self): + schema_generator = SchemaGenerator(title='Test View', patterns=urlpatterns2) + schema = schema_generator.get_schema() + expected = coreapi.Document( + url='', + title='Test View', + content={ + 'example-view': { + 'create': coreapi.Link( + url='/example-view/', + action='post', + fields=[] + ), + 'read': coreapi.Link( + url='/example-view/', + action='get', + fields=[] + ) + } + } + ) + self.assertEquals(schema, expected) From 1acbc29d580e7f36552c838d83ec3b80419d51da Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 27 Jul 2016 15:39:46 +0100 Subject: [PATCH 177/457] Minor style tweak. [ci skip] --- rest_framework/schemas.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index cdf2d5f3e..ee99a1c45 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -251,11 +251,11 @@ class SchemaGenerator(object): if method not in ('PUT', 'PATCH', 'POST'): return [] - fields = [] - - if not (hasattr(view, 'get_serializer_class') and callable(getattr(view, 'get_serializer_class'))): + if not hasattr(view, 'get_serializer_class'): return [] + fields = [] + serializer_class = view.get_serializer_class() serializer = serializer_class() From 6a7d34ec3452d4e8534f5c9bd238405548485a7a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 27 Jul 2016 15:40:04 +0100 Subject: [PATCH 178/457] Unique together checks should apply to fields that are read only, but have a default. (#4316) --- rest_framework/serializers.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8c39202f4..d5e6b66ed 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1396,9 +1396,8 @@ class ModelSerializer(Serializer): # cannot map to a field, and must be a traversal, so we're not # including those. field_names = { - field.source for field in self.fields.values() + field.source for field in self._writable_fields if (field.source != '*') and ('.' not in field.source) - and not field.read_only } # Note that we make sure to check `unique_together` both on the From 061e0ed0841274da930f61838c60af1324ea04bc Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 28 Jul 2016 12:08:34 +0100 Subject: [PATCH 179/457] Added url and schema_url arguments (#4321) --- docs/api-guide/schemas.md | 20 +++++++++++++++++--- rest_framework/compat.py | 6 ++++++ rest_framework/routers.py | 12 ++++++++---- rest_framework/schemas.py | 12 ++++++++---- 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index 8abd74646..848e57420 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -128,9 +128,11 @@ that include the Core JSON media type in their `Accept` header. This is a great zero-configuration option for when you want to get up and running really quickly. -The only other available option to `DefaultRouter` is `schema_renderers`, which -may be used to pass the set of renderer classes that can be used to render -schema output. +The other available options to `DefaultRouter` are: + +#### schema_renderers + +May be used to pass the set of renderer classes that can be used to render schema output. from rest_framework.renderers import CoreJSONRenderer from my_custom_package import APIBlueprintRenderer @@ -139,6 +141,17 @@ schema output. CoreJSONRenderer, APIBlueprintRenderer ]) +#### schema_url + +May be used to pass the root URL for the schema. This can either be used to ensure that +the schema URLs include a canonical hostname and schema, or to ensure that all the +schema URLs include a path prefix. + + router = DefaultRouter( + schema_title='Server Monitoring API', + schema_url='https://www.example.org/api/' + ) + If you want more flexibility over the schema output then you'll need to consider using `SchemaGenerator` instead. @@ -264,6 +277,7 @@ Typically you'll instantiate `SchemaGenerator` with a single argument, like so: Arguments: * `title` - The name of the API. **required** +* `url` - The root URL of the API schema. This option is not required unless the schema is included under path prefix. * `patterns` - A list of URLs to inspect when generating the schema. Defaults to the project's URL conf. * `urlconf` - A URL conf module name to use when generating the schema. Defaults to `settings.ROOT_URLCONF`. diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 94f64265a..3143e7654 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -23,6 +23,12 @@ except ImportError: from django.utils import importlib # Will be removed in Django 1.9 +try: + import urlparse # Python 2.x +except ImportError: + import urllib.parse as urlparse + + def unicode_repr(instance): # Get the repr of an instance, but ensure it is a unicode string # on both python 3 (already the case) and 2 (not the case). diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 9561fa4db..099c56d6c 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -278,11 +278,14 @@ class DefaultRouter(SimpleRouter): def __init__(self, *args, **kwargs): if 'schema_renderers' in kwargs: assert 'schema_title' in kwargs, 'Missing "schema_title" argument.' + if 'schema_url' in kwargs: + assert 'schema_title' in kwargs, 'Missing "schema_title" argument.' self.schema_title = kwargs.pop('schema_title', None) + self.schema_url = kwargs.pop('schema_url', None) self.schema_renderers = kwargs.pop('schema_renderers', self.default_schema_renderers) super(DefaultRouter, self).__init__(*args, **kwargs) - def get_api_root_view(self, schema_urls=None): + def get_api_root_view(self, api_urls=None): """ Return a view to use as the API root. """ @@ -294,11 +297,12 @@ class DefaultRouter(SimpleRouter): view_renderers = list(api_settings.DEFAULT_RENDERER_CLASSES) schema_media_types = [] - if schema_urls and self.schema_title: + if api_urls and self.schema_title: view_renderers += list(self.schema_renderers) schema_generator = SchemaGenerator( title=self.schema_title, - patterns=schema_urls + url=self.schema_url, + patterns=api_urls ) schema_media_types = [ renderer.media_type @@ -347,7 +351,7 @@ class DefaultRouter(SimpleRouter): urls = super(DefaultRouter, self).get_urls() if self.include_root_view: - view = self.get_api_root_view(schema_urls=urls) + view = self.get_api_root_view(api_urls=urls) root_url = url(r'^$', view, name=self.root_view_name) urls.append(root_url) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index ee99a1c45..41dc82da1 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -6,7 +6,7 @@ from django.core.urlresolvers import RegexURLPattern, RegexURLResolver from django.utils import six from rest_framework import exceptions, serializers -from rest_framework.compat import coreapi, uritemplate +from rest_framework.compat import coreapi, uritemplate, urlparse from rest_framework.request import clone_request from rest_framework.views import APIView @@ -57,7 +57,7 @@ class SchemaGenerator(object): 'delete': 'destroy', } - def __init__(self, title=None, patterns=None, urlconf=None): + def __init__(self, title=None, url=None, patterns=None, urlconf=None): assert coreapi, '`coreapi` must be installed for schema support.' if patterns is None and urlconf is not None: @@ -70,7 +70,11 @@ class SchemaGenerator(object): urls = import_module(settings.ROOT_URLCONF) patterns = urls.urlpatterns + if url and not url.endswith('/'): + url += '/' + self.title = title + self.url = url self.endpoints = self.get_api_endpoints(patterns) def get_schema(self, request=None): @@ -102,7 +106,7 @@ class SchemaGenerator(object): insert_into(content, key, link) # Return the schema document. - return coreapi.Document(title=self.title, content=content) + return coreapi.Document(title=self.title, content=content, url=self.url) def get_api_endpoints(self, patterns, prefix=''): """ @@ -203,7 +207,7 @@ class SchemaGenerator(object): encoding = None return coreapi.Link( - url=path, + url=urlparse.urljoin(self.url, path), action=method.lower(), encoding=encoding, fields=fields From 306726d9e8645aeb9528a3f6c6674d0fe5471374 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 28 Jul 2016 12:25:21 +0100 Subject: [PATCH 180/457] Improve datetime format docs (#4322) --- docs/api-guide/fields.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index f95608afb..a7ad4f70c 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -290,9 +290,9 @@ A date and time representation. Corresponds to `django.db.models.fields.DateTimeField`. -**Signature:** `DateTimeField(format=None, input_formats=None)` +**Signature:** `DateTimeField(format=api_settings.DATETIME_FORMAT, input_formats=None)` -* `format` - A string representing the output format. If not specified, this defaults to the same value as the `DATETIME_FORMAT` settings key, which will be `'iso-8601'` unless set. Setting to a format string indicates that `to_representation` return values should be coerced to string output. Format strings are described below. Setting this value to `None` indicates that Python `datetime` objects should be returned by `to_representation`. 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 the same value as the `DATETIME_FORMAT` settings key, which will be `'iso-8601'` unless set. Setting to a format string indicates that `to_representation` return values should be coerced to string output. Format strings are described below. Setting this value to `None` indicates that Python `datetime` objects should be returned by `to_representation`. 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']`. #### `DateTimeField` format strings. @@ -321,7 +321,7 @@ A date representation. Corresponds to `django.db.models.fields.DateField` -**Signature:** `DateField(format=None, input_formats=None)` +**Signature:** `DateField(format=api_settings.DATE_FORMAT, input_formats=None)` * `format` - A string representing the output format. If not specified, this defaults to the same value as the `DATE_FORMAT` settings key, which will be `'iso-8601'` unless set. Setting to a format string indicates that `to_representation` return values should be coerced to string output. Format strings are described below. Setting this value to `None` indicates that Python `date` objects should be returned by `to_representation`. In this case the date 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 `DATE_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`. @@ -336,7 +336,7 @@ A time representation. Corresponds to `django.db.models.fields.TimeField` -**Signature:** `TimeField(format=None, input_formats=None)` +**Signature:** `TimeField(format=api_settings.TIME_FORMAT, input_formats=None)` * `format` - A string representing the output format. If not specified, this defaults to the same value as the `TIME_FORMAT` settings key, which will be `'iso-8601'` unless set. Setting to a format string indicates that `to_representation` return values should be coerced to string output. Format strings are described below. Setting this value to `None` indicates that Python `time` objects should be returned by `to_representation`. In this case the time 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 `TIME_INPUT_FORMATS` setting will be used, which defaults to `['iso-8601']`. From e407dc7f019120540f57dc9a37bcc53a99ff2d9d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 28 Jul 2016 12:50:51 +0100 Subject: [PATCH 181/457] Added root_renderers argument (#4323) --- docs/api-guide/schemas.md | 4 ++++ rest_framework/routers.py | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index 848e57420..7f8af723e 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -155,6 +155,10 @@ schema URLs include a path prefix. If you want more flexibility over the schema output then you'll need to consider using `SchemaGenerator` instead. +#### root_renderers + +May be used to pass the set of renderer classes that can be used to render the API root endpoint. + ## Using SchemaGenerator The most common way to add a schema to your API is to use the `SchemaGenerator` diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 099c56d6c..4eec70bda 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -283,6 +283,10 @@ class DefaultRouter(SimpleRouter): self.schema_title = kwargs.pop('schema_title', None) self.schema_url = kwargs.pop('schema_url', None) self.schema_renderers = kwargs.pop('schema_renderers', self.default_schema_renderers) + if 'root_renderers' in kwargs: + self.root_renderers = kwargs.pop('root_renderers') + else: + self.root_renderers = list(api_settings.DEFAULT_RENDERER_CLASSES) super(DefaultRouter, self).__init__(*args, **kwargs) def get_api_root_view(self, api_urls=None): @@ -294,7 +298,7 @@ class DefaultRouter(SimpleRouter): for prefix, viewset, basename in self.registry: api_root_dict[prefix] = list_name.format(basename=basename) - view_renderers = list(api_settings.DEFAULT_RENDERER_CLASSES) + view_renderers = list(self.root_renderers) schema_media_types = [] if api_urls and self.schema_title: From 449ec1d724987885caa295e97ff98a50a06c718c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 28 Jul 2016 13:34:35 +0100 Subject: [PATCH 182/457] Version 3.4.1 [ci skip] (#4326) --- docs/topics/release-notes.md | 47 ++++++++++++++++++++++++++++++++++++ rest_framework/__init__.py | 2 +- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 5bfdad09b..831bf3446 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,24 @@ You can determine your currently installed version using `pip freeze`: ## 3.4.x series + ### 3.4.1 + + **Date**: [28th July 2016][3.4.1-milestone] + +* Added `root_renderers` argument to `DefaultRouter`. ([#4323][gh4323], [#4268][gh4268]) +* Added `url` and `schema_url` arguments. ([#4321][gh4321], [#4308][gh4308], [#4305][gh4305]) +* Unique together checks should apply to read-only fields which have a default. ([#4316][gh4316], [#4294][gh4294]) +* Set view.format_kwarg in schema generator. ([#4293][gh4293], [#4315][gh4315]) +* Fix schema generator for views with `pagination_class = None`. ([#4314][gh4314], [#4289][gh4289]) +* Fix schema generator for views with no `get_serializer_class`. ([#4265][gh4265], [#4285][gh4285]) +* Fixes for media type parameters in `Accept` and `Content-Type` headers. ([#4287][gh4287], [#4313][gh4313], [#4281][gh4281]) +* Use verbose_name instead of object_name in error messages. ([#4299][gh4299]) +* Minor version update to Twitter Bootstrap. ([#4307][gh4307]) +* SearchFilter raises error when using with related field. ([#4302][gh4302], [#4303][gh4303], [#4298][gh4298]) +* Adding support for RFC 4918 status codes. ([#4291][gh4291]) +* Add LICENSE.md to the built wheel. ([#4270][gh4270]) +* Serializing "complex" field returns None instead of the value since 3.4 ([#4272][gh4272], [#4273][gh4273], [#4288][gh4288]) + ### 3.4.0 **Date**: [14th July 2016][3.4.0-milestone] @@ -495,6 +513,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [3.3.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.3.2+Release%22 [3.3.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.3.3+Release%22 [3.4.0-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.0+Release%22 +[3.4.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.1+Release%22 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 @@ -896,3 +915,31 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh4255]: https://github.com/tomchristie/django-rest-framework/issues/4255 [gh4256]: https://github.com/tomchristie/django-rest-framework/issues/4256 [gh4259]: https://github.com/tomchristie/django-rest-framework/issues/4259 + + +[gh4323]: https://github.com/tomchristie/django-rest-framework/issues/4323 +[gh4268]: https://github.com/tomchristie/django-rest-framework/issues/4268 +[gh4321]: https://github.com/tomchristie/django-rest-framework/issues/4321 +[gh4308]: https://github.com/tomchristie/django-rest-framework/issues/4308 +[gh4305]: https://github.com/tomchristie/django-rest-framework/issues/4305 +[gh4316]: https://github.com/tomchristie/django-rest-framework/issues/4316 +[gh4294]: https://github.com/tomchristie/django-rest-framework/issues/4294 +[gh4293]: https://github.com/tomchristie/django-rest-framework/issues/4293 +[gh4315]: https://github.com/tomchristie/django-rest-framework/issues/4315 +[gh4314]: https://github.com/tomchristie/django-rest-framework/issues/4314 +[gh4289]: https://github.com/tomchristie/django-rest-framework/issues/4289 +[gh4265]: https://github.com/tomchristie/django-rest-framework/issues/4265 +[gh4285]: https://github.com/tomchristie/django-rest-framework/issues/4285 +[gh4287]: https://github.com/tomchristie/django-rest-framework/issues/4287 +[gh4313]: https://github.com/tomchristie/django-rest-framework/issues/4313 +[gh4281]: https://github.com/tomchristie/django-rest-framework/issues/4281 +[gh4299]: https://github.com/tomchristie/django-rest-framework/issues/4299 +[gh4307]: https://github.com/tomchristie/django-rest-framework/issues/4307 +[gh4302]: https://github.com/tomchristie/django-rest-framework/issues/4302 +[gh4303]: https://github.com/tomchristie/django-rest-framework/issues/4303 +[gh4298]: https://github.com/tomchristie/django-rest-framework/issues/4298 +[gh4291]: https://github.com/tomchristie/django-rest-framework/issues/4291 +[gh4270]: https://github.com/tomchristie/django-rest-framework/issues/4270 +[gh4272]: https://github.com/tomchristie/django-rest-framework/issues/4272 +[gh4273]: https://github.com/tomchristie/django-rest-framework/issues/4273 +[gh4288]: https://github.com/tomchristie/django-rest-framework/issues/4288 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index ab1a16597..5c61f8d7f 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.4.0' +__version__ = '3.4.1' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2016 Tom Christie' From 48a2f084aa2a616673c9adc5786f710acdc97d22 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 28 Jul 2016 13:38:05 +0100 Subject: [PATCH 183/457] Minor docs tweak [ci skip] --- 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 831bf3446..025231ccf 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,9 +40,9 @@ You can determine your currently installed version using `pip freeze`: ## 3.4.x series - ### 3.4.1 +### 3.4.1 - **Date**: [28th July 2016][3.4.1-milestone] +**Date**: [28th July 2016][3.4.1-milestone] * Added `root_renderers` argument to `DefaultRouter`. ([#4323][gh4323], [#4268][gh4268]) * Added `url` and `schema_url` arguments. ([#4321][gh4321], [#4308][gh4308], [#4305][gh4305]) From 5b071ab35e9f3b1d3f06e90741ded0e10e8a1651 Mon Sep 17 00:00:00 2001 From: Jaap Roes Date: Mon, 1 Aug 2016 13:05:47 +0200 Subject: [PATCH 184/457] Remove note about Django 1.3 (#4334) Remove note about Django 1.3 --- docs/api-guide/filtering.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 8664dcc8a..0ccd51dd3 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -241,7 +241,6 @@ For more details on using filter sets see the [django-filter documentation][djan * By default filtering is not enabled. If you want to use `DjangoFilterBackend` remember to make sure it is installed by using the `'DEFAULT_FILTER_BACKENDS'` setting. * When using boolean fields, you should use the values `True` and `False` in the URL query parameters, rather than `0`, `1`, `true` or `false`. (The allowed boolean values are currently hardwired in Django's [NullBooleanSelect implementation][nullbooleanselect].) * `django-filter` supports filtering across relationships, using Django's double-underscore syntax. -* For Django 1.3 support, make sure to install `django-filter` version 0.5.4, as later versions drop support for 1.3. --- From e997713313c36808ac7052a218b253087622e179 Mon Sep 17 00:00:00 2001 From: jsurloppe Date: Mon, 1 Aug 2016 15:14:55 +0200 Subject: [PATCH 185/457] urljoin with leading slash remove part of path (#4332) --- rest_framework/schemas.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 41dc82da1..02960083c 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -206,6 +206,9 @@ class SchemaGenerator(object): else: encoding = None + if self.url and path.startswith('/'): + path = path[1:] + return coreapi.Link( url=urlparse.urljoin(self.url, path), action=method.lower(), From aa349fe76729dbea1b8becf1846ce58c70871f35 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Aug 2016 16:14:26 +0100 Subject: [PATCH 186/457] Handle non-string input for IP fields (#4338) --- rest_framework/fields.py | 5 ++++- tests/test_fields.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 46d7ed09b..69cf9740b 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -804,7 +804,10 @@ class IPAddressField(CharField): self.validators.extend(validators) def to_internal_value(self, data): - if data and ':' in data: + if not isinstance(data, six.string_types): + self.fail('invalid', value=data) + + if ':' in data: try: if self.protocol in ('both', 'ipv6'): return clean_ipv6_address(data, self.unpack_ipv4) diff --git a/tests/test_fields.py b/tests/test_fields.py index 1149cc4b3..24ff25588 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -663,6 +663,7 @@ class TestIPAddressField(FieldValues): '127.122.111.2231': ['Enter a valid IPv4 or IPv6 address.'], '2001:::9652': ['Enter a valid IPv4 or IPv6 address.'], '2001:0db8:85a3:0042:1000:8a2e:0370:73341': ['Enter a valid IPv4 or IPv6 address.'], + 1000: ['Enter a valid IPv4 or IPv6 address.'], } outputs = {} field = serializers.IPAddressField() From 46a44e52aa8a0eae82cc9c1e290a83ecf156f81a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Aug 2016 17:15:41 +0100 Subject: [PATCH 187/457] Quantize incoming digitals (#4339) --- rest_framework/fields.py | 5 +++-- tests/test_fields.py | 20 ++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 69cf9740b..cbf12010c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -955,7 +955,7 @@ class DecimalField(Field): if value in (decimal.Decimal('Inf'), decimal.Decimal('-Inf')): self.fail('invalid') - return self.validate_precision(value) + return self.quantize(self.validate_precision(value)) def validate_precision(self, value): """ @@ -1018,7 +1018,8 @@ class DecimalField(Field): context.prec = self.max_digits return value.quantize( decimal.Decimal('.1') ** self.decimal_places, - context=context) + context=context + ) # Date & time fields... diff --git a/tests/test_fields.py b/tests/test_fields.py index 24ff25588..105a51973 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -912,6 +912,26 @@ class TestLocalizedDecimalField(TestCase): self.assertTrue(isinstance(field.to_representation(Decimal('1.1')), six.string_types)) +class TestQuantizedValueForDecimal(TestCase): + def test_int_quantized_value_for_decimal(self): + field = serializers.DecimalField(max_digits=4, decimal_places=2) + value = field.to_internal_value(12).as_tuple() + expected_digit_tuple = (0, (1, 2, 0, 0), -2) + self.assertEqual(value, expected_digit_tuple) + + def test_string_quantized_value_for_decimal(self): + field = serializers.DecimalField(max_digits=4, decimal_places=2) + value = field.to_internal_value('12').as_tuple() + expected_digit_tuple = (0, (1, 2, 0, 0), -2) + self.assertEqual(value, expected_digit_tuple) + + def test_part_precision_string_quantized_value_for_decimal(self): + field = serializers.DecimalField(max_digits=4, decimal_places=2) + value = field.to_internal_value('12.0').as_tuple() + expected_digit_tuple = (0, (1, 2, 0, 0), -2) + self.assertEqual(value, expected_digit_tuple) + + class TestNoDecimalPlaces(FieldValues): valid_inputs = { '0.12345': Decimal('0.12345'), From 3ef3fee92627d832962d1e5aed02c19a3eaa554b Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 1 Aug 2016 18:44:58 +0100 Subject: [PATCH 188/457] Descriptive error from FileUploadParser when filename not included. (#4340) * Descriptive error from FileUploadParser when filename not included. * Consistent handling of upload filenames --- rest_framework/parsers.py | 15 +++++++++++---- tests/test_parsers.py | 25 ++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index ab74a6e58..238382364 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -118,6 +118,10 @@ class FileUploadParser(BaseParser): Parser for file upload data. """ media_type = '*/*' + errors = { + 'unhandled': 'FileUpload parse error - none of upload handlers can handle the stream', + 'no_filename': 'Missing filename. Request should include a Content-Disposition header with a filename parameter.', + } def parse(self, stream, media_type=None, parser_context=None): """ @@ -134,6 +138,9 @@ class FileUploadParser(BaseParser): upload_handlers = request.upload_handlers filename = self.get_filename(stream, media_type, parser_context) + if not filename: + raise ParseError(self.errors['no_filename']) + # Note that this code is extracted from Django's handling of # file uploads in MultiPartParser. content_type = meta.get('HTTP_CONTENT_TYPE', @@ -146,7 +153,7 @@ class FileUploadParser(BaseParser): # See if the handler will want to take care of the parsing. for handler in upload_handlers: - result = handler.handle_raw_input(None, + result = handler.handle_raw_input(stream, meta, content_length, None, @@ -178,10 +185,10 @@ class FileUploadParser(BaseParser): for index, handler in enumerate(upload_handlers): file_obj = handler.file_complete(counters[index]) - if file_obj: + if file_obj is not None: return DataAndFiles({}, {'file': file_obj}) - raise ParseError("FileUpload parse error - " - "none of upload handlers can handle the stream") + + raise ParseError(self.errors['unhandled']) def get_filename(self, stream, media_type, parser_context): """ diff --git a/tests/test_parsers.py b/tests/test_parsers.py index 1e0f2e17f..f3af6817f 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -2,8 +2,11 @@ from __future__ import unicode_literals +import pytest from django import forms -from django.core.files.uploadhandler import MemoryFileUploadHandler +from django.core.files.uploadhandler import ( + MemoryFileUploadHandler, TemporaryFileUploadHandler +) from django.test import TestCase from django.utils.six.moves import StringIO @@ -63,8 +66,9 @@ class TestFileUploadParser(TestCase): parser = FileUploadParser() self.stream.seek(0) self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = '' - with self.assertRaises(ParseError): + with pytest.raises(ParseError) as excinfo: parser.parse(self.stream, None, self.parser_context) + assert str(excinfo.value) == 'Missing filename. Request should include a Content-Disposition header with a filename parameter.' def test_parse_missing_filename_multiple_upload_handlers(self): """ @@ -78,8 +82,23 @@ class TestFileUploadParser(TestCase): MemoryFileUploadHandler() ) self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = '' - with self.assertRaises(ParseError): + with pytest.raises(ParseError) as excinfo: parser.parse(self.stream, None, self.parser_context) + assert str(excinfo.value) == 'Missing filename. Request should include a Content-Disposition header with a filename parameter.' + + def test_parse_missing_filename_large_file(self): + """ + Parse raw file upload when filename is missing with TemporaryFileUploadHandler. + """ + parser = FileUploadParser() + self.stream.seek(0) + self.parser_context['request'].upload_handlers = ( + TemporaryFileUploadHandler(), + ) + self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = '' + with pytest.raises(ParseError) as excinfo: + parser.parse(self.stream, None, self.parser_context) + assert str(excinfo.value) == 'Missing filename. Request should include a Content-Disposition header with a filename parameter.' def test_get_filename(self): parser = FileUploadParser() From 296e47a9f8f5303d7862b68db7277aa87886e8a9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Aug 2016 10:23:56 +0100 Subject: [PATCH 189/457] Update from Django 1.10 beta to Django 1.10 (#4344) --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 89655aee2..1e8a7e5c4 100644 --- a/tox.ini +++ b/tox.ini @@ -16,8 +16,8 @@ setenv = PYTHONWARNINGS=once deps = django18: Django==1.8.14 - django19: Django==1.9.8 - django110: Django==1.10rc1 + django19: Django==1.9.9 + django110: Django==1.10 djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From e37619f7410bcfc472db56635aa573b33f83e92a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Aug 2016 13:05:12 +0100 Subject: [PATCH 190/457] Serializer defaults should not be included in partial updates. (#4346) Serializer default values should not be included in partial updates --- docs/api-guide/fields.md | 4 +++- rest_framework/fields.py | 3 ++- tests/test_serializer.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index a7ad4f70c..4b566d37e 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -49,7 +49,9 @@ Defaults to `False` ### `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 behaviour is to not populate the attribute at all. + +The `default` is not applied during partial update operations. In the partial update case only fields that are provided in the incoming data will have a validated value returned. May be set to a function or other callable, in which case the value will be evaluated each time it is used. When called, it will receive no arguments. If the callable has a `set_context` method, that will be called each time before getting the value with the field instance as only argument. This works the same way as for [validators](validators.md#using-set_context). diff --git a/rest_framework/fields.py b/rest_framework/fields.py index cbf12010c..704dbaf3f 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -435,7 +435,8 @@ class Field(object): return `empty`, indicating that no value should be set in the validated data for this field. """ - if self.default is empty: + if self.default is empty or getattr(self.root, 'partial', False): + # No default, or this is a partial update. raise SkipField() if callable(self.default): if hasattr(self.default, 'set_context'): diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 741c6ab17..4e9080909 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -309,3 +309,31 @@ class TestCacheSerializerData: pickled = pickle.dumps(serializer.data) data = pickle.loads(pickled) assert data == {'field1': 'a', 'field2': 'b'} + + +class TestDefaultInclusions: + def setup(self): + class ExampleSerializer(serializers.Serializer): + char = serializers.CharField(read_only=True, default='abc') + integer = serializers.IntegerField() + self.Serializer = ExampleSerializer + + def test_default_should_included_on_create(self): + serializer = self.Serializer(data={'integer': 456}) + assert serializer.is_valid() + assert serializer.validated_data == {'char': 'abc', 'integer': 456} + assert serializer.errors == {} + + def test_default_should_be_included_on_update(self): + instance = MockObject(char='def', integer=123) + serializer = self.Serializer(instance, data={'integer': 456}) + assert serializer.is_valid() + assert serializer.validated_data == {'char': 'abc', 'integer': 456} + assert serializer.errors == {} + + def test_default_should_not_be_included_on_partial_update(self): + instance = MockObject(char='def', integer=123) + serializer = self.Serializer(instance, data={'integer': 456}, partial=True) + assert serializer.is_valid() + assert serializer.validated_data == {'integer': 456} + assert serializer.errors == {} From 9f5e841daf8e086de2b2b90153356a52ce783283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Fleschenberg?= Date: Tue, 2 Aug 2016 14:11:41 +0200 Subject: [PATCH 191/457] Change template context generation in TemplateHTMLRenderer (#4236) - Change the name of ``resolve_context()`` to ``get_template_context()``. - Pass the renderer context to this method, to give subclasses more flexibility when overriding. --- rest_framework/renderers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index e313998d1..ef7747eaf 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -166,13 +166,14 @@ class TemplateHTMLRenderer(BaseRenderer): template_names = self.get_template_names(response, view) template = self.resolve_template(template_names) - context = self.resolve_context(data, request, response) + context = self.get_template_context(data, renderer_context) return template_render(template, context, request=request) def resolve_template(self, template_names): return loader.select_template(template_names) - def resolve_context(self, data, request, response): + def get_template_context(self, data, renderer_context): + response = renderer_context['response'] if response.exception: data['status_code'] = response.status_code return data From bda16a518a03ca79c912c8b90adfce3626cf1069 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Aug 2016 13:33:14 +0100 Subject: [PATCH 192/457] Dedent tabs. (#4347) --- rest_framework/utils/formatting.py | 13 +++++++++++-- tests/test_description.py | 5 +++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index 67aabd3f1..ca5b33c5e 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -32,13 +32,22 @@ def dedent(content): unindented text on the initial line. """ content = force_text(content) - whitespace_counts = [len(line) - len(line.lstrip(' ')) - for line in content.splitlines()[1:] if line.lstrip()] + whitespace_counts = [ + len(line) - len(line.lstrip(' ')) + for line in content.splitlines()[1:] if line.lstrip() + ] + tab_counts = [ + len(line) - len(line.lstrip('\t')) + for line in content.splitlines()[1:] if line.lstrip() + ] # unindent the content if needed if whitespace_counts: whitespace_pattern = '^' + (' ' * min(whitespace_counts)) content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) + elif tab_counts: + whitespace_pattern = '^' + ('\t' * min(whitespace_counts)) + content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) return content.strip() diff --git a/tests/test_description.py b/tests/test_description.py index 1683e106b..fcb88287b 100644 --- a/tests/test_description.py +++ b/tests/test_description.py @@ -6,6 +6,7 @@ from django.test import TestCase from django.utils.encoding import python_2_unicode_compatible from rest_framework.compat import apply_markdown +from rest_framework.utils.formatting import dedent from rest_framework.views import APIView @@ -120,3 +121,7 @@ class TestViewNamesAndDescriptions(TestCase): gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21 lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21 self.assertTrue(gte_21_match or lt_21_match) + + +def test_dedent_tabs(): + assert dedent("\tfirst string\n\n\tsecond string") == 'first string\n\n\tsecond string' From 5500b265bceb1d964725d3fd13f60429b834dbf4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Aug 2016 14:14:36 +0100 Subject: [PATCH 193/457] Test cases for DictField with allow_null options (#4348) --- tests/test_fields.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_fields.py b/tests/test_fields.py index 105a51973..92f4548e5 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1594,6 +1594,29 @@ class TestDictField(FieldValues): "Remove `source=` from the field declaration." ) + def test_allow_null(self): + """ + If `allow_null=True` then `None` is a valid input. + """ + field = serializers.DictField(allow_null=True) + output = field.run_validation(None) + assert output is None + + +class TestDictFieldWithNullChild(FieldValues): + """ + Values for `ListField` with allow_null CharField as child. + """ + valid_inputs = [ + ({'a': None, 'b': '2', 3: 3}, {'a': None, 'b': '2', '3': '3'}), + ] + invalid_inputs = [ + ] + outputs = [ + ({'a': None, 'b': '2', 3: 3}, {'a': None, 'b': '2', '3': '3'}), + ] + field = serializers.DictField(child=serializers.CharField(allow_null=True)) + class TestUnvalidatedDictField(FieldValues): """ From a9a097496ec86d4b5e7db756fe51a39a57f5b370 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 2 Aug 2016 14:33:15 +0100 Subject: [PATCH 194/457] extra_kwargs takes precedence over uniqueness kwargs (#4349) --- rest_framework/serializers.py | 5 ++--- tests/test_model_serializer.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d5e6b66ed..27c8cc229 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1324,9 +1324,8 @@ class ModelSerializer(Serializer): # Update `extra_kwargs` with any new options. for key, value in uniqueness_extra_kwargs.items(): if key in extra_kwargs: - extra_kwargs[key].update(value) - else: - extra_kwargs[key] = value + value.update(extra_kwargs[key]) + extra_kwargs[key] = value return extra_kwargs, hidden_fields diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 2cf6cb04c..b2d336d84 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -976,3 +976,22 @@ class TestModelFieldValues(TestCase): source = OneToOneSourceTestModel(target=target) serializer = ExampleSerializer(source) self.assertEqual(serializer.data, {'target': 1}) + + +class TestUniquenessOverride(TestCase): + def test_required_not_overwritten(self): + class TestModel(models.Model): + field_1 = models.IntegerField(null=True) + field_2 = models.IntegerField() + + class Meta: + unique_together = (('field_1', 'field_2'),) + + class TestSerializer(serializers.ModelSerializer): + class Meta: + model = TestModel + extra_kwargs = {'field_1': {'required': False}} + + fields = TestSerializer().fields + self.assertFalse(fields['field_1'].required) + self.assertTrue(fields['field_2'].required) From 54096dc22f255140f148906e85da5e4be0048f10 Mon Sep 17 00:00:00 2001 From: Corentin Smith Date: Thu, 4 Aug 2016 23:06:35 +0200 Subject: [PATCH 195/457] Add imports in validators docs (#4355) --- docs/api-guide/validators.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index a059f1197..f04c74c3c 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -64,6 +64,8 @@ It takes a single required argument, and an optional `messages` argument: This validator should be applied to *serializer fields*, like so: + from rest_framework.validators import UniqueValidator + slug = SlugField( max_length=100, validators=[UniqueValidator(queryset=BlogPost.objects.all())] @@ -80,6 +82,8 @@ It has two required arguments, and a single optional `messages` argument: The validator should be applied to *serializer classes*, like so: + from rest_framework.validators import UniqueTogetherValidator + class ExampleSerializer(serializers.Serializer): # ... class Meta: @@ -114,6 +118,8 @@ These validators can be used to enforce the `unique_for_date`, `unique_for_month The validator should be applied to *serializer classes*, like so: + from rest_framework.validators import UniqueForYearValidator + class ExampleSerializer(serializers.Serializer): # ... class Meta: @@ -183,7 +189,7 @@ It takes a single argument, which is the default value or callable that should b created_at = serializers.DateTimeField( read_only=True, - default=CreateOnlyDefault(timezone.now) + default=serializers.CreateOnlyDefault(timezone.now) ) --- From aff146ae83a64676cdcaf169832c29f4132cac6a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Aug 2016 10:23:40 +0100 Subject: [PATCH 196/457] Filter HEAD out from schemas (#4357) --- rest_framework/schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 02960083c..b5d2e0254 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -167,7 +167,7 @@ class SchemaGenerator(object): return [ method for method in - callback.cls().allowed_methods if method != 'OPTIONS' + callback.cls().allowed_methods if method not in ('OPTIONS', 'HEAD') ] def get_key(self, path, method, callback): From 11a2468379496a4e9ed29d84d135825a927a5595 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Aug 2016 11:04:01 +0100 Subject: [PATCH 197/457] Access `request.user.is_authenticated` as property not method, under Django 1.10+ (#4358) * For Django >=1.10 use user.is_authenticated, not user.is_authenticated() --- rest_framework/compat.py | 6 ++++++ rest_framework/permissions.py | 9 ++++++--- rest_framework/throttling.py | 7 ++++--- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 3143e7654..1ab1478f1 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -122,6 +122,12 @@ def _resolve_model(obj): raise ValueError("{0} is not a Django model".format(obj)) +def is_authenticated(user): + if django.VERSION < (1, 10): + return user.is_authenticated() + return user.is_authenticated + + def get_related_model(field): if django.VERSION < (1, 9): return _resolve_model(field.rel.to) diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py index 8f5de0256..dd2d35ccd 100644 --- a/rest_framework/permissions.py +++ b/rest_framework/permissions.py @@ -5,6 +5,9 @@ from __future__ import unicode_literals from django.http import Http404 +from rest_framework.compat import is_authenticated + + SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS') @@ -44,7 +47,7 @@ class IsAuthenticated(BasePermission): """ def has_permission(self, request, view): - return request.user and request.user.is_authenticated() + return request.user and is_authenticated(request.user) class IsAdminUser(BasePermission): @@ -65,7 +68,7 @@ class IsAuthenticatedOrReadOnly(BasePermission): return ( request.method in SAFE_METHODS or request.user and - request.user.is_authenticated() + is_authenticated(request.user) ) @@ -127,7 +130,7 @@ class DjangoModelPermissions(BasePermission): return ( request.user and - (request.user.is_authenticated() or not self.authenticated_users_only) and + (is_authenticated(request.user) or not self.authenticated_users_only) and request.user.has_perms(perms) ) diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 1449f501b..57f24d13f 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -8,6 +8,7 @@ import time from django.core.cache import cache as default_cache from django.core.exceptions import ImproperlyConfigured +from rest_framework.compat import is_authenticated from rest_framework.settings import api_settings @@ -173,7 +174,7 @@ class AnonRateThrottle(SimpleRateThrottle): scope = 'anon' def get_cache_key(self, request, view): - if request.user.is_authenticated(): + if is_authenticated(request.user): return None # Only throttle unauthenticated requests. return self.cache_format % { @@ -193,7 +194,7 @@ class UserRateThrottle(SimpleRateThrottle): scope = 'user' def get_cache_key(self, request, view): - if request.user.is_authenticated(): + if is_authenticated(request.user): ident = request.user.pk else: ident = self.get_ident(request) @@ -241,7 +242,7 @@ class ScopedRateThrottle(SimpleRateThrottle): Otherwise generate the unique cache key by concatenating the user id with the '.throttle_scope` property of the view. """ - if request.user.is_authenticated(): + if is_authenticated(request.user): ident = request.user.pk else: ident = self.get_ident(request) From d5178c92462f6d18337fb0d85b6f230e812ed2fb Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Aug 2016 11:19:39 +0100 Subject: [PATCH 198/457] Include kwargs passed to 'as_view' when generating schemas (#4359) --- rest_framework/schemas.py | 2 ++ rest_framework/viewsets.py | 1 + tests/test_schemas.py | 20 ++++++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index b5d2e0254..688deec88 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -195,6 +195,8 @@ class SchemaGenerator(object): Return a `coreapi.Link` instance for the given endpoint. """ view = callback.cls() + for attr, val in getattr(callback, 'initkwargs', {}).items(): + setattr(view, attr, val) fields = self.get_path_fields(path, method, callback, view) fields += self.get_serializer_fields(path, method, callback, view) diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 7687448c4..2f440c567 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -97,6 +97,7 @@ class ViewSetMixin(object): # generation can pick out these bits of information from a # resolved URL. view.cls = cls + view.initkwargs = initkwargs view.suffix = initkwargs.get('suffix', None) view.actions = actions return csrf_exempt(view) diff --git a/tests/test_schemas.py b/tests/test_schemas.py index a32b8a117..6c02c9d23 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -5,6 +5,7 @@ from django.test import TestCase, override_settings from rest_framework import filters, pagination, permissions, serializers from rest_framework.compat import coreapi +from rest_framework.decorators import detail_route from rest_framework.response import Response from rest_framework.routers import DefaultRouter from rest_framework.schemas import SchemaGenerator @@ -27,12 +28,21 @@ class ExampleSerializer(serializers.Serializer): b = serializers.CharField(required=False) +class AnotherSerializer(serializers.Serializer): + c = serializers.CharField(required=True) + d = serializers.CharField(required=False) + + class ExampleViewSet(ModelViewSet): pagination_class = ExamplePagination permission_classes = [permissions.IsAuthenticatedOrReadOnly] filter_backends = [filters.OrderingFilter] serializer_class = ExampleSerializer + @detail_route(methods=['post'], serializer_class=AnotherSerializer) + def custom_action(self, request, pk): + return super(ExampleSerializer, self).retrieve(self, request) + class ExampleView(APIView): permission_classes = [permissions.IsAuthenticatedOrReadOnly] @@ -120,6 +130,16 @@ class TestRouterGeneratedSchema(TestCase): coreapi.Field('pk', required=True, location='path') ] ), + 'custom_action': coreapi.Link( + url='/example/{pk}/custom_action/', + action='post', + encoding='application/json', + fields=[ + coreapi.Field('pk', required=True, location='path'), + coreapi.Field('c', required=True, location='form'), + coreapi.Field('d', required=False, location='form'), + ] + ), 'update': coreapi.Link( url='/example/{pk}/', action='put', From f9cf22edc88a19e83b70fc72de7844c948023f44 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Aug 2016 12:38:19 +0100 Subject: [PATCH 199/457] Version 3.4.2 (#4360) --- docs/topics/release-notes.md | 47 ++++++++++++++++++++++++++++++++++++ rest_framework/__init__.py | 2 +- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 025231ccf..7baff2277 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,24 @@ You can determine your currently installed version using `pip freeze`: ## 3.4.x series +### 3.4.2 + +**Date**: [5th August 2016][3.4.2-milestone] + +Include kwargs passed to 'as_view' when generating schemas. ([#4359][gh4359], [#4330][gh4330], [#4331][gh4331]) +Access `request.user.is_authenticated` as property not method, under Django 1.10+ ([#4358][gh4358], [#4354][gh4354]) +Filter HEAD out from schemas. ([#4357][gh4357]) +extra_kwargs takes precedence over uniqueness kwargs. ([#4198][gh4198], [#4199][gh4199], [#4349][gh4349]) +Correct descriptions when tabs are used in code indentation. ([#4345][gh4345], [#4347][gh4347]) +Change template context generation in TemplateHTMLRenderer. ([#4236][gh4236]) +Serializer defaults should not be included in partial updates. ([#4346][gh4346], [#3565][gh3565]) +Consistent behavior & descriptive error from FileUploadParser when filename not included. ([#4340][gh4340], [#3610][gh3610], [#4292][gh4292], [#4296][gh4296]) +DecimalField quantizes incoming digitals. ([#4339][gh4339], [#4318][gh4318]) +Handle non-string input for IP fields. ([#4335][gh4335], [#4336][gh4336], [#4338][gh4338]) +Fix leading slash handling when Schema generation includes a root URL. ([#4332][gh4332]) +Test cases for DictField with allow_null options. ([#4348][gh4348]) +Update tests from Django 1.10 beta to Django 1.10. ([#4344][gh4344]) + ### 3.4.1 **Date**: [28th July 2016][3.4.1-milestone] @@ -514,6 +532,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [3.3.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.3.3+Release%22 [3.4.0-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.0+Release%22 [3.4.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.1+Release%22 +[3.4.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.2+Release%22 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 @@ -943,3 +962,31 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh4272]: https://github.com/tomchristie/django-rest-framework/issues/4272 [gh4273]: https://github.com/tomchristie/django-rest-framework/issues/4273 [gh4288]: https://github.com/tomchristie/django-rest-framework/issues/4288 + + +[gh3565]: https://github.com/tomchristie/django-rest-framework/issues/3565 +[gh3610]: https://github.com/tomchristie/django-rest-framework/issues/3610 +[gh4198]: https://github.com/tomchristie/django-rest-framework/issues/4198 +[gh4199]: https://github.com/tomchristie/django-rest-framework/issues/4199 +[gh4236]: https://github.com/tomchristie/django-rest-framework/issues/4236 +[gh4292]: https://github.com/tomchristie/django-rest-framework/issues/4292 +[gh4296]: https://github.com/tomchristie/django-rest-framework/issues/4296 +[gh4318]: https://github.com/tomchristie/django-rest-framework/issues/4318 +[gh4330]: https://github.com/tomchristie/django-rest-framework/issues/4330 +[gh4331]: https://github.com/tomchristie/django-rest-framework/issues/4331 +[gh4332]: https://github.com/tomchristie/django-rest-framework/issues/4332 +[gh4335]: https://github.com/tomchristie/django-rest-framework/issues/4335 +[gh4336]: https://github.com/tomchristie/django-rest-framework/issues/4336 +[gh4338]: https://github.com/tomchristie/django-rest-framework/issues/4338 +[gh4339]: https://github.com/tomchristie/django-rest-framework/issues/4339 +[gh4340]: https://github.com/tomchristie/django-rest-framework/issues/4340 +[gh4344]: https://github.com/tomchristie/django-rest-framework/issues/4344 +[gh4345]: https://github.com/tomchristie/django-rest-framework/issues/4345 +[gh4346]: https://github.com/tomchristie/django-rest-framework/issues/4346 +[gh4347]: https://github.com/tomchristie/django-rest-framework/issues/4347 +[gh4348]: https://github.com/tomchristie/django-rest-framework/issues/4348 +[gh4349]: https://github.com/tomchristie/django-rest-framework/issues/4349 +[gh4354]: https://github.com/tomchristie/django-rest-framework/issues/4354 +[gh4357]: https://github.com/tomchristie/django-rest-framework/issues/4357 +[gh4358]: https://github.com/tomchristie/django-rest-framework/issues/4358 +[gh4359]: https://github.com/tomchristie/django-rest-framework/issues/4359 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 5c61f8d7f..fb6da68ce 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.4.1' +__version__ = '3.4.2' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2016 Tom Christie' From 35320b1f2d07fcc20b21f5ebc71629c1fc4ebb21 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Aug 2016 12:41:15 +0100 Subject: [PATCH 200/457] Add bullet points to release notes [ci skip] --- docs/topics/release-notes.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 7baff2277..692af57e9 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -44,19 +44,19 @@ You can determine your currently installed version using `pip freeze`: **Date**: [5th August 2016][3.4.2-milestone] -Include kwargs passed to 'as_view' when generating schemas. ([#4359][gh4359], [#4330][gh4330], [#4331][gh4331]) -Access `request.user.is_authenticated` as property not method, under Django 1.10+ ([#4358][gh4358], [#4354][gh4354]) -Filter HEAD out from schemas. ([#4357][gh4357]) -extra_kwargs takes precedence over uniqueness kwargs. ([#4198][gh4198], [#4199][gh4199], [#4349][gh4349]) -Correct descriptions when tabs are used in code indentation. ([#4345][gh4345], [#4347][gh4347]) -Change template context generation in TemplateHTMLRenderer. ([#4236][gh4236]) -Serializer defaults should not be included in partial updates. ([#4346][gh4346], [#3565][gh3565]) -Consistent behavior & descriptive error from FileUploadParser when filename not included. ([#4340][gh4340], [#3610][gh3610], [#4292][gh4292], [#4296][gh4296]) -DecimalField quantizes incoming digitals. ([#4339][gh4339], [#4318][gh4318]) -Handle non-string input for IP fields. ([#4335][gh4335], [#4336][gh4336], [#4338][gh4338]) -Fix leading slash handling when Schema generation includes a root URL. ([#4332][gh4332]) -Test cases for DictField with allow_null options. ([#4348][gh4348]) -Update tests from Django 1.10 beta to Django 1.10. ([#4344][gh4344]) +* Include kwargs passed to 'as_view' when generating schemas. ([#4359][gh4359], [#4330][gh4330], [#4331][gh4331]) +* Access `request.user.is_authenticated` as property not method, under Django 1.10+ ([#4358][gh4358], [#4354][gh4354]) +* Filter HEAD out from schemas. ([#4357][gh4357]) +* extra_kwargs takes precedence over uniqueness kwargs. ([#4198][gh4198], [#4199][gh4199], [#4349][gh4349]) +* Correct descriptions when tabs are used in code indentation. ([#4345][gh4345], [#4347][gh4347])* +* Change template context generation in TemplateHTMLRenderer. ([#4236][gh4236]) +* Serializer defaults should not be included in partial updates. ([#4346][gh4346], [#3565][gh3565]) +* Consistent behavior & descriptive error from FileUploadParser when filename not included. ([#4340][gh4340], [#3610][gh3610], [#4292][gh4292], [#4296][gh4296]) +* DecimalField quantizes incoming digitals. ([#4339][gh4339], [#4318][gh4318]) +* Handle non-string input for IP fields. ([#4335][gh4335], [#4336][gh4336], [#4338][gh4338]) +* Fix leading slash handling when Schema generation includes a root URL. ([#4332][gh4332]) +* Test cases for DictField with allow_null options. ([#4348][gh4348]) +* Update tests from Django 1.10 beta to Django 1.10. ([#4344][gh4344]) ### 3.4.1 From bb613c5ad19bb10a8e85a2bb3520bd4c4e6879a5 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 5 Aug 2016 13:33:25 +0100 Subject: [PATCH 201/457] Version 3.4.3 (#4361) * Version 3.4.3 --- docs/topics/release-notes.md | 10 ++++++++++ rest_framework/__init__.py | 2 +- rest_framework/renderers.py | 11 +++++++++-- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 692af57e9..6edbd2544 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,12 @@ You can determine your currently installed version using `pip freeze`: ## 3.4.x series +### 3.4.3 + +**Date**: [5th August 2016][3.4.3-milestone] + +* Include fallaback for users of older TemplateHTMLRenderer internal API. ([#4361][gh4361]) + ### 3.4.2 **Date**: [5th August 2016][3.4.2-milestone] @@ -533,6 +539,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [3.4.0-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.0+Release%22 [3.4.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.1+Release%22 [3.4.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.2+Release%22 +[3.4.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.3+Release%22 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 @@ -990,3 +997,6 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh4357]: https://github.com/tomchristie/django-rest-framework/issues/4357 [gh4358]: https://github.com/tomchristie/django-rest-framework/issues/4358 [gh4359]: https://github.com/tomchristie/django-rest-framework/issues/4359 + + +[gh4361]: https://github.com/tomchristie/django-rest-framework/issues/4361 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index fb6da68ce..19f83ecab 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.4.2' +__version__ = '3.4.3' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2016 Tom Christie' diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index ef7747eaf..91d9e9072 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -166,7 +166,11 @@ class TemplateHTMLRenderer(BaseRenderer): template_names = self.get_template_names(response, view) template = self.resolve_template(template_names) - context = self.get_template_context(data, renderer_context) + if hasattr(self, 'resolve_context'): + # Fallback for older versions. + context = self.resolve_context(self, data, request, response) + else: + context = self.get_template_context(data, renderer_context) return template_render(template, context, request=request) def resolve_template(self, template_names): @@ -229,7 +233,10 @@ class StaticHTMLRenderer(TemplateHTMLRenderer): if response and response.exception: request = renderer_context['request'] template = self.get_exception_template(response) - context = self.resolve_context(data, request, response) + if hasattr(self, 'resolve_context'): + context = self.resolve_context(data, request, response) + else: + context = self.get_template_context(data, renderer_context) return template_render(template, context, request=request) return data From 672e5a0f96c4e860f4eeee85f9cd26c8c4070d63 Mon Sep 17 00:00:00 2001 From: Marlon Date: Fri, 5 Aug 2016 11:57:43 -0700 Subject: [PATCH 202/457] Fix minor typo --- docs/api-guide/fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 4b566d37e..f986f1508 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -488,7 +488,7 @@ This field is used by default with `ModelSerializer` when including field names **Signature**: `ReadOnlyField()` -For example, is `has_expired` was a property on the `Account` model, then the following serializer would automatically generate it as a `ReadOnlyField`: +For example, if `has_expired` was a property on the `Account` model, then the following serializer would automatically generate it as a `ReadOnlyField`: class AccountSerializer(serializers.ModelSerializer): class Meta: From febaa4db00c322d169a5e60ecd22c909e8a836c2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 8 Aug 2016 09:28:15 +0100 Subject: [PATCH 203/457] Add import in docs. [ci skip] --- docs/api-guide/throttling.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index 4eef4fd5d..51d2beef1 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -184,6 +184,8 @@ If the `.wait()` method is implemented and the request is throttled, then a `Ret The following is an example of a rate throttle, that will randomly throttle 1 in every 10 requests. + import random + class RandomRateThrottle(throttling.BaseThrottle): def allow_request(self, request, view): return random.randint(1, 10) == 1 From e1768bdc161ee6c4fab422d0f47e54f9bf257ce2 Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Mon, 8 Aug 2016 10:32:22 +0200 Subject: [PATCH 204/457] Fixed various typos (#4366) --- rest_framework/compat.py | 2 +- rest_framework/fields.py | 6 +++--- rest_framework/negotiation.py | 2 +- rest_framework/pagination.py | 16 ++++++++-------- rest_framework/serializers.py | 4 ++-- rest_framework/templatetags/rest_framework.py | 2 +- rest_framework/utils/field_mapping.py | 2 +- rest_framework/utils/html.py | 2 +- rest_framework/versioning.py | 4 ++-- rest_framework/viewsets.py | 2 +- tests/test_authentication.py | 2 +- tests/test_fields.py | 4 ++-- tests/test_filters.py | 6 +++--- tests/test_model_serializer.py | 2 +- tests/test_pagination.py | 6 +++--- tests/test_templatetags.py | 2 +- tests/test_testing.py | 2 +- 17 files changed, 33 insertions(+), 33 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 1ab1478f1..cee430a84 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -168,7 +168,7 @@ except ImportError: crispy_forms = None -# coreapi is optional (Note that uritemplate is a dependancy of coreapi) +# coreapi is optional (Note that uritemplate is a dependency of coreapi) try: import coreapi import uritemplate diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 704dbaf3f..b4346cd85 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -395,8 +395,8 @@ class Field(object): # determine if we should use null instead. return '' if getattr(self, 'allow_blank', False) else None elif ret == '' and not self.required: - # If the field is blank, and emptyness is valid then - # determine if we should use emptyness instead. + # If the field is blank, and emptiness is valid then + # determine if we should use emptiness instead. return '' if getattr(self, 'allow_blank', False) else empty return ret return dictionary.get(self.field_name, empty) @@ -1346,7 +1346,7 @@ class FilePathField(ChoiceField): def __init__(self, path, match=None, recursive=False, allow_files=True, allow_folders=False, required=None, **kwargs): - # Defer to Django's FilePathField implmentation to get the + # Defer to Django's FilePathField implementation to get the # valid set of choices. field = DjangoFilePathField( path, match=match, recursive=recursive, allow_files=allow_files, diff --git a/rest_framework/negotiation.py b/rest_framework/negotiation.py index 2a2b6f168..ca1b59f12 100644 --- a/rest_framework/negotiation.py +++ b/rest_framework/negotiation.py @@ -90,7 +90,7 @@ class DefaultContentNegotiation(BaseContentNegotiation): def get_accept_list(self, request): """ - Given the incoming request, return a tokenised list of media + Given the incoming request, return a tokenized list of media type strings. """ header = request.META.get('HTTP_ACCEPT', '*/*') diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 6ad10d860..77c10337e 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -64,10 +64,10 @@ def _get_displayed_page_numbers(current, final): This implementation gives one page to each side of the cursor, or two pages to the side when the cursor is at the edge, then - ensures that any breaks between non-continous page numbers never + ensures that any breaks between non-continuous page numbers never remove only a single page. - For an alernativative implementation which gives two pages to each side of + For an alternative implementation which gives two pages to each side of the cursor, eg. as in GitHub issue list pagination, see: https://gist.github.com/tomchristie/321140cebb1c4a558b15 @@ -476,10 +476,10 @@ class CursorPagination(BasePagination): # Determine the position of the final item following the page. if len(results) > len(self.page): - has_following_postion = True + has_following_position = True following_position = self._get_position_from_instance(results[-1], self.ordering) else: - has_following_postion = False + has_following_position = False following_position = None # If we have a reverse queryset, then the query ordering was in reverse @@ -490,14 +490,14 @@ class CursorPagination(BasePagination): if reverse: # Determine next and previous positions for reverse cursors. self.has_next = (current_position is not None) or (offset > 0) - self.has_previous = has_following_postion + self.has_previous = has_following_position if self.has_next: self.next_position = current_position if self.has_previous: self.previous_position = following_position else: # Determine next and previous positions for forward cursors. - self.has_next = has_following_postion + self.has_next = has_following_position self.has_previous = (current_position is not None) or (offset > 0) if self.has_next: self.next_position = following_position @@ -534,7 +534,7 @@ class CursorPagination(BasePagination): # our marker. break - # The item in this postion has the same position as the item + # The item in this position has the same position as the item # following it, we can't use it as a marker position, so increment # the offset and keep seeking to the previous item. compare = position @@ -582,7 +582,7 @@ class CursorPagination(BasePagination): # our marker. break - # The item in this postion has the same position as the item + # The item in this position has the same position as the item # following it, we can't use it as a marker position, so increment # the offset and keep seeking to the previous item. compare = position diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 27c8cc229..41412af8a 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1383,7 +1383,7 @@ class ModelSerializer(Serializer): def get_unique_together_validators(self): """ - Determine a default set of validators for any unique_together contraints. + Determine a default set of validators for any unique_together constraints. """ model_class_inheritance_tree = ( [self.Meta.model] + @@ -1414,7 +1414,7 @@ class ModelSerializer(Serializer): def get_unique_for_date_validators(self): """ - Determine a default set of validators for the following contraints: + Determine a default set of validators for the following constraints: * unique_for_date * unique_for_month diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 05a7ce04f..225edcbee 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -189,7 +189,7 @@ def urlize_quoted_links(text, trim_url_limit=None, nofollow=True, autoescape=Tru leading punctuation (opening parens) and it'll still do the right thing. If trim_url_limit is not None, the URLs in link text longer than this limit - will truncated to trim_url_limit-3 characters and appended with an elipsis. + will truncated to trim_url_limit-3 characters and appended with an ellipsis. If nofollow is True, the URLs in link text will get a rel="nofollow" attribute. diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index 311d58317..64df9a08e 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -1,6 +1,6 @@ """ Helper functions for mapping model fields to a dictionary of default -keyword arguments that should be used for their equivelent serializer fields. +keyword arguments that should be used for their equivalent serializer fields. """ import inspect diff --git a/rest_framework/utils/html.py b/rest_framework/utils/html.py index 121c825c7..ca062c9fc 100644 --- a/rest_framework/utils/html.py +++ b/rest_framework/utils/html.py @@ -14,7 +14,7 @@ def is_html_input(dictionary): def parse_html_list(dictionary, prefix=''): """ - Used to suport list values in HTML forms. + Used to support list values in HTML forms. Supports lists of primitives and/or dictionaries. * List of primitives. diff --git a/rest_framework/versioning.py b/rest_framework/versioning.py index 763a92b4c..f533ef580 100644 --- a/rest_framework/versioning.py +++ b/rest_framework/versioning.py @@ -94,7 +94,7 @@ class NamespaceVersioning(BaseVersioning): The difference is in the backend - this implementation uses Django's URL namespaces to determine the version. - An example URL conf that is namespaced into two seperate versions + An example URL conf that is namespaced into two separate versions # users/urls.py urlpatterns = [ @@ -147,7 +147,7 @@ class HostNameVersioning(BaseVersioning): invalid_version_message = _('Invalid version in hostname.') def determine_version(self, request, *args, **kwargs): - hostname, seperator, port = request.get_host().partition(':') + hostname, separator, port = request.get_host().partition(':') match = self.hostname_regex.match(hostname) if not match: return self.default_version diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index 2f440c567..bd8333504 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -112,7 +112,7 @@ class ViewSetMixin(object): if method == 'options': # This is a special case as we always provide handling for the # options method in the base `View` class. - # Unlike the other explicitly defined actions, 'metadata' is implict. + # Unlike the other explicitly defined actions, 'metadata' is implicit. self.action = 'metadata' else: self.action = self.action_map.get(method) diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 1f95396aa..5ef620abe 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -440,7 +440,7 @@ class FailingAuthAccessedInRenderer(TestCase): class NoAuthenticationClassesTests(TestCase): def test_permission_message_with_no_authentication_classes(self): """ - An unauthenticated request made against a view that containes no + An unauthenticated request made against a view that contains no `authentication_classes` but do contain `permissions_classes` the error code returned should be 403 with the exception's message. """ diff --git a/tests/test_fields.py b/tests/test_fields.py index 92f4548e5..60f02777a 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -974,7 +974,7 @@ class TestDateField(FieldValues): class TestCustomInputFormatDateField(FieldValues): """ - Valid and invalid values for `DateField` with a cutom input format. + Valid and invalid values for `DateField` with a custom input format. """ valid_inputs = { '1 Jan 2001': datetime.date(2001, 1, 1), @@ -1041,7 +1041,7 @@ class TestDateTimeField(FieldValues): class TestCustomInputFormatDateTimeField(FieldValues): """ - Valid and invalid values for `DateTimeField` with a cutom input format. + Valid and invalid values for `DateTimeField` with a custom input format. """ valid_inputs = { '1:35pm, 1 Jan 2001': datetime.datetime(2001, 1, 1, 13, 35, tzinfo=timezone.UTC()), diff --git a/tests/test_filters.py b/tests/test_filters.py index 175ae5b12..03d61fc37 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -711,7 +711,7 @@ class OrderingFilterTests(TestCase): serializer_class = OrderingFilterSerializer filter_backends = (filters.OrderingFilter,) ordering = ('title',) - oredering_fields = ('text',) + ordering_fields = ('text',) view = OrderingListView.as_view() request = factory.get('') @@ -819,7 +819,7 @@ class OrderingFilterTests(TestCase): queryset = OrderingFilterModel.objects.all() filter_backends = (filters.OrderingFilter,) ordering = ('title',) - # note: no ordering_fields and serializer_class speficied + # note: no ordering_fields and serializer_class specified def get_serializer_class(self): return OrderingFilterSerializer @@ -842,7 +842,7 @@ class OrderingFilterTests(TestCase): filter_backends = (filters.OrderingFilter,) ordering = ('title',) # note: no ordering_fields and serializer_class - # or get_serializer_class speficied + # or get_serializer_class specified view = OrderingListView.as_view() request = factory.get('/', {'ordering': 'text'}) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index b2d336d84..a14972f04 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -136,7 +136,7 @@ class TestModelSerializer(TestCase): class TestRegularFieldMappings(TestCase): def test_regular_fields(self): """ - Model fields should map to their equivelent serializer fields. + Model fields should map to their equivalent serializer fields. """ class TestSerializer(serializers.ModelSerializer): class Meta: diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 4ea706429..f7feb4006 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -67,8 +67,8 @@ class TestPaginationIntegration: def test_setting_page_size_over_maximum(self): """ - When page_size parameter exceeds maxiumum allowable, - then it should be capped to the maxiumum. + When page_size parameter exceeds maximum allowable, + then it should be capped to the maximum. """ request = factory.get('/', {'page_size': 1000}) response = self.view(request) @@ -259,7 +259,7 @@ class TestPageNumberPaginationOverride: def setup(self): class OverriddenDjangoPaginator(DjangoPaginator): - # override the count in our overriden Django Paginator + # override the count in our overridden Django Paginator # we will only return one page, with one item count = 1 diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py index 746f51b7e..ac218df21 100644 --- a/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -13,7 +13,7 @@ factory = APIRequestFactory() class TemplateTagTests(TestCase): - def test_add_query_param_with_non_latin_charactor(self): + def test_add_query_param_with_non_latin_character(self): # Ensure we don't double-escape non-latin characters # that are present in the querystring. # See #1314. diff --git a/tests/test_testing.py b/tests/test_testing.py index e6c8de22d..3adcc55f8 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -78,7 +78,7 @@ class TestAPITestClient(TestCase): response = self.client.get('/session-view/') self.assertEqual(response.data['active_session'], False) - # Subsequant requests have an active session + # Subsequent requests have an active session response = self.client.get('/session-view/') self.assertEqual(response.data['active_session'], True) From 0781182646e4b292e88ec46b74cc5b709c190753 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 9 Aug 2016 17:48:29 +0100 Subject: [PATCH 205/457] Fix call to .resolve_context (#4371) --- rest_framework/renderers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 91d9e9072..371cd6ec7 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -168,7 +168,7 @@ class TemplateHTMLRenderer(BaseRenderer): if hasattr(self, 'resolve_context'): # Fallback for older versions. - context = self.resolve_context(self, data, request, response) + context = self.resolve_context(data, request, response) else: context = self.get_template_context(data, renderer_context) return template_render(template, context, request=request) From 8105a4ac5abc9760ac5dea8e567f333feb6f8e2a Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 10 Aug 2016 12:02:33 +0100 Subject: [PATCH 206/457] Resolve form display with ChoiceField, MultipleChoiceField and non-string choices. (#4374) * Add tests for html-form-rendering choice fields * Resolve issues with ChoiceField, MultipleChoiceField and non-string options * Ensure None template comparisons don't match string None --- .../horizontal/checkbox_multiple.html | 6 +- .../rest_framework/horizontal/radio.html | 6 +- .../rest_framework/horizontal/select.html | 4 +- .../horizontal/select_multiple.html | 4 +- .../inline/checkbox_multiple.html | 4 +- .../rest_framework/inline/radio.html | 3 +- .../rest_framework/inline/select.html | 4 +- .../inline/select_multiple.html | 3 +- .../vertical/checkbox_multiple.html | 6 +- .../rest_framework/vertical/radio.html | 5 +- .../rest_framework/vertical/select.html | 4 +- .../vertical/select_multiple.html | 3 +- rest_framework/templatetags/rest_framework.py | 15 ++++ rest_framework/utils/serializer_helpers.py | 2 +- tests/test_renderers.py | 87 +++++++++++++++++++ 15 files changed, 139 insertions(+), 17 deletions(-) diff --git a/rest_framework/templates/rest_framework/horizontal/checkbox_multiple.html b/rest_framework/templates/rest_framework/horizontal/checkbox_multiple.html index f01071297..7c7e57326 100644 --- a/rest_framework/templates/rest_framework/horizontal/checkbox_multiple.html +++ b/rest_framework/templates/rest_framework/horizontal/checkbox_multiple.html @@ -1,3 +1,5 @@ +{% load rest_framework %} +
{% if field.label %}
@@ -19,7 +21,7 @@ {% for key, text in field.choices.items %} {% endfor %} @@ -35,7 +37,7 @@ {% for key, text in field.choices.items %}
diff --git a/rest_framework/templates/rest_framework/horizontal/select.html b/rest_framework/templates/rest_framework/horizontal/select.html index 3c9e36bbb..7a3db2db8 100644 --- a/rest_framework/templates/rest_framework/horizontal/select.html +++ b/rest_framework/templates/rest_framework/horizontal/select.html @@ -1,3 +1,5 @@ +{% load rest_framework %} +
{% if field.label %}
@@ -16,7 +18,7 @@ {% elif select.end_option_group %} {% else %} - + {% endif %} {% empty %} diff --git a/rest_framework/templates/rest_framework/inline/checkbox_multiple.html b/rest_framework/templates/rest_framework/inline/checkbox_multiple.html index b02425a62..4c544ff8a 100644 --- a/rest_framework/templates/rest_framework/inline/checkbox_multiple.html +++ b/rest_framework/templates/rest_framework/inline/checkbox_multiple.html @@ -1,3 +1,5 @@ +{% load rest_framework %} +
{% if field.label %} @@ -6,7 +8,7 @@ {% for key, text in field.choices.items %}
diff --git a/rest_framework/templates/rest_framework/inline/radio.html b/rest_framework/templates/rest_framework/inline/radio.html index efaef6c9b..8d43cb390 100644 --- a/rest_framework/templates/rest_framework/inline/radio.html +++ b/rest_framework/templates/rest_framework/inline/radio.html @@ -1,4 +1,5 @@ {% load i18n %} +{% load rest_framework %} {% trans "None" as none_choice %}
@@ -20,7 +21,7 @@ {% for key, text in field.choices.items %}
diff --git a/rest_framework/templates/rest_framework/inline/select.html b/rest_framework/templates/rest_framework/inline/select.html index 99f10ae71..5023c2203 100644 --- a/rest_framework/templates/rest_framework/inline/select.html +++ b/rest_framework/templates/rest_framework/inline/select.html @@ -1,3 +1,5 @@ +{% load rest_framework %} +
{% if field.label %}
@@ -15,7 +16,7 @@ {% elif select.end_option_group %} {% else %} - + {% endif %} {% empty %} diff --git a/rest_framework/templates/rest_framework/vertical/checkbox_multiple.html b/rest_framework/templates/rest_framework/vertical/checkbox_multiple.html index 8412f8e2d..b933f4ff5 100644 --- a/rest_framework/templates/rest_framework/vertical/checkbox_multiple.html +++ b/rest_framework/templates/rest_framework/vertical/checkbox_multiple.html @@ -1,3 +1,5 @@ +{% load rest_framework %} +
{% if field.label %} @@ -7,7 +9,7 @@
{% for key, text in field.choices.items %} {% endfor %} @@ -16,7 +18,7 @@ {% for key, text in field.choices.items %}
diff --git a/rest_framework/templates/rest_framework/vertical/radio.html b/rest_framework/templates/rest_framework/vertical/radio.html index 9922ef6a1..6e5d2232c 100644 --- a/rest_framework/templates/rest_framework/vertical/radio.html +++ b/rest_framework/templates/rest_framework/vertical/radio.html @@ -1,4 +1,5 @@ {% load i18n %} +{% load rest_framework %} {% trans "None" as none_choice %}
@@ -19,7 +20,7 @@ {% for key, text in field.choices.items %} {% endfor %} @@ -37,7 +38,7 @@ {% for key, text in field.choices.items %}
diff --git a/rest_framework/templates/rest_framework/vertical/select.html b/rest_framework/templates/rest_framework/vertical/select.html index 9736fc072..6ccaaf27f 100644 --- a/rest_framework/templates/rest_framework/vertical/select.html +++ b/rest_framework/templates/rest_framework/vertical/select.html @@ -1,3 +1,5 @@ +{% load rest_framework %} +
{% if field.label %}
' + ) + rendered_packed = ''.join(rendered.split()) + assert rendered_packed == expected_packed + class TestNestedBoundField: def test_nested_empty_bound_field(self): From f1a2eeb818366b11c44c86d2ccfa1d6822d216b9 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 10 Aug 2016 16:38:59 +0100 Subject: [PATCH 212/457] .choices property of RelatedField should preserve non-string values. (#4379) Update RelatedField.choices to support non-string values --- rest_framework/relations.py | 2 +- tests/test_model_serializer.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index ad74d1f35..4b6b3bea4 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -168,7 +168,7 @@ class RelatedField(Field): return OrderedDict([ ( - six.text_type(self.to_representation(item)), + self.to_representation(item), self.display_value(item) ) for item in queryset diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index a14972f04..01243ff6e 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -614,7 +614,7 @@ class TestRelationalFieldDisplayValue(TestCase): fields = '__all__' serializer = TestSerializer() - expected = OrderedDict([('1', 'Red Color'), ('2', 'Yellow Color'), ('3', 'Green Color')]) + expected = OrderedDict([(1, 'Red Color'), (2, 'Yellow Color'), (3, 'Green Color')]) self.assertEqual(serializer.fields['color'].choices, expected) def test_custom_display_value(self): @@ -630,7 +630,7 @@ class TestRelationalFieldDisplayValue(TestCase): fields = '__all__' serializer = TestSerializer() - expected = OrderedDict([('1', 'My Red Color'), ('2', 'My Yellow Color'), ('3', 'My Green Color')]) + expected = OrderedDict([(1, 'My Red Color'), (2, 'My Yellow Color'), (3, 'My Green Color')]) self.assertEqual(serializer.fields['color'].choices, expected) From f16e8801679c81b60696d10428e172f0800f4bea Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 10 Aug 2016 17:22:19 +0100 Subject: [PATCH 213/457] Stricter type validation for CharField. (#4380) Stricter type validation for CharField --- rest_framework/fields.py | 6 ++++++ tests/test_fields.py | 2 ++ 2 files changed, 8 insertions(+) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 3a2f27205..fab79808f 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -672,6 +672,7 @@ class NullBooleanField(Field): class CharField(Field): default_error_messages = { + 'invalid': _('Not a valid string.'), 'blank': _('This field may not be blank.'), 'max_length': _('Ensure this field has no more than {max_length} characters.'), 'min_length': _('Ensure this field has at least {min_length} characters.') @@ -702,6 +703,11 @@ class CharField(Field): return super(CharField, self).run_validation(data) def to_internal_value(self, data): + # We're lenient with allowing basic numerics to be coerced into strings, + # but other types should fail. Eg. unclear if booleans should represent as `true` or `True`, + # and composites such as lists are likely user error. + if isinstance(data, bool) or not isinstance(data, six.string_types + six.integer_types + (float,)): + self.fail('invalid') value = six.text_type(data) return value.strip() if self.trim_whitespace else value diff --git a/tests/test_fields.py b/tests/test_fields.py index 1cbff9909..f1a588c27 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -535,6 +535,8 @@ class TestCharField(FieldValues): 'abc': 'abc' } invalid_inputs = { + (): ['Not a valid string.'], + True: ['Not a valid string.'], '': ['This field may not be blank.'] } outputs = { From 3698d9ea2ef95a6823b1bc98863ba7b49e2b6937 Mon Sep 17 00:00:00 2001 From: Kyle Hornberg Date: Wed, 10 Aug 2016 11:23:10 -0500 Subject: [PATCH 214/457] Update permissions.md (#4381) --- 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 402875cd5..e0838e94a 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -132,7 +132,7 @@ This permission is suitable if you want to your API to allow read permissions to ## DjangoModelPermissions -This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. This permission must only be applied to views that has a `.queryset` property set. Authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned. +This permission class ties into Django's standard `django.contrib.auth` [model permissions][contribauth]. This permission must only be applied to views that have a `.queryset` property set. Authorization will only be granted if the user *is authenticated* and has the *relevant model permissions* assigned. * `POST` requests require the user to have the `add` permission on the model. * `PUT` and `PATCH` requests require the user to have the `change` permission on the model. From b50d8950eeeac03843b9da1cb96ba52b5b98c8bc Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 11 Aug 2016 11:27:28 +0100 Subject: [PATCH 215/457] Pass request to schema generation (#4383) Pass request to schema generation --- rest_framework/schemas.py | 63 ++++++++++++++++++++------------------- tests/test_schemas.py | 4 +++ 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 688deec88..28f5dc8a1 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -65,44 +65,52 @@ class SchemaGenerator(object): urls = import_module(urlconf) else: urls = urlconf - patterns = urls.urlpatterns + self.patterns = urls.urlpatterns elif patterns is None and urlconf is None: urls = import_module(settings.ROOT_URLCONF) - patterns = urls.urlpatterns + self.patterns = urls.urlpatterns + else: + self.patterns = patterns if url and not url.endswith('/'): url += '/' self.title = title self.url = url - self.endpoints = self.get_api_endpoints(patterns) + self.endpoints = None def get_schema(self, request=None): - if request is None: - endpoints = self.endpoints - else: - # Filter the list of endpoints to only include those that - # the user has permission on. - endpoints = [] - for key, link, callback in self.endpoints: - method = link.action.upper() - view = callback.cls() + if self.endpoints is None: + self.endpoints = self.get_api_endpoints(self.patterns) + + links = [] + for key, path, method, callback in self.endpoints: + view = callback.cls() + for attr, val in getattr(callback, 'initkwargs', {}).items(): + setattr(view, attr, val) + view.args = () + view.kwargs = {} + view.format_kwarg = None + + if request is not None: view.request = clone_request(request, method) - view.format_kwarg = None try: view.check_permissions(view.request) except exceptions.APIException: - pass - else: - endpoints.append((key, link, callback)) + continue + else: + view.request = None - if not endpoints: + link = self.get_link(path, method, callback, view) + links.append((key, link)) + + if not link: return None # Generate the schema content structure, from the endpoints. # ('users', 'list'), Link -> {'users': {'list': Link()}} content = {} - for key, link, callback in endpoints: + for key, link in links: insert_into(content, key, link) # Return the schema document. @@ -122,8 +130,7 @@ class SchemaGenerator(object): if self.should_include_endpoint(path, callback): for method in self.get_allowed_methods(callback): key = self.get_key(path, method, callback) - link = self.get_link(path, method, callback) - endpoint = (key, link, callback) + endpoint = (key, path, method, callback) api_endpoints.append(endpoint) elif isinstance(pattern, RegexURLResolver): @@ -190,14 +197,10 @@ class SchemaGenerator(object): # Methods for generating each individual `Link` instance... - def get_link(self, path, method, callback): + def get_link(self, path, method, callback, view): """ Return a `coreapi.Link` instance for the given endpoint. """ - view = callback.cls() - for attr, val in getattr(callback, 'initkwargs', {}).items(): - setattr(view, attr, val) - fields = self.get_path_fields(path, method, callback, view) fields += self.get_serializer_fields(path, method, callback, view) fields += self.get_pagination_fields(path, method, callback, view) @@ -260,20 +263,18 @@ class SchemaGenerator(object): if method not in ('PUT', 'PATCH', 'POST'): return [] - if not hasattr(view, 'get_serializer_class'): + if not hasattr(view, 'get_serializer'): return [] - fields = [] - - serializer_class = view.get_serializer_class() - serializer = serializer_class() + serializer = view.get_serializer() if isinstance(serializer, serializers.ListSerializer): - return coreapi.Field(name='data', location='body', required=True) + return [coreapi.Field(name='data', location='body', required=True)] if not isinstance(serializer, serializers.Serializer): return [] + fields = [] for field in serializer.fields.values(): if field.read_only: continue diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 6c02c9d23..d8c0f2209 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -43,6 +43,10 @@ class ExampleViewSet(ModelViewSet): def custom_action(self, request, pk): return super(ExampleSerializer, self).retrieve(self, request) + def get_serializer(self, *args, **kwargs): + assert self.request + return super(ExampleViewSet, self).get_serializer(*args, **kwargs) + class ExampleView(APIView): permission_classes = [permissions.IsAuthenticatedOrReadOnly] From 01b498ec5109da22bf1b79d86efaecf45426ad51 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 11 Aug 2016 14:07:40 +0100 Subject: [PATCH 216/457] Fix schema categories for custom list actions (#4386) --- rest_framework/schemas.py | 98 +++++++++++++++++++++++---------------- tests/test_schemas.py | 14 +++++- 2 files changed, 70 insertions(+), 42 deletions(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 28f5dc8a1..c3a811bfb 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -30,24 +30,6 @@ def is_api_view(callback): return (cls is not None) and issubclass(cls, APIView) -def insert_into(target, keys, item): - """ - Insert `item` into the nested dictionary `target`. - - For example: - - target = {} - insert_into(target, ('users', 'list'), Link(...)) - insert_into(target, ('users', 'detail'), Link(...)) - assert target == {'users': {'list': Link(...), 'detail': Link(...)}} - """ - for key in keys[:1]: - if key not in target: - target[key] = {} - target = target[key] - target[keys[-1]] = item - - class SchemaGenerator(object): default_mapping = { 'get': 'read', @@ -84,7 +66,7 @@ class SchemaGenerator(object): self.endpoints = self.get_api_endpoints(self.patterns) links = [] - for key, path, method, callback in self.endpoints: + for path, method, category, action, callback in self.endpoints: view = callback.cls() for attr, val in getattr(callback, 'initkwargs', {}).items(): setattr(view, attr, val) @@ -102,16 +84,21 @@ class SchemaGenerator(object): view.request = None link = self.get_link(path, method, callback, view) - links.append((key, link)) + links.append((category, action, link)) - if not link: + if not links: return None - # Generate the schema content structure, from the endpoints. - # ('users', 'list'), Link -> {'users': {'list': Link()}} + # Generate the schema content structure, eg: + # {'users': {'list': Link()}} content = {} - for key, link in links: - insert_into(content, key, link) + for category, action, link in links: + if category is None: + content[action] = link + elif category in content: + content[category][action] = link + else: + content[category] = {action: link} # Return the schema document. return coreapi.Document(title=self.title, content=content, url=self.url) @@ -129,8 +116,8 @@ class SchemaGenerator(object): callback = pattern.callback if self.should_include_endpoint(path, callback): for method in self.get_allowed_methods(callback): - key = self.get_key(path, method, callback) - endpoint = (key, path, method, callback) + action = self.get_action(path, method, callback) + endpoint = (path, method, action, callback) api_endpoints.append(endpoint) elif isinstance(pattern, RegexURLResolver): @@ -140,7 +127,21 @@ class SchemaGenerator(object): ) api_endpoints.extend(nested_endpoints) - return api_endpoints + return self.add_categories(api_endpoints) + + def add_categories(self, api_endpoints): + """ + (path, method, action, callback) -> (path, method, category, action, callback) + """ + # Determine the top level categories for the schema content, + # based on the URLs of the endpoints. Eg `set(['users', 'organisations'])` + paths = [endpoint[0] for endpoint in api_endpoints] + categories = self.get_categories(paths) + + return [ + (path, method, self.get_category(categories, path), action, callback) + for (path, method, action, callback) in api_endpoints + ] def get_path(self, path_regex): """ @@ -177,23 +178,38 @@ class SchemaGenerator(object): callback.cls().allowed_methods if method not in ('OPTIONS', 'HEAD') ] - def get_key(self, path, method, callback): + def get_action(self, path, method, callback): """ - Return a tuple of strings, indicating the identity to use for a - given endpoint. eg. ('users', 'list'). + Return a description action string for the endpoint, eg. 'list'. """ - category = None - for item in path.strip('/').split('/'): - if '{' in item: - break - category = item - actions = getattr(callback, 'actions', self.default_mapping) - action = actions[method.lower()] + return actions[method.lower()] - if category: - return (category, action) - return (action,) + def get_categories(self, paths): + categories = set() + split_paths = set([ + tuple(path.split("{")[0].strip('/').split('/')) + for path in paths + ]) + + while split_paths: + for split_path in list(split_paths): + if len(split_path) == 0: + split_paths.remove(split_path) + elif len(split_path) == 1: + categories.add(split_path[0]) + split_paths.remove(split_path) + elif split_path[0] in categories: + split_paths.remove(split_path) + + return categories + + def get_category(self, categories, path): + path_components = path.split("{")[0].strip('/').split('/') + for path_component in path_components: + if path_component in categories: + return path_component + return None # Methods for generating each individual `Link` instance... diff --git a/tests/test_schemas.py b/tests/test_schemas.py index d8c0f2209..5e588483d 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -5,7 +5,7 @@ from django.test import TestCase, override_settings from rest_framework import filters, pagination, permissions, serializers from rest_framework.compat import coreapi -from rest_framework.decorators import detail_route +from rest_framework.decorators import detail_route, list_route from rest_framework.response import Response from rest_framework.routers import DefaultRouter from rest_framework.schemas import SchemaGenerator @@ -43,6 +43,10 @@ class ExampleViewSet(ModelViewSet): def custom_action(self, request, pk): return super(ExampleSerializer, self).retrieve(self, request) + @list_route() + def custom_list_action(self, request): + return super(ExampleViewSet, self).list(self, request) + def get_serializer(self, *args, **kwargs): assert self.request return super(ExampleViewSet, self).get_serializer(*args, **kwargs) @@ -88,6 +92,10 @@ class TestRouterGeneratedSchema(TestCase): coreapi.Field('ordering', required=False, location='query') ] ), + 'custom_list_action': coreapi.Link( + url='/example/custom_list_action/', + action='get' + ), 'retrieve': coreapi.Link( url='/example/{pk}/', action='get', @@ -144,6 +152,10 @@ class TestRouterGeneratedSchema(TestCase): coreapi.Field('d', required=False, location='form'), ] ), + 'custom_list_action': coreapi.Link( + url='/example/custom_list_action/', + action='get' + ), 'update': coreapi.Link( url='/example/{pk}/', action='put', From 116917dbed76ab37ab2d07f7b938d13f2c6e3efd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 11 Aug 2016 16:18:33 +0100 Subject: [PATCH 217/457] Add form field descriptions to schemas (#4387) --- rest_framework/schemas.py | 9 ++++++++- tests/test_schemas.py | 8 ++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index c3a811bfb..6b6324033 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -4,6 +4,7 @@ from django.conf import settings from django.contrib.admindocs.views import simplify_regex from django.core.urlresolvers import RegexURLPattern, RegexURLResolver from django.utils import six +from django.utils.encoding import force_text from rest_framework import exceptions, serializers from rest_framework.compat import coreapi, uritemplate, urlparse @@ -295,7 +296,13 @@ class SchemaGenerator(object): if field.read_only: continue required = field.required and method != 'PATCH' - field = coreapi.Field(name=field.source, location='form', required=required) + description = force_text(field.help_text) if field.help_text else '' + field = coreapi.Field( + name=field.source, + location='form', + required=required, + description=description + ) fields.append(field) return fields diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 5e588483d..81b796c35 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -24,7 +24,7 @@ class ExamplePagination(pagination.PageNumberPagination): class ExampleSerializer(serializers.Serializer): - a = serializers.CharField(required=True) + a = serializers.CharField(required=True, help_text='A field description') b = serializers.CharField(required=False) @@ -131,7 +131,7 @@ class TestRouterGeneratedSchema(TestCase): action='post', encoding='application/json', fields=[ - coreapi.Field('a', required=True, location='form'), + coreapi.Field('a', required=True, location='form', description='A field description'), coreapi.Field('b', required=False, location='form') ] ), @@ -162,7 +162,7 @@ class TestRouterGeneratedSchema(TestCase): encoding='application/json', fields=[ coreapi.Field('pk', required=True, location='path'), - coreapi.Field('a', required=True, location='form'), + coreapi.Field('a', required=True, location='form', description='A field description'), coreapi.Field('b', required=False, location='form') ] ), @@ -172,7 +172,7 @@ class TestRouterGeneratedSchema(TestCase): encoding='application/json', fields=[ coreapi.Field('pk', required=True, location='path'), - coreapi.Field('a', required=False, location='form'), + coreapi.Field('a', required=False, location='form', description='A field description'), coreapi.Field('b', required=False, location='form') ] ), From 1312acaf8b0ef2cdea1f6b296196899e890c8064 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 11 Aug 2016 16:53:34 +0100 Subject: [PATCH 218/457] Minor docs update [ci skip] --- README.md | 3 +-- docs/index.md | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a8e1afbf1..179f2891a 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,7 @@ REST framework commercially we strongly encourage you to invest in its continued development by **[signing up for a paid plan][funding]**. The initial aim is to provide a single full-time position on REST framework. -Right now we're over 58% of the way towards achieving that. -*Every single sign-up makes a significant impact.* +*Every single sign-up makes a significant impact towards making that possible.*

diff --git a/docs/index.md b/docs/index.md index 87e013b8e..88276e678 100644 --- a/docs/index.md +++ b/docs/index.md @@ -68,8 +68,7 @@ REST framework commercially we strongly encourage you to invest in its continued development by **[signing up for a paid plan][funding]**. The initial aim is to provide a single full-time position on REST framework. -Right now we're over 58% of the way towards achieving that. -*Every single sign-up makes a significant impact.* +*Every single sign-up makes a significant impact towards making that possible.*

-*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), and [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf).* +*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), and [Machinalis](http://www.machinalis.com/#services).* --- From b683cd7afc9e03ac5287172348346ff2e61cc348 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 25 Aug 2016 22:29:38 +0100 Subject: [PATCH 241/457] Update sponsor info [ci skip] --- docs/img/premium/stream-readme.png | Bin 19341 -> 20667 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/img/premium/stream-readme.png b/docs/img/premium/stream-readme.png index a04009d7fc0d1fe4fab5aa77215a94d5150d6354..955c11429c53bd18cbfbf888dca7663e134264eb 100644 GIT binary patch literal 20667 zcmeEuWmKHYvM4gRyTf21!3THOU;IwTEQx#a&!GBG%Fvw{{`(&&;N>MY4P7uxp=xc{3Vm61(%hBm7|rD zhdUe(^dHH=b^G6l{}+=!W-fmd`%8&`%O>`Ra}iA|cV`FBKPu36vh$GO5&KJ^e{cRD zjr@%z>+I;_X65b( z4zKs02>i409|(U`FY?OH$I3xh&JM27zog*f6%yn6k3Ij6DeLUu?55>nW?}V*v%gUO z(e&@of6>wVA38jI!vCc6kDh;ET3U#>dzyo-EIj@(U+`J`%lc)^Y`Ff>EXMU8p~c{~ zAfo1MX=m*#XXarg!NUy_5aAXQ;o%4J@cen;77*k5H<*8L@TW)C&C1Nf+3k(9vxCH6 zVmSVJ1@dyjXZ+v$|Ai{X^~aC?;a7h#M*&OB^j+-z9qH3J@Qu3WiV zG%%h+wdWXqUnZwa%p6Os9D}I}#J~qiF)4DAS-FDZHGJJfbz;_)V+bN8uSjc|LpW~N}iWVTtNTH)6 z(gOiQ+j;cV;k!QZ1nl0u?6=!j?t{(z!~@+e64yzNUZt;9n7Gn$kH5<`7}s)yYF zC;UgX|6kSr?;02SC)c$ZPkQckg6MGYS57c#(yXpprXK5*wc2Os9O9a<=PUI0FW~0P zlP%=wzs`ssH$C5SMh{!>N~_2rsCzZwu#{XYUMFltg-SE~X8 zvGCUQw)R_~pSGf=dwx0}g9BbwSxxh<0tYJ6d$=c0)W99rk5r@X?KXP2y?tJO#9=Di zX1&kLZ_<|57V{EqVoq!};WP|i-RAiM=qC#QKohI;za2#nKW(?W`>hYr&#uU!sLROc zTAc41B!B_<8g7YX6=ePzoEIG+sR>nuf#uQPpdf7(pLNSXW3*kHv7Yt4N|vhwEA&)8 zJNWuy(g8MR$+4tQ;$jDp$Bb?$b7fG!GkypCo`$Ng&wz|zT=X_=ISm6tb_I9oPgm?skvZJk0MWBb>$gv4=O{DJLQ}J)<+aUa3 zvctawzUPxZ8_C(vUL-)8iILiS!YU_XNzg~XVR)6)aR4JIg(4`~=?2a{i6-Npm{8Z? zam6gG3k{53gGHXhwF*M_cnCHJRUA;$zv@KqAqWW^AARlb;yXea1-$!_$%C}Gh^(^d zwZ=ifqNOaDIW-<3HNyZjFJJsT*U=n?FdF4O;&ctp68W+l@U0IIZ=wI=i@pj`_rf(y zM1ss?MEX|@NZ1|5kqCzx9TmaHH(f=uu{{h;If((|77=nFr-+G3Z<#!I_{y4XQN$bG z;Rzsj?z>OQl|g`}AR*4*OA}%w;bjfZ52`F2v`e}($64svA2WUUXue!2TOufvX9>q- zmMx)s!v-gl^VSlJeu4nLZ77OeDx+`FOUc+p&j>Zqh+=i@JT7oT7Vzc- zMT_muDVx1a`>W$+ot$aLJm$kLa!&dhE7D2o{4S-pcI1_EBj7tigm>G;_#6TKD{XX$ zlCF+p?`3pgSt`Pz0A!Kg)*`8O)xqAoRJ7Ob;gN6P3q_Hhvta&vHAqu3$jPL&8TWI8 z{?V29Pxb1b>h~HVt@Y~oNFU-u`08Kg7$sCJp-&*YO>TXAIBPy|ApbS;E?mz2T(exf zz6}Lt^J18EP>kzDeIaSKZr<#$JDEJp5~&NJ%}*zLg$Dy8b&@3pzCw4R1D-T+9uwLK zkhDWX4iIli@P|e~g0T^IIFB+~- zWYt`2dd}{BUnoN)GelSci*V!KgR>}sKH+LHo2C!@MRPSVUm#(#EabsIX|3V-axwL0 z6Q&s0J1lj8( zS43PV%AtEihD2BJhF`YU;3rTLf9Ludvk;~

pjlIcQ4o>(`XbcjU>*qQ2?ZI)oLo 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 literal 19341 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 dQepYy|C1zl-M+ID`#}d}dAj Date: Tue, 30 Aug 2016 11:37:48 +0200 Subject: [PATCH 242/457] [documentation] Throttling random request code example fix (#4451) --- 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 51d2beef1..da4d5f725 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -188,7 +188,7 @@ The following is an example of a rate throttle, that will randomly throttle 1 in class RandomRateThrottle(throttling.BaseThrottle): def allow_request(self, request, view): - return random.randint(1, 10) == 1 + return random.randint(1, 10) != 1 [cite]: https://dev.twitter.com/docs/error-codes-responses [permissions]: permissions.md From 07efbdb45e5c7ef70249e985b3c1ef1cead455ba Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Fri, 2 Sep 2016 18:00:03 +0200 Subject: [PATCH 243/457] Fix APIClient.get() when path contains unicode arguments (#4458) --- rest_framework/test.py | 9 ++++++--- tests/test_testing.py | 7 +++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/rest_framework/test.py b/rest_framework/test.py index 3ba4059a9..fd9f6ab13 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -79,10 +79,13 @@ 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] + # Fix to support old behavior where you have the arguments in the + # url. See #1461. + query_string = force_bytes(path.split('?')[1]) + if six.PY3: + query_string = query_string.decode('iso-8859-1') + r['QUERY_STRING'] = query_string r.update(extra) return self.generic('GET', path, **r) diff --git a/tests/test_testing.py b/tests/test_testing.py index 3adcc55f8..6683ae6ed 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -245,3 +245,10 @@ class TestAPIRequestFactory(TestCase): self.assertEqual(dict(request.GET), {'demo': ['test']}) request = factory.get('/view/', {'demo': 'test'}) self.assertEqual(dict(request.GET), {'demo': ['test']}) + + def test_request_factory_url_arguments_with_unicode(self): + 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 5df54a711f3770de28c0aaed9350e589df082117 Mon Sep 17 00:00:00 2001 From: TakesxiSximada Date: Mon, 5 Sep 2016 19:16:41 +0900 Subject: [PATCH 244/457] Set a view function's __module__ value to the WrappedAPIView object's __module__ (#4465) --- rest_framework/decorators.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 1b21e643b..554e5236c 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -55,6 +55,7 @@ def api_view(http_method_names=None): setattr(WrappedAPIView, method.lower(), handler) WrappedAPIView.__name__ = func.__name__ + WrappedAPIView.__module__ = func.__module__ WrappedAPIView.renderer_classes = getattr(func, 'renderer_classes', APIView.renderer_classes) From 6b6f3195092cb7f12cddf0dee2bfa4a0eaf3b15c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Thu, 8 Sep 2016 09:01:26 -0400 Subject: [PATCH 245/457] Add missing comma (#4473) --- rest_framework/templates/rest_framework/admin.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/templates/rest_framework/admin.html b/rest_framework/templates/rest_framework/admin.html index eb2b8f1c7..de011cd09 100644 --- a/rest_framework/templates/rest_framework/admin.html +++ b/rest_framework/templates/rest_framework/admin.html @@ -232,7 +232,7 @@ {% block script %} From e91ffc87cb1e5b1148e839a0c3536de4da8efeef Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 13 Sep 2016 07:21:10 +0200 Subject: [PATCH 246/457] Ignore empty args in the `MultipleFieldLookupMixin` definition - Closes #4484 --- docs/api-guide/generic-views.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index d7dc30ce1..5fea8d7e0 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -330,7 +330,8 @@ For example, if you need to lookup objects based on multiple fields in the URL c queryset = self.filter_queryset(queryset) # Apply any filter backends filter = {} for field in self.lookup_fields: - filter[field] = self.kwargs[field] + if self.kwargs[field]: # Ignore empty fields. + filter[field] = self.kwargs[field] return get_object_or_404(queryset, **filter) # Lookup the object You can then simply apply this mixin to a view or viewset anytime you need to apply the custom behavior. From a68b37d8bc432fae37ef5880aec500002b59f565 Mon Sep 17 00:00:00 2001 From: Jeff Willette Date: Tue, 13 Sep 2016 17:31:48 +0900 Subject: [PATCH 247/457] Update to correct location of reverse relation doc (#4481) --- 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 70fab448c..5772d940a 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -442,7 +442,7 @@ Declaring a `ModelSerializer` looks like this: By default, all the model fields on the class will be mapped to a corresponding serializer fields. -Any relationships such as foreign keys on the model will be mapped to `PrimaryKeyRelatedField`. Reverse relationships are not included by default unless explicitly included as described below. +Any relationships such as foreign keys on the model will be mapped to `PrimaryKeyRelatedField`. Reverse relationships are not included by default unless explicitly included as specified in the [serializer relations][relations] documentation. #### Inspecting a `ModelSerializer` From 4655501d51080fbc7733a81fab1a227229a33e3f Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 15 Sep 2016 12:44:45 +0100 Subject: [PATCH 248/457] Fix regression of `RegexField`. (#4490) * Don't deepcopy 'regex' arguments, instead treat as immutable. --- rest_framework/fields.py | 21 ++++++++++++--------- tests/test_fields.py | 15 +++++++++++++++ tests/test_serializer.py | 14 ++++++++++++++ 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f76e4e801..917a151e5 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -252,6 +252,8 @@ class SkipField(Exception): pass +REGEX_TYPE = type(re.compile('')) + NOT_READ_ONLY_WRITE_ONLY = 'May not set both `read_only` and `write_only`' NOT_READ_ONLY_REQUIRED = 'May not set both `read_only` and `required`' NOT_REQUIRED_DEFAULT = 'May not set both `required` and `default`' @@ -581,16 +583,17 @@ class Field(object): When cloning fields we instantiate using the arguments it was originally created with, rather than copying the complete state. """ - args = copy.deepcopy(self._args) - kwargs = dict(self._kwargs) - # Bit ugly, but we need to special case 'validators' as Django's - # RegexValidator does not support deepcopy. - # We treat validator callables as immutable objects. + # Treat regexes and validators as immutable. # See https://github.com/tomchristie/django-rest-framework/issues/1954 - validators = kwargs.pop('validators', None) - kwargs = copy.deepcopy(kwargs) - if validators is not None: - kwargs['validators'] = validators + # and https://github.com/tomchristie/django-rest-framework/pull/4489 + args = [ + copy.deepcopy(item) if not isinstance(item, REGEX_TYPE) else item + for item in self._args + ] + kwargs = { + key: (copy.deepcopy(value) if (key not in ('validators', 'regex')) else value) + for key, value in self._kwargs.items() + } return self.__class__(*args, **kwargs) def __repr__(self): diff --git a/tests/test_fields.py b/tests/test_fields.py index f1a588c27..4a4b741c5 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,5 +1,6 @@ import datetime import os +import re import uuid from decimal import Decimal @@ -590,6 +591,20 @@ class TestRegexField(FieldValues): field = serializers.RegexField(regex='[a-z][0-9]') +class TestiCompiledRegexField(FieldValues): + """ + Valid and invalid values for `RegexField`. + """ + valid_inputs = { + 'a9': 'a9', + } + invalid_inputs = { + 'A9': ["This value does not match the required pattern."] + } + outputs = {} + field = serializers.RegexField(regex=re.compile('[a-z][0-9]')) + + class TestSlugField(FieldValues): """ Valid and invalid values for `SlugField`. diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 4e9080909..bd9ef9500 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import pickle +import re import pytest @@ -337,3 +338,16 @@ class TestDefaultInclusions: assert serializer.is_valid() assert serializer.validated_data == {'integer': 456} assert serializer.errors == {} + + +class TestSerializerValidationWithCompiledRegexField: + def setup(self): + class ExampleSerializer(serializers.Serializer): + name = serializers.RegexField(re.compile(r'\d'), required=True) + self.Serializer = ExampleSerializer + + def test_validation_success(self): + serializer = self.Serializer(data={'name': '2'}) + assert serializer.is_valid() + assert serializer.validated_data == {'name': '2'} + assert serializer.errors == {} From fe4c4fa75147f114e014c3d6623d84de35c01fe7 Mon Sep 17 00:00:00 2001 From: Tanner Hobson Date: Fri, 16 Sep 2016 22:09:49 -0400 Subject: [PATCH 249/457] Fix indentation regression in API listing (#4493) In commit 5392be4ddba37da3e50c3feabc8b3b1c944481a8, there was a change made when cleaning up the template for the API listing that caused 2 spaces to appear before every header item (except the first) and before the first line of the body of the response. This meant that it often looked like: HTTP 200 OK Allow: GET, OPTIONS Content-Type: application/json Vary: Accept { "key": "value", "key2": "value2" } This change removes those leading spaces, so that it will now look like: HTTP 200 OK Allow: GET, OPTIONS Content-Type: application/json Vary: Accept { "key": "value", "key2": "value2" } --- rest_framework/templates/rest_framework/base.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 7f5170a62..5df23b767 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -150,10 +150,10 @@

-
HTTP {{ response.status_code }} {{ response.status_text }}{% autoescape off %}
-  {% for key, val in response_headers.items %}{{ key }}: {{ val|break_long_headers|urlize_quoted_links }}
-  {% endfor %}
-  {{ content|urlize_quoted_links }}
{% endautoescape %} +
HTTP {{ response.status_code }} {{ response.status_text }}{% autoescape off %}{% for key, val in response_headers.items %}
+{{ key }}: {{ val|break_long_headers|urlize_quoted_links }}{% endfor %}
+
+{{ content|urlize_quoted_links }}
{% endautoescape %}
From fe96ceced08fa36db122deeb53e525e25e3f3daf Mon Sep 17 00:00:00 2001 From: Ollie Ford Date: Sat, 17 Sep 2016 03:13:34 +0100 Subject: [PATCH 250/457] fixes response rendering with empty context (#4495) This commit allows `response.render` to be called when `response.rendered_context == {}`. This should be allowed, since if [the JSONRenderer, for example](https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/renderers.py#L85-L92) receives a `None` context, it sets it to an empty dictionary itself. --- rest_framework/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/response.py b/rest_framework/response.py index 4b863cb99..cb0f290ce 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -56,7 +56,7 @@ class Response(SimpleTemplateResponse): assert renderer, ".accepted_renderer not set on Response" assert accepted_media_type, ".accepted_media_type not set on Response" - assert context, ".renderer_context not set on Response" + assert context is not None, ".renderer_context not set on Response" context['response'] = self media_type = renderer.media_type From 76cc2f031940c2440f27c0070ee709f7f1304fe5 Mon Sep 17 00:00:00 2001 From: Jozef Knaperek Date: Mon, 19 Sep 2016 21:52:06 +0200 Subject: [PATCH 251/457] Rename an invalid reference to BasicToken in the docs --- docs/api-guide/authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 8d880b037..bf3a31eb7 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -148,7 +148,7 @@ For clients to authenticate, the token key should be included in the `Authorizat If successfully authenticated, `TokenAuthentication` provides the following credentials. * `request.user` will be a Django `User` instance. -* `request.auth` will be a `rest_framework.authtoken.models.BasicToken` instance. +* `request.auth` will be a `rest_framework.authtoken.models.Token` instance. Unauthenticated responses that are denied permission will result in an `HTTP 401 Unauthorized` response with an appropriate WWW-Authenticate header. For example: From be74d11165c364687ecc41bc6eb0970af24670fa Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 21 Sep 2016 11:49:09 +0100 Subject: [PATCH 252/457] Fallback behavior for request parsing when request.POST already accessed. (#4500) --- rest_framework/request.py | 27 +++++++++++++++++++++--- tests/test_parsers.py | 43 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index 355cccad7..0a827728a 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -15,6 +15,7 @@ import sys from django.conf import settings from django.http import QueryDict from django.http.multipartparser import parse_header +from django.http.request import RawPostDataException from django.utils import six from django.utils.datastructures import MultiValueDict @@ -263,10 +264,20 @@ class Request(object): if content_length == 0: self._stream = None - elif hasattr(self._request, 'read'): + elif not self._request._read_started: self._stream = self._request else: - self._stream = six.BytesIO(self.raw_post_data) + self._stream = six.BytesIO(self.body) + + def _supports_form_parsing(self): + """ + Return True if this requests supports parsing form data. + """ + form_media = ( + 'application/x-www-form-urlencoded', + 'multipart/form-data' + ) + return any([parser.media_type in form_media for parser in self.parsers]) def _parse(self): """ @@ -274,8 +285,18 @@ class Request(object): May raise an `UnsupportedMediaType`, or `ParseError` exception. """ - stream = self.stream media_type = self.content_type + try: + stream = self.stream + except RawPostDataException: + if not hasattr(self._request, '_post'): + raise + # If request.POST has been accessed in middleware, and a method='POST' + # request was made with 'multipart/form-data', then the request stream + # will already have been exhausted. + if self._supports_form_parsing(): + return (self._request.POST, self._request.FILES) + stream = None if stream is None or media_type is None: empty_data = QueryDict('', encoding=self._request._encoding) diff --git a/tests/test_parsers.py b/tests/test_parsers.py index f3af6817f..5052e2e53 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -7,11 +7,16 @@ from django import forms from django.core.files.uploadhandler import ( MemoryFileUploadHandler, TemporaryFileUploadHandler ) +from django.http.request import RawPostDataException from django.test import TestCase from django.utils.six.moves import StringIO from rest_framework.exceptions import ParseError -from rest_framework.parsers import FileUploadParser, FormParser +from rest_framework.parsers import ( + FileUploadParser, FormParser, JSONParser, MultiPartParser +) +from rest_framework.request import Request +from rest_framework.test import APIRequestFactory class Form(forms.Form): @@ -122,3 +127,39 @@ class TestFileUploadParser(TestCase): def __replace_content_disposition(self, disposition): self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition + + +class TestPOSTAccessed(TestCase): + def setUp(self): + self.factory = APIRequestFactory() + + def test_post_accessed_in_post_method(self): + django_request = self.factory.post('/', {'foo': 'bar'}) + request = Request(django_request, parsers=[FormParser(), MultiPartParser()]) + django_request.POST + assert request.POST == {'foo': ['bar']} + assert request.data == {'foo': ['bar']} + + def test_post_accessed_in_post_method_with_json_parser(self): + django_request = self.factory.post('/', {'foo': 'bar'}) + request = Request(django_request, parsers=[JSONParser()]) + django_request.POST + assert request.POST == {} + assert request.data == {} + + def test_post_accessed_in_put_method(self): + django_request = self.factory.put('/', {'foo': 'bar'}) + request = Request(django_request, parsers=[FormParser(), MultiPartParser()]) + django_request.POST + assert request.POST == {'foo': ['bar']} + assert request.data == {'foo': ['bar']} + + def test_request_read_before_parsing(self): + django_request = self.factory.put('/', {'foo': 'bar'}) + request = Request(django_request, parsers=[FormParser(), MultiPartParser()]) + django_request.read() + with pytest.raises(RawPostDataException): + request.POST + with pytest.raises(RawPostDataException): + request.POST + request.data From 7ab4a587d981f45ca440c53fa5514ad57e80abc3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 21 Sep 2016 12:24:26 +0100 Subject: [PATCH 253/457] Version 3.4.7 (#4501) --- docs/topics/release-notes.md | 28 ++++++++++++++++++++++++++++ rest_framework/__init__.py | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 63aa4e4a3..446abdd14 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,18 @@ You can determine your currently installed version using `pip freeze`: ## 3.4.x series +### 3.4.7 + +**Date**: [21st September 2016][3.4.7-milestone] + +* Fallback behavior for request parsing when request.POST already accessed. ([#3951][gh3951], [#4500][gh4500]) +* Fix regression of `RegexField`. ([#4489][gh4489], [#4490][gh4490], [#2617][gh2617]) +* Missing comma in `admin.html` causing CSRF error. ([#4472][gh4472], [#4473][gh4473]) +* Fix response rendering with empty context. ([#4495][gh4495]) +* Fix indentation regression in API listing. ([#4493][gh4493]) +* Fixed an issue where the incorrect value is set to `ResolverMatch.func_name` of api_view decorated view. ([#4465][gh4465], [#4462][gh4462]) +* Fix `APIClient.get()` when path contains unicode arguments ([#4458][gh4458]) + ### 3.4.6 **Date**: [23rd August 2016][3.4.6-milestone] @@ -583,6 +595,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [3.4.4-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.4+Release%22 [3.4.5-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.5+Release%22 [3.4.6-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.6+Release%22 +[3.4.7-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.7+Release%22 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 @@ -1109,3 +1122,18 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh3508]: https://github.com/tomchristie/django-rest-framework/issues/3508 [gh4419]: https://github.com/tomchristie/django-rest-framework/issues/4419 [gh4423]: https://github.com/tomchristie/django-rest-framework/issues/4423 + + + +[gh3951]: https://github.com/tomchristie/django-rest-framework/issues/3951 +[gh4500]: https://github.com/tomchristie/django-rest-framework/issues/4500 +[gh4489]: https://github.com/tomchristie/django-rest-framework/issues/4489 +[gh4490]: https://github.com/tomchristie/django-rest-framework/issues/4490 +[gh2617]: https://github.com/tomchristie/django-rest-framework/issues/2617 +[gh4472]: https://github.com/tomchristie/django-rest-framework/issues/4472 +[gh4473]: https://github.com/tomchristie/django-rest-framework/issues/4473 +[gh4495]: https://github.com/tomchristie/django-rest-framework/issues/4495 +[gh4493]: https://github.com/tomchristie/django-rest-framework/issues/4493 +[gh4465]: https://github.com/tomchristie/django-rest-framework/issues/4465 +[gh4462]: https://github.com/tomchristie/django-rest-framework/issues/4462 +[gh4458]: https://github.com/tomchristie/django-rest-framework/issues/4458 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index d99d82d23..457af6c88 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.4.6' +__version__ = '3.4.7' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2016 Tom Christie' From b82ec540ad447dcf703a9b21e9e94976109fd175 Mon Sep 17 00:00:00 2001 From: Dmitry Dygalo Date: Sat, 24 Sep 2016 00:03:02 +0200 Subject: [PATCH 254/457] Remove code for old Django versions (#4513) --- rest_framework/views.py | 1 - rest_framework/viewsets.py | 4 ---- 2 files changed, 5 deletions(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 15d8c6cde..e178a209f 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -126,7 +126,6 @@ class APIView(View): 'Use `.all()` or call `.get_queryset()` instead.' ) cls.queryset._fetch_all = force_evaluation - cls.queryset._result_iter = force_evaluation # Django <= 1.5 view = super(APIView, cls).as_view(**initkwargs) view.cls = cls diff --git a/rest_framework/viewsets.py b/rest_framework/viewsets.py index bd8333504..a9dc632ff 100644 --- a/rest_framework/viewsets.py +++ b/rest_framework/viewsets.py @@ -79,10 +79,6 @@ class ViewSetMixin(object): handler = getattr(self, action) setattr(self, method, handler) - # Patch this in as it's otherwise only present from 1.5 onwards - if hasattr(self, 'get') and not hasattr(self, 'head'): - self.head = self.get - # And continue as usual return self.dispatch(request, *args, **kwargs) From 38aa0ac28131f6c32cc3359efcf734012fb435d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Padilla?= Date: Wed, 28 Sep 2016 00:05:22 -0400 Subject: [PATCH 255/457] Update Django security releases 1.9.10 and 1.8.15 --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 1e8a7e5c4..8c20faa2b 100644 --- a/tox.ini +++ b/tox.ini @@ -15,8 +15,8 @@ setenv = PYTHONDONTWRITEBYTECODE=1 PYTHONWARNINGS=once deps = - django18: Django==1.8.14 - django19: Django==1.9.9 + django18: Django==1.8.15 + django19: Django==1.9.10 django110: Django==1.10 djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-testing.txt From e7fd166048de8017c11152b102a80602a80d20c1 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 29 Sep 2016 21:29:21 +0100 Subject: [PATCH 256/457] Docs tweaks --- docs/api-guide/relations.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 8695b2c1e..42533823c 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -286,7 +286,7 @@ Would serialize to a nested representation like this: ], } -# Writable nested serializers +## Writable nested serializers By default nested serializers are read-only. If you want to support write-operations to a nested serializer field you'll need to create `create()` and/or `update()` methods in order to explicitly specify how the child relationships should be saved. @@ -324,8 +324,14 @@ By default nested serializers are read-only. If you want to support write-operat >>> serializer.save() +--- + # Custom relational fields +In rare cases where none of the existing relational styles fit the representation you need, +you can implement a completely custom relational field, that describes exactly how the +output representation should be generated from the model instance. + To implement a custom relational field, you should override `RelatedField`, and implement the `.to_representation(self, value)` method. This method takes the target of the field as the `value` argument, and should return the representation that should be used to serialize the target. The `value` argument will typically be a model instance. If you want to implement a read-write relational field, you must also implement the `.to_internal_value(self, data)` method. From 73fd1ff4e71bec2cb70e8e0bd659712af98fa6d4 Mon Sep 17 00:00:00 2001 From: Manjit Kumar Date: Fri, 30 Sep 2016 16:32:23 +0530 Subject: [PATCH 257/457] add drf-url-filters app to django-rest-framework in filtering docs (#4528) - enabling validtions on incoming query params with the ease of adding new filters as easy as adding a new key in a dict. --- docs/api-guide/filtering.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 0ccd51dd3..bc502e970 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -432,6 +432,10 @@ The [djangorestframework-word-filter][django-rest-framework-word-search-filter] [django-url-filter][django-url-filter] provides a safe way to filter data via human-friendly URLs. It works very similar to DRF serializers and fields in a sense that they can be nested except they are called filtersets and filters. That provides easy way to filter related data. Also this library is generic-purpose so it can be used to filter other sources of data and not only Django `QuerySet`s. +## drf-url-filters + +[drf-url-filter][drf-url-filter] is a simple Django app to apply filters on drf `ModelViewSet`'s `Queryset` in a clean, simple and configurable way. It also supports validations on incoming query params and their values. A beautiful python package `Voluptouos` is being used for validations on the incoming query parameters. The best part about voluptouos is you can define your own validations as per your query params requirements. + [cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters [django-filter]: https://github.com/alex/django-filter [django-filter-docs]: https://django-filter.readthedocs.io/en/latest/index.html @@ -443,3 +447,5 @@ The [djangorestframework-word-filter][django-rest-framework-word-search-filter] [django-rest-framework-filters]: https://github.com/philipn/django-rest-framework-filters [django-rest-framework-word-search-filter]: https://github.com/trollknurr/django-rest-framework-word-search-filter [django-url-filter]: https://github.com/miki725/django-url-filter +[drf-url-filter]: https://github.com/manjitkumar/drf-url-filters + From 1b882f7281b475a8cb6eb993299d3ec2248dc535 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Fri, 30 Sep 2016 18:06:36 +0200 Subject: [PATCH 258/457] Add drf-dynamic-fields to third party packages (#4530) --- docs/api-guide/serializers.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 5772d940a..290e32f4f 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -1089,6 +1089,7 @@ The following third party packages are also available. The [django-rest-marshmallow][django-rest-marshmallow] package provides an alternative implementation for serializers, using the python [marshmallow][marshmallow] library. It exposes the same API as the REST framework serializers, and can be used as a drop-in replacement in some use-cases. ## Serpy + The [serpy][serpy] package is an alternative implementation for serializers that is built for speed. [Serpy][serpy] serializes complex datatypes to simple native types. The native types can be easily converted to JSON or any other format needed. ## MongoengineModelSerializer @@ -1107,7 +1108,12 @@ The [django-rest-framework-hstore][django-rest-framework-hstore] package provide The [dynamic-rest][dynamic-rest] package extends the ModelSerializer and ModelViewSet interfaces, adding API query parameters for filtering, sorting, and including / excluding all fields and relationships defined by your serializers. +## Dynamic Fields Mixin + +The [drf-dynamic-fields][drf-dynamic-fields] package provides a mixin to dynamically limit the fields per serializer to a subset specified by an URL parameter. + ## HTML JSON Forms + The [html-json-forms][html-json-forms] package provides an algorithm and serializer for processing `
` submissions per the (inactive) [HTML JSON Form specification][json-form-spec]. The serializer facilitates processing of arbitrarily nested JSON structures within HTML. For example, `` will be interpreted as `{"items": [{"id": "5"}]}`. [cite]: https://groups.google.com/d/topic/django-users/sVFaOfQi4wY/discussion @@ -1124,3 +1130,4 @@ The [html-json-forms][html-json-forms] package provides an algorithm and seriali [dynamic-rest]: https://github.com/AltSchool/dynamic-rest [html-json-forms]: https://github.com/wq/html-json-forms [json-form-spec]: https://www.w3.org/TR/html-json-forms/ +[drf-dynamic-fields]: https://github.com/dbrgn/drf-dynamic-fields From b8fcb7807a11f37c88e9bfc6eaf999875e392114 Mon Sep 17 00:00:00 2001 From: Angel Velasquez Date: Sun, 2 Oct 2016 03:11:40 -0300 Subject: [PATCH 259/457] Update Django security release 1.10.2 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 8c20faa2b..48cecccf7 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,7 @@ setenv = deps = django18: Django==1.8.15 django19: Django==1.9.10 - django110: Django==1.10 + django110: Django==1.10.2 djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From 883efbc19fb124db7741caad512afb5b482e291d Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Tue, 4 Oct 2016 14:44:50 +0200 Subject: [PATCH 260/457] Case insensitive uniqueness validation (#4534) --- docs/api-guide/validators.md | 1 + rest_framework/validators.py | 5 +++-- tests/test_validators.py | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index f04c74c3c..9df15ec15 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -61,6 +61,7 @@ It takes a single required argument, and an optional `messages` argument: * `queryset` *required* - This is the queryset against which uniqueness should be enforced. * `message` - The error message that should be used when validation fails. +* `lookup` - The lookup used to find an existing instance with the value being validated. Defaults to `'exact'`. This validator should be applied to *serializer fields*, like so: diff --git a/rest_framework/validators.py b/rest_framework/validators.py index ef23b9bd7..84af0b9d5 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -42,10 +42,11 @@ class UniqueValidator(object): """ message = _('This field must be unique.') - def __init__(self, queryset, message=None): + def __init__(self, queryset, message=None, lookup='exact'): self.queryset = queryset self.serializer_field = None self.message = message or self.message + self.lookup = lookup def set_context(self, serializer_field): """ @@ -62,7 +63,7 @@ class UniqueValidator(object): """ Filter the queryset to all instances matching the given attribute. """ - filter_kwargs = {self.field_name: value} + filter_kwargs = {'%s__%s' % (self.field_name, self.lookup): value} return qs_filter(queryset, **filter_kwargs) def exclude_current_instance(self, queryset): diff --git a/tests/test_validators.py b/tests/test_validators.py index ebc8b899f..ab2c87c11 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -31,7 +31,7 @@ class RelatedModel(models.Model): class RelatedModelSerializer(serializers.ModelSerializer): username = serializers.CharField(source='user.username', - validators=[UniqueValidator(queryset=UniquenessModel.objects.all())]) # NOQA + validators=[UniqueValidator(queryset=UniquenessModel.objects.all(), lookup='iexact')]) # NOQA class Meta: model = RelatedModel @@ -103,7 +103,7 @@ class TestUniquenessValidation(TestCase): AnotherUniquenessModel._meta.get_field('code').validators, []) def test_related_model_is_unique(self): - data = {'username': 'existing', 'email': 'new-email@example.com'} + data = {'username': 'Existing', 'email': 'new-email@example.com'} rs = RelatedModelSerializer(data=data) self.assertFalse(rs.is_valid()) self.assertEqual(rs.errors, From 915ac22aeb5cce447ea20863c594387d71159826 Mon Sep 17 00:00:00 2001 From: Alex Kahan Date: Tue, 4 Oct 2016 16:22:56 -0400 Subject: [PATCH 261/457] Adding tests for rest_framework.py (#4523) --- tests/test_templatetags.py | 145 ++++++++++++++++++++++++++++++++++++- 1 file changed, 144 insertions(+), 1 deletion(-) diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py index ac218df21..28390320b 100644 --- a/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -3,14 +3,24 @@ from __future__ import unicode_literals from django.test import TestCase +from rest_framework.relations import Hyperlink from rest_framework.templatetags.rest_framework import ( - add_query_param, urlize_quoted_links + add_nested_class, add_query_param, format_value, urlize_quoted_links ) from rest_framework.test import APIRequestFactory factory = APIRequestFactory() +def format_html(html): + """ + Helper function that formats HTML in order for easier comparison + :param html: raw HTML text to be formatted + :return: Cleaned HTML with no newlines or spaces + """ + return html.replace('\n', '').replace(' ', '') + + class TemplateTagTests(TestCase): def test_add_query_param_with_non_latin_character(self): @@ -22,6 +32,139 @@ class TemplateTagTests(TestCase): self.assertIn("q=%E6%9F%A5%E8%AF%A2", json_url) self.assertIn("format=json", json_url) + def test_format_value_boolean_or_none(self): + """ + Tests format_value with booleans and None + """ + self.assertEqual(format_value(True), 'true') + self.assertEqual(format_value(False), 'false') + self.assertEqual(format_value(None), 'null') + + def test_format_value_hyperlink(self): + url = 'http://url.com' + name = 'name_of_url' + hyperlink = Hyperlink(url, name) + self.assertEqual(format_value(hyperlink), '%s' % (url, name)) + + def test_format_value_list(self): + """ + Tests format_value with a list of strings + """ + list_items = ['item1', 'item2', 'item3'] + self.assertEqual(format_value(list_items), '\n item1, item2, item3\n') + self.assertEqual(format_value([]), '\n\n') + + def test_format_value_table(self): + """ + Tests format_value with a list of lists/dicts + """ + list_of_lists = [['list1'], ['list2'], ['list3']] + expected_list_format = """ + + + + 0 + list1 + + + 1 + list2 + + + 2 + list3 + + + """ + self.assertEqual( + format_html(format_value(list_of_lists)), + format_html(expected_list_format) + ) + + expected_dict_format = """ + + + + 0 + + + + 1 + + + + 2 + + + + """ + + list_of_dicts = [{'item1': 'value1'}, {'item2': 'value2'}, {'item3': 'value3'}] + self.assertEqual( + format_html(format_value(list_of_dicts)), + format_html(expected_dict_format) + ) + + def test_format_value_simple_string(self): + """ + Tests format_value with a simple string + """ + simple_string = 'this is an example of a string' + self.assertEqual(format_value(simple_string), simple_string) + + def test_format_value_string_hyperlink(self): + """ + Tests format_value with a url + """ + url = 'http://www.example.com' + self.assertEqual(format_value(url), 'http://www.example.com') + + def test_format_value_string_email(self): + """ + Tests format_value with an email address + """ + email = 'something@somewhere.com' + self.assertEqual(format_value(email), 'something@somewhere.com') + + def test_format_value_string_newlines(self): + """ + Tests format_value with a string with newline characters + :return: + """ + text = 'Dear user, \n this is a message \n from,\nsomeone' + self.assertEqual(format_value(text), '
Dear user, \n this is a message \n from,\nsomeone
') + + def test_format_value_object(self): + """ + Tests that format_value with a object returns the object's __str__ method + """ + obj = object() + self.assertEqual(format_value(obj), obj.__str__()) + + def test_add_nested_class(self): + """ + Tests that add_nested_class returns the proper class + """ + positive_cases = [ + [['item']], + [{'item1': 'value1'}], + {'item1': 'value1'} + ] + + negative_cases = [ + ['list'], + '', + None, + True, + False + ] + + for case in positive_cases: + self.assertEqual(add_nested_class(case), 'class=nested') + + for case in negative_cases: + self.assertEqual(add_nested_class(case), '') + class Issue1386Tests(TestCase): """ From 4ff9e96b4c3e75b9f799c7c401a4560f97e21b3a Mon Sep 17 00:00:00 2001 From: Alex Kahan Date: Wed, 5 Oct 2016 04:54:59 -0400 Subject: [PATCH 262/457] Adding tests to encoder.py (#4536) --- tests/test_encoders.py | 81 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tests/test_encoders.py diff --git a/tests/test_encoders.py b/tests/test_encoders.py new file mode 100644 index 000000000..d6f681932 --- /dev/null +++ b/tests/test_encoders.py @@ -0,0 +1,81 @@ +from datetime import date, datetime, timedelta, tzinfo +from decimal import Decimal +from uuid import uuid4 + +from django.test import TestCase + +from rest_framework.utils.encoders import JSONEncoder + + +class JSONEncoderTests(TestCase): + """ + Tests the JSONEncoder method + """ + + def setUp(self): + self.encoder = JSONEncoder() + + def test_encode_decimal(self): + """ + Tests encoding a decimal + """ + d = Decimal(3.14) + self.assertEqual(d, float(d)) + + def test_encode_datetime(self): + """ + Tests encoding a datetime object + """ + current_time = datetime.now() + self.assertEqual(self.encoder.default(current_time), current_time.isoformat()) + + def test_encode_time(self): + """ + Tests encoding a timezone + """ + current_time = datetime.now().time() + self.assertEqual(self.encoder.default(current_time), current_time.isoformat()[:12]) + + def test_encode_time_tz(self): + """ + Tests encoding a timezone aware timestamp + """ + + class UTC(tzinfo): + """ + Class extending tzinfo to mimic UTC time + """ + def utcoffset(self, dt): + return timedelta(0) + + def tzname(self, dt): + return "UTC" + + def dst(self, dt): + return timedelta(0) + + current_time = datetime.now().time() + current_time = current_time.replace(tzinfo=UTC()) + with self.assertRaises(ValueError): + self.encoder.default(current_time) + + def test_encode_date(self): + """ + Tests encoding a date object + """ + current_date = date.today() + self.assertEqual(self.encoder.default(current_date), current_date.isoformat()) + + def test_encode_timedelta(self): + """ + Tests encoding a timedelta object + """ + delta = timedelta(hours=1) + self.assertEqual(self.encoder.default(delta), str(delta.total_seconds())) + + def test_encode_uuid(self): + """ + Tests encoding a UUID object + """ + unique_id = uuid4() + self.assertEqual(self.encoder.default(unique_id), str(unique_id)) From e99b30d28b9173c38bb238b6d5af50a1797884eb Mon Sep 17 00:00:00 2001 From: Camille Harang Date: Mon, 10 Oct 2016 12:59:02 +0200 Subject: [PATCH 263/457] =?UTF-8?q?Fix=20rest=5Fframework.filters.Ordering?= =?UTF-8?q?Filter=20doesn't=20pass=20context=20to=20ser=E2=80=A6=20(#4543)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix rest_framework.filters.OrderingFilter doesn't pass context to serializers #4541 * #4541 Additional fix for remove_invalid_fields() --- rest_framework/filters.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index aaf313491..0050ee7cd 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -252,7 +252,7 @@ class OrderingFilter(BaseFilterBackend): params = request.query_params.get(self.ordering_param) if params: fields = [param.strip() for param in params.split(',')] - ordering = self.remove_invalid_fields(queryset, fields, view) + ordering = self.remove_invalid_fields(queryset, fields, view, request) if ordering: return ordering @@ -265,7 +265,7 @@ class OrderingFilter(BaseFilterBackend): return (ordering,) return ordering - def get_default_valid_fields(self, queryset, view): + def get_default_valid_fields(self, queryset, view, context={}): # If `ordering_fields` is not specified, then we determine a default # based on the serializer class, if one exists on the view. if hasattr(view, 'get_serializer_class'): @@ -288,16 +288,16 @@ class OrderingFilter(BaseFilterBackend): return [ (field.source or field_name, field.label) - for field_name, field in serializer_class().fields.items() + for field_name, field in serializer_class(context=context).fields.items() if not getattr(field, 'write_only', False) and not field.source == '*' ] - def get_valid_fields(self, queryset, view): + def get_valid_fields(self, queryset, view, context={}): valid_fields = getattr(view, 'ordering_fields', self.ordering_fields) if valid_fields is None: # Default to allowing filtering on serializer fields - return self.get_default_valid_fields(queryset, view) + return self.get_default_valid_fields(queryset, view, context) elif valid_fields == '__all__': # View explicitly allows filtering on any model field @@ -316,8 +316,8 @@ class OrderingFilter(BaseFilterBackend): return valid_fields - def remove_invalid_fields(self, queryset, fields, view): - valid_fields = [item[0] for item in self.get_valid_fields(queryset, view)] + def remove_invalid_fields(self, queryset, fields, view, request): + valid_fields = [item[0] for item in self.get_valid_fields(queryset, view, {'request': request})] return [term for term in fields if term.lstrip('-') in valid_fields] def filter_queryset(self, request, queryset, view): @@ -332,15 +332,16 @@ class OrderingFilter(BaseFilterBackend): current = self.get_ordering(request, queryset, view) current = None if current is None else current[0] options = [] - for key, label in self.get_valid_fields(queryset, view): - options.append((key, '%s - %s' % (label, _('ascending')))) - options.append(('-' + key, '%s - %s' % (label, _('descending')))) - return { + context = { 'request': request, 'current': current, 'param': self.ordering_param, - 'options': options, } + for key, label in self.get_valid_fields(queryset, view, context): + options.append((key, '%s - %s' % (label, _('ascending')))) + options.append(('-' + key, '%s - %s' % (label, _('descending')))) + context['options'] = options + return context def to_html(self, request, queryset, view): template = loader.get_template(self.template) From d49e26f1279adb8f36818358281279dc124522c6 Mon Sep 17 00:00:00 2001 From: Manjit Kumar Date: Mon, 10 Oct 2016 17:05:38 +0530 Subject: [PATCH 264/457] add drf-url-filters in third party filtering resources and fixed typo (#4548) --- docs/api-guide/filtering.md | 2 +- docs/topics/third-party-resources.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index bc502e970..ab7e5fa77 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -434,7 +434,7 @@ The [djangorestframework-word-filter][django-rest-framework-word-search-filter] ## drf-url-filters -[drf-url-filter][drf-url-filter] is a simple Django app to apply filters on drf `ModelViewSet`'s `Queryset` in a clean, simple and configurable way. It also supports validations on incoming query params and their values. A beautiful python package `Voluptouos` is being used for validations on the incoming query parameters. The best part about voluptouos is you can define your own validations as per your query params requirements. +[drf-url-filter][drf-url-filter] is a simple Django app to apply filters on drf `ModelViewSet`'s `Queryset` in a clean, simple and configurable way. It also supports validations on incoming query params and their values. A beautiful python package `Voluptuous` is being used for validations on the incoming query parameters. The best part about voluptuous is you can define your own validations as per your query params requirements. [cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters [django-filter]: https://github.com/alex/django-filter diff --git a/docs/topics/third-party-resources.md b/docs/topics/third-party-resources.md index 4366269bb..3fba9b5da 100644 --- a/docs/topics/third-party-resources.md +++ b/docs/topics/third-party-resources.md @@ -238,6 +238,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [djangorestframework-chain][djangorestframework-chain] - Allows arbitrary chaining of both relations and lookup filters. * [django-url-filter][django-url-filter] - Allows a safe way to filter data via human-friendly URLs. It is a generic library which is not tied to DRF but it provides easy integration with DRF. +* [drf-url-filter][drf-url-filter] is a simple Django app to apply filters on drf `ModelViewSet`'s `Queryset` in a clean, simple and configurable way. It also supports validations on incoming query params and their values. ### Misc @@ -351,6 +352,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [django-rest-framework-braces]: https://github.com/dealertrack/django-rest-framework-braces [dry-rest-permissions]: https://github.com/Helioscene/dry-rest-permissions [django-url-filter]: https://github.com/miki725/django-url-filter +[drf-url-filter]: https://github.com/manjitkumar/drf-url-filters [cookiecutter-django-rest]: https://github.com/agconti/cookiecutter-django-rest [drf-haystack]: https://drf-haystack.readthedocs.io/en/latest/ [django-rest-framework-version-transforms]: https://github.com/mrhwick/django-rest-framework-version-transforms From 0dec36eb413c98902d997ebd2f9cb2b7111e6e56 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 10 Oct 2016 13:03:46 +0100 Subject: [PATCH 265/457] Version 3.5 (#4525) * Start test case * Added 'requests' test client * Address typos * Graceful fallback if requests is not installed. * Add cookie support * Tests for auth and CSRF * Py3 compat * py3 compat * py3 compat * Add get_requests_client * Added SchemaGenerator.should_include_link * add settings for html cutoff on related fields * Router doesn't work if prefix is blank, though project urls.py handles prefix * Fix Django 1.10 to-many deprecation * Add django.core.urlresolvers compatibility * Update django-filter & django-guardian * Check for empty router prefix; adjust URL accordingly It's easiest to fix this issue after we have made the regex. To try to fix it before would require doing something different for List vs Detail, which means we'd have to know which type of url we're constructing before acting accordingly. * Fix misc django deprecations * Use TOC extension instead of header * Fix deprecations for py3k * Add py3k compatibility to is_simple_callable * Add is_simple_callable tests * Drop python 3.2 support (EOL, Dropped by Django) * schema_renderers= should *set* the renderers, not append to them. * API client (#4424) * Fix release notes * Add note about 'User account is disabled.' vs 'Unable to log in' * Clean up schema generation (#4527) * Handle multiple methods on custom action (#4529) * RequestsClient, CoreAPIClient * exclude_from_schema * Added 'get_schema_view()' shortcut * Added schema descriptions * Better descriptions for schemas * Add type annotation to schema generation * Coerce schema 'pk' in path to actual field name * Deprecations move into assertion errors * Use get_schema_view in tests * Updte CoreJSON media type * Handle schema structure correctly when path prefixs exist. Closes #4401 * Add PendingDeprecation to Router schema generation. * Added SCHEMA_COERCE_PATH_PK and SCHEMA_COERCE_METHOD_NAMES * Renamed and documented 'get_schema_fields' interface. --- .travis.yml | 1 - docs/api-guide/filtering.md | 6 + docs/api-guide/pagination.md | 6 + docs/api-guide/relations.md | 2 + docs/api-guide/reverse.md | 4 +- docs/api-guide/schemas.md | 140 +++-- docs/api-guide/settings.md | 38 ++ docs/api-guide/testing.md | 95 ++- docs/api-guide/views.md | 12 +- docs/tutorial/1-serialization.md | 14 +- docs/tutorial/2-requests-and-responses.md | 8 +- docs/tutorial/3-class-based-views.md | 18 +- .../4-authentication-and-permissions.md | 4 +- .../5-relationships-and-hyperlinked-apis.md | 14 +- docs/tutorial/6-viewsets-and-routers.md | 6 +- .../7-schemas-and-client-libraries.md | 47 +- requirements/requirements-optionals.txt | 6 +- rest_framework/__init__.py | 2 +- rest_framework/authtoken/serializers.py | 3 + rest_framework/compat.py | 40 +- rest_framework/decorators.py | 3 +- rest_framework/fields.py | 38 +- rest_framework/filters.py | 28 +- rest_framework/pagination.py | 34 +- rest_framework/relations.py | 37 +- rest_framework/renderers.py | 6 +- rest_framework/reverse.py | 6 +- rest_framework/routers.py | 79 ++- rest_framework/schemas.py | 551 +++++++++++++----- rest_framework/serializers.py | 29 +- rest_framework/settings.py | 11 + rest_framework/templatetags/rest_framework.py | 3 +- rest_framework/test.py | 127 ++++ rest_framework/urlpatterns.py | 2 +- rest_framework/utils/breadcrumbs.py | 2 +- rest_framework/views.py | 4 + tests/browsable_api/test_form_rendering.py | 1 + tests/conftest.py | 15 +- tests/test_api_client.py | 474 +++++++++++++++ tests/test_atomic_requests.py | 32 +- tests/test_authentication.py | 3 +- tests/test_fields.py | 62 ++ tests/test_filters.py | 6 +- tests/test_model_serializer.py | 7 +- tests/test_permissions.py | 11 +- tests/test_request.py | 5 +- tests/test_requests_client.py | 256 ++++++++ tests/test_reverse.py | 2 +- tests/test_routers.py | 49 ++ tests/test_schemas.py | 217 +++++-- tests/test_urlpatterns.py | 8 +- tests/utils.py | 2 +- tox.ini | 3 +- 53 files changed, 2132 insertions(+), 447 deletions(-) create mode 100644 tests/test_api_client.py create mode 100644 tests/test_requests_client.py diff --git a/.travis.yml b/.travis.yml index c9d9a1648..100a7cd8b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,6 @@ env: - TOX_ENV=py35-django18 - TOX_ENV=py34-django18 - TOX_ENV=py33-django18 - - TOX_ENV=py32-django18 - TOX_ENV=py27-django18 - TOX_ENV=py27-django110 - TOX_ENV=py35-django110 diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index ab7e5fa77..40a097174 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -416,6 +416,12 @@ Generic filters may also present an interface in the browsable API. To do so you The method should return a rendered HTML string. +## Pagination & schemas + +You can also make the filter controls available to the schema autogeneration +that REST framework provides, by implementing a `get_schema_fields()` method, +which should return a list of `coreapi.Field` instances. + # Third party packages The following third party packages provide additional filter implementations. diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index 088e07184..f990128c5 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -276,6 +276,12 @@ To have your custom pagination class be used by default, use the `DEFAULT_PAGINA API responses for list endpoints will now include a `Link` header, instead of including the pagination links as part of the body of the response, for example: +## Pagination & schemas + +You can also make the pagination controls available to the schema autogeneration +that REST framework provides, by implementing a `get_schema_fields()` method, +which should return a list of `coreapi.Field` instances. + --- ![Link Header][link-header] diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 42533823c..8fde53d3a 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -463,6 +463,8 @@ There are two keyword arguments you can use to control this behavior: - `html_cutoff` - If set this will be the maximum number of choices that will be displayed by a HTML select drop down. Set to `None` to disable any limiting. Defaults to `1000`. - `html_cutoff_text` - If set this will display a textual indicator if the maximum number of items have been cutoff in an HTML select drop down. Defaults to `"More than {count} items…"` +You can also control these globally using the settings `HTML_SELECT_CUTOFF` and `HTML_SELECT_CUTOFF_TEXT`. + In cases where the cutoff is being enforced you may want to instead use a plain input field in the HTML form. You can do so using the `style` keyword argument. For example: assigned_to = serializers.SlugRelatedField( diff --git a/docs/api-guide/reverse.md b/docs/api-guide/reverse.md index 71fb83f9e..35d88e2db 100644 --- a/docs/api-guide/reverse.md +++ b/docs/api-guide/reverse.md @@ -23,7 +23,7 @@ There's no requirement for you to use them, but if you do then the self-describi **Signature:** `reverse(viewname, *args, **kwargs)` -Has the same behavior as [`django.core.urlresolvers.reverse`][reverse], except that it returns a fully qualified URL, using the request to determine the host and port. +Has the same behavior as [`django.urls.reverse`][reverse], except that it returns a fully qualified URL, using the request to determine the host and port. You should **include the request as a keyword argument** to the function, for example: @@ -44,7 +44,7 @@ You should **include the request as a keyword argument** to the function, for ex **Signature:** `reverse_lazy(viewname, *args, **kwargs)` -Has the same behavior as [`django.core.urlresolvers.reverse_lazy`][reverse-lazy], except that it returns a fully qualified URL, using the request to determine the host and port. +Has the same behavior as [`django.urls.reverse_lazy`][reverse-lazy], except that it returns a fully qualified URL, using the request to determine the host and port. As with the `reverse` function, you should **include the request as a keyword argument** to the function, for example: diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index 7f8af723e..16d2bbb01 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -102,15 +102,20 @@ REST framework includes functionality for auto-generating a schema, or allows you to specify one explicitly. There are a few different ways to add a schema to your API, depending on exactly what you need. -## Using DefaultRouter +## The get_schema_view shortcut -If you're using `DefaultRouter` then you can include an auto-generated schema, -simply by adding a `schema_title` argument to the router. +The simplest way to include a schema in your project is to use the +`get_schema_view()` function. - router = DefaultRouter(schema_title='Server Monitoring API') + schema_view = get_schema_view(title="Server Monitoring API") -The schema will be included at the root URL, `/`, and presented to clients -that include the Core JSON media type in their `Accept` header. + urlpatterns = [ + url('^$', schema_view), + ... + ] + +Once the view has been added, you'll be able to make API requests to retrieve +the auto-generated schema definition. $ http http://127.0.0.1:8000/ Accept:application/vnd.coreapi+json HTTP/1.0 200 OK @@ -125,48 +130,43 @@ that include the Core JSON media type in their `Accept` header. ... } -This is a great zero-configuration option for when you want to get up and -running really quickly. +The arguments to `get_schema_view()` are: -The other available options to `DefaultRouter` are: +#### `title` -#### schema_renderers +May be used to provide a descriptive title for the schema definition. -May be used to pass the set of renderer classes that can be used to render schema output. +#### `url` + +May be used to pass a canonical URL for the schema. + + schema_view = get_schema_view( + title='Server Monitoring API', + url='https://www.example.org/api/' + ) + +#### `renderer_classes` + +May be used to pass the set of renderer classes that can be used to render the API root endpoint. from rest_framework.renderers import CoreJSONRenderer from my_custom_package import APIBlueprintRenderer - router = DefaultRouter(schema_title='Server Monitoring API', schema_renderers=[ - CoreJSONRenderer, APIBlueprintRenderer - ]) - -#### schema_url - -May be used to pass the root URL for the schema. This can either be used to ensure that -the schema URLs include a canonical hostname and schema, or to ensure that all the -schema URLs include a path prefix. - - router = DefaultRouter( - schema_title='Server Monitoring API', - schema_url='https://www.example.org/api/' + schema_view = get_schema_view( + title='Server Monitoring API', + url='https://www.example.org/api/', + renderer_classes=[CoreJSONRenderer, APIBlueprintRenderer] ) -If you want more flexibility over the schema output then you'll need to consider -using `SchemaGenerator` instead. +## Using an explicit schema view -#### root_renderers - -May be used to pass the set of renderer classes that can be used to render the API root endpoint. - -## Using SchemaGenerator - -The most common way to add a schema to your API is to use the `SchemaGenerator` -class to auto-generate the `Document` instance, and to return that from a view. +If you need a little more control than the `get_schema_view()` shortcut gives you, +then you can use the `SchemaGenerator` class directly to auto-generate the +`Document` instance, and to return that from a view. This option gives you the flexibility of setting up the schema endpoint -with whatever behavior you want. For example, you can apply different -permission, throttling or authentication policies to the schema endpoint. +with whatever behaviour you want. For example, you can apply different +permission, throttling, or authentication policies to the schema endpoint. Here's an example of using `SchemaGenerator` together with a view to return the schema. @@ -176,12 +176,13 @@ return the schema. from rest_framework.decorators import api_view, renderer_classes from rest_framework import renderers, response, schemas + generator = schemas.SchemaGenerator(title='Bookings API') @api_view() @renderer_classes([renderers.CoreJSONRenderer]) def schema_view(request): - generator = schemas.SchemaGenerator(title='Bookings API') - return response.Response(generator.get_schema()) + schema = generator.get_schema(request) + return response.Response(schema) **urls.py:** @@ -241,6 +242,69 @@ You could then either: --- +# Schemas as documentation + +One common usage of API schemas is to use them to build documentation pages. + +The schema generation in REST framework uses docstrings to automatically +populate descriptions in the schema document. + +These descriptions will be based on: + +* The corresponding method docstring if one exists. +* A named section within the class docstring, which can be either single line or multi-line. +* The class docstring. + +## Examples + +An `APIView`, with an explicit method docstring. + + class ListUsernames(APIView): + def get(self, request): + """ + Return a list of all user names in the system. + """ + usernames = [user.username for user in User.objects.all()] + return Response(usernames) + +A `ViewSet`, with an explict action docstring. + + class ListUsernames(ViewSet): + def list(self, request): + """ + Return a list of all user names in the system. + """ + usernames = [user.username for user in User.objects.all()] + return Response(usernames) + +A generic view with sections in the class docstring, using single-line style. + + class UserList(generics.ListCreateAPIView): + """ + get: Create a new user. + post: List all the users. + """ + queryset = User.objects.all() + serializer_class = UserSerializer + permission_classes = (IsAdminUser,) + +A generic viewset with sections in the class docstring, using multi-line style. + + class UserViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows users to be viewed or edited. + + retrieve: + Return a user instance. + + list: + Return all users, ordered by most recently joined. + """ + queryset = User.objects.all().order_by('-date_joined') + serializer_class = UserSerializer + +--- + # Alternate schema formats In order to support an alternate schema format, you need to implement a custom renderer diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index ea018053f..58ceeeeb4 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -234,6 +234,28 @@ Default: --- +## Schema generation controls + +#### SCHEMA_COERCE_PATH_PK + +If set, this maps the `'pk'` identifier in the URL conf onto the actual field +name when generating a schema path parameter. Typically this will be `'id'`. +This gives a more suitable representation as "primary key" is an implementation +detail, wheras "identifier" is a more general concept. + +Default: `True` + +#### SCHEMA_COERCE_METHOD_NAMES + +If set, this is used to map internal viewset method names onto external action +names used in the schema generation. This allows us to generate names that +are more suitable for an external representation than those that are used +internally in the codebase. + +Default: `{'retrieve': 'read', 'destroy': 'delete'}` + +--- + ## Content type controls #### URL_FORMAT_OVERRIDE @@ -382,6 +404,22 @@ This should be a function with the following signature: Default: `'rest_framework.views.get_view_description'` +## HTML Select Field cutoffs + +Global settings for [select field cutoffs for rendering relational fields](relations.md#select-field-cutoffs) in the browsable API. + +#### HTML_SELECT_CUTOFF + +Global setting for the `html_cutoff` value. Must be an integer. + +Default: 1000 + +#### HTML_SELECT_CUTOFF_TEXT + +A string representing a global setting for `html_cutoff_text`. + +Default: `"More than {count} items..."` + --- ## Miscellaneous settings diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 69da7d105..fdd60b7f4 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -184,6 +184,99 @@ As usual CSRF validation will only apply to any session authenticated views. Th --- +# RequestsClient + +REST framework also includes a client for interacting with your application +using the popular Python library, `requests`. + +This exposes exactly the same interface as if you were using a requests session +directly. + + client = RequestsClient() + response = client.get('http://testserver/users/') + +Note that the requests client requires you to pass fully qualified URLs. + +## Headers & Authentication + +Custom headers and authentication credentials can be provided in the same way +as [when using a standard `requests.Session` instance](http://docs.python-requests.org/en/master/user/advanced/#session-objects). + + from requests.auth import HTTPBasicAuth + + client.auth = HTTPBasicAuth('user', 'pass') + client.headers.update({'x-test': 'true'}) + +## CSRF + +If you're using `SessionAuthentication` then you'll need to include a CSRF token +for any `POST`, `PUT`, `PATCH` or `DELETE` requests. + +You can do so by following the same flow that a JavaScript based client would use. +First make a `GET` request in order to obtain a CRSF token, then present that +token in the following request. + +For example... + + client = RequestsClient() + + # Obtain a CSRF token. + response = client.get('/homepage/') + assert response.status_code == 200 + csrftoken = response.cookies['csrftoken'] + + # Interact with the API. + response = client.post('/organisations/', json={ + 'name': 'MegaCorp', + 'status': 'active' + }, headers={'X-CSRFToken': csrftoken}) + assert response.status_code == 200 + +## Live tests + +With careful usage both the `RequestsClient` and the `CoreAPIClient` provide +the ability to write test cases that can run either in development, or be run +directly against your staging server or production environment. + +Using this style to create basic tests of a few core piece of functionality is +a powerful way to validate your live service. Doing so may require some careful +attention to setup and teardown to ensure that the tests run in a way that they +do not directly affect customer data. + +--- + +# CoreAPIClient + +The CoreAPIClient allows you to interact with your API using the Python +`coreapi` client library. + + # Fetch the API schema + url = reverse('schema') + client = CoreAPIClient() + schema = client.get(url) + + # Create a new organisation + params = {'name': 'MegaCorp', 'status': 'active'} + client.action(schema, ['organisations', 'create'], params) + + # Ensure that the organisation exists in the listing + data = client.action(schema, ['organisations', 'list']) + assert(len(data) == 1) + assert(data == [{'name': 'MegaCorp', 'status': 'active'}]) + +## Headers & Authentication + +Custom headers and authentication may be used with `CoreAPIClient` in a +similar way as with `RequestsClient`. + + from requests.auth import HTTPBasicAuth + + client = CoreAPIClient() + client.session.auth = HTTPBasicAuth('user', 'pass') + client.session.headers.update({'x-test': 'true'}) + +--- + # Test cases REST framework includes the following test case classes, that mirror the existing Django test case classes, but use `APIClient` instead of Django's default `Client`. @@ -197,7 +290,7 @@ REST framework includes the following test case classes, that mirror the existin You can use any of REST framework's test case classes as you would for the regular Django test case classes. The `self.client` attribute will be an `APIClient` instance. - from django.core.urlresolvers import reverse + from django.urls import reverse from rest_framework import status from rest_framework.test import APITestCase from myproject.apps.core.models import Account diff --git a/docs/api-guide/views.md b/docs/api-guide/views.md index 62f14087f..55f6664e0 100644 --- a/docs/api-guide/views.md +++ b/docs/api-guide/views.md @@ -127,7 +127,7 @@ REST framework also allows you to work with regular function based views. It pr ## @api_view() -**Signature:** `@api_view(http_method_names=['GET'])` +**Signature:** `@api_view(http_method_names=['GET'], exclude_from_schema=False)` The core of this functionality is the `api_view` decorator, which takes a list of HTTP methods that your view should respond to. For example, this is how you would write a very simple view that just manually returns some data: @@ -139,7 +139,7 @@ The core of this functionality is the `api_view` decorator, which takes a list o This view will use the default renderers, parsers, authentication classes etc specified in the [settings]. -By default only `GET` methods will be accepted. Other methods will respond with "405 Method Not Allowed". To alter this behavior, specify which methods the view allows, like so: +By default only `GET` methods will be accepted. Other methods will respond with "405 Method Not Allowed". To alter this behaviour, specify which methods the view allows, like so: @api_view(['GET', 'POST']) def hello_world(request): @@ -147,6 +147,13 @@ By default only `GET` methods will be accepted. Other methods will respond with return Response({"message": "Got some data!", "data": request.data}) return Response({"message": "Hello, world!"}) +You can also mark an API view as being omitted from any [auto-generated schema][schemas], +using the `exclude_from_schema` argument.: + + @api_view(['GET'], exclude_from_schema=True) + def api_docs(request): + ... + ## API policy decorators To override the default settings, REST framework provides a set of additional decorators which can be added to your views. These must come *after* (below) the `@api_view` decorator. For example, to create a view that uses a [throttle][throttling] to ensure it can only be called once per day by a particular user, use the `@throttle_classes` decorator, passing a list of throttle classes: @@ -178,3 +185,4 @@ Each of these decorators takes a single argument which must be a list or tuple o [cite2]: http://www.boredomandlaziness.org/2012/05/djangos-cbvs-are-not-mistake-but.html [settings]: settings.md [throttling]: throttling.md +[schemas]: schemas.md diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 87856e037..434072e11 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -88,7 +88,7 @@ The first thing we need to get started on our Web API is to provide a way of ser class SnippetSerializer(serializers.Serializer): - pk = serializers.IntegerField(read_only=True) + id = serializers.IntegerField(read_only=True) title = serializers.CharField(required=False, allow_blank=True, max_length=100) code = serializers.CharField(style={'base_template': 'textarea.html'}) linenos = serializers.BooleanField(required=False) @@ -144,13 +144,13 @@ We've now got a few snippet instances to play with. Let's take a look at serial serializer = SnippetSerializer(snippet) serializer.data - # {'pk': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'} + # {'id': 2, 'title': u'', 'code': u'print "hello, world"\n', 'linenos': False, 'language': u'python', 'style': u'friendly'} At this point we've translated the model instance into Python native datatypes. To finalize the serialization process we render the data into `json`. content = JSONRenderer().render(serializer.data) content - # '{"pk": 2, "title": "", "code": "print \\"hello, world\\"\\n", "linenos": false, "language": "python", "style": "friendly"}' + # '{"id": 2, "title": "", "code": "print \\"hello, world\\"\\n", "linenos": false, "language": "python", "style": "friendly"}' Deserialization is similar. First we parse a stream into Python native datatypes... @@ -175,7 +175,7 @@ We can also serialize querysets instead of model instances. To do so we simply serializer = SnippetSerializer(Snippet.objects.all(), many=True) serializer.data - # [OrderedDict([('pk', 1), ('title', u''), ('code', u'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('pk', 2), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('pk', 3), ('title', u''), ('code', u'print "hello, world"'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])] + # [OrderedDict([('id', 1), ('title', u''), ('code', u'foo = "bar"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 2), ('title', u''), ('code', u'print "hello, world"\n'), ('linenos', False), ('language', 'python'), ('style', 'friendly')]), OrderedDict([('id', 3), ('title', u''), ('code', u'print "hello, world"'), ('linenos', False), ('language', 'python'), ('style', 'friendly')])] ## Using ModelSerializers @@ -259,12 +259,12 @@ Note that because we want to be able to POST to this view from clients that won' We'll also need a view which corresponds to an individual snippet, and can be used to retrieve, update or delete the snippet. @csrf_exempt - def snippet_detail(request, pk): + def snippet_detail(request, id): """ Retrieve, update or delete a code snippet. """ try: - snippet = Snippet.objects.get(pk=pk) + snippet = Snippet.objects.get(id=id) except Snippet.DoesNotExist: return HttpResponse(status=404) @@ -291,7 +291,7 @@ Finally we need to wire these views up. Create the `snippets/urls.py` file: urlpatterns = [ url(r'^snippets/$', views.snippet_list), - url(r'^snippets/(?P[0-9]+)/$', views.snippet_detail), + url(r'^snippets/(?P[0-9]+)/$', views.snippet_detail), ] We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet app's URLs. diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 5c020a1f7..4aa0062a3 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -66,12 +66,12 @@ Our instance view is an improvement over the previous example. It's a little mo Here is the view for an individual snippet, in the `views.py` module. @api_view(['GET', 'PUT', 'DELETE']) - def snippet_detail(request, pk): + def snippet_detail(request, id): """ Retrieve, update or delete a snippet instance. """ try: - snippet = Snippet.objects.get(pk=pk) + snippet = Snippet.objects.get(id=id) except Snippet.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) @@ -104,7 +104,7 @@ Start by adding a `format` keyword argument to both of the views, like so. and - def snippet_detail(request, pk, format=None): + def snippet_detail(request, id, format=None): Now update the `urls.py` file slightly, to append a set of `format_suffix_patterns` in addition to the existing URLs. @@ -114,7 +114,7 @@ Now update the `urls.py` file slightly, to append a set of `format_suffix_patter urlpatterns = [ url(r'^snippets/$', views.snippet_list), - url(r'^snippets/(?P[0-9]+)$', views.snippet_detail), + url(r'^snippets/(?P[0-9]+)$', views.snippet_detail), ] urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index f018666f5..6303994cd 100644 --- a/docs/tutorial/3-class-based-views.md +++ b/docs/tutorial/3-class-based-views.md @@ -36,27 +36,27 @@ So far, so good. It looks pretty similar to the previous case, but we've got be """ Retrieve, update or delete a snippet instance. """ - def get_object(self, pk): + def get_object(self, id): try: - return Snippet.objects.get(pk=pk) + return Snippet.objects.get(id=id) except Snippet.DoesNotExist: raise Http404 - def get(self, request, pk, format=None): - snippet = self.get_object(pk) + def get(self, request, id, format=None): + snippet = self.get_object(id) serializer = SnippetSerializer(snippet) return Response(serializer.data) - def put(self, request, pk, format=None): - snippet = self.get_object(pk) + def put(self, request, id, format=None): + snippet = self.get_object(id) serializer = SnippetSerializer(snippet, data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def delete(self, request, pk, format=None): - snippet = self.get_object(pk) + def delete(self, request, id, format=None): + snippet = self.get_object(id) snippet.delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -70,7 +70,7 @@ We'll also need to refactor our `urls.py` slightly now we're using class-based v urlpatterns = [ url(r'^snippets/$', views.SnippetList.as_view()), - url(r'^snippets/(?P[0-9]+)/$', views.SnippetDetail.as_view()), + url(r'^snippets/(?P[0-9]+)/$', views.SnippetDetail.as_view()), ] urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index d69c38552..958f9d3f0 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -88,7 +88,7 @@ Make sure to also import the `UserSerializer` class Finally we need to add those views into the API, by referencing them from the URL conf. Add the following to the patterns in `urls.py`. url(r'^users/$', views.UserList.as_view()), - url(r'^users/(?P[0-9]+)/$', views.UserDetail.as_view()), + url(r'^users/(?P[0-9]+)/$', views.UserDetail.as_view()), ## Associating Snippets with Users @@ -150,7 +150,7 @@ The `r'^api-auth/'` part of pattern can actually be whatever URL you want to use Now if you open up the browser again and refresh the page you'll see a 'Login' link in the top right of the page. If you log in as one of the users you created earlier, you'll be able to create code snippets again. -Once you've created a few code snippets, navigate to the '/users/' endpoint, and notice that the representation includes a list of the snippet pks that are associated with each user, in each user's 'snippets' field. +Once you've created a few code snippets, navigate to the '/users/' endpoint, and notice that the representation includes a list of the snippet ids that are associated with each user, in each user's 'snippets' field. ## Object level permissions diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index 8cda09c62..9fb6c53e0 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -48,7 +48,7 @@ We'll add a url pattern for our new API root in `snippets/urls.py`: And then add a url pattern for the snippet highlights: - url(r'^snippets/(?P[0-9]+)/highlight/$', views.SnippetHighlight.as_view()), + url(r'^snippets/(?P[0-9]+)/highlight/$', views.SnippetHighlight.as_view()), ## Hyperlinking our API @@ -67,7 +67,7 @@ In this case we'd like to use a hyperlinked style between entities. In order to The `HyperlinkedModelSerializer` has the following differences from `ModelSerializer`: -* It does not include the `pk` field by default. +* It does not include the `id` field by default. * It includes a `url` field, using `HyperlinkedIdentityField`. * Relationships use `HyperlinkedRelatedField`, instead of `PrimaryKeyRelatedField`. @@ -80,7 +80,7 @@ We can easily re-write our existing serializers to use hyperlinking. In your `sn class Meta: model = Snippet - fields = ('url', 'pk', 'highlight', 'owner', + fields = ('url', 'id', 'highlight', 'owner', 'title', 'code', 'linenos', 'language', 'style') @@ -89,7 +89,7 @@ We can easily re-write our existing serializers to use hyperlinking. In your `sn class Meta: model = User - fields = ('url', 'pk', 'username', 'snippets') + fields = ('url', 'id', 'username', 'snippets') Notice that we've also added a new `'highlight'` field. This field is of the same type as the `url` field, except that it points to the `'snippet-highlight'` url pattern, instead of the `'snippet-detail'` url pattern. @@ -116,16 +116,16 @@ After adding all those names into our URLconf, our final `snippets/urls.py` file url(r'^snippets/$', views.SnippetList.as_view(), name='snippet-list'), - url(r'^snippets/(?P[0-9]+)/$', + url(r'^snippets/(?P[0-9]+)/$', views.SnippetDetail.as_view(), name='snippet-detail'), - url(r'^snippets/(?P[0-9]+)/highlight/$', + url(r'^snippets/(?P[0-9]+)/highlight/$', views.SnippetHighlight.as_view(), name='snippet-highlight'), url(r'^users/$', views.UserList.as_view(), name='user-list'), - url(r'^users/(?P[0-9]+)/$', + url(r'^users/(?P[0-9]+)/$', views.UserDetail.as_view(), name='user-detail') ]) diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index 00152cc17..6e1321093 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -92,10 +92,10 @@ Now that we've bound our resources into concrete views, we can register the view urlpatterns = format_suffix_patterns([ url(r'^$', api_root), url(r'^snippets/$', snippet_list, name='snippet-list'), - url(r'^snippets/(?P[0-9]+)/$', snippet_detail, name='snippet-detail'), - url(r'^snippets/(?P[0-9]+)/highlight/$', snippet_highlight, name='snippet-highlight'), + url(r'^snippets/(?P[0-9]+)/$', snippet_detail, name='snippet-detail'), + url(r'^snippets/(?P[0-9]+)/highlight/$', snippet_highlight, name='snippet-highlight'), url(r'^users/$', user_list, name='user-list'), - url(r'^users/(?P[0-9]+)/$', user_detail, name='user-detail') + url(r'^users/(?P[0-9]+)/$', user_detail, name='user-detail') ]) ## Using Routers diff --git a/docs/tutorial/7-schemas-and-client-libraries.md b/docs/tutorial/7-schemas-and-client-libraries.md index 77cfdd3b3..705b79da6 100644 --- a/docs/tutorial/7-schemas-and-client-libraries.md +++ b/docs/tutorial/7-schemas-and-client-libraries.md @@ -33,10 +33,17 @@ API schema. $ pip install coreapi -We can now include a schema for our API, by adding a `schema_title` argument to -the router instantiation. +We can now include a schema for our API, by including an autogenerated schema +view in our URL configuration. - router = DefaultRouter(schema_title='Pastebin API') + from rest_framework.schemas import get_schema_view + + schema_view = get_schema_view(title='Pastebin API') + + urlpatterns = [ + url('^schema/$', schema_view), + ... + ] If you visit the API root endpoint in a browser you should now see `corejson` representation become available as an option. @@ -46,7 +53,7 @@ representation become available as an option. We can also request the schema from the command line, by specifying the desired content type in the `Accept` header. - $ http http://127.0.0.1:8000/ Accept:application/vnd.coreapi+json + $ http http://127.0.0.1:8000/schema/ Accept:application/vnd.coreapi+json HTTP/1.0 200 OK Allow: GET, HEAD, OPTIONS Content-Type: application/vnd.coreapi+json @@ -91,16 +98,16 @@ Now check that it is available on the command line... First we'll load the API schema using the command line client. - $ coreapi get http://127.0.0.1:8000/ - + $ coreapi get http://127.0.0.1:8000/schema/ + snippets: { - highlight(pk) + highlight(id) list() - retrieve(pk) + read(id) } users: { list() - retrieve(pk) + read(id) } We haven't authenticated yet, so right now we're only able to see the read only @@ -112,7 +119,7 @@ Let's try listing the existing snippets, using the command line client: [ { "url": "http://127.0.0.1:8000/snippets/1/", - "pk": 1, + "id": 1, "highlight": "http://127.0.0.1:8000/snippets/1/highlight/", "owner": "lucy", "title": "Example", @@ -126,7 +133,7 @@ Let's try listing the existing snippets, using the command line client: Some of the API endpoints require named parameters. For example, to get back the highlight HTML for a particular snippet we need to provide an id. - $ coreapi action snippets highlight --param pk=1 + $ coreapi action snippets highlight --param id=1 @@ -150,19 +157,19 @@ Now if we fetch the schema again, we should be able to see the full set of available interactions. $ coreapi reload - Pastebin API "http://127.0.0.1:8000/"> + Pastebin API "http://127.0.0.1:8000/schema/"> snippets: { create(code, [title], [linenos], [language], [style]) - destroy(pk) - highlight(pk) + delete(id) + highlight(id) list() - partial_update(pk, [title], [code], [linenos], [language], [style]) - retrieve(pk) - update(pk, code, [title], [linenos], [language], [style]) + partial_update(id, [title], [code], [linenos], [language], [style]) + read(id) + update(id, code, [title], [linenos], [language], [style]) } users: { list() - retrieve(pk) + read(id) } We're now able to interact with these endpoints. For example, to create a new @@ -171,7 +178,7 @@ snippet: $ coreapi action snippets create --param title="Example" --param code="print('hello, world')" { "url": "http://127.0.0.1:8000/snippets/7/", - "pk": 7, + "id": 7, "highlight": "http://127.0.0.1:8000/snippets/7/highlight/", "owner": "lucy", "title": "Example", @@ -183,7 +190,7 @@ snippet: And to delete a snippet: - $ coreapi action snippets destroy --param pk=7 + $ coreapi action snippets delete --param id=7 As well as the command line client, developers can also interact with your API using client libraries. The Python client library is the first of these diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index 20436e6b4..31f24f4b7 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -1,5 +1,5 @@ # Optional packages which may be used with REST framework. markdown==2.6.4 -django-guardian==1.4.3 -django-filter==0.13.0 -coreapi==1.32.0 +django-guardian==1.4.6 +django-filter==0.14.0 +coreapi==2.0.8 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 457af6c88..68e96703f 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.4.7' +__version__ = '3.5.0' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2016 Tom Christie' diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py index df0c48b86..90d3bd96e 100644 --- a/rest_framework/authtoken/serializers.py +++ b/rest_framework/authtoken/serializers.py @@ -16,6 +16,9 @@ class AuthTokenSerializer(serializers.Serializer): user = authenticate(username=username, password=password) if user: + # From Django 1.10 onwards the `authenticate` call simply + # returns `None` for is_active=False users. + # (Assuming the default `ModelBackend` authentication backend.) if not user.is_active: msg = _('User account is disabled.') raise serializers.ValidationError(msg) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index cee430a84..7ec39ba63 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -23,6 +23,16 @@ except ImportError: from django.utils import importlib # Will be removed in Django 1.9 +try: + from django.urls import ( + NoReverseMatch, RegexURLPattern, RegexURLResolver, ResolverMatch, Resolver404, get_script_prefix, reverse, reverse_lazy, resolve + ) +except ImportError: + from django.core.urlresolvers import ( # Will be removed in Django 2.0 + NoReverseMatch, RegexURLPattern, RegexURLResolver, ResolverMatch, Resolver404, get_script_prefix, reverse, reverse_lazy, resolve + ) + + try: import urlparse # Python 2.x except ImportError: @@ -128,6 +138,12 @@ def is_authenticated(user): return user.is_authenticated +def is_anonymous(user): + if django.VERSION < (1, 10): + return user.is_anonymous() + return user.is_anonymous + + def get_related_model(field): if django.VERSION < (1, 9): return _resolve_model(field.rel.to) @@ -178,6 +194,13 @@ except (ImportError, SyntaxError): uritemplate = None +# requests is optional +try: + import requests +except ImportError: + requests = None + + # Django-guardian is optional. Import only if guardian is in INSTALLED_APPS # Fixes (#1712). We keep the try/except for the test suite. guardian = None @@ -200,8 +223,13 @@ try: if markdown.version <= '2.2': HEADERID_EXT_PATH = 'headerid' - else: + LEVEL_PARAM = 'level' + elif markdown.version < '2.6': HEADERID_EXT_PATH = 'markdown.extensions.headerid' + LEVEL_PARAM = 'level' + else: + HEADERID_EXT_PATH = 'markdown.extensions.toc' + LEVEL_PARAM = 'baselevel' def apply_markdown(text): """ @@ -211,7 +239,7 @@ try: extensions = [HEADERID_EXT_PATH] extension_configs = { HEADERID_EXT_PATH: { - 'level': '2' + LEVEL_PARAM: '2' } } md = markdown.Markdown( @@ -277,3 +305,11 @@ def template_render(template, context=None, request=None): # backends template, e.g. django.template.backends.django.Template else: return template.render(context, request=request) + + +def set_many(instance, field, value): + if django.VERSION < (1, 10): + setattr(instance, field, value) + else: + field = getattr(instance, field) + field.set(value) diff --git a/rest_framework/decorators.py b/rest_framework/decorators.py index 554e5236c..bf9b32aaa 100644 --- a/rest_framework/decorators.py +++ b/rest_framework/decorators.py @@ -15,7 +15,7 @@ from django.utils import six from rest_framework.views import APIView -def api_view(http_method_names=None): +def api_view(http_method_names=None, exclude_from_schema=False): """ Decorator that converts a function-based view into an APIView subclass. Takes a list of allowed methods for the view as an argument. @@ -72,6 +72,7 @@ def api_view(http_method_names=None): WrappedAPIView.permission_classes = getattr(func, 'permission_classes', APIView.permission_classes) + WrappedAPIView.exclude_from_schema = exclude_from_schema return WrappedAPIView.as_view() return decorator diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 917a151e5..7f8391b8a 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -49,20 +49,34 @@ class empty: pass -def is_simple_callable(obj): - """ - True if the object is a callable that takes no arguments. - """ - function = inspect.isfunction(obj) - method = inspect.ismethod(obj) +if six.PY3: + def is_simple_callable(obj): + """ + True if the object is a callable that takes no arguments. + """ + if not callable(obj): + return False - if not (function or method): - return False + sig = inspect.signature(obj) + params = sig.parameters.values() + return all(param.default != param.empty for param in params) - args, _, _, defaults = inspect.getargspec(obj) - len_args = len(args) if function else len(args) - 1 - len_defaults = len(defaults) if defaults else 0 - return len_args <= len_defaults +else: + def is_simple_callable(obj): + function = inspect.isfunction(obj) + method = inspect.ismethod(obj) + + if not (function or method): + return False + + if method: + is_unbound = obj.im_self is None + + args, _, _, defaults = inspect.getargspec(obj) + + len_args = len(args) if function or is_unbound else len(args) - 1 + len_defaults = len(defaults) if defaults else 0 + return len_args <= len_defaults def get_attribute(instance, attrs): diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 0050ee7cd..65233978e 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -16,7 +16,7 @@ from django.utils import six from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import ( - crispy_forms, distinct, django_filters, guardian, template_render + coreapi, crispy_forms, distinct, django_filters, guardian, template_render ) from rest_framework.settings import api_settings @@ -72,7 +72,8 @@ class BaseFilterBackend(object): """ raise NotImplementedError(".filter_queryset() must be overridden.") - def get_fields(self, view): + def get_schema_fields(self, view): + assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' return [] @@ -131,14 +132,21 @@ class DjangoFilterBackend(BaseFilterBackend): template = loader.get_template(self.template) return template_render(template, context) - def get_fields(self, view): + def get_schema_fields(self, view): + assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' filter_class = getattr(view, 'filter_class', None) if filter_class: - return list(filter_class().filters.keys()) + return [ + coreapi.Field(name=field_name, required=False, location='query') + for field_name in filter_class().filters.keys() + ] filter_fields = getattr(view, 'filter_fields', None) if filter_fields: - return filter_fields + return [ + coreapi.Field(name=field_name, required=False, location='query') + for field_name in filter_fields + ] return [] @@ -231,8 +239,9 @@ class SearchFilter(BaseFilterBackend): template = loader.get_template(self.template) return template_render(template, context) - def get_fields(self, view): - return [self.search_param] + def get_schema_fields(self, view): + assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + return [coreapi.Field(name=self.search_param, required=False, location='query')] class OrderingFilter(BaseFilterBackend): @@ -348,8 +357,9 @@ class OrderingFilter(BaseFilterBackend): context = self.get_template_context(request, queryset, view) return template_render(template, context) - def get_fields(self, view): - return [self.ordering_param] + def get_schema_fields(self, view): + assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + return [coreapi.Field(name=self.ordering_param, required=False, location='query')] class DjangoObjectPermissionsFilter(BaseFilterBackend): diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index c54894efc..6b539b900 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -15,7 +15,7 @@ from django.utils import six from django.utils.six.moves.urllib import parse as urlparse from django.utils.translation import ugettext_lazy as _ -from rest_framework.compat import template_render +from rest_framework.compat import coreapi, template_render from rest_framework.exceptions import NotFound from rest_framework.response import Response from rest_framework.settings import api_settings @@ -157,7 +157,8 @@ class BasePagination(object): def get_results(self, data): return data['results'] - def get_fields(self, view): + def get_schema_fields(self, view): + assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' return [] @@ -283,10 +284,16 @@ class PageNumberPagination(BasePagination): context = self.get_html_context() return template_render(template, context) - def get_fields(self, view): - if self.page_size_query_param is None: - return [self.page_query_param] - return [self.page_query_param, self.page_size_query_param] + def get_schema_fields(self, view): + assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + fields = [ + coreapi.Field(name=self.page_query_param, required=False, location='query') + ] + if self.page_size_query_param is not None: + fields.append([ + coreapi.Field(name=self.page_size_query_param, required=False, location='query') + ]) + return fields class LimitOffsetPagination(BasePagination): @@ -415,8 +422,12 @@ class LimitOffsetPagination(BasePagination): context = self.get_html_context() return template_render(template, context) - def get_fields(self, view): - return [self.limit_query_param, self.offset_query_param] + def get_schema_fields(self, view): + assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + return [ + coreapi.Field(name=self.limit_query_param, required=False, location='query'), + coreapi.Field(name=self.offset_query_param, required=False, location='query') + ] class CursorPagination(BasePagination): @@ -721,5 +732,8 @@ class CursorPagination(BasePagination): context = self.get_html_context() return template_render(template, context) - def get_fields(self, view): - return [self.cursor_query_param] + def get_schema_fields(self, view): + assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' + return [ + coreapi.Field(name=self.cursor_query_param, required=False, location='query') + ] diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 65c4c0318..c6eb92a24 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -4,9 +4,6 @@ from __future__ import unicode_literals from collections import OrderedDict from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist -from django.core.urlresolvers import ( - NoReverseMatch, Resolver404, get_script_prefix, resolve -) from django.db.models import Manager from django.db.models.query import QuerySet from django.utils import six @@ -14,10 +11,14 @@ from django.utils.encoding import python_2_unicode_compatible, smart_text from django.utils.six.moves.urllib import parse as urlparse from django.utils.translation import ugettext_lazy as _ +from rest_framework.compat import ( + NoReverseMatch, Resolver404, get_script_prefix, resolve +) from rest_framework.fields import ( Field, empty, get_attribute, is_simple_callable, iter_options ) from rest_framework.reverse import reverse +from rest_framework.settings import api_settings from rest_framework.utils import html @@ -71,14 +72,19 @@ MANY_RELATION_KWARGS = ( class RelatedField(Field): queryset = None - html_cutoff = 1000 - html_cutoff_text = _('More than {count} items...') + html_cutoff = None + html_cutoff_text = None def __init__(self, **kwargs): self.queryset = kwargs.pop('queryset', self.queryset) - self.html_cutoff = kwargs.pop('html_cutoff', self.html_cutoff) - self.html_cutoff_text = kwargs.pop('html_cutoff_text', self.html_cutoff_text) - + self.html_cutoff = kwargs.pop( + 'html_cutoff', + self.html_cutoff or int(api_settings.HTML_SELECT_CUTOFF) + ) + self.html_cutoff_text = kwargs.pop( + 'html_cutoff_text', + self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT) + ) if not method_overridden('get_queryset', RelatedField, self): assert self.queryset is not None or kwargs.get('read_only', None), ( 'Relational field must provide a `queryset` argument, ' @@ -447,15 +453,20 @@ class ManyRelatedField(Field): 'not_a_list': _('Expected a list of items but got type "{input_type}".'), 'empty': _('This list may not be empty.') } - html_cutoff = 1000 - html_cutoff_text = _('More than {count} items...') + html_cutoff = None + html_cutoff_text = None def __init__(self, child_relation=None, *args, **kwargs): self.child_relation = child_relation self.allow_empty = kwargs.pop('allow_empty', True) - self.html_cutoff = kwargs.pop('html_cutoff', self.html_cutoff) - self.html_cutoff_text = kwargs.pop('html_cutoff_text', self.html_cutoff_text) - + self.html_cutoff = kwargs.pop( + 'html_cutoff', + self.html_cutoff or int(api_settings.HTML_SELECT_CUTOFF) + ) + self.html_cutoff_text = kwargs.pop( + 'html_cutoff_text', + self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT) + ) assert child_relation is not None, '`child_relation` is a required argument.' super(ManyRelatedField, self).__init__(*args, **kwargs) self.child_relation.bind(field_name='', parent=self) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 11e9fb960..97984daf9 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -276,6 +276,10 @@ class HTMLFormRenderer(BaseRenderer): 'base_template': 'input.html', 'input_type': 'number' }, + serializers.FloatField: { + 'base_template': 'input.html', + 'input_type': 'number' + }, serializers.DateTimeField: { 'base_template': 'input.html', 'input_type': 'datetime-local' @@ -809,7 +813,7 @@ class MultiPartRenderer(BaseRenderer): class CoreJSONRenderer(BaseRenderer): - media_type = 'application/vnd.coreapi+json' + media_type = 'application/coreapi+json' charset = None format = 'corejson' diff --git a/rest_framework/reverse.py b/rest_framework/reverse.py index 5a7ba09a8..fd418dcca 100644 --- a/rest_framework/reverse.py +++ b/rest_framework/reverse.py @@ -3,11 +3,11 @@ Provide urlresolver functions that return fully qualified URLs or view names """ from __future__ import unicode_literals -from django.core.urlresolvers import reverse as django_reverse -from django.core.urlresolvers import NoReverseMatch from django.utils import six from django.utils.functional import lazy +from rest_framework.compat import reverse as django_reverse +from rest_framework.compat import NoReverseMatch from rest_framework.settings import api_settings from rest_framework.utils.urls import replace_query_param @@ -54,7 +54,7 @@ def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra def _reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra): """ - Same as `django.core.urlresolvers.reverse`, but optionally takes a request + Same as `django.urls.reverse`, but optionally takes a request and returns a fully qualified URL, using the request to get the base URL. """ if format is not None: diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 4eec70bda..7a2d981a3 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -16,13 +16,15 @@ For example, you might have a `urls.py` that looks something like this: from __future__ import unicode_literals import itertools +import warnings from collections import OrderedDict, namedtuple from django.conf.urls import url from django.core.exceptions import ImproperlyConfigured -from django.core.urlresolvers import NoReverseMatch from rest_framework import exceptions, renderers, views +from rest_framework.compat import NoReverseMatch +from rest_framework.renderers import BrowsableAPIRenderer from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.schemas import SchemaGenerator @@ -83,6 +85,7 @@ class BaseRouter(object): class SimpleRouter(BaseRouter): + routes = [ # List route. Route( @@ -258,6 +261,13 @@ class SimpleRouter(BaseRouter): trailing_slash=self.trailing_slash ) + # If there is no prefix, the first part of the url is probably + # controlled by project's urls.py and the router is in an app, + # so a slash in the beginning will (A) cause Django to give + # warnings and (B) generate URLS that will require using '//'. + if not prefix and regex[:2] == '^/': + regex = '^' + regex[2:] + view = viewset.as_view(mapping, **route.initkwargs) name = route.name.format(basename=basename) ret.append(url(regex, view, name=name)) @@ -273,9 +283,15 @@ class DefaultRouter(SimpleRouter): include_root_view = True include_format_suffixes = True root_view_name = 'api-root' - default_schema_renderers = [renderers.CoreJSONRenderer] + default_schema_renderers = [renderers.CoreJSONRenderer, BrowsableAPIRenderer] def __init__(self, *args, **kwargs): + if 'schema_title' in kwargs: + warnings.warn( + "Including a schema directly via a router is now pending " + "deprecation. Use `get_schema_view()` instead.", + PendingDeprecationWarning + ) if 'schema_renderers' in kwargs: assert 'schema_title' in kwargs, 'Missing "schema_title" argument.' if 'schema_url' in kwargs: @@ -289,42 +305,44 @@ class DefaultRouter(SimpleRouter): self.root_renderers = list(api_settings.DEFAULT_RENDERER_CLASSES) super(DefaultRouter, self).__init__(*args, **kwargs) + def get_schema_root_view(self, api_urls=None): + """ + Return a schema root view. + """ + schema_renderers = self.schema_renderers + schema_generator = SchemaGenerator( + title=self.schema_title, + url=self.schema_url, + patterns=api_urls + ) + + class APISchemaView(views.APIView): + _ignore_model_permissions = True + exclude_from_schema = True + renderer_classes = schema_renderers + + def get(self, request, *args, **kwargs): + schema = schema_generator.get_schema(request) + if schema is None: + raise exceptions.PermissionDenied() + return Response(schema) + + return APISchemaView.as_view() + def get_api_root_view(self, api_urls=None): """ - Return a view to use as the API root. + Return a basic root view. """ api_root_dict = OrderedDict() list_name = self.routes[0].name for prefix, viewset, basename in self.registry: api_root_dict[prefix] = list_name.format(basename=basename) - view_renderers = list(self.root_renderers) - schema_media_types = [] - - if api_urls and self.schema_title: - view_renderers += list(self.schema_renderers) - schema_generator = SchemaGenerator( - title=self.schema_title, - url=self.schema_url, - patterns=api_urls - ) - schema_media_types = [ - renderer.media_type - for renderer in self.schema_renderers - ] - - class APIRoot(views.APIView): + class APIRootView(views.APIView): _ignore_model_permissions = True - renderer_classes = view_renderers + exclude_from_schema = True def get(self, request, *args, **kwargs): - if request.accepted_renderer.media_type in schema_media_types: - # Return a schema response. - schema = schema_generator.get_schema(request) - if schema is None: - raise exceptions.PermissionDenied() - return Response(schema) - # Return a plain {"name": "hyperlink"} response. ret = OrderedDict() namespace = request.resolver_match.namespace @@ -345,7 +363,7 @@ class DefaultRouter(SimpleRouter): return Response(ret) - return APIRoot.as_view() + return APIRootView.as_view() def get_urls(self): """ @@ -355,7 +373,10 @@ class DefaultRouter(SimpleRouter): urls = super(DefaultRouter, self).get_urls() if self.include_root_view: - view = self.get_api_root_view(api_urls=urls) + if self.schema_title: + view = self.get_schema_root_view(api_urls=urls) + else: + view = self.get_api_root_view(api_urls=urls) root_url = url(r'^$', view, name=self.root_view_name) urls.append(root_url) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 1b899450f..69698db0e 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -1,26 +1,45 @@ +import os +import re +from collections import OrderedDict from importlib import import_module from django.conf import settings from django.contrib.admindocs.views import simplify_regex -from django.core.urlresolvers import RegexURLPattern, RegexURLResolver from django.utils import six -from django.utils.encoding import force_text +from django.utils.encoding import force_text, smart_text -from rest_framework import exceptions, serializers -from rest_framework.compat import coreapi, uritemplate, urlparse +from rest_framework import exceptions, renderers, serializers +from rest_framework.compat import ( + RegexURLPattern, RegexURLResolver, coreapi, uritemplate, urlparse +) from rest_framework.request import clone_request +from rest_framework.response import Response +from rest_framework.settings import api_settings +from rest_framework.utils import formatting +from rest_framework.utils.field_mapping import ClassLookupDict +from rest_framework.utils.model_meta import _get_pk from rest_framework.views import APIView -def as_query_fields(items): - """ - Take a list of Fields and plain strings. - Convert any pain strings into `location='query'` Field instances. - """ - return [ - item if isinstance(item, coreapi.Field) else coreapi.Field(name=item, required=False, location='query') - for item in items - ] +header_regex = re.compile('^[a-zA-Z][0-9A-Za-z_]*:') + +types_lookup = ClassLookupDict({ + serializers.Field: 'string', + serializers.IntegerField: 'integer', + serializers.FloatField: 'number', + serializers.DecimalField: 'number', + serializers.BooleanField: 'boolean', + serializers.FileField: 'file', + serializers.MultipleChoiceField: 'array', + serializers.ManyRelatedField: 'array', + serializers.Serializer: 'object', + serializers.ListSerializer: 'array' +}) + + +def get_pk_name(model): + meta = model._meta.concrete_model._meta + return _get_pk(meta).name def is_api_view(callback): @@ -31,106 +50,92 @@ def is_api_view(callback): return (cls is not None) and issubclass(cls, APIView) -class SchemaGenerator(object): - default_mapping = { - 'get': 'read', - 'post': 'create', - 'put': 'update', - 'patch': 'partial_update', - 'delete': 'destroy', - } - known_actions = ( - 'create', 'read', 'retrieve', 'list', - 'update', 'partial_update', 'destroy' - ) +def insert_into(target, keys, value): + """ + Nested dictionary insertion. - def __init__(self, title=None, url=None, patterns=None, urlconf=None): - assert coreapi, '`coreapi` must be installed for schema support.' + >>> example = {} + >>> insert_into(example, ['a', 'b', 'c'], 123) + >>> example + {'a': {'b': {'c': 123}}} + """ + for key in keys[:-1]: + if key not in target: + target[key] = {} + target = target[key] + target[keys[-1]] = value - if patterns is None and urlconf is not None: + +def is_custom_action(action): + return action not in set([ + 'retrieve', 'list', 'create', 'update', 'partial_update', 'destroy' + ]) + + +def is_list_view(path, method, view): + """ + Return True if the given path/method appears to represent a list view. + """ + if hasattr(view, 'action'): + # Viewsets have an explicitly defined action, which we can inspect. + return view.action == 'list' + + if method.lower() != 'get': + return False + path_components = path.strip('/').split('/') + if path_components and '{' in path_components[-1]: + return False + return True + + +def endpoint_ordering(endpoint): + path, method, callback = endpoint + method_priority = { + 'GET': 0, + 'POST': 1, + 'PUT': 2, + 'PATCH': 3, + 'DELETE': 4 + }.get(method, 5) + return (path, method_priority) + + +class EndpointInspector(object): + """ + A class to determine the available API endpoints that a project exposes. + """ + def __init__(self, patterns=None, urlconf=None): + if patterns is None: + if urlconf is None: + # Use the default Django URL conf + urlconf = settings.ROOT_URLCONF + + # Load the given URLconf module if isinstance(urlconf, six.string_types): urls = import_module(urlconf) else: urls = urlconf - self.patterns = urls.urlpatterns - elif patterns is None and urlconf is None: - urls = import_module(settings.ROOT_URLCONF) - self.patterns = urls.urlpatterns - else: - self.patterns = patterns + patterns = urls.urlpatterns - if url and not url.endswith('/'): - url += '/' + self.patterns = patterns - self.title = title - self.url = url - self.endpoints = None - - def get_schema(self, request=None): - if self.endpoints is None: - self.endpoints = self.get_api_endpoints(self.patterns) - - links = [] - for path, method, category, action, callback in self.endpoints: - view = callback.cls() - for attr, val in getattr(callback, 'initkwargs', {}).items(): - setattr(view, attr, val) - view.args = () - view.kwargs = {} - view.format_kwarg = None - - actions = getattr(callback, 'actions', None) - if actions is not None: - if method == 'OPTIONS': - view.action = 'metadata' - else: - view.action = actions.get(method.lower()) - - if request is not None: - view.request = clone_request(request, method) - try: - view.check_permissions(view.request) - except exceptions.APIException: - continue - else: - view.request = None - - link = self.get_link(path, method, callback, view) - links.append((category, action, link)) - - if not links: - return None - - # Generate the schema content structure, eg: - # {'users': {'list': Link()}} - content = {} - for category, action, link in links: - if category is None: - content[action] = link - elif category in content: - content[category][action] = link - else: - content[category] = {action: link} - - # Return the schema document. - return coreapi.Document(title=self.title, content=content, url=self.url) - - def get_api_endpoints(self, patterns, prefix=''): + def get_api_endpoints(self, patterns=None, prefix=''): """ Return a list of all available API endpoints by inspecting the URL conf. """ + if patterns is None: + patterns = self.patterns + api_endpoints = [] for pattern in patterns: path_regex = prefix + pattern.regex.pattern if isinstance(pattern, RegexURLPattern): - path = self.get_path(path_regex) + path = self.get_path_from_regex(path_regex) callback = pattern.callback if self.should_include_endpoint(path, callback): for method in self.get_allowed_methods(callback): - action = self.get_action(path, method, callback) - category = self.get_category(path, method, callback, action) - endpoint = (path, method, category, action, callback) + endpoint = (path, method, callback) api_endpoints.append(endpoint) elif isinstance(pattern, RegexURLResolver): @@ -140,9 +145,11 @@ class SchemaGenerator(object): ) api_endpoints.extend(nested_endpoints) + api_endpoints = sorted(api_endpoints, key=endpoint_ordering) + return api_endpoints - def get_path(self, path_regex): + def get_path_from_regex(self, path_regex): """ Given a URL conf regex, return a URI template string. """ @@ -160,9 +167,6 @@ class SchemaGenerator(object): if path.endswith('.{format}') or path.endswith('.{format}/'): return False # Ignore .json style URLs. - if path == '/': - return False # Ignore the root endpoint. - return True def get_allowed_methods(self, callback): @@ -177,60 +181,190 @@ class SchemaGenerator(object): callback.cls().allowed_methods if method not in ('OPTIONS', 'HEAD') ] - def get_action(self, path, method, callback): - """ - Return a descriptive action string for the endpoint, eg. 'list'. - """ - actions = getattr(callback, 'actions', self.default_mapping) - return actions[method.lower()] - def get_category(self, path, method, callback, action): - """ - Return a descriptive category string for the endpoint, eg. 'users'. +class SchemaGenerator(object): + # Map HTTP methods onto actions. + default_mapping = { + 'get': 'retrieve', + 'post': 'create', + 'put': 'update', + 'patch': 'partial_update', + 'delete': 'destroy', + } + endpoint_inspector_cls = EndpointInspector - Examples of category/action pairs that should be generated for various - endpoints: + # Map the method names we use for viewset actions onto external schema names. + # These give us names that are more suitable for the external representation. + # Set by 'SCHEMA_COERCE_METHOD_NAMES'. + coerce_method_names = None - /users/ [users][list], [users][create] - /users/{pk}/ [users][read], [users][update], [users][destroy] - /users/enabled/ [users][enabled] (custom action) - /users/{pk}/star/ [users][star] (custom action) - /users/{pk}/groups/ [groups][list], [groups][create] - /users/{pk}/groups/{pk}/ [groups][read], [groups][update], [groups][destroy] + # 'pk' isn't great as an externally exposed name for an identifier, + # so by default we prefer to use the actual model field name for schemas. + # Set by 'SCHEMA_COERCE_PATH_PK'. + coerce_path_pk = None + + def __init__(self, title=None, url=None, patterns=None, urlconf=None): + assert coreapi, '`coreapi` must be installed for schema support.' + + if url and not url.endswith('/'): + url += '/' + + self.coerce_method_names = api_settings.SCHEMA_COERCE_METHOD_NAMES + self.coerce_path_pk = api_settings.SCHEMA_COERCE_PATH_PK + + self.patterns = patterns + self.urlconf = urlconf + self.title = title + self.url = url + self.endpoints = None + + def get_schema(self, request=None): """ - path_components = path.strip('/').split('/') - path_components = [ - component for component in path_components - if '{' not in component - ] - if action in self.known_actions: - # Default action, eg "/users/", "/users/{pk}/" - idx = -1 - else: - # Custom action, eg "/users/{pk}/activate/", "/users/active/" - idx = -2 + Generate a `coreapi.Document` representing the API schema. + """ + if self.endpoints is None: + inspector = self.endpoint_inspector_cls(self.patterns, self.urlconf) + self.endpoints = inspector.get_api_endpoints() + + links = self.get_links(request) + if not links: + return None + return coreapi.Document(title=self.title, url=self.url, content=links) + + def get_links(self, request=None): + """ + Return a dictionary containing all the links that should be + included in the API schema. + """ + links = OrderedDict() + + # Generate (path, method, view) given (path, method, callback). + paths = [] + view_endpoints = [] + for path, method, callback in self.endpoints: + view = self.create_view(callback, method, request) + if getattr(view, 'exclude_from_schema', False): + continue + path = self.coerce_path(path, method, view) + paths.append(path) + view_endpoints.append((path, method, view)) + + # Only generate the path prefix for paths that will be included + prefix = self.determine_path_prefix(paths) + + for path, method, view in view_endpoints: + if not self.has_view_permissions(path, method, view): + continue + link = self.get_link(path, method, view) + subpath = path[len(prefix):] + keys = self.get_keys(subpath, method, view) + insert_into(links, keys, link) + return links + + # Methods used when we generate a view instance from the raw callback... + + def determine_path_prefix(self, paths): + """ + Given a list of all paths, return the common prefix which should be + discounted when generating a schema structure. + + This will be the longest common string that does not include that last + component of the URL, or the last component before a path parameter. + + For example: + + /api/v1/users/ + /api/v1/users/{pk}/ + + The path prefix is '/api/v1/' + """ + prefixes = [] + for path in paths: + components = path.strip('/').split('/') + initial_components = [] + for component in components: + if '{' in component: + break + initial_components.append(component) + prefix = '/'.join(initial_components[:-1]) + if not prefix: + # We can just break early in the case that there's at least + # one URL that doesn't have a path prefix. + return '/' + prefixes.append('/' + prefix + '/') + return os.path.commonprefix(prefixes) + + def create_view(self, callback, method, request=None): + """ + Given a callback, return an actual view instance. + """ + view = callback.cls() + for attr, val in getattr(callback, 'initkwargs', {}).items(): + setattr(view, attr, val) + view.args = () + view.kwargs = {} + view.format_kwarg = None + view.request = None + view.action_map = getattr(callback, 'actions', None) + + actions = getattr(callback, 'actions', None) + if actions is not None: + if method == 'OPTIONS': + view.action = 'metadata' + else: + view.action = actions.get(method.lower()) + + if request is not None: + view.request = clone_request(request, method) + + return view + + def has_view_permissions(self, path, method, view): + """ + Return `True` if the incoming request has the correct view permissions. + """ + if view.request is None: + return True try: - return path_components[idx] - except IndexError: - return None + view.check_permissions(view.request) + except exceptions.APIException: + return False + return True + + def coerce_path(self, path, method, view): + """ + Coerce {pk} path arguments into the name of the model field, + where possible. This is cleaner for an external representation. + (Ie. "this is an identifier", not "this is a database primary key") + """ + if not self.coerce_path_pk or '{pk}' not in path: + return path + model = getattr(getattr(view, 'queryset', None), 'model', None) + if model: + field_name = get_pk_name(model) + else: + field_name = 'id' + return path.replace('{pk}', '{%s}' % field_name) # Methods for generating each individual `Link` instance... - def get_link(self, path, method, callback, view): + def get_link(self, path, method, view): """ Return a `coreapi.Link` instance for the given endpoint. """ - fields = self.get_path_fields(path, method, callback, view) - fields += self.get_serializer_fields(path, method, callback, view) - fields += self.get_pagination_fields(path, method, callback, view) - fields += self.get_filter_fields(path, method, callback, view) + fields = self.get_path_fields(path, method, view) + fields += self.get_serializer_fields(path, method, view) + fields += self.get_pagination_fields(path, method, view) + fields += self.get_filter_fields(path, method, view) if fields and any([field.location in ('form', 'body') for field in fields]): - encoding = self.get_encoding(path, method, callback, view) + encoding = self.get_encoding(path, method, view) else: encoding = None + description = self.get_description(path, method, view) + if self.url and path.startswith('/'): path = path[1:] @@ -238,10 +372,44 @@ class SchemaGenerator(object): url=urlparse.urljoin(self.url, path), action=method.lower(), encoding=encoding, - fields=fields + fields=fields, + description=description ) - def get_encoding(self, path, method, callback, view): + def get_description(self, path, method, view): + """ + Determine a link description. + + This will be based on the method docstring if one exists, + or else the class docstring. + """ + method_name = getattr(view, 'action', method.lower()) + method_docstring = getattr(view, method_name, None).__doc__ + if method_docstring: + # An explicit docstring on the method or action. + return formatting.dedent(smart_text(method_docstring)) + + description = view.get_view_description() + lines = [line.strip() for line in description.splitlines()] + current_section = '' + sections = {'': ''} + + for line in lines: + if header_regex.match(line): + current_section, seperator, lead = line.partition(':') + sections[current_section] = lead.strip() + else: + sections[current_section] += line + '\n' + + header = getattr(view, 'action', method.lower()) + if header in sections: + return sections[header].strip() + if header in self.coerce_method_names: + if self.coerce_method_names[header] in sections: + return sections[self.coerce_method_names[header]].strip() + return sections[''].strip() + + def get_encoding(self, path, method, view): """ Return the 'encoding' parameter to use for a given endpoint. """ @@ -262,7 +430,7 @@ class SchemaGenerator(object): return None - def get_path_fields(self, path, method, callback, view): + def get_path_fields(self, path, method, view): """ Return a list of `coreapi.Field` instances corresponding to any templated path variables. @@ -275,7 +443,7 @@ class SchemaGenerator(object): return fields - def get_serializer_fields(self, path, method, callback, view): + def get_serializer_fields(self, path, method, view): """ Return a list of `coreapi.Field` instances corresponding to any request body input, as determined by the serializer class. @@ -289,7 +457,14 @@ class SchemaGenerator(object): serializer = view.get_serializer() if isinstance(serializer, serializers.ListSerializer): - return [coreapi.Field(name='data', location='body', required=True)] + return [ + coreapi.Field( + name='data', + location='body', + required=True, + type='array' + ) + ] if not isinstance(serializer, serializers.Serializer): return [] @@ -305,36 +480,104 @@ class SchemaGenerator(object): name=field.source, location='form', required=required, - description=description + description=description, + type=types_lookup[field] ) fields.append(field) return fields - def get_pagination_fields(self, path, method, callback, view): - if method != 'GET': - return [] - - if hasattr(callback, 'actions') and ('list' not in callback.actions.values()): + def get_pagination_fields(self, path, method, view): + if not is_list_view(path, method, view): return [] if not getattr(view, 'pagination_class', None): return [] paginator = view.pagination_class() - return as_query_fields(paginator.get_fields(view)) + return paginator.get_schema_fields(view) - def get_filter_fields(self, path, method, callback, view): - if method != 'GET': + def get_filter_fields(self, path, method, view): + if not is_list_view(path, method, view): return [] - if hasattr(callback, 'actions') and ('list' not in callback.actions.values()): - return [] - - if not hasattr(view, 'filter_backends'): + if not getattr(view, 'filter_backends', None): return [] fields = [] for filter_backend in view.filter_backends: - fields += as_query_fields(filter_backend().get_fields(view)) + fields += filter_backend().get_schema_fields(view) return fields + + # Method for generating the link layout.... + + def get_keys(self, subpath, method, view): + """ + Return a list of keys that should be used to layout a link within + the schema document. + + /users/ ("users", "list"), ("users", "create") + /users/{pk}/ ("users", "read"), ("users", "update"), ("users", "delete") + /users/enabled/ ("users", "enabled") # custom viewset list action + /users/{pk}/star/ ("users", "star") # custom viewset detail action + /users/{pk}/groups/ ("users", "groups", "list"), ("users", "groups", "create") + /users/{pk}/groups/{pk}/ ("users", "groups", "read"), ("users", "groups", "update"), ("users", "groups", "delete") + """ + if hasattr(view, 'action'): + # Viewsets have explicitly named actions. + action = view.action + else: + # Views have no associated action, so we determine one from the method. + if is_list_view(subpath, method, view): + action = 'list' + else: + action = self.default_mapping[method.lower()] + + named_path_components = [ + component for component + in subpath.strip('/').split('/') + if '{' not in component + ] + + if is_custom_action(action): + # Custom action, eg "/users/{pk}/activate/", "/users/active/" + if len(view.action_map) > 1: + action = self.default_mapping[method.lower()] + if action in self.coerce_method_names: + action = self.coerce_method_names[action] + return named_path_components + [action] + else: + return named_path_components[:-1] + [action] + + if action in self.coerce_method_names: + action = self.coerce_method_names[action] + + # Default action, eg "/users/", "/users/{pk}/" + return named_path_components + [action] + + +def get_schema_view(title=None, url=None, renderer_classes=None): + """ + Return a schema view. + """ + generator = SchemaGenerator(title=title, url=url) + if renderer_classes is None: + if renderers.BrowsableAPIRenderer in api_settings.DEFAULT_RENDERER_CLASSES: + rclasses = [renderers.CoreJSONRenderer, renderers.BrowsableAPIRenderer] + else: + rclasses = [renderers.CoreJSONRenderer] + else: + rclasses = renderer_classes + + class SchemaView(APIView): + _ignore_model_permissions = True + exclude_from_schema = True + renderer_classes = rclasses + + def get(self, request, *args, **kwargs): + schema = generator.get_schema(request) + if schema is None: + raise exceptions.PermissionDenied() + return Response(schema) + + return SchemaView.as_view() diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 4d1ed63ae..7e99d40b3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -13,7 +13,6 @@ response content is handled by parsers and renderers. from __future__ import unicode_literals import traceback -import warnings from django.db import models from django.db.models import DurationField as ModelDurationField @@ -23,7 +22,7 @@ from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import JSONField as ModelJSONField -from rest_framework.compat import postgres_fields, unicode_to_repr +from rest_framework.compat import postgres_fields, set_many, unicode_to_repr from rest_framework.utils import model_meta from rest_framework.utils.field_mapping import ( ClassLookupDict, get_field_kwargs, get_nested_relation_kwargs, @@ -892,19 +891,23 @@ class ModelSerializer(Serializer): # Save many-to-many relationships after the instance is created. if many_to_many: for field_name, value in many_to_many.items(): - setattr(instance, field_name, value) + set_many(instance, field_name, value) return instance def update(self, instance, validated_data): raise_errors_on_nested_writes('update', self, validated_data) + info = model_meta.get_field_info(instance) # Simply set each attribute on the instance, and then save it. # Note that unlike `.create()` we don't need to treat many-to-many # relationships as being a special case. During updates we already # have an instance pk for the relationships to be associated with. for attr, value in validated_data.items(): - setattr(instance, attr, value) + if attr in info.relations and info.relations[attr].to_many: + set_many(instance, attr, value) + else: + setattr(instance, attr, value) instance.save() return instance @@ -1012,16 +1015,14 @@ class ModelSerializer(Serializer): ) ) - if fields is None and exclude is None: - warnings.warn( - "Creating a ModelSerializer without either the 'fields' " - "attribute or the 'exclude' attribute is deprecated " - "since 3.3.0. Add an explicit fields = '__all__' to the " - "{serializer_class} serializer.".format( - serializer_class=self.__class__.__name__ - ), - DeprecationWarning - ) + assert not (fields is None and exclude is None), ( + "Creating a ModelSerializer without either the 'fields' attribute " + "or the 'exclude' attribute has been deprecated since 3.3.0, " + "and is now disallowed. Add an explicit fields = '__all__' to the " + "{serializer_class} serializer.".format( + serializer_class=self.__class__.__name__ + ), + ) if fields == ALL_FIELDS: fields = None diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 68c7709e8..6d9ed2355 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -111,6 +111,17 @@ DEFAULTS = { 'COMPACT_JSON': True, 'COERCE_DECIMAL_TO_STRING': True, 'UPLOADED_FILES_USE_URL': True, + + # Browseable API + 'HTML_SELECT_CUTOFF': 1000, + 'HTML_SELECT_CUTOFF_TEXT': "More than {count} items...", + + # Schemas + 'SCHEMA_COERCE_PATH_PK': True, + 'SCHEMA_COERCE_METHOD_NAMES': { + 'retrieve': 'read', + 'destroy': 'delete' + }, } diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index 3bb85e472..c1c8a5396 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -3,14 +3,13 @@ from __future__ import absolute_import, unicode_literals import re from django import template -from django.core.urlresolvers import NoReverseMatch, reverse from django.template import loader from django.utils import six from django.utils.encoding import force_text, iri_to_uri from django.utils.html import escape, format_html, smart_urlquote from django.utils.safestring import SafeData, mark_safe -from rest_framework.compat import template_render +from rest_framework.compat import NoReverseMatch, reverse, template_render from rest_framework.renderers import HTMLFormRenderer from rest_framework.utils.urls import replace_query_param diff --git a/rest_framework/test.py b/rest_framework/test.py index fd9f6ab13..16b1b4cd5 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -4,7 +4,11 @@ # to make it harder for the user to import the wrong thing without realizing. from __future__ import unicode_literals +import io + from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.core.handlers.wsgi import WSGIHandler from django.test import testcases from django.test.client import Client as DjangoClient from django.test.client import RequestFactory as DjangoRequestFactory @@ -13,6 +17,7 @@ from django.utils import six from django.utils.encoding import force_bytes from django.utils.http import urlencode +from rest_framework.compat import coreapi, requests from rest_framework.settings import api_settings @@ -21,6 +26,128 @@ def force_authenticate(request, user=None, token=None): request._force_auth_token = token +if requests is not None: + class HeaderDict(requests.packages.urllib3._collections.HTTPHeaderDict): + def get_all(self, key, default): + return self.getheaders(key) + + class MockOriginalResponse(object): + def __init__(self, headers): + self.msg = HeaderDict(headers) + self.closed = False + + def isclosed(self): + return self.closed + + def close(self): + self.closed = True + + class DjangoTestAdapter(requests.adapters.HTTPAdapter): + """ + A transport adapter for `requests`, that makes requests via the + Django WSGI app, rather than making actual HTTP requests over the network. + """ + def __init__(self): + self.app = WSGIHandler() + self.factory = DjangoRequestFactory() + + def get_environ(self, request): + """ + Given a `requests.PreparedRequest` instance, return a WSGI environ dict. + """ + method = request.method + url = request.url + kwargs = {} + + # Set request content, if any exists. + if request.body is not None: + if hasattr(request.body, 'read'): + kwargs['data'] = request.body.read() + else: + kwargs['data'] = request.body + if 'content-type' in request.headers: + kwargs['content_type'] = request.headers['content-type'] + + # Set request headers. + for key, value in request.headers.items(): + key = key.upper() + if key in ('CONNECTION', 'CONTENT-LENGTH', 'CONTENT-TYPE'): + continue + kwargs['HTTP_%s' % key.replace('-', '_')] = value + + return self.factory.generic(method, url, **kwargs).environ + + def send(self, request, *args, **kwargs): + """ + Make an outgoing request to the Django WSGI application. + """ + raw_kwargs = {} + + def start_response(wsgi_status, wsgi_headers): + status, _, reason = wsgi_status.partition(' ') + raw_kwargs['status'] = int(status) + raw_kwargs['reason'] = reason + raw_kwargs['headers'] = wsgi_headers + raw_kwargs['version'] = 11 + raw_kwargs['preload_content'] = False + raw_kwargs['original_response'] = MockOriginalResponse(wsgi_headers) + + # Make the outgoing request via WSGI. + environ = self.get_environ(request) + wsgi_response = self.app(environ, start_response) + + # Build the underlying urllib3.HTTPResponse + raw_kwargs['body'] = io.BytesIO(b''.join(wsgi_response)) + raw = requests.packages.urllib3.HTTPResponse(**raw_kwargs) + + # Build the requests.Response + return self.build_response(request, raw) + + def close(self): + pass + + class NoExternalRequestsAdapter(requests.adapters.HTTPAdapter): + def send(self, request, *args, **kwargs): + msg = ( + 'RequestsClient refusing to make an outgoing network request ' + 'to "%s". Only "testserver" or hostnames in your ALLOWED_HOSTS ' + 'setting are valid.' % request.url + ) + raise RuntimeError(msg) + + class RequestsClient(requests.Session): + def __init__(self, *args, **kwargs): + super(RequestsClient, self).__init__(*args, **kwargs) + adapter = DjangoTestAdapter() + self.mount('http://', adapter) + self.mount('https://', adapter) + + def request(self, method, url, *args, **kwargs): + if ':' not in url: + raise ValueError('Missing "http:" or "https:". Use a fully qualified URL, eg "http://testserver%s"' % url) + return super(RequestsClient, self).request(method, url, *args, **kwargs) + +else: + def RequestsClient(*args, **kwargs): + raise ImproperlyConfigured('requests must be installed in order to use RequestsClient.') + + +if coreapi is not None: + class CoreAPIClient(coreapi.Client): + def __init__(self, *args, **kwargs): + self._session = RequestsClient() + kwargs['transports'] = [coreapi.transports.HTTPTransport(session=self.session)] + return super(CoreAPIClient, self).__init__(*args, **kwargs) + + @property + def session(self): + return self._session + +else: + def CoreAPIClient(*args, **kwargs): + raise ImproperlyConfigured('coreapi must be installed in order to use CoreAPIClient.') + + class APIRequestFactory(DjangoRequestFactory): renderer_classes_list = api_settings.TEST_REQUEST_RENDERER_CLASSES default_format = api_settings.TEST_REQUEST_DEFAULT_FORMAT diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index 7a02bb0f0..4ea55300e 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals from django.conf.urls import include, url -from django.core.urlresolvers import RegexURLResolver +from rest_framework.compat import RegexURLResolver from rest_framework.settings import api_settings diff --git a/rest_framework/utils/breadcrumbs.py b/rest_framework/utils/breadcrumbs.py index 2e3ab9084..74f4f7840 100644 --- a/rest_framework/utils/breadcrumbs.py +++ b/rest_framework/utils/breadcrumbs.py @@ -1,6 +1,6 @@ from __future__ import unicode_literals -from django.core.urlresolvers import get_script_prefix, resolve +from rest_framework.compat import get_script_prefix, resolve def get_breadcrumbs(url, request=None): diff --git a/rest_framework/views.py b/rest_framework/views.py index e178a209f..a8710c7a0 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -110,6 +110,9 @@ class APIView(View): # Allow dependency injection of other settings to make testing easier. settings = api_settings + # Mark the view as being included or excluded from schema generation. + exclude_from_schema = False + @classmethod def as_view(cls, **initkwargs): """ @@ -129,6 +132,7 @@ class APIView(View): view = super(APIView, cls).as_view(**initkwargs) view.cls = cls + view.initkwargs = initkwargs # Note: session based authentication is explicitly CSRF validated, # all other authentication is CSRF exempt. diff --git a/tests/browsable_api/test_form_rendering.py b/tests/browsable_api/test_form_rendering.py index 5a31ae0dd..8b79ab6ff 100644 --- a/tests/browsable_api/test_form_rendering.py +++ b/tests/browsable_api/test_form_rendering.py @@ -11,6 +11,7 @@ factory = APIRequestFactory() class BasicSerializer(serializers.ModelSerializer): class Meta: model = BasicModel + fields = '__all__' class ManyPostView(generics.GenericAPIView): diff --git a/tests/conftest.py b/tests/conftest.py index a5123b9d8..256678226 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,13 @@ def pytest_configure(): from django.conf import settings + MIDDLEWARE = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + ) + settings.configure( DEBUG_PROPAGATE_EXCEPTIONS=True, DATABASES={ @@ -21,12 +28,8 @@ def pytest_configure(): 'APP_DIRS': True, }, ], - MIDDLEWARE_CLASSES=( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - ), + MIDDLEWARE=MIDDLEWARE, + MIDDLEWARE_CLASSES=MIDDLEWARE, INSTALLED_APPS=( 'django.contrib.auth', 'django.contrib.contenttypes', diff --git a/tests/test_api_client.py b/tests/test_api_client.py new file mode 100644 index 000000000..a6d72357a --- /dev/null +++ b/tests/test_api_client.py @@ -0,0 +1,474 @@ +from __future__ import unicode_literals + +import os +import tempfile +import unittest + +from django.conf.urls import url +from django.http import HttpResponse +from django.test import override_settings + +from rest_framework.compat import coreapi +from rest_framework.parsers import FileUploadParser +from rest_framework.renderers import CoreJSONRenderer +from rest_framework.response import Response +from rest_framework.test import APITestCase, CoreAPIClient +from rest_framework.views import APIView + + +def get_schema(): + return coreapi.Document( + url='https://api.example.com/', + title='Example API', + content={ + 'simple_link': coreapi.Link('/example/', description='example link'), + 'headers': coreapi.Link('/headers/'), + 'location': { + 'query': coreapi.Link('/example/', fields=[ + coreapi.Field(name='example', description='example field') + ]), + 'form': coreapi.Link('/example/', action='post', fields=[ + coreapi.Field(name='example'), + ]), + 'body': coreapi.Link('/example/', action='post', fields=[ + coreapi.Field(name='example', location='body') + ]), + 'path': coreapi.Link('/example/{id}', fields=[ + coreapi.Field(name='id', location='path') + ]) + }, + 'encoding': { + 'multipart': coreapi.Link('/example/', action='post', encoding='multipart/form-data', fields=[ + coreapi.Field(name='example') + ]), + 'multipart-body': coreapi.Link('/example/', action='post', encoding='multipart/form-data', fields=[ + coreapi.Field(name='example', location='body') + ]), + 'urlencoded': coreapi.Link('/example/', action='post', encoding='application/x-www-form-urlencoded', fields=[ + coreapi.Field(name='example') + ]), + 'urlencoded-body': coreapi.Link('/example/', action='post', encoding='application/x-www-form-urlencoded', fields=[ + coreapi.Field(name='example', location='body') + ]), + 'raw_upload': coreapi.Link('/upload/', action='post', encoding='application/octet-stream', fields=[ + coreapi.Field(name='example', location='body') + ]), + }, + 'response': { + 'download': coreapi.Link('/download/'), + 'text': coreapi.Link('/text/') + } + } + ) + + +def _iterlists(querydict): + if hasattr(querydict, 'iterlists'): + return querydict.iterlists() + return querydict.lists() + + +def _get_query_params(request): + # Return query params in a plain dict, using a list value if more + # than one item is present for a given key. + return { + key: (value[0] if len(value) == 1 else value) + for key, value in + _iterlists(request.query_params) + } + + +def _get_data(request): + if not isinstance(request.data, dict): + return request.data + # Coerce multidict into regular dict, and remove files to + # make assertions simpler. + if hasattr(request.data, 'iterlists') or hasattr(request.data, 'lists'): + # Use a list value if a QueryDict contains multiple items for a key. + return { + key: value[0] if len(value) == 1 else value + for key, value in _iterlists(request.data) + if key not in request.FILES + } + return { + key: value + for key, value in request.data.items() + if key not in request.FILES + } + + +def _get_files(request): + if not request.FILES: + return {} + return { + key: {'name': value.name, 'content': value.read()} + for key, value in request.FILES.items() + } + + +class SchemaView(APIView): + renderer_classes = [CoreJSONRenderer] + + def get(self, request): + schema = get_schema() + return Response(schema) + + +class ListView(APIView): + def get(self, request): + return Response({ + 'method': request.method, + 'query_params': _get_query_params(request) + }) + + def post(self, request): + if request.content_type: + content_type = request.content_type.split(';')[0] + else: + content_type = None + + return Response({ + 'method': request.method, + 'query_params': _get_query_params(request), + 'data': _get_data(request), + 'files': _get_files(request), + 'content_type': content_type + }) + + +class DetailView(APIView): + def get(self, request, id): + return Response({ + 'id': id, + 'method': request.method, + 'query_params': _get_query_params(request) + }) + + +class UploadView(APIView): + parser_classes = [FileUploadParser] + + def post(self, request): + return Response({ + 'method': request.method, + 'files': _get_files(request), + 'content_type': request.content_type + }) + + +class DownloadView(APIView): + def get(self, request): + return HttpResponse('some file content', content_type='image/png') + + +class TextView(APIView): + def get(self, request): + return HttpResponse('123', content_type='text/plain') + + +class HeadersView(APIView): + def get(self, request): + headers = { + key[5:].replace('_', '-'): value + for key, value in request.META.items() + if key.startswith('HTTP_') + } + return Response({ + 'method': request.method, + 'headers': headers + }) + + +urlpatterns = [ + url(r'^$', SchemaView.as_view()), + url(r'^example/$', ListView.as_view()), + url(r'^example/(?P[0-9]+)/$', DetailView.as_view()), + url(r'^upload/$', UploadView.as_view()), + url(r'^download/$', DownloadView.as_view()), + url(r'^text/$', TextView.as_view()), + url(r'^headers/$', HeadersView.as_view()), +] + + +@unittest.skipUnless(coreapi, 'coreapi not installed') +@override_settings(ROOT_URLCONF='tests.test_api_client') +class APIClientTests(APITestCase): + def test_api_client(self): + client = CoreAPIClient() + schema = client.get('http://api.example.com/') + assert schema.title == 'Example API' + assert schema.url == 'https://api.example.com/' + assert schema['simple_link'].description == 'example link' + assert schema['location']['query'].fields[0].description == 'example field' + data = client.action(schema, ['simple_link']) + expected = { + 'method': 'GET', + 'query_params': {} + } + assert data == expected + + def test_query_params(self): + client = CoreAPIClient() + schema = client.get('http://api.example.com/') + data = client.action(schema, ['location', 'query'], params={'example': 123}) + expected = { + 'method': 'GET', + 'query_params': {'example': '123'} + } + assert data == expected + + def test_session_headers(self): + client = CoreAPIClient() + client.session.headers.update({'X-Custom-Header': 'foo'}) + schema = client.get('http://api.example.com/') + data = client.action(schema, ['headers']) + assert data['headers']['X-CUSTOM-HEADER'] == 'foo' + + def test_query_params_with_multiple_values(self): + client = CoreAPIClient() + schema = client.get('http://api.example.com/') + data = client.action(schema, ['location', 'query'], params={'example': [1, 2, 3]}) + expected = { + 'method': 'GET', + 'query_params': {'example': ['1', '2', '3']} + } + assert data == expected + + def test_form_params(self): + client = CoreAPIClient() + schema = client.get('http://api.example.com/') + data = client.action(schema, ['location', 'form'], params={'example': 123}) + expected = { + 'method': 'POST', + 'content_type': 'application/json', + 'query_params': {}, + 'data': {'example': 123}, + 'files': {} + } + assert data == expected + + def test_body_params(self): + client = CoreAPIClient() + schema = client.get('http://api.example.com/') + data = client.action(schema, ['location', 'body'], params={'example': 123}) + expected = { + 'method': 'POST', + 'content_type': 'application/json', + 'query_params': {}, + 'data': 123, + 'files': {} + } + assert data == expected + + def test_path_params(self): + client = CoreAPIClient() + schema = client.get('http://api.example.com/') + data = client.action(schema, ['location', 'path'], params={'id': 123}) + expected = { + 'method': 'GET', + 'query_params': {}, + 'id': '123' + } + assert data == expected + + def test_multipart_encoding(self): + client = CoreAPIClient() + schema = client.get('http://api.example.com/') + + temp = tempfile.NamedTemporaryFile() + temp.write(b'example file content') + temp.flush() + + with open(temp.name, 'rb') as upload: + name = os.path.basename(upload.name) + data = client.action(schema, ['encoding', 'multipart'], params={'example': upload}) + + expected = { + 'method': 'POST', + 'content_type': 'multipart/form-data', + 'query_params': {}, + 'data': {}, + 'files': {'example': {'name': name, 'content': 'example file content'}} + } + assert data == expected + + def test_multipart_encoding_no_file(self): + # When no file is included, multipart encoding should still be used. + client = CoreAPIClient() + schema = client.get('http://api.example.com/') + + data = client.action(schema, ['encoding', 'multipart'], params={'example': 123}) + + expected = { + 'method': 'POST', + 'content_type': 'multipart/form-data', + 'query_params': {}, + 'data': {'example': '123'}, + 'files': {} + } + assert data == expected + + def test_multipart_encoding_multiple_values(self): + client = CoreAPIClient() + schema = client.get('http://api.example.com/') + + data = client.action(schema, ['encoding', 'multipart'], params={'example': [1, 2, 3]}) + + expected = { + 'method': 'POST', + 'content_type': 'multipart/form-data', + 'query_params': {}, + 'data': {'example': ['1', '2', '3']}, + 'files': {} + } + assert data == expected + + def test_multipart_encoding_string_file_content(self): + # Test for `coreapi.utils.File` support. + from coreapi.utils import File + + client = CoreAPIClient() + schema = client.get('http://api.example.com/') + + example = File(name='example.txt', content='123') + data = client.action(schema, ['encoding', 'multipart'], params={'example': example}) + + expected = { + 'method': 'POST', + 'content_type': 'multipart/form-data', + 'query_params': {}, + 'data': {}, + 'files': {'example': {'name': 'example.txt', 'content': '123'}} + } + assert data == expected + + def test_multipart_encoding_in_body(self): + from coreapi.utils import File + + client = CoreAPIClient() + schema = client.get('http://api.example.com/') + + example = {'foo': File(name='example.txt', content='123'), 'bar': 'abc'} + data = client.action(schema, ['encoding', 'multipart-body'], params={'example': example}) + + expected = { + 'method': 'POST', + 'content_type': 'multipart/form-data', + 'query_params': {}, + 'data': {'bar': 'abc'}, + 'files': {'foo': {'name': 'example.txt', 'content': '123'}} + } + assert data == expected + + # URLencoded + + def test_urlencoded_encoding(self): + client = CoreAPIClient() + schema = client.get('http://api.example.com/') + data = client.action(schema, ['encoding', 'urlencoded'], params={'example': 123}) + expected = { + 'method': 'POST', + 'content_type': 'application/x-www-form-urlencoded', + 'query_params': {}, + 'data': {'example': '123'}, + 'files': {} + } + assert data == expected + + def test_urlencoded_encoding_multiple_values(self): + client = CoreAPIClient() + schema = client.get('http://api.example.com/') + data = client.action(schema, ['encoding', 'urlencoded'], params={'example': [1, 2, 3]}) + expected = { + 'method': 'POST', + 'content_type': 'application/x-www-form-urlencoded', + 'query_params': {}, + 'data': {'example': ['1', '2', '3']}, + 'files': {} + } + assert data == expected + + def test_urlencoded_encoding_in_body(self): + client = CoreAPIClient() + schema = client.get('http://api.example.com/') + data = client.action(schema, ['encoding', 'urlencoded-body'], params={'example': {'foo': 123, 'bar': True}}) + expected = { + 'method': 'POST', + 'content_type': 'application/x-www-form-urlencoded', + 'query_params': {}, + 'data': {'foo': '123', 'bar': 'true'}, + 'files': {} + } + assert data == expected + + # Raw uploads + + def test_raw_upload(self): + client = CoreAPIClient() + schema = client.get('http://api.example.com/') + + temp = tempfile.NamedTemporaryFile() + temp.write(b'example file content') + temp.flush() + + with open(temp.name, 'rb') as upload: + name = os.path.basename(upload.name) + data = client.action(schema, ['encoding', 'raw_upload'], params={'example': upload}) + + expected = { + 'method': 'POST', + 'files': {'file': {'name': name, 'content': 'example file content'}}, + 'content_type': 'application/octet-stream' + } + assert data == expected + + def test_raw_upload_string_file_content(self): + from coreapi.utils import File + + client = CoreAPIClient() + schema = client.get('http://api.example.com/') + + example = File('example.txt', '123') + data = client.action(schema, ['encoding', 'raw_upload'], params={'example': example}) + + expected = { + 'method': 'POST', + 'files': {'file': {'name': 'example.txt', 'content': '123'}}, + 'content_type': 'text/plain' + } + assert data == expected + + def test_raw_upload_explicit_content_type(self): + from coreapi.utils import File + + client = CoreAPIClient() + schema = client.get('http://api.example.com/') + + example = File('example.txt', '123', 'text/html') + data = client.action(schema, ['encoding', 'raw_upload'], params={'example': example}) + + expected = { + 'method': 'POST', + 'files': {'file': {'name': 'example.txt', 'content': '123'}}, + 'content_type': 'text/html' + } + assert data == expected + + # Responses + + def test_text_response(self): + client = CoreAPIClient() + schema = client.get('http://api.example.com/') + + data = client.action(schema, ['response', 'text']) + + expected = '123' + assert data == expected + + def test_download_response(self): + client = CoreAPIClient() + schema = client.get('http://api.example.com/') + + data = client.action(schema, ['response', 'download']) + assert data.basename == 'download.png' + assert data.read() == b'some file content' diff --git a/tests/test_atomic_requests.py b/tests/test_atomic_requests.py index 8342ad3af..09d7f2fb1 100644 --- a/tests/test_atomic_requests.py +++ b/tests/test_atomic_requests.py @@ -5,7 +5,7 @@ import unittest from django.conf.urls import url from django.db import connection, connections, transaction from django.http import Http404 -from django.test import TestCase, TransactionTestCase +from django.test import TestCase, TransactionTestCase, override_settings from django.utils.decorators import method_decorator from rest_framework import status @@ -36,6 +36,20 @@ class APIExceptionView(APIView): raise APIException +class NonAtomicAPIExceptionView(APIView): + @method_decorator(transaction.non_atomic_requests) + def dispatch(self, *args, **kwargs): + return super(NonAtomicAPIExceptionView, self).dispatch(*args, **kwargs) + + def get(self, request, *args, **kwargs): + BasicModel.objects.all() + raise Http404 + +urlpatterns = ( + url(r'^$', NonAtomicAPIExceptionView.as_view()), +) + + @unittest.skipUnless( connection.features.uses_savepoints, "'atomic' requires transactions and savepoints." @@ -124,22 +138,8 @@ class DBTransactionAPIExceptionTests(TestCase): connection.features.uses_savepoints, "'atomic' requires transactions and savepoints." ) +@override_settings(ROOT_URLCONF='tests.test_atomic_requests') class NonAtomicDBTransactionAPIExceptionTests(TransactionTestCase): - @property - def urls(self): - class NonAtomicAPIExceptionView(APIView): - @method_decorator(transaction.non_atomic_requests) - def dispatch(self, *args, **kwargs): - return super(NonAtomicAPIExceptionView, self).dispatch(*args, **kwargs) - - def get(self, request, *args, **kwargs): - BasicModel.objects.all() - raise Http404 - - return ( - url(r'^$', NonAtomicAPIExceptionView.as_view()), - ) - def setUp(self): connections.databases['default']['ATOMIC_REQUESTS'] = True diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 5ef620abe..6f17ea14f 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -20,6 +20,7 @@ from rest_framework.authentication import ( ) from rest_framework.authtoken.models import Token from rest_framework.authtoken.views import obtain_auth_token +from rest_framework.compat import is_authenticated from rest_framework.response import Response from rest_framework.test import APIClient, APIRequestFactory from rest_framework.views import APIView @@ -408,7 +409,7 @@ class FailingAuthAccessedInRenderer(TestCase): def render(self, data, media_type=None, renderer_context=None): request = renderer_context['request'] - if request.user.is_authenticated(): + if is_authenticated(request.user): return b'authenticated' return b'not authenticated' diff --git a/tests/test_fields.py b/tests/test_fields.py index 4a4b741c5..c271afa9e 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,6 +1,7 @@ import datetime import os import re +import unittest import uuid from decimal import Decimal @@ -11,6 +12,67 @@ from django.utils import six, timezone import rest_framework from rest_framework import serializers +from rest_framework.fields import is_simple_callable + +try: + import typings +except ImportError: + typings = False + + +# Tests for helper functions. +# --------------------------- + +class TestIsSimpleCallable: + + def test_method(self): + class Foo: + @classmethod + def classmethod(cls): + pass + + def valid(self): + pass + + def valid_kwargs(self, param='value'): + pass + + def invalid(self, param): + pass + + assert is_simple_callable(Foo.classmethod) + + # unbound methods + assert not is_simple_callable(Foo.valid) + assert not is_simple_callable(Foo.valid_kwargs) + assert not is_simple_callable(Foo.invalid) + + # bound methods + assert is_simple_callable(Foo().valid) + assert is_simple_callable(Foo().valid_kwargs) + assert not is_simple_callable(Foo().invalid) + + def test_function(self): + def simple(): + pass + + def valid(param='value', param2='value'): + pass + + def invalid(param, param2='value'): + pass + + assert is_simple_callable(simple) + assert is_simple_callable(valid) + assert not is_simple_callable(invalid) + + @unittest.skipUnless(typings, 'requires python 3.5') + def test_type_annotation(self): + # The annotation will otherwise raise a syntax error in python < 3.5 + exec("def valid(param: str='value'): pass", locals()) + valid = locals()['valid'] + + assert is_simple_callable(valid) # Tests for field keyword arguments and core functionality. diff --git a/tests/test_filters.py b/tests/test_filters.py index 03d61fc37..c67412dd7 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -6,7 +6,6 @@ from decimal import Decimal from django.conf.urls import url from django.core.exceptions import ImproperlyConfigured -from django.core.urlresolvers import reverse from django.db import models from django.test import TestCase from django.test.utils import override_settings @@ -14,7 +13,7 @@ from django.utils.dateparse import parse_date from django.utils.six.moves import reload_module from rest_framework import filters, generics, serializers, status -from rest_framework.compat import django_filters +from rest_framework.compat import django_filters, reverse from rest_framework.test import APIRequestFactory from .models import BaseFilterableItem, BasicModel, FilterableItem @@ -77,6 +76,7 @@ if django_filters: class Meta: model = BaseFilterableItem + fields = '__all__' class BaseFilterableItemFilterRootView(generics.ListCreateAPIView): queryset = FilterableItem.objects.all() @@ -456,7 +456,7 @@ class AttributeModel(models.Model): class SearchFilterModelFk(models.Model): title = models.CharField(max_length=20) - attribute = models.ForeignKey(AttributeModel) + attribute = models.ForeignKey(AttributeModel, on_delete=models.CASCADE) class SearchFilterFkSerializer(serializers.ModelSerializer): diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 01243ff6e..cd9b2dfc3 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -20,7 +20,7 @@ from django.test import TestCase from django.utils import six from rest_framework import serializers -from rest_framework.compat import unicode_repr +from rest_framework.compat import set_many, unicode_repr def dedent(blocktext): @@ -651,7 +651,7 @@ class TestIntegration(TestCase): foreign_key=self.foreign_key_target, one_to_one=self.one_to_one_target, ) - self.instance.many_to_many = self.many_to_many_targets + set_many(self.instance, 'many_to_many', self.many_to_many_targets) self.instance.save() def test_pk_retrival(self): @@ -962,7 +962,7 @@ class OneToOneTargetTestModel(models.Model): class OneToOneSourceTestModel(models.Model): - target = models.OneToOneField(OneToOneTargetTestModel, primary_key=True) + target = models.OneToOneField(OneToOneTargetTestModel, primary_key=True, on_delete=models.CASCADE) class TestModelFieldValues(TestCase): @@ -990,6 +990,7 @@ class TestUniquenessOverride(TestCase): class TestSerializer(serializers.ModelSerializer): class Meta: model = TestModel + fields = '__all__' extra_kwargs = {'field_1': {'required': False}} fields = TestSerializer().fields diff --git a/tests/test_permissions.py b/tests/test_permissions.py index 5cef22628..f8561e61d 100644 --- a/tests/test_permissions.py +++ b/tests/test_permissions.py @@ -4,7 +4,6 @@ import base64 import unittest from django.contrib.auth.models import Group, Permission, User -from django.core.urlresolvers import ResolverMatch from django.db import models from django.test import TestCase @@ -12,7 +11,7 @@ from rest_framework import ( HTTP_HEADER_ENCODING, authentication, generics, permissions, serializers, status ) -from rest_framework.compat import guardian +from rest_framework.compat import ResolverMatch, guardian, set_many from rest_framework.filters import DjangoObjectPermissionsFilter from rest_framework.routers import DefaultRouter from rest_framework.test import APIRequestFactory @@ -74,15 +73,15 @@ class ModelPermissionsIntegrationTests(TestCase): def setUp(self): User.objects.create_user('disallowed', 'disallowed@example.com', 'password') user = User.objects.create_user('permitted', 'permitted@example.com', 'password') - user.user_permissions = [ + set_many(user, 'user_permissions', [ Permission.objects.get(codename='add_basicmodel'), Permission.objects.get(codename='change_basicmodel'), Permission.objects.get(codename='delete_basicmodel') - ] + ]) user = User.objects.create_user('updateonly', 'updateonly@example.com', 'password') - user.user_permissions = [ + set_many(user, 'user_permissions', [ Permission.objects.get(codename='change_basicmodel'), - ] + ]) self.permitted_credentials = basic_auth_header('permitted', 'password') self.disallowed_credentials = basic_auth_header('disallowed', 'password') diff --git a/tests/test_request.py b/tests/test_request.py index dbfa695fd..32fbbc50b 100644 --- a/tests/test_request.py +++ b/tests/test_request.py @@ -13,6 +13,7 @@ from django.utils import six from rest_framework import status from rest_framework.authentication import SessionAuthentication +from rest_framework.compat import is_anonymous from rest_framework.parsers import BaseParser, FormParser, MultiPartParser from rest_framework.request import Request from rest_framework.response import Response @@ -169,9 +170,9 @@ class TestUserSetter(TestCase): def test_user_can_logout(self): self.request.user = self.user - self.assertFalse(self.request.user.is_anonymous()) + self.assertFalse(is_anonymous(self.request.user)) logout(self.request) - self.assertTrue(self.request.user.is_anonymous()) + self.assertTrue(is_anonymous(self.request.user)) def test_logged_in_user_is_set_on_wrapped_request(self): login(self.request, self.user) diff --git a/tests/test_requests_client.py b/tests/test_requests_client.py new file mode 100644 index 000000000..791ca4ff2 --- /dev/null +++ b/tests/test_requests_client.py @@ -0,0 +1,256 @@ +from __future__ import unicode_literals + +import unittest + +from django.conf.urls import url +from django.contrib.auth import authenticate, login +from django.contrib.auth.models import User +from django.shortcuts import redirect +from django.test import override_settings +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_protect, ensure_csrf_cookie + +from rest_framework.compat import is_authenticated, requests +from rest_framework.response import Response +from rest_framework.test import APITestCase, RequestsClient +from rest_framework.views import APIView + + +class Root(APIView): + def get(self, request): + return Response({ + 'method': request.method, + 'query_params': request.query_params, + }) + + def post(self, request): + files = { + key: (value.name, value.read()) + for key, value in request.FILES.items() + } + post = request.POST + json = None + if request.META.get('CONTENT_TYPE') == 'application/json': + json = request.data + + return Response({ + 'method': request.method, + 'query_params': request.query_params, + 'POST': post, + 'FILES': files, + 'JSON': json + }) + + +class HeadersView(APIView): + def get(self, request): + headers = { + key[5:].replace('_', '-'): value + for key, value in request.META.items() + if key.startswith('HTTP_') + } + return Response({ + 'method': request.method, + 'headers': headers + }) + + +class SessionView(APIView): + def get(self, request): + return Response({ + key: value for key, value in request.session.items() + }) + + def post(self, request): + for key, value in request.data.items(): + request.session[key] = value + return Response({ + key: value for key, value in request.session.items() + }) + + +class AuthView(APIView): + @method_decorator(ensure_csrf_cookie) + def get(self, request): + if is_authenticated(request.user): + username = request.user.username + else: + username = None + return Response({ + 'username': username + }) + + @method_decorator(csrf_protect) + def post(self, request): + username = request.data['username'] + password = request.data['password'] + user = authenticate(username=username, password=password) + if user is None: + return Response({'error': 'incorrect credentials'}) + login(request, user) + return redirect('/auth/') + + +urlpatterns = [ + url(r'^$', Root.as_view(), name='root'), + url(r'^headers/$', HeadersView.as_view(), name='headers'), + url(r'^session/$', SessionView.as_view(), name='session'), + url(r'^auth/$', AuthView.as_view(), name='auth'), +] + + +@unittest.skipUnless(requests, 'requests not installed') +@override_settings(ROOT_URLCONF='tests.test_requests_client') +class RequestsClientTests(APITestCase): + def test_get_request(self): + client = RequestsClient() + response = client.get('http://testserver/') + assert response.status_code == 200 + assert response.headers['Content-Type'] == 'application/json' + expected = { + 'method': 'GET', + 'query_params': {} + } + assert response.json() == expected + + def test_get_request_query_params_in_url(self): + client = RequestsClient() + response = client.get('http://testserver/?key=value') + assert response.status_code == 200 + assert response.headers['Content-Type'] == 'application/json' + expected = { + 'method': 'GET', + 'query_params': {'key': 'value'} + } + assert response.json() == expected + + def test_get_request_query_params_by_kwarg(self): + client = RequestsClient() + response = client.get('http://testserver/', params={'key': 'value'}) + assert response.status_code == 200 + assert response.headers['Content-Type'] == 'application/json' + expected = { + 'method': 'GET', + 'query_params': {'key': 'value'} + } + assert response.json() == expected + + def test_get_with_headers(self): + client = RequestsClient() + response = client.get('http://testserver/headers/', headers={'User-Agent': 'example'}) + assert response.status_code == 200 + assert response.headers['Content-Type'] == 'application/json' + headers = response.json()['headers'] + assert headers['USER-AGENT'] == 'example' + + def test_get_with_session_headers(self): + client = RequestsClient() + client.headers.update({'User-Agent': 'example'}) + response = client.get('http://testserver/headers/') + assert response.status_code == 200 + assert response.headers['Content-Type'] == 'application/json' + headers = response.json()['headers'] + assert headers['USER-AGENT'] == 'example' + + def test_post_form_request(self): + client = RequestsClient() + response = client.post('http://testserver/', data={'key': 'value'}) + assert response.status_code == 200 + assert response.headers['Content-Type'] == 'application/json' + expected = { + 'method': 'POST', + 'query_params': {}, + 'POST': {'key': 'value'}, + 'FILES': {}, + 'JSON': None + } + assert response.json() == expected + + def test_post_json_request(self): + client = RequestsClient() + response = client.post('http://testserver/', json={'key': 'value'}) + assert response.status_code == 200 + assert response.headers['Content-Type'] == 'application/json' + expected = { + 'method': 'POST', + 'query_params': {}, + 'POST': {}, + 'FILES': {}, + 'JSON': {'key': 'value'} + } + assert response.json() == expected + + def test_post_multipart_request(self): + client = RequestsClient() + files = { + 'file': ('report.csv', 'some,data,to,send\nanother,row,to,send\n') + } + response = client.post('http://testserver/', files=files) + assert response.status_code == 200 + assert response.headers['Content-Type'] == 'application/json' + expected = { + 'method': 'POST', + 'query_params': {}, + 'FILES': {'file': ['report.csv', 'some,data,to,send\nanother,row,to,send\n']}, + 'POST': {}, + 'JSON': None + } + assert response.json() == expected + + def test_session(self): + client = RequestsClient() + response = client.get('http://testserver/session/') + assert response.status_code == 200 + assert response.headers['Content-Type'] == 'application/json' + expected = {} + assert response.json() == expected + + response = client.post('http://testserver/session/', json={'example': 'abc'}) + assert response.status_code == 200 + assert response.headers['Content-Type'] == 'application/json' + expected = {'example': 'abc'} + assert response.json() == expected + + response = client.get('http://testserver/session/') + assert response.status_code == 200 + assert response.headers['Content-Type'] == 'application/json' + expected = {'example': 'abc'} + assert response.json() == expected + + def test_auth(self): + # Confirm session is not authenticated + client = RequestsClient() + response = client.get('http://testserver/auth/') + assert response.status_code == 200 + assert response.headers['Content-Type'] == 'application/json' + expected = { + 'username': None + } + assert response.json() == expected + assert 'csrftoken' in response.cookies + csrftoken = response.cookies['csrftoken'] + + user = User.objects.create(username='tom') + user.set_password('password') + user.save() + + # Perform a login + response = client.post('http://testserver/auth/', json={ + 'username': 'tom', + 'password': 'password' + }, headers={'X-CSRFToken': csrftoken}) + assert response.status_code == 200 + assert response.headers['Content-Type'] == 'application/json' + expected = { + 'username': 'tom' + } + assert response.json() == expected + + # Confirm session is authenticated + response = client.get('http://testserver/auth/') + assert response.status_code == 200 + assert response.headers['Content-Type'] == 'application/json' + expected = { + 'username': 'tom' + } + assert response.json() == expected diff --git a/tests/test_reverse.py b/tests/test_reverse.py index 03d31f1f9..f30a8bf9a 100644 --- a/tests/test_reverse.py +++ b/tests/test_reverse.py @@ -1,9 +1,9 @@ from __future__ import unicode_literals from django.conf.urls import url -from django.core.urlresolvers import NoReverseMatch from django.test import TestCase, override_settings +from rest_framework.compat import NoReverseMatch from rest_framework.reverse import reverse from rest_framework.test import APIRequestFactory diff --git a/tests/test_routers.py b/tests/test_routers.py index f45039f80..d28e301a0 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import json from collections import namedtuple from django.conf.urls import include, url @@ -47,6 +48,21 @@ class MockViewSet(viewsets.ModelViewSet): serializer_class = None +class EmptyPrefixSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = RouterTestModel + fields = ('uuid', 'text') + + +class EmptyPrefixViewSet(viewsets.ModelViewSet): + queryset = [RouterTestModel(id=1, uuid='111', text='First'), RouterTestModel(id=2, uuid='222', text='Second')] + serializer_class = EmptyPrefixSerializer + + def get_object(self, *args, **kwargs): + index = int(self.kwargs['pk']) - 1 + return self.queryset[index] + + notes_router = SimpleRouter() notes_router.register(r'notes', NoteViewSet) @@ -56,11 +72,19 @@ kwarged_notes_router.register(r'notes', KWargedNoteViewSet) namespaced_router = DefaultRouter() namespaced_router.register(r'example', MockViewSet, base_name='example') +empty_prefix_router = SimpleRouter() +empty_prefix_router.register(r'', EmptyPrefixViewSet, base_name='empty_prefix') +empty_prefix_urls = [ + url(r'^', include(empty_prefix_router.urls)), +] + urlpatterns = [ url(r'^non-namespaced/', include(namespaced_router.urls)), url(r'^namespaced/', include(namespaced_router.urls, namespace='example')), url(r'^example/', include(notes_router.urls)), url(r'^example2/', include(kwarged_notes_router.urls)), + + url(r'^empty-prefix/', include(empty_prefix_urls)), ] @@ -384,3 +408,28 @@ class TestDynamicListAndDetailRouter(TestCase): def test_inherited_list_and_detail_route_decorators(self): self._test_list_and_detail_route_decorators(SubDynamicListAndDetailViewSet) + + +@override_settings(ROOT_URLCONF='tests.test_routers') +class TestEmptyPrefix(TestCase): + def test_empty_prefix_list(self): + response = self.client.get('/empty-prefix/') + self.assertEqual(200, response.status_code) + self.assertEqual( + json.loads(response.content.decode('utf-8')), + [ + {'uuid': '111', 'text': 'First'}, + {'uuid': '222', 'text': 'Second'} + ] + ) + + def test_empty_prefix_detail(self): + response = self.client.get('/empty-prefix/1/') + self.assertEqual(200, response.status_code) + self.assertEqual( + json.loads(response.content.decode('utf-8')), + { + 'uuid': '111', + 'text': 'First' + } + ) diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 197e62eb0..c43fc1eff 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -6,9 +6,8 @@ from django.test import TestCase, override_settings from rest_framework import filters, pagination, permissions, serializers from rest_framework.compat import coreapi from rest_framework.decorators import detail_route, list_route -from rest_framework.response import Response from rest_framework.routers import DefaultRouter -from rest_framework.schemas import SchemaGenerator +from rest_framework.schemas import SchemaGenerator, get_schema_view from rest_framework.test import APIClient from rest_framework.views import APIView from rest_framework.viewsets import ModelViewSet @@ -23,6 +22,10 @@ class ExamplePagination(pagination.PageNumberPagination): page_size = 100 +class EmptySerializer(serializers.Serializer): + pass + + class ExampleSerializer(serializers.Serializer): a = serializers.CharField(required=True, help_text='A field description') b = serializers.CharField(required=False) @@ -43,36 +46,37 @@ class ExampleViewSet(ModelViewSet): @detail_route(methods=['post'], serializer_class=AnotherSerializer) def custom_action(self, request, pk): + """ + A description of custom action. + """ return super(ExampleSerializer, self).retrieve(self, request) @list_route() def custom_list_action(self, request): return super(ExampleViewSet, self).list(self, request) + @list_route(methods=['post', 'get'], serializer_class=EmptySerializer) + def custom_list_action_multiple_methods(self, request): + return super(ExampleViewSet, self).list(self, request) + def get_serializer(self, *args, **kwargs): assert self.request assert self.action return super(ExampleViewSet, self).get_serializer(*args, **kwargs) -class ExampleView(APIView): - permission_classes = [permissions.IsAuthenticatedOrReadOnly] +if coreapi: + schema_view = get_schema_view(title='Example API') +else: + def schema_view(request): + pass - def get(self, request, *args, **kwargs): - return Response() - - def post(self, request, *args, **kwargs): - return Response() - - -router = DefaultRouter(schema_title='Example API' if coreapi else None) +router = DefaultRouter() router.register('example', ExampleViewSet, base_name='example') urlpatterns = [ + url(r'^$', schema_view), url(r'^', include(router.urls)) ] -urlpatterns2 = [ - url(r'^example-view/$', ExampleView.as_view(), name='example-view') -] @unittest.skipUnless(coreapi, 'coreapi is not installed') @@ -80,7 +84,7 @@ urlpatterns2 = [ class TestRouterGeneratedSchema(TestCase): def test_anonymous_request(self): client = APIClient() - response = client.get('/', HTTP_ACCEPT='application/vnd.coreapi+json') + response = client.get('/', HTTP_ACCEPT='application/coreapi+json') self.assertEqual(response.status_code, 200) expected = coreapi.Document( url='', @@ -99,11 +103,17 @@ class TestRouterGeneratedSchema(TestCase): url='/example/custom_list_action/', action='get' ), - 'retrieve': coreapi.Link( - url='/example/{pk}/', + 'custom_list_action_multiple_methods': { + 'read': coreapi.Link( + url='/example/custom_list_action_multiple_methods/', + action='get' + ) + }, + 'read': coreapi.Link( + url='/example/{id}/', action='get', fields=[ - coreapi.Field('pk', required=True, location='path') + coreapi.Field('id', required=True, location='path') ] ) } @@ -114,7 +124,7 @@ class TestRouterGeneratedSchema(TestCase): def test_authenticated_request(self): client = APIClient() client.force_authenticate(MockUser()) - response = client.get('/', HTTP_ACCEPT='application/vnd.coreapi+json') + response = client.get('/', HTTP_ACCEPT='application/coreapi+json') self.assertEqual(response.status_code, 200) expected = coreapi.Document( url='', @@ -134,56 +144,67 @@ class TestRouterGeneratedSchema(TestCase): action='post', encoding='application/json', fields=[ - coreapi.Field('a', required=True, location='form', description='A field description'), - coreapi.Field('b', required=False, location='form') + coreapi.Field('a', required=True, location='form', type='string', description='A field description'), + coreapi.Field('b', required=False, location='form', type='string') ] ), - 'retrieve': coreapi.Link( - url='/example/{pk}/', + 'read': coreapi.Link( + url='/example/{id}/', action='get', fields=[ - coreapi.Field('pk', required=True, location='path') + coreapi.Field('id', required=True, location='path') ] ), 'custom_action': coreapi.Link( - url='/example/{pk}/custom_action/', + url='/example/{id}/custom_action/', action='post', encoding='application/json', + description='A description of custom action.', fields=[ - coreapi.Field('pk', required=True, location='path'), - coreapi.Field('c', required=True, location='form'), - coreapi.Field('d', required=False, location='form'), + coreapi.Field('id', required=True, location='path'), + coreapi.Field('c', required=True, location='form', type='string'), + coreapi.Field('d', required=False, location='form', type='string'), ] ), 'custom_list_action': coreapi.Link( url='/example/custom_list_action/', action='get' ), + 'custom_list_action_multiple_methods': { + 'read': coreapi.Link( + url='/example/custom_list_action_multiple_methods/', + action='get' + ), + 'create': coreapi.Link( + url='/example/custom_list_action_multiple_methods/', + action='post' + ) + }, 'update': coreapi.Link( - url='/example/{pk}/', + url='/example/{id}/', action='put', encoding='application/json', fields=[ - coreapi.Field('pk', required=True, location='path'), - coreapi.Field('a', required=True, location='form', description='A field description'), - coreapi.Field('b', required=False, location='form') + coreapi.Field('id', required=True, location='path'), + coreapi.Field('a', required=True, location='form', type='string', description='A field description'), + coreapi.Field('b', required=False, location='form', type='string') ] ), 'partial_update': coreapi.Link( - url='/example/{pk}/', + url='/example/{id}/', action='patch', encoding='application/json', fields=[ - coreapi.Field('pk', required=True, location='path'), - coreapi.Field('a', required=False, location='form', description='A field description'), - coreapi.Field('b', required=False, location='form') + coreapi.Field('id', required=True, location='path'), + coreapi.Field('a', required=False, location='form', type='string', description='A field description'), + coreapi.Field('b', required=False, location='form', type='string') ] ), - 'destroy': coreapi.Link( - url='/example/{pk}/', + 'delete': coreapi.Link( + url='/example/{id}/', action='delete', fields=[ - coreapi.Field('pk', required=True, location='path') + coreapi.Field('id', required=True, location='path') ] ) } @@ -192,27 +213,123 @@ class TestRouterGeneratedSchema(TestCase): self.assertEqual(response.data, expected) +class ExampleListView(APIView): + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def get(self, *args, **kwargs): + pass + + def post(self, request, *args, **kwargs): + pass + + +class ExampleDetailView(APIView): + permission_classes = [permissions.IsAuthenticatedOrReadOnly] + + def get(self, *args, **kwargs): + pass + + @unittest.skipUnless(coreapi, 'coreapi is not installed') class TestSchemaGenerator(TestCase): - def test_view(self): - schema_generator = SchemaGenerator(title='Test View', patterns=urlpatterns2) - schema = schema_generator.get_schema() + def setUp(self): + self.patterns = [ + url('^example/?$', ExampleListView.as_view()), + url('^example/(?P\d+)/?$', ExampleDetailView.as_view()), + url('^example/(?P\d+)/sub/?$', ExampleDetailView.as_view()), + ] + + def test_schema_for_regular_views(self): + """ + Ensure that schema generation works for APIView classes. + """ + generator = SchemaGenerator(title='Example API', patterns=self.patterns) + schema = generator.get_schema() expected = coreapi.Document( url='', - title='Test View', + title='Example API', content={ - 'example-view': { + 'example': { 'create': coreapi.Link( - url='/example-view/', + url='/example/', action='post', fields=[] ), - 'read': coreapi.Link( - url='/example-view/', + 'list': coreapi.Link( + url='/example/', action='get', fields=[] - ) + ), + 'read': coreapi.Link( + url='/example/{id}/', + action='get', + fields=[ + coreapi.Field('id', required=True, location='path') + ] + ), + 'sub': { + 'list': coreapi.Link( + url='/example/{id}/sub/', + action='get', + fields=[ + coreapi.Field('id', required=True, location='path') + ] + ) + } } } ) - self.assertEquals(schema, expected) + self.assertEqual(schema, expected) + + +@unittest.skipUnless(coreapi, 'coreapi is not installed') +class TestSchemaGeneratorNotAtRoot(TestCase): + def setUp(self): + self.patterns = [ + url('^api/v1/example/?$', ExampleListView.as_view()), + url('^api/v1/example/(?P\d+)/?$', ExampleDetailView.as_view()), + url('^api/v1/example/(?P\d+)/sub/?$', ExampleDetailView.as_view()), + ] + + def test_schema_for_regular_views(self): + """ + Ensure that schema generation with an API that is not at the URL + root continues to use correct structure for link keys. + """ + generator = SchemaGenerator(title='Example API', patterns=self.patterns) + schema = generator.get_schema() + expected = coreapi.Document( + url='', + title='Example API', + content={ + 'example': { + 'create': coreapi.Link( + url='/api/v1/example/', + action='post', + fields=[] + ), + 'list': coreapi.Link( + url='/api/v1/example/', + action='get', + fields=[] + ), + 'read': coreapi.Link( + url='/api/v1/example/{id}/', + action='get', + fields=[ + coreapi.Field('id', required=True, location='path') + ] + ), + 'sub': { + 'list': coreapi.Link( + url='/api/v1/example/{id}/sub/', + action='get', + fields=[ + coreapi.Field('id', required=True, location='path') + ] + ) + } + } + } + ) + self.assertEqual(schema, expected) diff --git a/tests/test_urlpatterns.py b/tests/test_urlpatterns.py index 78d37c1a8..33d367e1d 100644 --- a/tests/test_urlpatterns.py +++ b/tests/test_urlpatterns.py @@ -3,9 +3,9 @@ from __future__ import unicode_literals from collections import namedtuple from django.conf.urls import include, url -from django.core import urlresolvers from django.test import TestCase +from rest_framework.compat import RegexURLResolver, Resolver404 from rest_framework.test import APIRequestFactory from rest_framework.urlpatterns import format_suffix_patterns @@ -28,7 +28,7 @@ class FormatSuffixTests(TestCase): urlpatterns = format_suffix_patterns(urlpatterns) except Exception: self.fail("Failed to apply `format_suffix_patterns` on the supplied urlpatterns") - resolver = urlresolvers.RegexURLResolver(r'^/', urlpatterns) + resolver = RegexURLResolver(r'^/', urlpatterns) for test_path in test_paths: request = factory.get(test_path.path) try: @@ -43,7 +43,7 @@ class FormatSuffixTests(TestCase): urlpatterns = format_suffix_patterns([ url(r'^test/$', dummy_view), ]) - resolver = urlresolvers.RegexURLResolver(r'^/', urlpatterns) + resolver = RegexURLResolver(r'^/', urlpatterns) test_paths = [ (URLTestPath('/test.api', (), {'format': 'api'}), True), @@ -55,7 +55,7 @@ class FormatSuffixTests(TestCase): request = factory.get(test_path.path) try: callback, callback_args, callback_kwargs = resolver.resolve(request.path_info) - except urlresolvers.Resolver404: + except Resolver404: callback, callback_args, callback_kwargs = (None, None, None) if not expected_resolved: assert callback is None diff --git a/tests/utils.py b/tests/utils.py index 5b2d75864..52582f093 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,5 +1,5 @@ from django.core.exceptions import ObjectDoesNotExist -from django.core.urlresolvers import NoReverseMatch +from rest_framework.compat import NoReverseMatch class MockObject(object): diff --git a/tox.ini b/tox.ini index 48cecccf7..bac1569a0 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ addopts=--tb=short [tox] envlist = py27-{lint,docs}, - {py27,py32,py33,py34,py35}-django18, + {py27,py33,py34,py35}-django18, {py27,py34,py35}-django19, {py27,py34,py35}-django110, {py27,py34,py35}-django{master} @@ -25,7 +25,6 @@ basepython = py35: python3.5 py34: python3.4 py33: python3.3 - py32: python3.2 py27: python2.7 [testenv:py27-lint] From 072d14c2e1d4cf5d0b57196296657f19dd807e8c Mon Sep 17 00:00:00 2001 From: Steven Johns Date: Tue, 11 Oct 2016 11:20:48 +1100 Subject: [PATCH 266/457] Corrected `artist` and `album_name` `The Roots` are the band: https://en.wikipedia.org/wiki/The_Roots `Undun` is their album: https://en.wikipedia.org/wiki/Undun --- docs/api-guide/relations.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 8fde53d3a..aeefae8b1 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -99,8 +99,8 @@ For example, the following serializer: Would serialize to a representation like this: { - 'album_name': 'The Roots', - 'artist': 'Undun', + 'album_name': 'Undun', + 'artist': 'The Roots', 'tracks': [ 89, 90, From a3802504a0a26f1d03d2cf6f851030314b7372fd Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 11 Oct 2016 10:25:21 +0100 Subject: [PATCH 267/457] Error codes (#4550) Add error codes to `APIException` --- docs/api-guide/exceptions.md | 54 ++++++++-- rest_framework/authtoken/serializers.py | 6 +- rest_framework/exceptions.py | 132 +++++++++++++++++------- rest_framework/fields.py | 18 +++- rest_framework/serializers.py | 35 +++---- rest_framework/validators.py | 21 ++-- tests/test_exceptions.py | 38 +++++-- tests/test_validation.py | 2 +- tests/test_validation_error.py | 101 ++++++++++++++++++ 9 files changed, 316 insertions(+), 91 deletions(-) create mode 100644 tests/test_validation_error.py diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index 3e4b3e8be..f0f178d92 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -98,7 +98,7 @@ Note that the exception handler will only be called for responses generated by r The **base class** for all exceptions raised inside an `APIView` class or `@api_view`. -To provide a custom exception, subclass `APIException` and set the `.status_code` and `.default_detail` properties on the class. +To provide a custom exception, subclass `APIException` and set the `.status_code`, `.default_detail`, and `default_code` attributes 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: @@ -107,10 +107,42 @@ For example, if your API relies on a third party service that may sometimes be u class ServiceUnavailable(APIException): status_code = 503 default_detail = 'Service temporarily unavailable, try again later.' + default_code = 'service_unavailable' + +#### Inspecting API exceptions + +There are a number of different properties available for inspecting the status +of an API exception. You can use these to build custom exception handling +for your project. + +The available attributes and methods are: + +* `.detail` - Return the textual description of the error. +* `.get_codes()` - Return the code identifier of the error. +* `.full_details()` - Return both the textual description and the code identifier. + +In most cases the error detail will be a simple item: + + >>> print(exc.detail) + You do not have permission to perform this action. + >>> print(exc.get_codes()) + permission_denied + >>> print(exc.full_details()) + {'message':'You do not have permission to perform this action.','code':'permission_denied'} + +In the case of validation errors the error detail will be either a list or +dictionary of items: + + >>> print(exc.detail) + {"name":"This field is required.","age":"A valid integer is required."} + >>> print(exc.get_codes()) + {"name":"required","age":"invalid"} + >>> print(exc.get_full_details()) + {"name":{"message":"This field is required.","code":"required"},"age":{"message":"A valid integer is required.","code":"invalid"}} ## ParseError -**Signature:** `ParseError(detail=None)` +**Signature:** `ParseError(detail=None, code=None)` Raised if the request contains malformed data when accessing `request.data`. @@ -118,7 +150,7 @@ By default this exception results in a response with the HTTP status code "400 B ## AuthenticationFailed -**Signature:** `AuthenticationFailed(detail=None)` +**Signature:** `AuthenticationFailed(detail=None, code=None)` Raised when an incoming request includes incorrect authentication. @@ -126,7 +158,7 @@ By default this exception results in a response with the HTTP status code "401 U ## NotAuthenticated -**Signature:** `NotAuthenticated(detail=None)` +**Signature:** `NotAuthenticated(detail=None, code=None)` Raised when an unauthenticated request fails the permission checks. @@ -134,7 +166,7 @@ By default this exception results in a response with the HTTP status code "401 U ## PermissionDenied -**Signature:** `PermissionDenied(detail=None)` +**Signature:** `PermissionDenied(detail=None, code=None)` Raised when an authenticated request fails the permission checks. @@ -142,7 +174,7 @@ By default this exception results in a response with the HTTP status code "403 F ## NotFound -**Signature:** `NotFound(detail=None)` +**Signature:** `NotFound(detail=None, code=None)` Raised when a resource does not exists at the given URL. This exception is equivalent to the standard `Http404` Django exception. @@ -150,7 +182,7 @@ By default this exception results in a response with the HTTP status code "404 N ## MethodNotAllowed -**Signature:** `MethodNotAllowed(method, detail=None)` +**Signature:** `MethodNotAllowed(method, detail=None, code=None)` Raised when an incoming request occurs that does not map to a handler method on the view. @@ -158,7 +190,7 @@ By default this exception results in a response with the HTTP status code "405 M ## NotAcceptable -**Signature:** `NotAcceptable(detail=None)` +**Signature:** `NotAcceptable(detail=None, code=None)` Raised when an incoming request occurs with an `Accept` header that cannot be satisfied by any of the available renderers. @@ -166,7 +198,7 @@ By default this exception results in a response with the HTTP status code "406 N ## UnsupportedMediaType -**Signature:** `UnsupportedMediaType(media_type, detail=None)` +**Signature:** `UnsupportedMediaType(media_type, detail=None, code=None)` Raised if there are no parsers that can handle the content type of the request data when accessing `request.data`. @@ -174,7 +206,7 @@ By default this exception results in a response with the HTTP status code "415 U ## Throttled -**Signature:** `Throttled(wait=None, detail=None)` +**Signature:** `Throttled(wait=None, detail=None, code=None)` Raised when an incoming request fails the throttling checks. @@ -182,7 +214,7 @@ By default this exception results in a response with the HTTP status code "429 T ## ValidationError -**Signature:** `ValidationError(detail)` +**Signature:** `ValidationError(detail, code=None)` The `ValidationError` exception is slightly different from the other `APIException` classes: diff --git a/rest_framework/authtoken/serializers.py b/rest_framework/authtoken/serializers.py index 90d3bd96e..b91a8454f 100644 --- a/rest_framework/authtoken/serializers.py +++ b/rest_framework/authtoken/serializers.py @@ -21,13 +21,13 @@ class AuthTokenSerializer(serializers.Serializer): # (Assuming the default `ModelBackend` authentication backend.) if not user.is_active: msg = _('User account is disabled.') - raise serializers.ValidationError(msg) + raise serializers.ValidationError(msg, code='authorization') else: msg = _('Unable to log in with provided credentials.') - raise serializers.ValidationError(msg) + raise serializers.ValidationError(msg, code='authorization') else: msg = _('Must include "username" and "password".') - raise serializers.ValidationError(msg) + raise serializers.ValidationError(msg, code='authorization') attrs['user'] = user return attrs diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index 29afaffe0..e41655fef 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -17,27 +17,61 @@ from rest_framework import status from rest_framework.utils.serializer_helpers import ReturnDict, ReturnList -def _force_text_recursive(data): +def _get_error_details(data, default_code=None): """ Descend into a nested data structure, forcing any - lazy translation strings into plain text. + lazy translation strings or strings into `ErrorDetail`. """ if isinstance(data, list): ret = [ - _force_text_recursive(item) for item in data + _get_error_details(item, default_code) for item in data ] if isinstance(data, ReturnList): return ReturnList(ret, serializer=data.serializer) return ret elif isinstance(data, dict): ret = { - key: _force_text_recursive(value) + key: _get_error_details(value, default_code) for key, value in data.items() } if isinstance(data, ReturnDict): return ReturnDict(ret, serializer=data.serializer) return ret - return force_text(data) + + text = force_text(data) + code = getattr(data, 'code', default_code) + return ErrorDetail(text, code) + + +def _get_codes(detail): + if isinstance(detail, list): + return [_get_codes(item) for item in detail] + elif isinstance(detail, dict): + return {key: _get_codes(value) for key, value in detail.items()} + return detail.code + + +def _get_full_details(detail): + if isinstance(detail, list): + return [_get_full_details(item) for item in detail] + elif isinstance(detail, dict): + return {key: _get_full_details(value) for key, value in detail.items()} + return { + 'message': detail, + 'code': detail.code + } + + +class ErrorDetail(six.text_type): + """ + A string-like object that can additionally + """ + code = None + + def __new__(cls, string, code=None): + self = super(ErrorDetail, cls).__new__(cls, string) + self.code = code + return self class APIException(Exception): @@ -47,16 +81,35 @@ class APIException(Exception): """ status_code = status.HTTP_500_INTERNAL_SERVER_ERROR default_detail = _('A server error occurred.') + default_code = 'error' - def __init__(self, detail=None): - if detail is not None: - self.detail = force_text(detail) - else: - self.detail = force_text(self.default_detail) + def __init__(self, detail=None, code=None): + if detail is None: + detail = self.default_detail + if code is None: + code = self.default_code + + self.detail = _get_error_details(detail, code) def __str__(self): return self.detail + def get_codes(self): + """ + Return only the code part of the error details. + + Eg. {"name": ["required"]} + """ + return _get_codes(self.detail) + + def get_full_details(self): + """ + Return both the message & code parts of the error details. + + Eg. {"name": [{"message": "This field is required.", "code": "required"}]} + """ + return _get_full_details(self.detail) + # The recommended style for using `ValidationError` is to keep it namespaced # under `serializers`, in order to minimize potential confusion with Django's @@ -67,13 +120,21 @@ class APIException(Exception): class ValidationError(APIException): status_code = status.HTTP_400_BAD_REQUEST + default_detail = _('Invalid input.') + default_code = 'invalid' - def __init__(self, detail): - # For validation errors the 'detail' key is always required. - # The details should always be coerced to a list if not already. + def __init__(self, detail, code=None): + if detail is None: + detail = self.default_detail + if code is None: + code = self.default_code + + # For validation failures, we may collect may errors together, so the + # details should always be coerced to a list if not already. if not isinstance(detail, dict) and not isinstance(detail, list): detail = [detail] - self.detail = _force_text_recursive(detail) + + self.detail = _get_error_details(detail, code) def __str__(self): return six.text_type(self.detail) @@ -82,62 +143,63 @@ class ValidationError(APIException): class ParseError(APIException): status_code = status.HTTP_400_BAD_REQUEST default_detail = _('Malformed request.') + default_code = 'parse_error' class AuthenticationFailed(APIException): status_code = status.HTTP_401_UNAUTHORIZED default_detail = _('Incorrect authentication credentials.') + default_code = 'authentication_failed' class NotAuthenticated(APIException): status_code = status.HTTP_401_UNAUTHORIZED default_detail = _('Authentication credentials were not provided.') + default_code = 'not_authenticated' class PermissionDenied(APIException): status_code = status.HTTP_403_FORBIDDEN default_detail = _('You do not have permission to perform this action.') + default_code = 'permission_denied' class NotFound(APIException): status_code = status.HTTP_404_NOT_FOUND default_detail = _('Not found.') + default_code = 'not_found' class MethodNotAllowed(APIException): status_code = status.HTTP_405_METHOD_NOT_ALLOWED default_detail = _('Method "{method}" not allowed.') + default_code = 'method_not_allowed' - def __init__(self, method, detail=None): - if detail is not None: - self.detail = force_text(detail) - else: - self.detail = force_text(self.default_detail).format(method=method) + def __init__(self, method, detail=None, code=None): + if detail is None: + detail = force_text(self.default_detail).format(method=method) + super(MethodNotAllowed, self).__init__(detail, code) class NotAcceptable(APIException): status_code = status.HTTP_406_NOT_ACCEPTABLE default_detail = _('Could not satisfy the request Accept header.') + default_code = 'not_acceptable' - def __init__(self, detail=None, available_renderers=None): - if detail is not None: - self.detail = force_text(detail) - else: - self.detail = force_text(self.default_detail) + def __init__(self, detail=None, code=None, available_renderers=None): self.available_renderers = available_renderers + super(NotAcceptable, self).__init__(detail, code) class UnsupportedMediaType(APIException): status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE default_detail = _('Unsupported media type "{media_type}" in request.') + default_code = 'unsupported_media_type' - def __init__(self, media_type, detail=None): - if detail is not None: - self.detail = force_text(detail) - else: - self.detail = force_text(self.default_detail).format( - media_type=media_type - ) + def __init__(self, media_type, detail=None, code=None): + if detail is None: + detail = force_text(self.default_detail).format(media_type=media_type) + super(UnsupportedMediaType, self).__init__(detail, code) class Throttled(APIException): @@ -145,12 +207,10 @@ class Throttled(APIException): default_detail = _('Request was throttled.') extra_detail_singular = 'Expected available in {wait} second.' extra_detail_plural = 'Expected available in {wait} seconds.' + default_code = 'throttled' - def __init__(self, wait=None, detail=None): - if detail is not None: - self.detail = force_text(detail) - else: - self.detail = force_text(self.default_detail) + def __init__(self, wait=None, detail=None, code=None): + super(Throttled, self).__init__(detail, code) if wait is None: self.wait = None diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 7f8391b8a..1894b064c 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -34,7 +34,7 @@ from rest_framework import ISO_8601 from rest_framework.compat import ( get_remote_field, unicode_repr, unicode_to_repr, value_from_object ) -from rest_framework.exceptions import ValidationError +from rest_framework.exceptions import ErrorDetail, ValidationError from rest_framework.settings import api_settings from rest_framework.utils import html, humanize_datetime, representation @@ -224,6 +224,18 @@ def iter_options(grouped_choices, cutoff=None, cutoff_text=None): yield Option(value='n/a', display_text=cutoff_text, disabled=True) +def get_error_detail(exc_info): + """ + Given a Django ValidationError, return a list of ErrorDetail, + with the `code` populated. + """ + code = getattr(exc_info, 'code', None) or 'invalid' + return [ + ErrorDetail(msg, code=code) + for msg in exc_info.messages + ] + + class CreateOnlyDefault(object): """ This class may be used to provide default values that are only used @@ -525,7 +537,7 @@ class Field(object): raise errors.extend(exc.detail) except DjangoValidationError as exc: - errors.extend(exc.messages) + errors.extend(get_error_detail(exc)) if errors: raise ValidationError(errors) @@ -563,7 +575,7 @@ class Field(object): msg = MISSING_ERROR_MESSAGE.format(class_name=class_name, key=key) raise AssertionError(msg) message_string = msg.format(**kwargs) - raise ValidationError(message_string) + raise ValidationError(message_string, code=key) @cached_property def root(self): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 7e99d40b3..a6ed7d87e 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -291,32 +291,29 @@ class SerializerMetaclass(type): return super(SerializerMetaclass, cls).__new__(cls, name, bases, attrs) -def get_validation_error_detail(exc): +def as_serializer_error(exc): assert isinstance(exc, (ValidationError, DjangoValidationError)) if isinstance(exc, DjangoValidationError): - # Normally you should raise `serializers.ValidationError` - # inside your codebase, but we handle Django's validation - # exception class as well for simpler compat. - # Eg. Calling Model.clean() explicitly inside Serializer.validate() - return { - api_settings.NON_FIELD_ERRORS_KEY: list(exc.messages) - } - elif isinstance(exc.detail, dict): + detail = get_error_detail(exc) + else: + detail = exc.detail + + if isinstance(detail, dict): # If errors may be a dict we use the standard {key: list of values}. # Here we ensure that all the values are *lists* of errors. return { key: value if isinstance(value, (list, dict)) else [value] - for key, value in exc.detail.items() + for key, value in detail.items() } - elif isinstance(exc.detail, list): + elif isinstance(detail, list): # Errors raised as a list are non-field errors. return { - api_settings.NON_FIELD_ERRORS_KEY: exc.detail + api_settings.NON_FIELD_ERRORS_KEY: detail } # Errors raised as a string are non-field errors. return { - api_settings.NON_FIELD_ERRORS_KEY: [exc.detail] + api_settings.NON_FIELD_ERRORS_KEY: [detail] } @@ -410,7 +407,7 @@ class Serializer(BaseSerializer): value = self.validate(value) assert value is not None, '.validate() should return the validated data' except (ValidationError, DjangoValidationError) as exc: - raise ValidationError(detail=get_validation_error_detail(exc)) + raise ValidationError(detail=as_serializer_error(exc)) return value @@ -424,7 +421,7 @@ class Serializer(BaseSerializer): ) raise ValidationError({ api_settings.NON_FIELD_ERRORS_KEY: [message] - }) + }, code='invalid') ret = OrderedDict() errors = OrderedDict() @@ -440,7 +437,7 @@ class Serializer(BaseSerializer): except ValidationError as exc: errors[field.field_name] = exc.detail except DjangoValidationError as exc: - errors[field.field_name] = list(exc.messages) + errors[field.field_name] = get_error_detail(exc) except SkipField: pass else: @@ -564,7 +561,7 @@ class ListSerializer(BaseSerializer): value = self.validate(value) assert value is not None, '.validate() should return the validated data' except (ValidationError, DjangoValidationError) as exc: - raise ValidationError(detail=get_validation_error_detail(exc)) + raise ValidationError(detail=as_serializer_error(exc)) return value @@ -581,13 +578,13 @@ class ListSerializer(BaseSerializer): ) raise ValidationError({ api_settings.NON_FIELD_ERRORS_KEY: [message] - }) + }, code='not_a_list') if not self.allow_empty and len(data) == 0: message = self.error_messages['empty'] raise ValidationError({ api_settings.NON_FIELD_ERRORS_KEY: [message] - }) + }, code='empty') ret = [] errors = [] diff --git a/rest_framework/validators.py b/rest_framework/validators.py index 84af0b9d5..57f8bad53 100644 --- a/rest_framework/validators.py +++ b/rest_framework/validators.py @@ -80,7 +80,7 @@ class UniqueValidator(object): queryset = self.filter_queryset(value, queryset) queryset = self.exclude_current_instance(queryset) if qs_exists(queryset): - raise ValidationError(self.message) + raise ValidationError(self.message, code='unique') def __repr__(self): return unicode_to_repr('<%s(queryset=%s)>' % ( @@ -120,13 +120,13 @@ class UniqueTogetherValidator(object): if self.instance is not None: return - missing = { + missing_items = { field_name: self.missing_message for field_name in self.fields if field_name not in attrs } - if missing: - raise ValidationError(missing) + if missing_items: + raise ValidationError(missing_items, code='required') def filter_queryset(self, attrs, queryset): """ @@ -167,7 +167,8 @@ class UniqueTogetherValidator(object): ] if None not in checked_values and qs_exists(queryset): field_names = ', '.join(self.fields) - raise ValidationError(self.message.format(field_names=field_names)) + message = self.message.format(field_names=field_names) + raise ValidationError(message, code='unique') def __repr__(self): return unicode_to_repr('<%s(queryset=%s, fields=%s)>' % ( @@ -204,13 +205,13 @@ class BaseUniqueForValidator(object): The `UniqueForValidator` classes always force an implied 'required' state on the fields they are applied to. """ - missing = { + missing_items = { field_name: self.missing_message for field_name in [self.field, self.date_field] if field_name not in attrs } - if missing: - raise ValidationError(missing) + if missing_items: + raise ValidationError(missing_items, code='required') def filter_queryset(self, attrs, queryset): raise NotImplementedError('`filter_queryset` must be implemented.') @@ -231,7 +232,9 @@ class BaseUniqueForValidator(object): queryset = self.exclude_current_instance(attrs, queryset) if qs_exists(queryset): message = self.message.format(date_field=self.date_field) - raise ValidationError({self.field: message}) + raise ValidationError({ + self.field: message + }, code='unique') def __repr__(self): return unicode_to_repr('<%s(queryset=%s, field=%s, date_field=%s)>' % ( diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index cec050eb8..29703cb77 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -3,19 +3,39 @@ from __future__ import unicode_literals from django.test import TestCase from django.utils.translation import ugettext_lazy as _ -from rest_framework.exceptions import _force_text_recursive +from rest_framework.exceptions import ErrorDetail, _get_error_details class ExceptionTestCase(TestCase): - def test_force_text_recursive(self): + def test_get_error_details(self): - s = "sfdsfggiuytraetfdlklj" - self.assertEqual(_force_text_recursive(_(s)), s) - self.assertEqual(type(_force_text_recursive(_(s))), type(s)) + example = "string" + lazy_example = _(example) - self.assertEqual(_force_text_recursive({'a': _(s)})['a'], s) - self.assertEqual(type(_force_text_recursive({'a': _(s)})['a']), type(s)) + self.assertEqual( + _get_error_details(lazy_example), + example + ) + assert isinstance( + _get_error_details(lazy_example), + ErrorDetail + ) - self.assertEqual(_force_text_recursive([[_(s)]])[0][0], s) - self.assertEqual(type(_force_text_recursive([[_(s)]])[0][0]), type(s)) + self.assertEqual( + _get_error_details({'nested': lazy_example})['nested'], + example + ) + assert isinstance( + _get_error_details({'nested': lazy_example})['nested'], + ErrorDetail + ) + + self.assertEqual( + _get_error_details([[lazy_example]])[0][0], + example + ) + assert isinstance( + _get_error_details([[lazy_example]])[0][0], + ErrorDetail + ) diff --git a/tests/test_validation.py b/tests/test_validation.py index ab04d59e6..bc950dd22 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -60,7 +60,7 @@ class TestNestedValidationError(TestCase): } }) - self.assertEqual(serializers.get_validation_error_detail(e), { + self.assertEqual(serializers.as_serializer_error(e), { 'nested': { 'field': ['error'], } diff --git a/tests/test_validation_error.py b/tests/test_validation_error.py new file mode 100644 index 000000000..8e371a349 --- /dev/null +++ b/tests/test_validation_error.py @@ -0,0 +1,101 @@ +from django.test import TestCase + +from rest_framework import serializers, status +from rest_framework.decorators import api_view +from rest_framework.response import Response +from rest_framework.settings import api_settings +from rest_framework.test import APIRequestFactory +from rest_framework.views import APIView + +factory = APIRequestFactory() + + +class ExampleSerializer(serializers.Serializer): + char = serializers.CharField() + integer = serializers.IntegerField() + + +class ErrorView(APIView): + def get(self, request, *args, **kwargs): + ExampleSerializer(data={}).is_valid(raise_exception=True) + + +@api_view(['GET']) +def error_view(request): + ExampleSerializer(data={}).is_valid(raise_exception=True) + + +class TestValidationErrorWithFullDetails(TestCase): + def setUp(self): + self.DEFAULT_HANDLER = api_settings.EXCEPTION_HANDLER + + def exception_handler(exc, request): + data = exc.get_full_details() + return Response(data, status=status.HTTP_400_BAD_REQUEST) + + api_settings.EXCEPTION_HANDLER = exception_handler + + self.expected_response_data = { + 'char': [{ + 'message': 'This field is required.', + 'code': 'required', + }], + 'integer': [{ + 'message': 'This field is required.', + 'code': 'required' + }], + } + + def tearDown(self): + api_settings.EXCEPTION_HANDLER = self.DEFAULT_HANDLER + + def test_class_based_view_exception_handler(self): + view = ErrorView.as_view() + + request = factory.get('/', content_type='application/json') + response = view(request) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data, self.expected_response_data) + + def test_function_based_view_exception_handler(self): + view = error_view + + request = factory.get('/', content_type='application/json') + response = view(request) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data, self.expected_response_data) + + +class TestValidationErrorWithCodes(TestCase): + def setUp(self): + self.DEFAULT_HANDLER = api_settings.EXCEPTION_HANDLER + + def exception_handler(exc, request): + data = exc.get_codes() + return Response(data, status=status.HTTP_400_BAD_REQUEST) + + api_settings.EXCEPTION_HANDLER = exception_handler + + self.expected_response_data = { + 'char': ['required'], + 'integer': ['required'], + } + + def tearDown(self): + api_settings.EXCEPTION_HANDLER = self.DEFAULT_HANDLER + + def test_class_based_view_exception_handler(self): + view = ErrorView.as_view() + + request = factory.get('/', content_type='application/json') + response = view(request) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data, self.expected_response_data) + + def test_function_based_view_exception_handler(self): + view = error_view + + request = factory.get('/', content_type='application/json') + response = view(request) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data, self.expected_response_data) From aed4ed5e73638025ae3c9d591e2731b1e792a344 Mon Sep 17 00:00:00 2001 From: Akshay Sharma Date: Tue, 11 Oct 2016 15:29:00 +0530 Subject: [PATCH 268/457] Browsable API navbar gets overlapped by highlighted pagination item fix (#4547) --- rest_framework/static/rest_framework/css/bootstrap-tweaks.css | 1 - 1 file changed, 1 deletion(-) diff --git a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css index 17085b49d..c2fcb303d 100644 --- a/rest_framework/static/rest_framework/css/bootstrap-tweaks.css +++ b/rest_framework/static/rest_framework/css/bootstrap-tweaks.css @@ -32,7 +32,6 @@ a single block in the template. position: fixed; left: 0; top: 0; - z-index: 3; } .navbar { From d0b3b6470a4f16d8256d0db5ed1be0f5a65dc0dc Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 11 Oct 2016 11:07:40 +0100 Subject: [PATCH 269/457] Fix prefetch_related updates. (#4553) --- rest_framework/mixins.py | 7 ++++++ tests/test_prefetch_related.py | 41 ++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 tests/test_prefetch_related.py diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 1104aa29c..47a4923a1 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -68,6 +68,13 @@ class UpdateModelMixin(object): serializer = self.get_serializer(instance, data=request.data, partial=partial) serializer.is_valid(raise_exception=True) self.perform_update(serializer) + + if getattr(instance, '_prefetched_objects_cache', None): + # If 'prefetch_related' has been applied to a queryset, we need to + # refresh the instance from the database. + instance = self.get_object() + serializer = self.get_serializer(instance) + return Response(serializer.data) def perform_update(self, serializer): diff --git a/tests/test_prefetch_related.py b/tests/test_prefetch_related.py new file mode 100644 index 000000000..fc697adc1 --- /dev/null +++ b/tests/test_prefetch_related.py @@ -0,0 +1,41 @@ +from django.contrib.auth.models import Group, User +from django.test import TestCase + +from rest_framework import generics, serializers +from rest_framework.test import APIRequestFactory + +factory = APIRequestFactory() + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ('id', 'username', 'email', 'groups') + + +class UserUpdate(generics.UpdateAPIView): + queryset = User.objects.all().prefetch_related('groups') + serializer_class = UserSerializer + + +class TestPrefetchRelatedUpdates(TestCase): + def setUp(self): + self.user = User.objects.create(username='tom', email='tom@example.com') + self.groups = [Group.objects.create(name='a'), Group.objects.create(name='b')] + self.user.groups = self.groups + self.user.save() + + def test_prefetch_related_updates(self): + view = UserUpdate.as_view() + pk = self.user.pk + groups_pk = self.groups[0].pk + request = factory.put('/', {'username': 'new', 'groups': [groups_pk]}, format='json') + response = view(request, pk=pk) + assert User.objects.get(pk=pk).groups.count() == 1 + expected = { + 'id': pk, + 'username': 'new', + 'groups': [1], + 'email': 'tom@example.com' + } + assert response.data == expected From 7f29cfc931110078d5b915fe0001a6ce06998c86 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 11 Oct 2016 12:18:00 +0100 Subject: [PATCH 270/457] Lazy hyperlink names (#4554) --- rest_framework/relations.py | 17 ++++--- rest_framework/templatetags/rest_framework.py | 3 +- tests/test_lazy_hyperlinks.py | 49 +++++++++++++++++++ 3 files changed, 61 insertions(+), 8 deletions(-) create mode 100644 tests/test_lazy_hyperlinks.py diff --git a/rest_framework/relations.py b/rest_framework/relations.py index c6eb92a24..c1898d0d8 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -37,14 +37,21 @@ class Hyperlink(six.text_type): We use this for hyperlinked URLs that may render as a named link in some contexts, or render as a plain URL in others. """ - def __new__(self, url, name): + def __new__(self, url, obj): ret = six.text_type.__new__(self, url) - ret.name = name + ret.obj = obj return ret def __getnewargs__(self): return(str(self), self.name,) + @property + def name(self): + # This ensures that we only called `__str__` lazily, + # as in some cases calling __str__ on a model instances *might* + # involve a database lookup. + return six.text_type(self.obj) + is_hyperlink = True @@ -303,9 +310,6 @@ class HyperlinkedRelatedField(RelatedField): kwargs = {self.lookup_url_kwarg: lookup_value} return self.reverse(view_name, kwargs=kwargs, request=request, format=format) - def get_name(self, obj): - return six.text_type(obj) - def to_internal_value(self, data): request = self.context.get('request', None) try: @@ -384,8 +388,7 @@ class HyperlinkedRelatedField(RelatedField): if url is None: return None - name = self.get_name(value) - return Hyperlink(url, name) + return Hyperlink(url, value) class HyperlinkedIdentityField(HyperlinkedRelatedField): diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index c1c8a5396..cfba054f6 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -135,7 +135,8 @@ def add_class(value, css_class): @register.filter def format_value(value): if getattr(value, 'is_hyperlink', False): - return mark_safe('%s' % (value, escape(value.name))) + name = six.text_type(value.obj) + return mark_safe('%s' % (value, escape(name))) if value is None or isinstance(value, bool): return mark_safe('%s' % {True: 'true', False: 'false', None: 'null'}[value]) elif isinstance(value, list): diff --git a/tests/test_lazy_hyperlinks.py b/tests/test_lazy_hyperlinks.py new file mode 100644 index 000000000..cf3ee735f --- /dev/null +++ b/tests/test_lazy_hyperlinks.py @@ -0,0 +1,49 @@ +from django.conf.urls import url +from django.db import models +from django.test import TestCase, override_settings + +from rest_framework import serializers +from rest_framework.renderers import JSONRenderer +from rest_framework.templatetags.rest_framework import format_value + +str_called = False + + +class Example(models.Model): + text = models.CharField(max_length=100) + + def __str__(self): + global str_called + str_called = True + return 'An example' + + +class ExampleSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = Example + fields = ('url', 'id', 'text') + + +def dummy_view(request): + pass + + +urlpatterns = [ + url(r'^example/(?P[0-9]+)/$', dummy_view, name='example-detail'), +] + + +@override_settings(ROOT_URLCONF='tests.test_lazy_hyperlinks') +class TestLazyHyperlinkNames(TestCase): + def setUp(self): + self.example = Example.objects.create(text='foo') + + def test_lazy_hyperlink_names(self): + global str_called + context = {'request': None} + serializer = ExampleSerializer(self.example, context=context) + JSONRenderer().render(serializer.data) + assert not str_called + hyperlink_string = format_value(serializer.data['url']) + assert hyperlink_string == 'An example' + assert str_called From 02fcf6a3346a9d47a6c4297fd562708feb27ed89 Mon Sep 17 00:00:00 2001 From: SerenityCode Date: Wed, 12 Oct 2016 09:46:59 +0100 Subject: [PATCH 271/457] Use field name instead of source when generating docs (#4559) --- rest_framework/schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 69698db0e..af861426c 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -477,7 +477,7 @@ class SchemaGenerator(object): required = field.required and method != 'PATCH' description = force_text(field.help_text) if field.help_text else '' field = coreapi.Field( - name=field.source, + name=field.field_name, location='form', required=required, description=description, From 11a89ebff42773452d2d6382ef96e8b33a24424c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 12 Oct 2016 10:02:21 +0100 Subject: [PATCH 272/457] Removous erronous duplicate Danish translation file (#4563) --- .../locale/da_DK/LC_MESSAGES/django.mo | Bin 529 -> 0 bytes .../locale/da_DK/LC_MESSAGES/django.po | 439 ------------------ 2 files changed, 439 deletions(-) delete mode 100644 rest_framework/locale/da_DK/LC_MESSAGES/django.mo delete mode 100644 rest_framework/locale/da_DK/LC_MESSAGES/django.po diff --git a/rest_framework/locale/da_DK/LC_MESSAGES/django.mo b/rest_framework/locale/da_DK/LC_MESSAGES/django.mo deleted file mode 100644 index f34583e4c20fbd84e69fe3c25847ea9bc43df310..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 529 zcmZut!A=`75T$BQd+fP~snkOvnCvd4ZHTrc1ZXTzOA=@ zOJ@TL7oPNFd;G@FZ~lG$^t{LT!2H7e#{9@UV73@A|MKMzUR%u$b7$`<3wV{|Z*pE+ zBVlr{OkrU$y-i{1R84E^9-;*`)&-pTLg#p~Dm@+(Q>SWeAOo}=- BpPB#w diff --git a/rest_framework/locale/da_DK/LC_MESSAGES/django.po b/rest_framework/locale/da_DK/LC_MESSAGES/django.po deleted file mode 100644 index bb512e284..000000000 --- a/rest_framework/locale/da_DK/LC_MESSAGES/django.po +++ /dev/null @@ -1,439 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# -# Translators: -msgid "" -msgstr "" -"Project-Id-Version: Django REST framework\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-07-12 16:13+0100\n" -"PO-Revision-Date: 2016-07-12 15:14+0000\n" -"Last-Translator: Thomas Christie \n" -"Language-Team: Danish (Denmark) (http://www.transifex.com/django-rest-framework-1/django-rest-framework/language/da_DK/)\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Language: da_DK\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: authentication.py:73 -msgid "Invalid basic header. No credentials provided." -msgstr "" - -#: authentication.py:76 -msgid "Invalid basic header. Credentials string should not contain spaces." -msgstr "" - -#: authentication.py:82 -msgid "Invalid basic header. Credentials not correctly base64 encoded." -msgstr "" - -#: authentication.py:99 -msgid "Invalid username/password." -msgstr "" - -#: authentication.py:102 authentication.py:198 -msgid "User inactive or deleted." -msgstr "" - -#: authentication.py:176 -msgid "Invalid token header. No credentials provided." -msgstr "" - -#: authentication.py:179 -msgid "Invalid token header. Token string should not contain spaces." -msgstr "" - -#: authentication.py:185 -msgid "" -"Invalid token header. Token string should not contain invalid characters." -msgstr "" - -#: authentication.py:195 -msgid "Invalid token." -msgstr "" - -#: authtoken/apps.py:7 -msgid "Auth Token" -msgstr "" - -#: authtoken/models.py:15 -msgid "Key" -msgstr "" - -#: authtoken/models.py:18 -msgid "User" -msgstr "" - -#: authtoken/models.py:20 -msgid "Created" -msgstr "" - -#: authtoken/models.py:29 -msgid "Token" -msgstr "" - -#: authtoken/models.py:30 -msgid "Tokens" -msgstr "" - -#: authtoken/serializers.py:8 -msgid "Username" -msgstr "" - -#: authtoken/serializers.py:9 -msgid "Password" -msgstr "" - -#: authtoken/serializers.py:20 -msgid "User account is disabled." -msgstr "" - -#: authtoken/serializers.py:23 -msgid "Unable to log in with provided credentials." -msgstr "" - -#: authtoken/serializers.py:26 -msgid "Must include \"username\" and \"password\"." -msgstr "" - -#: exceptions.py:49 -msgid "A server error occurred." -msgstr "" - -#: exceptions.py:84 -msgid "Malformed request." -msgstr "" - -#: exceptions.py:89 -msgid "Incorrect authentication credentials." -msgstr "" - -#: exceptions.py:94 -msgid "Authentication credentials were not provided." -msgstr "" - -#: exceptions.py:99 -msgid "You do not have permission to perform this action." -msgstr "" - -#: exceptions.py:104 views.py:81 -msgid "Not found." -msgstr "" - -#: exceptions.py:109 -msgid "Method \"{method}\" not allowed." -msgstr "" - -#: exceptions.py:120 -msgid "Could not satisfy the request Accept header." -msgstr "" - -#: exceptions.py:132 -msgid "Unsupported media type \"{media_type}\" in request." -msgstr "" - -#: exceptions.py:145 -msgid "Request was throttled." -msgstr "" - -#: fields.py:269 relations.py:206 relations.py:239 validators.py:98 -#: validators.py:181 -msgid "This field is required." -msgstr "" - -#: fields.py:270 -msgid "This field may not be null." -msgstr "" - -#: fields.py:608 fields.py:639 -msgid "\"{input}\" is not a valid boolean." -msgstr "" - -#: fields.py:674 -msgid "This field may not be blank." -msgstr "" - -#: fields.py:675 fields.py:1675 -msgid "Ensure this field has no more than {max_length} characters." -msgstr "" - -#: fields.py:676 -msgid "Ensure this field has at least {min_length} characters." -msgstr "" - -#: fields.py:713 -msgid "Enter a valid email address." -msgstr "" - -#: fields.py:724 -msgid "This value does not match the required pattern." -msgstr "" - -#: fields.py:735 -msgid "" -"Enter a valid \"slug\" consisting of letters, numbers, underscores or " -"hyphens." -msgstr "" - -#: fields.py:747 -msgid "Enter a valid URL." -msgstr "" - -#: fields.py:760 -msgid "\"{value}\" is not a valid UUID." -msgstr "" - -#: fields.py:796 -msgid "Enter a valid IPv4 or IPv6 address." -msgstr "" - -#: fields.py:821 -msgid "A valid integer is required." -msgstr "" - -#: fields.py:822 fields.py:857 fields.py:891 -msgid "Ensure this value is less than or equal to {max_value}." -msgstr "" - -#: fields.py:823 fields.py:858 fields.py:892 -msgid "Ensure this value is greater than or equal to {min_value}." -msgstr "" - -#: fields.py:824 fields.py:859 fields.py:896 -msgid "String value too large." -msgstr "" - -#: fields.py:856 fields.py:890 -msgid "A valid number is required." -msgstr "" - -#: fields.py:893 -msgid "Ensure that there are no more than {max_digits} digits in total." -msgstr "" - -#: fields.py:894 -msgid "" -"Ensure that there are no more than {max_decimal_places} decimal places." -msgstr "" - -#: fields.py:895 -msgid "" -"Ensure that there are no more than {max_whole_digits} digits before the " -"decimal point." -msgstr "" - -#: fields.py:1025 -msgid "Datetime has wrong format. Use one of these formats instead: {format}." -msgstr "" - -#: fields.py:1026 -msgid "Expected a datetime but got a date." -msgstr "" - -#: fields.py:1103 -msgid "Date has wrong format. Use one of these formats instead: {format}." -msgstr "" - -#: fields.py:1104 -msgid "Expected a date but got a datetime." -msgstr "" - -#: fields.py:1170 -msgid "Time has wrong format. Use one of these formats instead: {format}." -msgstr "" - -#: fields.py:1232 -msgid "Duration has wrong format. Use one of these formats instead: {format}." -msgstr "" - -#: fields.py:1251 fields.py:1300 -msgid "\"{input}\" is not a valid choice." -msgstr "" - -#: fields.py:1254 relations.py:71 relations.py:441 -msgid "More than {count} items..." -msgstr "" - -#: fields.py:1301 fields.py:1448 relations.py:437 serializers.py:524 -msgid "Expected a list of items but got type \"{input_type}\"." -msgstr "" - -#: fields.py:1302 -msgid "This selection may not be empty." -msgstr "" - -#: fields.py:1339 -msgid "\"{input}\" is not a valid path choice." -msgstr "" - -#: fields.py:1358 -msgid "No file was submitted." -msgstr "" - -#: fields.py:1359 -msgid "" -"The submitted data was not a file. Check the encoding type on the form." -msgstr "" - -#: fields.py:1360 -msgid "No filename could be determined." -msgstr "" - -#: fields.py:1361 -msgid "The submitted file is empty." -msgstr "" - -#: fields.py:1362 -msgid "" -"Ensure this filename has at most {max_length} characters (it has {length})." -msgstr "" - -#: fields.py:1410 -msgid "" -"Upload a valid image. The file you uploaded was either not an image or a " -"corrupted image." -msgstr "" - -#: fields.py:1449 relations.py:438 serializers.py:525 -msgid "This list may not be empty." -msgstr "" - -#: fields.py:1502 -msgid "Expected a dictionary of items but got type \"{input_type}\"." -msgstr "" - -#: fields.py:1549 -msgid "Value must be valid JSON." -msgstr "" - -#: filters.py:36 templates/rest_framework/filters/django_filter.html:5 -msgid "Submit" -msgstr "" - -#: filters.py:336 -msgid "ascending" -msgstr "" - -#: filters.py:337 -msgid "descending" -msgstr "" - -#: pagination.py:193 -msgid "Invalid page." -msgstr "" - -#: pagination.py:427 -msgid "Invalid cursor" -msgstr "" - -#: relations.py:207 -msgid "Invalid pk \"{pk_value}\" - object does not exist." -msgstr "" - -#: relations.py:208 -msgid "Incorrect type. Expected pk value, received {data_type}." -msgstr "" - -#: relations.py:240 -msgid "Invalid hyperlink - No URL match." -msgstr "" - -#: relations.py:241 -msgid "Invalid hyperlink - Incorrect URL match." -msgstr "" - -#: relations.py:242 -msgid "Invalid hyperlink - Object does not exist." -msgstr "" - -#: relations.py:243 -msgid "Incorrect type. Expected URL string, received {data_type}." -msgstr "" - -#: relations.py:401 -msgid "Object with {slug_name}={value} does not exist." -msgstr "" - -#: relations.py:402 -msgid "Invalid value." -msgstr "" - -#: serializers.py:326 -msgid "Invalid data. Expected a dictionary, but got {datatype}." -msgstr "" - -#: templates/rest_framework/admin.html:116 -#: templates/rest_framework/base.html:128 -msgid "Filters" -msgstr "" - -#: templates/rest_framework/filters/django_filter.html:2 -#: templates/rest_framework/filters/django_filter_crispyforms.html:4 -msgid "Field filters" -msgstr "" - -#: templates/rest_framework/filters/ordering.html:3 -msgid "Ordering" -msgstr "" - -#: templates/rest_framework/filters/search.html:2 -msgid "Search" -msgstr "" - -#: templates/rest_framework/horizontal/radio.html:2 -#: templates/rest_framework/inline/radio.html:2 -#: templates/rest_framework/vertical/radio.html:2 -msgid "None" -msgstr "" - -#: templates/rest_framework/horizontal/select_multiple.html:2 -#: templates/rest_framework/inline/select_multiple.html:2 -#: templates/rest_framework/vertical/select_multiple.html:2 -msgid "No items to select." -msgstr "" - -#: validators.py:43 -msgid "This field must be unique." -msgstr "" - -#: validators.py:97 -msgid "The fields {field_names} must make a unique set." -msgstr "" - -#: validators.py:245 -msgid "This field must be unique for the \"{date_field}\" date." -msgstr "" - -#: validators.py:260 -msgid "This field must be unique for the \"{date_field}\" month." -msgstr "" - -#: validators.py:273 -msgid "This field must be unique for the \"{date_field}\" year." -msgstr "" - -#: versioning.py:42 -msgid "Invalid version in \"Accept\" header." -msgstr "" - -#: versioning.py:73 -msgid "Invalid version in URL path." -msgstr "" - -#: versioning.py:115 -msgid "Invalid version in URL path. Does not match any version namespace." -msgstr "" - -#: versioning.py:147 -msgid "Invalid version in hostname." -msgstr "" - -#: versioning.py:169 -msgid "Invalid version in query parameter." -msgstr "" - -#: views.py:88 -msgid "Permission denied." -msgstr "" From 4c9b14bd972f1c74fc400297be5ecd1a1e0317f0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 12 Oct 2016 10:13:46 +0100 Subject: [PATCH 273/457] Add --minimum-perc to transifex pull command [ci skip] --- docs/topics/project-management.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/topics/project-management.md b/docs/topics/project-management.md index e84f2acfb..37e5c6882 100644 --- a/docs/topics/project-management.md +++ b/docs/topics/project-management.md @@ -6,7 +6,7 @@ This document outlines our project management processes for REST framework. -The aim is to ensure that the project has a high +The aim is to ensure that the project has a high ["bus factor"][bus-factor], and can continue to remain well supported for the foreseeable future. Suggestions for improvements to our process are welcome. --- @@ -38,27 +38,27 @@ Members of the maintenance team will be added as collaborators to the repository The following template should be used for the description of the issue, and serves as the formal process for selecting the team. This issue is for determining the maintenance team for the *** period. - + Please see the [Project management](http://www.django-rest-framework.org/topics/project-management/) section of our documentation for more details. - + --- - + #### Renewing existing members. - + The following people are the current maintenance team. Please checkmark your name if you wish to continue to have write permission on the repository for the *** period. - + - [ ] @*** - [ ] @*** - [ ] @*** - [ ] @*** - [ ] @*** - + --- - + #### New members. - + If you wish to be considered for this or a future date, please comment against this or subsequent issues. - + To modify this process for future maintenance cycles make a pull request to the [project management](http://www.django-rest-framework.org/topics/project-management/) documentation. #### Responsibilities of team members @@ -116,7 +116,7 @@ The following template should be used for the description of the issue, and serv - [ ] Make a release announcement on the [discussion group](https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework). - [ ] Make a release announcement on twitter. - [ ] Close the milestone on GitHub. - + To modify this process for future releases make a pull request to the [project management](http://www.django-rest-framework.org/topics/project-management/) documentation. When pushing the release to PyPI ensure that your environment has been installed from our development `requirement.txt`, so that documentation and PyPI installs are consistently being built against a pinned set of packages. @@ -165,7 +165,7 @@ Here's how differences between the old and new source files will be handled: When a translator has finished translating their work needs to be downloaded from Transifex into the REST framework repository. To do this, run: # 3. Pull the translated django.po files from Transifex. - tx pull -a + tx pull -a --minimum-perc 10 cd rest_framework # 4. Compile the binary .mo files for all supported languages. django-admin.py compilemessages From b4199704316bd2d67281cbc3cf946eab9bded407 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 12 Oct 2016 10:47:17 +0100 Subject: [PATCH 274/457] Handle empty data with serializer (#4564) --- rest_framework/serializers.py | 10 ++++++++++ tests/test_serializer.py | 6 ++++++ tests/test_serializer_nested.py | 6 ++++++ 3 files changed, 22 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a6ed7d87e..02c21da15 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -507,6 +507,11 @@ class Serializer(BaseSerializer): @property def errors(self): ret = super(Serializer, self).errors + if isinstance(ret, list) and len(ret) == 1 and ret[0].code == 'null': + # Edge case. Provide a more descriptive error than + # "this field may not be null", when no data is passed. + detail = ErrorDetail('No data provided', code='null') + ret = {api_settings.NON_FIELD_ERRORS_KEY: [detail]} return ReturnDict(ret, serializer=self) @@ -700,6 +705,11 @@ class ListSerializer(BaseSerializer): @property def errors(self): ret = super(ListSerializer, self).errors + if isinstance(ret, list) and len(ret) == 1 and ret[0].code == 'null': + # Edge case. Provide a more descriptive error than + # "this field may not be null", when no data is passed. + detail = ErrorDetail('No data provided', code='null') + ret = {api_settings.NON_FIELD_ERRORS_KEY: [detail]} if isinstance(ret, dict): return ReturnDict(ret, serializer=self) return ReturnList(ret, serializer=self) diff --git a/tests/test_serializer.py b/tests/test_serializer.py index bd9ef9500..a2817f6a4 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -62,6 +62,12 @@ class TestSerializer: with pytest.raises(AssertionError): serializer.save() + def test_validate_none_data(self): + data = None + serializer = self.Serializer(data=data) + assert not serializer.is_valid() + assert serializer.errors == {'non_field_errors': ['No data provided']} + class TestValidateMethod: def test_non_field_error_validate_method(self): diff --git a/tests/test_serializer_nested.py b/tests/test_serializer_nested.py index 133600399..efb671918 100644 --- a/tests/test_serializer_nested.py +++ b/tests/test_serializer_nested.py @@ -41,6 +41,12 @@ class TestNestedSerializer: serializer = self.Serializer() assert serializer.data == expected_data + def test_nested_serialize_no_data(self): + data = None + serializer = self.Serializer(data=data) + assert not serializer.is_valid() + assert serializer.errors == {'non_field_errors': ['No data provided']} + class TestNotRequiredNestedSerializer: def setup(self): From 26e51ecd6c0805770615e768e46ecc6716872670 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 12 Oct 2016 14:04:10 +0100 Subject: [PATCH 275/457] When HTML form input is used with JSONField, treat the input as a JSON encoded string, not a JSON primative. (#4565) --- rest_framework/fields.py | 14 +++++++++++++- tests/test_fields.py | 13 +++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 1894b064c..e48285005 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1594,9 +1594,21 @@ class JSONField(Field): self.binary = kwargs.pop('binary', False) super(JSONField, self).__init__(*args, **kwargs) + def get_value(self, dictionary): + if html.is_html_input(dictionary) and self.field_name in dictionary: + # When HTML form input is used, mark up the input + # as being a JSON string, rather than a JSON primative. + class JSONString(six.text_type): + def __new__(self, value): + ret = six.text_type.__new__(self, value) + ret.is_json_string = True + return ret + return JSONString(dictionary[self.field_name]) + return dictionary.get(self.field_name, empty) + def to_internal_value(self, data): try: - if self.binary: + if self.binary or getattr(data, 'is_json_string', False): if isinstance(data, six.binary_type): data = data.decode('utf-8') return json.loads(data) diff --git a/tests/test_fields.py b/tests/test_fields.py index c271afa9e..6fea249ba 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1756,6 +1756,19 @@ class TestJSONField(FieldValues): ] field = serializers.JSONField() + def test_html_input_as_json_string(self): + """ + HTML inputs should be treated as a serialized JSON string. + """ + class TestSerializer(serializers.Serializer): + config = serializers.JSONField() + + data = QueryDict(mutable=True) + data.update({'config': '{"a":1}'}) + serializer = TestSerializer(data=data) + assert serializer.is_valid() + assert serializer.validated_data == {'config': {"a": 1}} + class TestBinaryJSONField(FieldValues): """ From 5677d063d8027bcacba07c203b7772816b78fb47 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 12 Oct 2016 15:46:24 +0100 Subject: [PATCH 276/457] Do not treat empty non-form input as HTML. (#4566) --- rest_framework/request.py | 5 ++++- tests/test_testing.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/rest_framework/request.py b/rest_framework/request.py index 0a827728a..7121689d2 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -299,7 +299,10 @@ class Request(object): stream = None if stream is None or media_type is None: - empty_data = QueryDict('', encoding=self._request._encoding) + if media_type and not is_form_media_type(media_type): + empty_data = QueryDict('', encoding=self._request._encoding) + else: + empty_data = {} empty_files = MultiValueDict() return (empty_data, empty_files) diff --git a/tests/test_testing.py b/tests/test_testing.py index 6683ae6ed..e6a47d914 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -8,6 +8,7 @@ from django.contrib.auth.models import User from django.shortcuts import redirect from django.test import TestCase, override_settings +from rest_framework import fields, serializers from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.test import ( @@ -37,10 +38,22 @@ def redirect_view(request): return redirect('/view/') +class BasicSerializer(serializers.Serializer): + flag = fields.BooleanField(default=lambda: True) + + +@api_view(['POST']) +def post_view(request): + serializer = BasicSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + return Response(serializer.validated_data) + + urlpatterns = [ url(r'^view/$', view), url(r'^session-view/$', session_view), url(r'^redirect-view/$', redirect_view), + url(r'^post-view/$', post_view) ] @@ -181,6 +194,15 @@ class TestAPITestClient(TestCase): path='/view/', data={'valid': 123, 'invalid': {'a': 123}} ) + def test_empty_post_uses_default_boolean_value(self): + response = self.client.post( + '/post-view/', + data=None, + content_type='application/json' + ) + self.assertEqual(response.status_code, 200, response.content) + self.assertEqual(response.data, {"flag": True}) + class TestAPIRequestFactory(TestCase): def test_csrf_exempt_by_default(self): From 2519ce9128e4e870e28d77c0f388e3d2deb130c7 Mon Sep 17 00:00:00 2001 From: Alexey Evseev Date: Wed, 12 Oct 2016 18:09:45 +0300 Subject: [PATCH 277/457] Fix schema generation with custom page_size pagination param (#4567) --- rest_framework/pagination.py | 4 ++-- tests/test_schemas.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 6b539b900..708da29cd 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -290,9 +290,9 @@ class PageNumberPagination(BasePagination): coreapi.Field(name=self.page_query_param, required=False, location='query') ] if self.page_size_query_param is not None: - fields.append([ + fields.append( coreapi.Field(name=self.page_size_query_param, required=False, location='query') - ]) + ) return fields diff --git a/tests/test_schemas.py b/tests/test_schemas.py index c43fc1eff..7188087c4 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -20,6 +20,7 @@ class MockUser(object): class ExamplePagination(pagination.PageNumberPagination): page_size = 100 + page_size_query_param = 'page_size' class EmptySerializer(serializers.Serializer): @@ -64,7 +65,6 @@ class ExampleViewSet(ModelViewSet): assert self.action return super(ExampleViewSet, self).get_serializer(*args, **kwargs) - if coreapi: schema_view = get_schema_view(title='Example API') else: @@ -96,6 +96,7 @@ class TestRouterGeneratedSchema(TestCase): action='get', fields=[ coreapi.Field('page', required=False, location='query'), + coreapi.Field('page_size', required=False, location='query'), coreapi.Field('ordering', required=False, location='query') ] ), @@ -136,6 +137,7 @@ class TestRouterGeneratedSchema(TestCase): action='get', fields=[ coreapi.Field('page', required=False, location='query'), + coreapi.Field('page_size', required=False, location='query'), coreapi.Field('ordering', required=False, location='query') ] ), From 88c6c380c54d163d140485c8b53c923b7ec539e3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Wed, 12 Oct 2016 16:51:01 +0100 Subject: [PATCH 278/457] Use field.source to perform check for writable nested field, not key (#4568) --- 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 02c21da15..f13c92a44 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -744,8 +744,8 @@ def raise_errors_on_nested_writes(method_name, serializer, validated_data): # profile = ProfileSerializer() assert not any( isinstance(field, BaseSerializer) and - (key in validated_data) and - isinstance(validated_data[key], (list, dict)) + (field.source in validated_data) and + isinstance(validated_data[field.source], (list, dict)) for key, field in serializer.fields.items() ), ( 'The `.{method_name}()` method does not support writable nested ' From 8d0a91b002d7d18a1526e80e9cea0702e18be90e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 13 Oct 2016 12:43:43 +0100 Subject: [PATCH 279/457] Fix 3674 (#4571) Handle ModelSerializer case for relationships to models with custom pk. --- rest_framework/serializers.py | 2 +- rest_framework/utils/field_mapping.py | 2 +- rest_framework/utils/model_meta.py | 15 ++++-- tests/test_model_serializer.py | 68 +++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 7 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index f13c92a44..098c3cd23 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1167,7 +1167,7 @@ class ModelSerializer(Serializer): field_kwargs = get_relation_kwargs(field_name, relation_info) to_field = field_kwargs.pop('to_field', None) - if to_field and not relation_info.related_model._meta.get_field(to_field).primary_key: + if to_field and not relation_info.reverse and not relation_info.related_model._meta.get_field(to_field).primary_key: field_kwargs['slug_field'] = to_field field_class = self.serializer_related_to_field diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index 64df9a08e..29005f6b7 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -238,7 +238,7 @@ def get_relation_kwargs(field_name, relation_info): """ Creates a default instance of a flat relational field. """ - model_field, related_model, to_many, to_field, has_through_model = relation_info + model_field, related_model, to_many, to_field, has_through_model, reverse = relation_info kwargs = { 'queryset': related_model._default_manager, 'view_name': get_detail_view_name(related_model) diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py index 94aa46e72..3e3e434e6 100644 --- a/rest_framework/utils/model_meta.py +++ b/rest_framework/utils/model_meta.py @@ -23,7 +23,8 @@ RelationInfo = namedtuple('RelationInfo', [ 'related_model', 'to_many', 'to_field', - 'has_through_model' + 'has_through_model', + 'reverse' ]) @@ -81,7 +82,8 @@ def _get_forward_relationships(opts): related_model=get_related_model(field), to_many=False, to_field=_get_to_field(field), - has_through_model=False + has_through_model=False, + reverse=False ) # Deal with forward many-to-many relationships. @@ -94,7 +96,8 @@ def _get_forward_relationships(opts): to_field=None, has_through_model=( not get_remote_field(field).through._meta.auto_created - ) + ), + reverse=False ) return forward_relations @@ -118,7 +121,8 @@ def _get_reverse_relationships(opts): related_model=related, to_many=get_remote_field(relation.field).multiple, to_field=_get_to_field(relation.field), - has_through_model=False + has_through_model=False, + reverse=True ) # Deal with reverse many-to-many relationships. @@ -135,7 +139,8 @@ def _get_reverse_relationships(opts): has_through_model=( (getattr(get_remote_field(relation.field), 'through', None) is not None) and not get_remote_field(relation.field).through._meta.auto_created - ) + ), + reverse=True ) return reverse_relations diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index cd9b2dfc3..06848302f 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -89,6 +89,15 @@ class ChoicesModel(models.Model): choices_field_with_nonstandard_args = models.DecimalField(max_digits=3, decimal_places=1, choices=DECIMAL_CHOICES, verbose_name='A label') +class Issue3674ParentModel(models.Model): + title = models.CharField(max_length=64) + + +class Issue3674ChildModel(models.Model): + parent = models.ForeignKey(Issue3674ParentModel, related_name='children') + value = models.CharField(primary_key=True, max_length=64) + + class TestModelSerializer(TestCase): def test_create_method(self): class TestSerializer(serializers.ModelSerializer): @@ -996,3 +1005,62 @@ class TestUniquenessOverride(TestCase): fields = TestSerializer().fields self.assertFalse(fields['field_1'].required) self.assertTrue(fields['field_2'].required) + + +class Issue3674Test(TestCase): + def test_nonPK_foreignkey_model_serializer(self): + class TestParentModel(models.Model): + title = models.CharField(max_length=64) + + class TestChildModel(models.Model): + parent = models.ForeignKey(TestParentModel, related_name='children') + value = models.CharField(primary_key=True, max_length=64) + + class TestChildModelSerializer(serializers.ModelSerializer): + class Meta: + model = TestChildModel + fields = ('value', 'parent') + + class TestParentModelSerializer(serializers.ModelSerializer): + class Meta: + model = TestParentModel + fields = ('id', 'title', 'children') + + parent_expected = dedent(""" + TestParentModelSerializer(): + id = IntegerField(label='ID', read_only=True) + title = CharField(max_length=64) + children = PrimaryKeyRelatedField(many=True, queryset=TestChildModel.objects.all()) + """) + self.assertEqual(unicode_repr(TestParentModelSerializer()), parent_expected) + + child_expected = dedent(""" + TestChildModelSerializer(): + value = CharField(max_length=64, validators=[]) + parent = PrimaryKeyRelatedField(queryset=TestParentModel.objects.all()) + """) + self.assertEqual(unicode_repr(TestChildModelSerializer()), child_expected) + + def test_nonID_PK_foreignkey_model_serializer(self): + + class TestChildModelSerializer(serializers.ModelSerializer): + class Meta: + model = Issue3674ChildModel + fields = ('value', 'parent') + + class TestParentModelSerializer(serializers.ModelSerializer): + class Meta: + model = Issue3674ParentModel + fields = ('id', 'title', 'children') + + parent = Issue3674ParentModel.objects.create(title='abc') + child = Issue3674ChildModel.objects.create(value='def', parent=parent) + + parent_serializer = TestParentModelSerializer(parent) + child_serializer = TestChildModelSerializer(child) + + parent_expected = {'children': ['def'], 'id': 1, 'title': 'abc'} + self.assertEqual(parent_serializer.data, parent_expected) + + child_expected = {'parent': 1, 'value': 'def'} + self.assertEqual(child_serializer.data, child_expected) From de08f28a9185c09667eace53b637b1cb4529c695 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 13 Oct 2016 14:21:23 +0100 Subject: [PATCH 280/457] Test one to one with inheritance (#4575) --- tests/test_one_to_one_with_inheritance.py | 46 +++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/test_one_to_one_with_inheritance.py diff --git a/tests/test_one_to_one_with_inheritance.py b/tests/test_one_to_one_with_inheritance.py new file mode 100644 index 000000000..06e1cd8b8 --- /dev/null +++ b/tests/test_one_to_one_with_inheritance.py @@ -0,0 +1,46 @@ +from __future__ import unicode_literals + +from django.db import models +from django.test import TestCase + +from rest_framework import serializers +from tests.models import RESTFrameworkModel + + +# Models +from tests.test_multitable_inheritance import ChildModel + + +# Regression test for #4290 + +class ChildAssociatedModel(RESTFrameworkModel): + child_model = models.OneToOneField(ChildModel) + child_name = models.CharField(max_length=100) + + +# Serializers +class DerivedModelSerializer(serializers.ModelSerializer): + class Meta: + model = ChildModel + fields = ['id', 'name1', 'name2', 'childassociatedmodel'] + + +class ChildAssociatedModelSerializer(serializers.ModelSerializer): + + class Meta: + model = ChildAssociatedModel + fields = ['id', 'child_name'] + + +# Tests +class InheritedModelSerializationTests(TestCase): + + def test_multitable_inherited_model_fields_as_expected(self): + """ + Assert that the parent pointer field is not included in the fields + serialized fields + """ + child = ChildModel(name1='parent name', name2='child name') + serializer = DerivedModelSerializer(child) + self.assertEqual(set(serializer.data.keys()), + set(['name1', 'name2', 'id', 'childassociatedmodel'])) From cca9792ae75f586f76b5de32c0c7e1f489719e8f Mon Sep 17 00:00:00 2001 From: Akshay Sharma Date: Fri, 14 Oct 2016 15:50:09 +0530 Subject: [PATCH 281/457] contributing.md django fix. (#4581) * installation for django added in requirements.txt * added line to install django first in contributing.md * added line to install django first in contributing.md and CONTRIBUTING.md --- CONTRIBUTING.md | 1 + docs/topics/contributing.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 03865b755..415e42ac0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,6 +61,7 @@ To run the tests, clone the repository, and then: # Setup the virtual environment virtualenv env source env/bin/activate + pip install django pip install -r requirements.txt # Run the tests diff --git a/docs/topics/contributing.md b/docs/topics/contributing.md index c4ef0efdf..ed7717ae2 100644 --- a/docs/topics/contributing.md +++ b/docs/topics/contributing.md @@ -61,6 +61,7 @@ To run the tests, clone the repository, and then: # Setup the virtual environment virtualenv env source env/bin/activate + pip install django pip install -r requirements.txt # Run the tests From a83997e1edde4d7ea18d8fdd9dec747e5f1a3044 Mon Sep 17 00:00:00 2001 From: Alex Poleha Date: Fri, 14 Oct 2016 13:29:48 +0300 Subject: [PATCH 282/457] Removed incorrect line from generic-views.md (#4583) --- docs/api-guide/generic-views.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 5fea8d7e0..c368d0b46 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -220,8 +220,6 @@ Also provides a `.partial_update(request, *args, **kwargs)` method, which is sim If an object is updated this returns a `200 OK` response, with a serialized representation of the object as the body of the response. -If an object is created, for example when making a `DELETE` request followed by a `PUT` request to the same URL, this returns a `201 Created` response, with a serialized representation of the object as the body of the response. - If the request data provided for updating the object was invalid, a `400 Bad Request` response will be returned, with the error details as the body of the response. ## DestroyModelMixin From d5e63d2d7f6570d6306e755bd7366c75dd1b845b Mon Sep 17 00:00:00 2001 From: Fa773N M0nK Date: Tue, 18 Oct 2016 15:36:04 +0530 Subject: [PATCH 283/457] Reflect that '@detail_route' responds to GET only by default. (#4582) --- 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 6e1321093..f4eb918bf 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -51,7 +51,7 @@ This time we've used the `ModelViewSet` class in order to get the complete set o Notice that we've also used the `@detail_route` decorator to create a custom action, named `highlight`. This decorator can be used to add any custom endpoints that don't fit into the standard `create`/`update`/`delete` style. -Custom actions which use the `@detail_route` decorator will respond to `GET` requests. We can use the `methods` argument if we wanted an action that responded to `POST` requests. +Custom actions which use the `@detail_route` decorator will respond to `GET` requests by default. We can use the `methods` argument if we wanted an action that responded to `POST` requests. The URLs for custom actions by default depend on the method name itself. If you want to change the way url should be constructed, you can include url_path as a decorator keyword argument. From fcff16c5c658fc3033b7ecefdd74221a596ae24c Mon Sep 17 00:00:00 2001 From: Zach Wernberg Date: Tue, 18 Oct 2016 23:14:55 -0500 Subject: [PATCH 284/457] minor typo --- rest_framework/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 65233978e..c377cec0d 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -222,7 +222,7 @@ class SearchFilter(BaseFilterBackend): # Filtering against a many-to-many field requires us to # call queryset.distinct() in order to avoid duplicate items # in the resulting queryset. - # We try to avoid this is possible, for performance reasons. + # We try to avoid this if possible, for performance reasons. queryset = distinct(queryset, base) return queryset From 3f6004c5a9edab6336e93da85ce3849dee0b1311 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 20 Oct 2016 09:42:40 +0100 Subject: [PATCH 285/457] Use pk for URL conf and views. (#4592) --- docs/tutorial/1-serialization.md | 6 +++--- docs/tutorial/2-requests-and-responses.md | 8 ++++---- docs/tutorial/3-class-based-views.md | 18 +++++++++--------- .../4-authentication-and-permissions.md | 2 +- .../5-relationships-and-hyperlinked-apis.md | 8 ++++---- docs/tutorial/6-viewsets-and-routers.md | 6 +++--- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 434072e11..04fb6914a 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -259,12 +259,12 @@ Note that because we want to be able to POST to this view from clients that won' We'll also need a view which corresponds to an individual snippet, and can be used to retrieve, update or delete the snippet. @csrf_exempt - def snippet_detail(request, id): + def snippet_detail(request, pk): """ Retrieve, update or delete a code snippet. """ try: - snippet = Snippet.objects.get(id=id) + snippet = Snippet.objects.get(pk=pk) except Snippet.DoesNotExist: return HttpResponse(status=404) @@ -291,7 +291,7 @@ Finally we need to wire these views up. Create the `snippets/urls.py` file: urlpatterns = [ url(r'^snippets/$', views.snippet_list), - url(r'^snippets/(?P[0-9]+)/$', views.snippet_detail), + url(r'^snippets/(?P[0-9]+)/$', views.snippet_detail), ] We also need to wire up the root urlconf, in the `tutorial/urls.py` file, to include our snippet app's URLs. diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 4aa0062a3..5c020a1f7 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -66,12 +66,12 @@ Our instance view is an improvement over the previous example. It's a little mo Here is the view for an individual snippet, in the `views.py` module. @api_view(['GET', 'PUT', 'DELETE']) - def snippet_detail(request, id): + def snippet_detail(request, pk): """ Retrieve, update or delete a snippet instance. """ try: - snippet = Snippet.objects.get(id=id) + snippet = Snippet.objects.get(pk=pk) except Snippet.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) @@ -104,7 +104,7 @@ Start by adding a `format` keyword argument to both of the views, like so. and - def snippet_detail(request, id, format=None): + def snippet_detail(request, pk, format=None): Now update the `urls.py` file slightly, to append a set of `format_suffix_patterns` in addition to the existing URLs. @@ -114,7 +114,7 @@ Now update the `urls.py` file slightly, to append a set of `format_suffix_patter urlpatterns = [ url(r'^snippets/$', views.snippet_list), - url(r'^snippets/(?P[0-9]+)$', views.snippet_detail), + url(r'^snippets/(?P[0-9]+)$', views.snippet_detail), ] urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index 6303994cd..f018666f5 100644 --- a/docs/tutorial/3-class-based-views.md +++ b/docs/tutorial/3-class-based-views.md @@ -36,27 +36,27 @@ So far, so good. It looks pretty similar to the previous case, but we've got be """ Retrieve, update or delete a snippet instance. """ - def get_object(self, id): + def get_object(self, pk): try: - return Snippet.objects.get(id=id) + return Snippet.objects.get(pk=pk) except Snippet.DoesNotExist: raise Http404 - def get(self, request, id, format=None): - snippet = self.get_object(id) + def get(self, request, pk, format=None): + snippet = self.get_object(pk) serializer = SnippetSerializer(snippet) return Response(serializer.data) - def put(self, request, id, format=None): - snippet = self.get_object(id) + def put(self, request, pk, format=None): + snippet = self.get_object(pk) serializer = SnippetSerializer(snippet, data=request.data) if serializer.is_valid(): serializer.save() return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def delete(self, request, id, format=None): - snippet = self.get_object(id) + def delete(self, request, pk, format=None): + snippet = self.get_object(pk) snippet.delete() return Response(status=status.HTTP_204_NO_CONTENT) @@ -70,7 +70,7 @@ We'll also need to refactor our `urls.py` slightly now we're using class-based v urlpatterns = [ url(r'^snippets/$', views.SnippetList.as_view()), - url(r'^snippets/(?P[0-9]+)/$', views.SnippetDetail.as_view()), + url(r'^snippets/(?P[0-9]+)/$', views.SnippetDetail.as_view()), ] urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 958f9d3f0..098194c29 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -88,7 +88,7 @@ Make sure to also import the `UserSerializer` class Finally we need to add those views into the API, by referencing them from the URL conf. Add the following to the patterns in `urls.py`. url(r'^users/$', views.UserList.as_view()), - url(r'^users/(?P[0-9]+)/$', views.UserDetail.as_view()), + url(r'^users/(?P[0-9]+)/$', views.UserDetail.as_view()), ## Associating Snippets with Users diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index 9fb6c53e0..9fd61b414 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -48,7 +48,7 @@ We'll add a url pattern for our new API root in `snippets/urls.py`: And then add a url pattern for the snippet highlights: - url(r'^snippets/(?P[0-9]+)/highlight/$', views.SnippetHighlight.as_view()), + url(r'^snippets/(?P[0-9]+)/highlight/$', views.SnippetHighlight.as_view()), ## Hyperlinking our API @@ -116,16 +116,16 @@ After adding all those names into our URLconf, our final `snippets/urls.py` file url(r'^snippets/$', views.SnippetList.as_view(), name='snippet-list'), - url(r'^snippets/(?P[0-9]+)/$', + url(r'^snippets/(?P[0-9]+)/$', views.SnippetDetail.as_view(), name='snippet-detail'), - url(r'^snippets/(?P[0-9]+)/highlight/$', + url(r'^snippets/(?P[0-9]+)/highlight/$', views.SnippetHighlight.as_view(), name='snippet-highlight'), url(r'^users/$', views.UserList.as_view(), name='user-list'), - url(r'^users/(?P[0-9]+)/$', + url(r'^users/(?P[0-9]+)/$', views.UserDetail.as_view(), name='user-detail') ]) diff --git a/docs/tutorial/6-viewsets-and-routers.md b/docs/tutorial/6-viewsets-and-routers.md index f4eb918bf..6189c7771 100644 --- a/docs/tutorial/6-viewsets-and-routers.md +++ b/docs/tutorial/6-viewsets-and-routers.md @@ -92,10 +92,10 @@ Now that we've bound our resources into concrete views, we can register the view urlpatterns = format_suffix_patterns([ url(r'^$', api_root), url(r'^snippets/$', snippet_list, name='snippet-list'), - url(r'^snippets/(?P[0-9]+)/$', snippet_detail, name='snippet-detail'), - url(r'^snippets/(?P[0-9]+)/highlight/$', snippet_highlight, name='snippet-highlight'), + url(r'^snippets/(?P[0-9]+)/$', snippet_detail, name='snippet-detail'), + url(r'^snippets/(?P[0-9]+)/highlight/$', snippet_highlight, name='snippet-highlight'), url(r'^users/$', user_list, name='user-list'), - url(r'^users/(?P[0-9]+)/$', user_detail, name='user-detail') + url(r'^users/(?P[0-9]+)/$', user_detail, name='user-detail') ]) ## Using Routers From 2395fb53867538ad83db335f3aaaef472eb2f0f4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 20 Oct 2016 10:47:09 +0100 Subject: [PATCH 286/457] Deprecate DjangoFilter backend (#4593) Deprecate the built-in `rest_framework.filters.DjangoFilterBackend` in favour of the third-party `django_filters.rest_framework.DjangoFilterBackend`. --- docs/api-guide/filtering.md | 40 +++++--- requirements/requirements-optionals.txt | 2 +- rest_framework/filters.py | 128 ++++-------------------- tests/test_filters.py | 34 +++++++ 4 files changed, 83 insertions(+), 121 deletions(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 40a097174..1b49d3a73 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -89,24 +89,24 @@ Generic filters can also present themselves as HTML controls in the browsable AP ## Setting filter backends -The default filter backends may be set globally, using the `DEFAULT_FILTER_BACKENDS` setting. For example. +The default filter backends may be set globally, using the `DEFAULT_FILTER_BACKENDS` setting. For example. REST_FRAMEWORK = { - 'DEFAULT_FILTER_BACKENDS': ('rest_framework.filters.DjangoFilterBackend',) + 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',) } You can also set the filter backends on a per-view, or per-viewset basis, using the `GenericAPIView` class-based views. + import django_filters from django.contrib.auth.models import User from myapp.serializers import UserSerializer - from rest_framework import filters from rest_framework import generics class UserListView(generics.ListAPIView): queryset = User.objects.all() serializer_class = UserSerializer - filter_backends = (filters.DjangoFilterBackend,) + filter_backends = (django_filters.rest_framework.DjangoFilterBackend,) ## Filtering and object lookups @@ -139,12 +139,27 @@ Note that you can use both an overridden `.get_queryset()` and generic filtering ## DjangoFilterBackend -The `DjangoFilterBackend` class supports highly customizable field filtering, using the [django-filter package][django-filter]. +The `django-filter` library includes a `DjangoFilterBackend` class which +supports highly customizable field filtering for REST framework. -To use REST framework's `DjangoFilterBackend`, first install `django-filter`. +To use `DjangoFilterBackend`, first install `django-filter`. pip install django-filter +You should now either add the filter backend to your settings: + + REST_FRAMEWORK = { + 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',) + } + +Or add the filter backend to an individual View or ViewSet. + + from django_filters.rest_framework import DjangoFilterBackend + + class UserListView(generics.ListAPIView): + ... + filter_backends = (DjangoFilterBackend,) + If you are using the browsable API or admin API you may also want to install `django-crispy-forms`, which will enhance the presentation of the filter forms in HTML views, by allowing them to render Bootstrap 3 HTML. pip install django-crispy-forms @@ -174,10 +189,9 @@ For more advanced filtering requirements you can specify a `FilterSet` class tha import django_filters from myapp.models import Product from myapp.serializers import ProductSerializer - from rest_framework import filters from rest_framework import generics - class ProductFilter(filters.FilterSet): + class ProductFilter(django_filters.rest_framework.FilterSet): min_price = django_filters.NumberFilter(name="price", lookup_expr='gte') max_price = django_filters.NumberFilter(name="price", lookup_expr='lte') class Meta: @@ -187,7 +201,7 @@ For more advanced filtering requirements you can specify a `FilterSet` class tha class ProductList(generics.ListAPIView): queryset = Product.objects.all() serializer_class = ProductSerializer - filter_backends = (filters.DjangoFilterBackend,) + filter_backends = (django_filters.rest_framework.DjangoFilterBackend,) filter_class = ProductFilter @@ -199,12 +213,12 @@ You can also span relationships using `django-filter`, let's assume that each product has foreign key to `Manufacturer` model, so we create filter that filters using `Manufacturer` name. For example: + import django_filters from myapp.models import Product from myapp.serializers import ProductSerializer - from rest_framework import filters from rest_framework import generics - class ProductFilter(filters.FilterSet): + class ProductFilter(django_filters.rest_framework.FilterSet): class Meta: model = Product fields = ['category', 'in_stock', 'manufacturer__name'] @@ -218,10 +232,9 @@ This is nice, but it exposes the Django's double underscore convention as part o import django_filters from myapp.models import Product from myapp.serializers import ProductSerializer - from rest_framework import filters from rest_framework import generics - class ProductFilter(filters.FilterSet): + class ProductFilter(django_filters.rest_framework.FilterSet): manufacturer = django_filters.CharFilter(name="manufacturer__name") class Meta: @@ -454,4 +467,3 @@ The [djangorestframework-word-filter][django-rest-framework-word-search-filter] [django-rest-framework-word-search-filter]: https://github.com/trollknurr/django-rest-framework-word-search-filter [django-url-filter]: https://github.com/miki725/django-url-filter [drf-url-filter]: https://github.com/manjitkumar/drf-url-filters - diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index 31f24f4b7..86c4f7709 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -1,5 +1,5 @@ # Optional packages which may be used with REST framework. markdown==2.6.4 django-guardian==1.4.6 -django-filter==0.14.0 +django-filter==0.15.3 coreapi==2.0.8 diff --git a/rest_framework/filters.py b/rest_framework/filters.py index c377cec0d..f55297b39 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -5,9 +5,9 @@ returned by list views. from __future__ import unicode_literals import operator +import warnings from functools import reduce -from django.conf import settings from django.core.exceptions import ImproperlyConfigured from django.db import models from django.db.models.constants import LOOKUP_SEP @@ -16,50 +16,10 @@ from django.utils import six from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import ( - coreapi, crispy_forms, distinct, django_filters, guardian, template_render + coreapi, distinct, django_filters, guardian, template_render ) from rest_framework.settings import api_settings -if 'crispy_forms' in settings.INSTALLED_APPS and crispy_forms and django_filters: - # If django-crispy-forms is installed, use it to get a bootstrap3 rendering - # of the DjangoFilterBackend controls when displayed as HTML. - from crispy_forms.helper import FormHelper - from crispy_forms.layout import Layout, Submit - - class FilterSet(django_filters.FilterSet): - def __init__(self, *args, **kwargs): - super(FilterSet, self).__init__(*args, **kwargs) - for field in self.form.fields.values(): - field.help_text = None - - layout_components = list(self.form.fields.keys()) + [ - Submit('', _('Submit'), css_class='btn-default'), - ] - - helper = FormHelper() - helper.form_method = 'GET' - helper.template_pack = 'bootstrap3' - helper.layout = Layout(*layout_components) - - self.form.helper = helper - - filter_template = 'rest_framework/filters/django_filter_crispyforms.html' - -elif django_filters: - # If django-crispy-forms is not installed, use the standard - # 'form.as_p' rendering when DjangoFilterBackend is displayed as HTML. - class FilterSet(django_filters.FilterSet): - def __init__(self, *args, **kwargs): - super(FilterSet, self).__init__(*args, **kwargs) - for field in self.form.fields.values(): - field.help_text = None - - filter_template = 'rest_framework/filters/django_filter.html' - -else: - FilterSet = None - filter_template = None - class BaseFilterBackend(object): """ @@ -77,78 +37,34 @@ class BaseFilterBackend(object): return [] +class FilterSet(object): + def __new__(cls, *args, **kwargs): + warnings.warn( + "The built in 'rest_framework.filters.FilterSet' is pending deprecation. " + "You should use 'django_filters.rest_framework.FilterSet' instead.", + PendingDeprecationWarning + ) + from django_filters.rest_framework import FilterSet + return FilterSet(*args, **kwargs) + + class DjangoFilterBackend(BaseFilterBackend): """ A filter backend that uses django-filter. """ - default_filter_set = FilterSet - template = filter_template - - def __init__(self): + def __new__(cls, *args, **kwargs): assert django_filters, 'Using DjangoFilterBackend, but django-filter is not installed' + assert django_filters.VERSION >= (0, 15, 3), 'django-filter 0.15.3 and above is required' - def get_filter_class(self, view, queryset=None): - """ - Return the django-filters `FilterSet` used to filter the queryset. - """ - filter_class = getattr(view, 'filter_class', None) - filter_fields = getattr(view, 'filter_fields', None) + warnings.warn( + "The built in 'rest_framework.filters.DjangoFilterBackend' is pending deprecation. " + "You should use 'django_filters.rest_framework.DjangoFilterBackend' instead.", + PendingDeprecationWarning + ) - if filter_class: - filter_model = filter_class.Meta.model + from django_filters.rest_framework import DjangoFilterBackend - assert issubclass(queryset.model, filter_model), \ - 'FilterSet model %s does not match queryset model %s' % \ - (filter_model, queryset.model) - - return filter_class - - if filter_fields: - class AutoFilterSet(self.default_filter_set): - class Meta: - model = queryset.model - fields = filter_fields - - return AutoFilterSet - - return None - - def filter_queryset(self, request, queryset, view): - filter_class = self.get_filter_class(view, queryset) - - if filter_class: - return filter_class(request.query_params, queryset=queryset).qs - - return queryset - - def to_html(self, request, queryset, view): - filter_class = self.get_filter_class(view, queryset) - if not filter_class: - return None - filter_instance = filter_class(request.query_params, queryset=queryset) - context = { - 'filter': filter_instance - } - template = loader.get_template(self.template) - return template_render(template, context) - - def get_schema_fields(self, view): - assert coreapi is not None, 'coreapi must be installed to use `get_schema_fields()`' - filter_class = getattr(view, 'filter_class', None) - if filter_class: - return [ - coreapi.Field(name=field_name, required=False, location='query') - for field_name in filter_class().filters.keys() - ] - - filter_fields = getattr(view, 'filter_fields', None) - if filter_fields: - return [ - coreapi.Field(name=field_name, required=False, location='query') - for field_name in filter_fields - ] - - return [] + return DjangoFilterBackend(*args, **kwargs) class SearchFilter(BaseFilterBackend): diff --git a/tests/test_filters.py b/tests/test_filters.py index c67412dd7..9795230d6 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import datetime import unittest +import warnings from decimal import Decimal from django.conf.urls import url @@ -134,6 +135,39 @@ class IntegrationTestFiltering(CommonFilteringTestCase): Integration tests for filtered list views. """ + @unittest.skipUnless(django_filters, 'django-filter not installed') + def test_backend_deprecation(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + + view = FilterFieldsRootView.as_view() + request = factory.get('/') + response = view(request).render() + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, self.data) + + self.assertTrue(issubclass(w[-1].category, PendingDeprecationWarning)) + self.assertIn("'rest_framework.filters.DjangoFilterBackend' is pending deprecation.", str(w[-1].message)) + + @unittest.skipUnless(django_filters, 'django-filter not installed') + def test_no_df_deprecation(self): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + + import django_filters.rest_framework + + class DFFilterFieldsRootView(FilterFieldsRootView): + filter_backends = (django_filters.rest_framework.DjangoFilterBackend,) + + view = DFFilterFieldsRootView.as_view() + request = factory.get('/') + response = view(request).render() + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data, self.data) + self.assertEqual(len(w), 0) + @unittest.skipUnless(django_filters, 'django-filter not installed') def test_get_filtered_fields_root_view(self): """ From 3b9afb571b6e7c2a445be675163c9f43d680d899 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 20 Oct 2016 16:25:40 +0100 Subject: [PATCH 287/457] Version 3.5.0 (#4596) --- docs/api-guide/schemas.md | 47 +++++- docs/api-guide/testing.md | 4 +- docs/img/raml.png | Bin 0 -> 37216 bytes docs/index.md | 2 + docs/topics/3.5-announcement.md | 266 ++++++++++++++++++++++++++++++++ docs/topics/api-clients.md | 2 +- docs/topics/release-notes.md | 9 ++ mkdocs.yml | 1 + 8 files changed, 324 insertions(+), 7 deletions(-) create mode 100644 docs/img/raml.png create mode 100644 docs/topics/3.5-announcement.md diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index 16d2bbb01..7da619034 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -344,12 +344,12 @@ Typically you'll instantiate `SchemaGenerator` with a single argument, like so: Arguments: -* `title` - The name of the API. **required** +* `title` **required** - The name of the API. * `url` - The root URL of the API schema. This option is not required unless the schema is included under path prefix. * `patterns` - A list of URLs to inspect when generating the schema. Defaults to the project's URL conf. * `urlconf` - A URL conf module name to use when generating the schema. Defaults to `settings.ROOT_URLCONF`. -### get_schema() +### get_schema(self, request) Returns a `coreapi.Document` instance that represents the API schema. @@ -359,9 +359,48 @@ Returns a `coreapi.Document` instance that represents the API schema. generator = schemas.SchemaGenerator(title='Bookings API') return Response(generator.get_schema()) -Arguments: +The `request` argument is optional, and may be used if you want to apply per-user +permissions to the resulting schema generation. -* `request` - The incoming request. Optionally used if you want to apply per-user permissions to the schema-generation. +### get_links(self, request) + +Return a nested dictionary containing all the links that should be included in the API schema. + +This is a good point to override if you want to modify the resulting structure of the generated schema, +as you can build a new dictionary with a different layout. + +### get_link(self, path, method, view) + +Returns a `coreapi.Link` instance corresponding to the given view. + +You can override this if you need to provide custom behaviors for particular views. + +### get_description(self, path, method, view) + +Returns a string to use as the link description. By default this is based on the +view docstring as described in the "Schemas as Documentation" section above. + +### get_encoding(self, path, method, view) + +Returns a string to indicate the encoding for any request body, when interacting +with the given view. Eg. `'application/json'`. May return a blank string for views +that do not expect a request body. + +### get_path_fields(self, path, method, view): + +Return a list of `coreapi.Link()` instances. One for each path parameter in the URL. + +### get_serializer_fields(self, path, method, view) + +Return a list of `coreapi.Link()` instances. One for each field in the serializer class used by the view. + +### get_pagination_fields(self, path, method, view + +Return a list of `coreapi.Link()` instances, as returned by the `get_schema_fields()` method on any pagination class used by the view. + +### get_filter_fields(self, path, method, view) + +Return a list of `coreapi.Link()` instances, as returned by the `get_schema_fields()` method of any filter classes used by the view. --- diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index fdd60b7f4..1f8c89233 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -194,6 +194,7 @@ directly. client = RequestsClient() response = client.get('http://testserver/users/') + assert response.status_code == 200 Note that the requests client requires you to pass fully qualified URLs. @@ -251,9 +252,8 @@ The CoreAPIClient allows you to interact with your API using the Python `coreapi` client library. # Fetch the API schema - url = reverse('schema') client = CoreAPIClient() - schema = client.get(url) + schema = client.get('http://testserver/schema/') # Create a new organisation params = {'name': 'MegaCorp', 'status': 'active'} diff --git a/docs/img/raml.png b/docs/img/raml.png new file mode 100644 index 0000000000000000000000000000000000000000..87790dc484f9c6c0617189ae2766bdc1e00a3bfa GIT binary patch literal 37216 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 literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md index 3019467ed..9b0913f00 100644 --- a/docs/index.md +++ b/docs/index.md @@ -244,6 +244,7 @@ General guides to using REST framework. * [3.2 Announcement][3.2-announcement] * [3.3 Announcement][3.3-announcement] * [3.4 Announcement][3.4-announcement] +* [3.5 Announcement][3.5-announcement] * [Kickstarter Announcement][kickstarter-announcement] * [Mozilla Grant][mozilla-grant] * [Funding][funding] @@ -370,6 +371,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [3.2-announcement]: topics/3.2-announcement.md [3.3-announcement]: topics/3.3-announcement.md [3.4-announcement]: topics/3.4-announcement.md +[3.5-announcement]: topics/3.5-announcement.md [kickstarter-announcement]: topics/kickstarter-announcement.md [mozilla-grant]: topics/mozilla-grant.md [funding]: topics/funding.md diff --git a/docs/topics/3.5-announcement.md b/docs/topics/3.5-announcement.md new file mode 100644 index 000000000..2ed8adf8e --- /dev/null +++ b/docs/topics/3.5-announcement.md @@ -0,0 +1,266 @@ + + +# Django REST framework 3.5 + +The 3.5 release is the second in a planned series that is addressing schema +generation, hypermedia support, API client libraries, and finally realtime support. + +--- + +## Funding + +The 3.5 release would not have been possible without our [collaborative funding model][funding]. +If you use REST framework commercially and would like to see this work continue, +we strongly encourage you to invest in its continued development by +**[signing up for a paid plan][funding]**. + + +
+ +*Many thanks to all our [sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), and [Machinalis](http://www.machinalis.com/#services).* + +--- + +## Improved schema generation + +Docstrings on views are now pulled through into schema definitions, allowing +you to [use the schema definition to document your API][schema-docs]. + +There is now also a shortcut function, `get_schema_view()`, which makes it easier to +[adding schema views][schema-view] to your API. + +For example, to include a swagger schema to your API, you would do the following: + +* Run `pip install django-rest-swagger`. + +* Add `'rest_framework_swagger'` to your `INSTALLED_APPS` setting. + +* Include the schema view in your URL conf: + +```py +from rest_framework.schemas import get_schema_view +from rest_framework_swagger.renderers import OpenAPIRenderer, SwaggerUIRenderer + +schema_view = get_schema_view( + title='Example API', + renderer_classes=[OpenAPIRenderer, SwaggerUIRenderer] +) + +urlpatterns = [ + url(r'^swagger/$', schema_view), + ... +] +``` + +There have been a large number of fixes to the schema generation. These should +resolve issues for anyone using the latest version of the `django-rest-swagger` +package. + +Some of these changes do affect the resulting schema structure, +so if you're already using schema generation you should make sure to review +[the deprecation notes](#deprecations), particularly if you're currently using +a dynamic client library to interact with your API. + +Finally, we're also now exposing the schema generation as a +[publicly documented API][schema-generation-api], allowing you to more easily +override the behaviour. + +## Requests test client + +You can now test your project using the `requests` library. + +This exposes exactly the same interface as if you were using a standard +requests session instance. + + client = RequestsClient() + response = client.get('http://testserver/users/') + assert response.status_code == 200 + +Rather than sending any HTTP requests to the network, this interface will +coerce all outgoing requests into WSGI, and call into your application directly. + +## Core API client + +You can also now test your project by interacting with it using the `coreapi` +client library. + + # Fetch the API schema + client = CoreAPIClient() + schema = client.get('http://testserver/schema/') + + # Create a new organisation + params = {'name': 'MegaCorp', 'status': 'active'} + client.action(schema, ['organisations', 'create'], params) + + # Ensure that the organisation exists in the listing + data = client.action(schema, ['organisations', 'list']) + assert(len(data) == 1) + assert(data == [{'name': 'MegaCorp', 'status': 'active'}]) + +Again, this will call directly into the application using the WSGI interface, +rather than making actual network calls. + +This is a good option if you are planning for clients to mainly interact with +your API using the `coreapi` client library, or some other auto-generated client. + +## Live tests + +One interesting aspect of both the `requests` client and the `coreapi` client +is that they allow you to write tests in such a way that they can also be made +to run against a live service. + +By switching the WSGI based client instances to actual instances of `requests.Session` +or `coreapi.Client` you can have the test cases make actual network calls. + +Being able to write test cases that can exercise your staging or production +environment is a powerful tool. However in order to do this, you'll need to pay +close attention to how you handle setup and teardown to ensure a strict isolation +of test data from other live or staging data. + +## RAML support + +We now have preliminary support for [RAML documentation generation][django-rest-raml]. + +![RAML Example][raml-image] + +Further work on the encoding and documentation generation is planned, in order to +make features such as the 'Try it now' support available at a later date. + +This work also now means that you can use the Core API client libraries to interact +with APIs that expose a RAML specification. The [RAML codec][raml-codec] gives some examples of +interacting with the Spotify API in this way. + +## Validation codes + +Exceptions raised by REST framework now include short code identifiers. +When used together with our customizable error handling, this now allows you to +modify the style of API error messages. + +As an example, this allows for the following style of error responses: + + { + "message": "You do not have permission to perform this action.", + "code": "permission_denied" + } + +This is particularly useful with validation errors, which use appropriate +codes to identify differing kinds of failure... + + { + "name": {"message": "This field is required.", "code": "required"}, + "age": {"message": "A valid integer is required.", "code": "invalid"} + } + +## Client upload & download support + +The Python `coreapi` client library and the Core API command line tool both +now fully support file [uploads][uploads] and [downloads][downloads]. + +--- + +## Deprecations + +### Generating schemas from Router + +The router arguments for generating a schema view, such as `schema_title`, +are now pending deprecation. + +Instead of using `DefaultRouter(schema_title='Example API')`, you should use +the `get_schema_view()` function, and include the view in your URL conf. + +Make sure to include the view before your router urls. For example: + + from rest_framework.schemas import get_schema_view + from my_project.routers import router + + schema_view = get_schema_view(title='Example API') + + urlpatterns = [ + url('^$', schema_view), + url(r'^', include(router.urls)), + ] + +### Schema path representations + +The `'pk'` identifier in schema paths is now mapped onto the actually model field +name by default. This will typically be `'id'`. + +This gives a better external representation for schemas, with less implementation +detail being exposed. It also reflects the behaviour of using a ModelSerializer +class with `fields = '__all__'`. + +You can revert to the previous behaviour by setting `'SCHEMA_COERCE_PATH_PK': False` +in the REST framework settings. + +### Schema action name representations + +The internal `retrieve()` and `destroy()` method names are now coerced to an +external representation of `read` and `delete`. + +You can revert to the previous behaviour by setting `'SCHEMA_COERCE_METHOD_NAMES': {}` +in the REST framework settings. + +### DjangoFilterBackend + +The functionality of the built-in `DjangoFilterBackend` is now completely +included by the `django-filter` package. + +You should change your imports and REST framework filter settings as follows: + +* `rest_framework.filters.DjangoFilterBackend` becomes `django_filters.rest_framework.DjangoFilterBackend`. +* `rest_framework.filters.FilterSet` becomes `django_filters.rest_framework.FilterSet`. + +The existing imports will continue to work but are now pending deprecation. + +### CoreJSON media type + +The media type for `CoreJSON` is now `application/json+coreapi`, rather than +the previous `application/vnd.json+coreapi`. This brings it more into line with +other custom media types, such as those used by Swagger and RAML. + +The clients currently accept either media type. The old style-media type will +be deprecated at a later date. + +### ModelSerializer 'fields' and 'exclude' + +ModelSerializer and HyperlinkedModelSerializer must include either a fields +option, or an exclude option. The fields = '__all__' shortcut may be used to +explicitly include all fields. + +Failing to set either `fields` or `exclude` raised a pending deprecation warning +in version 3.3 and raised a deprecation warning in 3.4. Its usage is now mandatory. + +--- + +[sponsors]: https://fund.django-rest-framework.org/topics/funding/#our-sponsors +[funding]: funding.md +[uploads]: http://core-api.github.io/python-client/api-guide/utils/#file +[downloads]: http://core-api.github.io/python-client/api-guide/codecs/#downloadcodec +[schema-generation-api]: ../api-guide/schemas/#schemagenerator +[schema-docs]: ../api-guide/schemas/#schemas-as-documentation +[schema-view]: ../api-guide/schemas/#the-get_schema_view-shortcut +[django-rest-raml]: https://github.com/tomchristie/django-rest-raml +[raml-image]: ../img/raml.png +[raml-codec]: https://github.com/core-api/python-raml-codec diff --git a/docs/topics/api-clients.md b/docs/topics/api-clients.md index f17f5e4d4..c12551aa6 100644 --- a/docs/topics/api-clients.md +++ b/docs/topics/api-clients.md @@ -257,7 +257,7 @@ Codecs are responsible for encoding or decoding Documents. The decoding process is used by a client to take a bytestring of an API schema definition, and returning the Core API `Document` that represents that interface. -A codec should be associated with a particular media type, such as **TODO**. +A codec should be associated with a particular media type, such as `'application/coreapi+json'`. This media type is used by the server in the response `Content-Type` header, in order to indicate what kind of data is being returned in the response. diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 446abdd14..30244f6d2 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -38,6 +38,14 @@ You can determine your currently installed version using `pip freeze`: --- +## 3.5.x series + +### 3.5.0 + +**Date**: [20th October 2016][3.5.0-milestone] + +--- + ## 3.4.x series ### 3.4.7 @@ -596,6 +604,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [3.4.5-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.5+Release%22 [3.4.6-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.6+Release%22 [3.4.7-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.7+Release%22 +[3.5.0-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.0+Release%22 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 diff --git a/mkdocs.yml b/mkdocs.yml index 0b89988b1..01c59caaa 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -66,6 +66,7 @@ pages: - '3.2 Announcement': 'topics/3.2-announcement.md' - '3.3 Announcement': 'topics/3.3-announcement.md' - '3.4 Announcement': 'topics/3.4-announcement.md' + - '3.5 Announcement': 'topics/3.5-announcement.md' - 'Kickstarter Announcement': 'topics/kickstarter-announcement.md' - 'Mozilla Grant': 'topics/mozilla-grant.md' - 'Funding': 'topics/funding.md' From c6f1686571026d1f2f3bb6bf9d73ca9a61b93182 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 20 Oct 2016 16:26:56 +0100 Subject: [PATCH 288/457] Remove erronous file [ci skip] --- schema-support | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 schema-support diff --git a/schema-support b/schema-support deleted file mode 100644 index e69de29bb..000000000 From 1aa6dff0b54e39d1fe7a0c7058f6f74ee01475fd Mon Sep 17 00:00:00 2001 From: Maxime Lorant Date: Thu, 20 Oct 2016 17:47:59 +0200 Subject: [PATCH 289/457] Fix code formatting missing in 3.5 announcement (#4597) ... in section ModelSerializer 'fields' and 'exclude' --- docs/topics/3.5-announcement.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/topics/3.5-announcement.md b/docs/topics/3.5-announcement.md index 2ed8adf8e..ea50b2418 100644 --- a/docs/topics/3.5-announcement.md +++ b/docs/topics/3.5-announcement.md @@ -246,7 +246,7 @@ be deprecated at a later date. ### ModelSerializer 'fields' and 'exclude' ModelSerializer and HyperlinkedModelSerializer must include either a fields -option, or an exclude option. The fields = '__all__' shortcut may be used to +option, or an exclude option. The `fields = '__all__'` shortcut may be used to explicitly include all fields. Failing to set either `fields` or `exclude` raised a pending deprecation warning From 856f086ce35e1b8e2abda16c860371543a8b12f2 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Oct 2016 14:42:42 +0100 Subject: [PATCH 290/457] Remove broken wheel check in setup.py [ci skip] --- setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.py b/setup.py index 86870489f..ca62366ed 100755 --- a/setup.py +++ b/setup.py @@ -61,9 +61,6 @@ if sys.argv[-1] == 'publish': import pypandoc except ImportError: print("pypandoc not installed.\nUse `pip install pypandoc`.\nExiting.") - if os.system("pip freeze | grep wheel"): - print("wheel not installed.\nUse `pip install wheel`.\nExiting.") - sys.exit() if os.system("pip freeze | grep twine"): print("twine not installed.\nUse `pip install twine`.\nExiting.") sys.exit() From e3686aca93a8224280b38b7669342bcadb24176e Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Oct 2016 14:47:26 +0100 Subject: [PATCH 291/457] Don't use bare 'raise'. [ci skip] --- 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 a8710c7a0..07575fba7 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -445,7 +445,7 @@ class APIView(View): renderer_format = getattr(request.accepted_renderer, 'format') use_plaintext_traceback = renderer_format not in ('html', 'api', 'admin') request.force_plaintext_errors(use_plaintext_traceback) - raise + raise exc # Note: Views are made CSRF exempt from within `as_view` as to prevent # accidental removal of this exemption in cases where `dispatch` needs to From 0b346e94b1ed3016e79dd39be3ee48691cecb035 Mon Sep 17 00:00:00 2001 From: Lukasz Karolewski Date: Fri, 21 Oct 2016 07:00:25 -0700 Subject: [PATCH 292/457] changing order of imports (#4601) when using with django-filter and rest_framework_swagger need to import coreapi before django-filter as django filter tries to load rest_framework.coreapi which is undefined at this point --- rest_framework/compat.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 7ec39ba63..2d6e7843c 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -170,6 +170,16 @@ except ImportError: JSONField = None +# coreapi is optional (Note that uritemplate is a dependency of coreapi) +try: + import coreapi + import uritemplate +except (ImportError, SyntaxError): + # SyntaxError is possible under python 3.2 + coreapi = None + uritemplate = None + + # django-filter is optional try: import django_filters @@ -184,16 +194,6 @@ except ImportError: crispy_forms = None -# coreapi is optional (Note that uritemplate is a dependency of coreapi) -try: - import coreapi - import uritemplate -except (ImportError, SyntaxError): - # SyntaxError is possible under python 3.2 - coreapi = None - uritemplate = None - - # requests is optional try: import requests From f1bdce17b547859a6d59e5c0fe85b9c46d061f44 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Oct 2016 15:21:23 +0100 Subject: [PATCH 293/457] Fix for case of ListSerializer with single item (#4609) --- rest_framework/serializers.py | 4 ++-- tests/test_serializer.py | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 098c3cd23..39987cd07 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -507,7 +507,7 @@ class Serializer(BaseSerializer): @property def errors(self): ret = super(Serializer, self).errors - if isinstance(ret, list) and len(ret) == 1 and ret[0].code == 'null': + if isinstance(ret, list) and len(ret) == 1 and getattr(ret[0], 'code', None) == 'null': # Edge case. Provide a more descriptive error than # "this field may not be null", when no data is passed. detail = ErrorDetail('No data provided', code='null') @@ -705,7 +705,7 @@ class ListSerializer(BaseSerializer): @property def errors(self): ret = super(ListSerializer, self).errors - if isinstance(ret, list) and len(ret) == 1 and ret[0].code == 'null': + if isinstance(ret, list) and len(ret) == 1 and getattr(ret[0], 'code', None) == 'null': # Edge case. Provide a more descriptive error than # "this field may not be null", when no data is passed. detail = ErrorDetail('No data provided', code='null') diff --git a/tests/test_serializer.py b/tests/test_serializer.py index a2817f6a4..32be39faa 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -357,3 +357,16 @@ class TestSerializerValidationWithCompiledRegexField: assert serializer.is_valid() assert serializer.validated_data == {'name': '2'} assert serializer.errors == {} + + +class Test4606Regression: + def setup(self): + class ExampleSerializer(serializers.Serializer): + name = serializers.CharField(required=True) + choices = serializers.CharField(required=True) + self.Serializer = ExampleSerializer + + def test_4606_regression(self): + serializer = self.Serializer(data=[{"name": "liz"}], many=True) + with pytest.raises(serializers.ValidationError): + serializer.is_valid(raise_exception=True) From d647d37a99df3673000f17f7d565a8dab0770805 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Oct 2016 15:45:28 +0100 Subject: [PATCH 294/457] Fix Accept header in tutorial. Closes #4604. [ci skip] --- docs/tutorial/7-schemas-and-client-libraries.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial/7-schemas-and-client-libraries.md b/docs/tutorial/7-schemas-and-client-libraries.md index 705b79da6..eb1982955 100644 --- a/docs/tutorial/7-schemas-and-client-libraries.md +++ b/docs/tutorial/7-schemas-and-client-libraries.md @@ -53,10 +53,10 @@ representation become available as an option. We can also request the schema from the command line, by specifying the desired content type in the `Accept` header. - $ http http://127.0.0.1:8000/schema/ Accept:application/vnd.coreapi+json + $ http http://127.0.0.1:8000/schema/ Accept:application/coreapi+json HTTP/1.0 200 OK Allow: GET, HEAD, OPTIONS - Content-Type: application/vnd.coreapi+json + Content-Type: application/coreapi+json { "_meta": { From 0fe0e1e7038e74f071e93a77bd5900dc191b086d Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Oct 2016 16:59:34 +0100 Subject: [PATCH 295/457] Fix schema base paths (#4611) --- rest_framework/schemas.py | 15 +++++++++++++-- tests/test_schemas.py | 11 +++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index af861426c..9b9984699 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -1,4 +1,3 @@ -import os import re from collections import OrderedDict from importlib import import_module @@ -37,6 +36,18 @@ types_lookup = ClassLookupDict({ }) +def common_path(paths): + split_paths = [path.strip('/').split('/') for path in paths] + s1 = min(split_paths) + s2 = max(split_paths) + common = s1 + for i, c in enumerate(s1): + if c != s2[i]: + common = s1[:i] + break + return '/' + '/'.join(common) + + def get_pk_name(model): meta = model._meta.concrete_model._meta return _get_pk(meta).name @@ -292,7 +303,7 @@ class SchemaGenerator(object): # one URL that doesn't have a path prefix. return '/' prefixes.append('/' + prefix + '/') - return os.path.commonprefix(prefixes) + return common_path(prefixes) def create_view(self, callback, method, request=None): """ diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 7188087c4..80b456ea0 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -335,3 +335,14 @@ class TestSchemaGeneratorNotAtRoot(TestCase): } ) self.assertEqual(schema, expected) + + +@unittest.skipUnless(coreapi, 'coreapi is not installed') +class Test4605Regression(TestCase): + def test_4605_regression(self): + generator = SchemaGenerator() + prefix = generator.determine_path_prefix([ + '/api/v1/items/', + '/auth/convert-token/' + ]) + assert prefix == '/' From 30bf9df5d0dc983d180000e413ec2254d7946ff8 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Oct 2016 16:59:43 +0100 Subject: [PATCH 296/457] Fix guardian import (#4612) --- rest_framework/compat.py | 1 - rest_framework/filters.py | 7 ++++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 2d6e7843c..b0e076203 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -207,7 +207,6 @@ guardian = None try: if 'guardian' in settings.INSTALLED_APPS: import guardian - import guardian.shortcuts # Fixes #1624 except ImportError: pass diff --git a/rest_framework/filters.py b/rest_framework/filters.py index f55297b39..47d9a0342 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -289,6 +289,11 @@ class DjangoObjectPermissionsFilter(BaseFilterBackend): perm_format = '%(app_label)s.view_%(model_name)s' def filter_queryset(self, request, queryset, view): + # We want to defer this import until run-time, rather than import-time. + # See https://github.com/tomchristie/django-rest-framework/issues/4608 + # (Also see #1624 for why we need to make this import explicitly) + from guardian.shortcuts import get_objects_for_user + extra = {} user = request.user model_cls = queryset.model @@ -302,4 +307,4 @@ class DjangoObjectPermissionsFilter(BaseFilterBackend): extra = {'accept_global_perms': False} else: extra = {} - return guardian.shortcuts.get_objects_for_user(user, permission, queryset, **extra) + return get_objects_for_user(user, permission, queryset, **extra) From 3b39d2d13a227b1f159663530ad754b183b7225c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 21 Oct 2016 17:10:38 +0100 Subject: [PATCH 297/457] Version 3.5.1 [ci skip] --- docs/topics/release-notes.md | 21 +++++++++++++++++++++ rest_framework/__init__.py | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 30244f6d2..3d3935684 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,15 @@ You can determine your currently installed version using `pip freeze`: ## 3.5.x series +### 3.5.1 + +**Date**: [21st October 2016][3.5.1-milestone] + +* Make `rest_framework/compat.py` imports. ([#4612][gh4612], [#4608][gh4608], [#4601][gh4601]) +* Fix bug in schema base path generation. ([#4611][gh4611], [#4605][gh4605]) +* Fix broken case of ListSerializer with single item. ([#4609][gh4609], [#4606][gh4606]) +* Remove bare `raise` for Python 3.5 compat. ([#4600][gh4600]) + ### 3.5.0 **Date**: [20th October 2016][3.5.0-milestone] @@ -605,6 +614,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [3.4.6-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.6+Release%22 [3.4.7-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.7+Release%22 [3.5.0-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.0+Release%22 +[3.5.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.1+Release%22 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 @@ -1146,3 +1156,14 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh4465]: https://github.com/tomchristie/django-rest-framework/issues/4465 [gh4462]: https://github.com/tomchristie/django-rest-framework/issues/4462 [gh4458]: https://github.com/tomchristie/django-rest-framework/issues/4458 + + + +[gh4612]: https://github.com/tomchristie/django-rest-framework/issues/4612 +[gh4608]: https://github.com/tomchristie/django-rest-framework/issues/4608 +[gh4601]: https://github.com/tomchristie/django-rest-framework/issues/4601 +[gh4611]: https://github.com/tomchristie/django-rest-framework/issues/4611 +[gh4605]: https://github.com/tomchristie/django-rest-framework/issues/4605 +[gh4609]: https://github.com/tomchristie/django-rest-framework/issues/4609 +[gh4606]: https://github.com/tomchristie/django-rest-framework/issues/4606 +[gh4600]: https://github.com/tomchristie/django-rest-framework/issues/4600 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 68e96703f..0a520bf80 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.5.0' +__version__ = '3.5.1' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2016 Tom Christie' From 8ac524915ca3759d7728206da101b63c6f33b6d3 Mon Sep 17 00:00:00 2001 From: Mads Jensen Date: Sat, 22 Oct 2016 17:37:23 +0200 Subject: [PATCH 298/457] added on_delete=models.CASCADE to models.ForeignKey in the documentation (#4614) --- docs/api-guide/relations.md | 6 +++--- docs/tutorial/4-authentication-and-permissions.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index aeefae8b1..aabe49412 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -39,7 +39,7 @@ In order to explain the various types of relational fields, we'll use a couple o artist = models.CharField(max_length=100) class Track(models.Model): - album = models.ForeignKey(Album, related_name='tracks') + album = models.ForeignKey(Album, related_name='tracks', on_delete=models.CASCADE) order = models.IntegerField() title = models.CharField(max_length=100) duration = models.IntegerField() @@ -484,7 +484,7 @@ Note that reverse relationships are not automatically included by the `ModelSeri You'll normally want to ensure that you've set an appropriate `related_name` argument on the relationship, that you can use as the field name. For example: class Track(models.Model): - album = models.ForeignKey(Album, related_name='tracks') + album = models.ForeignKey(Album, related_name='tracks', on_delete=models.CASCADE) ... If you have not set a related name for the reverse relationship, you'll need to use the automatically generated related name in the `fields` argument. For example: @@ -508,7 +508,7 @@ For example, given the following model for a tag, which has a generic relationsh See: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/ """ tag_name = models.SlugField() - content_type = models.ForeignKey(ContentType) + content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_id = models.PositiveIntegerField() tagged_object = GenericForeignKey('content_type', 'object_id') diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 098194c29..43ccf9186 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -14,7 +14,7 @@ First, let's add a couple of fields. One of those fields will be used to repres Add the following two fields to the `Snippet` model in `models.py`. - owner = models.ForeignKey('auth.User', related_name='snippets') + owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE) highlighted = models.TextField() We'd also need to make sure that when the model is saved, that we populate the highlighted field, using the `pygments` code highlighting library. From 72dc6d1d5cae32353e87b7724e666afbd8f04312 Mon Sep 17 00:00:00 2001 From: Phil Krylov Date: Sun, 23 Oct 2016 04:36:36 +0300 Subject: [PATCH 299/457] Add `drf-proxy-pagination` reference to Pagination docs --- docs/api-guide/pagination.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index f990128c5..f82614eca 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -321,9 +321,14 @@ The following third party packages are also available. 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. +## drf-proxy-pagination + +The [`drf-proxy-pagination` package][drf-proxy-pagination] includes a `ProxyPagination` class which allows to choose pagination class with a query parameter. + [cite]: https://docs.djangoproject.com/en/dev/topics/pagination/ [github-link-pagination]: https://developer.github.com/guides/traversing-with-pagination/ [link-header]: ../img/link-header-pagination.png [drf-extensions]: http://chibisov.github.io/drf-extensions/docs/ [paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin +[drf-proxy-pagination]: https://github.com/tuffnatty/drf-proxy-pagination [disqus-cursor-api]: http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api From eafc9a2393139445815feafe0392c170f1172532 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Tue, 25 Oct 2016 15:47:24 -0400 Subject: [PATCH 300/457] Fix is_simple_callable with variable args, kwargs (#4622) --- rest_framework/fields.py | 9 +++++++-- tests/test_fields.py | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index e48285005..f75fcfe05 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -54,12 +54,17 @@ if six.PY3: """ True if the object is a callable that takes no arguments. """ - if not callable(obj): + if not (inspect.isfunction(obj) or inspect.ismethod(obj)): return False sig = inspect.signature(obj) params = sig.parameters.values() - return all(param.default != param.empty for param in params) + return all( + param.kind == param.VAR_POSITIONAL or + param.kind == param.VAR_KEYWORD or + param.default != param.empty + for param in params + ) else: def is_simple_callable(obj): diff --git a/tests/test_fields.py b/tests/test_fields.py index 6fea249ba..92030e3ca 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -37,6 +37,9 @@ class TestIsSimpleCallable: def valid_kwargs(self, param='value'): pass + def valid_vargs_kwargs(self, *args, **kwargs): + pass + def invalid(self, param): pass @@ -45,11 +48,13 @@ class TestIsSimpleCallable: # unbound methods assert not is_simple_callable(Foo.valid) assert not is_simple_callable(Foo.valid_kwargs) + assert not is_simple_callable(Foo.valid_vargs_kwargs) assert not is_simple_callable(Foo.invalid) # bound methods assert is_simple_callable(Foo().valid) assert is_simple_callable(Foo().valid_kwargs) + assert is_simple_callable(Foo().valid_vargs_kwargs) assert not is_simple_callable(Foo().invalid) def test_function(self): @@ -59,13 +64,31 @@ class TestIsSimpleCallable: def valid(param='value', param2='value'): pass + def valid_vargs_kwargs(*args, **kwargs): + pass + def invalid(param, param2='value'): pass assert is_simple_callable(simple) assert is_simple_callable(valid) + assert is_simple_callable(valid_vargs_kwargs) assert not is_simple_callable(invalid) + def test_4602_regression(self): + from django.db import models + + class ChoiceModel(models.Model): + choice_field = models.CharField( + max_length=1, default='a', + choices=(('a', 'A'), ('b', 'B')), + ) + + class Meta: + app_label = 'tests' + + assert is_simple_callable(ChoiceModel().get_choice_field_display) + @unittest.skipUnless(typings, 'requires python 3.5') def test_type_annotation(self): # The annotation will otherwise raise a syntax error in python < 3.5 From 46f837a9d13d5f02fabfa49aef33c16c561a68cf Mon Sep 17 00:00:00 2001 From: Josep Cugat Date: Fri, 28 Oct 2016 13:05:32 +0200 Subject: [PATCH 301/457] Fix APIException full_details() typo in documentation (#4633) APIException has a get_full_details() method but the documentation refers to full_details(). --- docs/api-guide/exceptions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index f0f178d92..df8cad42d 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -119,7 +119,7 @@ The available attributes and methods are: * `.detail` - Return the textual description of the error. * `.get_codes()` - Return the code identifier of the error. -* `.full_details()` - Return both the textual description and the code identifier. +* `.get_full_details()` - Return both the textual description and the code identifier. In most cases the error detail will be a simple item: @@ -127,7 +127,7 @@ In most cases the error detail will be a simple item: You do not have permission to perform this action. >>> print(exc.get_codes()) permission_denied - >>> print(exc.full_details()) + >>> print(exc.get_full_details()) {'message':'You do not have permission to perform this action.','code':'permission_denied'} In the case of validation errors the error detail will be either a list or From 895c67c9a22eff1057703a8c578c4833f63a9d70 Mon Sep 17 00:00:00 2001 From: Alex Kahan Date: Mon, 31 Oct 2016 16:41:54 -0400 Subject: [PATCH 302/457] Fixes #4532 (#4636) --- .../rest_framework/admin/dict_value.html | 11 +++ tests/test_templatetags.py | 75 +++++++++++++++---- 2 files changed, 73 insertions(+), 13 deletions(-) diff --git a/rest_framework/templates/rest_framework/admin/dict_value.html b/rest_framework/templates/rest_framework/admin/dict_value.html index e69de29bb..3392c901b 100644 --- a/rest_framework/templates/rest_framework/admin/dict_value.html +++ b/rest_framework/templates/rest_framework/admin/dict_value.html @@ -0,0 +1,11 @@ +{% load rest_framework %} + + + {% for key, value in value.items %} + + + + + {% endfor %} + +
{{ key|format_value }}{{ value|format_value }}
diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py index 28390320b..cac1abf50 100644 --- a/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -41,6 +41,9 @@ class TemplateTagTests(TestCase): self.assertEqual(format_value(None), 'null') def test_format_value_hyperlink(self): + """ + Tests format_value with a URL + """ url = 'http://url.com' name = 'name_of_url' hyperlink = Hyperlink(url, name) @@ -54,6 +57,25 @@ class TemplateTagTests(TestCase): self.assertEqual(format_value(list_items), '\n item1, item2, item3\n') self.assertEqual(format_value([]), '\n\n') + def test_format_value_dict(self): + """ + Tests format_value with a dict + """ + test_dict = {'a': 'b'} + expected_dict_format = """ + + + + + + + +
ab
""" + self.assertEqual( + format_html(format_value(test_dict)), + format_html(expected_dict_format) + ) + def test_format_value_table(self): """ Tests format_value with a list of lists/dicts @@ -84,20 +106,47 @@ class TemplateTagTests(TestCase): expected_dict_format = """ - - 0 - - - - 1 - - - - 2 - - + + 0 + + + + + item1 + value1 + + + + + + + 1 + + + + + item2 + value2 + + + + + + + 2 + + + + + item3 + value3 + + + + + - """ + """ list_of_dicts = [{'item1': 'value1'}, {'item2': 'value2'}, {'item3': 'value3'}] self.assertEqual( From 7eb6cdca0080e0f321e660c48fa1d4ac5a6e7dab Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 1 Nov 2016 10:22:30 +0000 Subject: [PATCH 303/457] Don't lose exception info (#4638) --- 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 07575fba7..a8710c7a0 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -445,7 +445,7 @@ class APIView(View): renderer_format = getattr(request.accepted_renderer, 'format') use_plaintext_traceback = renderer_format not in ('html', 'api', 'admin') request.force_plaintext_errors(use_plaintext_traceback) - raise exc + raise # Note: Views are made CSRF exempt from within `as_view` as to prevent # accidental removal of this exemption in cases where `dispatch` needs to From 5c54b227c11688f12ce17f8aa9575e4ab52463d3 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 1 Nov 2016 10:24:53 +0000 Subject: [PATCH 304/457] Drop redundant requests adapter (#4639) --- rest_framework/test.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/rest_framework/test.py b/rest_framework/test.py index 16b1b4cd5..241f94c91 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -106,15 +106,6 @@ if requests is not None: def close(self): pass - class NoExternalRequestsAdapter(requests.adapters.HTTPAdapter): - def send(self, request, *args, **kwargs): - msg = ( - 'RequestsClient refusing to make an outgoing network request ' - 'to "%s". Only "testserver" or hostnames in your ALLOWED_HOSTS ' - 'setting are valid.' % request.url - ) - raise RuntimeError(msg) - class RequestsClient(requests.Session): def __init__(self, *args, **kwargs): super(RequestsClient, self).__init__(*args, **kwargs) From d92b24a0b74f681a538f6faff59bb00c6cd447e2 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Tue, 1 Nov 2016 06:27:11 -0400 Subject: [PATCH 305/457] Make serializer fields import explicit (#4628) --- rest_framework/serializers.py | 29 +++++++++++++++++++--- tests/test_serializer.py | 45 ++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 39987cd07..1bdcd12c3 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -12,18 +12,27 @@ response content is handled by parsers and renderers. """ from __future__ import unicode_literals +import copy +import inspect import traceback +from collections import OrderedDict +from django.core.exceptions import ValidationError as DjangoValidationError +from django.core.exceptions import ImproperlyConfigured from django.db import models from django.db.models import DurationField as ModelDurationField from django.db.models.fields import Field as DjangoModelField from django.db.models.fields import FieldDoesNotExist +from django.utils import six, timezone from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ from rest_framework.compat import JSONField as ModelJSONField from rest_framework.compat import postgres_fields, set_many, unicode_to_repr -from rest_framework.utils import model_meta +from rest_framework.exceptions import ErrorDetail, ValidationError +from rest_framework.fields import get_error_detail, set_value +from rest_framework.settings import api_settings +from rest_framework.utils import html, model_meta, representation from rest_framework.utils.field_mapping import ( ClassLookupDict, get_field_kwargs, get_nested_relation_kwargs, get_relation_kwargs, get_url_kwargs @@ -42,9 +51,23 @@ from rest_framework.validators import ( # # This helps keep the separation between model fields, form fields, and # serializer fields more explicit. +from rest_framework.fields import ( # NOQA # isort:skip + BooleanField, CharField, ChoiceField, DateField, DateTimeField, DecimalField, + DictField, DurationField, EmailField, Field, FileField, FilePathField, FloatField, + HiddenField, IPAddressField, ImageField, IntegerField, JSONField, ListField, + ModelField, MultipleChoiceField, NullBooleanField, ReadOnlyField, RegexField, + SerializerMethodField, SlugField, TimeField, URLField, UUIDField, +) +from rest_framework.relations import ( # NOQA # isort:skip + HyperlinkedIdentityField, HyperlinkedRelatedField, ManyRelatedField, + PrimaryKeyRelatedField, RelatedField, SlugRelatedField, StringRelatedField, +) -from rest_framework.fields import * # NOQA # isort:skip -from rest_framework.relations import * # NOQA # isort:skip +# Non-field imports, but public API +from rest_framework.fields import ( # NOQA # isort:skip + CreateOnlyDefault, CurrentUserDefault, SkipField, empty +) +from rest_framework.relations import Hyperlink, PKOnlyObject # NOQA # isort:skip # We assume that 'validators' are intended for the child serializer, # rather than the parent serializer. diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 32be39faa..8c8b5b163 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -1,17 +1,60 @@ # coding: utf-8 from __future__ import unicode_literals +import inspect import pickle import re import pytest -from rest_framework import serializers +from rest_framework import fields, relations, serializers from rest_framework.compat import unicode_repr +from rest_framework.fields import Field from .utils import MockObject +# Test serializer fields imports. +# ------------------------------- + +class TestFieldImports: + def is_field(self, name, value): + return ( + isinstance(value, type) and + issubclass(value, Field) and + not name.startswith('_') + ) + + def test_fields(self): + msg = "Expected `fields.%s` to be imported in `serializers`" + field_classes = [ + key for key, value + in inspect.getmembers(fields) + if self.is_field(key, value) + ] + + # sanity check + assert 'Field' in field_classes + assert 'BooleanField' in field_classes + + for field in field_classes: + assert hasattr(serializers, field), msg % field + + def test_relations(self): + msg = "Expected `relations.%s` to be imported in `serializers`" + field_classes = [ + key for key, value + in inspect.getmembers(relations) + if self.is_field(key, value) + ] + + # sanity check + assert 'RelatedField' in field_classes + + for field in field_classes: + assert hasattr(serializers, field), msg % field + + # Tests for core functionality. # ----------------------------- From 98df932194722d6fc81becedc55eb695a32f925f Mon Sep 17 00:00:00 2001 From: Kieran Spear Date: Tue, 1 Nov 2016 18:30:17 +0800 Subject: [PATCH 306/457] Fix FilterSet proxy (#4620) --- rest_framework/filters.py | 35 ++++++++++++++++++++++++++--------- tests/test_filters.py | 23 +++++++++++++++++++++++ 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 47d9a0342..531531efc 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -37,15 +37,32 @@ class BaseFilterBackend(object): return [] -class FilterSet(object): - def __new__(cls, *args, **kwargs): - warnings.warn( - "The built in 'rest_framework.filters.FilterSet' is pending deprecation. " - "You should use 'django_filters.rest_framework.FilterSet' instead.", - PendingDeprecationWarning - ) - from django_filters.rest_framework import FilterSet - return FilterSet(*args, **kwargs) +if django_filters: + from django_filters.filterset import FilterSetMetaclass as DFFilterSetMetaclass + from django_filters.rest_framework.filterset import FilterSet as DFFilterSet + + class FilterSetMetaclass(DFFilterSetMetaclass): + def __new__(cls, name, bases, attrs): + warnings.warn( + "The built in 'rest_framework.filters.FilterSet' is pending deprecation. " + "You should use 'django_filters.rest_framework.FilterSet' instead.", + PendingDeprecationWarning + ) + return super(FilterSetMetaclass, cls).__new__(cls, name, bases, attrs) + _BaseFilterSet = DFFilterSet +else: + # Dummy metaclass just so we can give a user-friendly error message. + class FilterSetMetaclass(type): + def __init__(self, name, bases, attrs): + # Assert only on subclasses, so we can define FilterSet below. + if bases != (object,): + assert False, 'django-filter must be installed to use the `FilterSet` class' + super(FilterSetMetaclass, self).__init__(name, bases, attrs) + _BaseFilterSet = object + + +class FilterSet(six.with_metaclass(FilterSetMetaclass, _BaseFilterSet)): + pass class DjangoFilterBackend(BaseFilterBackend): diff --git a/tests/test_filters.py b/tests/test_filters.py index 9795230d6..12fb85895 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -79,12 +79,23 @@ if django_filters: model = BaseFilterableItem fields = '__all__' + # Test the same filter using the deprecated internal FilterSet class. + class BaseFilterableItemFilterWithProxy(filters.FilterSet): + text = django_filters.CharFilter() + + class Meta: + model = BaseFilterableItem + fields = '__all__' + class BaseFilterableItemFilterRootView(generics.ListCreateAPIView): queryset = FilterableItem.objects.all() serializer_class = FilterableItemSerializer filter_class = BaseFilterableItemFilter filter_backends = (filters.DjangoFilterBackend,) + class BaseFilterableItemFilterWithProxyRootView(BaseFilterableItemFilterRootView): + filter_class = BaseFilterableItemFilterWithProxy + # Regression test for #814 class FilterFieldsQuerysetView(generics.ListCreateAPIView): queryset = FilterableItem.objects.all() @@ -296,6 +307,18 @@ class IntegrationTestFiltering(CommonFilteringTestCase): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data), 1) + @unittest.skipUnless(django_filters, 'django-filter not installed') + def test_base_model_filter_with_proxy(self): + """ + The `get_filter_class` model checks should allow base model filters. + """ + view = BaseFilterableItemFilterWithProxyRootView.as_view() + + request = factory.get('/?text=aaa') + response = view(request).render() + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(len(response.data), 1) + @unittest.skipUnless(django_filters, 'django-filter not installed') def test_unknown_filter(self): """ From 97d848413e5e13e9de83b7e840fc719b9ec0d9c7 Mon Sep 17 00:00:00 2001 From: Nicolas Delaby Date: Tue, 1 Nov 2016 11:38:56 +0100 Subject: [PATCH 307/457] Fix support of get_full_details() for Throttled exceptions (#4627) Since `str` objects are immutable, appending to existing `str` creates in fact a new `str` instance. Thus `ErrorDetail.detail.code` attribute is lost after `str` concatenation operation. --- rest_framework/exceptions.py | 20 ++++++++++---------- tests/test_exceptions.py | 20 +++++++++++++++++++- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/rest_framework/exceptions.py b/rest_framework/exceptions.py index e41655fef..e84074a07 100644 --- a/rest_framework/exceptions.py +++ b/rest_framework/exceptions.py @@ -210,14 +210,14 @@ class Throttled(APIException): default_code = 'throttled' def __init__(self, wait=None, detail=None, code=None): + if detail is None: + detail = force_text(self.default_detail) + if wait is not None: + wait = math.ceil(wait) + detail = ' '.join(( + detail, + force_text(ungettext(self.extra_detail_singular.format(wait=wait), + self.extra_detail_plural.format(wait=wait), + wait)))) + self.wait = wait super(Throttled, self).__init__(detail, code) - - if wait is None: - self.wait = None - else: - self.wait = math.ceil(wait) - self.detail += ' ' + force_text(ungettext( - self.extra_detail_singular.format(wait=self.wait), - self.extra_detail_plural.format(wait=self.wait), - self.wait - )) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 29703cb77..f1d172211 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -1,9 +1,12 @@ from __future__ import unicode_literals from django.test import TestCase +from django.utils import six from django.utils.translation import ugettext_lazy as _ -from rest_framework.exceptions import ErrorDetail, _get_error_details +from rest_framework.exceptions import ( + ErrorDetail, Throttled, _get_error_details +) class ExceptionTestCase(TestCase): @@ -39,3 +42,18 @@ class ExceptionTestCase(TestCase): _get_error_details([[lazy_example]])[0][0], ErrorDetail ) + + def test_get_full_details_with_throttling(self): + exception = Throttled() + assert exception.get_full_details() == { + 'message': 'Request was throttled.', 'code': 'throttled'} + + exception = Throttled(wait=2) + assert exception.get_full_details() == { + 'message': 'Request was throttled. Expected available in {} seconds.'.format(2 if six.PY3 else 2.), + 'code': 'throttled'} + + exception = Throttled(wait=2, detail='Slow down!') + assert exception.get_full_details() == { + 'message': 'Slow down! Expected available in {} seconds.'.format(2 if six.PY3 else 2.), + 'code': 'throttled'} From 70385711572e7ea141644f349b7180c68b4c15d2 Mon Sep 17 00:00:00 2001 From: Kennedy Mwenja Date: Tue, 1 Nov 2016 13:42:01 +0300 Subject: [PATCH 308/457] Enable cursor pagination of value querysets. (#4569) To do `GROUP_BY` queries in django requires one to use `.values()` eg this groups posts by user getting a count of posts per user. ``` Posts.objects.order_by('user').values('user').annotate(post_count=Count('post')) ``` This would produce a value queryset which serializes its result objects as dictionaries while `CursorPagination` requires a queryset with result objects that are model instances. This commit enables cursor pagination for value querysets. - had to mangle the tests a bit to test it out. They might need some refactoring. - tried the same for `.values_list()` but it turned out to be trickier than I expected since you have to use tuple indexes. --- rest_framework/pagination.py | 6 +- tests/test_pagination.py | 221 ++++++++++++++++++++++------------- 2 files changed, 147 insertions(+), 80 deletions(-) diff --git a/rest_framework/pagination.py b/rest_framework/pagination.py index 708da29cd..8ccdc342c 100644 --- a/rest_framework/pagination.py +++ b/rest_framework/pagination.py @@ -711,7 +711,11 @@ class CursorPagination(BasePagination): return replace_query_param(self.base_url, self.cursor_query_param, encoded) def _get_position_from_instance(self, instance, ordering): - attr = getattr(instance, ordering[0].lstrip('-')) + field_name = ordering[0].lstrip('-') + if isinstance(instance, dict): + attr = instance[field_name] + else: + attr = getattr(instance, field_name) return six.text_type(attr) def get_paginated_response(self, data): diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 170d95899..9f2e1c57c 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -3,6 +3,8 @@ from __future__ import unicode_literals import pytest from django.core.paginator import Paginator as DjangoPaginator +from django.db import models +from django.test import TestCase from rest_framework import ( exceptions, filters, generics, pagination, serializers, status @@ -530,85 +532,7 @@ class TestLimitOffset: assert content.get('previous') == prev_url -class TestCursorPagination: - """ - Unit tests for `pagination.CursorPagination`. - """ - - def setup(self): - class MockObject(object): - def __init__(self, idx): - self.created = idx - - class MockQuerySet(object): - def __init__(self, items): - self.items = items - - def filter(self, created__gt=None, created__lt=None): - if created__gt is not None: - return MockQuerySet([ - item for item in self.items - if item.created > int(created__gt) - ]) - - assert created__lt is not None - return MockQuerySet([ - item for item in self.items - if item.created < int(created__lt) - ]) - - def order_by(self, *ordering): - if ordering[0].startswith('-'): - return MockQuerySet(list(reversed(self.items))) - return self - - def __getitem__(self, sliced): - return self.items[sliced] - - class ExamplePagination(pagination.CursorPagination): - page_size = 5 - ordering = 'created' - - self.pagination = ExamplePagination() - self.queryset = MockQuerySet([ - MockObject(idx) for idx in [ - 1, 1, 1, 1, 1, - 1, 2, 3, 4, 4, - 4, 4, 5, 6, 7, - 7, 7, 7, 7, 7, - 7, 7, 7, 8, 9, - 9, 9, 9, 9, 9 - ] - ]) - - def get_pages(self, url): - """ - Given a URL return a tuple of: - - (previous page, current page, next page, previous url, next url) - """ - request = Request(factory.get(url)) - queryset = self.pagination.paginate_queryset(self.queryset, request) - current = [item.created for item in queryset] - - next_url = self.pagination.get_next_link() - previous_url = self.pagination.get_previous_link() - - if next_url is not None: - request = Request(factory.get(next_url)) - queryset = self.pagination.paginate_queryset(self.queryset, request) - next = [item.created for item in queryset] - else: - next = None - - if previous_url is not None: - request = Request(factory.get(previous_url)) - queryset = self.pagination.paginate_queryset(self.queryset, request) - previous = [item.created for item in queryset] - else: - previous = None - - return (previous, current, next, previous_url, next_url) +class CursorPaginationTestsMixin: def test_invalid_cursor(self): request = Request(factory.get('/', {'cursor': '123'})) @@ -703,6 +627,145 @@ class TestCursorPagination: assert isinstance(self.pagination.to_html(), type('')) +class TestCursorPagination(CursorPaginationTestsMixin): + """ + Unit tests for `pagination.CursorPagination`. + """ + + def setup(self): + class MockObject(object): + def __init__(self, idx): + self.created = idx + + class MockQuerySet(object): + def __init__(self, items): + self.items = items + + def filter(self, created__gt=None, created__lt=None): + if created__gt is not None: + return MockQuerySet([ + item for item in self.items + if item.created > int(created__gt) + ]) + + assert created__lt is not None + return MockQuerySet([ + item for item in self.items + if item.created < int(created__lt) + ]) + + def order_by(self, *ordering): + if ordering[0].startswith('-'): + return MockQuerySet(list(reversed(self.items))) + return self + + def __getitem__(self, sliced): + return self.items[sliced] + + class ExamplePagination(pagination.CursorPagination): + page_size = 5 + ordering = 'created' + + self.pagination = ExamplePagination() + self.queryset = MockQuerySet([ + MockObject(idx) for idx in [ + 1, 1, 1, 1, 1, + 1, 2, 3, 4, 4, + 4, 4, 5, 6, 7, + 7, 7, 7, 7, 7, + 7, 7, 7, 8, 9, + 9, 9, 9, 9, 9 + ] + ]) + + def get_pages(self, url): + """ + Given a URL return a tuple of: + + (previous page, current page, next page, previous url, next url) + """ + request = Request(factory.get(url)) + queryset = self.pagination.paginate_queryset(self.queryset, request) + current = [item.created for item in queryset] + + next_url = self.pagination.get_next_link() + previous_url = self.pagination.get_previous_link() + + if next_url is not None: + request = Request(factory.get(next_url)) + queryset = self.pagination.paginate_queryset(self.queryset, request) + next = [item.created for item in queryset] + else: + next = None + + if previous_url is not None: + request = Request(factory.get(previous_url)) + queryset = self.pagination.paginate_queryset(self.queryset, request) + previous = [item.created for item in queryset] + else: + previous = None + + return (previous, current, next, previous_url, next_url) + + +class CursorPaginationModel(models.Model): + created = models.IntegerField() + + +class TestCursorPaginationWithValueQueryset(CursorPaginationTestsMixin, TestCase): + """ + Unit tests for `pagination.CursorPagination` for value querysets. + """ + + def setUp(self): + class ExamplePagination(pagination.CursorPagination): + page_size = 5 + ordering = 'created' + + self.pagination = ExamplePagination() + data = [ + 1, 1, 1, 1, 1, + 1, 2, 3, 4, 4, + 4, 4, 5, 6, 7, + 7, 7, 7, 7, 7, + 7, 7, 7, 8, 9, + 9, 9, 9, 9, 9 + ] + for idx in data: + CursorPaginationModel.objects.create(created=idx) + + self.queryset = CursorPaginationModel.objects.values() + + def get_pages(self, url): + """ + Given a URL return a tuple of: + + (previous page, current page, next page, previous url, next url) + """ + request = Request(factory.get(url)) + queryset = self.pagination.paginate_queryset(self.queryset, request) + current = [item['created'] for item in queryset] + + next_url = self.pagination.get_next_link() + previous_url = self.pagination.get_previous_link() + + if next_url is not None: + request = Request(factory.get(next_url)) + queryset = self.pagination.paginate_queryset(self.queryset, request) + next = [item['created'] for item in queryset] + else: + next = None + + if previous_url is not None: + request = Request(factory.get(previous_url)) + queryset = self.pagination.paginate_queryset(self.queryset, request) + previous = [item['created'] for item in queryset] + else: + previous = None + + return (previous, current, next, previous_url, next_url) + + def test_get_displayed_page_numbers(): """ Test our contextual page display function. From 276ed80fd302184dddb3af01e53d43be4aef15e4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 1 Nov 2016 11:11:34 +0000 Subject: [PATCH 309/457] Support 'on'/'off' literals with BooleanField. Closes #4624 (#4640) --- rest_framework/fields.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index f75fcfe05..13b5145ba 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -644,8 +644,20 @@ class BooleanField(Field): } default_empty_html = False initial = False - TRUE_VALUES = {'t', 'T', 'true', 'True', 'TRUE', '1', 1, True} - FALSE_VALUES = {'f', 'F', 'false', 'False', 'FALSE', '0', 0, 0.0, False} + TRUE_VALUES = { + 't', 'T', + 'true', 'True', 'TRUE', + 'on', 'On', 'ON', + '1', 1, + True + } + FALSE_VALUES = { + 'f', 'F', + 'false', 'False', 'FALSE', + 'off', 'Off', 'OFF', + '0', 0, 0.0, + False + } def __init__(self, **kwargs): assert 'allow_null' not in kwargs, '`allow_null` is not a valid option. Use `NullBooleanField` instead.' From 2bf082a6236cf979d2d125f1f5a60f18509323fc Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Tue, 1 Nov 2016 11:31:20 +0000 Subject: [PATCH 310/457] Version 3.5.2 [ci skip] (#4641) --- docs/topics/release-notes.md | 31 +++++++++++++++++++++++++++++++ rest_framework/__init__.py | 2 +- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 3d3935684..d25a46ba0 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,20 @@ You can determine your currently installed version using `pip freeze`: ## 3.5.x series +### 3.5.2 + +**Date**: [1st November 2016][3.5.2-milestone] + +* Restore exception tracebacks in Python 2.7. ([#4631][gh4631], [#4638][gh4638]) +* Properly display dicts in the admin console. ([#4532][gh4532], [#4636][gh4636]) +* Fix is_simple_callable with variable args, kwargs. ([#4622][gh4622], [#4602][gh4602]) +* Support 'on'/'off' literals with BooleanField. ([#4640][gh4640], [#4624][gh4624]) +* Enable cursor pagination of value querysets. ([#4569][gh4569]) +* Fix support of get_full_details() for Throttled exceptions. ([#4627][gh4627]) +* Fix FilterSet proxy. ([#4620][gh4620]) +* Make serializer fields import explicit. ([#4628][gh4628]) +* Drop redundant requests adapter. ([#4639][gh4639]) + ### 3.5.1 **Date**: [21st October 2016][3.5.1-milestone] @@ -615,6 +629,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [3.4.7-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.4.7+Release%22 [3.5.0-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.0+Release%22 [3.5.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.1+Release%22 +[3.5.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.2+Release%22 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 @@ -1167,3 +1182,19 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh4609]: https://github.com/tomchristie/django-rest-framework/issues/4609 [gh4606]: https://github.com/tomchristie/django-rest-framework/issues/4606 [gh4600]: https://github.com/tomchristie/django-rest-framework/issues/4600 + + + +[gh4631]: https://github.com/tomchristie/django-rest-framework/issues/4631 +[gh4638]: https://github.com/tomchristie/django-rest-framework/issues/4638 +[gh4532]: https://github.com/tomchristie/django-rest-framework/issues/4532 +[gh4636]: https://github.com/tomchristie/django-rest-framework/issues/4636 +[gh4622]: https://github.com/tomchristie/django-rest-framework/issues/4622 +[gh4602]: https://github.com/tomchristie/django-rest-framework/issues/4602 +[gh4640]: https://github.com/tomchristie/django-rest-framework/issues/4640 +[gh4624]: https://github.com/tomchristie/django-rest-framework/issues/4624 +[gh4569]: https://github.com/tomchristie/django-rest-framework/issues/4569 +[gh4627]: https://github.com/tomchristie/django-rest-framework/issues/4627 +[gh4620]: https://github.com/tomchristie/django-rest-framework/issues/4620 +[gh4628]: https://github.com/tomchristie/django-rest-framework/issues/4628 +[gh4639]: https://github.com/tomchristie/django-rest-framework/issues/4639 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 0a520bf80..fe82763a9 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.5.1' +__version__ = '3.5.2' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2016 Tom Christie' From 45e058d7bace860300d3edf537b86996aec15c33 Mon Sep 17 00:00:00 2001 From: Andrzej Pragacz Date: Wed, 2 Nov 2016 10:04:01 +0100 Subject: [PATCH 311/457] Fix unhandled Http404, PermissionDenied in schema generation (#4645) (#4646) --- rest_framework/schemas.py | 4 ++- tests/test_schemas.py | 68 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 9b9984699..9439cb691 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -4,6 +4,8 @@ from importlib import import_module from django.conf import settings from django.contrib.admindocs.views import simplify_regex +from django.core.exceptions import PermissionDenied +from django.http import Http404 from django.utils import six from django.utils.encoding import force_text, smart_text @@ -339,7 +341,7 @@ class SchemaGenerator(object): try: view.check_permissions(view.request) - except exceptions.APIException: + except (exceptions.APIException, Http404, PermissionDenied): return False return True diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 80b456ea0..e196a1f61 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -1,17 +1,22 @@ import unittest from django.conf.urls import include, url +from django.core.exceptions import PermissionDenied +from django.http import Http404 from django.test import TestCase, override_settings from rest_framework import filters, pagination, permissions, serializers from rest_framework.compat import coreapi from rest_framework.decorators import detail_route, list_route +from rest_framework.request import Request from rest_framework.routers import DefaultRouter from rest_framework.schemas import SchemaGenerator, get_schema_view -from rest_framework.test import APIClient +from rest_framework.test import APIClient, APIRequestFactory from rest_framework.views import APIView from rest_framework.viewsets import ModelViewSet +factory = APIRequestFactory() + class MockUser(object): def is_authenticated(self): @@ -215,6 +220,32 @@ class TestRouterGeneratedSchema(TestCase): self.assertEqual(response.data, expected) +class DenyAllUsingHttp404(permissions.BasePermission): + + def has_permission(self, request, view): + raise Http404() + + def has_object_permission(self, request, view, obj): + raise Http404() + + +class DenyAllUsingPermissionDenied(permissions.BasePermission): + + def has_permission(self, request, view): + raise PermissionDenied() + + def has_object_permission(self, request, view, obj): + raise PermissionDenied() + + +class Http404ExampleViewSet(ExampleViewSet): + permission_classes = [DenyAllUsingHttp404] + + +class PermissionDeniedExampleViewSet(ExampleViewSet): + permission_classes = [DenyAllUsingPermissionDenied] + + class ExampleListView(APIView): permission_classes = [permissions.IsAuthenticatedOrReadOnly] @@ -337,6 +368,41 @@ class TestSchemaGeneratorNotAtRoot(TestCase): self.assertEqual(schema, expected) +@unittest.skipUnless(coreapi, 'coreapi is not installed') +class TestSchemaGeneratorWithRestrictedViewSets(TestCase): + def setUp(self): + router = DefaultRouter() + router.register('example1', Http404ExampleViewSet, base_name='example1') + router.register('example2', PermissionDeniedExampleViewSet, base_name='example2') + self.patterns = [ + url('^example/?$', ExampleListView.as_view()), + url(r'^', include(router.urls)) + ] + + def test_schema_for_regular_views(self): + """ + Ensure that schema generation works for ViewSet classes + with permission classes raising exceptions. + """ + generator = SchemaGenerator(title='Example API', patterns=self.patterns) + request = factory.get('/') + schema = generator.get_schema(Request(request)) + expected = coreapi.Document( + url='', + title='Example API', + content={ + 'example': { + 'list': coreapi.Link( + url='/example/', + action='get', + fields=[] + ), + }, + } + ) + self.assertEqual(schema, expected) + + @unittest.skipUnless(coreapi, 'coreapi is not installed') class Test4605Regression(TestCase): def test_4605_regression(self): From d55e176a1e882d498b63b46a01f0ac389428f084 Mon Sep 17 00:00:00 2001 From: Carlos de la Torre Date: Wed, 2 Nov 2016 11:03:53 -0300 Subject: [PATCH 312/457] Fix documentation error: removed unused variable (#4647) --- docs/api-guide/fields.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index f986f1508..17168b721 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -626,7 +626,6 @@ The `.fail()` method is a shortcut for raising `ValidationError` that takes a me def to_internal_value(self, data): if not isinstance(data, six.text_type): - msg = 'Incorrect type. Expected a string, but got %s' self.fail('incorrect_type', input_type=type(data).__name__) if not re.match(r'^rgb\([0-9]+,[0-9]+,[0-9]+\)$', data): From 7f437123bdd6f81c3f6c1216e6308aad4aac5068 Mon Sep 17 00:00:00 2001 From: pkrzyzaniak Date: Mon, 7 Nov 2016 06:12:52 +0800 Subject: [PATCH 313/457] Added "drf_tweaks" to third party packages (#4659) --- docs/topics/third-party-resources.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/topics/third-party-resources.md b/docs/topics/third-party-resources.md index 3fba9b5da..1713da1a1 100644 --- a/docs/topics/third-party-resources.md +++ b/docs/topics/third-party-resources.md @@ -251,6 +251,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [ember-django-adapter][ember-django-adapter] - An adapter for working with Ember.js * [django-versatileimagefield][django-versatileimagefield] - Provides a drop-in replacement for Django's stock `ImageField` that makes it easy to serve images in multiple sizes/renditions from a single field. For DRF-specific implementation docs, [click here][django-versatileimagefield-drf-docs]. * [drf-tracking][drf-tracking] - Utilities to track requests to DRF API views. +* [drf_tweaks][drf_tweaks] - Serializers with one-step validation (and more), pagination without counts and other tweaks. * [django-rest-framework-braces][django-rest-framework-braces] - Collection of utilities for working with Django Rest Framework. The most notable ones are [FormSerializer](https://django-rest-framework-braces.readthedocs.io/en/latest/overview.html#formserializer) and [SerializerForm](https://django-rest-framework-braces.readthedocs.io/en/latest/overview.html#serializerform), which are adapters between DRF serializers and Django forms. * [drf-haystack][drf-haystack] - Haystack search for Django Rest Framework * [django-rest-framework-version-transforms][django-rest-framework-version-transforms] - Enables the use of delta transformations for versioning of DRF resource representations. @@ -363,3 +364,4 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [django-rest-messaging-js]: https://github.com/raphaelgyory/django-rest-messaging-js [medium-django-rest-framework]: https://medium.com/django-rest-framework [django-rest-framework-course]: https://teamtreehouse.com/library/django-rest-framework +[drf_tweaks]: https://github.com/ArabellaTech/drf_tweaks From 0b9304014d4505cec6c4a7df09bc66616d3827b8 Mon Sep 17 00:00:00 2001 From: Aaron Lelevier Date: Mon, 7 Nov 2016 12:30:46 +0100 Subject: [PATCH 314/457] Add documentation link for single 'field-level validation' to the Validator docs page (#3772) (#4657) --- docs/api-guide/validators.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index 9df15ec15..1af9f02a5 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -272,6 +272,12 @@ A validator may be any callable that raises a `serializers.ValidationError` on f if value % 2 != 0: raise serializers.ValidationError('This field must be an even number.') +#### Field-level validation + +You can specify custom field-level validation by adding `.validate_` methods +to your `Serializer` subclass. This is documented in the +[Serializer docs](http://www.django-rest-framework.org/api-guide/serializers/#field-level-validation) + ## Class-based To write a class-based validator, use the `__call__` method. Class-based validators are useful as they allow you to parameterize and reuse behavior. From befacfb00d4c39436a307ddc4bf1edb6f82a1a5e Mon Sep 17 00:00:00 2001 From: James Beith Date: Mon, 7 Nov 2016 22:34:53 +1100 Subject: [PATCH 315/457] Add autofocus support for input.html templates (#4650) This change adds support to use `'autofocus': True` in the style options and have the `autofocus` attribute included on the input field when rendered. --- docs/topics/html-and-forms.md | 6 +++--- .../templates/rest_framework/horizontal/input.html | 2 +- rest_framework/templates/rest_framework/inline/input.html | 2 +- rest_framework/templates/rest_framework/vertical/input.html | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/topics/html-and-forms.md b/docs/topics/html-and-forms.md index 6660607fe..f77ae3af7 100644 --- a/docs/topics/html-and-forms.md +++ b/docs/topics/html-and-forms.md @@ -108,7 +108,7 @@ Let's take a look at how to render each of the three available template packs. F class LoginSerializer(serializers.Serializer): email = serializers.EmailField( max_length=100, - style={'placeholder': 'Email'} + style={'placeholder': 'Email', 'autofocus': True} ) password = serializers.CharField( max_length=100, @@ -207,9 +207,9 @@ Field templates can also use additional style properties, depending on their typ The complete list of `base_template` options and their associated style options is listed below. -base_template | Valid field types | Additional style options +base_template | Valid field types | Additional style options ----|----|---- -input.html | Any string, numeric or date/time field | input_type, placeholder, hide_label +input.html | Any string, numeric or date/time field | input_type, placeholder, hide_label, autofocus textarea.html | `CharField` | rows, placeholder, hide_label select.html | `ChoiceField` or relational field types | hide_label radio.html | `ChoiceField` or relational field types | inline, hide_label diff --git a/rest_framework/templates/rest_framework/horizontal/input.html b/rest_framework/templates/rest_framework/horizontal/input.html index 9e5bbd0f7..a6d657d7d 100644 --- a/rest_framework/templates/rest_framework/horizontal/input.html +++ b/rest_framework/templates/rest_framework/horizontal/input.html @@ -6,7 +6,7 @@ {% endif %}
- + {% if field.errors %} {% for error in field.errors %} diff --git a/rest_framework/templates/rest_framework/inline/input.html b/rest_framework/templates/rest_framework/inline/input.html index f28e8f11c..085c63cb3 100644 --- a/rest_framework/templates/rest_framework/inline/input.html +++ b/rest_framework/templates/rest_framework/inline/input.html @@ -5,5 +5,5 @@ {% endif %} - +
diff --git a/rest_framework/templates/rest_framework/vertical/input.html b/rest_framework/templates/rest_framework/vertical/input.html index 504dcc28e..a7cff2ca6 100644 --- a/rest_framework/templates/rest_framework/vertical/input.html +++ b/rest_framework/templates/rest_framework/vertical/input.html @@ -3,7 +3,7 @@ {% endif %} - + {% if field.errors %} {% for error in field.errors %} From ee60aaa945213723f9574317fa81ca43b7170454 Mon Sep 17 00:00:00 2001 From: Angel Velasquez Date: Mon, 7 Nov 2016 08:37:58 -0300 Subject: [PATCH 316/457] Update versions of Django on tox.ini (#4651) Bump release versions to 1.10.3, 1.9.11 and 1.8.16 More info on: https://www.djangoproject.com/weblog/2016/nov/01/security-releases/ --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index bac1569a0..e89274661 100644 --- a/tox.ini +++ b/tox.ini @@ -15,9 +15,9 @@ setenv = PYTHONDONTWRITEBYTECODE=1 PYTHONWARNINGS=once deps = - django18: Django==1.8.15 - django19: Django==1.9.10 - django110: Django==1.10.2 + django18: Django==1.8.16 + django19: Django==1.9.11 + django110: Django==1.10.3 djangomaster: https://github.com/django/django/archive/master.tar.gz -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt From 06df61e38c6ed99732007b0e9f3cc26e8317389e Mon Sep 17 00:00:00 2001 From: Rex Salisbury Date: Mon, 7 Nov 2016 03:41:10 -0800 Subject: [PATCH 317/457] handle error when no links are found (#4649) This is to address https://github.com/tomchristie/django-rest-raml/issues/5 The problem is that if you try to generate RAML docs when you haven't set up any views, you get the above error (min called on an empty list). unfortunately, this PR is not very helpful since it doesn't actually surface a readable error to the user. Not sure what the best way to address this would be... --- rest_framework/schemas.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_framework/schemas.py b/rest_framework/schemas.py index 9439cb691..773df6261 100644 --- a/rest_framework/schemas.py +++ b/rest_framework/schemas.py @@ -263,6 +263,8 @@ class SchemaGenerator(object): view_endpoints.append((path, method, view)) # Only generate the path prefix for paths that will be included + if not paths: + return None prefix = self.determine_path_prefix(paths) for path, method, view in view_endpoints: From 8d72535be9d7ab31c8a46ce5fbbdb39a6d8e0209 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 7 Nov 2016 12:55:18 +0000 Subject: [PATCH 318/457] Fix FilterSet warnings. (#4660) --- rest_framework/filters.py | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 531531efc..00e753d42 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -38,31 +38,19 @@ class BaseFilterBackend(object): if django_filters: - from django_filters.filterset import FilterSetMetaclass as DFFilterSetMetaclass from django_filters.rest_framework.filterset import FilterSet as DFFilterSet - class FilterSetMetaclass(DFFilterSetMetaclass): - def __new__(cls, name, bases, attrs): + class FilterSet(DFFilterSet): + def __init__(self, *args, **kwargs): warnings.warn( "The built in 'rest_framework.filters.FilterSet' is pending deprecation. " "You should use 'django_filters.rest_framework.FilterSet' instead.", PendingDeprecationWarning ) - return super(FilterSetMetaclass, cls).__new__(cls, name, bases, attrs) - _BaseFilterSet = DFFilterSet + return super(FilterSet, self).__init__(*args, **kwargs) else: - # Dummy metaclass just so we can give a user-friendly error message. - class FilterSetMetaclass(type): - def __init__(self, name, bases, attrs): - # Assert only on subclasses, so we can define FilterSet below. - if bases != (object,): - assert False, 'django-filter must be installed to use the `FilterSet` class' - super(FilterSetMetaclass, self).__init__(name, bases, attrs) - _BaseFilterSet = object - - -class FilterSet(six.with_metaclass(FilterSetMetaclass, _BaseFilterSet)): - pass + def FilterSet(): + assert False, 'django-filter must be installed to use the `FilterSet` class' class DjangoFilterBackend(BaseFilterBackend): From ea60872e9e18909577a2603f5ff630a7c9d04b16 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Mon, 7 Nov 2016 13:38:48 +0000 Subject: [PATCH 319/457] Version 3.5.3 [ci skip] --- docs/topics/release-notes.md | 18 ++++++++++++++++++ rest_framework/__init__.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index d25a46ba0..e5683360b 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,6 +40,14 @@ You can determine your currently installed version using `pip freeze`: ## 3.5.x series +### 3.5.3 + +**Date**: [7th November 2016][3.5.3-milestone] + +* Don't raise incorrect FilterSet deprecation warnings. ([#4660][gh4660], [#4643][gh4643], [#4644][gh4644]) +* Schema generation should not raise 404 when a view permission class does. ([#4645][gh4645], [#4646][gh4646]) +* Add `autofocus` support for input controls. ([#4650][gh4650]) + ### 3.5.2 **Date**: [1st November 2016][3.5.2-milestone] @@ -630,6 +638,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [3.5.0-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.0+Release%22 [3.5.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.1+Release%22 [3.5.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.2+Release%22 +[3.5.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.5.3+Release%22 [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 @@ -1198,3 +1207,12 @@ For older release notes, [please see the version 2.x documentation][old-release- [gh4620]: https://github.com/tomchristie/django-rest-framework/issues/4620 [gh4628]: https://github.com/tomchristie/django-rest-framework/issues/4628 [gh4639]: https://github.com/tomchristie/django-rest-framework/issues/4639 + + + +[gh4660]: https://github.com/tomchristie/django-rest-framework/issues/4660 +[gh4643]: https://github.com/tomchristie/django-rest-framework/issues/4643 +[gh4644]: https://github.com/tomchristie/django-rest-framework/issues/4644 +[gh4645]: https://github.com/tomchristie/django-rest-framework/issues/4645 +[gh4646]: https://github.com/tomchristie/django-rest-framework/issues/4646 +[gh4650]: https://github.com/tomchristie/django-rest-framework/issues/4650 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index fe82763a9..795b84e95 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.5.2' +__version__ = '3.5.3' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2016 Tom Christie' From 388cf7622c24fee33f998cbe44198bada1872ebb Mon Sep 17 00:00:00 2001 From: Michael Barr Date: Wed, 9 Nov 2016 07:59:11 -0500 Subject: [PATCH 320/457] Adds Django/Python Trove Classifiers (#4662) --- setup.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/setup.py b/setup.py index ca62366ed..5d14ddf6a 100755 --- a/setup.py +++ b/setup.py @@ -92,11 +92,19 @@ setup( 'Development Status :: 5 - Production/Stable', 'Environment :: Web Environment', 'Framework :: Django', + 'Framework :: Django :: 1.8', + 'Framework :: Django :: 1.9', + 'Framework :: Django :: 1.10', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', 'Topic :: Internet :: WWW/HTTP', ] ) From 8bab7f8d582c6c8c25e913ec4092899c78875a2c Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 10 Nov 2016 16:36:56 +0000 Subject: [PATCH 321/457] Only apply the nested writes test to writable fields. (#4669) --- 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 1bdcd12c3..02c24b70e 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -769,7 +769,7 @@ def raise_errors_on_nested_writes(method_name, serializer, validated_data): isinstance(field, BaseSerializer) and (field.source in validated_data) and isinstance(validated_data[field.source], (list, dict)) - for key, field in serializer.fields.items() + for field in serializer._writable_fields ), ( 'The `.{method_name}()` method does not support writable nested ' 'fields by default.\nWrite an explicit `.{method_name}()` method for ' From 24791cb353d1924086b30abe2188280547d9a6c4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 11 Nov 2016 09:44:35 +0000 Subject: [PATCH 322/457] Invalidate any existing prefetch cache on PUT requests. (#4668) --- docs/api-guide/testing.md | 11 ++++++++++- rest_framework/mixins.py | 5 ++--- tests/test_prefetch_related.py | 20 +++++++++++++++++++- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index 1f8c89233..de79a1e2f 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -187,7 +187,12 @@ As usual CSRF validation will only apply to any session authenticated views. Th # RequestsClient REST framework also includes a client for interacting with your application -using the popular Python library, `requests`. +using the popular Python library, `requests`. This may be useful if: + +* You are expecting to interface with the API primarily from another Python service, +and want to test the service at the same level as the client will see. +* You want to write tests in such a way that they can also be run against a staging or +live environment. (See "Live tests" below.) This exposes exactly the same interface as if you were using a requests session directly. @@ -198,6 +203,10 @@ directly. Note that the requests client requires you to pass fully qualified URLs. +## `RequestsClient` and working with the database + +The `RequestsClient` class is useful if + ## Headers & Authentication Custom headers and authentication credentials can be provided in the same way diff --git a/rest_framework/mixins.py b/rest_framework/mixins.py index 47a4923a1..f3695e665 100644 --- a/rest_framework/mixins.py +++ b/rest_framework/mixins.py @@ -71,9 +71,8 @@ class UpdateModelMixin(object): if getattr(instance, '_prefetched_objects_cache', None): # If 'prefetch_related' has been applied to a queryset, we need to - # refresh the instance from the database. - instance = self.get_object() - serializer = self.get_serializer(instance) + # forcibly invalidate the prefetch cache on the instance. + instance._prefetched_objects_cache = {} return Response(serializer.data) diff --git a/tests/test_prefetch_related.py b/tests/test_prefetch_related.py index fc697adc1..a9fa238ea 100644 --- a/tests/test_prefetch_related.py +++ b/tests/test_prefetch_related.py @@ -14,7 +14,7 @@ class UserSerializer(serializers.ModelSerializer): class UserUpdate(generics.UpdateAPIView): - queryset = User.objects.all().prefetch_related('groups') + queryset = User.objects.exclude(username='exclude').prefetch_related('groups') serializer_class = UserSerializer @@ -39,3 +39,21 @@ class TestPrefetchRelatedUpdates(TestCase): 'email': 'tom@example.com' } assert response.data == expected + + def test_prefetch_related_excluding_instance_from_original_queryset(self): + """ + Regression test for https://github.com/tomchristie/django-rest-framework/issues/4661 + """ + view = UserUpdate.as_view() + pk = self.user.pk + groups_pk = self.groups[0].pk + request = factory.put('/', {'username': 'exclude', 'groups': [groups_pk]}, format='json') + response = view(request, pk=pk) + assert User.objects.get(pk=pk).groups.count() == 1 + expected = { + 'id': pk, + 'username': 'exclude', + 'groups': [1], + 'email': 'tom@example.com' + } + assert response.data == expected From 0c02bbbfa728267909ea73aeac57b2e99aba5857 Mon Sep 17 00:00:00 2001 From: Bruno Alla Date: Mon, 14 Nov 2016 16:58:16 +0000 Subject: [PATCH 323/457] Correct a small typo in exceptions documentation --- 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 df8cad42d..03f16222d 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -47,7 +47,7 @@ Any example validation error might look like this: You can implement custom exception handling by creating a handler function that converts exceptions raised in your API views into response objects. This allows you to control the style of error responses used by your API. -The function must take a pair of arguments, this first is the exception to be handled, and the second is a dictionary containing any extra context such as the view currently being handled. The exception handler function should either return a `Response` object, or return `None` if the exception cannot be handled. If the handler returns `None` then the exception will be re-raised and Django will return a standard HTTP 500 'server error' response. +The function must take a pair of arguments, the first is the exception to be handled, and the second is a dictionary containing any extra context such as the view currently being handled. The exception handler function should either return a `Response` object, or return `None` if the exception cannot be handled. If the handler returns `None` then the exception will be re-raised and Django will return a standard HTTP 500 'server error' response. For example, you might want to ensure that all error responses include the HTTP status code in the body of the response, like so: From 1b9013ebae6f4b28de6b88c65a97b5ab0e817200 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Tue, 22 Nov 2016 19:46:12 +0600 Subject: [PATCH 324/457] update django-filter to version 1.0.0 --- requirements/requirements-optionals.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements-optionals.txt b/requirements/requirements-optionals.txt index 86c4f7709..a59f153d1 100644 --- a/requirements/requirements-optionals.txt +++ b/requirements/requirements-optionals.txt @@ -1,5 +1,5 @@ # Optional packages which may be used with REST framework. markdown==2.6.4 django-guardian==1.4.6 -django-filter==0.15.3 +django-filter==1.0.0 coreapi==2.0.8 From 8030f5b74f49e3144e8fb45f367227bac1192aec Mon Sep 17 00:00:00 2001 From: Vinay Anantharaman Date: Tue, 22 Nov 2016 14:50:47 -0800 Subject: [PATCH 325/457] Edit to the import in Setting filter backends `django_filters` doesn't export `rest_framework` by default so it's required to import it. --- docs/api-guide/filtering.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 1b49d3a73..3f212ced3 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -98,7 +98,7 @@ The default filter backends may be set globally, using the `DEFAULT_FILTER_BACKE You can also set the filter backends on a per-view, or per-viewset basis, using the `GenericAPIView` class-based views. - import django_filters + import django_filters.rest_framework from django.contrib.auth.models import User from myapp.serializers import UserSerializer from rest_framework import generics From 4b59ec27fa725a83646792b560fae1b646baadcd Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 23 Nov 2016 19:17:00 +0600 Subject: [PATCH 326/457] convert tests asserts to pytest style (#4696) --- tests/test_views.py | 16 ++++++++-------- tests/test_viewsets.py | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/test_views.py b/tests/test_views.py index 05c499481..adafa1cd7 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -71,8 +71,8 @@ class ClassBasedViewIntegrationTests(TestCase): expected = { 'detail': JSON_ERROR } - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(sanitise_json_error(response.data), expected) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert sanitise_json_error(response.data) == expected class FunctionBasedViewIntegrationTests(TestCase): @@ -85,8 +85,8 @@ class FunctionBasedViewIntegrationTests(TestCase): expected = { 'detail': JSON_ERROR } - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(sanitise_json_error(response.data), expected) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert sanitise_json_error(response.data) == expected class TestCustomExceptionHandler(TestCase): @@ -107,8 +107,8 @@ class TestCustomExceptionHandler(TestCase): request = factory.get('/', content_type='application/json') response = view(request) expected = 'Error!' - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, expected) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data == expected def test_function_based_view_exception_handler(self): view = error_view @@ -116,5 +116,5 @@ class TestCustomExceptionHandler(TestCase): request = factory.get('/', content_type='application/json') response = view(request) expected = 'Error!' - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, expected) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data == expected diff --git a/tests/test_viewsets.py b/tests/test_viewsets.py index 0d9b6b310..a3c4e6be5 100644 --- a/tests/test_viewsets.py +++ b/tests/test_viewsets.py @@ -21,15 +21,15 @@ class InitializeViewSetsTestCase(TestCase): }) response = my_view(request) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'ACTION': 'LIST'}) + assert response.status_code == status.HTTP_200_OK + assert response.data == {'ACTION': 'LIST'} def test_initialize_view_set_with_empty_actions(self): try: BasicViewSet.as_view() except TypeError as e: - self.assertEqual(str(e), "The `actions` argument must be provided " - "when calling `.as_view()` on a ViewSet. " - "For example `.as_view({'get': 'list'})`") + assert str(e) == ("The `actions` argument must be provided " + "when calling `.as_view()` on a ViewSet. " + "For example `.as_view({'get': 'list'})`") else: self.fail("actions must not be empty.") From 5ec223bca0f2df5be06b583f58012e115d2f52a2 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 23 Nov 2016 20:05:34 +0600 Subject: [PATCH 327/457] converted validators and write_only_fields test to pytest style (#4697) --- tests/test_validators.py | 13 +++++-------- tests/test_write_only_fields.py | 6 +++--- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/test_validators.py b/tests/test_validators.py index ab2c87c11..37e662b8e 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -94,23 +94,20 @@ class TestUniquenessValidation(TestCase): def test_doesnt_pollute_model(self): instance = AnotherUniquenessModel.objects.create(code='100') serializer = AnotherUniquenessSerializer(instance) - self.assertEqual( - AnotherUniquenessModel._meta.get_field('code').validators, []) + assert AnotherUniquenessModel._meta.get_field('code').validators == [] # Accessing data shouldn't effect validators on the model serializer.data - self.assertEqual( - AnotherUniquenessModel._meta.get_field('code').validators, []) + assert AnotherUniquenessModel._meta.get_field('code').validators == [] def test_related_model_is_unique(self): data = {'username': 'Existing', 'email': 'new-email@example.com'} rs = RelatedModelSerializer(data=data) - self.assertFalse(rs.is_valid()) - self.assertEqual(rs.errors, - {'username': ['This field must be unique.']}) + assert not rs.is_valid() + assert rs.errors == {'username': ['This field must be unique.']} data = {'username': 'new-username', 'email': 'new-email@example.com'} rs = RelatedModelSerializer(data=data) - self.assertTrue(rs.is_valid()) + assert rs.is_valid() def test_value_error_treated_as_not_unique(self): serializer = UniquenessIntegerSerializer(data={'integer': 'abc'}) diff --git a/tests/test_write_only_fields.py b/tests/test_write_only_fields.py index 3a289afab..272a05ff3 100644 --- a/tests/test_write_only_fields.py +++ b/tests/test_write_only_fields.py @@ -20,8 +20,8 @@ class WriteOnlyFieldTests(TestCase): 'password': '123' } serializer = self.Serializer(data=data) - self.assertTrue(serializer.is_valid()) - self.assertEqual(serializer.validated_data, data) + assert serializer.is_valid() + assert serializer.validated_data == data def write_only_fields_are_not_present_on_output(self): instance = { @@ -29,4 +29,4 @@ class WriteOnlyFieldTests(TestCase): 'password': '123' } serializer = self.Serializer(instance) - self.assertEqual(serializer.data, {'email': 'foo@example.com'}) + assert serializer.data == {'email': 'foo@example.com'} From a13b8d5560845612318fb030eb610bd34d986955 Mon Sep 17 00:00:00 2001 From: Nik Nyby Date: Wed, 23 Nov 2016 10:13:03 -0500 Subject: [PATCH 328/457] docs: grammar fix --- docs/api-guide/validators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index 1af9f02a5..e5b949496 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -159,7 +159,7 @@ If you want the date field to be entirely hidden from the user, then use `Hidden --- -**Note**: The `UniqueForValidation` classes always imposes an implicit constraint that the fields they are applied to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input. +**Note**: The `UniqueForValidation` classes impose an implicit constraint that the fields they are applied to are always treated as required. Fields with `default` values are an exception to this as they always supply a value even when omitted from user input. --- From eaec60ae1d4e34cfef115df6c626cf96f6750e56 Mon Sep 17 00:00:00 2001 From: Nik Nyby Date: Wed, 23 Nov 2016 11:10:39 -0500 Subject: [PATCH 329/457] docs: grammar fix - it's -> its (#4698) --- docs/api-guide/validators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index e5b949496..d6350ae5e 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -153,7 +153,7 @@ The field will not be writable to the user, but the default value will still be #### Using with a hidden date field. -If you want the date field to be entirely hidden from the user, then use `HiddenField`. This field type does not accept user input, but instead always returns it's default value to the `validated_data` in the serializer. +If you want the date field to be entirely hidden from the user, then use `HiddenField`. This field type does not accept user input, but instead always returns its default value to the `validated_data` in the serializer. published = serializers.HiddenField(default=timezone.now) From abc62afddb9e49b199ee5f4f491093014515da0e Mon Sep 17 00:00:00 2001 From: Nik Nyby Date: Thu, 24 Nov 2016 04:39:18 -0500 Subject: [PATCH 330/457] docs typo fix (#4701) Remove unnecessary "a" --- docs/api-guide/validators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index d6350ae5e..e041e9072 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -20,7 +20,7 @@ With `ModelForm` the validation is performed partially on the form, and partiall * It is easy to switch between using shortcut `ModelSerializer` classes and using explicit `Serializer` classes. Any validation behavior being used for `ModelSerializer` is simple to replicate. * Printing the `repr` of a serializer instance will show you exactly what validation rules it applies. There's no extra hidden validation behavior being called on the model instance. -When you're using `ModelSerializer` all of this is handled automatically for you. If you want to drop down to using a `Serializer` classes instead, then you need to define the validation rules explicitly. +When you're using `ModelSerializer` all of this is handled automatically for you. If you want to drop down to using `Serializer` classes instead, then you need to define the validation rules explicitly. #### Example From cd3fd36d0e5fa7e2d4907a7f74cd45641a8ddd03 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Sun, 27 Nov 2016 23:55:09 +0600 Subject: [PATCH 331/457] converted generic relations assert to pytest style --- tests/test_relations_generic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_relations_generic.py b/tests/test_relations_generic.py index babc2269c..a3798b0a3 100644 --- a/tests/test_relations_generic.py +++ b/tests/test_relations_generic.py @@ -75,7 +75,7 @@ class TestGenericRelations(TestCase): 'tags': ['django', 'python'], 'url': 'https://www.djangoproject.com/' } - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_generic_fk(self): """ @@ -105,4 +105,4 @@ class TestGenericRelations(TestCase): 'tagged_item': 'Note: Remember the milk' } ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected From 42d6098c74e3c8536458f4d423bedb652da16db7 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Mon, 28 Nov 2016 15:43:48 +0600 Subject: [PATCH 332/457] converted primary key relations test asserts to pytest (#4709) --- tests/test_relations_pk.py | 118 ++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/tests/test_relations_pk.py b/tests/test_relations_pk.py index 7a3d45927..ea02f2577 100644 --- a/tests/test_relations_pk.py +++ b/tests/test_relations_pk.py @@ -84,7 +84,7 @@ class PKManyToManyTests(TestCase): {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]} ] with self.assertNumQueries(4): - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_many_to_many_retrieve_prefetch_related(self): queryset = ManyToManySource.objects.all().prefetch_related('targets') @@ -101,15 +101,15 @@ class PKManyToManyTests(TestCase): {'id': 3, 'name': 'target-3', 'sources': [3]} ] with self.assertNumQueries(4): - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_many_to_many_update(self): data = {'id': 1, 'name': 'source-1', 'targets': [1, 2, 3]} instance = ManyToManySource.objects.get(pk=1) serializer = ManyToManySourceSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() serializer.save() - self.assertEqual(serializer.data, data) + assert serializer.data == data # Ensure source 1 is updated, and everything else is as expected queryset = ManyToManySource.objects.all() @@ -119,15 +119,15 @@ class PKManyToManyTests(TestCase): {'id': 2, 'name': 'source-2', 'targets': [1, 2]}, {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]} ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_reverse_many_to_many_update(self): data = {'id': 1, 'name': 'target-1', 'sources': [1]} instance = ManyToManyTarget.objects.get(pk=1) serializer = ManyToManyTargetSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() serializer.save() - self.assertEqual(serializer.data, data) + assert serializer.data == data # Ensure target 1 is updated, and everything else is as expected queryset = ManyToManyTarget.objects.all() @@ -137,15 +137,15 @@ class PKManyToManyTests(TestCase): {'id': 2, 'name': 'target-2', 'sources': [2, 3]}, {'id': 3, 'name': 'target-3', 'sources': [3]} ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_many_to_many_create(self): data = {'id': 4, 'name': 'source-4', 'targets': [1, 3]} serializer = ManyToManySourceSerializer(data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'source-4') + assert serializer.data == data + assert obj.name == 'source-4' # Ensure source 4 is added, and everything else is as expected queryset = ManyToManySource.objects.all() @@ -156,7 +156,7 @@ class PKManyToManyTests(TestCase): {'id': 3, 'name': 'source-3', 'targets': [1, 2, 3]}, {'id': 4, 'name': 'source-4', 'targets': [1, 3]}, ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_many_to_many_unsaved(self): source = ManyToManySource(name='source-unsaved') @@ -166,15 +166,15 @@ class PKManyToManyTests(TestCase): expected = {'id': None, 'name': 'source-unsaved', 'targets': []} # no query if source hasn't been created yet with self.assertNumQueries(0): - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_reverse_many_to_many_create(self): data = {'id': 4, 'name': 'target-4', 'sources': [1, 3]} serializer = ManyToManyTargetSerializer(data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'target-4') + assert serializer.data == data + assert obj.name == 'target-4' # Ensure target 4 is added, and everything else is as expected queryset = ManyToManyTarget.objects.all() @@ -185,7 +185,7 @@ class PKManyToManyTests(TestCase): {'id': 3, 'name': 'target-3', 'sources': [3]}, {'id': 4, 'name': 'target-4', 'sources': [1, 3]} ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected class PKForeignKeyTests(TestCase): @@ -207,7 +207,7 @@ class PKForeignKeyTests(TestCase): {'id': 3, 'name': 'source-3', 'target': 1} ] with self.assertNumQueries(1): - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_reverse_foreign_key_retrieve(self): queryset = ForeignKeyTarget.objects.all() @@ -217,7 +217,7 @@ class PKForeignKeyTests(TestCase): {'id': 2, 'name': 'target-2', 'sources': []}, ] with self.assertNumQueries(3): - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_reverse_foreign_key_retrieve_prefetch_related(self): queryset = ForeignKeyTarget.objects.all().prefetch_related('sources') @@ -229,9 +229,9 @@ class PKForeignKeyTests(TestCase): data = {'id': 1, 'name': 'source-1', 'target': 2} instance = ForeignKeySource.objects.get(pk=1) serializer = ForeignKeySourceSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() serializer.save() - self.assertEqual(serializer.data, data) + assert serializer.data == data # Ensure source 1 is updated, and everything else is as expected queryset = ForeignKeySource.objects.all() @@ -241,20 +241,20 @@ class PKForeignKeyTests(TestCase): {'id': 2, 'name': 'source-2', 'target': 1}, {'id': 3, 'name': 'source-3', 'target': 1} ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_foreign_key_update_incorrect_type(self): data = {'id': 1, 'name': 'source-1', 'target': 'foo'} instance = ForeignKeySource.objects.get(pk=1) serializer = ForeignKeySourceSerializer(instance, data=data) - self.assertFalse(serializer.is_valid()) - self.assertEqual(serializer.errors, {'target': ['Incorrect type. Expected pk value, received %s.' % six.text_type.__name__]}) + assert not serializer.is_valid() + assert serializer.errors == {'target': ['Incorrect type. Expected pk value, received %s.' % six.text_type.__name__]} def test_reverse_foreign_key_update(self): data = {'id': 2, 'name': 'target-2', 'sources': [1, 3]} instance = ForeignKeyTarget.objects.get(pk=2) serializer = ForeignKeyTargetSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() # We shouldn't have saved anything to the db yet since save # hasn't been called. queryset = ForeignKeyTarget.objects.all() @@ -263,10 +263,10 @@ class PKForeignKeyTests(TestCase): {'id': 1, 'name': 'target-1', 'sources': [1, 2, 3]}, {'id': 2, 'name': 'target-2', 'sources': []}, ] - self.assertEqual(new_serializer.data, expected) + assert new_serializer.data == expected serializer.save() - self.assertEqual(serializer.data, data) + assert serializer.data == data # Ensure target 2 is update, and everything else is as expected queryset = ForeignKeyTarget.objects.all() @@ -275,15 +275,15 @@ class PKForeignKeyTests(TestCase): {'id': 1, 'name': 'target-1', 'sources': [2]}, {'id': 2, 'name': 'target-2', 'sources': [1, 3]}, ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_foreign_key_create(self): data = {'id': 4, 'name': 'source-4', 'target': 2} serializer = ForeignKeySourceSerializer(data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'source-4') + assert serializer.data == data + assert obj.name == 'source-4' # Ensure source 4 is added, and everything else is as expected queryset = ForeignKeySource.objects.all() @@ -294,15 +294,15 @@ class PKForeignKeyTests(TestCase): {'id': 3, 'name': 'source-3', 'target': 1}, {'id': 4, 'name': 'source-4', 'target': 2}, ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_reverse_foreign_key_create(self): data = {'id': 3, 'name': 'target-3', 'sources': [1, 3]} serializer = ForeignKeyTargetSerializer(data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'target-3') + assert serializer.data == data + assert obj.name == 'target-3' # Ensure target 3 is added, and everything else is as expected queryset = ForeignKeyTarget.objects.all() @@ -312,14 +312,14 @@ class PKForeignKeyTests(TestCase): {'id': 2, 'name': 'target-2', 'sources': []}, {'id': 3, 'name': 'target-3', 'sources': [1, 3]}, ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_foreign_key_update_with_invalid_null(self): data = {'id': 1, 'name': 'source-1', 'target': None} instance = ForeignKeySource.objects.get(pk=1) serializer = ForeignKeySourceSerializer(instance, data=data) - self.assertFalse(serializer.is_valid()) - self.assertEqual(serializer.errors, {'target': ['This field may not be null.']}) + assert not serializer.is_valid() + assert serializer.errors == {'target': ['This field may not be null.']} def test_foreign_key_with_unsaved(self): source = ForeignKeySource(name='source-unsaved') @@ -329,7 +329,7 @@ class PKForeignKeyTests(TestCase): # no query if source hasn't been created yet with self.assertNumQueries(0): - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_foreign_key_with_empty(self): """ @@ -338,7 +338,7 @@ class PKForeignKeyTests(TestCase): https://github.com/tomchristie/django-rest-framework/issues/1072 """ serializer = NullableForeignKeySourceSerializer() - self.assertEqual(serializer.data['target'], None) + assert serializer.data['target'] is None def test_foreign_key_not_required(self): """ @@ -350,7 +350,7 @@ class PKForeignKeyTests(TestCase): extra_kwargs = {'target': {'required': False}} serializer = ModelSerializer(data={'name': 'test'}) serializer.is_valid(raise_exception=True) - self.assertNotIn('target', serializer.validated_data) + assert 'target' not in serializer.validated_data class PKNullableForeignKeyTests(TestCase): @@ -371,15 +371,15 @@ class PKNullableForeignKeyTests(TestCase): {'id': 2, 'name': 'source-2', 'target': 1}, {'id': 3, 'name': 'source-3', 'target': None}, ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_foreign_key_create_with_valid_null(self): data = {'id': 4, 'name': 'source-4', 'target': None} serializer = NullableForeignKeySourceSerializer(data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() obj = serializer.save() - self.assertEqual(serializer.data, data) - self.assertEqual(obj.name, 'source-4') + assert serializer.data == data + assert obj.name == 'source-4' # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() @@ -390,7 +390,7 @@ class PKNullableForeignKeyTests(TestCase): {'id': 3, 'name': 'source-3', 'target': None}, {'id': 4, 'name': 'source-4', 'target': None} ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_foreign_key_create_with_valid_emptystring(self): """ @@ -400,10 +400,10 @@ class PKNullableForeignKeyTests(TestCase): data = {'id': 4, 'name': 'source-4', 'target': ''} expected_data = {'id': 4, 'name': 'source-4', 'target': None} serializer = NullableForeignKeySourceSerializer(data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() obj = serializer.save() - self.assertEqual(serializer.data, expected_data) - self.assertEqual(obj.name, 'source-4') + assert serializer.data == expected_data + assert obj.name == 'source-4' # Ensure source 4 is created, and everything else is as expected queryset = NullableForeignKeySource.objects.all() @@ -414,15 +414,15 @@ class PKNullableForeignKeyTests(TestCase): {'id': 3, 'name': 'source-3', 'target': None}, {'id': 4, 'name': 'source-4', 'target': None} ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_foreign_key_update_with_valid_null(self): data = {'id': 1, 'name': 'source-1', 'target': None} instance = NullableForeignKeySource.objects.get(pk=1) serializer = NullableForeignKeySourceSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() serializer.save() - self.assertEqual(serializer.data, data) + assert serializer.data == data # Ensure source 1 is updated, and everything else is as expected queryset = NullableForeignKeySource.objects.all() @@ -432,7 +432,7 @@ class PKNullableForeignKeyTests(TestCase): {'id': 2, 'name': 'source-2', 'target': 1}, {'id': 3, 'name': 'source-3', 'target': None} ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_foreign_key_update_with_valid_emptystring(self): """ @@ -443,9 +443,9 @@ class PKNullableForeignKeyTests(TestCase): expected_data = {'id': 1, 'name': 'source-1', 'target': None} instance = NullableForeignKeySource.objects.get(pk=1) serializer = NullableForeignKeySourceSerializer(instance, data=data) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() serializer.save() - self.assertEqual(serializer.data, expected_data) + assert serializer.data == expected_data # Ensure source 1 is updated, and everything else is as expected queryset = NullableForeignKeySource.objects.all() @@ -455,18 +455,18 @@ class PKNullableForeignKeyTests(TestCase): {'id': 2, 'name': 'source-2', 'target': 1}, {'id': 3, 'name': 'source-3', 'target': None} ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected def test_null_uuid_foreign_key_serializes_as_none(self): source = NullableUUIDForeignKeySource(name='Source') serializer = NullableUUIDForeignKeySourceSerializer(source) data = serializer.data - self.assertEqual(data["target"], None) + assert data["target"] is None def test_nullable_uuid_foreign_key_is_valid_when_none(self): data = {"name": "Source", "target": None} serializer = NullableUUIDForeignKeySourceSerializer(data=data) - self.assertTrue(serializer.is_valid(), serializer.errors) + assert serializer.is_valid(), serializer.errors class PKNullableOneToOneTests(TestCase): @@ -485,4 +485,4 @@ class PKNullableOneToOneTests(TestCase): {'id': 1, 'name': 'target-1', 'nullable_source': None}, {'id': 2, 'name': 'target-2', 'nullable_source': 1}, ] - self.assertEqual(serializer.data, expected) + assert serializer.data == expected From 9f4c9691f47c534338cdaa4d8a22efc201c8fb62 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Mon, 28 Nov 2016 20:31:27 +0600 Subject: [PATCH 333/457] converted filters tests asserts to pytest style (#4711) --- tests/test_filters.py | 300 +++++++++++++++++------------------------- 1 file changed, 124 insertions(+), 176 deletions(-) diff --git a/tests/test_filters.py b/tests/test_filters.py index 12fb85895..0cc326239 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -155,8 +155,8 @@ class IntegrationTestFiltering(CommonFilteringTestCase): request = factory.get('/') response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, self.data) + assert response.status_code == status.HTTP_200_OK + assert response.data == self.data self.assertTrue(issubclass(w[-1].category, PendingDeprecationWarning)) self.assertIn("'rest_framework.filters.DjangoFilterBackend' is pending deprecation.", str(w[-1].message)) @@ -175,9 +175,9 @@ class IntegrationTestFiltering(CommonFilteringTestCase): request = factory.get('/') response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, self.data) - self.assertEqual(len(w), 0) + assert response.status_code == status.HTTP_200_OK + assert response.data == self.data + assert len(w) == 0 @unittest.skipUnless(django_filters, 'django-filter not installed') def test_get_filtered_fields_root_view(self): @@ -189,24 +189,24 @@ class IntegrationTestFiltering(CommonFilteringTestCase): # Basic test with no filter. request = factory.get('/') response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, self.data) + assert response.status_code == status.HTTP_200_OK + assert response.data == self.data # Tests that the decimal filter works. search_decimal = Decimal('2.25') request = factory.get('/', {'decimal': '%s' % search_decimal}) response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal] - self.assertEqual(response.data, expected_data) + assert response.data == expected_data # Tests that the date filter works. search_date = datetime.date(2012, 9, 22) request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-09-22' response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK expected_data = [f for f in self.data if parse_date(f['date']) == search_date] - self.assertEqual(response.data, expected_data) + assert response.data == expected_data @unittest.skipUnless(django_filters, 'django-filter not installed') def test_filter_with_queryset(self): @@ -219,9 +219,9 @@ class IntegrationTestFiltering(CommonFilteringTestCase): search_decimal = Decimal('2.25') request = factory.get('/', {'decimal': '%s' % search_decimal}) response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK expected_data = [f for f in self.data if Decimal(f['decimal']) == search_decimal] - self.assertEqual(response.data, expected_data) + assert response.data == expected_data @unittest.skipUnless(django_filters, 'django-filter not installed') def test_filter_with_get_queryset_only(self): @@ -245,32 +245,32 @@ class IntegrationTestFiltering(CommonFilteringTestCase): # Basic test with no filter. request = factory.get('/') response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, self.data) + assert response.status_code == status.HTTP_200_OK + assert response.data == self.data # 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}) response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK expected_data = [f for f in self.data if Decimal(f['decimal']) < search_decimal] - self.assertEqual(response.data, expected_data) + assert response.data == expected_data # Tests that the date filter set with 'gt' in the filter class works. search_date = datetime.date(2012, 10, 2) request = factory.get('/', {'date': '%s' % search_date}) # search_date str: '2012-10-02' response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK expected_data = [f for f in self.data if parse_date(f['date']) > search_date] - self.assertEqual(response.data, expected_data) + assert response.data == expected_data # Tests that the text filter set with 'icontains' in the filter class works. search_text = 'ff' request = factory.get('/', {'text': '%s' % search_text}) response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK expected_data = [f for f in self.data if search_text in f['text'].lower()] - self.assertEqual(response.data, expected_data) + assert response.data == expected_data # Tests that multiple filters works. search_decimal = Decimal('5.25') @@ -280,10 +280,10 @@ class IntegrationTestFiltering(CommonFilteringTestCase): 'date': '%s' % (search_date,) }) response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK expected_data = [f for f in self.data if parse_date(f['date']) > search_date and Decimal(f['decimal']) < search_decimal] - self.assertEqual(response.data, expected_data) + assert response.data == expected_data @unittest.skipUnless(django_filters, 'django-filter not installed') def test_incorrectly_configured_filter(self): @@ -304,8 +304,8 @@ class IntegrationTestFiltering(CommonFilteringTestCase): request = factory.get('/?text=aaa') response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) + assert response.status_code == status.HTTP_200_OK + assert len(response.data) == 1 @unittest.skipUnless(django_filters, 'django-filter not installed') def test_base_model_filter_with_proxy(self): @@ -316,8 +316,8 @@ class IntegrationTestFiltering(CommonFilteringTestCase): request = factory.get('/?text=aaa') response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) + assert response.status_code == status.HTTP_200_OK + assert len(response.data) == 1 @unittest.skipUnless(django_filters, 'django-filter not installed') def test_unknown_filter(self): @@ -329,7 +329,7 @@ class IntegrationTestFiltering(CommonFilteringTestCase): search_integer = 10 request = factory.get('/', {'integer': '%s' % search_integer}) response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK @override_settings(ROOT_URLCONF='tests.test_filters') @@ -351,8 +351,8 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase): # Basic test with no filter. response = self.client.get(self._get_url(item)) - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, data) + assert response.status_code == status.HTTP_200_OK + assert response.data == data # Tests that the decimal filter set that should fail. search_decimal = Decimal('4.25') @@ -360,7 +360,7 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase): 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) + assert response.status_code == status.HTTP_404_NOT_FOUND # Tests that the decimal filter set that should succeed. search_decimal = Decimal('4.25') @@ -369,8 +369,8 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase): 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) + assert response.status_code == status.HTTP_200_OK + assert response.data == low_item_data # Tests that multiple filters works. search_decimal = Decimal('5.25') @@ -382,8 +382,8 @@ class IntegrationTestDetailFiltering(CommonFilteringTestCase): '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) + assert response.status_code == status.HTTP_200_OK + assert response.data == valid_item_data class SearchFilterModel(models.Model): @@ -424,13 +424,10 @@ class SearchFilterTests(TestCase): view = SearchListView.as_view() request = factory.get('/', {'search': 'b'}) response = view(request) - self.assertEqual( - response.data, - [ - {'id': 1, 'title': 'z', 'text': 'abc'}, - {'id': 2, 'title': 'zz', 'text': 'bcd'} - ] - ) + assert response.data == [ + {'id': 1, 'title': 'z', 'text': 'abc'}, + {'id': 2, 'title': 'zz', 'text': 'bcd'} + ] def test_exact_search(self): class SearchListView(generics.ListAPIView): @@ -442,12 +439,9 @@ class SearchFilterTests(TestCase): view = SearchListView.as_view() request = factory.get('/', {'search': 'zzz'}) response = view(request) - self.assertEqual( - response.data, - [ - {'id': 3, 'title': 'zzz', 'text': 'cde'} - ] - ) + assert response.data == [ + {'id': 3, 'title': 'zzz', 'text': 'cde'} + ] def test_startswith_search(self): class SearchListView(generics.ListAPIView): @@ -459,12 +453,9 @@ class SearchFilterTests(TestCase): view = SearchListView.as_view() request = factory.get('/', {'search': 'b'}) response = view(request) - self.assertEqual( - response.data, - [ - {'id': 2, 'title': 'zz', 'text': 'bcd'} - ] - ) + assert response.data == [ + {'id': 2, 'title': 'zz', 'text': 'bcd'} + ] def test_regexp_search(self): class SearchListView(generics.ListAPIView): @@ -476,12 +467,9 @@ class SearchFilterTests(TestCase): view = SearchListView.as_view() request = factory.get('/', {'search': 'z{2} ^b'}) response = view(request) - self.assertEqual( - response.data, - [ - {'id': 2, 'title': 'zz', 'text': 'bcd'} - ] - ) + assert response.data == [ + {'id': 2, 'title': 'zz', 'text': 'bcd'} + ] def test_search_with_nonstandard_search_param(self): with override_settings(REST_FRAMEWORK={'SEARCH_PARAM': 'query'}): @@ -496,13 +484,10 @@ class SearchFilterTests(TestCase): 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'} - ] - ) + assert response.data == [ + {'id': 1, 'title': 'z', 'text': 'abc'}, + {'id': 2, 'title': 'zz', 'text': 'bcd'} + ] reload_module(filters) @@ -528,15 +513,13 @@ class SearchFilterFkTests(TestCase): filter_ = filters.SearchFilter() prefixes = [''] + list(filter_.lookup_prefixes) for prefix in prefixes: - self.assertFalse( - filter_.must_call_distinct( - SearchFilterModelFk._meta, ["%stitle" % prefix] - ) + assert not filter_.must_call_distinct( + SearchFilterModelFk._meta, + ["%stitle" % prefix] ) - self.assertFalse( - filter_.must_call_distinct( - SearchFilterModelFk._meta, ["%stitle" % prefix, "%sattribute__label" % prefix] - ) + assert not filter_.must_call_distinct( + SearchFilterModelFk._meta, + ["%stitle" % prefix, "%sattribute__label" % prefix] ) def test_must_call_distinct_restores_meta_for_each_field(self): @@ -545,10 +528,9 @@ class SearchFilterFkTests(TestCase): filter_ = filters.SearchFilter() prefixes = [''] + list(filter_.lookup_prefixes) for prefix in prefixes: - self.assertFalse( - filter_.must_call_distinct( - SearchFilterModelFk._meta, ["%sattribute__label" % prefix, "%stitle" % prefix] - ) + assert not filter_.must_call_distinct( + SearchFilterModelFk._meta, + ["%sattribute__label" % prefix, "%stitle" % prefix] ) @@ -596,21 +578,20 @@ class SearchFilterM2MTests(TestCase): view = SearchListView.as_view() request = factory.get('/', {'search': 'zz'}) response = view(request) - self.assertEqual(len(response.data), 1) + assert len(response.data) == 1 def test_must_call_distinct(self): filter_ = filters.SearchFilter() prefixes = [''] + list(filter_.lookup_prefixes) for prefix in prefixes: - self.assertFalse( - filter_.must_call_distinct( - SearchFilterModelM2M._meta, ["%stitle" % prefix] - ) + assert not filter_.must_call_distinct( + SearchFilterModelM2M._meta, + ["%stitle" % prefix] ) - self.assertTrue( - filter_.must_call_distinct( - SearchFilterModelM2M._meta, ["%stitle" % prefix, "%sattributes__label" % prefix] - ) + + assert filter_.must_call_distinct( + SearchFilterModelM2M._meta, + ["%stitle" % prefix, "%sattributes__label" % prefix] ) @@ -672,14 +653,11 @@ class DjangoFilterOrderingTests(TestCase): request = factory.get('/') response = view(request) - self.assertEqual( - response.data, - [ - {'id': 3, 'date': '2014-10-08', 'text': 'cde'}, - {'id': 2, 'date': '2013-10-08', 'text': 'bcd'}, - {'id': 1, 'date': '2012-10-08', 'text': 'abc'} - ] - ) + assert response.data == [ + {'id': 3, 'date': '2014-10-08', 'text': 'cde'}, + {'id': 2, 'date': '2013-10-08', 'text': 'bcd'}, + {'id': 1, 'date': '2012-10-08', 'text': 'abc'} + ] class OrderingFilterTests(TestCase): @@ -713,14 +691,11 @@ class OrderingFilterTests(TestCase): view = OrderingListView.as_view() request = factory.get('/', {'ordering': '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'}, - ] - ) + assert response.data == [ + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + ] def test_reverse_ordering(self): class OrderingListView(generics.ListAPIView): @@ -733,14 +708,11 @@ class OrderingFilterTests(TestCase): view = OrderingListView.as_view() request = factory.get('/', {'ordering': '-text'}) response = view(request) - self.assertEqual( - response.data, - [ - {'id': 3, 'title': 'xwv', 'text': 'cde'}, - {'id': 2, 'title': 'yxw', 'text': 'bcd'}, - {'id': 1, 'title': 'zyx', 'text': 'abc'}, - ] - ) + assert response.data == [ + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + ] def test_incorrectfield_ordering(self): class OrderingListView(generics.ListAPIView): @@ -753,14 +725,11 @@ class OrderingFilterTests(TestCase): view = OrderingListView.as_view() request = factory.get('/', {'ordering': 'foobar'}) response = view(request) - self.assertEqual( - response.data, - [ - {'id': 3, 'title': 'xwv', 'text': 'cde'}, - {'id': 2, 'title': 'yxw', 'text': 'bcd'}, - {'id': 1, 'title': 'zyx', 'text': 'abc'}, - ] - ) + assert response.data == [ + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + ] def test_default_ordering(self): class OrderingListView(generics.ListAPIView): @@ -773,14 +742,11 @@ class OrderingFilterTests(TestCase): view = OrderingListView.as_view() request = factory.get('') response = view(request) - self.assertEqual( - response.data, - [ - {'id': 3, 'title': 'xwv', 'text': 'cde'}, - {'id': 2, 'title': 'yxw', 'text': 'bcd'}, - {'id': 1, 'title': 'zyx', 'text': 'abc'}, - ] - ) + assert response.data == [ + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + ] def test_default_ordering_using_string(self): class OrderingListView(generics.ListAPIView): @@ -793,14 +759,11 @@ class OrderingFilterTests(TestCase): view = OrderingListView.as_view() request = factory.get('') response = view(request) - self.assertEqual( - response.data, - [ - {'id': 3, 'title': 'xwv', 'text': 'cde'}, - {'id': 2, 'title': 'yxw', 'text': 'bcd'}, - {'id': 1, 'title': 'zyx', 'text': 'abc'}, - ] - ) + assert response.data == [ + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + ] def test_ordering_by_aggregate_field(self): # create some related models to aggregate order by @@ -824,14 +787,11 @@ class OrderingFilterTests(TestCase): view = OrderingListView.as_view() request = factory.get('/', {'ordering': 'relateds__count'}) response = view(request) - self.assertEqual( - response.data, - [ - {'id': 1, 'title': 'zyx', 'text': 'abc'}, - {'id': 3, 'title': 'xwv', 'text': 'cde'}, - {'id': 2, 'title': 'yxw', 'text': 'bcd'}, - ] - ) + assert response.data == [ + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + ] def test_ordering_with_nonstandard_ordering_param(self): with override_settings(REST_FRAMEWORK={'ORDERING_PARAM': 'order'}): @@ -847,14 +807,11 @@ class OrderingFilterTests(TestCase): 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'}, - ] - ) + assert response.data == [ + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + ] reload_module(filters) @@ -884,14 +841,11 @@ class OrderingFilterTests(TestCase): view = OrderingListView.as_view() request = factory.get('/', {'ordering': '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'}, - ] - ) + assert response.data == [ + {'id': 1, 'title': 'zyx', 'text': 'abc'}, + {'id': 2, 'title': 'yxw', 'text': 'bcd'}, + {'id': 3, 'title': 'xwv', 'text': 'cde'}, + ] def test_ordering_with_improper_configuration(self): class OrderingListView(generics.ListAPIView): @@ -967,14 +921,11 @@ class SensitiveOrderingFilterTests(TestCase): username_field = 'username' # Note: Inverse username ordering correctly applied. - self.assertEqual( - response.data, - [ - {'id': 3, username_field: 'userC'}, - {'id': 2, username_field: 'userB'}, - {'id': 1, username_field: 'userA'}, - ] - ) + assert response.data == [ + {'id': 3, username_field: 'userC'}, + {'id': 2, username_field: 'userB'}, + {'id': 1, username_field: 'userA'}, + ] def test_cannot_order_by_non_serializer_fields(self): for serializer_cls in [ @@ -997,11 +948,8 @@ class SensitiveOrderingFilterTests(TestCase): username_field = 'username' # Note: The passwords are not in order. Default ordering is used. - self.assertEqual( - response.data, - [ - {'id': 1, username_field: 'userA'}, # PassB - {'id': 2, username_field: 'userB'}, # PassC - {'id': 3, username_field: 'userC'}, # PassA - ] - ) + assert response.data == [ + {'id': 1, username_field: 'userA'}, # PassB + {'id': 2, username_field: 'userB'}, # PassC + {'id': 3, username_field: 'userC'}, # PassA + ] From 649876674933c7c05225e8f33a278ce4210df598 Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Tue, 29 Nov 2016 04:49:18 -0500 Subject: [PATCH 334/457] Fix django deprecation warnings (#4712) --- tests/test_model_serializer.py | 4 ++-- tests/test_prefetch_related.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index 06848302f..b839f56ca 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -94,7 +94,7 @@ class Issue3674ParentModel(models.Model): class Issue3674ChildModel(models.Model): - parent = models.ForeignKey(Issue3674ParentModel, related_name='children') + parent = models.ForeignKey(Issue3674ParentModel, related_name='children', on_delete=models.CASCADE) value = models.CharField(primary_key=True, max_length=64) @@ -1013,7 +1013,7 @@ class Issue3674Test(TestCase): title = models.CharField(max_length=64) class TestChildModel(models.Model): - parent = models.ForeignKey(TestParentModel, related_name='children') + parent = models.ForeignKey(TestParentModel, related_name='children', on_delete=models.CASCADE) value = models.CharField(primary_key=True, max_length=64) class TestChildModelSerializer(serializers.ModelSerializer): diff --git a/tests/test_prefetch_related.py b/tests/test_prefetch_related.py index a9fa238ea..750173b38 100644 --- a/tests/test_prefetch_related.py +++ b/tests/test_prefetch_related.py @@ -2,6 +2,7 @@ from django.contrib.auth.models import Group, User from django.test import TestCase from rest_framework import generics, serializers +from rest_framework.compat import set_many from rest_framework.test import APIRequestFactory factory = APIRequestFactory() @@ -22,7 +23,7 @@ class TestPrefetchRelatedUpdates(TestCase): def setUp(self): self.user = User.objects.create(username='tom', email='tom@example.com') self.groups = [Group.objects.create(name='a'), Group.objects.create(name='b')] - self.user.groups = self.groups + set_many(self.user, 'groups', self.groups) self.user.save() def test_prefetch_related_updates(self): From 1e0988686cda1f9c150e239b945c1d9a0ea9b19c Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Tue, 29 Nov 2016 13:27:00 +0100 Subject: [PATCH 335/457] Update the Python doc links to use https and point to Python 3 (#4713) --- docs/api-guide/fields.md | 2 +- docs/api-guide/settings.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 17168b721..3dcc9d0a5 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -669,7 +669,7 @@ The [django-rest-framework-hstore][django-rest-framework-hstore] package provide [html-and-forms]: ../topics/html-and-forms.md [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 +[strftime]: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior [django-widgets]: https://docs.djangoproject.com/en/dev/ref/forms/widgets/ [iso8601]: http://www.w3.org/TR/NOTE-datetime [drf-compound-fields]: https://drf-compound-fields.readthedocs.io diff --git a/docs/api-guide/settings.md b/docs/api-guide/settings.md index 58ceeeeb4..a1ea12d6e 100644 --- a/docs/api-guide/settings.md +++ b/docs/api-guide/settings.md @@ -456,7 +456,7 @@ An integer of 0 or more, that may be used to specify the number of application p Default: `None` -[cite]: http://www.python.org/dev/peps/pep-0020/ +[cite]: https://www.python.org/dev/peps/pep-0020/ [rfc4627]: http://www.ietf.org/rfc/rfc4627.txt [heroku-minified-json]: https://github.com/interagent/http-api-design#keep-json-minified-in-all-responses -[strftime]: http://docs.python.org/2/library/time.html#time.strftime +[strftime]: https://docs.python.org/3/library/time.html#time.strftime From aed8387e05a7a665439044d90b7fac5d1fd61225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Germ=C3=A1n=20Larra=C3=ADn?= Date: Tue, 29 Nov 2016 12:35:43 -0300 Subject: [PATCH 336/457] docs: Fix description of `DecimalField`'s `max_digits` (#4714) As of PR #4377, `max_digits=None` is allowed for `DecimalField`. --- docs/api-guide/fields.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 3dcc9d0a5..6b6ae612d 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -261,7 +261,7 @@ Corresponds to `django.db.models.fields.DecimalField`. **Signature**: `DecimalField(max_digits, decimal_places, coerce_to_string=None, max_value=None, min_value=None)` -- `max_digits` The maximum number of digits allowed in the number. Note that this number must be greater than or equal to decimal_places. +- `max_digits` The maximum number of digits allowed in the number. It must be either `None` or an integer greater than or equal to `decimal_places`. - `decimal_places` The number of decimal places to store with the number. - `coerce_to_string` Set to `True` if string values should be returned for the representation, or `False` if `Decimal` objects should be returned. Defaults to the same value as the `COERCE_DECIMAL_TO_STRING` settings key, which will be `True` unless overridden. If `Decimal` objects are returned by the serializer, then the final output format will be determined by the renderer. Note that setting `localize` will force the value to `True`. - `max_value` Validate that the number provided is no greater than this value. From 27641e07b5b56ea6fcd4676419e8fd99c58e9c66 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 01:13:21 +0600 Subject: [PATCH 337/457] converted test asserts of generics-test to pytest --- tests/test_generics.py | 102 ++++++++++++++++++++--------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/tests/test_generics.py b/tests/test_generics.py index 247237584..2c3679c46 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -98,8 +98,8 @@ class TestRootView(TestCase): request = factory.get('/') with self.assertNumQueries(1): response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, self.data) + assert response.status_code == status.HTTP_200_OK + assert response.data == self.data def test_post_root_view(self): """ @@ -109,10 +109,10 @@ class TestRootView(TestCase): request = factory.post('/', data, format='json') with self.assertNumQueries(1): response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response.data, {'id': 4, 'text': 'foobar'}) + assert response.status_code == status.HTTP_201_CREATED + assert response.data == {'id': 4, 'text': 'foobar'} created = self.objects.get(id=4) - self.assertEqual(created.text, 'foobar') + assert created.text == 'foobar' def test_put_root_view(self): """ @@ -122,8 +122,8 @@ class TestRootView(TestCase): request = factory.put('/', data, format='json') with self.assertNumQueries(0): response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) - self.assertEqual(response.data, {"detail": 'Method "PUT" not allowed.'}) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + assert response.data == {"detail": 'Method "PUT" not allowed.'} def test_delete_root_view(self): """ @@ -132,8 +132,8 @@ class TestRootView(TestCase): request = factory.delete('/') with self.assertNumQueries(0): response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) - self.assertEqual(response.data, {"detail": 'Method "DELETE" not allowed.'}) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + assert response.data == {"detail": 'Method "DELETE" not allowed.'} def test_post_cannot_set_id(self): """ @@ -143,10 +143,10 @@ class TestRootView(TestCase): request = factory.post('/', data, format='json') with self.assertNumQueries(1): response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(response.data, {'id': 4, 'text': 'foobar'}) + assert response.status_code == status.HTTP_201_CREATED + assert response.data == {'id': 4, 'text': 'foobar'} created = self.objects.get(id=4) - self.assertEqual(created.text, 'foobar') + assert created.text == 'foobar' def test_post_error_root_view(self): """ @@ -156,7 +156,7 @@ class TestRootView(TestCase): request = factory.post('/', data, HTTP_ACCEPT='text/html') response = self.view(request).render() expected_error = 'Ensure this field has no more than 100 characters.' - self.assertIn(expected_error, response.rendered_content.decode('utf-8')) + assert expected_error in response.rendered_content.decode('utf-8') EXPECTED_QUERIES_FOR_PUT = 2 @@ -185,8 +185,8 @@ class TestInstanceView(TestCase): request = factory.get('/1') with self.assertNumQueries(1): response = self.view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, self.data[0]) + assert response.status_code == status.HTTP_200_OK + assert response.data == self.data[0] def test_post_instance_view(self): """ @@ -196,8 +196,8 @@ class TestInstanceView(TestCase): request = factory.post('/', data, format='json') with self.assertNumQueries(0): response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) - self.assertEqual(response.data, {"detail": 'Method "POST" not allowed.'}) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED + assert response.data == {"detail": 'Method "POST" not allowed.'} def test_put_instance_view(self): """ @@ -207,10 +207,10 @@ class TestInstanceView(TestCase): request = factory.put('/1', data, format='json') with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT): response = self.view(request, pk='1').render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(dict(response.data), {'id': 1, 'text': 'foobar'}) + assert response.status_code == status.HTTP_200_OK + assert dict(response.data) == {'id': 1, 'text': 'foobar'} updated = self.objects.get(id=1) - self.assertEqual(updated.text, 'foobar') + assert updated.text == 'foobar' def test_patch_instance_view(self): """ @@ -221,10 +221,10 @@ class TestInstanceView(TestCase): with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT): response = self.view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) + assert response.status_code == status.HTTP_200_OK + assert response.data == {'id': 1, 'text': 'foobar'} updated = self.objects.get(id=1) - self.assertEqual(updated.text, 'foobar') + assert updated.text == 'foobar' def test_delete_instance_view(self): """ @@ -233,10 +233,10 @@ class TestInstanceView(TestCase): request = factory.delete('/1') with self.assertNumQueries(2): response = self.view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) - self.assertEqual(response.content, six.b('')) + assert response.status_code == status.HTTP_204_NO_CONTENT + assert response.content == six.b('') ids = [obj.id for obj in self.objects.all()] - self.assertEqual(ids, [2, 3]) + assert ids == [2, 3] def test_get_instance_view_incorrect_arg(self): """ @@ -246,7 +246,7 @@ class TestInstanceView(TestCase): request = factory.get('/a') with self.assertNumQueries(0): response = self.view(request, pk='a').render() - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + assert response.status_code == status.HTTP_404_NOT_FOUND def test_put_cannot_set_id(self): """ @@ -256,10 +256,10 @@ class TestInstanceView(TestCase): request = factory.put('/1', data, format='json') with self.assertNumQueries(EXPECTED_QUERIES_FOR_PUT): response = self.view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'id': 1, 'text': 'foobar'}) + assert response.status_code == status.HTTP_200_OK + assert response.data == {'id': 1, 'text': 'foobar'} updated = self.objects.get(id=1) - self.assertEqual(updated.text, 'foobar') + assert updated.text == 'foobar' def test_put_to_deleted_instance(self): """ @@ -271,7 +271,7 @@ class TestInstanceView(TestCase): request = factory.put('/1', data, format='json') with self.assertNumQueries(1): response = self.view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + assert response.status_code == status.HTTP_404_NOT_FOUND def test_put_to_filtered_out_instance(self): """ @@ -282,7 +282,7 @@ class TestInstanceView(TestCase): filtered_out_pk = BasicModel.objects.filter(text='filtered out')[0].pk request = factory.put('/{0}'.format(filtered_out_pk), data, format='json') response = self.view(request, pk=filtered_out_pk).render() - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + assert response.status_code == status.HTTP_404_NOT_FOUND def test_patch_cannot_create_an_object(self): """ @@ -292,8 +292,8 @@ class TestInstanceView(TestCase): request = factory.patch('/999', data, format='json') with self.assertNumQueries(1): response = self.view(request, pk=999).render() - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertFalse(self.objects.filter(id=999).exists()) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert not self.objects.filter(id=999).exists() def test_put_error_instance_view(self): """ @@ -303,7 +303,7 @@ class TestInstanceView(TestCase): request = factory.put('/', data, HTTP_ACCEPT='text/html') response = self.view(request, pk=1).render() expected_error = 'Ensure this field has no more than 100 characters.' - self.assertIn(expected_error, response.rendered_content.decode('utf-8')) + assert expected_error in response.rendered_content.decode('utf-8') class TestFKInstanceView(TestCase): @@ -363,8 +363,8 @@ class TestOverriddenGetObject(TestCase): request = factory.get('/1') with self.assertNumQueries(1): response = self.view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, self.data[0]) + assert response.status_code == status.HTTP_200_OK + assert response.data == self.data[0] # Regression test for #285 @@ -394,9 +394,9 @@ class TestCreateModelWithAutoNowAddField(TestCase): data = {'email': 'foobar@example.com', 'content': 'foobar'} request = factory.post('/', data, format='json') response = self.view(request).render() - self.assertEqual(response.status_code, status.HTTP_201_CREATED) + assert response.status_code == status.HTTP_201_CREATED created = self.objects.get(id=1) - self.assertEqual(created.content, 'foobar') + assert created.content == 'foobar' # Test for particularly ugly regression with m2m in browsable API @@ -432,7 +432,7 @@ class TestM2MBrowsableAPI(TestCase): request = factory.get('/', HTTP_ACCEPT='text/html') view = ExampleView().as_view() response = view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK class InclusiveFilterBackend(object): @@ -489,9 +489,9 @@ class TestFilterBackendAppliedToViews(TestCase): root_view = RootView.as_view(filter_backends=(InclusiveFilterBackend,)) request = factory.get('/') response = root_view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(len(response.data), 1) - self.assertEqual(response.data, [{'id': 1, 'text': 'foo'}]) + assert response.status_code == status.HTTP_200_OK + assert len(response.data) == 1 + assert response.data == [{'id': 1, 'text': 'foo'}] def test_get_root_view_filters_out_all_models_with_exclusive_filter_backend(self): """ @@ -500,8 +500,8 @@ class TestFilterBackendAppliedToViews(TestCase): root_view = RootView.as_view(filter_backends=(ExclusiveFilterBackend,)) request = factory.get('/') response = root_view(request).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, []) + assert response.status_code == status.HTTP_200_OK + assert response.data == [] def test_get_instance_view_filters_out_name_with_filter_backend(self): """ @@ -510,8 +510,8 @@ class TestFilterBackendAppliedToViews(TestCase): instance_view = InstanceView.as_view(filter_backends=(ExclusiveFilterBackend,)) request = factory.get('/1') response = instance_view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertEqual(response.data, {'detail': 'Not found.'}) + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response.data == {'detail': 'Not found.'} def test_get_instance_view_will_return_single_object_when_filter_does_not_exclude_it(self): """ @@ -520,8 +520,8 @@ class TestFilterBackendAppliedToViews(TestCase): instance_view = InstanceView.as_view(filter_backends=(InclusiveFilterBackend,)) request = factory.get('/1') response = instance_view(request, pk=1).render() - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data, {'id': 1, 'text': 'foo'}) + assert response.status_code == status.HTTP_200_OK + assert response.data == {'id': 1, 'text': 'foo'} def test_dynamic_serializer_form_in_browsable_api(self): """ @@ -530,8 +530,8 @@ class TestFilterBackendAppliedToViews(TestCase): view = DynamicSerializerView.as_view() request = factory.get('/') response = view(request).render() - self.assertContains(response, 'field_b') - self.assertNotContains(response, 'field_a') + assert 'field_b' in response + assert 'field_a' not in response class TestGuardedQueryset(TestCase): From 4a0829d6ec58b8b2fd12c36a34cdcc319def2695 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 02:08:37 +0600 Subject: [PATCH 338/457] attempt to fix test --- tests/test_generics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_generics.py b/tests/test_generics.py index 2c3679c46..67191d329 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -530,7 +530,7 @@ class TestFilterBackendAppliedToViews(TestCase): view = DynamicSerializerView.as_view() request = factory.get('/') response = view(request).render() - assert 'field_b' in response + assert response is 'field_b' assert 'field_a' not in response From a5c8a8c2265c6462208c2defc0b943bc51ace6df Mon Sep 17 00:00:00 2001 From: Jeff Fein-Worton Date: Tue, 29 Nov 2016 18:00:10 -0800 Subject: [PATCH 339/457] typo --- 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 7cdb59531..f2d2fcaae 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -164,7 +164,7 @@ As with `DjangoModelPermissions`, this permission must only be applied to views Note that `DjangoObjectPermissions` **does not** require the `django-guardian` package, and should support other object-level backends equally well. -As with `DjangoModelPermissions` you can use custom model permissions by overriding `DjangoModelPermissions` and setting the `.perms_map` property. Refer to the source code for details. +As with `DjangoModelPermissions` you can use custom model permissions by overriding `DjangoObjectPermissions` and setting the `.perms_map` property. Refer to the source code for details. --- From a5bb9825f3b238d89fdb5d8754d057116fff56a9 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 09:56:22 +0600 Subject: [PATCH 340/457] attempt to fix test again --- tests/test_generics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_generics.py b/tests/test_generics.py index 67191d329..2c3679c46 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -530,7 +530,7 @@ class TestFilterBackendAppliedToViews(TestCase): view = DynamicSerializerView.as_view() request = factory.get('/') response = view(request).render() - assert response is 'field_b' + assert 'field_b' in response assert 'field_a' not in response From f5a900a404a85163ba36ec34034d0bacd626b010 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 10:01:37 +0600 Subject: [PATCH 341/457] some reverts to fix test --- tests/test_generics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_generics.py b/tests/test_generics.py index 2c3679c46..aa3951154 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -530,8 +530,8 @@ class TestFilterBackendAppliedToViews(TestCase): view = DynamicSerializerView.as_view() request = factory.get('/') response = view(request).render() - assert 'field_b' in response - assert 'field_a' not in response + self.assertContains(response, 'field_b') + self.assertNotContains(response, 'field_a') class TestGuardedQueryset(TestCase): From 10b5f36fec92871e1d96c85a008324683a32f6b5 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 12:35:34 +0600 Subject: [PATCH 342/457] added fixes --- tests/test_generics.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_generics.py b/tests/test_generics.py index aa3951154..c24cda006 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -530,8 +530,9 @@ class TestFilterBackendAppliedToViews(TestCase): view = DynamicSerializerView.as_view() request = factory.get('/') response = view(request).render() - self.assertContains(response, 'field_b') - self.assertNotContains(response, 'field_a') + content = response.content.decode('utf8') + assert 'field_b' in content + assert 'field_a' not in content class TestGuardedQueryset(TestCase): From e03d88ced7f2b22f96f50c2a885264147b242b4a Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 15:48:33 +0600 Subject: [PATCH 343/457] more pytest style assert (#4719) --- tests/test_validation.py | 38 +++++++++++++++++++--------------- tests/test_validation_error.py | 16 +++++++------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/tests/test_validation.py b/tests/test_validation.py index bc950dd22..8ff4aaf38 100644 --- a/tests/test_validation.py +++ b/tests/test_validation.py @@ -60,11 +60,11 @@ class TestNestedValidationError(TestCase): } }) - self.assertEqual(serializers.as_serializer_error(e), { + assert serializers.as_serializer_error(e) == { 'nested': { 'field': ['error'], } - }) + } class TestPreSaveValidationExclusionsSerializer(TestCase): @@ -75,20 +75,20 @@ class TestPreSaveValidationExclusionsSerializer(TestCase): # We've set `required=False` on the serializer, but the model # does not have `blank=True`, so this serializer should not validate. serializer = ShouldValidateModelSerializer(data={'renamed': ''}) - self.assertEqual(serializer.is_valid(), False) - self.assertIn('renamed', serializer.errors) - self.assertNotIn('should_validate_field', serializer.errors) + assert serializer.is_valid() is False + assert 'renamed' in serializer.errors + assert 'should_validate_field' not in serializer.errors class TestCustomValidationMethods(TestCase): def test_custom_validation_method_is_executed(self): serializer = ShouldValidateModelSerializer(data={'renamed': 'fo'}) - self.assertFalse(serializer.is_valid()) - self.assertIn('renamed', serializer.errors) + assert not serializer.is_valid() + assert 'renamed' in serializer.errors def test_custom_validation_method_passing(self): serializer = ShouldValidateModelSerializer(data={'renamed': 'foo'}) - self.assertTrue(serializer.is_valid()) + assert serializer.is_valid() class ValidationSerializer(serializers.Serializer): @@ -108,12 +108,12 @@ class TestAvoidValidation(TestCase): """ def test_serializer_errors_has_only_invalid_data_error(self): serializer = ValidationSerializer(data='invalid data') - self.assertFalse(serializer.is_valid()) - self.assertDictEqual(serializer.errors, { + assert not serializer.is_valid() + assert serializer.errors == { 'non_field_errors': [ 'Invalid data. Expected a dictionary, but got %s.' % type('').__name__ ] - }) + } # regression tests for issue: 1493 @@ -137,27 +137,31 @@ class TestMaxValueValidatorValidation(TestCase): def test_max_value_validation_serializer_success(self): serializer = ValidationMaxValueValidatorModelSerializer(data={'number_value': 99}) - self.assertTrue(serializer.is_valid()) + assert 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) + assert not serializer.is_valid() + assert serializer.errors == { + 'number_value': [ + 'Ensure this value is less than or equal to 100.' + ] + } 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) + assert 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, b'{"number_value":["Ensure this value is less than or equal to 100."]}') - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + assert response.content == b'{"number_value":["Ensure this value is less than or equal to 100."]}' + assert response.status_code == status.HTTP_400_BAD_REQUEST # regression tests for issue: 1533 diff --git a/tests/test_validation_error.py b/tests/test_validation_error.py index 8e371a349..562fe37e6 100644 --- a/tests/test_validation_error.py +++ b/tests/test_validation_error.py @@ -54,16 +54,16 @@ class TestValidationErrorWithFullDetails(TestCase): request = factory.get('/', content_type='application/json') response = view(request) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, self.expected_response_data) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data == self.expected_response_data def test_function_based_view_exception_handler(self): view = error_view request = factory.get('/', content_type='application/json') response = view(request) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, self.expected_response_data) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data == self.expected_response_data class TestValidationErrorWithCodes(TestCase): @@ -89,13 +89,13 @@ class TestValidationErrorWithCodes(TestCase): request = factory.get('/', content_type='application/json') response = view(request) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, self.expected_response_data) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data == self.expected_response_data def test_function_based_view_exception_handler(self): view = error_view request = factory.get('/', content_type='application/json') response = view(request) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data, self.expected_response_data) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data == self.expected_response_data From 504f4b44c6dc96e1c1dd2cece3e9d3ac1052011d Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 16:17:30 +0600 Subject: [PATCH 344/457] converted asserts of atomic requests test to pytest --- tests/test_atomic_requests.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/test_atomic_requests.py b/tests/test_atomic_requests.py index 09d7f2fb1..9085bfc89 100644 --- a/tests/test_atomic_requests.py +++ b/tests/test_atomic_requests.py @@ -67,8 +67,8 @@ class DBTransactionTests(TestCase): with self.assertNumQueries(1): response = self.view(request) - self.assertFalse(transaction.get_rollback()) - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert not transaction.get_rollback() + assert response.status_code == status.HTTP_200_OK assert BasicModel.objects.count() == 1 @@ -98,7 +98,7 @@ class DBTransactionErrorTests(TestCase): # 3 - release savepoint with transaction.atomic(): self.assertRaises(Exception, self.view, request) - self.assertFalse(transaction.get_rollback()) + assert not transaction.get_rollback() assert BasicModel.objects.count() == 1 @@ -128,9 +128,8 @@ class DBTransactionAPIExceptionTests(TestCase): # 4 - release savepoint (django>=1.8 only) with transaction.atomic(): response = self.view(request) - self.assertTrue(transaction.get_rollback()) - self.assertEqual(response.status_code, - status.HTTP_500_INTERNAL_SERVER_ERROR) + assert transaction.get_rollback() + assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR assert BasicModel.objects.count() == 0 @@ -151,5 +150,4 @@ class NonAtomicDBTransactionAPIExceptionTests(TransactionTestCase): # without checking connection.in_atomic_block view raises 500 # due attempt to rollback without transaction - self.assertEqual(response.status_code, - status.HTTP_404_NOT_FOUND) + assert response.status_code == status.HTTP_404_NOT_FOUND From a9b6c974852c7b5c6fb95c586fdab9fbe29e73f7 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 16:24:48 +0600 Subject: [PATCH 345/457] converted asserts of decorators test to pytest --- tests/test_decorators.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 46e4a6ad7..b187e5fd6 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -56,11 +56,11 @@ class DecoratorTestCase(TestCase): request = self.factory.get('/') response = view(request) - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK request = self.factory.post('/') response = view(request) - self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED def test_calling_put_method(self): @@ -70,11 +70,11 @@ class DecoratorTestCase(TestCase): request = self.factory.put('/') response = view(request) - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK request = self.factory.post('/') response = view(request) - self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED def test_calling_patch_method(self): @@ -84,11 +84,11 @@ class DecoratorTestCase(TestCase): request = self.factory.patch('/') response = view(request) - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK request = self.factory.post('/') response = view(request) - self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED def test_renderer_classes(self): @@ -99,16 +99,15 @@ class DecoratorTestCase(TestCase): request = self.factory.get('/') response = view(request) - self.assertTrue(isinstance(response.accepted_renderer, JSONRenderer)) + assert isinstance(response.accepted_renderer, JSONRenderer) def test_parser_classes(self): @api_view(['GET']) @parser_classes([JSONParser]) def view(request): - self.assertEqual(len(request.parsers), 1) - self.assertTrue(isinstance(request.parsers[0], - JSONParser)) + assert len(request.parsers) == 1 + assert isinstance(request.parsers[0], JSONParser) return Response({}) request = self.factory.get('/') @@ -119,9 +118,8 @@ class DecoratorTestCase(TestCase): @api_view(['GET']) @authentication_classes([BasicAuthentication]) def view(request): - self.assertEqual(len(request.authenticators), 1) - self.assertTrue(isinstance(request.authenticators[0], - BasicAuthentication)) + assert len(request.authenticators) == 1 + assert isinstance(request.authenticators[0], BasicAuthentication) return Response({}) request = self.factory.get('/') @@ -136,7 +134,7 @@ class DecoratorTestCase(TestCase): request = self.factory.get('/') response = view(request) - self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + assert response.status_code == status.HTTP_403_FORBIDDEN def test_throttle_classes(self): class OncePerDayUserThrottle(UserRateThrottle): @@ -149,7 +147,7 @@ class DecoratorTestCase(TestCase): request = self.factory.get('/') response = view(request) - self.assertEqual(response.status_code, status.HTTP_200_OK) + assert response.status_code == status.HTTP_200_OK response = view(request) - self.assertEqual(response.status_code, status.HTTP_429_TOO_MANY_REQUESTS) + assert response.status_code == status.HTTP_429_TOO_MANY_REQUESTS From 9a3f8d9a9c63fc14a4e13738407f2b262de6649a Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 16:42:43 +0600 Subject: [PATCH 346/457] converted asserts of descriptions test to pytest --- tests/test_description.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_description.py b/tests/test_description.py index fcb88287b..08d8bddec 100644 --- a/tests/test_description.py +++ b/tests/test_description.py @@ -60,7 +60,7 @@ class TestViewNamesAndDescriptions(TestCase): """ class MockView(APIView): pass - self.assertEqual(MockView().get_view_name(), 'Mock') + assert MockView().get_view_name() == 'Mock' def test_view_description_uses_docstring(self): """Ensure view descriptions are based on the docstring.""" @@ -80,7 +80,7 @@ class TestViewNamesAndDescriptions(TestCase): # hash style header #""" - self.assertEqual(MockView().get_view_description(), DESCRIPTION) + assert MockView().get_view_description() == DESCRIPTION def test_view_description_can_be_empty(self): """ @@ -89,7 +89,7 @@ class TestViewNamesAndDescriptions(TestCase): """ class MockView(APIView): pass - self.assertEqual(MockView().get_view_description(), '') + assert MockView().get_view_description() == '' def test_view_description_can_be_promise(self): """ @@ -111,7 +111,7 @@ class TestViewNamesAndDescriptions(TestCase): class MockView(APIView): __doc__ = MockLazyStr("a gettext string") - self.assertEqual(MockView().get_view_description(), 'a gettext string') + assert MockView().get_view_description() == 'a gettext string' def test_markdown(self): """ @@ -120,7 +120,7 @@ class TestViewNamesAndDescriptions(TestCase): if apply_markdown: gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21 lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21 - self.assertTrue(gte_21_match or lt_21_match) + assert gte_21_match or lt_21_match def test_dedent_tabs(): From 7e8b01dbd2e780b1204d8c362dcb40d31d1b27d5 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 16:45:48 +0600 Subject: [PATCH 347/457] converted asserts of encoders test to pytest --- tests/test_encoders.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_encoders.py b/tests/test_encoders.py index d6f681932..687141476 100644 --- a/tests/test_encoders.py +++ b/tests/test_encoders.py @@ -20,21 +20,21 @@ class JSONEncoderTests(TestCase): Tests encoding a decimal """ d = Decimal(3.14) - self.assertEqual(d, float(d)) + assert d == float(d) def test_encode_datetime(self): """ Tests encoding a datetime object """ current_time = datetime.now() - self.assertEqual(self.encoder.default(current_time), current_time.isoformat()) + assert self.encoder.default(current_time) == current_time.isoformat() def test_encode_time(self): """ Tests encoding a timezone """ current_time = datetime.now().time() - self.assertEqual(self.encoder.default(current_time), current_time.isoformat()[:12]) + assert self.encoder.default(current_time) == current_time.isoformat()[:12] def test_encode_time_tz(self): """ @@ -64,18 +64,18 @@ class JSONEncoderTests(TestCase): Tests encoding a date object """ current_date = date.today() - self.assertEqual(self.encoder.default(current_date), current_date.isoformat()) + assert self.encoder.default(current_date) == current_date.isoformat() def test_encode_timedelta(self): """ Tests encoding a timedelta object """ delta = timedelta(hours=1) - self.assertEqual(self.encoder.default(delta), str(delta.total_seconds())) + assert self.encoder.default(delta) == str(delta.total_seconds()) def test_encode_uuid(self): """ Tests encoding a UUID object """ unique_id = uuid4() - self.assertEqual(self.encoder.default(unique_id), str(unique_id)) + assert self.encoder.default(unique_id) == str(unique_id) From 1a741bb2a2759d9b20afc6160b5c7d58ec5ff382 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 17:12:01 +0600 Subject: [PATCH 348/457] converted asserts of exceptions test to pytest (#4723) --- tests/test_exceptions.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index f1d172211..8b5628ef2 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -16,28 +16,22 @@ class ExceptionTestCase(TestCase): example = "string" lazy_example = _(example) - self.assertEqual( - _get_error_details(lazy_example), - example - ) + assert _get_error_details(lazy_example) == example + assert isinstance( _get_error_details(lazy_example), ErrorDetail ) - self.assertEqual( - _get_error_details({'nested': lazy_example})['nested'], - example - ) + assert _get_error_details({'nested': lazy_example})['nested'] == example + assert isinstance( _get_error_details({'nested': lazy_example})['nested'], ErrorDetail ) - self.assertEqual( - _get_error_details([[lazy_example]])[0][0], - example - ) + assert _get_error_details([[lazy_example]])[0][0] == example + assert isinstance( _get_error_details([[lazy_example]])[0][0], ErrorDetail From 4f6c326a9955e70e64eb83499015bba10175468a Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Wed, 30 Nov 2016 18:52:32 +0600 Subject: [PATCH 349/457] converted remaining unittes asserts of fields test to pytest (#4724) --- tests/test_fields.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index 92030e3ca..069ba879d 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1014,16 +1014,16 @@ class TestLocalizedDecimalField(TestCase): @override_settings(USE_L10N=True, LANGUAGE_CODE='pl') def test_to_internal_value(self): field = serializers.DecimalField(max_digits=2, decimal_places=1, localize=True) - self.assertEqual(field.to_internal_value('1,1'), Decimal('1.1')) + assert field.to_internal_value('1,1') == Decimal('1.1') @override_settings(USE_L10N=True, LANGUAGE_CODE='pl') def test_to_representation(self): field = serializers.DecimalField(max_digits=2, decimal_places=1, localize=True) - self.assertEqual(field.to_representation(Decimal('1.1')), '1,1') + assert field.to_representation(Decimal('1.1')) == '1,1' def test_localize_forces_coerce_to_string(self): field = serializers.DecimalField(max_digits=2, decimal_places=1, coerce_to_string=False, localize=True) - self.assertTrue(isinstance(field.to_representation(Decimal('1.1')), six.string_types)) + assert isinstance(field.to_representation(Decimal('1.1')), six.string_types) class TestQuantizedValueForDecimal(TestCase): @@ -1031,19 +1031,19 @@ class TestQuantizedValueForDecimal(TestCase): field = serializers.DecimalField(max_digits=4, decimal_places=2) value = field.to_internal_value(12).as_tuple() expected_digit_tuple = (0, (1, 2, 0, 0), -2) - self.assertEqual(value, expected_digit_tuple) + assert value == expected_digit_tuple def test_string_quantized_value_for_decimal(self): field = serializers.DecimalField(max_digits=4, decimal_places=2) value = field.to_internal_value('12').as_tuple() expected_digit_tuple = (0, (1, 2, 0, 0), -2) - self.assertEqual(value, expected_digit_tuple) + assert value == expected_digit_tuple def test_part_precision_string_quantized_value_for_decimal(self): field = serializers.DecimalField(max_digits=4, decimal_places=2) value = field.to_internal_value('12.0').as_tuple() expected_digit_tuple = (0, (1, 2, 0, 0), -2) - self.assertEqual(value, expected_digit_tuple) + assert value == expected_digit_tuple class TestNoDecimalPlaces(FieldValues): From 22578525eff075e329e8840c42c358d59105a0d3 Mon Sep 17 00:00:00 2001 From: Xavier Ordoquy Date: Wed, 30 Nov 2016 13:58:34 +0100 Subject: [PATCH 350/457] Documentation update (#4717) --- docs/api-guide/authentication.md | 2 +- docs/api-guide/fields.md | 6 +++--- docs/api-guide/filtering.md | 4 ++-- docs/api-guide/generic-views.md | 2 +- docs/api-guide/pagination.md | 2 +- docs/api-guide/parsers.md | 2 +- docs/api-guide/permissions.md | 4 ++-- docs/api-guide/relations.md | 6 +++--- docs/api-guide/renderers.md | 4 ++-- docs/api-guide/responses.md | 2 +- docs/api-guide/reverse.md | 4 ++-- docs/api-guide/schemas.md | 4 ++-- docs/api-guide/serializers.md | 2 +- docs/api-guide/testing.md | 4 ++-- docs/api-guide/throttling.md | 4 ++-- docs/api-guide/validators.md | 2 +- docs/topics/2.2-announcement.md | 8 ++++---- docs/topics/2.4-announcement.md | 2 +- docs/topics/3.0-announcement.md | 4 ++-- docs/topics/ajax-csrf-cors.md | 2 +- docs/topics/release-notes.md | 2 +- 21 files changed, 36 insertions(+), 36 deletions(-) diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index bf3a31eb7..4a01188f3 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -363,7 +363,7 @@ HTTP Signature (currently a [IETF draft][http-signature-ietf-draft]) provides a [oauth]: http://oauth.net/2/ [permission]: permissions.md [throttling]: throttling.md -[csrf-ajax]: https://docs.djangoproject.com/en/dev/ref/csrf/#ajax +[csrf-ajax]: https://docs.djangoproject.com/en/stable/ref/csrf/#ajax [mod_wsgi_official]: http://code.google.com/p/modwsgi/wiki/ConfigurationDirectives#WSGIPassAuthorization [django-oauth-toolkit-getting-started]: https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html [django-rest-framework-oauth]: http://jpadilla.github.io/django-rest-framework-oauth/ diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 6b6ae612d..b527b016b 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -665,12 +665,12 @@ The [django-rest-framework-gis][django-rest-framework-gis] package provides geog The [django-rest-framework-hstore][django-rest-framework-hstore] package provides an `HStoreField` to support [django-hstore][django-hstore] `DictionaryField` model field. -[cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data +[cite]: https://docs.djangoproject.com/en/stable/ref/forms/api/#django.forms.Form.cleaned_data [html-and-forms]: ../topics/html-and-forms.md -[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS +[FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS [ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 [strftime]: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior -[django-widgets]: https://docs.djangoproject.com/en/dev/ref/forms/widgets/ +[django-widgets]: https://docs.djangoproject.com/en/stable/ref/forms/widgets/ [iso8601]: http://www.w3.org/TR/NOTE-datetime [drf-compound-fields]: https://drf-compound-fields.readthedocs.io [drf-extra-fields]: https://github.com/Hipo/drf-extra-fields diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 3f212ced3..8a23a2ea3 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -455,14 +455,14 @@ The [djangorestframework-word-filter][django-rest-framework-word-search-filter] [drf-url-filter][drf-url-filter] is a simple Django app to apply filters on drf `ModelViewSet`'s `Queryset` in a clean, simple and configurable way. It also supports validations on incoming query params and their values. A beautiful python package `Voluptuous` is being used for validations on the incoming query parameters. The best part about voluptuous is you can define your own validations as per your query params requirements. -[cite]: https://docs.djangoproject.com/en/dev/topics/db/queries/#retrieving-specific-objects-with-filters +[cite]: https://docs.djangoproject.com/en/stable/topics/db/queries/#retrieving-specific-objects-with-filters [django-filter]: https://github.com/alex/django-filter [django-filter-docs]: https://django-filter.readthedocs.io/en/latest/index.html [guardian]: https://django-guardian.readthedocs.io/ [view-permissions]: https://django-guardian.readthedocs.io/en/latest/userguide/assign.html [view-permissions-blogpost]: http://blog.nyaruka.com/adding-a-view-permission-to-django-models [nullbooleanselect]: https://github.com/django/django/blob/master/django/forms/widgets.py -[search-django-admin]: https://docs.djangoproject.com/en/dev/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields +[search-django-admin]: https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.search_fields [django-rest-framework-filters]: https://github.com/philipn/django-rest-framework-filters [django-rest-framework-word-search-filter]: https://github.com/trollknurr/django-rest-framework-word-search-filter [django-url-filter]: https://github.com/miki725/django-url-filter diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index c368d0b46..606a3787a 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -382,7 +382,7 @@ The [django-rest-framework-bulk package][django-rest-framework-bulk] implements [Django Rest Multiple Models][django-rest-multiple-models] provides a generic view (and mixin) for sending multiple serialized models and/or querysets via a single API request. -[cite]: https://docs.djangoproject.com/en/dev/ref/class-based-views/#base-vs-generic-views +[cite]: https://docs.djangoproject.com/en/stable/ref/class-based-views/#base-vs-generic-views [GenericAPIView]: #genericapiview [ListModelMixin]: #listmodelmixin [CreateModelMixin]: #createmodelmixin diff --git a/docs/api-guide/pagination.md b/docs/api-guide/pagination.md index f82614eca..bc7a5602d 100644 --- a/docs/api-guide/pagination.md +++ b/docs/api-guide/pagination.md @@ -325,7 +325,7 @@ The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin` The [`drf-proxy-pagination` package][drf-proxy-pagination] includes a `ProxyPagination` class which allows to choose pagination class with a query parameter. -[cite]: https://docs.djangoproject.com/en/dev/topics/pagination/ +[cite]: https://docs.djangoproject.com/en/stable/topics/pagination/ [github-link-pagination]: https://developer.github.com/guides/traversing-with-pagination/ [link-header]: ../img/link-header-pagination.png [drf-extensions]: http://chibisov.github.io/drf-extensions/docs/ diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index ef2859fe1..7bf932d06 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -224,7 +224,7 @@ Modify your REST framework settings. [jquery-ajax]: http://api.jquery.com/jQuery.ajax/ [cite]: https://groups.google.com/d/topic/django-developers/dxI4qVzrBY4/discussion -[upload-handlers]: https://docs.djangoproject.com/en/dev/topics/http/file-uploads/#upload-handlers +[upload-handlers]: https://docs.djangoproject.com/en/stable/topics/http/file-uploads/#upload-handlers [rest-framework-yaml]: http://jpadilla.github.io/django-rest-framework-yaml/ [rest-framework-xml]: http://jpadilla.github.io/django-rest-framework-xml/ [yaml]: http://www.yaml.org/ diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index f2d2fcaae..be2981327 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -269,8 +269,8 @@ The [Django Rest Framework Roles][django-rest-framework-roles] package makes it [authentication]: authentication.md [throttling]: throttling.md [filtering]: filtering.md -[contribauth]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#custom-permissions -[objectpermissions]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#handling-object-permissions +[contribauth]: https://docs.djangoproject.com/en/stable/topics/auth/customizing/#custom-permissions +[objectpermissions]: https://docs.djangoproject.com/en/stable/topics/auth/customizing/#handling-object-permissions [guardian]: https://github.com/lukaszb/django-guardian [get_objects_for_user]: http://pythonhosted.org/django-guardian/api/guardian.shortcuts.html#get-objects-for-user [2.2-announcement]: ../topics/2.2-announcement.md diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index aabe49412..662fd4809 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -505,7 +505,7 @@ For example, given the following model for a tag, which has a generic relationsh """ Tags arbitrary model instances using a generic relation. - See: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/ + See: https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/ """ tag_name = models.SlugField() content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) @@ -593,9 +593,9 @@ The [drf-nested-routers package][drf-nested-routers] provides routers and relati The [rest-framework-generic-relations][drf-nested-relations] library provides read/write serialization for generic foreign keys. [cite]: http://lwn.net/Articles/193245/ -[reverse-relationships]: https://docs.djangoproject.com/en/dev/topics/db/queries/#following-relationships-backward +[reverse-relationships]: https://docs.djangoproject.com/en/stable/topics/db/queries/#following-relationships-backward [routers]: http://www.django-rest-framework.org/api-guide/routers#defaultrouter -[generic-relations]: https://docs.djangoproject.com/en/dev/ref/contrib/contenttypes/#id1 +[generic-relations]: https://docs.djangoproject.com/en/stable/ref/contrib/contenttypes/#id1 [2.2-announcement]: ../topics/2.2-announcement.md [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [drf-nested-relations]: https://github.com/Ian-Foote/rest-framework-generic-relations diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index a95778350..648eafdde 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -476,7 +476,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily [Rest Framework Latex] provides a renderer that outputs PDFs using Laulatex. It is maintained by [Pebble (S/F Software)][mypebble]. -[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/#the-rendering-process +[cite]: https://docs.djangoproject.com/en/stable/stable/template-response/#the-rendering-process [conneg]: content-negotiation.md [html-and-forms]: ../topics/html-and-forms.md [browser-accept-headers]: http://www.gethifi.com/blog/browser-rest-http-accept-headers @@ -485,7 +485,7 @@ Comma-separated values are a plain-text tabular data format, that can be easily [quote]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven [application/vnd.github+json]: http://developer.github.com/v3/media/ [application/vnd.collection+json]: http://www.amundsen.com/media-types/collection/ -[django-error-views]: https://docs.djangoproject.com/en/dev/topics/http/views/#customizing-error-views +[django-error-views]: https://docs.djangoproject.com/en/stable/topics/http/views/#customizing-error-views [rest-framework-jsonp]: http://jpadilla.github.io/django-rest-framework-jsonp/ [cors]: http://www.w3.org/TR/cors/ [cors-docs]: http://www.django-rest-framework.org/topics/ajax-csrf-cors/ diff --git a/docs/api-guide/responses.md b/docs/api-guide/responses.md index 97f312710..8ee14eefa 100644 --- a/docs/api-guide/responses.md +++ b/docs/api-guide/responses.md @@ -91,5 +91,5 @@ As with any other `TemplateResponse`, this method is called to render the serial You won't typically need to call `.render()` yourself, as it's handled by Django's standard response cycle. -[cite]: https://docs.djangoproject.com/en/dev/ref/template-response/ +[cite]: https://docs.djangoproject.com/en/stable/stable/template-response/ [statuscodes]: status-codes.md diff --git a/docs/api-guide/reverse.md b/docs/api-guide/reverse.md index 35d88e2db..ee0b2054f 100644 --- a/docs/api-guide/reverse.md +++ b/docs/api-guide/reverse.md @@ -51,5 +51,5 @@ As with the `reverse` function, you should **include the request as a keyword ar api_root = reverse_lazy('api-root', request=request) [cite]: http://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm#sec_5_1_5 -[reverse]: https://docs.djangoproject.com/en/dev/topics/http/urls/#reverse -[reverse-lazy]: https://docs.djangoproject.com/en/dev/topics/http/urls/#reverse-lazy +[reverse]: https://docs.djangoproject.com/en/stable/topics/http/urls/#reverse +[reverse-lazy]: https://docs.djangoproject.com/en/stable/topics/http/urls/#reverse-lazy diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index 7da619034..c89c100fe 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -541,5 +541,5 @@ A short description of the meaning and intended usage of the input field. [open-api]: https://openapis.org/ [json-hyperschema]: http://json-schema.org/latest/json-schema-hypermedia.html [api-blueprint]: https://apiblueprint.org/ -[static-files]: https://docs.djangoproject.com/en/dev/howto/static-files/ -[named-arguments]: https://docs.djangoproject.com/en/dev/topics/http/urls/#named-groups +[static-files]: https://docs.djangoproject.com/en/stable/howto/static-files/ +[named-arguments]: https://docs.djangoproject.com/en/stable/topics/http/urls/#named-groups diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 290e32f4f..d36812f3f 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -1118,7 +1118,7 @@ The [html-json-forms][html-json-forms] package provides an algorithm and seriali [cite]: https://groups.google.com/d/topic/django-users/sVFaOfQi4wY/discussion [relations]: relations.md -[model-managers]: https://docs.djangoproject.com/en/dev/topics/db/managers/ +[model-managers]: https://docs.djangoproject.com/en/stable/topics/db/managers/ [encapsulation-blogpost]: http://www.dabapps.com/blog/django-models-and-encapsulation/ [django-rest-marshmallow]: http://tomchristie.github.io/django-rest-marshmallow/ [marshmallow]: https://marshmallow.readthedocs.io/en/latest/ diff --git a/docs/api-guide/testing.md b/docs/api-guide/testing.md index de79a1e2f..410f6d78a 100644 --- a/docs/api-guide/testing.md +++ b/docs/api-guide/testing.md @@ -373,6 +373,6 @@ For example, to add support for using `format='html'` in test requests, you migh } [cite]: http://jacobian.org/writing/django-apps-with-buildout/#s-create-a-test-wrapper -[client]: https://docs.djangoproject.com/en/dev/topics/testing/tools/#the-test-client -[requestfactory]: https://docs.djangoproject.com/en/dev/topics/testing/advanced/#django.test.client.RequestFactory +[client]: https://docs.djangoproject.com/en/stable/topics/testing/tools/#the-test-client +[requestfactory]: https://docs.djangoproject.com/en/stable/topics/testing/advanced/#django.test.client.RequestFactory [configuration]: #configuration diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index da4d5f725..2a6337e81 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -193,5 +193,5 @@ The following is an example of a rate throttle, that will randomly throttle 1 in [cite]: https://dev.twitter.com/docs/error-codes-responses [permissions]: permissions.md [identifing-clients]: http://oxpedia.org/wiki/index.php?title=AppSuite:Grizzly#Multiple_Proxies_in_front_of_the_cluster -[cache-setting]: https://docs.djangoproject.com/en/dev/ref/settings/#caches -[cache-docs]: https://docs.djangoproject.com/en/dev/topics/cache/#setting-up-the-cache +[cache-setting]: https://docs.djangoproject.com/en/stable/stable/settings/#caches +[cache-docs]: https://docs.djangoproject.com/en/stable/topics/cache/#setting-up-the-cache diff --git a/docs/api-guide/validators.md b/docs/api-guide/validators.md index e041e9072..0e58c6fff 100644 --- a/docs/api-guide/validators.md +++ b/docs/api-guide/validators.md @@ -300,4 +300,4 @@ In some advanced cases you might want a validator to be passed the serializer fi # In `__call__` we can then use that information to modify the validation behavior. self.is_update = serializer_field.parent.instance is not None -[cite]: https://docs.djangoproject.com/en/dev/ref/validators/ +[cite]: https://docs.djangoproject.com/en/stable/ref/validators/ diff --git a/docs/topics/2.2-announcement.md b/docs/topics/2.2-announcement.md index e6220f427..ca4ed2efa 100644 --- a/docs/topics/2.2-announcement.md +++ b/docs/topics/2.2-announcement.md @@ -147,10 +147,10 @@ When using a serializer with a `HyperlinkedRelatedField` or `HyperlinkedIdentity From version 2.2 onwards, serializers with hyperlinked relationships *always* require a `'request'` key to be supplied in the context dictionary. The implicit behavior will continue to function, but its use will raise a `PendingDeprecationWarning`. [xordoquy]: https://github.com/xordoquy -[django-python-3]: https://docs.djangoproject.com/en/dev/faq/install/#can-i-use-django-with-python-3 -[porting-python-3]: https://docs.djangoproject.com/en/dev/topics/python3/ -[python-compat]: https://docs.djangoproject.com/en/dev/releases/1.5/#python-compatibility -[django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy +[django-python-3]: https://docs.djangoproject.com/en/stable/faq/install/#can-i-use-django-with-python-3 +[porting-python-3]: https://docs.djangoproject.com/en/stable/topics/python3/ +[python-compat]: https://docs.djangoproject.com/en/stable/releases/1.5/#python-compatibility +[django-deprecation-policy]: https://docs.djangoproject.com/en/stable/internals/release-process/#internal-release-deprecation-policy [credits]: http://www.django-rest-framework.org/topics/credits [mailing-list]: https://groups.google.com/forum/?fromgroups#!forum/django-rest-framework [django-rest-framework-docs]: https://github.com/marcgibbons/django-rest-framework-docs diff --git a/docs/topics/2.4-announcement.md b/docs/topics/2.4-announcement.md index 3009daa49..96f68c865 100644 --- a/docs/topics/2.4-announcement.md +++ b/docs/topics/2.4-announcement.md @@ -162,7 +162,7 @@ The next planned release will be 3.0, featuring an improved and simplified seria Once again, many thanks to all the generous [backers and sponsors][kickstarter-sponsors] who've helped make this possible! -[lts-releases]: https://docs.djangoproject.com/en/dev/internals/release-process/#long-term-support-lts-releases +[lts-releases]: https://docs.djangoproject.com/en/stable/internals/release-process/#long-term-support-lts-releases [2-4-release-notes]: release-notes#240 [view-name-and-description-settings]: ../api-guide/settings#view-names-and-descriptions [client-ip-identification]: ../api-guide/throttling#how-clients-are-identified diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md index e6cbf7238..25ab4fd5b 100644 --- a/docs/topics/3.0-announcement.md +++ b/docs/topics/3.0-announcement.md @@ -870,7 +870,7 @@ The `COMPACT_JSON` setting has been added, and can be used to revert this behavi #### File fields as URLs -The `FileField` and `ImageField` classes are now represented as URLs by default. You should ensure you set Django's [standard `MEDIA_URL` setting](https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-MEDIA_URL) appropriately, and ensure your application [serves the uploaded files](https://docs.djangoproject.com/en/dev/howto/static-files/#serving-uploaded-files-in-development). +The `FileField` and `ImageField` classes are now represented as URLs by default. You should ensure you set Django's [standard `MEDIA_URL` setting](https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-MEDIA_URL) appropriately, and ensure your application [serves the uploaded files](https://docs.djangoproject.com/en/stable/howto/static-files/#serving-uploaded-files-in-development). You can revert this behavior, and display filenames in the representation by using the `UPLOADED_FILES_USE_URL` settings key: @@ -962,4 +962,4 @@ You can follow development on the GitHub site, where we use [milestones to indic [kickstarter]: http://kickstarter.com/projects/tomchristie/django-rest-framework-3 [sponsors]: http://www.django-rest-framework.org/topics/kickstarter-announcement/#sponsors [mixins.py]: https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/mixins.py -[django-localization]: https://docs.djangoproject.com/en/dev/topics/i18n/translation/#localization-how-to-create-language-files +[django-localization]: https://docs.djangoproject.com/en/stable/topics/i18n/translation/#localization-how-to-create-language-files diff --git a/docs/topics/ajax-csrf-cors.md b/docs/topics/ajax-csrf-cors.md index ad88810da..4960e0881 100644 --- a/docs/topics/ajax-csrf-cors.md +++ b/docs/topics/ajax-csrf-cors.md @@ -35,7 +35,7 @@ The best way to deal with CORS in REST framework is to add the required response [cite]: http://www.codinghorror.com/blog/2008/10/preventing-csrf-and-xsrf-attacks.html [csrf]: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF) -[csrf-ajax]: https://docs.djangoproject.com/en/dev/ref/csrf/#ajax +[csrf-ajax]: https://docs.djangoproject.com/en/stable/ref/csrf/#ajax [cors]: http://www.w3.org/TR/cors/ [ottoyiu]: https://github.com/ottoyiu/ [django-cors-headers]: https://github.com/ottoyiu/django-cors-headers/ diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index e5683360b..5628bcaec 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -598,7 +598,7 @@ For older release notes, [please see the version 2.x documentation][old-release- [cite]: http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html [deprecation-policy]: #deprecation-policy -[django-deprecation-policy]: https://docs.djangoproject.com/en/dev/internals/release-process/#internal-release-deprecation-policy +[django-deprecation-policy]: https://docs.djangoproject.com/en/stable/internals/release-process/#internal-release-deprecation-policy [defusedxml-announce]: http://blog.python.org/2013/02/announcing-defusedxml-fixes-for-xml.html [743]: https://github.com/tomchristie/django-rest-framework/pull/743 [staticfiles14]: https://docs.djangoproject.com/en/1.4/howto/static-files/#with-a-template-tag From 16f5d42cbc3e966ab2d4d8dc00163942f2769e43 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 1 Dec 2016 10:11:25 +0100 Subject: [PATCH 351/457] Add additional link to HTML & Forms topic page (#4726) Just makes the topic page easier to find. Closes #1673 --- docs/api-guide/renderers.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api-guide/renderers.md b/docs/api-guide/renderers.md index 648eafdde..236504850 100644 --- a/docs/api-guide/renderers.md +++ b/docs/api-guide/renderers.md @@ -123,6 +123,8 @@ You can use `TemplateHTMLRenderer` either to return regular HTML pages using RES If you're building websites that use `TemplateHTMLRenderer` along with other renderer classes, you should consider listing `TemplateHTMLRenderer` as the first class in the `renderer_classes` list, so that it will be prioritised first even for browsers that send poorly formed `ACCEPT:` headers. +See the [_HTML & Forms_ Topic Page][html-and-forms] for further examples of `TemplateHTMLRenderer` usage. + **.media_type**: `text/html` **.format**: `'.html'` From 932d04a4beb79afa7a52bfec7f66bb6ccfe53814 Mon Sep 17 00:00:00 2001 From: Asif Saifuddin Auvi Date: Thu, 1 Dec 2016 22:17:36 +0600 Subject: [PATCH 352/457] Browsable API tests asserts to pytest (#4725) --- tests/browsable_api/test_browsable_api.py | 18 ++++++++++++------ .../browsable_api/test_browsable_nested_api.py | 8 ++++---- tests/browsable_api/test_form_rendering.py | 4 ++-- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/tests/browsable_api/test_browsable_api.py b/tests/browsable_api/test_browsable_api.py index 3d49c353b..684d7ae14 100644 --- a/tests/browsable_api/test_browsable_api.py +++ b/tests/browsable_api/test_browsable_api.py @@ -26,16 +26,19 @@ class DropdownWithAuthTests(TestCase): def test_name_shown_when_logged_in(self): self.client.login(username=self.username, password=self.password) response = self.client.get('/') - self.assertContains(response, 'john') + content = response.content.decode('utf8') + assert 'john' in content def test_logout_shown_when_logged_in(self): self.client.login(username=self.username, password=self.password) response = self.client.get('/') - self.assertContains(response, '>Log out<') + content = response.content.decode('utf8') + assert '>Log out<' in content def test_login_shown_when_logged_out(self): response = self.client.get('/') - self.assertContains(response, '>Log in<') + content = response.content.decode('utf8') + assert '>Log in<' in content @override_settings(ROOT_URLCONF='tests.browsable_api.no_auth_urls') @@ -58,13 +61,16 @@ class NoDropdownWithoutAuthTests(TestCase): def test_name_shown_when_logged_in(self): self.client.login(username=self.username, password=self.password) response = self.client.get('/') - self.assertContains(response, 'john') + content = response.content.decode('utf8') + assert 'john' in content def test_dropdown_not_shown_when_logged_in(self): self.client.login(username=self.username, password=self.password) response = self.client.get('/') - self.assertNotContains(response, '
  • Rover.com
  • Sentry
  • Stream
  • -
  • Machinalis
  • +
  • Machinalis
  • From 220be31791693f056591e2b9593b80d17c31830c Mon Sep 17 00:00:00 2001 From: Artem Muterko Date: Fri, 27 Jan 2017 17:44:00 +0200 Subject: [PATCH 447/457] Git add remaining tests for BaseSerializer (#4857) --- tests/test_serializer.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 47258fdd1..04fa39c0d 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -159,6 +159,32 @@ class TestBaseSerializer: self.Serializer = ExampleSerializer + def test_abstract_methods_raise_proper_errors(self): + serializer = serializers.BaseSerializer() + with pytest.raises(NotImplementedError): + serializer.to_internal_value(None) + with pytest.raises(NotImplementedError): + serializer.to_representation(None) + with pytest.raises(NotImplementedError): + serializer.update(None, None) + with pytest.raises(NotImplementedError): + serializer.create(None) + + def test_access_to_data_attribute_before_validation_raises_error(self): + serializer = serializers.BaseSerializer(data={'foo': 'bar'}) + with pytest.raises(AssertionError): + serializer.data + + def test_access_to_errors_attribute_before_validation_raises_error(self): + serializer = serializers.BaseSerializer(data={'foo': 'bar'}) + with pytest.raises(AssertionError): + serializer.errors + + def test_access_to_validated_data_attribute_before_validation_raises_error(self): + serializer = serializers.BaseSerializer(data={'foo': 'bar'}) + with pytest.raises(AssertionError): + serializer.validated_data + def test_serialize_instance(self): instance = {'id': 1, 'name': 'tom', 'domain': 'example.com'} serializer = self.Serializer(instance) From a8dbc2202811c85b84b3112dc2f317154906da57 Mon Sep 17 00:00:00 2001 From: Anna Ossowski Date: Sat, 28 Jan 2017 23:41:21 +0100 Subject: [PATCH 448/457] Updated Support section and added funding email (#4860) --- docs/index.md | 2 +- docs/topics/funding.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 29a11791e..f3897696f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -260,7 +260,7 @@ Framework. For support please see the [REST framework discussion group][group], try the `#restframework` channel on `irc.freenode.net`, search [the IRC archives][botbot], or raise a question on [Stack Overflow][stack-overflow], making sure to include the ['django-rest-framework'][django-rest-framework-tag] tag. -[Paid support is available][paid-support] from [DabApps][dabapps], and can include work on REST framework core, or support with building your REST framework API. Please [contact DabApps][contact-dabapps] if you'd like to discuss commercial support options. +For priority support please sign up for a [professional or premium sponsorship plan](https://fund.django-rest-framework.org/topics/funding/). For updates on REST framework development, you may also want to follow [the author][twitter] on Twitter. diff --git a/docs/topics/funding.md b/docs/topics/funding.md index 814de0a3c..91099cb3f 100644 --- a/docs/topics/funding.md +++ b/docs/topics/funding.md @@ -308,7 +308,7 @@ Our professional and premium plans also include **priority support**. At any tim Once you've signed up I'll contact you via email and arrange your ad placements on the site. -For further enquires please contact tom@tomchristie.com. +For further enquires please contact funding@django-rest-framework.org. --- From 1c437a793c3f3cedb1aa53f0cccec23a96cc3f34 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Sun, 29 Jan 2017 20:38:39 +0100 Subject: [PATCH 449/457] Removed unnecessary importlib wrapper. --- rest_framework/compat.py | 5 ----- rest_framework/settings.py | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index b0e076203..872441abc 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -17,11 +17,6 @@ from django.template import Context, RequestContext, Template from django.utils import six from django.views.generic import View -try: - import importlib # Available in Python 3.1+ -except ImportError: - from django.utils import importlib # Will be removed in Django 1.9 - try: from django.urls import ( diff --git a/rest_framework/settings.py b/rest_framework/settings.py index 6d9ed2355..b699d7caf 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -18,13 +18,13 @@ REST framework settings, checking for user settings first, then falling back to the defaults. """ from __future__ import unicode_literals +from importlib import import_module from django.conf import settings from django.test.signals import setting_changed from django.utils import six from rest_framework import ISO_8601 -from rest_framework.compat import importlib DEFAULTS = { # Base API policies @@ -174,7 +174,7 @@ def import_from_string(val, setting_name): # Nod to tastypie's use of importlib. parts = val.split('.') module_path, class_name = '.'.join(parts[:-1]), parts[-1] - module = importlib.import_module(module_path) + module = import_module(module_path) return getattr(module, class_name) except (ImportError, AttributeError) as e: msg = "Could not import '%s' for API setting '%s'. %s: %s." % (val, setting_name, e.__class__.__name__, e) From 31e9f7dfbba7ad967d7f912e2014d9ad291fca3e Mon Sep 17 00:00:00 2001 From: Artem Muterko Date: Mon, 30 Jan 2017 13:08:03 +0200 Subject: [PATCH 450/457] Add remaining tests for generics (#4865) --- tests/test_generics.py | 91 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/tests/test_generics.py b/tests/test_generics.py index c24cda006..59278572e 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -547,3 +547,94 @@ class TestGuardedQueryset(TestCase): request = factory.get('/') with pytest.raises(RuntimeError): view(request).render() + + +class ApiViewsTests(TestCase): + + def test_create_api_view_post(self): + class MockCreateApiView(generics.CreateAPIView): + def create(self, request, *args, **kwargs): + self.called = True + self.call_args = (request, args, kwargs) + view = MockCreateApiView() + data = ('test request', ('test arg',), {'test_kwarg': 'test'}) + view.post('test request', 'test arg', test_kwarg='test') + assert view.called is True + assert view.call_args == data + + def test_destroy_api_view_delete(self): + class MockDestroyApiView(generics.DestroyAPIView): + def destroy(self, request, *args, **kwargs): + self.called = True + self.call_args = (request, args, kwargs) + view = MockDestroyApiView() + data = ('test request', ('test arg',), {'test_kwarg': 'test'}) + view.delete('test request', 'test arg', test_kwarg='test') + assert view.called is True + assert view.call_args == data + + def test_update_api_view_partial_update(self): + class MockUpdateApiView(generics.UpdateAPIView): + def partial_update(self, request, *args, **kwargs): + self.called = True + self.call_args = (request, args, kwargs) + view = MockUpdateApiView() + data = ('test request', ('test arg',), {'test_kwarg': 'test'}) + view.patch('test request', 'test arg', test_kwarg='test') + assert view.called is True + assert view.call_args == data + + def test_retrieve_update_api_view_get(self): + class MockRetrieveUpdateApiView(generics.RetrieveUpdateAPIView): + def retrieve(self, request, *args, **kwargs): + self.called = True + self.call_args = (request, args, kwargs) + view = MockRetrieveUpdateApiView() + data = ('test request', ('test arg',), {'test_kwarg': 'test'}) + view.get('test request', 'test arg', test_kwarg='test') + assert view.called is True + assert view.call_args == data + + def test_retrieve_update_api_view_put(self): + class MockRetrieveUpdateApiView(generics.RetrieveUpdateAPIView): + def update(self, request, *args, **kwargs): + self.called = True + self.call_args = (request, args, kwargs) + view = MockRetrieveUpdateApiView() + data = ('test request', ('test arg',), {'test_kwarg': 'test'}) + view.put('test request', 'test arg', test_kwarg='test') + assert view.called is True + assert view.call_args == data + + def test_retrieve_update_api_view_patch(self): + class MockRetrieveUpdateApiView(generics.RetrieveUpdateAPIView): + def partial_update(self, request, *args, **kwargs): + self.called = True + self.call_args = (request, args, kwargs) + view = MockRetrieveUpdateApiView() + data = ('test request', ('test arg',), {'test_kwarg': 'test'}) + view.patch('test request', 'test arg', test_kwarg='test') + assert view.called is True + assert view.call_args == data + + def test_retrieve_destroy_api_view_get(self): + class MockRetrieveDestroyUApiView(generics.RetrieveDestroyAPIView): + def retrieve(self, request, *args, **kwargs): + self.called = True + self.call_args = (request, args, kwargs) + view = MockRetrieveDestroyUApiView() + data = ('test request', ('test arg',), {'test_kwarg': 'test'}) + view.get('test request', 'test arg', test_kwarg='test') + assert view.called is True + assert view.call_args == data + + def test_retrieve_destroy_api_view_delete(self): + class MockRetrieveDestroyUApiView(generics.RetrieveDestroyAPIView): + def destroy(self, request, *args, **kwargs): + self.called = True + self.call_args = (request, args, kwargs) + view = MockRetrieveDestroyUApiView() + data = ('test request', ('test arg',), {'test_kwarg': 'test'}) + view.delete('test request', 'test arg', test_kwarg='test') + assert view.called is True + assert view.call_args == data From 3001b56e069a8aee118c2cd0977d04aa16733af4 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Mon, 30 Jan 2017 17:11:19 +0100 Subject: [PATCH 451/457] Fixed Django 2.0 compatibility due to `django.conf.urls.include` parameters change. (#4866) --- rest_framework/compat.py | 7 +++++++ rest_framework/urlpatterns.py | 4 ++-- tests/test_routers.py | 5 +++-- tests/test_versioning.py | 11 ++++++----- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 872441abc..16bc41f3a 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -307,3 +307,10 @@ def set_many(instance, field, value): else: field = getattr(instance, field) field.set(value) + +def include(module, namespace=None, app_name=None): + from django.conf.urls import include + if django.VERSION < (1,9): + return include(module, namespace, app_name) + else: + return include((module, app_name), namespace) diff --git a/rest_framework/urlpatterns.py b/rest_framework/urlpatterns.py index 4ea55300e..2ce4ba52d 100644 --- a/rest_framework/urlpatterns.py +++ b/rest_framework/urlpatterns.py @@ -1,8 +1,8 @@ from __future__ import unicode_literals -from django.conf.urls import include, url +from django.conf.urls import url -from rest_framework.compat import RegexURLResolver +from rest_framework.compat import RegexURLResolver, include from rest_framework.settings import api_settings diff --git a/tests/test_routers.py b/tests/test_routers.py index 99e4391c0..dc3df2e7b 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -4,12 +4,13 @@ import json from collections import namedtuple import pytest -from django.conf.urls import include, url +from django.conf.urls import url from django.core.exceptions import ImproperlyConfigured from django.db import models from django.test import TestCase, override_settings from rest_framework import permissions, serializers, viewsets +from rest_framework.compat import include from rest_framework.decorators import detail_route, list_route from rest_framework.response import Response from rest_framework.routers import DefaultRouter, SimpleRouter @@ -81,7 +82,7 @@ empty_prefix_urls = [ urlpatterns = [ url(r'^non-namespaced/', include(namespaced_router.urls)), - url(r'^namespaced/', include(namespaced_router.urls, namespace='example')), + url(r'^namespaced/', include(namespaced_router.urls, namespace='example', app_name='example')), url(r'^example/', include(notes_router.urls)), url(r'^example2/', include(kwarged_notes_router.urls)), diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 195f3fec1..098b09b65 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -1,8 +1,9 @@ import pytest -from django.conf.urls import include, url +from django.conf.urls import url from django.test import override_settings from rest_framework import serializers, status, versioning +from rest_framework.compat import include from rest_framework.decorators import APIView from rest_framework.relations import PKOnlyObject from rest_framework.response import Response @@ -170,7 +171,7 @@ class TestURLReversing(URLPatternsTestCase): ] urlpatterns = [ - url(r'^v1/', include(included, namespace='v1')), + url(r'^v1/', include(included, namespace='v1', app_name='v1')), url(r'^another/$', dummy_view, name='another'), url(r'^(?P[v1|v2]+)/another/$', dummy_view, name='another'), ] @@ -335,8 +336,8 @@ class TestHyperlinkedRelatedField(URLPatternsTestCase): ] urlpatterns = [ - url(r'^v1/', include(included, namespace='v1')), - url(r'^v2/', include(included, namespace='v2')) + url(r'^v1/', include(included, namespace='v1', app_name='v1')), + url(r'^v2/', include(included, namespace='v2', app_name='v2')) ] def setUp(self): @@ -367,7 +368,7 @@ class TestNamespaceVersioningHyperlinkedRelatedFieldScheme(URLPatternsTestCase): ] included = [ url(r'^namespaced/(?P\d+)/$', dummy_pk_view, name='namespaced'), - url(r'^nested/', include(nested, namespace='nested-namespace')) + url(r'^nested/', include(nested, namespace='nested-namespace', app_name='nested-namespace')) ] urlpatterns = [ From d610d150f1f2bceb7769c15c0214567870013a41 Mon Sep 17 00:00:00 2001 From: Artem Muterko Date: Tue, 31 Jan 2017 16:51:09 +0200 Subject: [PATCH 452/457] Add test for pagination when limit not set --- tests/test_pagination.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_pagination.py b/tests/test_pagination.py index 9f2e1c57c..dd7f70330 100644 --- a/tests/test_pagination.py +++ b/tests/test_pagination.py @@ -370,6 +370,13 @@ class TestLimitOffset: assert self.pagination.display_page_controls assert isinstance(self.pagination.to_html(), type('')) + def test_pagination_not_applied_if_limit_or_default_limit_not_set(self): + class MockPagination(pagination.LimitOffsetPagination): + default_limit = None + request = Request(factory.get('/')) + queryset = MockPagination().paginate_queryset(self.queryset, request) + assert queryset is None + def test_single_offset(self): """ When the offset is not a multiple of the limit we get some edge cases: From b99272c4251b718a1ea3715205abf1fd3be89039 Mon Sep 17 00:00:00 2001 From: Mariusz Felisiak Date: Tue, 31 Jan 2017 20:57:52 +0100 Subject: [PATCH 453/457] Fixed `dedent` for tab indent. --- rest_framework/utils/formatting.py | 25 ++++++++++--------------- tests/test_description.py | 6 +++++- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/rest_framework/utils/formatting.py b/rest_framework/utils/formatting.py index ca5b33c5e..78cb37e56 100644 --- a/rest_framework/utils/formatting.py +++ b/rest_framework/utils/formatting.py @@ -32,23 +32,18 @@ def dedent(content): unindented text on the initial line. """ content = force_text(content) - whitespace_counts = [ - len(line) - len(line.lstrip(' ')) - for line in content.splitlines()[1:] if line.lstrip() - ] - tab_counts = [ - len(line) - len(line.lstrip('\t')) - for line in content.splitlines()[1:] if line.lstrip() - ] + lines = [line for line in content.splitlines()[1:] if line.lstrip()] # unindent the content if needed - if whitespace_counts: - whitespace_pattern = '^' + (' ' * min(whitespace_counts)) - content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) - elif tab_counts: - whitespace_pattern = '^' + ('\t' * min(whitespace_counts)) - content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) - + if lines: + whitespace_counts = min([len(line) - len(line.lstrip(' ')) for line in lines]) + tab_counts = min([len(line) - len(line.lstrip('\t')) for line in lines]) + if whitespace_counts: + whitespace_pattern = '^' + (' ' * whitespace_counts) + content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) + elif tab_counts: + whitespace_pattern = '^' + ('\t' * tab_counts) + content = re.sub(re.compile(whitespace_pattern, re.MULTILINE), '', content) return content.strip() diff --git a/tests/test_description.py b/tests/test_description.py index 08d8bddec..001a3ea21 100644 --- a/tests/test_description.py +++ b/tests/test_description.py @@ -124,4 +124,8 @@ class TestViewNamesAndDescriptions(TestCase): def test_dedent_tabs(): - assert dedent("\tfirst string\n\n\tsecond string") == 'first string\n\n\tsecond string' + result = 'first string\n\nsecond string' + assert dedent(" first string\n\n second string") == result + assert dedent("first string\n\n second string") == result + assert dedent("\tfirst string\n\n\tsecond string") == result + assert dedent("first string\n\n\tsecond string") == result From 2ec3db81777234c7038c0d6a4edde5162fc1edc6 Mon Sep 17 00:00:00 2001 From: Mohammad Ashraful Islam Date: Wed, 1 Feb 2017 18:20:06 +0600 Subject: [PATCH 454/457] fixed url checker ':' to 'http' (#4678) --- rest_framework/test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/test.py b/rest_framework/test.py index 241f94c91..87255bca0 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -114,7 +114,7 @@ if requests is not None: self.mount('https://', adapter) def request(self, method, url, *args, **kwargs): - if ':' not in url: + if not url.startswith('http'): raise ValueError('Missing "http:" or "https:". Use a fully qualified URL, eg "http://testserver%s"' % url) return super(RequestsClient, self).request(method, url, *args, **kwargs) From 3c93c3f7b44b878f9970f9f89a5237b6aabf0794 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 3 Feb 2017 16:54:13 +0000 Subject: [PATCH 455/457] Added Rollbar to premium sponsors --- docs/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 29a11791e..b9806c544 100644 --- a/docs/index.md +++ b/docs/index.md @@ -75,10 +75,11 @@ The initial aim is to provide a single full-time position on REST framework.
  • Sentry
  • Stream
  • Machinalis
  • +
  • Rollbar
  • -*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), and [Machinalis](https://hello.machinalis.co.uk/).* +*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Machinalis](https://hello.machinalis.co.uk/), and [Rollbar](https://rollbar.com).* --- From 79f431c44a215aeab2aa2cacb8587995dad9e709 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 3 Feb 2017 17:10:52 +0000 Subject: [PATCH 456/457] Update sponsors on README to include rollbar (#4876) --- README.md | 3 ++- docs/img/premium/machinalis-readme.png | Bin 14046 -> 12872 bytes docs/img/premium/rollbar-readme.png | Bin 0 -> 12367 bytes docs/img/premium/rover-readme.png | Bin 55611 -> 53118 bytes docs/img/premium/sentry-readme.png | Bin 26353 -> 24584 bytes docs/img/premium/stream-readme.png | Bin 20667 -> 19341 bytes 6 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 docs/img/premium/rollbar-readme.png diff --git a/README.md b/README.md index c26252ce5..609f99184 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,10 @@ The initial aim is to provide a single full-time position on REST framework. +

    -*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), and [Machinalis](https://hello.machinalis.co.uk/).* +*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Machinalis](https://hello.machinalis.co.uk/), and [Rollbar](https://rollbar.com).* --- diff --git a/docs/img/premium/machinalis-readme.png b/docs/img/premium/machinalis-readme.png index 4bdb020c2ecee7861668bb45e42f8eec304781f7..cd98c23c7b4dd2fd1bd8e79f401707bb7ba8d416 100644 GIT binary patch literal 12872 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 Pul!}D6{N}}-Us|Ikovn{ literal 0 HcmV?d00001 diff --git a/docs/img/premium/rover-readme.png b/docs/img/premium/rover-readme.png index c9865f2a93c1d67adc2f175cd422e764f7fe3a66..b8055d62e5804df24f131ae27ca142655faa4441 100644 GIT binary patch delta 52468 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 C>*S1s<>*FY2{omKg$)QGFZ4CETEr#h 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 diff --git a/docs/img/premium/sentry-readme.png b/docs/img/premium/sentry-readme.png index 1e869f3b1dd36442faed67d15705890aff7d4dd1..5536ce52fa15e0b9e20b3f8a7f3f6b6df3fa9759 100644 GIT binary patch delta 23710 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 literal 26353 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 diff --git a/docs/img/premium/stream-readme.png b/docs/img/premium/stream-readme.png index 955c11429c53bd18cbfbf888dca7663e134264eb..fc3733c704818f68eb52c453df9ba06eb288d21c 100644 GIT binary patch literal 19341 zcmeFZgO_B>wl7?^ZM&+=wz_O|xw>rIwr!hTw$WvE*|wSG?w5O?bMM{X8TXCvA9y)N zX0A13{$e7qB4W1_6Ob|LYA3l9hu4lYx$7iUj9 z7ekLPcFyGgs^q`w5jS-V$5V}V`^(^=i&^+vHY*% zfWG~2;QvQS4?~B43H#fL|FVt$FU@&XOr7m*T>o-`x}BwqAS?gh2K^`be?0Opl$gD( zgOjPVGY}@o@o&gKY5&Qu`9ER=x&BMWKZ*YdP;{~cT59;0UV<$D7VuBnfAVYnZvy^F z{0HDK*Yhe_dYIa1idzC5`nMIVJV3Sn*OLE4irL%PJE=Mt8k_#5+24?Vkp7eVZ#i23 zki*XNPdWc6`3KU(nAh3W$ja2%+G%G;Y*nt6&F zx|j;GGP7{;GPCkBvyuGkf(@w0fAIW6gTG3|oJTM{-#&VM2Q zS^jS*Kht01=pWa!U2*0H3JC3864k9f`ABvNQ;Z8d4OK|LOW2bcpf9m z5ixMy7&*`IKs%)yW>Tn7{FGB}#EfgCoYWBP(sgxP8aK_qxc0eEopl}H-C(6uNy!*y zfJ#I=NXmX-%m4{&aEE^Zb;*{fDju#uR`& z%ZXj6U8{ns1R(Eq6kO6l0l+WyPT{5h6e1Fq#z^(*~%*pc!vQAN?B6WUzJJ+ z79}hQLqmELoRwD$Wcn&13|mQ4Z&ex^5~YKy(XIrd4}$~H zR0J!|cz{ew7!WB5QDD{Is{Wsd|L3Ux{~q<*M`7L>1C$KqR$YpM1`X+()Ym2{vNZuC=`rIaSWytkwDerC8fc{%q-1cD+asAp(w>A z_YoyqD1tEw4tO?Z%x-$hXcP|cVXdiCB+@}k()HTjA}`UNa9glE}#em~Gu6mRwjZsIQ%ut;w2SlXY&kBRWX?CVSc_O> zR!A1Okbn~sK|sodB$Qy>H0x~xD#7D;UPyO0BcmkKDr}{+{uWB|jvwmi#GffCR*5r5 zDMN(TMTdYX&D4cD8L6Yi4Weh6I_!fGBFXN#D9cQ6;$)>sP9u&#(bdL{4<USQRJJ@Rj#(`mmMVZ(Y+{NibESm}Xa6-huJW$X8&npHA{U z(k%03jcH771d4bn>pm1~6C;QG+fmBeoGdiT%_7p{kxH+=Gd3*@Li`iwanc}D*3A~W z!q?W+w+*-^E;#eE&ei9pN7o~6j@r%zs1&NmjnNYrB@}lmlQ_kXpO~l&ES5>??|AY_ zZElzQSNb~0r@b&0A&b86KZd@POWMxR^nLuYMLmn=Can_<;&$zj&eK@{c~=ru(PAB; z?n1x+IEUf^<3op?u11jL?tiwA9ji&yjELs6r2P)jEot~_q=Myc=egJmO_ss@;+4eF zmffehLeZzaLt<|qJJfaT7V_?n0RA!!;9hKl;_;w!JU7tEk}k#ou7OBFE0B9U!~Gq09yZ*cc-cp}wvOb4xpmWW zRGHcinBsu-vJBGJbq_O@E^9NKUz<}%N}yfIRrZ+!W_Wq;wP%5LNT zFcP5)2Dq`4A2Y4+3TUB#8-=^N-bJlE#J^zcJCJ<9B}L#4$gjP5DX2!$kgAgkeH{7J zIADqcZ9EW9&go4_(!QQoq2pQ$l}Z;Nbp%1YJW94_f*%*te39mA*O4~5b(mvMC=pgt z6KEITx2lmoeVVUJ1uUR3M8JiWXO_z@#r*m0zWu=@o&_I)cnxqON`O^NK z6ij(P-oW8x*dMyYrLw#Z+y2=ySJGWQrw~=cakRKZ5n19A>2?`X$j^3sv`kW_-q9Nk z64Zp@7r`*4d<^jhib#Y*@q04bB3|hrYMR_Tu7~hvicX5S@<%f!1g=Z;+5|pxh@(hK zQk%;$+G~A}*j#EF+@B^xdxOBfwRDA$W0RP291ANmC#G^vc!w)piRJN$G^resegHr) zoMV(QG9*rvn(|J7{=Sf}3JfI*l%|9TLyrMoHP{etWGEdPC6?S9`>Rrv zv@CNB8dK>Y%ggzQc$YPZY~}P%3)wYvH0T$U)i{}22$sl~6(+I%qNeE-B0Qal7&IJ8 z78$MJDtc2TKI!E+Qjfc@IQ2s7nanEbP1mCSRG@yXT%bX_P+&`}1?%2tW9=e-uLm1N z61L=tC6rn76lp$=`#6W149_aSW{z~+H$vn4;f2H2`3q*xVTtcgmw5Z~Z%_ z6Co$)Vc}xUpvI)*>SC9E&#Bag9?}}dtf(fFJ^t%z^OoI@pfAU2L&os04 zP>T&{7B4CN+Fr+F34NftgT62f<|LLcGu(Uta|4Ga6_G$e)(J9)%eL_p{6!$+PQ$9YLyk4K%o>>`pb)hA|e!nzn>JM<2LV0v$W$i%*jSmgC zHn*4JE(|h*KfuA8QO<>Ltq-@&ecjTMkl$t!_@>+Q=XF~*MxvuAY2!|Zq&AU+e1PWO z6Iza6$&TaDy&3bHq0yi)9H5;==8HVZn1sQIQnUoW3l90WGwU=O#LL^mUxo>aJ1pJ7 z#&O1VUHoJBV%+adDGA#3`~BX=&Z5Wh(j?YKG$)`VI2mvVno#IJ_##NSDufZ|z<46a zU1Zq;W`=u!>fGn$?}Z%~#0{1a{d~V@V2{WJ4b)=cpb|zdF%5d1vp!}s1O4utiNKyi z)Rz&2nmx%aqbRS}QI0xt1@*doFN4c9XMWAJu{R|xUmHH06t^L2gLTWrqFvi-%>yN# z+%N4Dkg-}1u|I$!IzZ!#e!0G1X<~z8JVw)zSxS715GOv7LWvI6=wUO*WSe1oHLs^2w%q%$7|1nPe#EOM(=p%18mw*xW^e{;d{~2Kow^zABlYb>WF+vW@ zX=kR?fA8qE?w+2 z4rug7xT;@vHSm1jHyE%JzDFo80R|PDMUnAyhpLP*l!568GH(U+dJ2NP9Q7N(g-8a##a?Wvy2QO1QJ+s0QGHR9#%w)m4zgj zG;k1MwM^;j;o#1LM?Z7+z#*w0xpW7ib*>!wOP7w(pksfju(mciaJi3%{18dkKdZrE z5THKM%`%tB4+l+zZQvKgPWWB;O^clq1V2SF?DrhSBWuBpxoL_Qx~fGH+wg>`0EB4?6%1c#TEX;0!@V~>Oeu?GD92d+^{ zRYBf1C~ZPjl?JJ2f{Xw9Dv9?mYz5fG3w}TQMSXtmj9^Q_<>f7yjk!Yv3D@-u#-M;W zG1d(S8cJ(q&Ixvt&Kyu_I&|DS?TvWKL)?<5BbR1>%Wp*jLXrqab z)Qmysvz3$yMU1b5Z$3DSo=91MoLel3TJYJBFICt6<6b8DRAxis23BHBT;A+=yob&| zM*fN?{fc{$WHVf3g#-&89;qAkp$nK&IenU#VnjDoO*SJJn!E@!>PTTSoUZLV2`J+g zZIaF0jwU04Yz_}LMYX%QbMD<}pqL9Y;9PCigcdw7lb2Hu)rVyCNk z`3M%uXAILK_wnX)Ao$P8jY+_fMY>VSqhJW6fiWf;Nc*=RHi2TH088_EUI_u9TzA!kNSSc z!=P)Hqb_NjUh{qySx2O|fo{+7>}`}YmK{-$3rb6&N&{jXn;~)V>nknJFuq(f3p4|H z!gsCw1THR06SR_2Q<8*^VqZ-;I2~|=GB?Z+#Y@i&5@(jZir!i=g(8mkq(*oEE#4s0 zpzI1l^I88ngZ-{eyGW!^ zN$k+wPJ32bXQoBbS|oPaKNi|}SuFERTm{5tq(j0SFf+9dwmXoI1Cg@Q;=7Ks*0|!raS|MP}GpB*C zhUAGp2*IUS12$0iamLcGV4eX|q7RZ{r9;ON3yD}EBCekW^ID3&f~L@-jc=?@vO9vY z-i4*5wNFZ27OI9sm+vQ`hTNlpM4{-rW3MUQ&As~Dqpe>BN^77Rfcw%7E_I`1FyM?g z;`T{|@R{>5>|?PbqlIuFv~)Vqzv0BY>R5=Hn~jnas@-w`p~h-LW}rZml-$Eq>%6Kc zJMt=@-*m&4Dt3}|k6vMwPGadOXL%xrXwQp^Uu2{g#W+`W*p;&cjdk3qW<+tDIQ#EB?_qA+RO(^^ezKcnOecZe8@ zfY`mhHQN3sQUQyXr7G_)?_%SyHf2?EbdT2eX7d19U53p41VWiEJz8<2&LCpI>7%5<3F zmAztfzdl8FwXxW7QR9=&@T}j0yQ}#M^V3r*wz<+apg2H_ENEhQ6Y_p`zz26xccULXo&(Koh{5RGP~*0tntf8U=7ji3qJC0&h< zaG*5dgpTwmqD%l|w-rGPbW;@6se7?yh$nIRax@A3GL^GlLk=r$v}Cw-A(o=dpVC};j`l2$8t*xVYK0u?Ht#U%lYaXGy=Fts&vyB+xlHGXt$3pN?tZzypk zmP7=u{hIHC8Hb9q2j&aCvyrLZC+Fyf1i{LFfBz5+(rPLDQPCY1|=O zTJ-l!%z!f%R#V6^47h{?)9Jk&P4j~Jo=|q$8++>z^B;r5iwprWTxj~lKT3;JOmICb z*Ac&#yl9+!ttXO0f!(%PKC=xvce+Pg6qAyFd`nd^)PxW)5qpo&%JCszPEv-XB;|%z zS6(;AY4^a$#&8hgkqHf-J+4dQsT-K>Xn#Q-`uV3Wzan}Eanz<)RdfQ1a+Kv8O~2yq z%xAP)BUm#w+-?Q$MZDS>AaDP>oL`He9oQ$0$P_{1w=Z>SO?I11V=+B9Xy%xNW&=<^+sDwe2LVU zR^BpfT8Z(WKWA&w-y3pMI$s_xU@1#J9N6r18IcX0KEfLgYB5(&YZ%j&Ku$W={Eo$S zL!~>gAlM4rr%XUJ5%}P@<~JMCUVSd<2yL0T6k*33rAeh^Xi-$bGC|OsF3Ax3Ny>RC z=;%YTk}E$c@EEO1)wq6?sbGmr=_ta%k5u;!Aij%*(=mcStwYid>M=K?L{|B%y-NQn*yCo8AI~Y0b9pgtVcw$S> zjF5$nu;R3L5QO3))gIBM$w)RHE{n!L#`pUYg*E?*Xwil}Tvr$b3?I?S!!R&h+E6>H zdf~?u%tzgHMk*%!k z$4c8eQirVs7x+p$XaZ?K)31RFmtZ&~-5L1+3pv*2i2!OK+AdI@&`VAbEC7{Etb(r2 zOT|x4yL!dyo9J%AGZt+KHCiu;gLujwLd>5o%Byce5-^YfT=LRV2;!|<77zvBhC))! zOz>ThQN}#7#$lx*%%cc=OM!rwjnBNH!p8n1wD2=NT%>_=a_Y^vpbv?4ByP+_- zhLbru+-=D?oRWFB5Mj5=w{mAN)ueZXi;Hh!wfJg?sYt`a{gZc^KL!bn--8(1NkQQe z_loP^a{b+B%VOzz(&9L?4y4Woq#VlpZR2q&l9bO2Wk!w|*(6>N*5n7ma@3Y`LOtu4 zSj=GP4*VDn3d*fGBF1_fw>=eDgpeUT&6=WwppZ{x$LueOG}M`Q<5u!kTd6j zegM%i7{>0smWGHJeg1w7i9hr}`T;aKKZs}MaqQhB4uz-jH;J4ZWX=sY#@mO!g7g(S5?ZZG_5@ z6&?WOJ@+q7qT@yom+bz(ODi@Zf*s2SJu>r!hyPHfdAPK2g@KbmNSm!Iet`D%IVB zH#tQ*xg%yBeRh_>!>@e7#+UQDd8~3kk4N}_*Ft&z-rA7!fv(fm;My#X{rToKxkGkb z0`rx!1T1hO(hCj!9^2Z8s+7SU^p38%oS$8Y%scZD9!*jMYlggv5MKmNZPJQlP0ogt z5)FN4MY#3=Ajd$V&!~MwObNjkI;S+9`lKKoal+#NXjx(GEh0=o_WSPBa5|tU8 zd5!Dpi%FUI^JP{0nr`#vSj4ARO;L!Nut}@{tSp9p|9lehrv8x?{oWCGW%y9n6Jm`! zA5_;A3&OPu4Mo_2rnl<{8d;Y}3V9+zT;xUUB7_lMIDxn?s;B`f()T)gkM25lORZQjG{Z6j7;Uip#_nz1laC3|H={zY9hS|N2 znqb$485Iih_1IPHhdJsi({C>zEIKy^CkZOu@o|ryv^mviDuLdCDg$2`zIZyy6X-MK zv%q-k1?g8XO(Pg4ab{uu)*p5K`J94+V=9RH+GRyt2=8Tji zwDCxP9RsC4kV@7iNF(&N3NpYYI9Ggu>y6J?cjz3_v8;JTP})8)yr)K>8~ff#1R7aK zTK5!!S62Rn1ZmJ~E+OsQ@|LYRzJpr`Qj$qiLMkIfQOL9E5une(NZBCipmVDvV5oE` zxEfMnbRFg`1jli?y;f))GQ3*SmN=PBD@+xn zrlAFmohv$7Zkdf~K)N-aXJf}mYZlgHV6|%o<*CA-irFE+f-uC9@uY(py}43j=rEhU zXtRhtrX;er3w?sMk6r0gnUQeEOw(u>32dIc)77Uj8lJZqWsd}WTy1h7kC3-uJ{-qnvSrse(42DYg8 zsnp8nn@5Al|i>Q2rIO3=TrkA{vo9;$LC zx*D-(cj+xzv*CQ_-^>>Kd#TV$#U}=8(Uck6VyGV)t=z{? zPxIa&ayZ)MWDZtkB4$uf#xAPSqetls#n_0L5>}DK+!CG`!TgFW>e{IWao|Oebt+Qf z{h`wlKAg;qwveNIR+EtO?Gi#{KSuy9%%S32v!WIqVJU~#S%$C>acW$2bR!kc_ zdm$|eIa6v(a&%XP$`@AD#gT<2i!?`j66SgUVRB@wvjTZ#rg1bhuvt#9F9?jJ*8rQW3OI-~}+r*3}ux6YG z>$te9lk&_b_@C+y;tY+FQNoQ!hVVk3xxIFzPm5S;oTZWOa6WKkdV@MD{Va)E+1)EO zW>^Yd4a-?IyRS-2@$ccVbHn+D?SB%7Kv|+Noc#?Z46HlrD-B3nbT1^|8mnG4aDR{` z6DBR28e=k?LFzT$t;X5}N1VKr6b3N7&l@aSR=BG@39#4W@0rGE%Q8aih^!~iY^T{I zSD)Lf7Xt2K(CYL`jcrtUeMqh_o2y-1?%43MGtf5VOos~FyTQabOSxMJOhv11fx$;i zqJ>jTdxU+GsM>X0#On zThx&_&q;jnVlTWngOVg#ISt{70kKw+VlS>Y89>p$DH%1(4D$|6^zZV{d?HQDYVt0j z*BAUiv4vt^b4z=@p8R?IZUpcrP-7K3fh#nJW@UX%e6eon3M5!JfQjA9oR;J zNvxil>$1m{NuSGA!mROoxFcJ1$ zyL9Ne9#grLO}`Ue`tl>YRUTEfZczP$sm@+FpsMqwhnWuWEF<+(Vzt;0==eTz(6%;W z#p{IX&Y!9ag&=Y~_zoO{^ScxLa+Z8>SatJhV4}Q`b!wnPZv59p&V6Qd`XijSUv1DI zI9Xc9?}JrF&-3G1+~`Xw69Q9qTQ^axVNQGpDWP*SBa zy}`zi9xIoVo)#1GRB!BN_^J+-0 zE)3tMEUkeD6yNm^QO?P7A+bRYn~mvRwJ)Wi!e=irvheAQwulQOKL#aYV_J{1T0l2> zII~ZC8EsuHzvoC?JV|vpFBcseqM>eMI8ZQd8J7Nnr zaLeLniV|vFfjT{y06Zv%72jL(yj;`k$qVUdJVkMrIpopxTCSqh=3uPEqabQxL7$3I zBvs{4eS6nC0(-D<)uWQ~diQqG_S*ea&E&;mqHE{n%%Aaz>wFCJ^ry)ljna%=m1xu{4!t8a^o)dt(yAx5W&7qEVNg+LJSj>aMwB1pbNi5=;1w>~+hGsAKc zDBu~@SH%Sdl*QjplIc31I}lUZl{V-#*v5EA9o#tS$+QwHPT*%7J6%`3&z;oTw`i;k_SK2L4PQR;eI}uJxkd}l?9;vAA6HVm3z0d*eiaX|g$AgBk8kypV zf%ExsJullf3t3J>4u0}95K|>~Am}7?pc(7nYPX}LMqrM{S_%32XfpM%rq%sqWtqH( zZ$}!*_M{+>b&=_NCVC1FdGI`Ti83N>0Qfet=wAEbaI~?Kt|iO__=C#*Yj4PhnYSoX zo~^8$h}<$hRmdSempDQs&KU$6WGO{kP*IeM}hI{zEGPG74&n$8lXtpE8JZ*@gkJzF$@?Hrxe2MKYU8EQNzU$rfy4t&bh3CD?A^;(ktCz$MJET`C4Ff#MazKUY>?Kbk zClz&yMj9T1^fBb(r0m$nPc~H>&zq=faT~r%(3=oI>2Zc8_aRehMh^f1iO@#*4wv}=6e_uDN41Y9bLPfdTY6`wD%`{BF zhb?GCgk6g_e=oMsz)-#M3bjaYF@_creiG@jd8)|8l#`pOeiZKp`5|i8CcJ76GT+uPjM-cWa6oYVY#lX)r^6gxJ9%}T{4cM3}O@t-T#0hyi<0L6lTm+t9 z`Xh16Kp@YCTZP@-B`e}<%iEpYg}%tKx?uT9KAj`mMNaHmeg9@M%=?2qA>l_goo{jK z$nEdpqzHuIkX^SgCoMgKs$WY&Zs)+{i~=62pYCd;A~OUVNhpv-jtKbXnAYTc>Nx8^ zhYIySf6qsV7qlj|H_>7eIs0A~2un!+sWTY@X3n?I7vl$=LrygB@7BTBG znBsTqU0dKZ6)NxQV}cjmA6)*4{&C;GX(7Q{)bP^GBEvZ;yqao}0N|m3Lh@4RuA7P- z1E5PAkDf(|poG3ug)+At68q#MUC?!&^^080lciO_#Ha@LJsscbXcGh7_LH9;5LMVUr6|2 zB7z@qE<6|!-`4jl9tT(yl1gpwan4Cmmv;rLg!A-G5f*2{PePCW_^le)Wa*DSrQA#g zC2(8xfzlQ*T|&=@rk*k*jMa%uuOh_DBQRc>W#C(O;R=8ZgX=5|I@V(PEAT78RLSPw z1iQA0!@)i+Y{6>58go%2U9~p5{?Zfzal;xDh5-$*ML6)EP|EkY7^$GfT_Tq=cnWO) zaD(o)xUHbnwqt`J!&|NswadjzDy@&5+csem_kdAr<0B;{0Q+%)>~+9~bw1TokNa!) zLPEl&+CIqexvd`diZ>O_s|e3gQP2eh*7LAmX4n(bP#4*A9qh_>@>u}Z@hA^sL`qjd z&oXRJ(Dvj48hTKmzH2LlbI$jML7@rcD%igN^po`MbYDHL$?OqvG*gi%yptE#jfr}o zVYzM!cD#;Ra8g9({4QiTP~pjp(}rA5h66%a_Rj1}k$boIIeGYPV-7V}11BFP^s227C(|DqyQuXaB zKnB6t@8TCQiI|jeBcAGrVDmjDwgC>>6(osBAcEoXo*#nM)vMqIzfMwd2S0oN9+qma zGFpBzZIq8ndfJDA`tvnk)=-q-6#fRB z87!%71^%}oJ87{$&C&QJeb9K|mm|7*oVMYaw zgK(nnAFDDl4cOP0MNC({Ta^L(kv-rn0@%Xh2y^cyip7~F%N#Ey_(<`SJ3`wVZ|Cdu zl*ar@Svs4L)+!y@9#t`SShn-4#)Xq&|jy}2Y!2)bhxXoq;WFP1#x z0S9q8iWBf8#XaSy^Sh^M6eCm0ySK|}X1H9;jL*~2IOEfPy-&ZdY+*gI{e56~!tmZ| z*7&D>A03#XBTF*m4o_&Z=PANAP^T{!S3qymLZ28^XxN2PG$WSmQ;@$m5QKQMNdK9I z#<|1kU{_n-|03|s^^VF|ao2|@UV2oh`nEGNWina|IX}qCa9#d+_U_WF)?p%NVmexdF^}nY?EoS>Y$qr5e2)GC{hny-6k%+P7LzCb#60QV+-1`%u$k*3d*G}wGje8ET?#REoEzV3g8lD_;`NZ6N*+b|^?#OFv z>@1<2RMSShO}^9iwJv;%D&LPqNA1Taz{emKHvx+kGjEo9l~2d`ekKja22Xko{?{k_ zupQ&*t%rws)w~p8CY0GovaJwNukMq^8!W9fCI*Q%=U0eQEc#Y zs%;0DijmTxjFiueN-Ro%meLjuvI!GKQK&I)@NXx=XtGN94_rU{1O4%^p_NR+&w&o!YQwomibw&4~pEoM7(rJ6BxLH=}ziPjG z^OH8%_oVqM#l9!=RT+*t{5rAOl9?}L2V)Z+kfExa@LhM&)KLqm_5!;xupP%q%@;ph$$c@Wlu4uJChE!Ge#DLP?&uUpb`a&n62BYd7 zmpt5Z=2s5(gg%@lPL_HY%608pdQh=ROD?;5Ox?-Wvvlab-FBEY4VQf3U0z%Hjx6@o z1s~tX17L3=f1$w-fw%J;_7_4v8QQtJM<2W&IsgCmDw0;)bQT> zwn%!Ns7ex+1PG9n=-R;fqHOYbm+-w_^NZkXvg+7bFPSKeBx{mO9P~CZix)w)a#b@I z`LY*nzw>rGfB51r|0Lv|bah;l)2kj?uImohBa!?8Ioaw_`SZkDRbpwXwXFISoiV{R zZBps1@yZ@x9vmJrQLsQXBe{QH<1)B5Eo98D7wQP`tl4=Jr-@;q>G$T^ntba>CR0h(8-fS1N}Xz*dCr&dTcczVH56YcLJ6n}a1!O=DA z@~2B$YU#yK$-eUp)d?i2;GBg()^JlQAlj8S)dhmsYoIqk&jD3UluM&~W6MKY%5FYx z<8|3RWo~m_Avtj4dD(Mj#Upy|f+C-pLN{24F_nj&>2B6m(maxjd*!&{=G4&3Q*YB_ zqTSGka&WjixBJH+C# z;mu*~F?^K4C1A+vnf9Dn98yKz_e8T~+ZuJNNG@_*^a3Mw-90dl7n;6bzCcU_CBX2& z2H0Kw`R$flBb3rV_>)F05|Gc6({~0-Ip3$Dgd@}h+hLCb)sI2?nr5@PcD0lXPpuN< z=={{?9xM7Wb87dyfjM5O-8P1fY#^HwiknYnu^VcLWs_h1LtfUBTuCrw*smW!FueTs z0v}!TJ~3sZ9J2miD8zsrVxdC{Y39d4i5XdIc6nGs)CSX^7r)$^8RIjUND3YD@+L&U zY=@jaUY;fOd6a1C-2LS*i)^`;)cFil)@a7Ds#jepy^7Ap&H^GeW(eRSmSLDX(h}dO zegob*>u~xDY}2jFna3et(3S-h1<89^+_EP?<9IrgeVEDLPb2k1z6NWXRA({&vmfDx z&l?g1JItxYUjt*`R9b42A{m#uqj5$bM;RG(u_A>q)s6<1H~sr+UFnT1c3>kpgT-f z@+>c}VFNn#xbkl*7_}wL@P05Eti|XU>OXAr;fqLF-aGM8W#5#MC5yPGjjuIUD#oAj zL#)p<&UCqr>y68yuWP?RdrzusYR!_XxFciD|K44W*bGX+pBZGQoTBKbr0l>@n2*DH zw!G4#cZRhM^%_S=1`MuZG2ZM_L2p77WE@eHP@1t9a7|~bTB0tu^@MQV1}0qeXxW-Y zWo8Zqf0dUs(v$rTA1{0%X}WiDv&^9aXPAJOZt-!L?A5do`VdB=w@7k3TWgY~7Fthw zL`TVTG;W5xf5=EKo3In%85X5gK}7Tw?HxVh(k%^C%FFFQ{5e-UXVlBL7jyirvawQm z1HN$nckd-kBzUUyqJ01=U$&@9q2wi42|Sc0FZ^>{J--xS$AhBn{Eh+F@1DOrSZFT8 zAEhG{kQ>G_u8gzZZ(`|Af*zpL$UeG9_|}dOnjm+w$8eebeiCMaruIXaC}wa=IC%pKR!6h!P7QM{bRtHgka2;rtv%Zb_IGgoJ7xD)K_sM3P;5(fu zinFu=JCc$TdnQT0E0K=c4I_h-HivDs)xs$GinZN>(Zg`zcxnm>8sIOfNd6o&SiO!? zR~=0@kx?0wJv@J`>5X}A#@!_cdt<2d@ zja%(wh(J&-bD9CUWN#asbl}eC!HVzwAHj-LS4}Bv7L=`#)IdF>8-g+g6T z*qnyqL}&7dRD5O8tyWpn-iDp)b)rk!LEMaH0VOU^?_A>3H}=AK#Z{*aG2rho7vLSM zD;@cS@0OtV8Gq8D>5s6-^k4Z7pN_G%X9honqw6avx#%+B@o~~i4^{6|p&mNY@{(~u zpS99nbL^hzrT2VI_H+b0YQtl!mroo2*dsrStgohw?|Q&q6lBnQdhEa%6C5F7`J8tO zR7z9jsKppN(KqurZWL;f>-?GMq%v4p@9UOi&T8D_ZQifOQuw${)nR6=(Ou^lnovpb$$Ti3d(QVh`YT|TCH%vwPX!W z6KF_-usg+i8%_xak{0cIFhs4--m<{C^|N@v2|Aj1kRpJ>loIL>E4Iqu<{%!20&`y` z$W*?#(Jnx8O5*%0ObR=IoMU`euX$oYfnna;@ta5!kto(=?p9gOSJP&39WtH@i}Sn8nE* zsj3u{(@yF@9(aFhT9(qu`exJ_U}gBw_f2lf zg(kzjg)U{NYYJ*z&nHBRQaN*K)xY;Xp)NJP!*+#qea{|F_8WBWG-Z^y8d!L{X;50B zAFFtaItKxPdid+V0OC33dWR$1A-hzj!t{obWGa$e zS(b?!jknzrDd<_HnG0GAv;LvN2`OGlbLYOGxGP!O0yH$tP7mZL+T{9dkwINtXPF2b zWoVN4784H>Y@}>-Tl0ic)j5Ik)UML1v<*^f~oQD!l7Vjdq zCQ3Ct5OSTCkd)%v^cKH@q1Y()^3TKcYCLUQmARQxw%KfyH1L5q0h z8jKMFO8We(74-Fxoi2C1_>LEUYYfM*{GYXl&UJt0x_;Y>@3H+3{nICK7^>$9%U$w0 z_kVue%E|2FCa03QmD0j5=Im}LjX84pPiW4K?~kh9C^UY*sS>%EH`|)y&3wBRlkN8| z4i9@*w@fQlsYa@JFPFK#j!WW=L#`R^clfft{&TBjC|jJ$;kU$BY3L3J&~Un`IBYyl~*5lsU7~bKinmyrg!(P6J?tw$%f8@4< zg<-S%*R;))GB0wUdU@+7sibu~wBIKExws)`?UnpurF{!_ZD(%T&9=MvLa+Z}^IYe= zkKuXGqwi0Q-?{Zllz@==!a2;zmYs!$S1jJOyiQxbXzzOk8Ky0l|FZo(Ik)Kb10IEw zmjvd&eWy7sC2Ol|{BFL+H@!@jTMB^(Mu=ssdQxyvhVQBGWdo_!z$xpJ(%P21u2i_@ zcyYrV7njGXK77yC*zuGuk`wN!oVYV+uCV(2LyrE|XB^%*S;pGl!9{6|S4xZeE zg}SZTd%h)q?~lE4bjl?*&m@){&z^Z@V!IArZ)TCGj6HFv?9yqEW;5RRJdq2IPmJ5T z@6KttX?n+u4EMeI6T0MwYtsMI8!jGA*!=8JjF)w#)0J0d8S%*}nBV37I!CTO;5hEl z-}8F^9$t2bs_GT5@0VWP4Ln2R#LYcTCQCJ@PP0HGMV-&BYF5VSg|W>?4)eqtBLOQ++(+Yeam+JH%iBYx*l2W&Iws$ zGkey)I&Qs!Ws04$S%ESPc7L`#tT-d%TqOVc>ob`B1O+x8yj93pmCNn= zqif64ca_wGWrQ{JdbV$id|1T!@6_Cw*9A4< zBE_`{hdu8bhC3XOI)Czv{g)6wQN#biX3uB%%*tIXRJ2Qsf7>?g^FLjVb53D6z4gP) z<*vX(MnoDXf#x$+b~#Ef1WpmUH~_cmHv&%wnZg5{0#v;$dP~67K`_ASk|F5u5mnHU zAty^(&0K&wr%Wi}0i7vw5;T2j6uXc&kVS-1RYjH=be@P2Xa>|LJi}Rp(G_H5!>Ex% cDlC8ef0E>`+jmxCKj?rgPgg&ebxsLQ0PQ_YNB{r; literal 20667 zcmeEuWmKHYvM4gRyTf21!3THOU;IwTEQx#a&!GBG%Fvw{{`(&&;N>MY4P7uxp=xc{3Vm61(%hBm7|rD zhdUe(^dHH=b^G6l{}+=!W-fmd`%8&`%O>`Ra}iA|cV`FBKPu36vh$GO5&KJ^e{cRD zjr@%z>+I;_X65b( z4zKs02>i409|(U`FY?OH$I3xh&JM27zog*f6%yn6k3Ij6DeLUu?55>nW?}V*v%gUO z(e&@of6>wVA38jI!vCc6kDh;ET3U#>dzyo-EIj@(U+`J`%lc)^Y`Ff>EXMU8p~c{~ zAfo1MX=m*#XXarg!NUy_5aAXQ;o%4J@cen;77*k5H<*8L@TW)C&C1Nf+3k(9vxCH6 zVmSVJ1@dyjXZ+v$|Ai{X^~aC?;a7h#M*&OB^j+-z9qH3J@Qu3WiV zG%%h+wdWXqUnZwa%p6Os9D}I}#J~qiF)4DAS-FDZHGJJfbz;_)V+bN8uSjc|LpW~N}iWVTtNTH)6 z(gOiQ+j;cV;k!QZ1nl0u?6=!j?t{(z!~@+e64yzNUZt;9n7Gn$kH5<`7}s)yYF zC;UgX|6kSr?;02SC)c$ZPkQckg6MGYS57c#(yXpprXK5*wc2Os9O9a<=PUI0FW~0P zlP%=wzs`ssH$C5SMh{!>N~_2rsCzZwu#{XYUMFltg-SE~X8 zvGCUQw)R_~pSGf=dwx0}g9BbwSxxh<0tYJ6d$=c0)W99rk5r@X?KXP2y?tJO#9=Di zX1&kLZ_<|57V{EqVoq!};WP|i-RAiM=qC#QKohI;za2#nKW(?W`>hYr&#uU!sLROc zTAc41B!B_<8g7YX6=ePzoEIG+sR>nuf#uQPpdf7(pLNSXW3*kHv7Yt4N|vhwEA&)8 zJNWuy(g8MR$+4tQ;$jDp$Bb?$b7fG!GkypCo`$Ng&wz|zT=X_=ISm6tb_I9oPgm?skvZJk0MWBb>$gv4=O{DJLQ}J)<+aUa3 zvctawzUPxZ8_C(vUL-)8iILiS!YU_XNzg~XVR)6)aR4JIg(4`~=?2a{i6-Npm{8Z? zam6gG3k{53gGHXhwF*M_cnCHJRUA;$zv@KqAqWW^AARlb;yXea1-$!_$%C}Gh^(^d zwZ=ifqNOaDIW-<3HNyZjFJJsT*U=n?FdF4O;&ctp68W+l@U0IIZ=wI=i@pj`_rf(y zM1ss?MEX|@NZ1|5kqCzx9TmaHH(f=uu{{h;If((|77=nFr-+G3Z<#!I_{y4XQN$bG z;Rzsj?z>OQl|g`}AR*4*OA}%w;bjfZ52`F2v`e}($64svA2WUUXue!2TOufvX9>q- zmMx)s!v-gl^VSlJeu4nLZ77OeDx+`FOUc+p&j>Zqh+=i@JT7oT7Vzc- zMT_muDVx1a`>W$+ot$aLJm$kLa!&dhE7D2o{4S-pcI1_EBj7tigm>G;_#6TKD{XX$ zlCF+p?`3pgSt`Pz0A!Kg)*`8O)xqAoRJ7Ob;gN6P3q_Hhvta&vHAqu3$jPL&8TWI8 z{?V29Pxb1b>h~HVt@Y~oNFU-u`08Kg7$sCJp-&*YO>TXAIBPy|ApbS;E?mz2T(exf zz6}Lt^J18EP>kzDeIaSKZr<#$JDEJp5~&NJ%}*zLg$Dy8b&@3pzCw4R1D-T+9uwLK zkhDWX4iIli@P|e~g0T^IIFB+~- zWYt`2dd}{BUnoN)GelSci*V!KgR>}sKH+LHo2C!@MRPSVUm#(#EabsIX|3V-axwL0 z6Q&s0J1lj8( zS43PV%AtEihD2BJhF`YU;3rTLf9Ludvk;~

    pjlIcQ4o>(`XbcjU>*qQ2?ZI)oLo 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 From cbad236f6d817d992873cd4df6527d46ab243ed1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E6=89=AC?= Date: Mon, 6 Feb 2017 17:36:03 +0800 Subject: [PATCH 457/457] Add max_length and min_length arguments for ListField (#4877) --- docs/api-guide/fields.md | 4 +++- rest_framework/fields.py | 12 +++++++++++- tests/test_fields.py | 10 ++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index b527b016b..22ce7dd77 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -434,9 +434,11 @@ Requires either the `Pillow` package or `PIL` package. The `Pillow` package is A field class that validates a list of objects. -**Signature**: `ListField(child)` +**Signature**: `ListField(child, min_length=None, max_length=None)` - `child` - A field instance that should be used for validating the objects in the list. If this argument is not provided then objects in the list will not be validated. +- `min_length` - Validates that the list contains no fewer than this number of elements. +- `max_length` - Validates that the list contains no more than this number of elements. For example, to validate a list of integers you might use something like the following: diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 6d21d670a..f46edef1e 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1505,12 +1505,16 @@ class ListField(Field): initial = [] default_error_messages = { 'not_a_list': _('Expected a list of items but got type "{input_type}".'), - 'empty': _('This list may not be empty.') + 'empty': _('This list may not be empty.'), + 'min_length': _('Ensure this field has at least {min_length} elements.'), + 'max_length': _('Ensure this field has no more than {max_length} elements.') } def __init__(self, *args, **kwargs): self.child = kwargs.pop('child', copy.deepcopy(self.child)) self.allow_empty = kwargs.pop('allow_empty', True) + self.max_length = kwargs.pop('max_length', None) + self.min_length = kwargs.pop('min_length', None) assert not inspect.isclass(self.child), '`child` has not been instantiated.' assert self.child.source is None, ( @@ -1520,6 +1524,12 @@ class ListField(Field): super(ListField, self).__init__(*args, **kwargs) self.child.bind(field_name='', parent=self) + if self.max_length is not None: + message = self.error_messages['max_length'].format(max_length=self.max_length) + self.validators.append(MaxLengthValidator(self.max_length, message=message)) + if self.min_length is not None: + message = self.error_messages['min_length'].format(min_length=self.min_length) + self.validators.append(MinLengthValidator(self.min_length, message=message)) def get_value(self, dictionary): if self.field_name not in dictionary: diff --git a/tests/test_fields.py b/tests/test_fields.py index 5de6546fc..16221d4cc 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1668,6 +1668,16 @@ class TestEmptyListField(FieldValues): field = serializers.ListField(child=serializers.IntegerField(), allow_empty=False) +class TestListFieldLengthLimit(FieldValues): + valid_inputs = () + invalid_inputs = [ + ((0, 1), ['Ensure this field has at least 3 elements.']), + ((0, 1, 2, 3, 4, 5), ['Ensure this field has no more than 4 elements.']), + ] + outputs = () + field = serializers.ListField(child=serializers.IntegerField(), min_length=3, max_length=4) + + class TestUnvalidatedListField(FieldValues): """ Values for `ListField` with no `child` argument.