diff options
112 files changed, 562 insertions, 189 deletions
diff --git a/.travis.yml b/.travis.yml index 0c9b4455..7f1fda83 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,23 +8,21 @@ python: - "3.4" env: - - DJANGO="https://www.djangoproject.com/download/1.7b2/tarball/" - - DJANGO="django==1.6.3" - - DJANGO="django==1.5.6" - - DJANGO="django==1.4.11" + - DJANGO="https://www.djangoproject.com/download/1.7c2/tarball/" + - DJANGO="django==1.6.5" + - DJANGO="django==1.5.8" + - DJANGO="django==1.4.13" - DJANGO="django==1.3.7" install: - pip install $DJANGO - - pip install defusedxml==0.3 Pillow==2.3.0 + - pip install defusedxml==0.3 Pillow==2.3.0 django-guardian==1.2.3 - "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.7b2/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,16 +31,16 @@ script: matrix: exclude: - python: "2.6" - env: DJANGO="https://www.djangoproject.com/download/1.7b2/tarball/" + env: DJANGO="https://www.djangoproject.com/download/1.7c2/tarball/" - python: "3.2" - env: DJANGO="django==1.4.11" + env: DJANGO="django==1.4.13" - python: "3.2" env: DJANGO="django==1.3.7" - python: "3.3" - env: DJANGO="django==1.4.11" + env: DJANGO="django==1.4.13" - python: "3.3" env: DJANGO="django==1.3.7" - python: "3.4" - env: DJANGO="django==1.4.11" + env: DJANGO="django==1.4.13" - python: "3.4" env: DJANGO="django==1.3.7" @@ -136,6 +136,7 @@ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + [build-status-image]: https://secure.travis-ci.org/tomchristie/django-rest-framework.png?branch=master [travis]: http://travis-ci.org/tomchristie/django-rest-framework?branch=master [twitter]: https://twitter.com/_tomchristie diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 88a7a011..1cb37d67 100755 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -119,7 +119,7 @@ Unauthenticated responses that are denied permission will result in an `HTTP 401 This authentication scheme uses a simple token-based HTTP Authentication scheme. Token authentication is appropriate for client-server setups, such as native desktop and mobile clients. -To use the `TokenAuthentication` scheme, include `rest_framework.authtoken` in your `INSTALLED_APPS` setting: +To use the `TokenAuthentication` scheme you'll need to [configure the authentication classes](#setting-the-authentication-scheme) to include `TokenAuthentication`, and additionally include `rest_framework.authtoken` in your `INSTALLED_APPS` setting: INSTALLED_APPS = ( ... diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index 67fa65d2..b41e0ebc 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` @@ -184,7 +184,9 @@ Corresponds to `django.db.models.fields.SlugField`. ## ChoiceField -A field that can accept a value out of a limited set of choices. +A field that can accept a value out of a limited set of choices. Optionally takes a `blank_display_value` parameter that customizes the display value of an empty choice. + +**Signature:** `ChoiceField(choices=(), blank_display_value=None)` ## EmailField @@ -345,7 +347,7 @@ As an example, let's create a field that can be used represent the class name of """ Serialize the object's class name. """ - return obj.__class__ + return obj.__class__.__name__ # Third party packages @@ -355,9 +357,21 @@ The following third party packages are also available. The [drf-compound-fields][drf-compound-fields] package provides "compound" serializer fields, such as lists of simple values, which can be described by other fields rather than serializers with the `many=True` option. Also provided are fields for typed dictionaries and values that can be either a specific type or a list of items of that type. +## DRF Extra Fields + +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/filtering.md b/docs/api-guide/filtering.md index 6a8a267b..ec5ab61f 100644 --- a/docs/api-guide/filtering.md +++ b/docs/api-guide/filtering.md @@ -199,8 +199,7 @@ This enables us to make queries like: http://example.com/api/products?manufacturer__name=foo -This is nice, but it shows underlying model structure in REST API, which may -be undesired, but you can use: +This is nice, but it exposes the Django's double underscore convention as part of the API. If you instead want to explicitly name the filter argument you can instead explicitly include it on the `FilterSet` class: import django_filters from myapp.models import Product @@ -208,7 +207,6 @@ be undesired, but you can use: from rest_framework import generics class ProductFilter(django_filters.FilterSet): - manufacturer = django_filters.CharFilter(name="manufacturer__name") class Meta: diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index 43c5782f..e9efe709 100755 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -195,7 +195,7 @@ Remember that the `pre_save()` method is not called by `GenericAPIView` itself, You won't typically need to override the following methods, although you might need to call into them if you're writing custom views using `GenericAPIView`. * `get_serializer_context(self)` - Returns a dictionary containing any extra context that should be supplied to the serializer. Defaults to including `'request'`, `'view'` and `'format'` keys. -* `get_serializer(self, instance=None, data=None, files=None, many=False, partial=False)` - Returns a serializer instance. +* `get_serializer(self, instance=None, data=None, files=None, many=False, partial=False, allow_add_remove=False)` - Returns a serializer instance. * `get_pagination_serializer(self, page)` - Returns a serializer instance to use with paginated data. * `paginate_queryset(self, queryset)` - Paginate a queryset if required, either returning a page object, or `None` if pagination is not configured for this view. * `filter_queryset(self, queryset)` - Given a queryset, filter it with whichever filter backends are in use, returning a new queryset. diff --git a/docs/api-guide/permissions.md b/docs/api-guide/permissions.md index 50f669a2..38ae3d0a 100644 --- a/docs/api-guide/permissions.md +++ b/docs/api-guide/permissions.md @@ -36,6 +36,12 @@ For example: self.check_object_permissions(self.request, obj) return obj +#### Limitations of object level permissions + +For performance reasons the generic views will not automatically apply object level permissions to each instance in a queryset when returning a list of objects. + +Often when you're using object level permissions you'll also want to [filter the queryset][filtering] appropriately, to ensure that users only have visibility onto instances that they are permitted to view. + ## Setting the permission policy The default permission policy may be set globally, using the `DEFAULT_PERMISSION_CLASSES` setting. For example. @@ -237,7 +243,8 @@ The [REST Condition][rest-condition] package is another extension for building c [cite]: https://developer.apple.com/library/mac/#documentation/security/Conceptual/AuthenticationAndAuthorizationGuide/Authorization/Authorization.html [authentication]: authentication.md [throttling]: throttling.md -[contribauth]: https://docs.djangoproject.com/en/1.0/topics/auth/#permissions +[filtering]: filtering.md +[contribauth]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#custom-permissions [objectpermissions]: https://docs.djangoproject.com/en/dev/topics/auth/customizing/#handling-object-permissions [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/routers.md b/docs/api-guide/routers.md index 7efc140a..64f05af3 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -179,7 +179,16 @@ The [wq.db package][wq.db] provides an advanced [Router][wq.db-router] class (an app.router.register_model(MyModel) +## DRF-extensions + +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 [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 +[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/ +[drf-extensions-routers]: http://chibisov.github.io/drf-extensions/docs/#routers +[drf-extensions-nested-viewsets]: http://chibisov.github.io/drf-extensions/docs/#nested-routes +[drf-extensions-collection-level-controllers]: http://chibisov.github.io/drf-extensions/docs/#collection-level-controllers +[drf-extensions-customizable-endpoint-names]: http://chibisov.github.io/drf-extensions/docs/#controller-endpoint-name
\ No newline at end of file diff --git a/docs/api-guide/serializers.md b/docs/api-guide/serializers.md index 0044f070..29b7851b 100644 --- a/docs/api-guide/serializers.md +++ b/docs/api-guide/serializers.md @@ -73,8 +73,8 @@ Sometimes when serializing objects, you may not want to represent everything exa If you need to customize the serialized value of a particular field, you can do this by creating a `transform_<fieldname>` method. For example if you needed to render some markdown from a text field: - description = serializers.TextField() - description_html = serializers.TextField(source='description', read_only=True) + description = serializers.CharField() + description_html = serializers.CharField(source='description', read_only=True) def transform_description_html(self, obj, value): from django.contrib.markup.templatetags.markup import markdown @@ -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 b7c320f0..92f4c22b 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 4f345abb..aa2ceb7f 100644 --- a/docs/api-guide/viewsets.md +++ b/docs/api-guide/viewsets.md @@ -137,7 +137,7 @@ The `@action` and `@link` decorators can additionally take extra arguments that def set_password(self, request, pk=None): ... -The `@action` decorator will route `POST` requests by default, but may also accept other HTTP methods, by using the `method` argument. For example: +The `@action` decorator will route `POST` requests by default, but may also accept other HTTP methods, by using the `methods` argument. For example: @action(methods=['POST', 'DELETE']) def unset_password(self, request, pk=None): diff --git a/docs/css/default.css b/docs/css/default.css index af6a9cc0..7f3acfed 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 Binary files differnew file mode 100644 index 00000000..6c739293 --- /dev/null +++ b/docs/img/sponsors/0-eventbrite.png diff --git a/docs/img/sponsors/1-cyan.png b/docs/img/sponsors/1-cyan.png Binary files differnew file mode 100644 index 00000000..d6b55b4c --- /dev/null +++ b/docs/img/sponsors/1-cyan.png diff --git a/docs/img/sponsors/1-divio.png b/docs/img/sponsors/1-divio.png Binary files differnew file mode 100644 index 00000000..8ced88f8 --- /dev/null +++ b/docs/img/sponsors/1-divio.png diff --git a/docs/img/sponsors/1-kuwaitnet.png b/docs/img/sponsors/1-kuwaitnet.png Binary files differnew file mode 100644 index 00000000..8b2d0550 --- /dev/null +++ b/docs/img/sponsors/1-kuwaitnet.png diff --git a/docs/img/sponsors/1-lulu.png b/docs/img/sponsors/1-lulu.png Binary files differnew file mode 100644 index 00000000..8a28bfa9 --- /dev/null +++ b/docs/img/sponsors/1-lulu.png diff --git a/docs/img/sponsors/1-potato.png b/docs/img/sponsors/1-potato.png Binary files differnew file mode 100644 index 00000000..ad38abdd --- /dev/null +++ b/docs/img/sponsors/1-potato.png diff --git a/docs/img/sponsors/1-purplebit.png b/docs/img/sponsors/1-purplebit.png Binary files differnew file mode 100644 index 00000000..0df63bf6 --- /dev/null +++ b/docs/img/sponsors/1-purplebit.png diff --git a/docs/img/sponsors/1-runscope.png b/docs/img/sponsors/1-runscope.png Binary files differnew file mode 100644 index 00000000..d80a4b85 --- /dev/null +++ b/docs/img/sponsors/1-runscope.png diff --git a/docs/img/sponsors/1-simple-energy.png b/docs/img/sponsors/1-simple-energy.png Binary files differnew file mode 100644 index 00000000..f59f7374 --- /dev/null +++ b/docs/img/sponsors/1-simple-energy.png diff --git a/docs/img/sponsors/1-vokal_interactive.png b/docs/img/sponsors/1-vokal_interactive.png Binary files differnew file mode 100644 index 00000000..431482dc --- /dev/null +++ b/docs/img/sponsors/1-vokal_interactive.png diff --git a/docs/img/sponsors/1-wiredrive.png b/docs/img/sponsors/1-wiredrive.png Binary files differnew file mode 100644 index 00000000..c9befefe --- /dev/null +++ b/docs/img/sponsors/1-wiredrive.png diff --git a/docs/img/sponsors/2-byte.png b/docs/img/sponsors/2-byte.png Binary files differnew file mode 100644 index 00000000..2c3777b5 --- /dev/null +++ b/docs/img/sponsors/2-byte.png diff --git a/docs/img/sponsors/2-compile.png b/docs/img/sponsors/2-compile.png Binary files differnew file mode 100644 index 00000000..858aa09d --- /dev/null +++ b/docs/img/sponsors/2-compile.png diff --git a/docs/img/sponsors/2-crate.png b/docs/img/sponsors/2-crate.png Binary files differnew file mode 100644 index 00000000..6ef6b5da --- /dev/null +++ b/docs/img/sponsors/2-crate.png diff --git a/docs/img/sponsors/2-cryptico.png b/docs/img/sponsors/2-cryptico.png Binary files differnew file mode 100644 index 00000000..2d86afe8 --- /dev/null +++ b/docs/img/sponsors/2-cryptico.png diff --git a/docs/img/sponsors/2-django.png b/docs/img/sponsors/2-django.png Binary files differnew file mode 100644 index 00000000..c89e19cb --- /dev/null +++ b/docs/img/sponsors/2-django.png diff --git a/docs/img/sponsors/2-galileo_press.png b/docs/img/sponsors/2-galileo_press.png Binary files differnew file mode 100644 index 00000000..f77e6c0a --- /dev/null +++ b/docs/img/sponsors/2-galileo_press.png diff --git a/docs/img/sponsors/2-heroku.png b/docs/img/sponsors/2-heroku.png Binary files differnew file mode 100644 index 00000000..22447659 --- /dev/null +++ b/docs/img/sponsors/2-heroku.png diff --git a/docs/img/sponsors/2-hipflask.png b/docs/img/sponsors/2-hipflask.png Binary files differnew file mode 100644 index 00000000..c74735c3 --- /dev/null +++ b/docs/img/sponsors/2-hipflask.png diff --git a/docs/img/sponsors/2-hipo.png b/docs/img/sponsors/2-hipo.png Binary files differnew file mode 100644 index 00000000..2b854c6d --- /dev/null +++ b/docs/img/sponsors/2-hipo.png diff --git a/docs/img/sponsors/2-koordinates.png b/docs/img/sponsors/2-koordinates.png Binary files differnew file mode 100644 index 00000000..f38601b3 --- /dev/null +++ b/docs/img/sponsors/2-koordinates.png diff --git a/docs/img/sponsors/2-laterpay.png b/docs/img/sponsors/2-laterpay.png Binary files differnew file mode 100644 index 00000000..75eb97d3 --- /dev/null +++ b/docs/img/sponsors/2-laterpay.png diff --git a/docs/img/sponsors/2-lightning_kite.png b/docs/img/sponsors/2-lightning_kite.png Binary files differnew file mode 100644 index 00000000..ffdced04 --- /dev/null +++ b/docs/img/sponsors/2-lightning_kite.png diff --git a/docs/img/sponsors/2-mirus_research.png b/docs/img/sponsors/2-mirus_research.png Binary files differnew file mode 100644 index 00000000..b1544070 --- /dev/null +++ b/docs/img/sponsors/2-mirus_research.png diff --git a/docs/img/sponsors/2-nexthub.png b/docs/img/sponsors/2-nexthub.png Binary files differnew file mode 100644 index 00000000..9bf76e0b --- /dev/null +++ b/docs/img/sponsors/2-nexthub.png diff --git a/docs/img/sponsors/2-opbeat.png b/docs/img/sponsors/2-opbeat.png Binary files differnew file mode 100644 index 00000000..c71a5241 --- /dev/null +++ b/docs/img/sponsors/2-opbeat.png diff --git a/docs/img/sponsors/2-prorenata.png b/docs/img/sponsors/2-prorenata.png Binary files differnew file mode 100644 index 00000000..f5e8bb76 --- /dev/null +++ b/docs/img/sponsors/2-prorenata.png diff --git a/docs/img/sponsors/2-rapasso.png b/docs/img/sponsors/2-rapasso.png Binary files differnew file mode 100644 index 00000000..618e294b --- /dev/null +++ b/docs/img/sponsors/2-rapasso.png diff --git a/docs/img/sponsors/2-schuberg_philis.png b/docs/img/sponsors/2-schuberg_philis.png Binary files differnew file mode 100644 index 00000000..fd9282ee --- /dev/null +++ b/docs/img/sponsors/2-schuberg_philis.png diff --git a/docs/img/sponsors/2-security_compass.png b/docs/img/sponsors/2-security_compass.png Binary files differnew file mode 100644 index 00000000..abd63dbe --- /dev/null +++ b/docs/img/sponsors/2-security_compass.png diff --git a/docs/img/sponsors/2-sga.png b/docs/img/sponsors/2-sga.png Binary files differnew file mode 100644 index 00000000..2b2a3b3b --- /dev/null +++ b/docs/img/sponsors/2-sga.png diff --git a/docs/img/sponsors/2-sirono.png b/docs/img/sponsors/2-sirono.png Binary files differnew file mode 100644 index 00000000..0a243001 --- /dev/null +++ b/docs/img/sponsors/2-sirono.png diff --git a/docs/img/sponsors/2-vinta.png b/docs/img/sponsors/2-vinta.png Binary files differnew file mode 100644 index 00000000..4f4d75bc --- /dev/null +++ b/docs/img/sponsors/2-vinta.png diff --git a/docs/img/sponsors/3-aba.png b/docs/img/sponsors/3-aba.png Binary files differnew file mode 100644 index 00000000..cefa3dd6 --- /dev/null +++ b/docs/img/sponsors/3-aba.png diff --git a/docs/img/sponsors/3-aditium.png b/docs/img/sponsors/3-aditium.png Binary files differnew file mode 100644 index 00000000..0952b08c --- /dev/null +++ b/docs/img/sponsors/3-aditium.png diff --git a/docs/img/sponsors/3-alwaysdata.png b/docs/img/sponsors/3-alwaysdata.png Binary files differnew file mode 100644 index 00000000..4095774b --- /dev/null +++ b/docs/img/sponsors/3-alwaysdata.png diff --git a/docs/img/sponsors/3-ax_semantics.png b/docs/img/sponsors/3-ax_semantics.png Binary files differnew file mode 100644 index 00000000..c072e028 --- /dev/null +++ b/docs/img/sponsors/3-ax_semantics.png diff --git a/docs/img/sponsors/3-beefarm.png b/docs/img/sponsors/3-beefarm.png Binary files differnew file mode 100644 index 00000000..3348df42 --- /dev/null +++ b/docs/img/sponsors/3-beefarm.png diff --git a/docs/img/sponsors/3-blimp.png b/docs/img/sponsors/3-blimp.png Binary files differnew file mode 100644 index 00000000..494bf792 --- /dev/null +++ b/docs/img/sponsors/3-blimp.png diff --git a/docs/img/sponsors/3-brightloop.png b/docs/img/sponsors/3-brightloop.png Binary files differnew file mode 100644 index 00000000..8d5e85a6 --- /dev/null +++ b/docs/img/sponsors/3-brightloop.png diff --git a/docs/img/sponsors/3-cantemo.gif b/docs/img/sponsors/3-cantemo.gif Binary files differnew file mode 100644 index 00000000..17b1e8d0 --- /dev/null +++ b/docs/img/sponsors/3-cantemo.gif diff --git a/docs/img/sponsors/3-crosswordtracker.png b/docs/img/sponsors/3-crosswordtracker.png Binary files differnew file mode 100644 index 00000000..f72362ea --- /dev/null +++ b/docs/img/sponsors/3-crosswordtracker.png diff --git a/docs/img/sponsors/3-fluxility.png b/docs/img/sponsors/3-fluxility.png Binary files differnew file mode 100644 index 00000000..eacd7da9 --- /dev/null +++ b/docs/img/sponsors/3-fluxility.png diff --git a/docs/img/sponsors/3-garfo.png b/docs/img/sponsors/3-garfo.png Binary files differnew file mode 100644 index 00000000..a9bdea0a --- /dev/null +++ b/docs/img/sponsors/3-garfo.png diff --git a/docs/img/sponsors/3-gizmag.png b/docs/img/sponsors/3-gizmag.png Binary files differnew file mode 100644 index 00000000..a8d41bd0 --- /dev/null +++ b/docs/img/sponsors/3-gizmag.png diff --git a/docs/img/sponsors/3-holvi.png b/docs/img/sponsors/3-holvi.png Binary files differnew file mode 100644 index 00000000..255e391e --- /dev/null +++ b/docs/img/sponsors/3-holvi.png diff --git a/docs/img/sponsors/3-imt_computer_services.png b/docs/img/sponsors/3-imt_computer_services.png Binary files differnew file mode 100644 index 00000000..00643c97 --- /dev/null +++ b/docs/img/sponsors/3-imt_computer_services.png diff --git a/docs/img/sponsors/3-infinite_code.png b/docs/img/sponsors/3-infinite_code.png Binary files differnew file mode 100644 index 00000000..7a8fdcf1 --- /dev/null +++ b/docs/img/sponsors/3-infinite_code.png diff --git a/docs/img/sponsors/3-ipushpull.png b/docs/img/sponsors/3-ipushpull.png Binary files differnew file mode 100644 index 00000000..e70b8bad --- /dev/null +++ b/docs/img/sponsors/3-ipushpull.png diff --git a/docs/img/sponsors/3-isl.png b/docs/img/sponsors/3-isl.png Binary files differnew file mode 100644 index 00000000..0bf0cf7c --- /dev/null +++ b/docs/img/sponsors/3-isl.png diff --git a/docs/img/sponsors/3-life_the_game.png b/docs/img/sponsors/3-life_the_game.png Binary files differnew file mode 100644 index 00000000..9292685e --- /dev/null +++ b/docs/img/sponsors/3-life_the_game.png diff --git a/docs/img/sponsors/3-makespace.png b/docs/img/sponsors/3-makespace.png Binary files differnew file mode 100644 index 00000000..80b79361 --- /dev/null +++ b/docs/img/sponsors/3-makespace.png diff --git a/docs/img/sponsors/3-nephila.png b/docs/img/sponsors/3-nephila.png Binary files differnew file mode 100644 index 00000000..a905fa93 --- /dev/null +++ b/docs/img/sponsors/3-nephila.png diff --git a/docs/img/sponsors/3-openeye.png b/docs/img/sponsors/3-openeye.png Binary files differnew file mode 100644 index 00000000..573140ed --- /dev/null +++ b/docs/img/sponsors/3-openeye.png diff --git a/docs/img/sponsors/3-pathwright.png b/docs/img/sponsors/3-pathwright.png Binary files differnew file mode 100644 index 00000000..71be3b28 --- /dev/null +++ b/docs/img/sponsors/3-pathwright.png diff --git a/docs/img/sponsors/3-phurba.png b/docs/img/sponsors/3-phurba.png Binary files differnew file mode 100644 index 00000000..657d872c --- /dev/null +++ b/docs/img/sponsors/3-phurba.png diff --git a/docs/img/sponsors/3-pkgfarm.png b/docs/img/sponsors/3-pkgfarm.png Binary files differnew file mode 100644 index 00000000..9224cc2e --- /dev/null +++ b/docs/img/sponsors/3-pkgfarm.png diff --git a/docs/img/sponsors/3-providenz.png b/docs/img/sponsors/3-providenz.png Binary files differnew file mode 100644 index 00000000..55d9c992 --- /dev/null +++ b/docs/img/sponsors/3-providenz.png diff --git a/docs/img/sponsors/3-safari.png b/docs/img/sponsors/3-safari.png Binary files differnew file mode 100644 index 00000000..c03e40e8 --- /dev/null +++ b/docs/img/sponsors/3-safari.png diff --git a/docs/img/sponsors/3-shippo.png b/docs/img/sponsors/3-shippo.png Binary files differnew file mode 100644 index 00000000..4f5ae133 --- /dev/null +++ b/docs/img/sponsors/3-shippo.png diff --git a/docs/img/sponsors/3-teonite.png b/docs/img/sponsors/3-teonite.png Binary files differnew file mode 100644 index 00000000..0c098478 --- /dev/null +++ b/docs/img/sponsors/3-teonite.png diff --git a/docs/img/sponsors/3-thermondo-gmbh.png b/docs/img/sponsors/3-thermondo-gmbh.png Binary files differnew file mode 100644 index 00000000..fe8691c8 --- /dev/null +++ b/docs/img/sponsors/3-thermondo-gmbh.png diff --git a/docs/img/sponsors/3-tivix.png b/docs/img/sponsors/3-tivix.png Binary files differnew file mode 100644 index 00000000..bc2616a6 --- /dev/null +++ b/docs/img/sponsors/3-tivix.png diff --git a/docs/img/sponsors/3-trackmaven.png b/docs/img/sponsors/3-trackmaven.png Binary files differnew file mode 100644 index 00000000..3880e370 --- /dev/null +++ b/docs/img/sponsors/3-trackmaven.png diff --git a/docs/img/sponsors/3-transcode.png b/docs/img/sponsors/3-transcode.png Binary files differnew file mode 100644 index 00000000..1faad69d --- /dev/null +++ b/docs/img/sponsors/3-transcode.png diff --git a/docs/img/sponsors/3-triggered_messaging.png b/docs/img/sponsors/3-triggered_messaging.png Binary files differnew file mode 100644 index 00000000..4f8e5063 --- /dev/null +++ b/docs/img/sponsors/3-triggered_messaging.png diff --git a/docs/img/sponsors/3-vzzual.png b/docs/img/sponsors/3-vzzual.png Binary files differnew file mode 100644 index 00000000..98edce02 --- /dev/null +++ b/docs/img/sponsors/3-vzzual.png diff --git a/docs/img/sponsors/3-wildfish.png b/docs/img/sponsors/3-wildfish.png Binary files differnew file mode 100644 index 00000000..fa13ea70 --- /dev/null +++ b/docs/img/sponsors/3-wildfish.png diff --git a/docs/index.md b/docs/index.md index 2a4ad885..dd407497 100644 --- a/docs/index.md +++ b/docs/index.md @@ -201,6 +201,7 @@ General guides to using REST framework. * [2.0 Announcement][rest-framework-2-announcement] * [2.2 Announcement][2.2-announcement] * [2.3 Announcement][2.3-announcement] +* [Kickstarter Announcement][kickstarter-announcement] * [Release Notes][release-notes] * [Credits][credits] @@ -325,6 +326,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. [rest-framework-2-announcement]: topics/rest-framework-2-announcement.md [2.2-announcement]: topics/2.2-announcement.md [2.3-announcement]: topics/2.3-announcement.md +[kickstarter-announcement]: topics/kickstarter-announcement.md [release-notes]: topics/release-notes.md [credits]: topics/credits.md diff --git a/docs/template.html b/docs/template.html index a397d067..ac225679 100644 --- a/docs/template.html +++ b/docs/template.html @@ -33,6 +33,21 @@ })(); </script> + <style> +span.fusion-wrap a { + display: block; + margin-top: 10px; + color: black; +} + +a.fusion-poweredby { + display: block; + margin-top: 10px; +} +@media (max-width: 767px) { + div.promo {display: none;} +} +</style> </head> <body onload="prettyPrint()" class="{{ page_id }}-page"> @@ -106,6 +121,7 @@ <li><a href="{{ base_url }}/topics/rest-framework-2-announcement{{ suffix }}">2.0 Announcement</a></li> <li><a href="{{ base_url }}/topics/2.2-announcement{{ suffix }}">2.2 Announcement</a></li> <li><a href="{{ base_url }}/topics/2.3-announcement{{ suffix }}">2.3 Announcement</a></li> + <li><a href="{{ base_url }}/topics/kickstarter-announcement{{ suffix }}">Kickstarter Announcement</a></li> <li><a href="{{ base_url }}/topics/release-notes{{ suffix }}">Release Notes</a></li> <li><a href="{{ base_url }}/topics/credits{{ suffix }}">Credits</a></li> </ul> @@ -169,11 +185,9 @@ <div id="table-of-contents"> <ul class="nav nav-list side-nav well sidebar-nav-fixed"> {{ toc }} - <div> - -{{ ad_block }} - -</div> + <div class="promo"> + {{ ad_block }} + </div> </ul> </div> @@ -199,6 +213,7 @@ <script src="{{ base_url }}/js/jquery-1.8.1-min.js"></script> <script src="{{ base_url }}/js/prettify-1.0.js"></script> <script src="{{ base_url }}/js/bootstrap-2.1.1-min.js"></script> + <script> //$('.side-nav').scrollspy() var shiftWindow = function() { scrollBy(0, -50) }; diff --git a/docs/topics/browsable-api.md b/docs/topics/browsable-api.md index e32db695..96cdabe6 100644 --- a/docs/topics/browsable-api.md +++ b/docs/topics/browsable-api.md @@ -167,10 +167,10 @@ You can now add the `autocomplete_light.ChoiceWidget` widget to the serializer f [bootstrap]: http://getbootstrap.com [cerulean]: ../img/cerulean.png [slate]: ../img/slate.png -[bcustomize]: http://twitter.github.com/bootstrap/customize.html#variables +[bcustomize]: http://getbootstrap.com/2.3.2/customize.html [bswatch]: http://bootswatch.com/ -[bcomponents]: http://twitter.github.com/bootstrap/components.html -[bcomponentsnav]: http://twitter.github.com/bootstrap/components.html#navbar +[bcomponents]: http://getbootstrap.com/2.3.2/components.html +[bcomponentsnav]: http://getbootstrap.com/2.3.2/components.html#navbar [autocomplete-packages]: https://www.djangopackages.com/grids/g/auto-complete/ [django-autocomplete-light]: https://github.com/yourlabs/django-autocomplete-light [django-autocomplete-light-install]: http://django-autocomplete-light.readthedocs.org/en/latest/#install diff --git a/docs/topics/documenting-your-api.md b/docs/topics/documenting-your-api.md index 6291c924..e20f9712 100644 --- a/docs/topics/documenting-your-api.md +++ b/docs/topics/documenting-your-api.md @@ -95,7 +95,7 @@ You can modify the response behavior to `OPTIONS` requests by overriding the `me To be fully RESTful an API should present its available actions as hypermedia controls in the responses that it sends. -In this approach, rather than documenting the available API endpoints up front, the description instead concentrates on the *media types* that are used. The available actions take may be taken on any given URL are not strictly fixed, but are instead made available by the presence of link and form controls in the returned document. +In this approach, rather than documenting the available API endpoints up front, the description instead concentrates on the *media types* that are used. The available actions that may be taken on any given URL are not strictly fixed, but are instead made available by the presence of link and form controls in the returned document. To implement a hypermedia API you'll need to decide on an appropriate media type for the API, and implement a custom renderer and parser for that media type. The [REST, Hypermedia & HATEOAS][hypermedia-docs] section of the documentation includes pointers to background reading, as well as links to various hypermedia formats. diff --git a/docs/topics/kickstarter-announcement.md b/docs/topics/kickstarter-announcement.md new file mode 100644 index 00000000..84dc8511 --- /dev/null +++ b/docs/topics/kickstarter-announcement.md @@ -0,0 +1,162 @@ +# Kickstarting Django REST framework 3 + +--- + +<iframe width="480" height="360" src="https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3/widget/video.html" frameborder="0" scrolling="no"> </iframe> + +--- + +In order to continue to drive the project forward, I'm launching a Kickstarter campaign to help fund the development of a major new release - Django REST framework 3. + +## Project details + +This new release will allow us to comprehensively address some of the shortcomings of the framework, and will aim to include the following: + +* Faster, simpler and easier-to-use serializers. +* An alternative admin-style interface for the browsable API. +* Search and filtering controls made accessible in the browsable API. +* Alternative API pagination styles. +* Documentation around API versioning. +* Triage of outstanding tickets. +* Improving the ongoing quality and maintainability of the project. + +Full details are available now on the [project page](https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3). + +If you're interested in helping make sustainable open source development a reality please [visit the Kickstarter page](https://www.kickstarter.com/projects/tomchristie/django-rest-framework-3) and consider funding the project. + +I can't wait to see where this takes us! + +Many thanks to everyone for your support so far, + + Tom Christie :) + +--- + +## Sponsors + +We've now blazed way past all our goals, with a staggering £30,000 (~$50,000), meaning I'll be in a position to work on the project significantly beyond what we'd originally planned for. I owe a huge debt of gratitude to all the wonderful companies and individuals who have been backing the project so generously, and making this possible. + +--- + +### Platinum sponsors + +Our platinum sponsors have each made a hugely substantial contribution to the future development of Django REST framework, and I simply can't thank them enough. + +<ul class="sponsor diamond"> +<li><a href="https://www.eventbrite.com/" rel="nofollow" style="background-image:url(../img/sponsors/0-eventbrite.png);">Eventbrite</a></li> +</ul> + +<ul class="sponsor platinum"> +<li><a href="https://www.divio.ch/" rel="nofollow" style="background-image:url(../img/sponsors/1-divio.png);">Divio</a></li> +<li><a href="http://company.onlulu.com/en/" rel="nofollow" style="background-image:url(../img/sponsors/1-lulu.png);">Lulu</a></li> +<li><a href="https://p.ota.to/" rel="nofollow" style="background-image:url(../img/sponsors/1-potato.png);">Potato</a></li> +<li><a href="http://www.wiredrive.com/" rel="nofollow" style="background-image:url(../img/sponsors/1-wiredrive.png);">Wiredrive</a></li> +<li><a href="http://www.cyaninc.com/" rel="nofollow" style="background-image:url(../img/sponsors/1-cyan.png);">Cyan</a></li> +<li><a href="https://www.runscope.com/" rel="nofollow" style="background-image:url(../img/sponsors/1-runscope.png);">Runscope</a></li> +<li><a href="http://simpleenergy.com/" rel="nofollow" style="background-image:url(../img/sponsors/1-simple-energy.png);">Simple Energy</a></li> +<li><a href="http://vokalinteractive.com/" rel="nofollow" style="background-image:url(../img/sponsors/1-vokal_interactive.png);">VOKAL Interactive</a></li> +<li><a href="http://www.purplebit.com/" rel="nofollow" style="background-image:url(../img/sponsors/1-purplebit.png);">Purple Bit</a></li> +<li><a href="http://www.kuwaitnet.net/" rel="nofollow" style="background-image:url(../img/sponsors/1-kuwaitnet.png);">KuwaitNET</a></li> +</ul> + +<div style="clear: both"></div> + +--- + +### 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. + +<ul class="sponsor gold"> +<li><a href="https://laterpay.net/" rel="nofollow" style="background-image:url(../img/sponsors/2-laterpay.png);">LaterPay</a></li> +<li><a href="https://www.schubergphilis.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-schuberg_philis.png);">Schuberg Philis</a></li> +<li><a href="http://prorenata.se/" rel="nofollow" style="background-image:url(../img/sponsors/2-prorenata.png);">ProReNata AB</a></li> +<li><a href="https://www.sgawebsites.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-sga.png);">SGA Websites</a></li> +<li><a href="http://www.sirono.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-sirono.png);">Sirono</a></li> +<li><a href="http://www.vinta.com.br/" rel="nofollow" style="background-image:url(../img/sponsors/2-vinta.png);">Vinta Software Studio</a></li> +<li><a href="http://www.rapasso.nl/index.php/en" rel="nofollow" style="background-image:url(../img/sponsors/2-rapasso.png);">Rapasso</a></li> +<li><a href="https://mirusresearch.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-mirus_research.png);">Mirus Research</a></li> +<li><a href="http://hipolabs.com" rel="nofollow" style="background-image:url(../img/sponsors/2-hipo.png);">Hipo</a></li> +<li><a href="http://www.byte.nl" rel="nofollow" style="background-image:url(../img/sponsors/2-byte.png);">Byte</a></li> +<li><a href="http://lightningkite.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-lightning_kite.png);">Lightning Kite</a></li> +<li><a href="https://opbeat.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-opbeat.png);">Opbeat</a></li> +<li><a href="https://koordinates.com" rel="nofollow" style="background-image:url(../img/sponsors/2-koordinates.png);">Koordinates</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="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> +<li><a href="http://www.crate.io/" rel="nofollow" style="background-image:url(../img/sponsors/2-crate.png);">Crate</a></li> +<li><a href="http://crypticocorp.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-cryptico.png);">Cryptico Corp</a></li> +<li><a href="http://www.nexthub.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-nexthub.png);">NextHub</a></li> +<li><a href="https://www.compile.com/" rel="nofollow" style="background-image:url(../img/sponsors/2-compile.png);">Compile</a></li> +<li><a href="http://envisionlinux.org/blog" rel="nofollow">Envision Linux</a></li> +</ul> + +<div style="clear: both; padding-bottom: 40px;"></div> + +**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. + +<ul class="sponsor silver"> +<li><a href="http://www.imtapps.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-imt_computer_services.png);">IMT Computer Services</a></li> +<li><a href="http://wildfish.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-wildfish.png);">Wildfish</a></li> +<li><a href="http://www.thermondo.de/" rel="nofollow" style="background-image:url(../img/sponsors/3-thermondo-gmbh.png);">Thermondo GmbH</a></li> +<li><a href="http://providenz.fr/" rel="nofollow" style="background-image:url(../img/sponsors/3-providenz.png);">Providenz</a></li> +<li><a href="https://www.alwaysdata.com" rel="nofollow" style="background-image:url(../img/sponsors/3-alwaysdata.png);">alwaysdata.com</a></li> +<li><a href="http://www.triggeredmessaging.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-triggered_messaging.png);">Triggered Messaging</a></li> +<li><a href="https://www.ipushpull.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-ipushpull.png);">PushPull Technology Ltd</a></li> +<li><a href="http://www.transcode.de/" rel="nofollow" style="background-image:url(../img/sponsors/3-transcode.png);">Transcode</a></li> +<li><a href="https://garfo.io/" rel="nofollow" style="background-image:url(../img/sponsors/3-garfo.png);">Garfo</a></li> +<li><a href="https://goshippo.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-shippo.png);">Shippo</a></li> +<li><a href="http://www.gizmag.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-gizmag.png);">Gizmag</a></li> +<li><a href="http://www.tivix.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-tivix.png);">Tivix</a></li> +<li><a href="http://www.safaribooksonline.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-safari.png);">Safari</a></li> +<li><a href="http://brightloop.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-brightloop.png);">Bright Loop</a></li> +<li><a href="http://www.aba-systems.com.au/" rel="nofollow" style="background-image:url(../img/sponsors/3-aba.png);">ABA Systems</a></li> +<li><a href="http://beefarm.ru/" rel="nofollow" style="background-image:url(../img/sponsors/3-beefarm.png);">beefarm.ru</a></li> +<li><a href="http://www.vzzual.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-vzzual.png);">Vzzual.com</a></li> +<li><a href="http://infinite-code.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-infinite_code.png);">Infinite Code</a></li> +<li><a href="http://crosswordtracker.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-crosswordtracker.png);">Crossword Tracker</a></li> +<li><a href="https://www.pkgfarm.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-pkgfarm.png);">PkgFarm</a></li> +<li><a href="http://life.tl/" rel="nofollow" style="background-image:url(../img/sponsors/3-life_the_game.png);">Life. The Game.</a></li> +<li><a href="http://blimp.io/" rel="nofollow" style="background-image:url(../img/sponsors/3-blimp.png);">Blimp</a></li> +<li><a href="http://pathwright.com" rel="nofollow" style="background-image:url(../img/sponsors/3-pathwright.png);">Pathwright</a></li> +<li><a href="http://fluxility.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-fluxility.png);">Fluxility</a></li> +<li><a href="http://teonite.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-teonite.png);">Teonite</a></li> +<li><a href="http://trackmaven.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-trackmaven.png);">TrackMaven</a></li> +<li><a href="http://www.phurba.net/" rel="nofollow" style="background-image:url(../img/sponsors/3-phurba.png);">Phurba</a></li> +<li><a href="http://www.nephila.co.uk/" rel="nofollow" style="background-image:url(../img/sponsors/3-nephila.png);">Nephila</a></li> +<li><a href="http://www.aditium.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-aditium.png);">Aditium</a></li> +<li><a href="http://www.eyesopen.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-openeye.png);">OpenEye Scientific Software</a></li> +<li><a href="https://holvi.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-holvi.png);">Holvi</a></li> +<li><a href="http://cantemo.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-cantemo.gif);">Cantemo</a></li> +<li><a href="https://www.makespace.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-makespace.png);">MakeSpace</a></li> +<li><a href="https://www.ax-semantics.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-ax_semantics.png);">AX Semantics</a></li> +<li><a href="http://istrategylabs.com/" rel="nofollow" style="background-image:url(../img/sponsors/3-isl.png);">ISL</a></li> +</ul> + +<div style="clear: both; padding-bottom: 40px;"></div> + +**Individual backers**: Paul Hallet, <a href="http://www.paulwhippconsulting.com/">Paul Whipp</a>, Dylan Roy, Jannis Leidel, <a href="https://linovia.com/en/">Xavier Ordoquy</a>, <a href="http://spielmannsolutions.com/">Johannes Spielmann</a>, <a href="http://brooklynhacker.com/">Rob Spectre</a>, <a href="http://chrisheisel.com/">Chris Heisel</a>, 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/topics/release-notes.md b/docs/topics/release-notes.md index 335497ee..ea4c912c 100644 --- a/docs/topics/release-notes.md +++ b/docs/topics/release-notes.md @@ -40,24 +40,28 @@ You can determine your currently installed version using `pip freeze`: ## 2.3.x series -### 2.3.x - -**Date**: April 2014 - -* Fix nested serializers linked through a backward foreign key relation -* Fix bad links for the `BrowsableAPIRenderer` with `YAMLRenderer` -* Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode -* Fix `parse_header` argument convertion -* Fix mediatype detection under Python3 -* Web browseable API now offers blank option on dropdown when the field is not required -* `APIException` representation improved for logging purposes -* Allow source="*" within nested serializers -* Better support for custom oauth2 provider backends -* Fix field validation if it's optional and has no value -* Add `SEARCH_PARAM` and `ORDERING_PARAM` -* Fix `APIRequestFactory` to support arguments within the url string for GET -* Allow three transport modes for access tokens when accessing a protected resource -* Fix `Request`'s `QueryDict` encoding +### 2.3.14 + +**Date**: 12th June 2014 + +* **Security fix**: Escape request path when it is include as part of the login and logout links in the browsable API. +* `help_text` and `verbose_name` automatically set for related fields on `ModelSerializer`. +* Fix nested serializers linked through a backward foreign key relation. +* Fix bad links for the `BrowsableAPIRenderer` with `YAMLRenderer`. +* Add `UnicodeYAMLRenderer` that extends `YAMLRenderer` with unicode. +* Fix `parse_header` argument convertion. +* Fix mediatype detection under Python 3. +* Web browseable API now offers blank option on dropdown when the field is not required. +* `APIException` representation improved for logging purposes. +* Allow source="*" within nested serializers. +* Better support for custom oauth2 provider backends. +* Fix field validation if it's optional and has no value. +* Add `SEARCH_PARAM` and `ORDERING_PARAM`. +* Fix `APIRequestFactory` to support arguments within the url string for GET. +* Allow three transport modes for access tokens when accessing a protected resource. +* Fix `QueryDict` encoding on request objects. +* Ensure throttle keys do not contain spaces, as those are invalid if using `memcached`. +* Support `blank_display_value` on `ChoiceField`. ### 2.3.13 diff --git a/docs/tutorial/1-serialization.md b/docs/tutorial/1-serialization.md index 55b19457..96214f5b 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 # <Snippet: Snippet 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 603edd08..e70bbbfc 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<pk>[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 b37bc31b..e04072ca 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<pk>[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 491df160..74ad9a55 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 2cf44bf9..9c61fe3d 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 8bf8c7f5..98e5f439 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" } ] @@ -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' @@ -162,8 +162,8 @@ for (dirpath, dirnames, filenames) in os.walk(docs_dir): output = output.replace('{{ canonical_url }}', canonical_url) if filename =='index.md': - output = output.replace('{{ ad_block }}', """<hr><p><strong>The team behind REST framework is launching a new API service.</strong></p> -<p>If you want to be first in line when we start issuing invitations, please <a href="http://brightapi.com">sign up here</a>.</p>""") + output = output.replace('{{ ad_block }}', """<hr/> + <script type="text/javascript" src="//cdn.fusionads.net/fusion.js?zoneid=1332&serve=C6SDP2Y&placement=djangorestframework" id="_fusionads_js"></script>""") else: output = output.replace('{{ ad_block }}', '') diff --git a/rest_framework/__init__.py b/rest_framework/__init__.py index 2d76b55d..01036cef 100644 --- a/rest_framework/__init__.py +++ b/rest_framework/__init__.py @@ -8,7 +8,7 @@ ______ _____ _____ _____ __ _ """ __title__ = 'Django REST framework' -__version__ = '2.3.13' +__version__ = '2.3.14' __author__ = 'Tom Christie' __license__ = 'BSD 2-Clause' __copyright__ = 'Copyright 2011-2014 Tom Christie' diff --git a/rest_framework/authentication.py b/rest_framework/authentication.py index da9ca510..887ef5d7 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 d155f554..9ad8b0d2 100644 --- a/rest_framework/compat.py +++ b/rest_framework/compat.py @@ -48,11 +48,15 @@ try: except ImportError: django_filters = None -# guardian is optional -try: - import guardian -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/fields.py b/rest_framework/fields.py index d80aab56..6caae924 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -187,7 +187,7 @@ class Field(object): def field_to_native(self, obj, field_name): """ - Given and object and a field name, returns the value that should be + Given an object and a field name, returns the value that should be serialized for that field. """ if obj is None: @@ -474,8 +474,12 @@ class CharField(WritableField): self.validators.append(validators.MaxLengthValidator(max_length)) def from_native(self, value): - if isinstance(value, six.string_types) or value is None: + if isinstance(value, six.string_types): return value + + if value is None: + return '' + return smart_text(value) @@ -506,7 +510,7 @@ class SlugField(CharField): class ChoiceField(WritableField): type_name = 'ChoiceField' - type_label = 'multiple choice' + type_label = 'choice' form_field_class = forms.ChoiceField widget = widgets.Select default_error_messages = { @@ -514,12 +518,16 @@ class ChoiceField(WritableField): 'the available choices.'), } - def __init__(self, choices=(), *args, **kwargs): + def __init__(self, choices=(), blank_display_value=None, *args, **kwargs): self.empty = kwargs.pop('empty', '') super(ChoiceField, self).__init__(*args, **kwargs) self.choices = choices if not self.required: - self.choices = BLANK_CHOICE_DASH + self.choices + if blank_display_value is None: + blank_choice = BLANK_CHOICE_DASH + else: + blank_choice = [('', blank_display_value)] + self.choices = blank_choice + self.choices def _get_choices(self): return self._choices @@ -1023,9 +1031,9 @@ class SerializerMethodField(Field): A field that gets its value by calling a method on the serializer it's attached to. """ - def __init__(self, method_name): + def __init__(self, method_name, *args, **kwargs): self.method_name = method_name - super(SerializerMethodField, self).__init__() + super(SerializerMethodField, self).__init__(*args, **kwargs) def field_to_native(self, obj, field_name): value = getattr(self.parent, self.method_name)(obj) diff --git a/rest_framework/filters.py b/rest_framework/filters.py index 96d15eb9..c3b846ae 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 65ccd952..42204841 100644 --- a/rest_framework/generics.py +++ b/rest_framework/generics.py @@ -94,8 +94,8 @@ class GenericAPIView(views.APIView): 'view': self } - def get_serializer(self, instance=None, data=None, - files=None, many=False, partial=False): + def get_serializer(self, instance=None, data=None, files=None, many=False, + partial=False, allow_add_remove=False): """ Return the serializer instance that should be used for validating and deserializing input, and for serializing output. @@ -103,7 +103,9 @@ class GenericAPIView(views.APIView): serializer_class = self.get_serializer_class() context = self.get_serializer_context() return serializer_class(instance, data=data, files=files, - many=many, partial=partial, context=context) + many=many, partial=partial, + allow_add_remove=allow_add_remove, + context=context) def get_pagination_serializer(self, page): """ diff --git a/rest_framework/renderers.py b/rest_framework/renderers.py index 484961ad..7048d87d 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 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')) + try: + 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. + Render `data` into JSON, returning a bytestring. """ 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) - - 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 + 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/response.py b/rest_framework/response.py index 1dc6abcf..5c02ea50 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,6 +16,9 @@ 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, template_name=None, headers=None, diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 6dd09f68..43d339da 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -33,8 +33,8 @@ from rest_framework.settings import api_settings # This helps keep the separation between model fields, form fields, and # serializer fields more explicit. -from rest_framework.relations import * -from rest_framework.fields import * +from rest_framework.relations import * # NOQA +from rest_framework.fields import * # NOQA def _resolve_model(obj): @@ -49,7 +49,7 @@ def _resolve_model(obj): String representations should have the format: 'appname.ModelName' """ - if type(obj) == str and len(obj.split('.')) == 2: + if isinstance(obj, six.string_types) and len(obj.split('.')) == 2: app_name, model_name = obj.split('.') return models.get_model(app_name, model_name) elif inspect.isclass(obj) and issubclass(obj, models.Model): @@ -345,7 +345,7 @@ class BaseSerializer(WritableField): for field_name, field in self.fields.items(): if field.read_only and obj is None: - continue + continue field.initialize(parent=self, field_name=field_name) key = self.get_field_key(field_name) value = field.field_to_native(obj, field_name) @@ -759,9 +759,9 @@ class ModelSerializer(Serializer): field.read_only = True ret[accessor_name] = field - + # Ensure that 'read_only_fields' is an iterable - assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' + assert isinstance(self.opts.read_only_fields, (list, tuple)), '`read_only_fields` must be a list or tuple' # Add the `read_only` flag to any fields that have been specified # in the `read_only_fields` option @@ -776,10 +776,10 @@ class ModelSerializer(Serializer): "on serializer '%s'." % (field_name, self.__class__.__name__)) ret[field_name].read_only = True - + # Ensure that 'write_only_fields' is an iterable - assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple' - + assert isinstance(self.opts.write_only_fields, (list, tuple)), '`write_only_fields` must be a list or tuple' + for field_name in self.opts.write_only_fields: assert field_name not in self.base_fields.keys(), ( "field '%s' on serializer '%s' specified in " @@ -790,7 +790,7 @@ class ModelSerializer(Serializer): "Non-existant field '%s' specified in `write_only_fields` " "on serializer '%s'." % (field_name, self.__class__.__name__)) - ret[field_name].write_only = True + ret[field_name].write_only = True return ret @@ -977,7 +977,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 7067ee2f..e96fa8ec 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -93,7 +93,7 @@ {% endif %} {% if options_form %} - <form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right"> + <form class="button-form" action="{{ request.get_full_path }}" method="POST"> {% csrf_token %} <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="OPTIONS" /> <button class="btn btn-primary js-tooltip" title="Make an OPTIONS request on the {{ name }} resource">OPTIONS</button> @@ -101,7 +101,7 @@ {% endif %} {% if delete_form %} - <form class="button-form" action="{{ request.get_full_path }}" method="POST" class="pull-right"> + <form class="button-form" action="{{ request.get_full_path }}" method="POST"> {% csrf_token %} <input type="hidden" name="{{ api_settings.FORM_METHOD_OVERRIDE }}" value="DELETE" /> <button class="btn btn-danger js-tooltip" title="Make a DELETE request on the {{ name }} resource">DELETE</button> diff --git a/rest_framework/templatetags/rest_framework.py b/rest_framework/templatetags/rest_framework.py index dff176d6..a155d8d2 100644 --- a/rest_framework/templatetags/rest_framework.py +++ b/rest_framework/templatetags/rest_framework.py @@ -122,7 +122,7 @@ def optional_login(request): except NoReverseMatch: return '' - snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, request.path) + snippet = "<a href='%s?next=%s'>Log in</a>" % (login_url, escape(request.path)) return snippet @@ -136,7 +136,7 @@ def optional_logout(request): except NoReverseMatch: return '' - snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, request.path) + snippet = "<a href='%s?next=%s'>Log out</a>" % (logout_url, escape(request.path)) return snippet diff --git a/rest_framework/test.py b/rest_framework/test.py index df5a5b3b..284bcee0 100644 --- a/rest_framework/test.py +++ b/rest_framework/test.py @@ -36,7 +36,7 @@ class APIRequestFactory(DjangoRequestFactory): """ if not data: - return ('', None) + return ('', content_type) assert format is None or content_type is None, ( 'You may not set both `format` and `content_type`.' diff --git a/rest_framework/tests/models.py b/rest_framework/tests/models.py index e171d3bd..fba3f8f7 100644 --- a/rest_framework/tests/models.py +++ b/rest_framework/tests/models.py @@ -105,6 +105,7 @@ class Album(RESTFrameworkModel): title = models.CharField(max_length=100, unique=True) ref = models.CharField(max_length=10, unique=True, null=True, blank=True) + class Photo(RESTFrameworkModel): description = models.TextField() album = models.ForeignKey(Album) @@ -112,7 +113,8 @@ class Photo(RESTFrameworkModel): # Model for issue #324 class BlankFieldModel(RESTFrameworkModel): - title = models.CharField(max_length=100, blank=True, null=False) + title = models.CharField(max_length=100, blank=True, null=False, + default="title") # Model for issue #380 diff --git a/rest_framework/tests/test_authentication.py b/rest_framework/tests/test_authentication.py index a1c43d9c..34bf2910 100644 --- a/rest_framework/tests/test_authentication.py +++ b/rest_framework/tests/test_authentication.py @@ -550,6 +550,15 @@ class OAuth2Tests(TestCase): 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""" auth = self._create_authorization_header() diff --git a/rest_framework/tests/test_fields.py b/rest_framework/tests/test_fields.py index 3ae1c438..17d12f23 100644 --- a/rest_framework/tests/test_fields.py +++ b/rest_framework/tests/test_fields.py @@ -717,6 +717,15 @@ class ChoiceFieldTests(TestCase): f = serializers.ChoiceField(required=False, choices=SAMPLE_CHOICES) self.assertEqual(f.choices, models.fields.BLANK_CHOICE_DASH + SAMPLE_CHOICES) + def test_blank_choice_display(self): + blank = 'No Preference' + f = serializers.ChoiceField( + required=False, + choices=SAMPLE_CHOICES, + blank_display_value=blank, + ) + self.assertEqual(f.choices, [('', blank)] + SAMPLE_CHOICES) + def test_invalid_choice_model(self): s = ChoiceFieldModelSerializer(data={'choice': 'wrong_value'}) self.assertFalse(s.is_valid()) @@ -993,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/rest_framework/tests/test_serializer.py b/rest_framework/tests/test_serializer.py index e688c823..fb2eac0b 100644 --- a/rest_framework/tests/test_serializer.py +++ b/rest_framework/tests/test_serializer.py @@ -685,7 +685,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): """ @@ -1236,6 +1236,9 @@ class BlankFieldTests(TestCase): def test_create_model_null_field(self): serializer = self.model_serializer_class(data={'title': None}) self.assertEqual(serializer.is_valid(), True) + serializer.save() + self.assertIsNot(serializer.object.pk, None) + self.assertEqual(serializer.object.title, '') def test_create_not_blank_field(self): """ diff --git a/rest_framework/tests/test_serializers.py b/rest_framework/tests/test_serializers.py index 082a400c..120510ac 100644 --- a/rest_framework/tests/test_serializers.py +++ b/rest_framework/tests/test_serializers.py @@ -3,6 +3,7 @@ from django.test import TestCase from rest_framework.serializers import _resolve_model from rest_framework.tests.models import BasicModel +from rest_framework.compat import six class ResolveModelTests(TestCase): @@ -19,6 +20,10 @@ class ResolveModelTests(TestCase): resolved_model = _resolve_model('tests.BasicModel') self.assertEqual(resolved_model, BasicModel) + def test_resolve_unicode_representation(self): + resolved_model = _resolve_model(six.text_type('tests.BasicModel')) + self.assertEqual(resolved_model, BasicModel) + def test_resolve_non_django_model(self): with self.assertRaises(ValueError): _resolve_model(TestCase) diff --git a/rest_framework/urls.py b/rest_framework/urls.py index 9c4719f1..5d70f899 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 rest_framework.compat import patterns, url @@ -12,34 +12,34 @@ commands = {envpython} rest_framework/runtests/runtests.py [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 [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 [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 [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 [testenv:py3.4-django1.6] @@ -71,7 +71,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 [testenv:py2.6-django1.6] @@ -82,7 +82,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 [testenv:py3.4-django1.5] @@ -114,7 +114,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 [testenv:py2.6-django1.5] @@ -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 [testenv:py2.7-django1.4] @@ -136,7 +136,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 [testenv:py2.6-django1.4] @@ -147,7 +147,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 [testenv:py2.7-django1.3] @@ -158,7 +158,7 @@ deps = django==1.3.5 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 [testenv:py2.6-django1.3] @@ -169,5 +169,5 @@ deps = django==1.3.5 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 |
