diff --git a/.travis.yml b/.travis.yml index 01f3209e3..5a6900a5e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,23 +8,23 @@ python: - "3.4" env: - - DJANGO="https://www.djangoproject.com/download/1.7.b4/tarball/" + - DJANGO="https://www.djangoproject.com/download/1.7c2/tarball/" - DJANGO="django==1.6.5" - DJANGO="django==1.5.8" - DJANGO="django==1.4.13" install: - pip install $DJANGO - - pip install defusedxml==0.3 Pillow==2.3.0 + - pip install defusedxml==0.3 + - pip install Pillow==2.3.0 + - pip install django-guardian==1.2.3 - pip install pytest-django==2.6.1 - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install oauth2==1.5.211; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth-plus==2.2.4; fi" - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-oauth2-provider==0.2.4; fi" - - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install django-guardian==1.1.1; fi" - "if [[ ${DJANGO::11} == 'django==1.3' ]]; then pip install django-filter==0.5.4; fi" - "if [[ ${DJANGO::11} != 'django==1.3' ]]; then pip install django-filter==0.7; fi" - - "if [[ ${TRAVIS_PYTHON_VERSION::1} == '3' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" - - "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7.b4/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" + - "if [[ ${DJANGO} == 'https://www.djangoproject.com/download/1.7c2/tarball/' ]]; then pip install -e git+https://github.com/linovia/django-guardian.git@feature/django_1_7#egg=django-guardian-1.2.0; fi" - export PYTHONPATH=. script: @@ -33,7 +33,7 @@ script: matrix: exclude: - python: "2.6" - env: DJANGO="https://www.djangoproject.com/download/1.7.b4/tarball/" + env: DJANGO="https://www.djangoproject.com/download/1.7c2/tarball/" - python: "3.2" env: DJANGO="django==1.4.13" - python: "3.3" diff --git a/README.md b/README.md index eea002b4a..0eaf5c83c 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,3 @@ ---- - -#### Django REST framework 3 - Kickstarter announcement! - -We are currently running a Kickstarter campaign to help fund the development of Django REST framework 3. - -If you want to help drive sustainable open-source development forward, then **please check out [the Kickstarter project](https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3) and consider funding us.** - ---- - # Django REST framework [![build-status-image]][travis] @@ -18,7 +8,7 @@ If you want to help drive sustainable open-source development forward, then **pl # Overview -Django REST framework is a powerful and flexible toolkit that makes it easy to build Web APIs. +Django REST framework is a powerful and flexible toolkit for building Web APIs. Some reasons you might want to use REST framework: diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index d23f3d193..95d9fad33 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -62,7 +62,7 @@ A dictionary of error codes to error messages. ### `widget` Used only if rendering the field to HTML. -This argument sets the widget that should be used to render the field. +This argument sets the widget that should be used to render the field. For more details, and a list of available widgets, see [the Django documentation on form widgets][django-widgets]. ### `label` @@ -362,12 +362,17 @@ The [drf-compound-fields][drf-compound-fields] package provides "compound" seria The [drf-extra-fields][drf-extra-fields] package provides extra serializer fields for REST framework, including `Base64ImageField` and `PointField` classes. +## django-rest-framework-gis + +The [django-rest-framework-gis][django-rest-framework-gis] package provides geographic addons for django rest framework like a `GeometryField` field and a GeoJSON serializer. [cite]: https://docs.djangoproject.com/en/dev/ref/forms/api/#django.forms.Form.cleaned_data [FILE_UPLOAD_HANDLERS]: https://docs.djangoproject.com/en/dev/ref/settings/#std:setting-FILE_UPLOAD_HANDLERS [ecma262]: http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15 [strftime]: http://docs.python.org/2/library/datetime.html#strftime-and-strptime-behavior +[django-widgets]: https://docs.djangoproject.com/en/dev/ref/forms/widgets/ [iso8601]: http://www.w3.org/TR/NOTE-datetime [drf-compound-fields]: http://drf-compound-fields.readthedocs.org [drf-extra-fields]: https://github.com/Hipo/drf-extra-fields +[django-rest-framework-gis]: https://github.com/djangonauts/django-rest-framework-gis diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index bb748981e..e9efe7092 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -43,6 +43,12 @@ For more complex cases you might also want to override various methods on the vi return 20 return 100 + def list(self, request): + # Note the use of `get_queryset()` instead of `self.queryset` + queryset = self.get_queryset() + serializer = UserSerializer(queryset, many=True) + return Response(serializer.data) + For very simple cases you might want to pass through any class attributes using the `.as_view()` method. For example, your URLconf might include something the following entry. url(r'^/users/', ListCreateAPIView.as_view(model=User), name='user-list') @@ -63,7 +69,7 @@ Each of the concrete generic views provided is built by combining `GenericAPIVie The following attributes control the basic view behavior. -* `queryset` - The queryset that should be used for returning objects from this view. Typically, you must either set this attribute, or override the `get_queryset()` method. +* `queryset` - The queryset that should be used for returning objects from this view. Typically, you must either set this attribute, or override the `get_queryset()` method. If you are overriding a view method, it is important that you call `get_queryset()` instead of accessing this property directly, as `queryset` will get evaluated once, and those results will be cached for all subsequent requests. * `serializer_class` - The serializer class that should be used for validating and deserializing input, and for serializing output. Typically, you must either set this attribute, or override the `get_serializer_class()` method. * `lookup_field` - The model field that should be used to for performing object lookup of individual model instances. Defaults to `'pk'`. Note that when using hyperlinked APIs you'll need to ensure that *both* the API views *and* the serializer classes set the lookup fields if you need to use a custom value. * `lookup_url_kwarg` - The URL keyword argument that should be used for object lookup. The URL conf should include a keyword argument corresponding to this value. If unset this defaults to using the same value as `lookup_field`. @@ -93,6 +99,8 @@ The following attributes are used to control pagination when used with list view 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. +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. + May be overridden to provide dynamic behavior such as returning a queryset that is specific to the user making the request. For example: diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index c44b22de8..38ae3d0a9 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -244,7 +244,7 @@ The [REST Condition][rest-condition] package is another extension for building c [authentication]: authentication.md [throttling]: throttling.md [filtering]: filtering.md -[contribauth]: https://docs.djangoproject.com/en/1.0/topics/auth/#permissions +[contribauth]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#custom-permissions [objectpermissions]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#handling-object-permissions [guardian]: https://github.com/lukaszb/django-guardian [get_objects_for_user]: http://pythonhosted.org/django-guardian/api/guardian.shortcuts.html#get-objects-for-user diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index cedf1ff7b..29b7851b6 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -580,7 +580,21 @@ The following custom model serializer could be used as a base class for model se def get_pk_field(self, model_field): return None +--- +# Third party packages + +The following third party packages are also available. + +## MongoengineModelSerializer + +The [django-rest-framework-mongoengine][mongoengine] package provides a `MongoEngineModelSerializer` serializer class that supports using MongoDB as the storage layer for Django REST framework. + +## GeoFeatureModelSerializer + +The [django-rest-framework-gis][django-rest-framework-gis] package provides a `GeoFeatureModelSerializer` serializer class that supports GeoJSON both for read and write operations. [cite]: https://groups.google.com/d/topic/django-users/sVFaOfQi4wY/discussion [relations]: relations.md +[mongoengine]: https://github.com/umutbozkurt/django-rest-framework-mongoengine +[django-rest-framework-gis]: https://github.com/djangonauts/django-rest-framework-gis diff --git a/docs/api-guide/throttling.md b/docs/api-guide/throttling.md index d223f9b35..832304f15 100644 --- a/docs/api-guide/throttling.md +++ b/docs/api-guide/throttling.md @@ -58,7 +58,7 @@ using the `APIView` class based views. Or, if you're using the `@api_view` decorator with function based views. - @api_view('GET') + @api_view(['GET']) @throttle_classes([UserRateThrottle]) def example_view(request, format=None): content = { diff --git a/docs/api-guide/viewsets.md b/docs/api-guide/viewsets.md index dc5d01a25..b32f5a805 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -151,7 +151,7 @@ The `@action` decorator will route `POST` requests by default, but may also acce @detail_route(methods=['post', 'delete']) def unset_password(self, request, pk=None): ... - + The two new actions will then be available at the urls `^users/{pk}/set_password/$` and `^users/{pk}/unset_password/$` --- diff --git a/docs/css/default.css b/docs/css/default.css index af6a9cc03..7f3acfed2 100644 --- a/docs/css/default.css +++ b/docs/css/default.css @@ -307,3 +307,76 @@ table { .side-nav { overflow-y: scroll; } + + +ul.sponsor.diamond li a { + float: left; + width: 600px; + height: 20px; + text-align: center; + margin: 10px 70px; + padding: 300px 0 0 0; + background-position: 0 50%; + background-size: 600px auto; + background-repeat: no-repeat; + font-size: 200%; +} + +@media (max-width: 1000px) { + ul.sponsor.diamond li a { + float: left; + width: 300px; + height: 20px; + text-align: center; + margin: 10px 40px; + padding: 300px 0 0 0; + background-position: 0 50%; + background-size: 280px auto; + background-repeat: no-repeat; + font-size: 150%; + } +} + +ul.sponsor.platinum li a { + float: left; + width: 300px; + height: 20px; + text-align: center; + margin: 10px 40px; + padding: 300px 0 0 0; + background-position: 0 50%; + background-size: 280px auto; + background-repeat: no-repeat; + font-size: 150%; +} + +ul.sponsor.gold li a { + float: left; + width: 130px; + height: 20px; + text-align: center; + margin: 10px 30px; + padding: 150px 0 0 0; + background-position: 0 50%; + background-size: 130px auto; + background-repeat: no-repeat; + font-size: 120%; +} + +ul.sponsor.silver li a { + float: left; + width: 130px; + height: 20px; + text-align: center; + margin: 10px 30px; + padding: 150px 0 0 0; + background-position: 0 50%; + background-size: 130px auto; + background-repeat: no-repeat; + font-size: 120%; +} + +ul.sponsor { + list-style: none; + display: block; +} diff --git a/docs/img/sponsors/0-eventbrite.png b/docs/img/sponsors/0-eventbrite.png new file mode 100644 index 000000000..6c7392936 Binary files /dev/null and b/docs/img/sponsors/0-eventbrite.png differ diff --git a/docs/img/sponsors/1-cyan.png b/docs/img/sponsors/1-cyan.png new file mode 100644 index 000000000..d6b55b4c5 Binary files /dev/null and b/docs/img/sponsors/1-cyan.png differ diff --git a/docs/img/sponsors/1-divio.png b/docs/img/sponsors/1-divio.png new file mode 100644 index 000000000..8ced88f82 Binary files /dev/null and b/docs/img/sponsors/1-divio.png differ diff --git a/docs/img/sponsors/1-kuwaitnet.png b/docs/img/sponsors/1-kuwaitnet.png new file mode 100644 index 000000000..8b2d0550a Binary files /dev/null and b/docs/img/sponsors/1-kuwaitnet.png differ diff --git a/docs/img/sponsors/1-lulu.png b/docs/img/sponsors/1-lulu.png new file mode 100644 index 000000000..8a28bfa9f Binary files /dev/null and b/docs/img/sponsors/1-lulu.png differ diff --git a/docs/img/sponsors/1-potato.png b/docs/img/sponsors/1-potato.png new file mode 100644 index 000000000..ad38abdd2 Binary files /dev/null and b/docs/img/sponsors/1-potato.png differ diff --git a/docs/img/sponsors/1-purplebit.png b/docs/img/sponsors/1-purplebit.png new file mode 100644 index 000000000..0df63bf60 Binary files /dev/null and b/docs/img/sponsors/1-purplebit.png differ diff --git a/docs/img/sponsors/1-runscope.png b/docs/img/sponsors/1-runscope.png new file mode 100644 index 000000000..d80a4b85b Binary files /dev/null and b/docs/img/sponsors/1-runscope.png differ diff --git a/docs/img/sponsors/1-simple-energy.png b/docs/img/sponsors/1-simple-energy.png new file mode 100644 index 000000000..f59f7374c Binary files /dev/null and b/docs/img/sponsors/1-simple-energy.png differ diff --git a/docs/img/sponsors/1-vokal_interactive.png b/docs/img/sponsors/1-vokal_interactive.png new file mode 100644 index 000000000..431482dca Binary files /dev/null and b/docs/img/sponsors/1-vokal_interactive.png differ diff --git a/docs/img/sponsors/1-wiredrive.png b/docs/img/sponsors/1-wiredrive.png new file mode 100644 index 000000000..c9befefe4 Binary files /dev/null and b/docs/img/sponsors/1-wiredrive.png differ diff --git a/docs/img/sponsors/2-byte.png b/docs/img/sponsors/2-byte.png new file mode 100644 index 000000000..2c3777b50 Binary files /dev/null and b/docs/img/sponsors/2-byte.png differ diff --git a/docs/img/sponsors/2-compile.png b/docs/img/sponsors/2-compile.png new file mode 100644 index 000000000..858aa09d4 Binary files /dev/null and b/docs/img/sponsors/2-compile.png differ diff --git a/docs/img/sponsors/2-crate.png b/docs/img/sponsors/2-crate.png new file mode 100644 index 000000000..6ef6b5da5 Binary files /dev/null and b/docs/img/sponsors/2-crate.png differ diff --git a/docs/img/sponsors/2-cryptico.png b/docs/img/sponsors/2-cryptico.png new file mode 100644 index 000000000..2d86afe81 Binary files /dev/null and b/docs/img/sponsors/2-cryptico.png differ diff --git a/docs/img/sponsors/2-django.png b/docs/img/sponsors/2-django.png new file mode 100644 index 000000000..c89e19cb3 Binary files /dev/null and b/docs/img/sponsors/2-django.png differ diff --git a/docs/img/sponsors/2-galileo_press.png b/docs/img/sponsors/2-galileo_press.png new file mode 100644 index 000000000..f77e6c0a8 Binary files /dev/null and b/docs/img/sponsors/2-galileo_press.png differ diff --git a/docs/img/sponsors/2-heroku.png b/docs/img/sponsors/2-heroku.png new file mode 100644 index 000000000..224476596 Binary files /dev/null and b/docs/img/sponsors/2-heroku.png differ diff --git a/docs/img/sponsors/2-hipflask.png b/docs/img/sponsors/2-hipflask.png new file mode 100644 index 000000000..c74735c34 Binary files /dev/null and b/docs/img/sponsors/2-hipflask.png differ diff --git a/docs/img/sponsors/2-hipo.png b/docs/img/sponsors/2-hipo.png new file mode 100644 index 000000000..2b854c6d4 Binary files /dev/null and b/docs/img/sponsors/2-hipo.png differ diff --git a/docs/img/sponsors/2-koordinates.png b/docs/img/sponsors/2-koordinates.png new file mode 100644 index 000000000..f38601b34 Binary files /dev/null and b/docs/img/sponsors/2-koordinates.png differ diff --git a/docs/img/sponsors/2-laterpay.png b/docs/img/sponsors/2-laterpay.png new file mode 100644 index 000000000..75eb97d3f Binary files /dev/null and b/docs/img/sponsors/2-laterpay.png differ diff --git a/docs/img/sponsors/2-lightning_kite.png b/docs/img/sponsors/2-lightning_kite.png new file mode 100644 index 000000000..ffdced04e Binary files /dev/null and b/docs/img/sponsors/2-lightning_kite.png differ diff --git a/docs/img/sponsors/2-mirus_research.png b/docs/img/sponsors/2-mirus_research.png new file mode 100644 index 000000000..b15440708 Binary files /dev/null and b/docs/img/sponsors/2-mirus_research.png differ diff --git a/docs/img/sponsors/2-nexthub.png b/docs/img/sponsors/2-nexthub.png new file mode 100644 index 000000000..9bf76e0bf Binary files /dev/null and b/docs/img/sponsors/2-nexthub.png differ diff --git a/docs/img/sponsors/2-opbeat.png b/docs/img/sponsors/2-opbeat.png new file mode 100644 index 000000000..c71a52417 Binary files /dev/null and b/docs/img/sponsors/2-opbeat.png differ diff --git a/docs/img/sponsors/2-prorenata.png b/docs/img/sponsors/2-prorenata.png new file mode 100644 index 000000000..f5e8bb762 Binary files /dev/null and b/docs/img/sponsors/2-prorenata.png differ diff --git a/docs/img/sponsors/2-rapasso.png b/docs/img/sponsors/2-rapasso.png new file mode 100644 index 000000000..618e294be Binary files /dev/null and b/docs/img/sponsors/2-rapasso.png differ diff --git a/docs/img/sponsors/2-schuberg_philis.png b/docs/img/sponsors/2-schuberg_philis.png new file mode 100644 index 000000000..fd9282eeb Binary files /dev/null and b/docs/img/sponsors/2-schuberg_philis.png differ diff --git a/docs/img/sponsors/2-security_compass.png b/docs/img/sponsors/2-security_compass.png new file mode 100644 index 000000000..abd63dbe3 Binary files /dev/null and b/docs/img/sponsors/2-security_compass.png differ diff --git a/docs/img/sponsors/2-sga.png b/docs/img/sponsors/2-sga.png new file mode 100644 index 000000000..2b2a3b3bb Binary files /dev/null and b/docs/img/sponsors/2-sga.png differ diff --git a/docs/img/sponsors/2-sirono.png b/docs/img/sponsors/2-sirono.png new file mode 100644 index 000000000..0a243001a Binary files /dev/null and b/docs/img/sponsors/2-sirono.png differ diff --git a/docs/img/sponsors/2-vinta.png b/docs/img/sponsors/2-vinta.png new file mode 100644 index 000000000..4f4d75bc1 Binary files /dev/null and b/docs/img/sponsors/2-vinta.png differ diff --git a/docs/img/sponsors/3-aba.png b/docs/img/sponsors/3-aba.png new file mode 100644 index 000000000..cefa3dd60 Binary files /dev/null and b/docs/img/sponsors/3-aba.png differ diff --git a/docs/img/sponsors/3-aditium.png b/docs/img/sponsors/3-aditium.png new file mode 100644 index 000000000..0952b08c8 Binary files /dev/null and b/docs/img/sponsors/3-aditium.png differ diff --git a/docs/img/sponsors/3-alwaysdata.png b/docs/img/sponsors/3-alwaysdata.png new file mode 100644 index 000000000..4095774b7 Binary files /dev/null and b/docs/img/sponsors/3-alwaysdata.png differ diff --git a/docs/img/sponsors/3-ax_semantics.png b/docs/img/sponsors/3-ax_semantics.png new file mode 100644 index 000000000..c072e028a Binary files /dev/null and b/docs/img/sponsors/3-ax_semantics.png differ diff --git a/docs/img/sponsors/3-beefarm.png b/docs/img/sponsors/3-beefarm.png new file mode 100644 index 000000000..3348df42a Binary files /dev/null and b/docs/img/sponsors/3-beefarm.png differ diff --git a/docs/img/sponsors/3-blimp.png b/docs/img/sponsors/3-blimp.png new file mode 100644 index 000000000..494bf7924 Binary files /dev/null and b/docs/img/sponsors/3-blimp.png differ diff --git a/docs/img/sponsors/3-brightloop.png b/docs/img/sponsors/3-brightloop.png new file mode 100644 index 000000000..8d5e85a66 Binary files /dev/null and b/docs/img/sponsors/3-brightloop.png differ diff --git a/docs/img/sponsors/3-cantemo.gif b/docs/img/sponsors/3-cantemo.gif new file mode 100644 index 000000000..17b1e8d05 Binary files /dev/null and b/docs/img/sponsors/3-cantemo.gif differ diff --git a/docs/img/sponsors/3-crosswordtracker.png b/docs/img/sponsors/3-crosswordtracker.png new file mode 100644 index 000000000..f72362ea9 Binary files /dev/null and b/docs/img/sponsors/3-crosswordtracker.png differ diff --git a/docs/img/sponsors/3-fluxility.png b/docs/img/sponsors/3-fluxility.png new file mode 100644 index 000000000..eacd7da97 Binary files /dev/null and b/docs/img/sponsors/3-fluxility.png differ diff --git a/docs/img/sponsors/3-garfo.png b/docs/img/sponsors/3-garfo.png new file mode 100644 index 000000000..a9bdea0a0 Binary files /dev/null and b/docs/img/sponsors/3-garfo.png differ diff --git a/docs/img/sponsors/3-gizmag.png b/docs/img/sponsors/3-gizmag.png new file mode 100644 index 000000000..a8d41bd02 Binary files /dev/null and b/docs/img/sponsors/3-gizmag.png differ diff --git a/docs/img/sponsors/3-holvi.png b/docs/img/sponsors/3-holvi.png new file mode 100644 index 000000000..255e391e0 Binary files /dev/null and b/docs/img/sponsors/3-holvi.png differ diff --git a/docs/img/sponsors/3-imt_computer_services.png b/docs/img/sponsors/3-imt_computer_services.png new file mode 100644 index 000000000..00643c978 Binary files /dev/null and b/docs/img/sponsors/3-imt_computer_services.png differ diff --git a/docs/img/sponsors/3-infinite_code.png b/docs/img/sponsors/3-infinite_code.png new file mode 100644 index 000000000..7a8fdcf16 Binary files /dev/null and b/docs/img/sponsors/3-infinite_code.png differ diff --git a/docs/img/sponsors/3-ipushpull.png b/docs/img/sponsors/3-ipushpull.png new file mode 100644 index 000000000..e70b8bad2 Binary files /dev/null and b/docs/img/sponsors/3-ipushpull.png differ diff --git a/docs/img/sponsors/3-isl.png b/docs/img/sponsors/3-isl.png new file mode 100644 index 000000000..0bf0cf7c9 Binary files /dev/null and b/docs/img/sponsors/3-isl.png differ diff --git a/docs/img/sponsors/3-life_the_game.png b/docs/img/sponsors/3-life_the_game.png new file mode 100644 index 000000000..9292685e7 Binary files /dev/null and b/docs/img/sponsors/3-life_the_game.png differ diff --git a/docs/img/sponsors/3-makespace.png b/docs/img/sponsors/3-makespace.png new file mode 100644 index 000000000..80b793619 Binary files /dev/null and b/docs/img/sponsors/3-makespace.png differ diff --git a/docs/img/sponsors/3-nephila.png b/docs/img/sponsors/3-nephila.png new file mode 100644 index 000000000..a905fa938 Binary files /dev/null and b/docs/img/sponsors/3-nephila.png differ diff --git a/docs/img/sponsors/3-openeye.png b/docs/img/sponsors/3-openeye.png new file mode 100644 index 000000000..573140ed6 Binary files /dev/null and b/docs/img/sponsors/3-openeye.png differ diff --git a/docs/img/sponsors/3-pathwright.png b/docs/img/sponsors/3-pathwright.png new file mode 100644 index 000000000..71be3b28b Binary files /dev/null and b/docs/img/sponsors/3-pathwright.png differ diff --git a/docs/img/sponsors/3-phurba.png b/docs/img/sponsors/3-phurba.png new file mode 100644 index 000000000..657d872c9 Binary files /dev/null and b/docs/img/sponsors/3-phurba.png differ diff --git a/docs/img/sponsors/3-pkgfarm.png b/docs/img/sponsors/3-pkgfarm.png new file mode 100644 index 000000000..9224cc2ee Binary files /dev/null and b/docs/img/sponsors/3-pkgfarm.png differ diff --git a/docs/img/sponsors/3-providenz.png b/docs/img/sponsors/3-providenz.png new file mode 100644 index 000000000..55d9c992a Binary files /dev/null and b/docs/img/sponsors/3-providenz.png differ diff --git a/docs/img/sponsors/3-safari.png b/docs/img/sponsors/3-safari.png new file mode 100644 index 000000000..c03e40e84 Binary files /dev/null and b/docs/img/sponsors/3-safari.png differ diff --git a/docs/img/sponsors/3-shippo.png b/docs/img/sponsors/3-shippo.png new file mode 100644 index 000000000..4f5ae133a Binary files /dev/null and b/docs/img/sponsors/3-shippo.png differ diff --git a/docs/img/sponsors/3-teonite.png b/docs/img/sponsors/3-teonite.png new file mode 100644 index 000000000..0c0984783 Binary files /dev/null and b/docs/img/sponsors/3-teonite.png differ diff --git a/docs/img/sponsors/3-thermondo-gmbh.png b/docs/img/sponsors/3-thermondo-gmbh.png new file mode 100644 index 000000000..fe8691c8d Binary files /dev/null and b/docs/img/sponsors/3-thermondo-gmbh.png differ diff --git a/docs/img/sponsors/3-tivix.png b/docs/img/sponsors/3-tivix.png new file mode 100644 index 000000000..bc2616a62 Binary files /dev/null and b/docs/img/sponsors/3-tivix.png differ diff --git a/docs/img/sponsors/3-trackmaven.png b/docs/img/sponsors/3-trackmaven.png new file mode 100644 index 000000000..3880e3707 Binary files /dev/null and b/docs/img/sponsors/3-trackmaven.png differ diff --git a/docs/img/sponsors/3-transcode.png b/docs/img/sponsors/3-transcode.png new file mode 100644 index 000000000..1faad69d9 Binary files /dev/null and b/docs/img/sponsors/3-transcode.png differ diff --git a/docs/img/sponsors/3-triggered_messaging.png b/docs/img/sponsors/3-triggered_messaging.png new file mode 100644 index 000000000..4f8e50635 Binary files /dev/null and b/docs/img/sponsors/3-triggered_messaging.png differ diff --git a/docs/img/sponsors/3-vzzual.png b/docs/img/sponsors/3-vzzual.png new file mode 100644 index 000000000..98edce028 Binary files /dev/null and b/docs/img/sponsors/3-vzzual.png differ diff --git a/docs/img/sponsors/3-wildfish.png b/docs/img/sponsors/3-wildfish.png new file mode 100644 index 000000000..fa13ea703 Binary files /dev/null and b/docs/img/sponsors/3-wildfish.png differ diff --git a/docs/index.md b/docs/index.md index dd060ecc3..6abc4f040 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,14 +9,6 @@ --- -#### Django REST framework 3 - Kickstarter announcement! - -We are currently running a Kickstarter campaign to help fund the development of Django REST framework 3. - -If you want to help drive sustainable open-source development **please [check out the Kickstarter project](https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3) and consider funding us.** - ---- -

+
  • Eventbrite
  • + + + + +
    + +--- + +### Gold sponsors + +Our gold sponsors include companies large and small. Many thanks for their significant funding of the project and their commitment to sustainable open-source development. + + + +
    + +**Individual backers**: Xitij Ritesh Patel, Howard Sandford, Simon Haugk. + +--- + +### Silver sponsors + +The serious financial contribution that our silver sponsors have made is very much appreciated. I'd like to say a particular thank you to individuals who have choosen to privately support the project at this level. + + + +
    + +**Individual backers**: Paul Hallet, Paul Whipp, Dylan Roy, Jannis Leidel, Xavier Ordoquy, Johannes Spielmann, Rob Spectre, Chris Heisel, Marwan Alsabbagh, Haris Ali, Tuomas Toivonen. + +--- + +### Advocates + +The following individuals made a significant financial contribution to the development of Django REST framework 3, for which I can only offer a huge, warm and sincere thank you! + +**Individual backers**: Jure Cuhalev, Kevin Brolly, Ferenc Szalai, Dougal Matthews, Stefan Foulis, Carlos Hernando, Alen Mujezinovic, Ross Crawford-d'Heureuse, George Kappel, Alasdair Nicol, John Carr, Steve Winton, Trey, Manuel Miranda, David Horn, Vince Mi, Daniel Sears, Jamie Matthews, Ryan Currah, Marty Kemka, Scott Nixon, Moshin Elahi, Kevin Campbell, Jose Antonio Leiva Izquierdo, Kevin Stone, Andrew Godwin, Tijs Teulings, Roger Boardman, Xavier Antoviaque, Darian Moody, Lujeni, Jon Dugan, Wiley Kestner, Daniel C. Silverstein, Daniel Hahler, Subodh Nijsure, Philipp Weidenhiller, Yusuke Muraoka, Danny Roa, Reto Aebersold, Kyle Getrost, Décébal Hormuz, James Dacosta, Matt Long, Mauro Rocco, Tyrel Souza, Ryan Campbell, Ville Jyrkkä, Charalampos Papaloizou, Nikolai Røed Kristiansen, Antoni Aloy López, Celia Oakley, Michał Krawczak, Ivan VenOsdel, Tim Watts, Martin Warne, Nicola Jordan, Ryan Kaskel. + +**Corporate backers**: Savannah Informatics, Prism Skylabs, Musical Operating Devices. + +--- + +### Supporters + +There were also almost 300 further individuals choosing to help fund the project at other levels or choosing to give anonymously. Again, thank you, thank you, thank you! \ No newline at end of file diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 55b194576..96214f5b4 100644 --- a/docs/tutorial/1-serialization.md +++ b/docs/tutorial/1-serialization.md @@ -81,8 +81,8 @@ For the purposes of this tutorial we're going to start by creating a simple `Sni LEXERS = [item for item in get_all_lexers() if item[1]] LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS]) STYLE_CHOICES = sorted((item, item) for item in get_all_styles()) - - + + class Snippet(models.Model): created = models.DateTimeField(auto_now_add=True) title = models.CharField(max_length=100, blank=True, default='') @@ -94,7 +94,7 @@ For the purposes of this tutorial we're going to start by creating a simple `Sni style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100) - + class Meta: ordering = ('created',) @@ -122,12 +122,12 @@ The first thing we need to get started on our Web API is to provide a way of ser default='python') style = serializers.ChoiceField(choices=STYLE_CHOICES, default='friendly') - + def restore_object(self, attrs, instance=None): """ Create or update a new snippet instance, given a dictionary of deserialized field values. - + Note that if we don't define this method, then deserializing data will simply return a dictionary of items. """ @@ -180,7 +180,7 @@ At this point we've translated the model instance into Python native datatypes. content # '{"pk": 2, "title": "", "code": "print \\"hello, world\\"\\n", "linenos": false, "language": "python", "style": "friendly"}' -Deserialization is similar. First we parse a stream into Python native datatypes... +Deserialization is similar. First we parse a stream into Python native datatypes... # This import will use either `StringIO.StringIO` or `io.BytesIO` # as appropriate, depending on if we're running Python 2 or Python 3. @@ -196,7 +196,7 @@ Deserialization is similar. First we parse a stream into Python native datatype # True serializer.object # - + Notice how similar the API is to working with forms. The similarity should become even more apparent when we start writing views that use our serializer. We can also serialize querysets instead of model instances. To do so we simply add a `many=True` flag to the serializer arguments. @@ -264,7 +264,7 @@ The root of our API is going to be a view that supports listing all the existing return JSONResponse(serializer.data, status=201) return JSONResponse(serializer.errors, status=400) -Note that because we want to be able to POST to this view from clients that won't have a CSRF token we need to mark the view as `csrf_exempt`. This isn't something that you'd normally want to do, and REST framework views actually use more sensible behavior than this, but it'll do for our purposes right now. +Note that because we want to be able to POST to this view from clients that won't have a CSRF token we need to mark the view as `csrf_exempt`. This isn't something that you'd normally want to do, and REST framework views actually use more sensible behavior than this, but it'll do for our purposes right now. We'll also need a view which corresponds to an individual snippet, and can be used to retrieve, update or delete the snippet. @@ -277,11 +277,11 @@ We'll also need a view which corresponds to an individual snippet, and can be us snippet = Snippet.objects.get(pk=pk) except Snippet.DoesNotExist: return HttpResponse(status=404) - + if request.method == 'GET': serializer = SnippetSerializer(snippet) return JSONResponse(serializer.data) - + elif request.method == 'PUT': data = JSONParser().parse(request) serializer = SnippetSerializer(snippet, data=data) diff --git a/docs/tutorial/2-requests-and-responses.md b/docs/tutorial/2-requests-and-responses.md index 603edd081..e70bbbfc4 100644 --- a/docs/tutorial/2-requests-and-responses.md +++ b/docs/tutorial/2-requests-and-responses.md @@ -33,7 +33,7 @@ The wrappers also provide behaviour such as returning `405 Method Not Allowed` r ## Pulling it all together -Okay, let's go ahead and start using these new components to write a few views. +Okay, let's go ahead and start using these new components to write a few views. We don't need our `JSONResponse` class in `views.py` anymore, so go ahead and delete that. Once that's done we can start refactoring our views slightly. @@ -69,7 +69,7 @@ Here is the view for an individual snippet, in the `views.py` module. def snippet_detail(request, pk): """ Retrieve, update or delete a snippet instance. - """ + """ try: snippet = Snippet.objects.get(pk=pk) except Snippet.DoesNotExist: @@ -115,7 +115,7 @@ Now update the `urls.py` file slightly, to append a set of `format_suffix_patter url(r'^snippets/$', 'snippet_list'), url(r'^snippets/(?P[0-9]+)$', 'snippet_detail'), ) - + urlpatterns = format_suffix_patterns(urlpatterns) We don't necessarily need to add these extra url patterns in, but it gives us a simple, clean way of referring to a specific format. @@ -146,7 +146,7 @@ Similarly, we can control the format of the request that we send, using the `Con curl -X POST http://127.0.0.1:8000/snippets/ -d "code=print 123" {"id": 3, "title": "", "code": "print 123", "linenos": false, "language": "python", "style": "friendly"} - + # POST using JSON curl -X POST http://127.0.0.1:8000/snippets/ -d '{"code": "print 456"}' -H "Content-Type: application/json" diff --git a/docs/tutorial/3-class-based-views.md b/docs/tutorial/3-class-based-views.md index b37bc31bd..e04072ca5 100644 --- a/docs/tutorial/3-class-based-views.md +++ b/docs/tutorial/3-class-based-views.md @@ -30,7 +30,7 @@ We'll start by rewriting the root view as a class based view. All this involves return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -So far, so good. It looks pretty similar to the previous case, but we've got better separation between the different HTTP methods. We'll also need to update the instance view in `views.py`. +So far, so good. It looks pretty similar to the previous case, but we've got better separation between the different HTTP methods. We'll also need to update the instance view in `views.py`. class SnippetDetail(APIView): """ @@ -72,7 +72,7 @@ We'll also need to refactor our `urls.py` slightly now we're using class based v url(r'^snippets/$', views.SnippetList.as_view()), url(r'^snippets/(?P[0-9]+)/$', views.SnippetDetail.as_view()), ) - + urlpatterns = format_suffix_patterns(urlpatterns) Okay, we're done. If you run the development server everything should be working just as before. diff --git a/docs/tutorial/4-authentication-and-permissions.md b/docs/tutorial/4-authentication-and-permissions.md index 491df1608..74ad9a551 100644 --- a/docs/tutorial/4-authentication-and-permissions.md +++ b/docs/tutorial/4-authentication-and-permissions.md @@ -73,12 +73,12 @@ We'll also add a couple of views to `views.py`. We'd like to just use read-only class UserList(generics.ListAPIView): queryset = User.objects.all() serializer_class = UserSerializer - - + + class UserDetail(generics.RetrieveAPIView): queryset = User.objects.all() serializer_class = UserSerializer - + Make sure to also import the `UserSerializer` class from snippets.serializers import UserSerializer @@ -129,7 +129,7 @@ Then, add the following property to **both** the `SnippetList` and `SnippetDetai If you open a browser and navigate to the browsable API at the moment, you'll find that you're no longer able to create new code snippets. In order to do so we'd need to be able to login as a user. -We can add a login view for use with the browsable API, by editing the URLconf in our project-level urls.py file. +We can add a login view for use with the browsable API, by editing the URLconf in our project-level `urls.py` file. Add the following import at the top of the file: @@ -157,8 +157,8 @@ To do that we're going to need to create a custom permission. In the snippets app, create a new file, `permissions.py` from rest_framework import permissions - - + + class IsOwnerOrReadOnly(permissions.BasePermission): """ Custom permission to only allow owners of an object to edit it. @@ -201,7 +201,7 @@ If we try to create a snippet without authenticating, we'll get an error: We can make a successful request by including the username and password of one of the users we created earlier. curl -X POST http://127.0.0.1:8000/snippets/ -d "code=print 789" -u tom:password - + {"id": 5, "owner": "tom", "title": "foo", "code": "print 789", "linenos": false, "language": "python", "style": "friendly"} ## Summary diff --git a/docs/tutorial/5-relationships-and-hyperlinked-apis.md b/docs/tutorial/5-relationships-and-hyperlinked-apis.md index 2cf44bf99..9c61fe3d3 100644 --- a/docs/tutorial/5-relationships-and-hyperlinked-apis.md +++ b/docs/tutorial/5-relationships-and-hyperlinked-apis.md @@ -1,10 +1,10 @@ # Tutorial 5: Relationships & Hyperlinked APIs -At the moment relationships within our API are represented by using primary keys. In this part of the tutorial we'll improve the cohesion and discoverability of our API, by instead using hyperlinking for relationships. +At the moment relationships within our API are represented by using primary keys. In this part of the tutorial we'll improve the cohesion and discoverability of our API, by instead using hyperlinking for relationships. ## Creating an endpoint for the root of our API -Right now we have endpoints for 'snippets' and 'users', but we don't have a single entry point to our API. To create one, we'll use a regular function-based view and the `@api_view` decorator we introduced earlier. +Right now we have endpoints for 'snippets' and 'users', but we don't have a single entry point to our API. To create one, we'll use a regular function-based view and the `@api_view` decorator we introduced earlier. In your `snippets/views.py` add: from rest_framework import renderers from rest_framework.decorators import api_view @@ -29,7 +29,7 @@ Unlike all our other API endpoints, we don't want to use JSON, but instead just The other thing we need to consider when creating the code highlight view is that there's no existing concrete generic view that we can use. We're not returning an object instance, but instead a property of an object instance. -Instead of using a concrete generic view, we'll use the base class for representing instances, and create our own `.get()` method. In your snippets.views add: +Instead of using a concrete generic view, we'll use the base class for representing instances, and create our own `.get()` method. In your `snippets/views.py` add: from rest_framework import renderers from rest_framework.response import Response @@ -37,13 +37,13 @@ Instead of using a concrete generic view, we'll use the base class for represent class SnippetHighlight(generics.GenericAPIView): queryset = Snippet.objects.all() renderer_classes = (renderers.StaticHTMLRenderer,) - + def get(self, request, *args, **kwargs): snippet = self.get_object() return Response(snippet.highlighted) As usual we need to add the new views that we've created in to our URLconf. -We'll add a url pattern for our new API root: +We'll add a url pattern for our new API root in `snippets/urls.py`: url(r'^$', 'api_root'), @@ -73,21 +73,21 @@ The `HyperlinkedModelSerializer` has the following differences from `ModelSerial * Relationships use `HyperlinkedRelatedField`, instead of `PrimaryKeyRelatedField`. -We can easily re-write our existing serializers to use hyperlinking. +We can easily re-write our existing serializers to use hyperlinking. In your `snippets/serializers.py` add: class SnippetSerializer(serializers.HyperlinkedModelSerializer): owner = serializers.Field(source='owner.username') highlight = serializers.HyperlinkedIdentityField(view_name='snippet-highlight', format='html') - + class Meta: model = Snippet fields = ('url', 'highlight', 'owner', 'title', 'code', 'linenos', 'language', 'style') - - + + class UserSerializer(serializers.HyperlinkedModelSerializer): snippets = serializers.HyperlinkedRelatedField(many=True, view_name='snippet-detail') - + class Meta: model = User fields = ('url', 'username', 'snippets') @@ -105,7 +105,7 @@ If we're going to have a hyperlinked API, we need to make sure we name our URL p * Our user serializer includes a field that refers to `'snippet-detail'`. * Our snippet and user serializers include `'url'` fields that by default will refer to `'{model_name}-detail'`, which in this case will be `'snippet-detail'` and `'user-detail'`. -After adding all those names into our URLconf, our final `'urls.py'` file should look something like this: +After adding all those names into our URLconf, our final `snippets/urls.py` file should look something like this: # API endpoints urlpatterns = format_suffix_patterns(patterns('snippets.views', @@ -126,9 +126,9 @@ After adding all those names into our URLconf, our final `'urls.py'` file should views.UserDetail.as_view(), name='user-detail') )) - + # Login and logout views for the browsable API - urlpatterns += patterns('', + urlpatterns += patterns('', url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), ) diff --git a/docs/tutorial/quickstart.md b/docs/tutorial/quickstart.md index 8bf8c7f5c..98e5f439f 100644 --- a/docs/tutorial/quickstart.md +++ b/docs/tutorial/quickstart.md @@ -6,8 +6,8 @@ We're going to create a simple API to allow admin users to view and edit the use Create a new Django project named `tutorial`, then start a new app called `quickstart`. - # Set up a new project - django-admin.py startproject tutorial + # Create the project directory + mkdir tutorial cd tutorial # Create a virtualenv to isolate our package dependencies locally @@ -18,6 +18,9 @@ Create a new Django project named `tutorial`, then start a new app called `quick pip install django pip install djangorestframework + # Set up a new project + django-admin.py startproject tutorial + # Create a new app python manage.py startapp quickstart @@ -46,14 +49,14 @@ First up we're going to define some serializers in `quickstart/serializers.py` t from django.contrib.auth.models import User, Group from rest_framework import serializers - - + + class UserSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = User fields = ('url', 'username', 'email', 'groups') - - + + class GroupSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Group @@ -68,16 +71,16 @@ Right, we'd better write some views then. Open `quickstart/views.py` and get ty from django.contrib.auth.models import User, Group from rest_framework import viewsets from quickstart.serializers import UserSerializer, GroupSerializer - - + + class UserViewSet(viewsets.ModelViewSet): """ API endpoint that allows users to be viewed or edited. """ queryset = User.objects.all() serializer_class = UserSerializer - - + + class GroupViewSet(viewsets.ModelViewSet): """ API endpoint that allows groups to be viewed or edited. @@ -144,22 +147,22 @@ We're now ready to test the API we've built. Let's fire up the server from the We can now access our API, both from the command-line, using tools like `curl`... - bash: curl -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/ + bash: curl -H 'Accept: application/json; indent=4' -u admin:password http://127.0.0.1:8000/users/ { - "count": 2, - "next": null, - "previous": null, + "count": 2, + "next": null, + "previous": null, "results": [ { - "email": "admin@example.com", - "groups": [], - "url": "http://127.0.0.1:8000/users/1/", + "email": "admin@example.com", + "groups": [], + "url": "http://127.0.0.1:8000/users/1/", "username": "admin" - }, + }, { - "email": "tom@example.com", - "groups": [ ], - "url": "http://127.0.0.1:8000/users/2/", + "email": "tom@example.com", + "groups": [ ], + "url": "http://127.0.0.1:8000/users/2/", "username": "tom" } ] diff --git a/mkdocs.py b/mkdocs.py index 529d2314c..adeb60538 100755 --- a/mkdocs.py +++ b/mkdocs.py @@ -142,7 +142,7 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir): toc += template + '\n' if filename == 'index.md': - main_title = 'Django REST framework - APIs made easy' + main_title = 'Django REST framework - Web APIs for Django' else: main_title = main_title + ' - Django REST framework' diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index cbc83574a..82cea70fc 100644 --- a/rest_framework/authentication.py +++ b/rest_framework/authentication.py @@ -310,6 +310,13 @@ class OAuth2Authentication(BaseAuthentication): auth = get_authorization_header(request).split() + if len(auth) == 1: + msg = 'Invalid bearer header. No credentials provided.' + raise exceptions.AuthenticationFailed(msg) + elif len(auth) > 2: + msg = 'Invalid bearer header. Token string should not contain spaces.' + raise exceptions.AuthenticationFailed(msg) + if auth and auth[0].lower() == b'bearer': access_token = auth[1] elif 'access_token' in request.POST: @@ -319,13 +326,6 @@ class OAuth2Authentication(BaseAuthentication): else: return None - if len(auth) == 1: - msg = 'Invalid bearer header. No credentials provided.' - raise exceptions.AuthenticationFailed(msg) - elif len(auth) > 2: - msg = 'Invalid bearer header. Token string should not contain spaces.' - raise exceptions.AuthenticationFailed(msg) - return self.authenticate_credentials(request, access_token) def authenticate_credentials(self, request, access_token): diff --git a/rest_framework/compat.py b/rest_framework/compat.py index 6a5cbbe4f..4b16a8ca2 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -44,12 +44,15 @@ except ImportError: django_filters = None -# django-guardian is optional -try: - import guardian - import guardian.shortcuts # Fixes #1624 -except ImportError: - guardian = None +# Django-guardian is optional. Import only if guardian is in INSTALLED_APPS +# Fixes (#1712). We keep the try/except for the test suite. +guardian = None +if 'guardian' in settings.INSTALLED_APPS: + try: + import guardian + import guardian.shortcuts # Fixes #1624 + except ImportError: + pass # cStringIO only if it's available, otherwise StringIO diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 96d15eb9d..c3b846aed 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -116,6 +116,10 @@ class OrderingFilter(BaseFilterBackend): def get_ordering(self, request): """ Ordering is set by a comma delimited ?ordering=... query parameter. + + The `ordering` query parameter can be overridden by setting + the `ordering_param` value on the OrderingFilter or by + specifying an `ORDERING_PARAM` value in the API settings. """ params = request.QUERY_PARAMS.get(self.ordering_param) if params: diff --git a/rest_framework/generics.py b/rest_framework/generics.py index e38c52b15..cecb548fb 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -43,6 +43,10 @@ class GenericAPIView(views.APIView): # You'll need to either set these attributes, # or override `get_queryset()`/`get_serializer_class()`. + # If you are overriding a view method, it is important that you call + # `get_queryset()` instead of accessing the `queryset` property directly, + # as `queryset` will get evaluated only once, and those results are cached + # for all subsequent requests. queryset = None serializer_class = None @@ -185,7 +189,13 @@ class GenericAPIView(views.APIView): """ Returns the list of filter backends that this view requires. """ - filter_backends = self.filter_backends or [] + if self.filter_backends is None: + filter_backends = [] + else: + # Note that we are returning a *copy* of the class attribute, + # so that it is safe for the view to mutate it if needed. + filter_backends = list(self.filter_backends) + if not filter_backends and self.filter_backend: warnings.warn( 'The `filter_backend` attribute and `FILTER_BACKEND` setting ' @@ -195,6 +205,7 @@ class GenericAPIView(views.APIView): DeprecationWarning, stacklevel=2 ) filter_backends = [self.filter_backend] + return filter_backends @@ -258,6 +269,10 @@ class GenericAPIView(views.APIView): This must be an iterable, and may be a queryset. Defaults to using `self.queryset`. + 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. + You may want to override this if you need to provide different querysets depending on the incoming request. diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 484961add..7048d87de 100644 --- a/rest_framework/renderers.py +++ b/rest_framework/renderers.py @@ -54,32 +54,37 @@ class JSONRenderer(BaseRenderer): format = 'json' encoder_class = encoders.JSONEncoder ensure_ascii = True - charset = None - # JSON is a binary encoding, that can be encoded as utf-8, utf-16 or utf-32. + + # We don't set a charset because JSON is a binary encoding, + # that can be encoded as utf-8, utf-16 or utf-32. # See: http://www.ietf.org/rfc/rfc4627.txt # Also: http://lucumr.pocoo.org/2013/7/19/application-mimetypes-and-encodings/ + charset = None - def render(self, data, accepted_media_type=None, renderer_context=None): - """ - Render `data` into JSON. - """ - if data is None: - return bytes() - - # If 'indent' is provided in the context, then pretty print the result. - # E.g. If we're being called by the BrowsableAPIRenderer. - renderer_context = renderer_context or {} - indent = renderer_context.get('indent', None) - + def get_indent(self, accepted_media_type, renderer_context): if accepted_media_type: # If the media type looks like 'application/json; indent=4', # then pretty print the result. base_media_type, params = parse_header(accepted_media_type.encode('ascii')) - indent = params.get('indent', indent) try: - indent = max(min(int(indent), 8), 0) - except (ValueError, TypeError): - indent = None + return max(min(int(params['indent']), 8), 0) + except (KeyError, ValueError, TypeError): + pass + + # If 'indent' is provided in the context, then pretty print the result. + # E.g. If we're being called by the BrowsableAPIRenderer. + return renderer_context.get('indent', None) + + + def render(self, data, accepted_media_type=None, renderer_context=None): + """ + Render `data` into JSON, returning a bytestring. + """ + if data is None: + return bytes() + + renderer_context = renderer_context or {} + indent = self.get_indent(accepted_media_type, renderer_context) ret = json.dumps(data, cls=self.encoder_class, indent=indent, ensure_ascii=self.ensure_ascii) diff --git a/rest_framework/request.py b/rest_framework/request.py index 40467c03d..d508f9b43 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -42,13 +42,20 @@ class override_method(object): self.view = view self.request = request self.method = method + self.action = getattr(view, 'action', None) def __enter__(self): self.view.request = clone_request(self.request, self.method) + if self.action is not None: + # For viewsets we also set the `.action` attribute. + action_map = getattr(self.view, 'action_map', {}) + self.view.action = action_map.get(self.method.lower()) return self.view.request def __exit__(self, *args, **kwarg): self.view.request = self.request + if self.action is not None: + self.view.action = self.action class Empty(object): @@ -280,8 +287,8 @@ class Request(object): self._method = self._request.method # Allow X-HTTP-METHOD-OVERRIDE header - self._method = self.META.get('HTTP_X_HTTP_METHOD_OVERRIDE', - self._method) + if 'HTTP_X_HTTP_METHOD_OVERRIDE' in self.META: + self._method = self.META['HTTP_X_HTTP_METHOD_OVERRIDE'].upper() def _load_stream(self): """ diff --git a/rest_framework/response.py b/rest_framework/response.py index 1dc6abcf6..25b785245 100644 --- a/rest_framework/response.py +++ b/rest_framework/response.py @@ -5,6 +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 +import django from django.core.handlers.wsgi import STATUS_CODE_TEXT from django.template.response import SimpleTemplateResponse from rest_framework.compat import six @@ -15,8 +16,11 @@ class Response(SimpleTemplateResponse): An HttpResponse that allows its data to be rendered into arbitrary media types. """ + # TODO: remove that once Django 1.3 isn't supported + if django.VERSION >= (1, 4): + rendering_attrs = SimpleTemplateResponse.rendering_attrs + ['_closable_objects'] - def __init__(self, data=None, status=200, + def __init__(self, data=None, status=None, template_name=None, headers=None, exception=False, content_type=None): """ diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index a15b8f225..2fdc9b9da 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -976,7 +976,7 @@ class ModelSerializer(Serializer): try: setattr(instance, key, val) except ValueError: - self._errors[key] = self.error_messages['required'] + self._errors[key] = [self.error_messages['required']] # Any relations that cannot be set until we've # saved the model get hidden away on these diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index 210741ed6..b6e9ca5ca 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -25,6 +25,7 @@ {% endblock %} + {% block body %}
    @@ -94,7 +95,7 @@ {% endif %} {% if options_form %} -
    + {% csrf_token %} @@ -102,7 +103,7 @@ {% endif %} {% if delete_form %} - + {% csrf_token %} @@ -231,4 +232,5 @@ {% endblock %} + {% endblock %} diff --git a/rest_framework/templates/rest_framework/login_base.html b/rest_framework/templates/rest_framework/login_base.html index be83c2f53..43860e53e 100644 --- a/rest_framework/templates/rest_framework/login_base.html +++ b/rest_framework/templates/rest_framework/login_base.html @@ -1,18 +1,9 @@ +{% extends "rest_framework/base.html" %} {% load url from future %} {% load staticfiles %} {% load rest_framework %} - - - - {% block style %} - {% block bootstrap_theme %} - - - {% endblock %} - - {% endblock %} - + {% block body %}
    @@ -51,4 +42,4 @@
    - + {% endblock %} diff --git a/rest_framework/test.py b/rest_framework/test.py index 284bcee07..d4ec50a06 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -154,6 +154,10 @@ class APIClient(APIRequestFactory, DjangoClient): kwargs.update(self._credentials) return super(APIClient, self).request(**kwargs) + def logout(self): + self._credentials = {} + return super(APIClient, self).logout() + class APITransactionTestCase(testcases.TransactionTestCase): client_class = APIClient diff --git a/rest_framework/urls.py b/rest_framework/urls.py index 87ec0f0ae..eed4bd140 100644 --- a/rest_framework/urls.py +++ b/rest_framework/urls.py @@ -2,15 +2,15 @@ Login and logout views for the browsable API. Add these to your root URLconf if you're using the browsable API and -your API requires authentication. - -The urls must be namespaced as 'rest_framework', and you should make sure -your authentication settings include `SessionAuthentication`. +your API requires authentication: urlpatterns = patterns('', ... url(r'^auth', include('rest_framework.urls', namespace='rest_framework')) ) + +The urls must be namespaced as 'rest_framework', and you should make sure +your authentication settings include `SessionAuthentication`. """ from __future__ import unicode_literals from django.conf.urls import patterns, url diff --git a/tests/test_authentication.py b/tests/test_authentication.py index 5b97d60bb..f5bfc5e61 100644 --- a/tests/test_authentication.py +++ b/tests/test_authentication.py @@ -550,6 +550,15 @@ class OAuth2Tests(TestCase): response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth) self.assertEqual(response.status_code, 401) + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') + def test_get_form_with_wrong_authorization_header_token_missing(self): + """Ensure that a missing token lead to the correct HTTP error status code""" + auth = "Bearer" + response = self.csrf_client.get('/oauth2-test/', {}, HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 401) + response = self.csrf_client.get('/oauth2-test/', HTTP_AUTHORIZATION=auth) + self.assertEqual(response.status_code, 401) + @unittest.skipUnless(oauth2_provider, 'django-oauth2-provider not installed') def test_get_form_passing_auth(self): """Ensure GETing form over OAuth with correct client credentials succeed""" diff --git a/tests/test_fields.py b/tests/test_fields.py index 73b156410..97ef016fa 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1002,3 +1002,21 @@ class BooleanField(TestCase): bool_field = serializers.BooleanField(required=True) self.assertFalse(BooleanRequiredSerializer(data={}).is_valid()) + + +class SerializerMethodFieldTest(TestCase): + """ + Tests for the SerializerMethodField field_to_native() behavior + """ + class SerializerTest(serializers.Serializer): + def get_my_test(self, obj): + return obj.my_test[0:5] + + class Example(): + my_test = 'Hey, this is a test !' + + def test_field_to_native(self): + s = serializers.SerializerMethodField('get_my_test') + s.initialize(self.SerializerTest(), 'name') + result = s.field_to_native(self.Example(), None) + self.assertEqual(result, 'Hey, ') diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 6a1a3521d..7d57fcf01 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -686,7 +686,7 @@ class ModelValidationTests(TestCase): photo_serializer = PhotoSerializer(instance=photo, data={'album': ''}, partial=True) self.assertFalse(photo_serializer.is_valid()) self.assertTrue('album' in photo_serializer.errors) - self.assertEqual(photo_serializer.errors['album'], photo_serializer.error_messages['required']) + self.assertEqual(photo_serializer.errors['album'], [photo_serializer.error_messages['required']]) def test_foreign_key_with_partial(self): """ diff --git a/tests/test_testing.py b/tests/test_testing.py index e2e4e2173..1b126e002 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -99,6 +99,17 @@ class TestAPITestClient(TestCase): self.assertEqual(response.status_code, 403) self.assertEqual(response.data, expected) + def test_can_logout(self): + """ + `logout()` reset stored credentials + """ + self.client.credentials(HTTP_AUTHORIZATION='example') + response = self.client.get('/view/') + self.assertEqual(response.data['auth'], 'example') + self.client.logout() + response = self.client.get('/view/') + self.assertEqual(response.data['auth'], b'') + class TestAPIRequestFactory(TestCase): def test_csrf_exempt_by_default(self): diff --git a/tox.ini b/tox.ini index 191923086..484053a69 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ commands = py.test -q [testenv:py3.4-django1.7] basepython = python3.4 -deps = https://www.djangoproject.com/download/1.7b2/tarball/ +deps = https://www.djangoproject.com/download/1.7c2/tarball/ django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 @@ -19,7 +19,7 @@ deps = https://www.djangoproject.com/download/1.7b2/tarball/ [testenv:py3.3-django1.7] basepython = python3.3 -deps = https://www.djangoproject.com/download/1.7b2/tarball/ +deps = https://www.djangoproject.com/download/1.7c2/tarball/ django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 @@ -27,7 +27,7 @@ deps = https://www.djangoproject.com/download/1.7b2/tarball/ [testenv:py3.2-django1.7] basepython = python3.2 -deps = https://www.djangoproject.com/download/1.7b2/tarball/ +deps = https://www.djangoproject.com/download/1.7c2/tarball/ django-filter==0.7 defusedxml==0.3 Pillow==2.3.0 @@ -35,13 +35,13 @@ deps = https://www.djangoproject.com/download/1.7b2/tarball/ [testenv:py2.7-django1.7] basepython = python2.7 -deps = https://www.djangoproject.com/download/1.7b2/tarball/ +deps = https://www.djangoproject.com/download/1.7c2/tarball/ django-filter==0.7 defusedxml==0.3 - django-oauth-plus==2.2.1 - oauth2==1.5.211 - django-oauth2-provider==0.2.4 - django-guardian==1.1.1 + # django-oauth-plus==2.2.1 + # oauth2==1.5.211 + # django-oauth2-provider==0.2.4 + django-guardian==1.2.3 Pillow==2.3.0 pytest-django==2.6.1 @@ -77,7 +77,7 @@ deps = Django==1.6.3 django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.4 - django-guardian==1.1.1 + django-guardian==1.2.3 Pillow==2.3.0 pytest-django==2.6.1 @@ -89,7 +89,7 @@ deps = Django==1.6.3 django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.4 - django-guardian==1.1.1 + django-guardian==1.2.3 Pillow==2.3.0 pytest-django==2.6.1 @@ -125,7 +125,7 @@ deps = django==1.5.6 django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.3 - django-guardian==1.1.1 + django-guardian==1.2.3 Pillow==2.3.0 pytest-django==2.6.1 @@ -137,7 +137,7 @@ deps = django==1.5.6 django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.3 - django-guardian==1.1.1 + django-guardian==1.2.3 Pillow==2.3.0 pytest-django==2.6.1 @@ -149,7 +149,7 @@ deps = django==1.4.11 django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.3 - django-guardian==1.1.1 + django-guardian==1.2.3 Pillow==2.3.0 pytest-django==2.6.1 @@ -161,6 +161,6 @@ deps = django==1.4.11 django-oauth-plus==2.2.1 oauth2==1.5.211 django-oauth2-provider==0.2.3 - django-guardian==1.1.1 + django-guardian==1.2.3 Pillow==2.3.0 pytest-django==2.6.1