diff options
50 files changed, 528 insertions, 231 deletions
diff --git a/.travis.yml b/.travis.yml index 6191e7e2..28ebfc00 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,10 @@ env: - TOX_ENV=py26-django15 - TOX_ENV=py27-django14 - TOX_ENV=py26-django14 + - TOX_ENV=py34-django18alpha + - TOX_ENV=py33-django18alpha + - TOX_ENV=py32-django18alpha + - TOX_ENV=py27-django18alpha - TOX_ENV=py34-djangomaster - TOX_ENV=py33-djangomaster - TOX_ENV=py32-djangomaster @@ -34,7 +34,7 @@ There is a live example API for testing purposes, [available here][sandbox]. # Requirements * Python (2.6.5+, 2.7, 3.2, 3.3, 3.4) -* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7) +* Django (1.4.11+, 1.5.6+, 1.6.3+, 1.7, 1.8-alpha) # Installation @@ -190,18 +190,18 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [sandbox]: http://restframework.herokuapp.com/ [index]: http://www.django-rest-framework.org/ -[oauth1-section]: http://www.django-rest-framework.org/api-guide/authentication.html#oauthauthentication -[oauth2-section]: http://www.django-rest-framework.org/api-guide/authentication.html#oauth2authentication -[serializer-section]: http://www.django-rest-framework.org/api-guide/serializers.html#serializers -[modelserializer-section]: http://www.django-rest-framework.org/api-guide/serializers.html#modelserializer -[functionview-section]: http://www.django-rest-framework.org/api-guide/views.html#function-based-views -[generic-views]: http://www.django-rest-framework.org/api-guide/generic-views.html -[viewsets]: http://www.django-rest-framework.org/api-guide/viewsets.html -[routers]: http://www.django-rest-framework.org/api-guide/routers.html -[serializers]: http://www.django-rest-framework.org/api-guide/serializers.html -[authentication]: http://www.django-rest-framework.org/api-guide/authentication.html - -[rest-framework-2-announcement]: http://www.django-rest-framework.org/topics/rest-framework-2-announcement.html +[oauth1-section]: http://www.django-rest-framework.org/api-guide/authentication/#oauthauthentication +[oauth2-section]: http://www.django-rest-framework.org/api-guide/authentication/#oauth2authentication +[serializer-section]: http://www.django-rest-framework.org/api-guide/serializers/#serializers +[modelserializer-section]: http://www.django-rest-framework.org/api-guide/serializers/#modelserializer +[functionview-section]: http://www.django-rest-framework.org/api-guide/views/#function-based-views +[generic-views]: http://www.django-rest-framework.org/api-guide/generic-views/ +[viewsets]: http://www.django-rest-framework.org/api-guide/viewsets/ +[routers]: http://www.django-rest-framework.org/api-guide/routers/ +[serializers]: http://www.django-rest-framework.org/api-guide/serializers/ +[authentication]: http://www.django-rest-framework.org/api-guide/authentication/ + +[rest-framework-2-announcement]: http://www.django-rest-framework.org/topics/rest-framework-2-announcement [2.1.0-notes]: https://groups.google.com/d/topic/django-rest-framework/Vv2M0CMY9bg/discussion [image]: http://www.django-rest-framework.org/img/quickstart.png diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 1222dbf0..0d53de70 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -427,7 +427,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/contrib/csrf/#ajax +[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 diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index f379ac72..4d7d9eee 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -182,6 +182,12 @@ Corresponds to `django.db.models.fields.URLField`. Uses Django's `django.core.v **Signature:** `URLField(max_length=200, min_length=None, allow_blank=False)` +## UUIDField + +A field that ensures the input is a valid UUID string. The `to_internal_value` method will return a `uuid.UUID` instance. On output the field will return a string in the canonical hyphenated format, for example: + + "de305d54-75b4-431b-adb2-eb6b9e546013" + --- # Numeric fields @@ -320,7 +326,7 @@ Both the `allow_blank` and `allow_null` are valid options on `ChoiceField`, alth ## MultipleChoiceField -A field that can accept a set of zero, one or many values, chosen from a limited set of choices. Takes a single mandatory argument. `to_internal_representation` returns a `set` containing the selected values. +A field that can accept a set of zero, one or many values, chosen from a limited set of choices. Takes a single mandatory argument. `to_internal_value` returns a `set` containing the selected values. **Signature:** `MultipleChoiceField(choices)` @@ -374,7 +380,7 @@ A field class that validates a list of objects. **Signature**: `ListField(child)` -- `child` - A field instance that should be used for validating the objects in the list. +- `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. For example, to validate a list of integers you might use something like the following: @@ -389,6 +395,23 @@ The `ListField` class also supports a declarative style that allows you to write We can now reuse our custom `StringListField` class throughout our application, without having to provide a `child` argument to it. +## DictField + +A field class that validates a dictionary of objects. The keys in `DictField` are always assumed to be string values. + +**Signature**: `DictField(child)` + +- `child` - A field instance that should be used for validating the values in the dictionary. If this argument is not provided then values in the mapping will not be validated. + +For example, to create a field that validates a mapping of strings to strings, you would write something like this: + + document = DictField(child=CharField()) + +You can also use the declarative style, as with `ListField`. For example: + + class DocumentField(DictField): + child = CharField() + --- # Miscellaneous fields @@ -438,7 +461,7 @@ This is a read-only field. It gets its value by calling a method on the serializ **Signature**: `SerializerMethodField(method_name=None)` -- `method-name` - The name of the method on the serializer to be called. If not included this defaults to `get_<field_name>`. +- `method_name` - The name of the method on the serializer to be called. If not included this defaults to `get_<field_name>`. The serializer method referred to by the `method_name` argument should accept a single argument (in addition to `self`), which is the object being serialized. It should return whatever you want to be included in the serialized representation of the object. For example: diff --git a/docs/api-guide/filtering.md b/docs/api-guide/filtering.md index 3eb1538f..b16b6be5 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -391,16 +391,16 @@ We could achieve the same behavior by overriding `get_queryset()` on the views, The following third party packages provide additional filter implementations. -## Django REST framework chain +## Django REST framework filters package -The [django-rest-framework-chain package][django-rest-framework-chain] works together with the `DjangoFilterBackend` class, and allows you to easily create filters across relationships, or create multiple filter lookup types for a given field. +The [django-rest-framework-filters package][django-rest-framework-filters] works together with the `DjangoFilterBackend` class, and allows you to easily create filters across relationships, or create multiple filter lookup types for a given field. [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.org/en/latest/index.html -[guardian]: http://pythonhosted.org/django-guardian/ -[view-permissions]: http://pythonhosted.org/django-guardian/userguide/assign.html +[guardian]: https://django-guardian.readthedocs.org/ +[view-permissions]: https://django-guardian.readthedocs.org/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 -[django-rest-framework-chain]: https://github.com/philipn/django-rest-framework-chain +[django-rest-framework-filters]: https://github.com/philipn/django-rest-framework-filters diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 6374e305..61c8e8d8 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -93,17 +93,13 @@ The following attributes are used to control pagination when used with list view * `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. -**Deprecated attributes**: - -* `model` - This shortcut may be used instead of setting either (or both) of the `queryset`/`serializer_class` attributes. The explicit style is preferred over the `.model` shortcut, and usage of this attribute is now deprecated. - ### Methods **Base methods**: #### `get_queryset(self)` -Returns the queryset that should be used for list views, and that should be used as the base for lookups in detail views. Defaults to returning the queryset specified by the `queryset` attribute, or the default queryset for the model if the `model` shortcut is being used. +Returns the queryset that should be used for list views, and that should be used as the base for lookups in detail views. Defaults to returning the queryset specified by the `queryset` attribute. This method should always be used rather than accessing `self.queryset` directly, as `self.queryset` gets evaluated only once, and those results are cached for all subsequent requests. @@ -153,7 +149,7 @@ For example: #### `get_serializer_class(self)` -Returns the class that should be used for the serializer. Defaults to returning the `serializer_class` attribute, or dynamically generating a serializer class if the `model` shortcut is being used. +Returns the class that should be used for the serializer. Defaults to returning the `serializer_class` attribute. May be overridden to provide dynamic behavior, such as using different serializers for read and write operations, or providing different serializers to different types of users. diff --git a/docs/api-guide/parsers.md b/docs/api-guide/parsers.md index 73e3a705..3d44fe56 100644 --- a/docs/api-guide/parsers.md +++ b/docs/api-guide/parsers.md @@ -128,7 +128,7 @@ If the view used with `FileUploadParser` is called with a `filename` URL keyword def put(self, request, filename, format=None): file_obj = request.data['file'] # ... - # do some staff with uploaded file + # do some stuff with uploaded file # ... return Response(status=204) diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 3a8a8f6c..222b6cd2 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -28,7 +28,7 @@ There are two mandatory arguments to the `register()` method: Optionally, you may also specify an additional argument: -* `base_name` - The base to use for the URL names that are created. If unset the basename will be automatically generated based on the `model` or `queryset` attribute on the viewset, if it has one. Note that if the viewset does not include a `model` or `queryset` attribute then you must set `base_name` when registering the viewset. +* `base_name` - The base to use for the URL names that are created. If unset the basename will be automatically generated based on the `queryset` attribute of the viewset, if it has one. Note that if the viewset does not include a `queryset` attribute then you must set `base_name` when registering the viewset. The example above would generate the following URL patterns: @@ -60,7 +60,7 @@ For example, you can append `router.urls` to a list of existing views… router.register(r'accounts', AccountViewSet) urlpatterns = [ - url(r'^forgot-password/$, ForgotPasswordFormView.as_view(), + url(r'^forgot-password/$', ForgotPasswordFormView.as_view()), ] urlpatterns += router.urls @@ -68,15 +68,15 @@ For example, you can append `router.urls` to a list of existing views… Alternatively you can use Django's `include` function, like so… urlpatterns = [ - url(r'^forgot-password/$, ForgotPasswordFormView.as_view(), - url(r'^', include(router.urls)) + url(r'^forgot-password/$', ForgotPasswordFormView.as_view()), + url(r'^', include(router.urls)), ] Router URL patterns can also be namespaces. urlpatterns = [ - url(r'^forgot-password/$, ForgotPasswordFormView.as_view(), - url(r'^api/', include(router.urls, namespace='api')) + url(r'^forgot-password/$', ForgotPasswordFormView.as_view()), + url(r'^api/', include(router.urls, namespace='api')), ] If using namespacing with hyperlinked serializers you'll also need to ensure that any `view_name` parameters on the serializers correctly reflect the namespace. In the example above you'd need to include a parameter such as `view_name='api:user-detail'` for serializer fields hyperlinked to the user detail view. @@ -304,7 +304,7 @@ The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (an The [`DRF-extensions` package][drf-extensions] provides [routers][drf-extensions-routers] for creating [nested viewsets][drf-extensions-nested-viewsets], [collection level controllers][drf-extensions-collection-level-controllers] with [customizable endpoint names][drf-extensions-customizable-endpoint-names]. [cite]: http://guides.rubyonrails.org/routing.html -[route-decorators]: viewsets.html#marking-extra-actions-for-routing +[route-decorators]: viewsets.md#marking-extra-actions-for-routing [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [wq.db]: http://wq.io/wq.db [wq.db-router]: http://wq.io/docs/app.py diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index 3e37cef8..bbf92c6c 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -146,7 +146,7 @@ The decorators can additionally take extra arguments that will be set for the ro def set_password(self, request, pk=None): ... -Theses decorators will route `GET` requests by default, but may also accept other HTTP methods, by using the `methods` argument. For example: +These decorators will route `GET` requests by default, but may also accept other HTTP methods, by using the `methods` argument. For example: @detail_route(methods=['post', 'delete']) def unset_password(self, request, pk=None): @@ -201,7 +201,7 @@ Note that you can use any of the standard attributes or method overrides provide def get_queryset(self): return self.request.user.accounts.all() -Note however that upon removal of the `queryset` property from your `ViewSet`, any associated [router][routers] will be unable to derive the base_name of your Model automatically, and so you you will have to specify the `base_name` kwarg as part of your [router registration][routers]. +Note however that upon removal of the `queryset` property from your `ViewSet`, any associated [router][routers] will be unable to derive the base_name of your Model automatically, and so you will have to specify the `base_name` kwarg as part of your [router registration][routers]. Also note that although this class provides the complete set of create/list/retrieve/update/destroy actions by default, you can restrict the available operations by using the standard permission classes. diff --git a/docs/img/sponsors/2-galileo_press.png b/docs/img/sponsors/2-galileo_press.png Binary files differdeleted file mode 100644 index f77e6c0a..00000000 --- a/docs/img/sponsors/2-galileo_press.png +++ /dev/null diff --git a/docs/img/sponsors/2-rheinwerk_verlag.png b/docs/img/sponsors/2-rheinwerk_verlag.png Binary files differnew file mode 100644 index 00000000..ad454e17 --- /dev/null +++ b/docs/img/sponsors/2-rheinwerk_verlag.png diff --git a/docs/index.md b/docs/index.md index d40f8972..c1110788 100644 --- a/docs/index.md +++ b/docs/index.md @@ -58,7 +58,7 @@ The following packages are optional: * [Markdown][markdown] (2.1.0+) - Markdown support for the browsable API. * [PyYAML][yaml] (3.10+) - YAML content-type support. * [defusedxml][defusedxml] (0.3+) - XML content-type support. -* [django-filter][django-filter] (0.5.4+) - Filtering support. +* [django-filter][django-filter] (0.9.2+) - Filtering support. * [django-oauth-plus][django-oauth-plus] (2.0+) and [oauth2][oauth2] (1.5.211+) - OAuth 1.0a support. * [django-oauth2-provider][django-oauth2-provider] (0.2.3+) - OAuth 2.0 support. * [django-guardian][django-guardian] (1.1.1+) - Object level permissions support. diff --git a/docs/topics/3.0-announcement.md b/docs/topics/3.0-announcement.md index 5dbc5600..59fe779c 100644 --- a/docs/topics/3.0-announcement.md +++ b/docs/topics/3.0-announcement.md @@ -665,7 +665,7 @@ This code *would be valid* in `2.4.3`: class Meta: model = Account -However this code *would not be valid* in `2.4.3`: +However this code *would not be valid* in `3.0`: # Missing `queryset` class AccountSerializer(serializers.Serializer): @@ -826,7 +826,7 @@ The `style` keyword argument can be used to pass through additional information For example, to use a `textarea` control instead of the default `input` control, you would use the following… additional_notes = serializers.CharField( - style={'base_template': 'text_area.html'} + style={'base_template': 'textarea.html'} ) Similarly, to use a radio button control instead of the default `select` control, you would use the following… diff --git a/docs/topics/kickstarter-announcement.md b/docs/topics/kickstarter-announcement.md index 9f99e3e6..78c5cce6 100644 --- a/docs/topics/kickstarter-announcement.md +++ b/docs/topics/kickstarter-announcement.md @@ -84,7 +84,7 @@ Our gold sponsors include companies large and small. Many thanks for their signi <li><a href="http://pulsecode.ca" rel="nofollow" style="background-image:url(../../img/sponsors/2-pulsecode.png);">Pulsecode Inc.</a></li> <li><a href="http://singinghorsestudio.com" rel="nofollow" style="background-image:url(../../img/sponsors/2-singing-horse.png);">Singing Horse Studio Ltd.</a></li> <li><a href="https://www.heroku.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-heroku.png);">Heroku</a></li> -<li><a href="https://www.galileo-press.de/" rel="nofollow" style="background-image:url(../../img/sponsors/2-galileo_press.png);">Galileo Press</a></li> +<li><a href="https://www.rheinwerk-verlag.de/" rel="nofollow" style="background-image:url(../../img/sponsors/2-rheinwerk_verlag.png);">Rheinwerk Verlag</a></li> <li><a href="http://www.securitycompass.com/" rel="nofollow" style="background-image:url(../../img/sponsors/2-security_compass.png);">Security Compass</a></li> <li><a href="https://www.djangoproject.com/foundation/" rel="nofollow" style="background-image:url(../../img/sponsors/2-django.png);">Django Software Foundation</a></li> <li><a href="http://www.hipflaskapp.com" rel="nofollow" style="background-image:url(../../img/sponsors/2-hipflask.png);">Hipflask</a></li> diff --git a/docs/topics/release-notes.md b/docs/topics/release-notes.md index c49dd62c..e0894d2d 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -41,6 +41,24 @@ You can determine your currently installed version using `pip freeze`: ## 3.0.x series +### 3.0.4 + +**Date**: [28th January 2015][3.0.4-milestone]. + +* Django 1.8a1 support. ([#2425][gh2425], [#2446][gh2446], [#2441][gh2441]) +* Add `DictField` and support Django 1.8 `HStoreField`. ([#2451][gh2451], [#2106][gh2106]) +* Add `UUIDField` and support Django 1.8 `UUIDField`. ([#2448][gh2448], [#2433][gh2433], [#2432][gh2432]) +* `BaseRenderer.render` now raises `NotImplementedError`. ([#2434][gh2434]) +* Fix timedelta JSON serialization on Python 2.6. ([#2430][gh2430]) +* `ResultDict` and `ResultList` now appear as standard dict/list. ([#2421][gh2421]) +* Fix visible `HiddenField` in the HTML form of the web browsable API page. ([#2410][gh2410]) +* Use `OrderedDict` for `RelatedField.choices`. ([#2408][gh2408]) +* Fix ident format when using `HTTP_X_FORWARDED_FOR`. ([#2401][gh2401]) +* Fix invalid key with memcached while using throttling. ([#2400][gh2400]) +* Fix `FileUploadParser` with version 3.x. ([#2399][gh2399]) +* Fix the serializer inheritance. ([#2388][gh2388]) +* Fix caching issues with `ReturnDict`. ([#2360][gh2360]) + ### 3.0.3 **Date**: [8th January 2015][3.0.3-milestone]. @@ -702,6 +720,7 @@ For older release notes, [please see the GitHub repo](old-release-notes). [3.0.1-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.1+Release%22 [3.0.2-milestone]: https://github.com/tomchristie/django-rest-framework/issues?q=milestone%3A%223.0.2+Release%22 [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.1 --> [gh2013]: https://github.com/tomchristie/django-rest-framework/issues/2013 @@ -770,3 +789,22 @@ For older release notes, [please see the GitHub repo](old-release-notes). [gh2355]: https://github.com/tomchristie/django-rest-framework/issues/2355 [gh2369]: https://github.com/tomchristie/django-rest-framework/issues/2369 [gh2386]: https://github.com/tomchristie/django-rest-framework/issues/2386 +<!-- 3.0.4 --> +[gh2425]: https://github.com/tomchristie/django-rest-framework/issues/2425 +[gh2446]: https://github.com/tomchristie/django-rest-framework/issues/2446 +[gh2441]: https://github.com/tomchristie/django-rest-framework/issues/2441 +[gh2451]: https://github.com/tomchristie/django-rest-framework/issues/2451 +[gh2106]: https://github.com/tomchristie/django-rest-framework/issues/2106 +[gh2448]: https://github.com/tomchristie/django-rest-framework/issues/2448 +[gh2433]: https://github.com/tomchristie/django-rest-framework/issues/2433 +[gh2432]: https://github.com/tomchristie/django-rest-framework/issues/2432 +[gh2434]: https://github.com/tomchristie/django-rest-framework/issues/2434 +[gh2430]: https://github.com/tomchristie/django-rest-framework/issues/2430 +[gh2421]: https://github.com/tomchristie/django-rest-framework/issues/2421 +[gh2410]: https://github.com/tomchristie/django-rest-framework/issues/2410 +[gh2408]: https://github.com/tomchristie/django-rest-framework/issues/2408 +[gh2401]: https://github.com/tomchristie/django-rest-framework/issues/2401 +[gh2400]: https://github.com/tomchristie/django-rest-framework/issues/2400 +[gh2399]: https://github.com/tomchristie/django-rest-framework/issues/2399 +[gh2388]: https://github.com/tomchristie/django-rest-framework/issues/2388 +[gh2360]: https://github.com/tomchristie/django-rest-framework/issues/2360 diff --git a/docs/topics/third-party-resources.md b/docs/topics/third-party-resources.md index 6f4df288..e26e3a2f 100644 --- a/docs/topics/third-party-resources.md +++ b/docs/topics/third-party-resources.md @@ -237,7 +237,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque * [django-rest-framework-proxy][django-rest-framework-proxy] - Proxy to redirect incoming request to another API server. * [gaiarestframework][gaiarestframework] - Utils for django-rest-framewok * [drf-extensions][drf-extensions] - A collection of custom extensions -* [ember-data-django-rest-adapter][ember-data-django-rest-adapter] - An ember-data adapter +* [ember-django-adapter][ember-django-adapter] - An adapter for working with Ember.js ## Other Resources @@ -309,7 +309,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [django-rest-framework-proxy]: https://github.com/eofs/django-rest-framework-proxy [gaiarestframework]: https://github.com/AppsFuel/gaiarestframework [drf-extensions]: https://github.com/chibisov/drf-extensions -[ember-data-django-rest-adapter]: https://github.com/toranb/ember-data-django-rest-adapter +[ember-django-adapter]: https://github.com/dustinfarris/ember-django-adapter [beginners-guide-to-the-django-rest-framework]: http://code.tutsplus.com/tutorials/beginners-guide-to-the-django-rest-framework--cms-19786 [getting-started-with-django-rest-framework-and-angularjs]: http://blog.kevinastone.com/getting-started-with-django-rest-framework-and-angularjs.html [end-to-end-web-app-with-django-rest-framework-angularjs]: http://blog.mourafiq.com/post/55034504632/end-to-end-web-app-with-django-rest-framework diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 60a3d989..ceb23a02 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -97,7 +97,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) title = serializers.CharField(required=False, allow_blank=True, max_length=100) - code = serializers.CharField(style={'type': 'textarea'}) + code = serializers.CharField(style={'base_template': 'textarea.html'}) linenos = serializers.BooleanField(required=False) language = serializers.ChoiceField(choices=LANGUAGE_CHOICES, default='python') style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly') @@ -191,14 +191,14 @@ Our `SnippetSerializer` class is replicating a lot of information that's also co In the same way that Django provides both `Form` classes and `ModelForm` classes, REST framework includes both `Serializer` classes, and `ModelSerializer` classes. Let's look at refactoring our serializer using the `ModelSerializer` class. -Open the file `snippets/serializers.py` again, and edit the `SnippetSerializer` class. +Open the file `snippets/serializers.py` again, and replace the `SnippetSerializer` class with the following. class SnippetSerializer(serializers.ModelSerializer): class Meta: model = Snippet fields = ('id', 'title', 'code', 'linenos', 'language', 'style') -One nice property that serializers have is that you can inspect all the fields in a serializer instance, by printing it's representation. Open the Django shell with `python manage.py shell`, then try the following: +One nice property that serializers have is that you can inspect all the fields in a serializer instance, by printing its representation. Open the Django shell with `python manage.py shell`, then try the following: >>> from snippets.serializers import SnippetSerializer >>> serializer = SnippetSerializer() diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index 0a9ea3f1..abf82e49 100644 --- a/docs/tutorial/3-class-based-views.md +++ b/docs/tutorial/3-class-based-views.md @@ -64,7 +64,7 @@ That's looking good. Again, it's still pretty similar to the function based vie We'll also need to refactor our `urls.py` slightly now we're using class based views. - from django.conf.urls import patterns, url + from django.conf.urls import url from rest_framework.urlpatterns import format_suffix_patterns from snippets import views diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 592c77e8..887d1e56 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -177,7 +177,7 @@ In the snippets app, create a new file, `permissions.py` # Write permissions are only allowed to the owner of the snippet. return obj.owner == request.user -Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` class: +Now we can add that custom permission to our snippet instance endpoint, by editing the `permission_classes` property on the `SnippetDetail` view class: permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly,) diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index c21efd7f..2841f03e 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -106,6 +106,8 @@ If we're going to have a hyperlinked API, we need to make sure we name our URL p After adding all those names into our URLconf, our final `snippets/urls.py` file should look something like this: + from django.conf.urls import url, include + # API endpoints urlpatterns = format_suffix_patterns([ url(r'^$', views.api_root), diff --git a/requirements.txt b/requirements.txt index 48b29e61..00d973cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,8 @@ Django>=1.4.11 # Test requirements -pytest-django==2.6 -pytest==2.5.2 +pytest-django==2.8.0 +pytest==2.6.4 pytest-cov==1.6 flake8==2.2.2 @@ -12,7 +12,7 @@ markdown>=2.1.0 PyYAML>=3.10 defusedxml>=0.3 django-guardian==1.2.4 -django-filter>=0.5.4 +django-filter>=0.9.2 django-oauth-plus>=2.2.1 oauth2>=1.5.211 django-oauth2-provider>=0.2.4 diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index fdcebb7b..57e5421b 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ """ __title__ = 'Django REST framework' -__version__ = '3.0.3' +__version__ = '3.0.4' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2015 Tom Christie' diff --git a/rest_framework/authtoken/south_migrations/0001_initial.py b/rest_framework/authtoken/south_migrations/0001_initial.py index 926de02b..5b927f3e 100644 --- a/rest_framework/authtoken/south_migrations/0001_initial.py +++ b/rest_framework/authtoken/south_migrations/0001_initial.py @@ -40,7 +40,7 @@ class Migration(SchemaMigration): 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) }, "%s.%s" % (User._meta.app_label, User._meta.module_name): { - 'Meta': {'object_name': User._meta.module_name}, + 'Meta': {'object_name': User._meta.module_name, 'db_table': repr(User._meta.db_table)}, }, 'authtoken.token': { 'Meta': {'object_name': 'Token'}, diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 971dee9c..36413394 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -20,7 +20,7 @@ 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). if six.PY2: - repr(instance).decode('utf-8') + return repr(instance).decode('utf-8') return repr(instance) @@ -33,6 +33,14 @@ def unicode_to_repr(value): return value +def total_seconds(timedelta): + # TimeDelta.total_seconds() is only available in Python 2.7 + if hasattr(timedelta, 'total_seconds'): + return timedelta.total_seconds() + else: + return (timedelta.days * 86400.0) + float(timedelta.seconds) + (timedelta.microseconds / 1000000.0) + + # OrderedDict only available in Python 2.7. # This will always be the case in Django 1.7 and above, as these versions # no longer support Python 2.6. @@ -50,6 +58,13 @@ except ImportError: from django.http import HttpResponse as HttpResponseBase +# contrib.postgres only supported from 1.8 onwards. +try: + from django.contrib.postgres import fields as postgres_fields +except ImportError: + postgres_fields = None + + # request only provides `resolver_match` from 1.5 onwards. def get_resolver_match(request): try: diff --git a/rest_framework/fields.py b/rest_framework/fields.py index cc9410aa..71a9f193 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -23,6 +23,7 @@ import datetime import decimal import inspect import re +import uuid class empty: @@ -632,6 +633,23 @@ class URLField(CharField): self.validators.append(validator) +class UUIDField(Field): + default_error_messages = { + 'invalid': _('"{value}" is not a valid UUID.'), + } + + def to_internal_value(self, data): + if not isinstance(data, uuid.UUID): + try: + return uuid.UUID(data) + except (ValueError, TypeError): + self.fail('invalid', value=data) + return data + + def to_representation(self, value): + return str(value) + + # Number types... class IntegerField(Field): @@ -1114,8 +1132,21 @@ class ImageField(FileField): # Composite field types... +class _UnvalidatedField(Field): + def __init__(self, *args, **kwargs): + super(_UnvalidatedField, self).__init__(*args, **kwargs) + self.allow_blank = True + self.allow_null = True + + def to_internal_value(self, data): + return data + + def to_representation(self, value): + return value + + class ListField(Field): - child = None + child = _UnvalidatedField() initial = [] default_error_messages = { 'not_a_list': _('Expected a list of items but got type `{input_type}`') @@ -1123,7 +1154,6 @@ class ListField(Field): def __init__(self, *args, **kwargs): self.child = kwargs.pop('child', copy.deepcopy(self.child)) - assert self.child is not None, '`child` is a required argument.' assert not inspect.isclass(self.child), '`child` has not been instantiated.' super(ListField, self).__init__(*args, **kwargs) self.child.bind(field_name='', parent=self) @@ -1152,6 +1182,49 @@ class ListField(Field): return [self.child.to_representation(item) for item in data] +class DictField(Field): + child = _UnvalidatedField() + initial = [] + default_error_messages = { + 'not_a_dict': _('Expected a dictionary of items but got type `{input_type}`') + } + + def __init__(self, *args, **kwargs): + self.child = kwargs.pop('child', copy.deepcopy(self.child)) + assert not inspect.isclass(self.child), '`child` has not been instantiated.' + super(DictField, self).__init__(*args, **kwargs) + self.child.bind(field_name='', parent=self) + + def get_value(self, dictionary): + # We override the default field access in order to support + # lists in HTML forms. + if html.is_html_input(dictionary): + return html.parse_html_list(dictionary, prefix=self.field_name) + return dictionary.get(self.field_name, empty) + + def to_internal_value(self, data): + """ + Dicts of native values <- Dicts of primitive datatypes. + """ + if html.is_html_input(data): + data = html.parse_html_dict(data) + if not isinstance(data, dict): + self.fail('not_a_dict', input_type=type(data).__name__) + return dict([ + (six.text_type(key), self.child.run_validation(value)) + for key, value in data.items() + ]) + + def to_representation(self, value): + """ + List of object instances -> List of dicts of primitive datatypes. + """ + return dict([ + (six.text_type(key), self.child.to_representation(val)) + for key, val in value.items() + ]) + + # Miscellaneous field types... class ReadOnlyField(Field): diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 3e3395c0..1efab85b 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -250,7 +250,7 @@ class FileUploadParser(BaseParser): None, encoding) if result is not None: - return DataAndFiles(None, {'file': result[1]}) + return DataAndFiles({}, {'file': result[1]}) # This is the standard case. possible_sizes = [x.chunk_size for x in upload_handlers if x.chunk_size] @@ -277,7 +277,7 @@ class FileUploadParser(BaseParser): for index, handler in enumerate(upload_handlers): file_obj = handler.file_complete(counters[index]) if file_obj: - return DataAndFiles(None, {'file': file_obj}) + return DataAndFiles({}, {'file': file_obj}) raise ParseError("FileUpload parse error - " "none of upload handlers can handle the stream") @@ -298,7 +298,7 @@ class FileUploadParser(BaseParser): if 'filename*' in filename_parm: return self.get_encoded_filename(filename_parm) return force_text(filename_parm['filename']) - except (AttributeError, KeyError): + except (AttributeError, KeyError, ValueError): pass def get_encoded_filename(self, filename_parm): diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 7b119291..13793f37 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -7,6 +7,7 @@ from django.utils import six from django.utils.encoding import 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 OrderedDict from rest_framework.fields import get_attribute, empty, Field from rest_framework.reverse import reverse from rest_framework.utils import html @@ -103,7 +104,7 @@ class RelatedField(Field): @property def choices(self): - return dict([ + return OrderedDict([ ( six.text_type(self.to_representation(item)), six.text_type(item) @@ -337,7 +338,12 @@ class ManyRelatedField(Field): # We override the default field access in order to support # lists in HTML forms. if html.is_html_input(dictionary): + # Don't return [] if the update is partial + if self.field_name not in dictionary: + if getattr(self.root, 'partial', False): + return empty return dictionary.getlist(self.field_name) + return dictionary.get(self.field_name, empty) def to_internal_value(self, data): @@ -364,7 +370,7 @@ class ManyRelatedField(Field): (item, self.child_relation.to_representation(item)) for item in iterable ] - return dict([ + return OrderedDict([ ( six.text_type(item_representation), six.text_type(item) + ' - ' + six.text_type(item_representation) diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 634338e9..584332e6 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -46,7 +46,7 @@ class BaseRenderer(object): render_style = 'text' def render(self, data, accepted_media_type=None, renderer_context=None): - raise NotImplemented('Renderer class requires .render() to be implemented') + raise NotImplementedError('Renderer class requires .render() to be implemented') class JSONRenderer(BaseRenderer): @@ -410,6 +410,9 @@ class HTMLFormRenderer(BaseRenderer): }) def render_field(self, field, parent_style): + if isinstance(field, serializers.HiddenField): + return '' + style = dict(self.default_style[field]) style.update(field.style) if 'template_pack' not in style: diff --git a/rest_framework/response.py b/rest_framework/response.py index d6ca1aad..c21c60a2 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -81,10 +81,14 @@ class Response(SimpleTemplateResponse): def __getstate__(self): """ - Remove attributes from the response that shouldn't be cached + Remove attributes from the response that shouldn't be cached. """ state = super(Response, self).__getstate__() - for key in ('accepted_renderer', 'renderer_context', 'data'): + for key in ( + 'accepted_renderer', 'renderer_context', 'resolver_match', + 'client', 'request', 'wsgi_request' + ): if key in state: del state[key] + state['_closable_objects'] = [] return state diff --git a/rest_framework/routers.py b/rest_framework/routers.py index 61f3ccab..6a4184e2 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -65,13 +65,13 @@ class BaseRouter(object): If `base_name` is not specified, attempt to automatically determine it from the viewset. """ - raise NotImplemented('get_default_base_name must be overridden') + raise NotImplementedError('get_default_base_name must be overridden') def get_urls(self): """ Return a list of URL patterns, given the registered viewsets. """ - raise NotImplemented('get_urls must be overridden') + raise NotImplementedError('get_urls must be overridden') @property def urls(self): @@ -130,19 +130,13 @@ class SimpleRouter(BaseRouter): If `base_name` is not specified, attempt to automatically determine it from the viewset. """ - # Note that `.model` attribute on views is deprecated, although we - # enforce the deprecation on the view `get_serializer_class()` and - # `get_queryset()` methods, rather than here. - model_cls = getattr(viewset, 'model', None) queryset = getattr(viewset, 'queryset', None) - if model_cls is None and queryset is not None: - model_cls = queryset.model - assert model_cls, '`base_name` argument not specified, and could ' \ + assert queryset is not None, '`base_name` argument not specified, and could ' \ 'not automatically determine the name from the viewset, as ' \ 'it does not have a `.queryset` attribute.' - return model_cls._meta.object_name.lower() + return queryset.model._meta.object_name.lower() def get_routes(self, viewset): """ diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e373cd10..d76658b0 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -12,9 +12,9 @@ response content is handled by parsers and renderers. """ from __future__ import unicode_literals from django.db import models -from django.db.models.fields import FieldDoesNotExist +from django.db.models.fields import FieldDoesNotExist, Field as DjangoField from django.utils.translation import ugettext_lazy as _ -from rest_framework.compat import unicode_to_repr +from rest_framework.compat import postgres_fields, unicode_to_repr from rest_framework.utils import model_meta from rest_framework.utils.field_mapping import ( get_url_kwargs, get_field_kwargs, @@ -177,7 +177,7 @@ class BaseSerializer(Field): ) assert hasattr(self, 'initial_data'), ( - 'Cannot call `.is_valid()` as no `data=` keyword argument was' + 'Cannot call `.is_valid()` as no `data=` keyword argument was ' 'passed when instantiating the serializer instance.' ) @@ -253,7 +253,7 @@ class SerializerMetaclass(type): # If this class is subclassing another Serializer, add that Serializer's # fields. Note that we loop over the bases in *reverse*. This is necessary # in order to maintain the correct order of fields. - for base in bases[::-1]: + for base in reversed(bases): if hasattr(base, '_declared_fields'): fields = list(base._declared_fields.items()) + fields @@ -633,11 +633,11 @@ def raise_errors_on_nested_writes(method_name, serializer, validated_data): If we don't do this explicitly they'd get a less helpful error when calling `.save()` on the serializer. - We don't *automatically* support these sorts of nested writes brecause + We don't *automatically* support these sorts of nested writes because there are too many ambiguities to define a default behavior. Eg. Suppose we have a `UserSerializer` with a nested profile. How should - we handle the case of an update, where the `profile` realtionship does + we handle the case of an update, where the `profile` relationship does not exist? Any of the following might be valid: * Raise an application error. @@ -702,6 +702,7 @@ class ModelSerializer(Serializer): you need you should either declare the extra/differing fields explicitly on the serializer class, or simply use a `Serializer` class. """ + _field_mapping = ClassLookupDict({ models.AutoField: IntegerField, models.BigIntegerField: IntegerField, @@ -724,7 +725,8 @@ class ModelSerializer(Serializer): models.SmallIntegerField: IntegerField, models.TextField: CharField, models.TimeField: TimeField, - models.URLField: URLField, + models.URLField: URLField + # Note: Some version-specific mappings also defined below. }) _related_class = PrimaryKeyRelatedField @@ -880,8 +882,8 @@ class ModelSerializer(Serializer): # Retrieve metadata about fields & relationships on the model class. info = model_meta.get_field_info(model) - # Use the default set of field names if none is supplied explicitly. if fields is None: + # Use the default set of field names if none is supplied explicitly. fields = self._get_default_field_names(declared_fields, info) exclude = getattr(self.Meta, 'exclude', None) if exclude is not None: @@ -891,6 +893,23 @@ class ModelSerializer(Serializer): field_name ) fields.remove(field_name) + else: + # Check that any fields declared on the class are + # also explicitly included in `Meta.fields`. + + # Note that we ignore any fields that were declared on a parent + # class, in order to support only including a subset of fields + # when subclassing serializers. + declared_field_names = set(declared_fields.keys()) + for cls in self.__class__.__bases__: + declared_field_names -= set(getattr(cls, '_declared_fields', [])) + + missing_fields = declared_field_names - set(fields) + assert not missing_fields, ( + 'Field `%s` has been declared on serializer `%s`, but ' + 'is missing from `Meta.fields`.' % + (list(missing_fields)[0], self.__class__.__name__) + ) # Determine the set of model fields, and the fields that they map to. # We actually only need this to deal with the slightly awkward case @@ -922,6 +941,9 @@ class ModelSerializer(Serializer): except FieldDoesNotExist: continue + if not isinstance(model_field, DjangoField): + continue + # Include each of the `unique_for_*` field names. unique_constraint_names |= set([ model_field.unique_for_date, @@ -1024,17 +1046,6 @@ class ModelSerializer(Serializer): (field_name, model.__class__.__name__) ) - # Check that any fields declared on the class are - # also explicitly included in `Meta.fields`. - missing_fields = set(declared_fields.keys()) - set(fields) - if missing_fields: - missing_field = list(missing_fields)[0] - raise ImproperlyConfigured( - 'Field `%s` has been declared on serializer `%s`, but ' - 'is missing from `Meta.fields`.' % - (missing_field, self.__class__.__name__) - ) - # Populate any kwargs defined in `Meta.extra_kwargs` extras = extra_kwargs.get(field_name, {}) if extras.get('read_only', False): @@ -1123,6 +1134,16 @@ class ModelSerializer(Serializer): return NestedSerializer +if hasattr(models, 'UUIDField'): + ModelSerializer._field_mapping[models.UUIDField] = UUIDField + +if postgres_fields: + class CharMappingField(DictField): + child = CharField() + + ModelSerializer._field_mapping[postgres_fields.HStoreField] = CharMappingField + + class HyperlinkedModelSerializer(ModelSerializer): """ A type of `ModelSerializer` that uses hyperlinked relationships instead diff --git a/rest_framework/settings.py b/rest_framework/settings.py index fc6dfecd..e5e5edaf 100644 --- a/rest_framework/settings.py +++ b/rest_framework/settings.py @@ -18,6 +18,7 @@ REST framework settings, checking for user settings first, then falling back to the defaults. """ from __future__ import unicode_literals +from django.test.signals import setting_changed from django.conf import settings from django.utils import importlib, six from rest_framework import ISO_8601 @@ -198,3 +199,13 @@ class APISettings(object): api_settings = APISettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS) + + +def reload_api_settings(*args, **kwargs): + global api_settings + setting, value = kwargs['setting'], kwargs['value'] + if setting == 'REST_FRAMEWORK': + api_settings = APISettings(value, DEFAULTS, IMPORT_STRINGS) + + +setting_changed.connect(reload_api_settings) diff --git a/rest_framework/throttling.py b/rest_framework/throttling.py index 361dbddf..0f10136d 100644 --- a/rest_framework/throttling.py +++ b/rest_framework/throttling.py @@ -32,10 +32,10 @@ class BaseThrottle(object): if num_proxies == 0 or xff is None: return remote_addr addrs = xff.split(',') - client_addr = addrs[-min(num_proxies, len(xff))] + client_addr = addrs[-min(num_proxies, len(addrs))] return client_addr.strip() - return xff if xff else remote_addr + return ''.join(xff.split()) if xff else remote_addr def wait(self): """ @@ -173,12 +173,6 @@ class AnonRateThrottle(SimpleRateThrottle): if request.user.is_authenticated(): return None # Only throttle unauthenticated requests. - ident = request.META.get('HTTP_X_FORWARDED_FOR') - if ident is None: - ident = request.META.get('REMOTE_ADDR') - else: - ident = ''.join(ident.split()) - return self.cache_format % { 'scope': self.scope, 'ident': self.get_ident(request) diff --git a/rest_framework/utils/encoders.py b/rest_framework/utils/encoders.py index 73cbe5d8..bf753271 100644 --- a/rest_framework/utils/encoders.py +++ b/rest_framework/utils/encoders.py @@ -6,12 +6,13 @@ from django.db.models.query import QuerySet from django.utils import six, timezone from django.utils.encoding import force_text from django.utils.functional import Promise -from rest_framework.compat import OrderedDict +from rest_framework.compat import OrderedDict, total_seconds from rest_framework.utils.serializer_helpers import ReturnDict, ReturnList import datetime import decimal import types import json +import uuid class JSONEncoder(json.JSONEncoder): @@ -41,10 +42,12 @@ class JSONEncoder(json.JSONEncoder): representation = representation[:12] return representation elif isinstance(obj, datetime.timedelta): - return six.text_type(obj.total_seconds()) + return six.text_type(total_seconds(obj)) elif isinstance(obj, decimal.Decimal): # Serializers will coerce decimals to strings by default. return float(obj) + elif isinstance(obj, uuid.UUID): + return six.text_type(obj) elif isinstance(obj, QuerySet): return tuple(obj) elif hasattr(obj, 'tolist'): diff --git a/rest_framework/utils/field_mapping.py b/rest_framework/utils/field_mapping.py index cba40d31..c97ec5d0 100644 --- a/rest_framework/utils/field_mapping.py +++ b/rest_framework/utils/field_mapping.py @@ -38,6 +38,9 @@ class ClassLookupDict(object): return self.mapping[cls] raise KeyError('Class %s not found in lookup.', cls.__name__) + def __setitem__(self, key, value): + self.mapping[key] = value + def needs_label(model_field, field_name): """ diff --git a/rest_framework/utils/model_meta.py b/rest_framework/utils/model_meta.py index 375d2e8c..6a5835f5 100644 --- a/rest_framework/utils/model_meta.py +++ b/rest_framework/utils/model_meta.py @@ -121,12 +121,17 @@ def _get_reverse_relationships(opts): """ Returns an `OrderedDict` of field names to `RelationInfo`. """ + # Note that we have a hack here to handle internal API differences for + # this internal API across Django 1.7 -> Django 1.8. + # See: https://code.djangoproject.com/ticket/24208 + reverse_relations = OrderedDict() for relation in opts.get_all_related_objects(): accessor_name = relation.get_accessor_name() + related = getattr(relation, 'related_model', relation.model) reverse_relations[accessor_name] = RelationInfo( model_field=None, - related=relation.model, + related=related, to_many=relation.field.rel.multiple, has_through_model=False ) @@ -134,9 +139,10 @@ def _get_reverse_relationships(opts): # Deal with reverse many-to-many relationships. for relation in opts.get_all_related_many_to_many_objects(): accessor_name = relation.get_accessor_name() + related = getattr(relation, 'related_model', relation.model) reverse_relations[accessor_name] = RelationInfo( model_field=None, - related=relation.model, + related=related, to_many=True, has_through_model=( (getattr(relation.field.rel, 'through', None) is not None) diff --git a/rest_framework/utils/serializer_helpers.py b/rest_framework/utils/serializer_helpers.py index 65a04d06..87bb3ac0 100644 --- a/rest_framework/utils/serializer_helpers.py +++ b/rest_framework/utils/serializer_helpers.py @@ -16,6 +16,14 @@ class ReturnDict(OrderedDict): def copy(self): return ReturnDict(self, serializer=self.serializer) + def __repr__(self): + return dict.__repr__(self) + + def __reduce__(self): + # Pickling these objects will drop the .serializer backlink, + # but preserve the raw data. + return (dict, (dict(self),)) + class ReturnList(list): """ @@ -27,6 +35,14 @@ class ReturnList(list): self.serializer = kwargs.pop('serializer') super(ReturnList, self).__init__(*args, **kwargs) + def __repr__(self): + return list.__repr__(self) + + def __reduce__(self): + # Pickling these objects will drop the .serializer backlink, + # but preserve the raw data. + return (list, (list(self),)) + class BoundField(object): """ @@ -99,3 +115,6 @@ class BindingDict(collections.MutableMapping): def __len__(self): return len(self.fields) + + def __repr__(self): + return dict.__repr__(self.fields) diff --git a/runtests.py b/runtests.py index abf15a62..0008bfae 100755 --- a/runtests.py +++ b/runtests.py @@ -8,8 +8,8 @@ import subprocess PYTEST_ARGS = { - 'default': ['tests'], - 'fast': ['tests', '-q'], + 'default': ['tests', '--tb=short'], + 'fast': ['tests', '--tb=short', '-q'], } FLAKE8_ARGS = ['rest_framework', 'tests', '--ignore=E501'] diff --git a/tests/test_fields.py b/tests/test_fields.py index 775d4618..6744cf64 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -4,6 +4,7 @@ from rest_framework import serializers import datetime import django import pytest +import uuid # Tests for field keyword arguments and core functionality. @@ -467,6 +468,23 @@ class TestURLField(FieldValues): field = serializers.URLField() +class TestUUIDField(FieldValues): + """ + Valid and invalid values for `UUIDField`. + """ + valid_inputs = { + '825d7aeb-05a9-45b5-a5b7-05df87923cda': uuid.UUID('825d7aeb-05a9-45b5-a5b7-05df87923cda'), + '825d7aeb05a945b5a5b705df87923cda': uuid.UUID('825d7aeb-05a9-45b5-a5b7-05df87923cda') + } + invalid_inputs = { + '825d7aeb-05a9-45b5-a5b7': ['"825d7aeb-05a9-45b5-a5b7" is not a valid UUID.'] + } + outputs = { + uuid.UUID('825d7aeb-05a9-45b5-a5b7-05df87923cda'): '825d7aeb-05a9-45b5-a5b7-05df87923cda' + } + field = serializers.UUIDField() + + # Number types... class TestIntegerField(FieldValues): @@ -1029,7 +1047,7 @@ class TestValidImageField(FieldValues): class TestListField(FieldValues): """ - Values for `ListField`. + Values for `ListField` with IntegerField as child. """ valid_inputs = [ ([1, 2, 3], [1, 2, 3]), @@ -1046,6 +1064,55 @@ class TestListField(FieldValues): field = serializers.ListField(child=serializers.IntegerField()) +class TestUnvalidatedListField(FieldValues): + """ + Values for `ListField` with no `child` argument. + """ + valid_inputs = [ + ([1, '2', True, [4, 5, 6]], [1, '2', True, [4, 5, 6]]), + ] + invalid_inputs = [ + ('not a list', ['Expected a list of items but got type `str`']), + ] + outputs = [ + ([1, '2', True, [4, 5, 6]], [1, '2', True, [4, 5, 6]]), + ] + field = serializers.ListField() + + +class TestDictField(FieldValues): + """ + Values for `ListField` with CharField as child. + """ + valid_inputs = [ + ({'a': 1, 'b': '2', 3: 3}, {'a': '1', 'b': '2', '3': '3'}), + ] + invalid_inputs = [ + ({'a': 1, 'b': None}, ['This field may not be null.']), + ('not a dict', ['Expected a dictionary of items but got type `str`']), + ] + outputs = [ + ({'a': 1, 'b': '2', 3: 3}, {'a': '1', 'b': '2', '3': '3'}), + ] + field = serializers.DictField(child=serializers.CharField()) + + +class TestUnvalidatedDictField(FieldValues): + """ + Values for `ListField` with no `child` argument. + """ + valid_inputs = [ + ({'a': 1, 'b': [4, 5, 6], 1: 123}, {'a': 1, 'b': [4, 5, 6], '1': 123}), + ] + invalid_inputs = [ + ('not a dict', ['Expected a dictionary of items but got type `str`']), + ] + outputs = [ + ({'a': 1, 'b': [4, 5, 6]}, {'a': 1, 'b': [4, 5, 6]}), + ] + field = serializers.DictField() + + # Tests for FieldField. # --------------------- diff --git a/tests/test_filters.py b/tests/test_filters.py index dc84dcbd..355f02ce 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -5,13 +5,15 @@ from django.db import models from django.conf.urls import patterns, url from django.core.urlresolvers import reverse from django.test import TestCase +from django.test.utils import override_settings from django.utils import unittest from django.utils.dateparse import parse_date +from django.utils.six.moves import reload_module from rest_framework import generics, serializers, status, filters from rest_framework.compat import django_filters from rest_framework.test import APIRequestFactory from .models import BaseFilterableItem, FilterableItem, BasicModel -from .utils import temporary_setting + factory = APIRequestFactory() @@ -404,7 +406,9 @@ class SearchFilterTests(TestCase): ) def test_search_with_nonstandard_search_param(self): - with temporary_setting('SEARCH_PARAM', 'query', module=filters): + with override_settings(REST_FRAMEWORK={'SEARCH_PARAM': 'query'}): + reload_module(filters) + class SearchListView(generics.ListAPIView): queryset = SearchFilterModel.objects.all() serializer_class = SearchFilterSerializer @@ -422,6 +426,8 @@ class SearchFilterTests(TestCase): ] ) + reload_module(filters) + class OrderingFilterModel(models.Model): title = models.CharField(max_length=20) @@ -467,6 +473,7 @@ class DjangoFilterOrderingTests(TestCase): for d in data: DjangoFilterOrderingModel.objects.create(**d) + @unittest.skipUnless(django_filters, 'django-filter not installed') def test_default_ordering(self): class DjangoFilterOrderingView(generics.ListAPIView): serializer_class = DjangoFilterOrderingSerializer @@ -641,7 +648,9 @@ class OrderingFilterTests(TestCase): ) def test_ordering_with_nonstandard_ordering_param(self): - with temporary_setting('ORDERING_PARAM', 'order', filters): + with override_settings(REST_FRAMEWORK={'ORDERING_PARAM': 'order'}): + reload_module(filters) + class OrderingListView(generics.ListAPIView): queryset = OrderingFilterModel.objects.all() serializer_class = OrderingFilterSerializer @@ -661,6 +670,8 @@ class OrderingFilterTests(TestCase): ] ) + reload_module(filters) + class SensitiveOrderingFilterModel(models.Model): username = models.CharField(max_length=20) diff --git a/tests/test_htmlrenderer.py b/tests/test_htmlrenderer.py index 2edc6b4b..a33b832f 100644 --- a/tests/test_htmlrenderer.py +++ b/tests/test_htmlrenderer.py @@ -56,7 +56,13 @@ class TemplateHTMLRendererTests(TestCase): return Template("example: {{ object }}") raise TemplateDoesNotExist(template_name) + def select_template(template_name_list, dirs=None, using=None): + if template_name_list == ['example.html']: + return Template("example: {{ object }}") + raise TemplateDoesNotExist(template_name_list[0]) + django.template.loader.get_template = get_template + django.template.loader.select_template = select_template def tearDown(self): """ diff --git a/tests/test_model_serializer.py b/tests/test_model_serializer.py index ee556dbc..247b309a 100644 --- a/tests/test_model_serializer.py +++ b/tests/test_model_serializer.py @@ -5,11 +5,14 @@ shortcuts for automatically creating serializers based on a given model class. These tests deal with ensuring that we correctly map the model fields onto an appropriate set of serializer fields for each case. """ +from __future__ import unicode_literals from django.core.exceptions import ImproperlyConfigured from django.core.validators import MaxValueValidator, MinValueValidator, MinLengthValidator from django.db import models from django.test import TestCase +from django.utils import six from rest_framework import serializers +from rest_framework.compat import unicode_repr def dedent(blocktext): @@ -124,7 +127,7 @@ class TestRegularFieldMappings(TestCase): url_field = URLField(max_length=100) custom_field = ModelField(model_field=<tests.test_model_serializer.CustomField: custom_field>) """) - self.assertEqual(repr(TestSerializer()), expected) + self.assertEqual(unicode_repr(TestSerializer()), expected) def test_field_options(self): class TestSerializer(serializers.ModelSerializer): @@ -142,7 +145,14 @@ class TestRegularFieldMappings(TestCase): descriptive_field = IntegerField(help_text='Some help text', label='A label') choices_field = ChoiceField(choices=[('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')]) """) - self.assertEqual(repr(TestSerializer()), expected) + if six.PY2: + # This particular case is too awkward to resolve fully across + # both py2 and py3. + expected = expected.replace( + "('red', 'Red'), ('blue', 'Blue'), ('green', 'Green')", + "(u'red', u'Red'), (u'blue', u'Blue'), (u'green', u'Green')" + ) + self.assertEqual(unicode_repr(TestSerializer()), expected) def test_method_field(self): """ @@ -221,7 +231,7 @@ class TestRegularFieldMappings(TestCase): model = RegularFieldsModel fields = ('auto_field',) - with self.assertRaises(ImproperlyConfigured) as excinfo: + with self.assertRaises(AssertionError) as excinfo: TestSerializer().fields expected = ( 'Field `missing` has been declared on serializer ' @@ -229,6 +239,26 @@ class TestRegularFieldMappings(TestCase): ) assert str(excinfo.exception) == expected + def test_missing_superclass_field(self): + """ + Fields that have been declared on a parent of the serializer class may + be excluded from the `Meta.fields` option. + """ + class TestSerializer(serializers.ModelSerializer): + missing = serializers.ReadOnlyField() + + class Meta: + model = RegularFieldsModel + + class ChildSerializer(TestSerializer): + missing = serializers.ReadOnlyField() + + class Meta: + model = RegularFieldsModel + fields = ('auto_field',) + + ChildSerializer().fields + # Tests for relational field mappings. # ------------------------------------ @@ -276,7 +306,7 @@ class TestRelationalFieldMappings(TestCase): many_to_many = PrimaryKeyRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all()) through = PrimaryKeyRelatedField(many=True, read_only=True) """) - self.assertEqual(repr(TestSerializer()), expected) + self.assertEqual(unicode_repr(TestSerializer()), expected) def test_nested_relations(self): class TestSerializer(serializers.ModelSerializer): @@ -300,7 +330,7 @@ class TestRelationalFieldMappings(TestCase): id = IntegerField(label='ID', read_only=True) name = CharField(max_length=100) """) - self.assertEqual(repr(TestSerializer()), expected) + self.assertEqual(unicode_repr(TestSerializer()), expected) def test_hyperlinked_relations(self): class TestSerializer(serializers.HyperlinkedModelSerializer): @@ -315,7 +345,7 @@ class TestRelationalFieldMappings(TestCase): many_to_many = HyperlinkedRelatedField(many=True, queryset=ManyToManyTargetModel.objects.all(), view_name='manytomanytargetmodel-detail') through = HyperlinkedRelatedField(many=True, read_only=True, view_name='throughtargetmodel-detail') """) - self.assertEqual(repr(TestSerializer()), expected) + self.assertEqual(unicode_repr(TestSerializer()), expected) def test_nested_hyperlinked_relations(self): class TestSerializer(serializers.HyperlinkedModelSerializer): @@ -339,7 +369,7 @@ class TestRelationalFieldMappings(TestCase): url = HyperlinkedIdentityField(view_name='throughtargetmodel-detail') name = CharField(max_length=100) """) - self.assertEqual(repr(TestSerializer()), expected) + self.assertEqual(unicode_repr(TestSerializer()), expected) def test_pk_reverse_foreign_key(self): class TestSerializer(serializers.ModelSerializer): @@ -353,7 +383,7 @@ class TestRelationalFieldMappings(TestCase): name = CharField(max_length=100) reverse_foreign_key = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all()) """) - self.assertEqual(repr(TestSerializer()), expected) + self.assertEqual(unicode_repr(TestSerializer()), expected) def test_pk_reverse_one_to_one(self): class TestSerializer(serializers.ModelSerializer): @@ -367,7 +397,7 @@ class TestRelationalFieldMappings(TestCase): name = CharField(max_length=100) reverse_one_to_one = PrimaryKeyRelatedField(queryset=RelationalModel.objects.all()) """) - self.assertEqual(repr(TestSerializer()), expected) + self.assertEqual(unicode_repr(TestSerializer()), expected) def test_pk_reverse_many_to_many(self): class TestSerializer(serializers.ModelSerializer): @@ -381,7 +411,7 @@ class TestRelationalFieldMappings(TestCase): name = CharField(max_length=100) reverse_many_to_many = PrimaryKeyRelatedField(many=True, queryset=RelationalModel.objects.all()) """) - self.assertEqual(repr(TestSerializer()), expected) + self.assertEqual(unicode_repr(TestSerializer()), expected) def test_pk_reverse_through(self): class TestSerializer(serializers.ModelSerializer): @@ -395,7 +425,7 @@ class TestRelationalFieldMappings(TestCase): name = CharField(max_length=100) reverse_through = PrimaryKeyRelatedField(many=True, read_only=True) """) - self.assertEqual(repr(TestSerializer()), expected) + self.assertEqual(unicode_repr(TestSerializer()), expected) class TestIntegration(TestCase): diff --git a/tests/test_multitable_inheritance.py b/tests/test_multitable_inheritance.py index e1b40cc7..15627e1d 100644 --- a/tests/test_multitable_inheritance.py +++ b/tests/test_multitable_inheritance.py @@ -48,8 +48,8 @@ class InheritedModelSerializationTests(TestCase): Assert that a model with a onetoone field that is the primary key is not treated like a derived model """ - parent = ParentModel(name1='parent name') - associate = AssociatedModel(name='hello', ref=parent) + parent = ParentModel.objects.create(name1='parent name') + associate = AssociatedModel.objects.create(name='hello', ref=parent) serializer = AssociatedModelSerializer(associate) self.assertEqual(set(serializer.data.keys()), set(['name', 'ref'])) diff --git a/tests/test_parsers.py b/tests/test_parsers.py index d28d8bd4..1d2054ac 100644 --- a/tests/test_parsers.py +++ b/tests/test_parsers.py @@ -161,7 +161,9 @@ class TestFileUploadParser(TestCase): self.__replace_content_disposition('inline; filename=fallback.txt; filename*=utf-8--ÀĥƦ.txt') filename = parser.get_filename(self.stream, None, self.parser_context) - self.assertEqual(filename, 'fallback.txt') + # Malformed. Either None or 'fallback.txt' will be acceptable. + # See also https://code.djangoproject.com/ticket/24209 + self.assertIn(filename, ('fallback.txt', None)) def __replace_content_disposition(self, disposition): self.parser_context['request'].META['HTTP_CONTENT_DISPOSITION'] = disposition diff --git a/tests/test_relations.py b/tests/test_relations.py index 62353dc2..d478d855 100644 --- a/tests/test_relations.py +++ b/tests/test_relations.py @@ -1,6 +1,8 @@ from .utils import mock_reverse, fail_reverse, BadType, MockObject, MockQueryset from django.core.exceptions import ImproperlyConfigured +from django.utils.datastructures import MultiValueDict from rest_framework import serializers +from rest_framework.fields import empty from rest_framework.test import APISimpleTestCase import pytest @@ -134,3 +136,34 @@ class TestSlugRelatedField(APISimpleTestCase): def test_representation(self): representation = self.field.to_representation(self.instance) assert representation == self.instance.name + + +class TestManyRelatedField(APISimpleTestCase): + def setUp(self): + self.instance = MockObject(pk=1, name='foo') + self.field = serializers.StringRelatedField(many=True) + self.field.field_name = 'foo' + + def test_get_value_regular_dictionary_full(self): + assert 'bar' == self.field.get_value({'foo': 'bar'}) + assert empty == self.field.get_value({'baz': 'bar'}) + + def test_get_value_regular_dictionary_partial(self): + setattr(self.field.root, 'partial', True) + assert 'bar' == self.field.get_value({'foo': 'bar'}) + assert empty == self.field.get_value({'baz': 'bar'}) + + def test_get_value_multi_dictionary_full(self): + mvd = MultiValueDict({'foo': ['bar1', 'bar2']}) + assert ['bar1', 'bar2'] == self.field.get_value(mvd) + + mvd = MultiValueDict({'baz': ['bar1', 'bar2']}) + assert [] == self.field.get_value(mvd) + + def test_get_value_multi_dictionary_partial(self): + setattr(self.field.root, 'partial', True) + mvd = MultiValueDict({'foo': ['bar1', 'bar2']}) + assert ['bar1', 'bar2'] == self.field.get_value(mvd) + + mvd = MultiValueDict({'baz': ['bar1', 'bar2']}) + assert empty == self.field.get_value(mvd) diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 00a24fb1..54eea8ce 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -22,7 +22,6 @@ from rest_framework.test import APIRequestFactory from collections import MutableMapping import datetime import json -import pickle import re @@ -618,84 +617,24 @@ class CacheRenderTest(TestCase): urls = 'tests.test_renderers' - cache_key = 'just_a_cache_key' - - @classmethod - def _get_pickling_errors(cls, obj, seen=None): - """ Return any errors that would be raised if `obj' is pickled - Courtesy of koffie @ http://stackoverflow.com/a/7218986/109897 - """ - if seen is None: - seen = [] - try: - state = obj.__getstate__() - except AttributeError: - return - if state is None: - return - if isinstance(state, tuple): - if not isinstance(state[0], dict): - state = state[1] - else: - state = state[0].update(state[1]) - result = {} - for i in state: - try: - pickle.dumps(state[i], protocol=2) - except pickle.PicklingError: - if not state[i] in seen: - seen.append(state[i]) - result[i] = cls._get_pickling_errors(state[i], seen) - return result - - def http_resp(self, http_method, url): - """ - Simple wrapper for Client http requests - Removes the `client' and `request' attributes from as they are - added by django.test.client.Client and not part of caching - responses outside of tests. - """ - method = getattr(self.client, http_method) - resp = method(url) - resp._closable_objects = [] - del resp.client, resp.request - try: - del resp.wsgi_request - except AttributeError: - pass - return resp - - def test_obj_pickling(self): - """ - Test that responses are properly pickled - """ - resp = self.http_resp('get', '/cache') - - # Make sure that no pickling errors occurred - self.assertEqual(self._get_pickling_errors(resp), {}) - - # Unfortunately LocMem backend doesn't raise PickleErrors but returns - # None instead. - cache.set(self.cache_key, resp) - self.assertTrue(cache.get(self.cache_key) is not None) - def test_head_caching(self): """ Test caching of HEAD requests """ - resp = self.http_resp('head', '/cache') - cache.set(self.cache_key, resp) - - cached_resp = cache.get(self.cache_key) - self.assertIsInstance(cached_resp, Response) + response = self.client.head('/cache') + cache.set('key', response) + cached_response = cache.get('key') + assert isinstance(cached_response, Response) + assert cached_response.content == response.content + assert cached_response.status_code == response.status_code def test_get_caching(self): """ Test caching of GET requests """ - resp = self.http_resp('get', '/cache') - cache.set(self.cache_key, resp) - - cached_resp = cache.get(self.cache_key) - self.assertIsInstance(cached_resp, Response) - self.assertEqual(cached_resp.content, resp.content) + response = self.client.get('/cache') + cache.set('key', response) + cached_response = cache.get('key') + assert isinstance(cached_response, Response) + assert cached_response.content == response.content + assert cached_response.status_code == response.status_code diff --git a/tests/test_routers.py b/tests/test_routers.py index 86113f5d..948c69bb 100644 --- a/tests/test_routers.py +++ b/tests/test_routers.py @@ -180,7 +180,7 @@ class TestLookupValueRegex(TestCase): class TestTrailingSlashIncluded(TestCase): def setUp(self): class NoteViewSet(viewsets.ModelViewSet): - model = RouterTestModel + queryset = RouterTestModel.objects.all() self.router = SimpleRouter() self.router.register(r'notes', NoteViewSet) @@ -195,7 +195,7 @@ class TestTrailingSlashIncluded(TestCase): class TestTrailingSlashRemoved(TestCase): def setUp(self): class NoteViewSet(viewsets.ModelViewSet): - model = RouterTestModel + queryset = RouterTestModel.objects.all() self.router = SimpleRouter(trailing_slash=False) self.router.register(r'notes', NoteViewSet) @@ -210,7 +210,8 @@ class TestTrailingSlashRemoved(TestCase): class TestNameableRoot(TestCase): def setUp(self): class NoteViewSet(viewsets.ModelViewSet): - model = RouterTestModel + queryset = RouterTestModel.objects.all() + self.router = DefaultRouter() self.router.root_view_name = 'nameable-root' self.router.register(r'notes', NoteViewSet) diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 68bbbe98..b7a0484b 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -3,6 +3,7 @@ from __future__ import unicode_literals from .utils import MockObject from rest_framework import serializers from rest_framework.compat import unicode_repr +import pickle import pytest @@ -278,3 +279,19 @@ class TestNotRequiredOutput: serializer = ExampleSerializer(instance) with pytest.raises(AttributeError): serializer.data + + +class TestCacheSerializerData: + def test_cache_serializer_data(self): + """ + Caching serializer data with pickle will drop the serializer info, + but does preserve the data itself. + """ + class ExampleSerializer(serializers.Serializer): + field1 = serializers.CharField() + field2 = serializers.CharField() + + serializer = ExampleSerializer({'field1': 'a', 'field2': 'b'}) + pickled = pickle.dumps(serializer.data) + data = pickle.loads(pickled) + assert data == {'field1': 'a', 'field2': 'b'} diff --git a/tests/utils.py b/tests/utils.py index 5e902ba9..5b2d7586 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,30 +1,5 @@ -from contextlib import contextmanager from django.core.exceptions import ObjectDoesNotExist from django.core.urlresolvers import NoReverseMatch -from django.utils import six -from rest_framework.settings import api_settings - - -@contextmanager -def temporary_setting(setting, value, module=None): - """ - Temporarily change value of setting for test. - - Optionally reload given module, useful when module uses value of setting on - import. - """ - original_value = getattr(api_settings, setting) - setattr(api_settings, setting, value) - - if module is not None: - six.moves.reload_module(module) - - yield - - setattr(api_settings, setting, original_value) - - if module is not None: - six.moves.reload_module(module) class MockObject(object): @@ -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,master} + {py27,py32,py33,py34}-django{17,18alpha,master} [testenv] commands = ./runtests.py --fast @@ -14,21 +14,22 @@ 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 + django18alpha: https://www.djangoproject.com/download/1.8a1/tarball/ djangomaster: https://github.com/django/django/zipball/master {py26,py27}-django{14,15,16,17}: django-guardian==1.2.3 {py26,py27}-django{14,15,16}: oauth2==1.5.211 {py26,py27}-django{14,15,16}: django-oauth-plus==2.2.1 {py26,py27}-django{14,15}: django-oauth2-provider==0.2.3 {py26,py27}-django16: django-oauth2-provider==0.2.4 - pytest-django==2.6.1 - django-filter==0.7 + pytest-django==2.8.0 + django-filter==0.9.2 defusedxml==0.3 markdown>=2.1.0 PyYAML>=3.10 [testenv:py27-flake8] deps = - pytest==2.5.2 + pytest==2.6.4 flake8==2.2.2 commands = ./runtests.py --lintonly |
