diff options
26 files changed, 847 insertions, 52 deletions
diff --git a/.travis.yml b/.travis.yml index 3eb89dc4..71cec7be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,10 @@ sudo: false env: - TOX_ENV=py27-flake8 - TOX_ENV=py27-docs + - TOX_ENV=py34-django18 + - TOX_ENV=py33-django18 + - TOX_ENV=py32-django18 + - TOX_ENV=py27-django18 - TOX_ENV=py34-django17 - TOX_ENV=py33-django17 - TOX_ENV=py32-django17 @@ -21,10 +25,7 @@ env: - TOX_ENV=py26-django15 - TOX_ENV=py27-django14 - TOX_ENV=py26-django14 - - TOX_ENV=py34-django18beta - - TOX_ENV=py33-django18beta - - TOX_ENV=py32-django18beta - - TOX_ENV=py27-django18beta + install: - pip install tox diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 5edc997a..c87db785 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -434,7 +434,7 @@ A field class that does not take a value based on user input, but instead takes For example, to include a field that always provides the current time as part of the serializer validated data, you would use the following: - modified = serializer.HiddenField(default=timezone.now) + modified = serializers.HiddenField(default=timezone.now) The `HiddenField` class is usually only needed if you have some validation that needs to run based on some pre-provided field values, but you do not want to expose all of those fields to the end user. @@ -481,7 +481,7 @@ If you want to create a custom field, you'll need to subclass `Field` and then o The `.to_representation()` method is called to convert the initial datatype into a primitive, serializable datatype. -The `to_internal_value()` method is called to restore a primitive datatype into its internal python representation. This method should raise a `serializer.ValidationError` if the data is invalid. +The `to_internal_value()` method is called to restore a primitive datatype into its internal python representation. This method should raise a `serializers.ValidationError` if the data is invalid. Note that the `WritableField` class that was present in version 2.x no longer exists. You should subclass `Field` and override `to_internal_value()` if the field supports data input. diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index b16b6be5..50c3f2cf 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -72,7 +72,7 @@ We can override `.get_queryset()` to deal with URLs such as `http://example.com/ by filtering against a `username` query parameter in the URL. """ queryset = Purchase.objects.all() - username = self.request.QUERY_PARAMS.get('username', None) + username = self.request.query_params.get('username', None) if username is not None: queryset = queryset.filter(purchaser__username=username) return queryset diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 7df3d6ff..ccf84592 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -133,9 +133,9 @@ May be overridden to provide more complex behavior with filters, such as using d For example: def get_filter_backends(self): - if "geo_route" in self.request.QUERY_PARAMS: + if "geo_route" in self.request.query_params: return (GeoRouteFilter, CategoryFilter) - elif "geo_point" in self.request.QUERY_PARAMS: + elif "geo_point" in self.request.query_params: return (GeoPointFilter, CategoryFilter) return (CategoryFilter,) diff --git a/docs/api-guide/relations.md b/docs/api-guide/relations.md index 093bbdd0..31d59e1f 100644 --- a/docs/api-guide/relations.md +++ b/docs/api-guide/relations.md @@ -46,7 +46,7 @@ In order to explain the various types of relational fields, we'll use a couple o class Meta: unique_together = ('album', 'order') - order_by = 'order' + ordering = ['order'] def __unicode__(self): return '%d: %s' % (self.order, self.title) diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index aad2236f..d9ded52f 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -344,7 +344,7 @@ Here's an example for an `update()` method on our previous `UserSerializer` clas return instance -Because the behavior of nested creates and updates can be ambiguous, and may require complex dependancies between related models, REST framework 3 requires you to always write these methods explicitly. The default `ModelSerializer` `.create()` and `.update()` methods do not include support for writable nested representations. +Because the behavior of nested creates and updates can be ambiguous, and may require complex dependencies between related models, REST framework 3 requires you to always write these methods explicitly. The default `ModelSerializer` `.create()` and `.update()` methods do not include support for writable nested representations. It is possible that a third party package, providing automatic support some kinds of automatic writable nested representations may be released alongside the 3.1 release. diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index 84d310c2..270a3360 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -38,7 +38,24 @@ You can determine your currently installed version using `pip freeze`: --- -## 3.0.x series +## 3.1.x series + +### 3.1.1 + +**Date**: [23rd March 2015][3.1.1-milestone]. + +* **Security fix**: Escape tab switching cookie name in browsable API. +* Display input forms in browsable API if `serializer_class` is used, even when `get_serializer` method does not exist on the view. ([#2743](gh2743)) +* Use a password input for the AuthTokenSerializer. ([#2741](gh2741)) +* Fix missing anchor closing tag after next button. ([#2691][gh2691]) +* Fix `lookup_url_kwarg` handling in viewsets. ([#2685][gh2685], [#2591][gh2591]) +* Fix problem with importing `rest_framework.views` in `apps.py` ([#2678][gh2678]) +* LimitOffsetPagination raises `TypeError` if PAGE_SIZE not set ([#2667][gh2667], [#2700][gh2700]) +* German translation for `min_value` field error message references `max_value`. ([#2645][gh2645]) +* Remove `MergeDict`. ([#2640][gh2640]) +* Support serializing unsaved models with related fields. ([#2637][gh2637], [#2641][gh2641]) +* Allow blank/null on radio.html choices. ([#2631][gh2631]) + ### 3.1.0 @@ -46,6 +63,10 @@ You can determine your currently installed version using `pip freeze`: For full details see the [3.1 release announcement](3.1-announcement.md). +--- + +## 3.0.x series + ### 3.0.5 **Date**: [10th February 2015][3.0.5-milestone]. @@ -142,7 +163,7 @@ For full details see the [3.0 release announcement](3.0-announcement.md). --- -For older release notes, [please see the version 2.x documentation](old-release-notes). +For older release notes, [please see the version 2.x documentation][old-release-notes]. [cite]: http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html [deprecation-policy]: #deprecation-policy @@ -161,6 +182,8 @@ For older release notes, [please see the version 2.x documentation](old-release- [3.0.3-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.3+Release%22 [3.0.4-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.4+Release%22 [3.0.5-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.5+Release%22 +[3.1.0-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.1.0+Release%22 +[3.1.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.1.1+Release%22 <!-- 3.0.1 --> [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 @@ -259,3 +282,17 @@ For older release notes, [please see the version 2.x documentation](old-release- [gh2519]: https://github.com/tomchristie/django-rest-framework/issues/2519 [gh2524]: https://github.com/tomchristie/django-rest-framework/issues/2524 [gh2530]: https://github.com/tomchristie/django-rest-framework/issues/2530 +<!-- 3.1.1 --> +[gh2691]: https://github.com/tomchristie/django-rest-framework/issues/2691 +[gh2685]: https://github.com/tomchristie/django-rest-framework/issues/2685 +[gh2591]: https://github.com/tomchristie/django-rest-framework/issues/2591 +[gh2678]: https://github.com/tomchristie/django-rest-framework/issues/2678 +[gh2667]: https://github.com/tomchristie/django-rest-framework/issues/2667 +[gh2700]: https://github.com/tomchristie/django-rest-framework/issues/2700 +[gh2645]: https://github.com/tomchristie/django-rest-framework/issues/2645 +[gh2640]: https://github.com/tomchristie/django-rest-framework/issues/2640 +[gh2637]: https://github.com/tomchristie/django-rest-framework/issues/2637 +[gh2641]: https://github.com/tomchristie/django-rest-framework/issues/2641 +[gh2631]: https://github.com/tomchristie/django-rest-framework/issues/2631 +[gh2741]: https://github.com/tomchristie/django-rest-framework/issues/2641 +[gh2743]: https://github.com/tomchristie/django-rest-framework/issues/2643 diff --git a/requirements/requirements-codestyle.txt b/requirements/requirements-codestyle.txt index 4e2be24c..88f61fdf 100644 --- a/requirements/requirements-codestyle.txt +++ b/requirements/requirements-codestyle.txt @@ -1,3 +1,3 @@ # PEP8 code linting, which we run on all commits. -flake8==2.3.0 -pep8==1.6.2 +flake8==2.4.0 +pep8==1.5.7 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index f8bbeee3..62bdd2c5 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.1.0' +__version__ = '3.1.1' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2015 Tom Christie' diff --git a/rest_framework/authtoken/views.py b/rest_framework/authtoken/views.py index b75c2e25..66bbc49b 100644 --- a/rest_framework/authtoken/views.py +++ b/rest_framework/authtoken/views.py @@ -11,9 +11,10 @@ class ObtainAuthToken(APIView): permission_classes = () parser_classes = (parsers.FormParser, parsers.MultiPartParser, parsers.JSONParser,) renderer_classes = (renderers.JSONRenderer,) + serializer_class = AuthTokenSerializer def post(self, request): - serializer = AuthTokenSerializer(data=request.data) + serializer = self.serializer_class(data=request.data) serializer.is_valid(raise_exception=True) user = serializer.validated_data['user'] token, created = Token.objects.get_or_create(user=user) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index a80862e8..bea77300 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -339,7 +339,7 @@ class Field(object): * Raise `ValidationError`, indicating invalid data. * Raise `SkipField`, indicating that the field should be ignored. * Return (True, data), indicating an empty value that should be - returned without any furhter validation being applied. + returned without any further validation being applied. * Return (False, data), indicating a non-empty value, that should have validation applied as normal. """ diff --git a/rest_framework/locale/fr/LC_MESSAGES/django.mo b/rest_framework/locale/fr/LC_MESSAGES/django.mo Binary files differindex 68519d45..0b5224a5 100644 --- a/rest_framework/locale/fr/LC_MESSAGES/django.mo +++ b/rest_framework/locale/fr/LC_MESSAGES/django.mo diff --git a/rest_framework/locale/fr/LC_MESSAGES/django.po b/rest_framework/locale/fr/LC_MESSAGES/django.po index e8597c30..a942af6d 100644 --- a/rest_framework/locale/fr/LC_MESSAGES/django.po +++ b/rest_framework/locale/fr/LC_MESSAGES/django.po @@ -5,13 +5,15 @@ # Translators: # Etienne Desgagné <etienne.desgagne@evimbec.ca>, 2015 # Martin Maillard <martin.maillard@gmail.com>, 2015 +# Martin Maillard <martin.maillard@gmail.com>, 2015 +# Xavier Ordoquy <xordoquy@linovia.com>, 2015 msgid "" msgstr "" "Project-Id-Version: Django REST framework\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2015-01-30 16:23+0000\n" -"PO-Revision-Date: 2015-01-30 16:27+0000\n" -"Last-Translator: Thomas Christie <tom@tomchristie.com>\n" +"PO-Revision-Date: 2015-03-19 22:23+0000\n" +"Last-Translator: Xavier Ordoquy <xordoquy@linovia.com>\n" "Language-Team: French (http://www.transifex.com/projects/p/django-rest-framework/language/fr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -73,7 +75,7 @@ msgstr "Vous n'avez pas la permission d'effectuer cette action." #: exceptions.py:93 msgid "Not found." -msgstr "" +msgstr "Pas trouvé." #: exceptions.py:98 msgid "Method \"{method}\" not allowed." @@ -81,15 +83,15 @@ msgstr "Méthode \"{method}\" non autorisée." #: exceptions.py:109 msgid "Could not satisfy the request Accept header." -msgstr "" +msgstr "L'en-tête « Accept » n'a pas pu être satisfaite." #: exceptions.py:121 msgid "Unsupported media type \"{media_type}\" in request." -msgstr "" +msgstr "Type de média \"{media_type}\" non supporté." #: exceptions.py:134 msgid "Request was throttled." -msgstr "" +msgstr "Requête ralentie." #: fields.py:153 relations.py:132 relations.py:156 validators.py:77 #: validators.py:155 @@ -136,11 +138,11 @@ msgstr "Saisissez une URL valide." #: fields.py:638 msgid "\"{value}\" is not a valid UUID." -msgstr "" +msgstr "\"{value}\" n'est pas un UUID valide." #: fields.py:657 msgid "A valid integer is required." -msgstr "Saisissez un nombre entier valide." +msgstr "Un nombre entier valide est requis." #: fields.py:658 fields.py:692 fields.py:725 msgid "Ensure this value is less than or equal to {max_value}." @@ -175,23 +177,23 @@ msgstr "Assurez-vous qu'il n'y a pas plus de {max_whole_digits} chiffres avant l #: fields.py:813 msgid "Datetime has wrong format. Use one of these formats instead: {format}." -msgstr "" +msgstr "La date + heure n'a pas le bon format. Utilisez un des formats suivants : {format}." #: fields.py:814 msgid "Expected a datetime but got a date." -msgstr "" +msgstr "Attendait une date + heure mais a reçu une date." #: fields.py:878 msgid "Date has wrong format. Use one of these formats instead: {format}." -msgstr "" +msgstr "La date n'a pas le bon format. Utilisez un des formats suivants : {format}." #: fields.py:879 msgid "Expected a date but got a datetime." -msgstr "" +msgstr "Attendait une date mais a reçu une date + heure." #: fields.py:936 msgid "Time has wrong format. Use one of these formats instead: {format}." -msgstr "" +msgstr "L'heure n'a pas le bon format. Utilisez un des formats suivants : {format}." #: fields.py:992 fields.py:1036 msgid "\"{input}\" is not a valid choice." @@ -199,7 +201,7 @@ msgstr "\"{input}\" n'est pas un choix valide." #: fields.py:1037 fields.py:1151 serializers.py:482 msgid "Expected a list of items but got type \"{input_type}\"." -msgstr "" +msgstr "Attendait une liste d'éléments mais a reçu \"{input_type}\"." #: fields.py:1067 msgid "No file was submitted." @@ -231,7 +233,7 @@ msgstr "Transférez une image valide. Le fichier que vous avez transféré n'est #: fields.py:1188 msgid "Expected a dictionary of items but got type \"{input_type}\"." -msgstr "" +msgstr "Attendait un dictionnaire d'éléments mais a reçu \"{input_type}\"." #: pagination.py:221 msgid "Invalid page \"{page_number}\": {message}." @@ -239,7 +241,7 @@ msgstr "Page \"{page_number}\" non valide : {message}." #: pagination.py:442 msgid "Invalid cursor" -msgstr "" +msgstr "Curseur non valide" #: relations.py:133 msgid "Invalid pk \"{pk_value}\" - object does not exist." @@ -247,23 +249,23 @@ msgstr "Clé primaire \"{pk_value}\" non valide - l'objet n'existe pas." #: relations.py:134 msgid "Incorrect type. Expected pk value, received {data_type}." -msgstr "" +msgstr "Type incorrect. Attendait une clé primaire, a reçu {data_type}." #: relations.py:157 msgid "Invalid hyperlink - No URL match." -msgstr "" +msgstr "Lien non valide : pas d'URL correspondante." #: relations.py:158 msgid "Invalid hyperlink - Incorrect URL match." -msgstr "" +msgstr "Lien non valide : URL correspondante incorrecte." #: relations.py:159 msgid "Invalid hyperlink - Object does not exist." -msgstr "" +msgstr "Lien non valide : l'objet n'existe pas." #: relations.py:160 msgid "Incorrect type. Expected URL string, received {data_type}." -msgstr "" +msgstr "Type incorrect. Attendait une URL, a reçu {data_type}." #: relations.py:295 msgid "Object with {slug_name}={value} does not exist." @@ -275,7 +277,7 @@ msgstr "Valeur non valide." #: serializers.py:299 msgid "Invalid data. Expected a dictionary, but got {datatype}." -msgstr "" +msgstr "Donnée non valide. Attendait un dictionnaire, a reçu {datatype}." #: validators.py:22 msgid "This field must be unique." diff --git a/rest_framework/locale/pt_PT/LC_MESSAGES/django.mo b/rest_framework/locale/pt_PT/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..35db0dd7 --- /dev/null +++ b/rest_framework/locale/pt_PT/LC_MESSAGES/django.mo diff --git a/rest_framework/locale/pt_PT/LC_MESSAGES/django.po b/rest_framework/locale/pt_PT/LC_MESSAGES/django.po new file mode 100644 index 00000000..9da496fe --- /dev/null +++ b/rest_framework/locale/pt_PT/LC_MESSAGES/django.po @@ -0,0 +1,324 @@ +# 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: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-02 10:46+0000\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: Portuguese (Portugal) (http://www.transifex.com/projects/p/django-rest-framework/language/pt_PT/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: pt_PT\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: authentication.py:69 +msgid "Invalid basic header. No credentials provided." +msgstr "" + +#: authentication.py:72 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "" + +#: authentication.py:78 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "" + +#: authentication.py:90 +msgid "Invalid username/password." +msgstr "" + +#: authentication.py:156 +msgid "Invalid token header. No credentials provided." +msgstr "" + +#: authentication.py:159 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "" + +#: authentication.py:168 +msgid "Invalid token." +msgstr "" + +#: authentication.py:171 +msgid "User inactive or deleted." +msgstr "" + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "" + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "" + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "" + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "" + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "" + +#: exceptions.py:93 +msgid "Not found." +msgstr "" + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "" + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "" + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "" + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "" + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "" + +#: fields.py:154 +msgid "This field may not be null." +msgstr "" + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "" + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "" + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "" + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "" + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "" + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "" + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "" + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "" + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "" + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "" + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "" + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "" + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "" + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "" + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "" + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "" + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "" + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "" + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "" + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "" + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "" + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "" + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "" + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "" + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "" + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "" + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "" + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "" + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "" + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "" + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "" + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "" + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "" + +#: relations.py:296 +msgid "Invalid value." +msgstr "" + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "" + +#: validators.py:22 +msgid "This field must be unique." +msgstr "" + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "" + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "" + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "" + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "" + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "" + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "" + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "" + +#: versioning.py:160 +msgid "Invalid version in query parameter." +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 "" diff --git a/rest_framework/locale/vi/LC_MESSAGES/django.mo b/rest_framework/locale/vi/LC_MESSAGES/django.mo Binary files differnew file mode 100644 index 00000000..dea3a362 --- /dev/null +++ b/rest_framework/locale/vi/LC_MESSAGES/django.mo diff --git a/rest_framework/locale/vi/LC_MESSAGES/django.po b/rest_framework/locale/vi/LC_MESSAGES/django.po new file mode 100644 index 00000000..b378e4de --- /dev/null +++ b/rest_framework/locale/vi/LC_MESSAGES/django.po @@ -0,0 +1,324 @@ +# 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: 2015-01-30 16:23+0000\n" +"PO-Revision-Date: 2015-01-02 10:46+0000\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: Vietnamese (http://www.transifex.com/projects/p/django-rest-framework/language/vi/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: vi\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: authentication.py:69 +msgid "Invalid basic header. No credentials provided." +msgstr "" + +#: authentication.py:72 +msgid "Invalid basic header. Credentials string should not contain spaces." +msgstr "" + +#: authentication.py:78 +msgid "Invalid basic header. Credentials not correctly base64 encoded." +msgstr "" + +#: authentication.py:90 +msgid "Invalid username/password." +msgstr "" + +#: authentication.py:156 +msgid "Invalid token header. No credentials provided." +msgstr "" + +#: authentication.py:159 +msgid "Invalid token header. Token string should not contain spaces." +msgstr "" + +#: authentication.py:168 +msgid "Invalid token." +msgstr "" + +#: authentication.py:171 +msgid "User inactive or deleted." +msgstr "" + +#: exceptions.py:38 +msgid "A server error occurred." +msgstr "" + +#: exceptions.py:73 +msgid "Malformed request." +msgstr "" + +#: exceptions.py:78 +msgid "Incorrect authentication credentials." +msgstr "" + +#: exceptions.py:83 +msgid "Authentication credentials were not provided." +msgstr "" + +#: exceptions.py:88 +msgid "You do not have permission to perform this action." +msgstr "" + +#: exceptions.py:93 +msgid "Not found." +msgstr "" + +#: exceptions.py:98 +msgid "Method \"{method}\" not allowed." +msgstr "" + +#: exceptions.py:109 +msgid "Could not satisfy the request Accept header." +msgstr "" + +#: exceptions.py:121 +msgid "Unsupported media type \"{media_type}\" in request." +msgstr "" + +#: exceptions.py:134 +msgid "Request was throttled." +msgstr "" + +#: fields.py:153 relations.py:132 relations.py:156 validators.py:77 +#: validators.py:155 +msgid "This field is required." +msgstr "" + +#: fields.py:154 +msgid "This field may not be null." +msgstr "" + +#: fields.py:487 fields.py:515 +msgid "\"{input}\" is not a valid boolean." +msgstr "" + +#: fields.py:550 +msgid "This field may not be blank." +msgstr "" + +#: fields.py:551 fields.py:1324 +msgid "Ensure this field has no more than {max_length} characters." +msgstr "" + +#: fields.py:552 +msgid "Ensure this field has at least {min_length} characters." +msgstr "" + +#: fields.py:587 +msgid "Enter a valid email address." +msgstr "" + +#: fields.py:604 +msgid "This value does not match the required pattern." +msgstr "" + +#: fields.py:615 +msgid "" +"Enter a valid \"slug\" consisting of letters, numbers, underscores or " +"hyphens." +msgstr "" + +#: fields.py:627 +msgid "Enter a valid URL." +msgstr "" + +#: fields.py:638 +msgid "\"{value}\" is not a valid UUID." +msgstr "" + +#: fields.py:657 +msgid "A valid integer is required." +msgstr "" + +#: fields.py:658 fields.py:692 fields.py:725 +msgid "Ensure this value is less than or equal to {max_value}." +msgstr "" + +#: fields.py:659 fields.py:693 fields.py:726 +msgid "Ensure this value is greater than or equal to {min_value}." +msgstr "" + +#: fields.py:660 fields.py:694 fields.py:730 +msgid "String value too large." +msgstr "" + +#: fields.py:691 fields.py:724 +msgid "A valid number is required." +msgstr "" + +#: fields.py:727 +msgid "Ensure that there are no more than {max_digits} digits in total." +msgstr "" + +#: fields.py:728 +msgid "" +"Ensure that there are no more than {max_decimal_places} decimal places." +msgstr "" + +#: fields.py:729 +msgid "" +"Ensure that there are no more than {max_whole_digits} digits before the " +"decimal point." +msgstr "" + +#: fields.py:813 +msgid "Datetime has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:814 +msgid "Expected a datetime but got a date." +msgstr "" + +#: fields.py:878 +msgid "Date has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:879 +msgid "Expected a date but got a datetime." +msgstr "" + +#: fields.py:936 +msgid "Time has wrong format. Use one of these formats instead: {format}." +msgstr "" + +#: fields.py:992 fields.py:1036 +msgid "\"{input}\" is not a valid choice." +msgstr "" + +#: fields.py:1037 fields.py:1151 serializers.py:482 +msgid "Expected a list of items but got type \"{input_type}\"." +msgstr "" + +#: fields.py:1067 +msgid "No file was submitted." +msgstr "" + +#: fields.py:1068 +msgid "" +"The submitted data was not a file. Check the encoding type on the form." +msgstr "" + +#: fields.py:1069 +msgid "No filename could be determined." +msgstr "" + +#: fields.py:1070 +msgid "The submitted file is empty." +msgstr "" + +#: fields.py:1071 +msgid "" +"Ensure this filename has at most {max_length} characters (it has {length})." +msgstr "" + +#: fields.py:1113 +msgid "" +"Upload a valid image. The file you uploaded was either not an image or a " +"corrupted image." +msgstr "" + +#: fields.py:1188 +msgid "Expected a dictionary of items but got type \"{input_type}\"." +msgstr "" + +#: pagination.py:221 +msgid "Invalid page \"{page_number}\": {message}." +msgstr "" + +#: pagination.py:442 +msgid "Invalid cursor" +msgstr "" + +#: relations.py:133 +msgid "Invalid pk \"{pk_value}\" - object does not exist." +msgstr "" + +#: relations.py:134 +msgid "Incorrect type. Expected pk value, received {data_type}." +msgstr "" + +#: relations.py:157 +msgid "Invalid hyperlink - No URL match." +msgstr "" + +#: relations.py:158 +msgid "Invalid hyperlink - Incorrect URL match." +msgstr "" + +#: relations.py:159 +msgid "Invalid hyperlink - Object does not exist." +msgstr "" + +#: relations.py:160 +msgid "Incorrect type. Expected URL string, received {data_type}." +msgstr "" + +#: relations.py:295 +msgid "Object with {slug_name}={value} does not exist." +msgstr "" + +#: relations.py:296 +msgid "Invalid value." +msgstr "" + +#: serializers.py:299 +msgid "Invalid data. Expected a dictionary, but got {datatype}." +msgstr "" + +#: validators.py:22 +msgid "This field must be unique." +msgstr "" + +#: validators.py:76 +msgid "The fields {field_names} must make a unique set." +msgstr "" + +#: validators.py:219 +msgid "This field must be unique for the \"{date_field}\" date." +msgstr "" + +#: validators.py:234 +msgid "This field must be unique for the \"{date_field}\" month." +msgstr "" + +#: validators.py:247 +msgid "This field must be unique for the \"{date_field}\" year." +msgstr "" + +#: versioning.py:39 +msgid "Invalid version in \"Accept\" header." +msgstr "" + +#: versioning.py:70 versioning.py:112 +msgid "Invalid version in URL path." +msgstr "" + +#: versioning.py:138 +msgid "Invalid version in hostname." +msgstr "" + +#: versioning.py:160 +msgid "Invalid version in query parameter." +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 "" diff --git a/rest_framework/metadata.py b/rest_framework/metadata.py index bf3611aa..b2c48b81 100644 --- a/rest_framework/metadata.py +++ b/rest_framework/metadata.py @@ -36,6 +36,7 @@ class SimpleMetadata(BaseMetadata): label_lookup = ClassLookupDict({ serializers.Field: 'field', serializers.BooleanField: 'boolean', + serializers.NullBooleanField: 'boolean', serializers.CharField: 'string', serializers.URLField: 'url', serializers.EmailField: 'email', diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 920d2bc4..2350344c 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -421,6 +421,14 @@ class BrowsableAPIRenderer(BaseRenderer): return False # Doesn't have permissions return True + def _get_serializer(self, serializer_class, view_instance, request, *args, **kwargs): + kwargs['context'] = { + 'request': request, + 'format': self.format, + 'view': view_instance + } + return serializer_class(*args, **kwargs) + def get_rendered_html_form(self, data, view, method, request): """ Return a string representing a rendered HTML form, possibly bound to @@ -457,8 +465,11 @@ class BrowsableAPIRenderer(BaseRenderer): if method in ('DELETE', 'OPTIONS'): return True # Don't actually need to return a form + has_serializer = getattr(view, 'get_serializer', None) + has_serializer_class = getattr(view, 'serializer_class', None) + if ( - not getattr(view, 'get_serializer', None) or + (not has_serializer and not has_serializer_class) or not any(is_form_media_type(parser.media_type) for parser in view.parser_classes) ): return @@ -466,10 +477,19 @@ class BrowsableAPIRenderer(BaseRenderer): if existing_serializer is not None: serializer = existing_serializer else: - if method in ('PUT', 'PATCH'): - serializer = view.get_serializer(instance=instance, **kwargs) + if has_serializer: + if method in ('PUT', 'PATCH'): + serializer = view.get_serializer(instance=instance, **kwargs) + else: + serializer = view.get_serializer(**kwargs) else: - serializer = view.get_serializer(**kwargs) + # at this point we must have a serializer_class + if method in ('PUT', 'PATCH'): + serializer = self._get_serializer(view.serializer_class, view, + request, instance=instance, **kwargs) + else: + serializer = self._get_serializer(view.serializer_class, view, + request, **kwargs) if hasattr(serializer, 'initial_data'): serializer.is_valid() diff --git a/rest_framework/response.py b/rest_framework/response.py index c21c60a2..9319e708 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -5,7 +5,7 @@ it is initialized with unrendered data, instead of a pre-rendered string. The appropriate renderer is called during Django's template response rendering. """ from __future__ import unicode_literals -from django.core.handlers.wsgi import STATUS_CODE_TEXT +from django.utils.six.moves.http_client import responses from django.template.response import SimpleTemplateResponse from django.utils import six @@ -77,7 +77,7 @@ class Response(SimpleTemplateResponse): """ # TODO: Deprecate and use a template tag instead # TODO: Status code text for RFC 6585 status codes - return STATUS_CODE_TEXT.get(self.status_code, '') + return responses.get(self.status_code, '') def __getstate__(self): """ diff --git a/rest_framework/reverse.py b/rest_framework/reverse.py index a251d99d..e6d3f563 100644 --- a/rest_framework/reverse.py +++ b/rest_framework/reverse.py @@ -3,6 +3,7 @@ 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 @@ -15,7 +16,13 @@ def reverse(viewname, args=None, kwargs=None, request=None, format=None, **extra """ scheme = getattr(request, 'versioning_scheme', None) if scheme is not None: - return scheme.reverse(viewname, args, kwargs, request, format, **extra) + try: + return scheme.reverse(viewname, args, kwargs, request, format, **extra) + except NoReverseMatch: + # In case the versioning scheme reversal fails, fallback to the + # default implementation + pass + return _reverse(viewname, args, kwargs, request, format, **extra) diff --git a/rest_framework/static/rest_framework/js/default.js b/rest_framework/static/rest_framework/js/default.js index c8812132..22e5efde 100644 --- a/rest_framework/static/rest_framework/js/default.js +++ b/rest_framework/static/rest_framework/js/default.js @@ -45,6 +45,10 @@ var selectedTab = null; var selectedTabName = getCookie('tabstyle'); if (selectedTabName) { + selectedTabName = selectedTabName.replace(/[^a-z-]/g, ''); +} + +if (selectedTabName) { selectedTab = $('.form-switcher a[name=' + selectedTabName + ']'); } diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 3a435f02..731aedba 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -1,5 +1,5 @@ from __future__ import unicode_literals -from rest_framework import exceptions, serializers, status, views, versioning +from rest_framework import exceptions, metadata, serializers, status, views, versioning from rest_framework.request import Request from rest_framework.renderers import BrowsableAPIRenderer from rest_framework.test import APIRequestFactory @@ -207,3 +207,8 @@ class TestMetadata: scheme = versioning.QueryParameterVersioning view = ExampleView.as_view(versioning_class=scheme) view(request=request) + + def test_null_boolean_field_info_type(self): + options = metadata.SimpleMetadata() + field_info = options.get_field_info(serializers.NullBooleanField()) + assert field_info['type'] == 'boolean' diff --git a/tests/test_reverse.py b/tests/test_reverse.py index 675a9d5a..08c27023 100644 --- a/tests/test_reverse.py +++ b/tests/test_reverse.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url +from django.core.urlresolvers import NoReverseMatch from django.test import TestCase from rest_framework.reverse import reverse from rest_framework.test import APIRequestFactory @@ -16,6 +17,18 @@ urlpatterns = patterns( ) +class MockVersioningScheme(object): + + def __init__(self, raise_error=False): + self.raise_error = raise_error + + def reverse(self, *args, **kwargs): + if self.raise_error: + raise NoReverseMatch() + + return 'http://scheme-reversed/view' + + class ReverseTests(TestCase): """ Tests for fully qualified URLs when using `reverse`. @@ -26,3 +39,17 @@ class ReverseTests(TestCase): request = factory.get('/view') url = reverse('view', request=request) self.assertEqual(url, 'http://testserver/view') + + def test_reverse_with_versioning_scheme(self): + request = factory.get('/view') + request.versioning_scheme = MockVersioningScheme() + + url = reverse('view', request=request) + self.assertEqual(url, 'http://scheme-reversed/view') + + def test_reverse_with_versioning_scheme_fallback_to_default_on_error(self): + request = factory.get('/view') + request.versioning_scheme = MockVersioningScheme(raise_error=True) + + url = reverse('view', request=request) + self.assertEqual(url, 'http://testserver/view') diff --git a/tests/test_versioning.py b/tests/test_versioning.py index 90ad8afd..88ae56dd 100644 --- a/tests/test_versioning.py +++ b/tests/test_versioning.py @@ -7,6 +7,7 @@ from rest_framework.response import Response from rest_framework.reverse import reverse from rest_framework.test import APIRequestFactory, APITestCase from rest_framework.versioning import NamespaceVersioning +from rest_framework.relations import PKOnlyObject import pytest @@ -234,7 +235,7 @@ class TestInvalidVersion: class TestHyperlinkedRelatedField(UsingURLPatterns, APITestCase): included = [ - url(r'^namespaced/(?P<pk>\d+)/$', dummy_view, name='namespaced'), + url(r'^namespaced/(?P<pk>\d+)/$', dummy_pk_view, name='namespaced'), ] urlpatterns = [ @@ -262,3 +263,44 @@ class TestHyperlinkedRelatedField(UsingURLPatterns, APITestCase): assert self.field.to_internal_value('/v1/namespaced/3/') == 'object 3' with pytest.raises(serializers.ValidationError): self.field.to_internal_value('/v2/namespaced/3/') + + +class TestNamespaceVersioningHyperlinkedRelatedFieldScheme(UsingURLPatterns, APITestCase): + included = [ + url(r'^namespaced/(?P<pk>\d+)/$', dummy_pk_view, name='namespaced'), + ] + + urlpatterns = [ + url(r'^v1/', include(included, namespace='v1')), + url(r'^v2/', include(included, namespace='v2')), + url(r'^non-api/(?P<pk>\d+)/$', dummy_pk_view, name='non-api-view') + ] + + def _create_field(self, view_name, version): + request = factory.get("/") + request.versioning_scheme = NamespaceVersioning() + request.version = version + + field = serializers.HyperlinkedRelatedField( + view_name=view_name, + read_only=True) + field._context = {'request': request} + return field + + def test_api_url_is_properly_reversed_with_v1(self): + field = self._create_field('namespaced', 'v1') + assert field.to_representation(PKOnlyObject(3)) == 'http://testserver/v1/namespaced/3/' + + def test_api_url_is_properly_reversed_with_v2(self): + field = self._create_field('namespaced', 'v2') + assert field.to_representation(PKOnlyObject(5)) == 'http://testserver/v2/namespaced/5/' + + def test_non_api_url_is_properly_reversed_regardless_of_the_version(self): + """ + Regression test for #2711 + """ + field = self._create_field('non-api-view', 'v1') + assert field.to_representation(PKOnlyObject(10)) == 'http://testserver/non-api/10/' + + field = self._create_field('non-api-view', 'v2') + assert field.to_representation(PKOnlyObject(10)) == 'http://testserver/non-api/10/' @@ -3,7 +3,7 @@ envlist = py27-{flake8,docs}, {py26,py27}-django14, {py26,py27,py32,py33,py34}-django{15,16}, - {py27,py32,py33,py34}-django{17,18beta} + {py27,py32,py33,py34}-django{17,18} [testenv] commands = ./runtests.py --fast @@ -14,14 +14,14 @@ deps = django15: Django==1.5.6 # Should track minimum supported django16: Django==1.6.3 # Should track minimum supported django17: Django==1.7.2 # Should track maximum supported - django18beta: https://www.djangoproject.com/download/1.8b1/tarball/ + django18: Django==1.8 # Should track maximum supported -rrequirements/requirements-testing.txt -rrequirements/requirements-optionals.txt [testenv:py27-flake8] deps = - -rrequirements/requirements-testing.txt -rrequirements/requirements-codestyle.txt + -rrequirements/requirements-testing.txt commands = ./runtests.py --lintonly [testenv:py27-docs] |
