8 |
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | *Note*: Before submitting this pull request, please review our [contributing guidelines](https://github.com/tomchristie/django-rest-framework/blob/master/CONTRIBUTING.md#pull-requests).
2 |
3 | ## Description
4 |
5 | Please describe your pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. When linking to an issue, please use `refs #...` in the description of the pull request.
6 |
--------------------------------------------------------------------------------
/rest_framework/templates/rest_framework/raw_data_form.html:
--------------------------------------------------------------------------------
1 | {% load rest_framework %}
2 | {{ form.non_field_errors }}
3 | {% for field in form %}
4 |
22 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | # The base set of requirements for REST framework is actually
2 | # just Django, but for the purposes of development and testing
3 | # there are a number of packages that are useful to install.
4 |
5 | # Laying these out as seperate requirements files, allows us to
6 | # only included the relevent sets when running tox, and ensures
7 | # we are only ever declaring our dependencies in one place.
8 |
9 | -r requirements/requirements-optionals.txt
10 | -r requirements/requirements-testing.txt
11 | -r requirements/requirements-documentation.txt
12 | -r requirements/requirements-codestyle.txt
13 | -r requirements/requirements-packaging.txt
14 |
--------------------------------------------------------------------------------
/rest_framework/templates/rest_framework/filters/ordering.html:
--------------------------------------------------------------------------------
1 | {% load rest_framework %}
2 | {% load i18n %}
3 |
34 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # License
2 |
3 | Copyright (c) 2011-2016, Tom Christie
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 | Redistributions in binary form must reproduce the above copyright notice, this
12 | list of conditions and the following disclaimer in the documentation and/or
13 | other materials provided with the distribution.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 |
--------------------------------------------------------------------------------
/tests/test_viewsets.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | from rest_framework import status
4 | from rest_framework.response import Response
5 | from rest_framework.test import APIRequestFactory
6 | from rest_framework.viewsets import GenericViewSet
7 |
8 | factory = APIRequestFactory()
9 |
10 |
11 | class BasicViewSet(GenericViewSet):
12 | def list(self, request, *args, **kwargs):
13 | return Response({'ACTION': 'LIST'})
14 |
15 |
16 | class InitializeViewSetsTestCase(TestCase):
17 | def test_initialize_view_set_with_actions(self):
18 | request = factory.get('/', '', content_type='application/json')
19 | my_view = BasicViewSet.as_view(actions={
20 | 'get': 'list',
21 | })
22 |
23 | response = my_view(request)
24 | self.assertEqual(response.status_code, status.HTTP_200_OK)
25 | self.assertEqual(response.data, {'ACTION': 'LIST'})
26 |
27 | def test_initialize_view_set_with_empty_actions(self):
28 | try:
29 | BasicViewSet.as_view()
30 | except TypeError as e:
31 | self.assertEqual(str(e), "The `actions` argument must be provided "
32 | "when calling `.as_view()` on a ViewSet. "
33 | "For example `.as_view({'get': 'list'})`")
34 | else:
35 | self.fail("actions must not be empty.")
36 |
--------------------------------------------------------------------------------
/rest_framework/templates/rest_framework/horizontal/checkbox_multiple.html:
--------------------------------------------------------------------------------
1 | {% load rest_framework %}
2 |
3 |
')
71 |
--------------------------------------------------------------------------------
/rest_framework/utils/encoders.py:
--------------------------------------------------------------------------------
1 | """
2 | Helper classes for parsers.
3 | """
4 | from __future__ import unicode_literals
5 |
6 | import datetime
7 | import decimal
8 | import json
9 | import uuid
10 |
11 | from django.db.models.query import QuerySet
12 | from django.utils import six, timezone
13 | from django.utils.encoding import force_text
14 | from django.utils.functional import Promise
15 |
16 | from rest_framework.compat import coreapi, total_seconds
17 |
18 |
19 | class JSONEncoder(json.JSONEncoder):
20 | """
21 | JSONEncoder subclass that knows how to encode date/time/timedelta,
22 | decimal types, generators and other basic python objects.
23 | """
24 | def default(self, obj):
25 | # For Date Time string spec, see ECMA 262
26 | # http://ecma-international.org/ecma-262/5.1/#sec-15.9.1.15
27 | if isinstance(obj, Promise):
28 | return force_text(obj)
29 | elif isinstance(obj, datetime.datetime):
30 | representation = obj.isoformat()
31 | if representation.endswith('+00:00'):
32 | representation = representation[:-6] + 'Z'
33 | return representation
34 | elif isinstance(obj, datetime.date):
35 | return obj.isoformat()
36 | elif isinstance(obj, datetime.time):
37 | if timezone and timezone.is_aware(obj):
38 | raise ValueError("JSON can't represent timezone-aware times.")
39 | representation = obj.isoformat()
40 | if obj.microsecond:
41 | representation = representation[:12]
42 | return representation
43 | elif isinstance(obj, datetime.timedelta):
44 | return six.text_type(total_seconds(obj))
45 | elif isinstance(obj, decimal.Decimal):
46 | # Serializers will coerce decimals to strings by default.
47 | return float(obj)
48 | elif isinstance(obj, uuid.UUID):
49 | return six.text_type(obj)
50 | elif isinstance(obj, QuerySet):
51 | return tuple(obj)
52 | elif isinstance(obj, six.binary_type):
53 | # Best-effort for binary blobs. See #4187.
54 | return obj.decode('utf-8')
55 | elif hasattr(obj, 'tolist'):
56 | # Numpy arrays and array scalars.
57 | return obj.tolist()
58 | elif hasattr(obj, '__getitem__'):
59 | try:
60 | return dict(obj)
61 | except:
62 | pass
63 | elif hasattr(obj, '__iter__'):
64 | return tuple(item for item in obj)
65 | elif (coreapi is not None) and isinstance(obj, (coreapi.Document, coreapi.Error)):
66 | raise RuntimeError(
67 | 'Cannot return a coreapi object from a JSON view. '
68 | 'You should be using a schema renderer instead for this view.'
69 | )
70 | return super(JSONEncoder, self).default(obj)
71 |
--------------------------------------------------------------------------------
/tests/models.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | import uuid
4 |
5 | from django.db import models
6 | from django.utils.translation import ugettext_lazy as _
7 |
8 |
9 | class RESTFrameworkModel(models.Model):
10 | """
11 | Base for test models that sets app_label, so they play nicely.
12 | """
13 |
14 | class Meta:
15 | app_label = 'tests'
16 | abstract = True
17 |
18 |
19 | class BasicModel(RESTFrameworkModel):
20 | text = models.CharField(
21 | max_length=100,
22 | verbose_name=_("Text comes here"),
23 | help_text=_("Text description.")
24 | )
25 |
26 |
27 | class BaseFilterableItem(RESTFrameworkModel):
28 | text = models.CharField(max_length=100)
29 |
30 |
31 | class FilterableItem(BaseFilterableItem):
32 | decimal = models.DecimalField(max_digits=4, decimal_places=2)
33 | date = models.DateField()
34 |
35 |
36 | # Models for relations tests
37 | # ManyToMany
38 | class ManyToManyTarget(RESTFrameworkModel):
39 | name = models.CharField(max_length=100)
40 |
41 |
42 | class ManyToManySource(RESTFrameworkModel):
43 | name = models.CharField(max_length=100)
44 | targets = models.ManyToManyField(ManyToManyTarget, related_name='sources')
45 |
46 |
47 | # ForeignKey
48 | class ForeignKeyTarget(RESTFrameworkModel):
49 | name = models.CharField(max_length=100)
50 |
51 |
52 | class UUIDForeignKeyTarget(RESTFrameworkModel):
53 | uuid = models.UUIDField(primary_key=True, default=uuid.uuid4)
54 | name = models.CharField(max_length=100)
55 |
56 |
57 | class ForeignKeySource(RESTFrameworkModel):
58 | name = models.CharField(max_length=100)
59 | target = models.ForeignKey(ForeignKeyTarget, related_name='sources',
60 | help_text='Target', verbose_name='Target',
61 | on_delete=models.CASCADE)
62 |
63 |
64 | # Nullable ForeignKey
65 | class NullableForeignKeySource(RESTFrameworkModel):
66 | name = models.CharField(max_length=100)
67 | target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
68 | related_name='nullable_sources',
69 | verbose_name='Optional target object',
70 | on_delete=models.CASCADE)
71 |
72 |
73 | class NullableUUIDForeignKeySource(RESTFrameworkModel):
74 | name = models.CharField(max_length=100)
75 | target = models.ForeignKey(ForeignKeyTarget, null=True, blank=True,
76 | related_name='nullable_sources',
77 | verbose_name='Optional target object',
78 | on_delete=models.CASCADE)
79 |
80 |
81 | # OneToOne
82 | class OneToOneTarget(RESTFrameworkModel):
83 | name = models.CharField(max_length=100)
84 |
85 |
86 | class NullableOneToOneSource(RESTFrameworkModel):
87 | name = models.CharField(max_length=100)
88 | target = models.OneToOneField(
89 | OneToOneTarget, null=True, blank=True,
90 | related_name='nullable_source', on_delete=models.CASCADE)
91 |
--------------------------------------------------------------------------------
/rest_framework/mixins.py:
--------------------------------------------------------------------------------
1 | """
2 | Basic building blocks for generic class based views.
3 |
4 | We don't bind behaviour to http method handlers yet,
5 | which allows mixin classes to be composed in interesting ways.
6 | """
7 | from __future__ import unicode_literals
8 |
9 | from rest_framework import status
10 | from rest_framework.response import Response
11 | from rest_framework.settings import api_settings
12 |
13 |
14 | class CreateModelMixin(object):
15 | """
16 | Create a model instance.
17 | """
18 | def create(self, request, *args, **kwargs):
19 | serializer = self.get_serializer(data=request.data)
20 | serializer.is_valid(raise_exception=True)
21 | self.perform_create(serializer)
22 | headers = self.get_success_headers(serializer.data)
23 | return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
24 |
25 | def perform_create(self, serializer):
26 | serializer.save()
27 |
28 | def get_success_headers(self, data):
29 | try:
30 | return {'Location': data[api_settings.URL_FIELD_NAME]}
31 | except (TypeError, KeyError):
32 | return {}
33 |
34 |
35 | class ListModelMixin(object):
36 | """
37 | List a queryset.
38 | """
39 | def list(self, request, *args, **kwargs):
40 | queryset = self.filter_queryset(self.get_queryset())
41 |
42 | page = self.paginate_queryset(queryset)
43 | if page is not None:
44 | serializer = self.get_serializer(page, many=True)
45 | return self.get_paginated_response(serializer.data)
46 |
47 | serializer = self.get_serializer(queryset, many=True)
48 | return Response(serializer.data)
49 |
50 |
51 | class RetrieveModelMixin(object):
52 | """
53 | Retrieve a model instance.
54 | """
55 | def retrieve(self, request, *args, **kwargs):
56 | instance = self.get_object()
57 | serializer = self.get_serializer(instance)
58 | return Response(serializer.data)
59 |
60 |
61 | class UpdateModelMixin(object):
62 | """
63 | Update a model instance.
64 | """
65 | def update(self, request, *args, **kwargs):
66 | partial = kwargs.pop('partial', False)
67 | instance = self.get_object()
68 | serializer = self.get_serializer(instance, data=request.data, partial=partial)
69 | serializer.is_valid(raise_exception=True)
70 | self.perform_update(serializer)
71 | return Response(serializer.data)
72 |
73 | def perform_update(self, serializer):
74 | serializer.save()
75 |
76 | def partial_update(self, request, *args, **kwargs):
77 | kwargs['partial'] = True
78 | return self.update(request, *args, **kwargs)
79 |
80 |
81 | class DestroyModelMixin(object):
82 | """
83 | Destroy a model instance.
84 | """
85 | def destroy(self, request, *args, **kwargs):
86 | instance = self.get_object()
87 | self.perform_destroy(instance)
88 | return Response(status=status.HTTP_204_NO_CONTENT)
89 |
90 | def perform_destroy(self, instance):
91 | instance.delete()
92 |
--------------------------------------------------------------------------------
/rest_framework/utils/mediatypes.py:
--------------------------------------------------------------------------------
1 | """
2 | Handling of media types, as found in HTTP Content-Type and Accept headers.
3 |
4 | See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
5 | """
6 | from __future__ import unicode_literals
7 |
8 | from django.http.multipartparser import parse_header
9 | from django.utils.encoding import python_2_unicode_compatible
10 |
11 | from rest_framework import HTTP_HEADER_ENCODING
12 |
13 |
14 | def media_type_matches(lhs, rhs):
15 | """
16 | Returns ``True`` if the media type in the first argument <= the
17 | media type in the second argument. The media types are strings
18 | as described by the HTTP spec.
19 |
20 | Valid media type strings include:
21 |
22 | 'application/json; indent=4'
23 | 'application/json'
24 | 'text/*'
25 | '*/*'
26 | """
27 | lhs = _MediaType(lhs)
28 | rhs = _MediaType(rhs)
29 | return lhs.match(rhs)
30 |
31 |
32 | def order_by_precedence(media_type_lst):
33 | """
34 | Returns a list of sets of media type strings, ordered by precedence.
35 | Precedence is determined by how specific a media type is:
36 |
37 | 3. 'type/subtype; param=val'
38 | 2. 'type/subtype'
39 | 1. 'type/*'
40 | 0. '*/*'
41 | """
42 | ret = [set(), set(), set(), set()]
43 | for media_type in media_type_lst:
44 | precedence = _MediaType(media_type).precedence
45 | ret[3 - precedence].add(media_type)
46 | return [media_types for media_types in ret if media_types]
47 |
48 |
49 | @python_2_unicode_compatible
50 | class _MediaType(object):
51 | def __init__(self, media_type_str):
52 | if media_type_str is None:
53 | media_type_str = ''
54 | self.orig = media_type_str
55 | self.full_type, self.params = parse_header(media_type_str.encode(HTTP_HEADER_ENCODING))
56 | self.main_type, sep, self.sub_type = self.full_type.partition('/')
57 |
58 | def match(self, other):
59 | """Return true if this MediaType satisfies the given MediaType."""
60 | for key in self.params.keys():
61 | if key != 'q' and other.params.get(key, None) != self.params.get(key, None):
62 | return False
63 |
64 | if self.sub_type != '*' and other.sub_type != '*' and other.sub_type != self.sub_type:
65 | return False
66 |
67 | if self.main_type != '*' and other.main_type != '*' and other.main_type != self.main_type:
68 | return False
69 |
70 | return True
71 |
72 | @property
73 | def precedence(self):
74 | """
75 | Return a precedence level from 0-3 for the media type given how specific it is.
76 | """
77 | if self.main_type == '*':
78 | return 0
79 | elif self.sub_type == '*':
80 | return 1
81 | elif not self.params or list(self.params.keys()) == ['q']:
82 | return 2
83 | return 3
84 |
85 | def __str__(self):
86 | ret = "%s/%s" % (self.main_type, self.sub_type)
87 | for key, val in self.params.items():
88 | ret += "; %s=%s" % (key, val)
89 | return ret
90 |
--------------------------------------------------------------------------------
/rest_framework/templates/rest_framework/login_base.html:
--------------------------------------------------------------------------------
1 | {% extends "rest_framework/base.html" %}
2 | {% load staticfiles %}
3 | {% load rest_framework %}
4 |
5 | {% block body %}
6 |
7 |
8 |
9 |
10 |
11 |
12 | {% block branding %}
Django REST framework
{% endblock %}
13 |
14 |
15 |
16 |
17 |
18 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | {% endblock %}
67 |
--------------------------------------------------------------------------------
/docs/topics/ajax-csrf-cors.md:
--------------------------------------------------------------------------------
1 | # Working with AJAX, CSRF & CORS
2 |
3 | > "Take a close look at possible CSRF / XSRF vulnerabilities on your own websites. They're the worst kind of vulnerability — very easy to exploit by attackers, yet not so intuitively easy to understand for software developers, at least until you've been bitten by one."
4 | >
5 | > — [Jeff Atwood][cite]
6 |
7 | ## Javascript clients
8 |
9 | If you’re building a JavaScript client to interface with your Web API, you'll need to consider if the client can use the same authentication policy that is used by the rest of the website, and also determine if you need to use CSRF tokens or CORS headers.
10 |
11 | AJAX requests that are made within the same context as the API they are interacting with will typically use `SessionAuthentication`. This ensures that once a user has logged in, any AJAX requests made can be authenticated using the same session-based authentication that is used for the rest of the website.
12 |
13 | AJAX requests that are made on a different site from the API they are communicating with will typically need to use a non-session-based authentication scheme, such as `TokenAuthentication`.
14 |
15 | ## CSRF protection
16 |
17 | [Cross Site Request Forgery][csrf] protection is a mechanism of guarding against a particular type of attack, which can occur when a user has not logged out of a web site, and continues to have a valid session. In this circumstance a malicious site may be able to perform actions against the target site, within the context of the logged-in session.
18 |
19 | To guard against these type of attacks, you need to do two things:
20 |
21 | 1. Ensure that the 'safe' HTTP operations, such as `GET`, `HEAD` and `OPTIONS` cannot be used to alter any server-side state.
22 | 2. Ensure that any 'unsafe' HTTP operations, such as `POST`, `PUT`, `PATCH` and `DELETE`, always require a valid CSRF token.
23 |
24 | If you're using `SessionAuthentication` you'll need to include valid CSRF tokens for any `POST`, `PUT`, `PATCH` or `DELETE` operations.
25 |
26 | In order to make AJAX requests, you need to include CSRF token in the HTTP header, as [described in the Django documentation][csrf-ajax].
27 |
28 | ## CORS
29 |
30 | [Cross-Origin Resource Sharing][cors] is a mechanism for allowing clients to interact with APIs that are hosted on a different domain. CORS works by requiring the server to include a specific set of headers that allow a browser to determine if and when cross-domain requests should be allowed.
31 |
32 | The best way to deal with CORS in REST framework is to add the required response headers in middleware. This ensures that CORS is supported transparently, without having to change any behavior in your views.
33 |
34 | [Otto Yiu][ottoyiu] maintains the [django-cors-headers] package, which is known to work correctly with REST framework APIs.
35 |
36 | [cite]: http://www.codinghorror.com/blog/2008/10/preventing-csrf-and-xsrf-attacks.html
37 | [csrf]: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)
38 | [csrf-ajax]: https://docs.djangoproject.com/en/dev/ref/csrf/#ajax
39 | [cors]: http://www.w3.org/TR/cors/
40 | [ottoyiu]: https://github.com/ottoyiu/
41 | [django-cors-headers]: https://github.com/ottoyiu/django-cors-headers/
42 |
--------------------------------------------------------------------------------
/rest_framework/static/rest_framework/js/ajax-form.js:
--------------------------------------------------------------------------------
1 | function replaceDocument(docString) {
2 | var doc = document.open("text/html");
3 |
4 | doc.write(docString);
5 | doc.close();
6 | }
7 |
8 | function doAjaxSubmit(e) {
9 | var form = $(this);
10 | var btn = $(this.clk);
11 | var method = (
12 | btn.data('method') ||
13 | form.data('method') ||
14 | form.attr('method') || 'GET'
15 | ).toUpperCase();
16 |
17 | if (method === 'GET') {
18 | // GET requests can always use standard form submits.
19 | return;
20 | }
21 |
22 | var contentType =
23 | form.find('input[data-override="content-type"]').val() ||
24 | form.find('select[data-override="content-type"] option:selected').text();
25 |
26 | if (method === 'POST' && !contentType) {
27 | // POST requests can use standard form submits, unless we have
28 | // overridden the content type.
29 | return;
30 | }
31 |
32 | // At this point we need to make an AJAX form submission.
33 | e.preventDefault();
34 |
35 | var url = form.attr('action');
36 | var data;
37 |
38 | if (contentType) {
39 | data = form.find('[data-override="content"]').val() || ''
40 | } else {
41 | contentType = form.attr('enctype') || form.attr('encoding')
42 |
43 | if (contentType === 'multipart/form-data') {
44 | if (!window.FormData) {
45 | alert('Your browser does not support AJAX multipart form submissions');
46 | return;
47 | }
48 |
49 | // Use the FormData API and allow the content type to be set automatically,
50 | // so it includes the boundary string.
51 | // See https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects
52 | contentType = false;
53 | data = new FormData(form[0]);
54 | } else {
55 | contentType = 'application/x-www-form-urlencoded; charset=UTF-8'
56 | data = form.serialize();
57 | }
58 | }
59 |
60 | var ret = $.ajax({
61 | url: url,
62 | method: method,
63 | data: data,
64 | contentType: contentType,
65 | processData: false,
66 | headers: {
67 | 'Accept': 'text/html; q=1.0, */*'
68 | },
69 | });
70 |
71 | ret.always(function(data, textStatus, jqXHR) {
72 | if (textStatus != 'success') {
73 | jqXHR = data;
74 | }
75 |
76 | var responseContentType = jqXHR.getResponseHeader("content-type") || "";
77 |
78 | if (responseContentType.toLowerCase().indexOf('text/html') === 0) {
79 | replaceDocument(jqXHR.responseText);
80 |
81 | try {
82 | // Modify the location and scroll to top, as if after page load.
83 | history.replaceState({}, '', url);
84 | scroll(0, 0);
85 | } catch (err) {
86 | // History API not supported, so redirect.
87 | window.location = url;
88 | }
89 | } else {
90 | // Not HTML content. We can't open this directly, so redirect.
91 | window.location = url;
92 | }
93 | });
94 |
95 | return ret;
96 | }
97 |
98 | function captureSubmittingElement(e) {
99 | var target = e.target;
100 | var form = this;
101 |
102 | form.clk = target;
103 | }
104 |
105 | $.fn.ajaxForm = function() {
106 | var options = {}
107 |
108 | return this
109 | .unbind('submit.form-plugin click.form-plugin')
110 | .bind('submit.form-plugin', options, doAjaxSubmit)
111 | .bind('click.form-plugin', options, captureSubmittingElement);
112 | };
113 |
--------------------------------------------------------------------------------
/rest_framework/utils/representation.py:
--------------------------------------------------------------------------------
1 | """
2 | Helper functions for creating user-friendly representations
3 | of serializer classes and serializer fields.
4 | """
5 | from __future__ import unicode_literals
6 |
7 | import re
8 |
9 | from django.db import models
10 | from django.utils.encoding import force_text
11 | from django.utils.functional import Promise
12 |
13 | from rest_framework.compat import get_names_and_managers, unicode_repr
14 |
15 |
16 | def manager_repr(value):
17 | model = value.model
18 | opts = model._meta
19 | for manager_name, manager_instance in get_names_and_managers(opts):
20 | if manager_instance == value:
21 | return '%s.%s.all()' % (model._meta.object_name, manager_name)
22 | return repr(value)
23 |
24 |
25 | def smart_repr(value):
26 | if isinstance(value, models.Manager):
27 | return manager_repr(value)
28 |
29 | if isinstance(value, Promise) and value._delegate_text:
30 | value = force_text(value)
31 |
32 | value = unicode_repr(value)
33 |
34 | # Representations like u'help text'
35 | # should simply be presented as 'help text'
36 | if value.startswith("u'") and value.endswith("'"):
37 | return value[1:]
38 |
39 | # Representations like
40 | #
41 | # Should be presented as
42 | #
43 | value = re.sub(' at 0x[0-9A-Fa-f]{4,32}>', '>', value)
44 |
45 | return value
46 |
47 |
48 | def field_repr(field, force_many=False):
49 | kwargs = field._kwargs
50 | if force_many:
51 | kwargs = kwargs.copy()
52 | kwargs['many'] = True
53 | kwargs.pop('child', None)
54 |
55 | arg_string = ', '.join([smart_repr(val) for val in field._args])
56 | kwarg_string = ', '.join([
57 | '%s=%s' % (key, smart_repr(val))
58 | for key, val in sorted(kwargs.items())
59 | ])
60 | if arg_string and kwarg_string:
61 | arg_string += ', '
62 |
63 | if force_many:
64 | class_name = force_many.__class__.__name__
65 | else:
66 | class_name = field.__class__.__name__
67 |
68 | return "%s(%s%s)" % (class_name, arg_string, kwarg_string)
69 |
70 |
71 | def serializer_repr(serializer, indent, force_many=None):
72 | ret = field_repr(serializer, force_many) + ':'
73 | indent_str = ' ' * indent
74 |
75 | if force_many:
76 | fields = force_many.fields
77 | else:
78 | fields = serializer.fields
79 |
80 | for field_name, field in fields.items():
81 | ret += '\n' + indent_str + field_name + ' = '
82 | if hasattr(field, 'fields'):
83 | ret += serializer_repr(field, indent + 1)
84 | elif hasattr(field, 'child'):
85 | ret += list_repr(field, indent + 1)
86 | elif hasattr(field, 'child_relation'):
87 | ret += field_repr(field.child_relation, force_many=field.child_relation)
88 | else:
89 | ret += field_repr(field)
90 |
91 | if serializer.validators:
92 | ret += '\n' + indent_str + 'class Meta:'
93 | ret += '\n' + indent_str + ' validators = ' + smart_repr(serializer.validators)
94 |
95 | return ret
96 |
97 |
98 | def list_repr(serializer, indent):
99 | child = serializer.child
100 | if hasattr(child, 'fields'):
101 | return serializer_repr(serializer, indent, force_many=child)
102 | return field_repr(serializer)
103 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: Django REST framework
2 | site_url: http://www.django-rest-framework.org/
3 | site_description: Django REST framework - Web APIs for Django
4 |
5 | repo_url: https://github.com/tomchristie/django-rest-framework
6 |
7 | theme_dir: docs_theme
8 |
9 | markdown_extensions:
10 | - toc:
11 | anchorlink: True
12 |
13 | pages:
14 | - Home: 'index.md'
15 | - Tutorial:
16 | - 'Quickstart': 'tutorial/quickstart.md'
17 | - '1 - Serialization': 'tutorial/1-serialization.md'
18 | - '2 - Requests and responses': 'tutorial/2-requests-and-responses.md'
19 | - '3 - Class based views': 'tutorial/3-class-based-views.md'
20 | - '4 - Authentication and permissions': 'tutorial/4-authentication-and-permissions.md'
21 | - '5 - Relationships and hyperlinked APIs': 'tutorial/5-relationships-and-hyperlinked-apis.md'
22 | - '6 - Viewsets and routers': 'tutorial/6-viewsets-and-routers.md'
23 | - '7 - Schemas and client libraries': 'tutorial/7-schemas-and-client-libraries.md'
24 | - API Guide:
25 | - 'Requests': 'api-guide/requests.md'
26 | - 'Responses': 'api-guide/responses.md'
27 | - 'Views': 'api-guide/views.md'
28 | - 'Generic views': 'api-guide/generic-views.md'
29 | - 'Viewsets': 'api-guide/viewsets.md'
30 | - 'Routers': 'api-guide/routers.md'
31 | - 'Parsers': 'api-guide/parsers.md'
32 | - 'Renderers': 'api-guide/renderers.md'
33 | - 'Serializers': 'api-guide/serializers.md'
34 | - 'Serializer fields': 'api-guide/fields.md'
35 | - 'Serializer relations': 'api-guide/relations.md'
36 | - 'Validators': 'api-guide/validators.md'
37 | - 'Authentication': 'api-guide/authentication.md'
38 | - 'Permissions': 'api-guide/permissions.md'
39 | - 'Throttling': 'api-guide/throttling.md'
40 | - 'Filtering': 'api-guide/filtering.md'
41 | - 'Pagination': 'api-guide/pagination.md'
42 | - 'Versioning': 'api-guide/versioning.md'
43 | - 'Content negotiation': 'api-guide/content-negotiation.md'
44 | - 'Metadata': 'api-guide/metadata.md'
45 | - 'Schemas': 'api-guide/schemas.md'
46 | - 'Format suffixes': 'api-guide/format-suffixes.md'
47 | - 'Returning URLs': 'api-guide/reverse.md'
48 | - 'Exceptions': 'api-guide/exceptions.md'
49 | - 'Status codes': 'api-guide/status-codes.md'
50 | - 'Testing': 'api-guide/testing.md'
51 | - 'Settings': 'api-guide/settings.md'
52 | - Topics:
53 | - 'Documenting your API': 'topics/documenting-your-api.md'
54 | - 'API Clients': 'topics/api-clients.md'
55 | - 'Internationalization': 'topics/internationalization.md'
56 | - 'AJAX, CSRF & CORS': 'topics/ajax-csrf-cors.md'
57 | - 'HTML & Forms': 'topics/html-and-forms.md'
58 | - 'Browser Enhancements': 'topics/browser-enhancements.md'
59 | - 'The Browsable API': 'topics/browsable-api.md'
60 | - 'REST, Hypermedia & HATEOAS': 'topics/rest-hypermedia-hateoas.md'
61 | - 'Third Party Resources': 'topics/third-party-resources.md'
62 | - 'Contributing to REST framework': 'topics/contributing.md'
63 | - 'Project management': 'topics/project-management.md'
64 | - '3.0 Announcement': 'topics/3.0-announcement.md'
65 | - '3.1 Announcement': 'topics/3.1-announcement.md'
66 | - '3.2 Announcement': 'topics/3.2-announcement.md'
67 | - '3.3 Announcement': 'topics/3.3-announcement.md'
68 | - '3.4 Announcement': 'topics/3.4-announcement.md'
69 | - 'Kickstarter Announcement': 'topics/kickstarter-announcement.md'
70 | - 'Mozilla Grant': 'topics/mozilla-grant.md'
71 | - 'Funding': 'topics/funding.md'
72 | - 'Release Notes': 'topics/release-notes.md'
73 |
--------------------------------------------------------------------------------
/docs/topics/rest-hypermedia-hateoas.md:
--------------------------------------------------------------------------------
1 | # REST, Hypermedia & HATEOAS
2 |
3 | > You keep using that word "REST". I do not think it means what you think it means.
4 | >
5 | > — Mike Amundsen, [REST fest 2012 keynote][cite].
6 |
7 | First off, the disclaimer. The name "Django REST framework" was decided back in early 2011 and was chosen simply to sure the project would be easily found by developers. Throughout the documentation we try to use the more simple and technically correct terminology of "Web APIs".
8 |
9 | If you are serious about designing a Hypermedia API, you should look to resources outside of this documentation to help inform your design choices.
10 |
11 | The following fall into the "required reading" category.
12 |
13 | * Roy Fielding's dissertation - [Architectural Styles and
14 | the Design of Network-based Software Architectures][dissertation].
15 | * Roy Fielding's "[REST APIs must be hypertext-driven][hypertext-driven]" blog post.
16 | * Leonard Richardson & Mike Amundsen's [RESTful Web APIs][restful-web-apis].
17 | * Mike Amundsen's [Building Hypermedia APIs with HTML5 and Node][building-hypermedia-apis].
18 | * Steve Klabnik's [Designing Hypermedia APIs][designing-hypermedia-apis].
19 | * The [Richardson Maturity Model][maturitymodel].
20 |
21 | For a more thorough background, check out Klabnik's [Hypermedia API reading list][readinglist].
22 |
23 | ## Building Hypermedia APIs with REST framework
24 |
25 | REST framework is an agnostic Web API toolkit. It does help guide you towards building well-connected APIs, and makes it easy to design appropriate media types, but it does not strictly enforce any particular design style.
26 |
27 | ## What REST framework provides.
28 |
29 | It is self evident that REST framework makes it possible to build Hypermedia APIs. The browsable API that it offers is built on HTML - the hypermedia language of the web.
30 |
31 | REST framework also includes [serialization] and [parser]/[renderer] components that make it easy to build appropriate media types, [hyperlinked relations][fields] for building well-connected systems, and great support for [content negotiation][conneg].
32 |
33 | ## What REST framework doesn't provide.
34 |
35 | What REST framework doesn't do is give you machine readable hypermedia formats such as [HAL][hal], [Collection+JSON][collection], [JSON API][json-api] or HTML [microformats] by default, or the ability to auto-magically create fully HATEOAS style APIs that include hypermedia-based form descriptions and semantically labelled hyperlinks. Doing so would involve making opinionated choices about API design that should really remain outside of the framework's scope.
36 |
37 | [cite]: http://vimeo.com/channels/restfest/page:2
38 | [dissertation]: http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm
39 | [hypertext-driven]: http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
40 | [restful-web-apis]: http://restfulwebapis.org/
41 | [building-hypermedia-apis]: http://www.amazon.com/Building-Hypermedia-APIs-HTML5-Node/dp/1449306578
42 | [designing-hypermedia-apis]: http://designinghypermediaapis.com/
43 | [restisover]: http://blog.steveklabnik.com/posts/2012-02-23-rest-is-over
44 | [readinglist]: http://blog.steveklabnik.com/posts/2012-02-27-hypermedia-api-reading-list
45 | [maturitymodel]: http://martinfowler.com/articles/richardsonMaturityModel.html
46 |
47 | [hal]: http://stateless.co/hal_specification.html
48 | [collection]: http://www.amundsen.com/media-types/collection/
49 | [json-api]: http://jsonapi.org/
50 | [microformats]: http://microformats.org/wiki/Main_Page
51 | [serialization]: ../api-guide/serializers.md
52 | [parser]: ../api-guide/parsers.md
53 | [renderer]: ../api-guide/renderers.md
54 | [fields]: ../api-guide/fields.md
55 | [conneg]: ../api-guide/content-negotiation.md
56 |
--------------------------------------------------------------------------------
/tests/test_relations_generic.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | from django.contrib.contenttypes.fields import (
4 | GenericForeignKey, GenericRelation
5 | )
6 | from django.contrib.contenttypes.models import ContentType
7 | from django.db import models
8 | from django.test import TestCase
9 | from django.utils.encoding import python_2_unicode_compatible
10 |
11 | from rest_framework import serializers
12 |
13 |
14 | @python_2_unicode_compatible
15 | class Tag(models.Model):
16 | """
17 | Tags have a descriptive slug, and are attached to an arbitrary object.
18 | """
19 | tag = models.SlugField()
20 | content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
21 | object_id = models.PositiveIntegerField()
22 | tagged_item = GenericForeignKey('content_type', 'object_id')
23 |
24 | def __str__(self):
25 | return self.tag
26 |
27 |
28 | @python_2_unicode_compatible
29 | class Bookmark(models.Model):
30 | """
31 | A URL bookmark that may have multiple tags attached.
32 | """
33 | url = models.URLField()
34 | tags = GenericRelation(Tag)
35 |
36 | def __str__(self):
37 | return 'Bookmark: %s' % self.url
38 |
39 |
40 | @python_2_unicode_compatible
41 | class Note(models.Model):
42 | """
43 | A textual note that may have multiple tags attached.
44 | """
45 | text = models.TextField()
46 | tags = GenericRelation(Tag)
47 |
48 | def __str__(self):
49 | return 'Note: %s' % self.text
50 |
51 |
52 | class TestGenericRelations(TestCase):
53 | def setUp(self):
54 | self.bookmark = Bookmark.objects.create(url='https://www.djangoproject.com/')
55 | Tag.objects.create(tagged_item=self.bookmark, tag='django')
56 | Tag.objects.create(tagged_item=self.bookmark, tag='python')
57 | self.note = Note.objects.create(text='Remember the milk')
58 | Tag.objects.create(tagged_item=self.note, tag='reminder')
59 |
60 | def test_generic_relation(self):
61 | """
62 | Test a relationship that spans a GenericRelation field.
63 | IE. A reverse generic relationship.
64 | """
65 |
66 | class BookmarkSerializer(serializers.ModelSerializer):
67 | tags = serializers.StringRelatedField(many=True)
68 |
69 | class Meta:
70 | model = Bookmark
71 | fields = ('tags', 'url')
72 |
73 | serializer = BookmarkSerializer(self.bookmark)
74 | expected = {
75 | 'tags': ['django', 'python'],
76 | 'url': 'https://www.djangoproject.com/'
77 | }
78 | self.assertEqual(serializer.data, expected)
79 |
80 | def test_generic_fk(self):
81 | """
82 | Test a relationship that spans a GenericForeignKey field.
83 | IE. A forward generic relationship.
84 | """
85 |
86 | class TagSerializer(serializers.ModelSerializer):
87 | tagged_item = serializers.StringRelatedField()
88 |
89 | class Meta:
90 | model = Tag
91 | fields = ('tag', 'tagged_item')
92 |
93 | serializer = TagSerializer(Tag.objects.all(), many=True)
94 | expected = [
95 | {
96 | 'tag': 'django',
97 | 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
98 | },
99 | {
100 | 'tag': 'python',
101 | 'tagged_item': 'Bookmark: https://www.djangoproject.com/'
102 | },
103 | {
104 | 'tag': 'reminder',
105 | 'tagged_item': 'Note: Remember the milk'
106 | }
107 | ]
108 | self.assertEqual(serializer.data, expected)
109 |
--------------------------------------------------------------------------------
/runtests.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env python
2 | from __future__ import print_function
3 |
4 | import os
5 | import subprocess
6 | import sys
7 |
8 | import pytest
9 |
10 | PYTEST_ARGS = {
11 | 'default': ['tests', '--tb=short', '-s', '-rw'],
12 | 'fast': ['tests', '--tb=short', '-q', '-s', '-rw'],
13 | }
14 |
15 | FLAKE8_ARGS = ['rest_framework', 'tests', '--ignore=E501']
16 |
17 | ISORT_ARGS = ['--recursive', '--check-only', '-o' 'uritemplate', '-p', 'tests', 'rest_framework', 'tests']
18 |
19 | sys.path.append(os.path.dirname(__file__))
20 |
21 |
22 | def exit_on_failure(ret, message=None):
23 | if ret:
24 | sys.exit(ret)
25 |
26 |
27 | def flake8_main(args):
28 | print('Running flake8 code linting')
29 | ret = subprocess.call(['flake8'] + args)
30 | print('flake8 failed' if ret else 'flake8 passed')
31 | return ret
32 |
33 |
34 | def isort_main(args):
35 | print('Running isort code checking')
36 | ret = subprocess.call(['isort'] + args)
37 |
38 | if ret:
39 | print('isort failed: Some modules have incorrectly ordered imports. Fix by running `isort --recursive .`')
40 | else:
41 | print('isort passed')
42 |
43 | return ret
44 |
45 |
46 | def split_class_and_function(string):
47 | class_string, function_string = string.split('.', 1)
48 | return "%s and %s" % (class_string, function_string)
49 |
50 |
51 | def is_function(string):
52 | # `True` if it looks like a test function is included in the string.
53 | return string.startswith('test_') or '.test_' in string
54 |
55 |
56 | def is_class(string):
57 | # `True` if first character is uppercase - assume it's a class name.
58 | return string[0] == string[0].upper()
59 |
60 |
61 | if __name__ == "__main__":
62 | try:
63 | sys.argv.remove('--nolint')
64 | except ValueError:
65 | run_flake8 = True
66 | run_isort = True
67 | else:
68 | run_flake8 = False
69 | run_isort = False
70 |
71 | try:
72 | sys.argv.remove('--lintonly')
73 | except ValueError:
74 | run_tests = True
75 | else:
76 | run_tests = False
77 |
78 | try:
79 | sys.argv.remove('--fast')
80 | except ValueError:
81 | style = 'default'
82 | else:
83 | style = 'fast'
84 | run_flake8 = False
85 | run_isort = False
86 |
87 | if len(sys.argv) > 1:
88 | pytest_args = sys.argv[1:]
89 | first_arg = pytest_args[0]
90 |
91 | try:
92 | pytest_args.remove('--coverage')
93 | except ValueError:
94 | pass
95 | else:
96 | pytest_args = [
97 | '--cov-report',
98 | 'xml',
99 | '--cov',
100 | 'rest_framework'] + pytest_args
101 |
102 | if first_arg.startswith('-'):
103 | # `runtests.py [flags]`
104 | pytest_args = ['tests'] + pytest_args
105 | elif is_class(first_arg) and is_function(first_arg):
106 | # `runtests.py TestCase.test_function [flags]`
107 | expression = split_class_and_function(first_arg)
108 | pytest_args = ['tests', '-k', expression] + pytest_args[1:]
109 | elif is_class(first_arg) or is_function(first_arg):
110 | # `runtests.py TestCase [flags]`
111 | # `runtests.py test_function [flags]`
112 | pytest_args = ['tests', '-k', pytest_args[0]] + pytest_args[1:]
113 | else:
114 | pytest_args = PYTEST_ARGS[style]
115 |
116 | if run_tests:
117 | exit_on_failure(pytest.main(pytest_args))
118 |
119 | if run_flake8:
120 | exit_on_failure(flake8_main(FLAKE8_ARGS))
121 |
122 | if run_isort:
123 | exit_on_failure(isort_main(ISORT_ARGS))
124 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | import os
4 | import re
5 | import shutil
6 | import sys
7 | from io import open
8 |
9 | from setuptools import setup
10 |
11 | try:
12 | from pypandoc import convert
13 |
14 | def read_md(f):
15 | return convert(f, 'rst')
16 | except ImportError:
17 | print("warning: pypandoc module not found, could not convert Markdown to RST")
18 |
19 | def read_md(f):
20 | return open(f, 'r', encoding='utf-8').read()
21 |
22 |
23 | def get_version(package):
24 | """
25 | Return package version as listed in `__version__` in `init.py`.
26 | """
27 | init_py = open(os.path.join(package, '__init__.py')).read()
28 | return re.search("__version__ = ['\"]([^'\"]+)['\"]", init_py).group(1)
29 |
30 |
31 | def get_packages(package):
32 | """
33 | Return root package and all sub-packages.
34 | """
35 | return [dirpath
36 | for dirpath, dirnames, filenames in os.walk(package)
37 | if os.path.exists(os.path.join(dirpath, '__init__.py'))]
38 |
39 |
40 | def get_package_data(package):
41 | """
42 | Return all files under the root package, that are not in a
43 | package themselves.
44 | """
45 | walk = [(dirpath.replace(package + os.sep, '', 1), filenames)
46 | for dirpath, dirnames, filenames in os.walk(package)
47 | if not os.path.exists(os.path.join(dirpath, '__init__.py'))]
48 |
49 | filepaths = []
50 | for base, filenames in walk:
51 | filepaths.extend([os.path.join(base, filename)
52 | for filename in filenames])
53 | return {package: filepaths}
54 |
55 |
56 | version = get_version('rest_framework')
57 |
58 |
59 | if sys.argv[-1] == 'publish':
60 | try:
61 | import pypandoc
62 | except ImportError:
63 | print("pypandoc not installed.\nUse `pip install pypandoc`.\nExiting.")
64 | if os.system("pip freeze | grep wheel"):
65 | print("wheel not installed.\nUse `pip install wheel`.\nExiting.")
66 | sys.exit()
67 | if os.system("pip freeze | grep twine"):
68 | print("twine not installed.\nUse `pip install twine`.\nExiting.")
69 | sys.exit()
70 | os.system("python setup.py sdist bdist_wheel")
71 | os.system("twine upload dist/*")
72 | print("You probably want to also tag the version now:")
73 | print(" git tag -a %s -m 'version %s'" % (version, version))
74 | print(" git push --tags")
75 | shutil.rmtree('dist')
76 | shutil.rmtree('build')
77 | shutil.rmtree('djangorestframework.egg-info')
78 | sys.exit()
79 |
80 |
81 | setup(
82 | name='djangorestframework',
83 | version=version,
84 | url='http://www.django-rest-framework.org',
85 | license='BSD',
86 | description='Web APIs for Django, made easy.',
87 | long_description=read_md('README.md'),
88 | author='Tom Christie',
89 | author_email='tom@tomchristie.com', # SEE NOTE BELOW (*)
90 | packages=get_packages('rest_framework'),
91 | package_data=get_package_data('rest_framework'),
92 | install_requires=[],
93 | zip_safe=False,
94 | classifiers=[
95 | 'Development Status :: 5 - Production/Stable',
96 | 'Environment :: Web Environment',
97 | 'Framework :: Django',
98 | 'Intended Audience :: Developers',
99 | 'License :: OSI Approved :: BSD License',
100 | 'Operating System :: OS Independent',
101 | 'Programming Language :: Python',
102 | 'Programming Language :: Python :: 3',
103 | 'Topic :: Internet :: WWW/HTTP',
104 | ]
105 | )
106 |
107 | # (*) Please direct queries to the discussion group, rather than to me directly
108 | # Doing so helps ensure your question is helpful to other users.
109 | # Queries directly to my email are likely to receive a canned response.
110 | #
111 | # Many thanks for your understanding.
112 |
--------------------------------------------------------------------------------
/tests/test_description.py:
--------------------------------------------------------------------------------
1 | # -- coding: utf-8 --
2 |
3 | from __future__ import unicode_literals
4 |
5 | from django.test import TestCase
6 | from django.utils.encoding import python_2_unicode_compatible
7 |
8 | from rest_framework.compat import apply_markdown
9 | from rest_framework.utils.formatting import dedent
10 | from rest_framework.views import APIView
11 |
12 |
13 | # We check that docstrings get nicely un-indented.
14 | DESCRIPTION = """an example docstring
15 | ====================
16 |
17 | * list
18 | * list
19 |
20 | another header
21 | --------------
22 |
23 | code block
24 |
25 | indented
26 |
27 | # hash style header #"""
28 |
29 | # If markdown is installed we also test it's working
30 | # (and that our wrapped forces '=' to h2 and '-' to h3)
31 |
32 | # We support markdown < 2.1 and markdown >= 2.1
33 | MARKED_DOWN_lt_21 = """
an example docstring
34 |
35 |
list
36 |
list
37 |
38 |
another header
39 |
code block
40 |
41 |
indented
42 |
hash style header
"""
43 |
44 | MARKED_DOWN_gte_21 = """
an example docstring
45 |
46 |
list
47 |
list
48 |
49 |
another header
50 |
code block
51 |
52 |
indented
53 |
hash style header
"""
54 |
55 |
56 | class TestViewNamesAndDescriptions(TestCase):
57 | def test_view_name_uses_class_name(self):
58 | """
59 | Ensure view names are based on the class name.
60 | """
61 | class MockView(APIView):
62 | pass
63 | self.assertEqual(MockView().get_view_name(), 'Mock')
64 |
65 | def test_view_description_uses_docstring(self):
66 | """Ensure view descriptions are based on the docstring."""
67 | class MockView(APIView):
68 | """an example docstring
69 | ====================
70 |
71 | * list
72 | * list
73 |
74 | another header
75 | --------------
76 |
77 | code block
78 |
79 | indented
80 |
81 | # hash style header #"""
82 |
83 | self.assertEqual(MockView().get_view_description(), DESCRIPTION)
84 |
85 | def test_view_description_can_be_empty(self):
86 | """
87 | Ensure that if a view has no docstring,
88 | then it's description is the empty string.
89 | """
90 | class MockView(APIView):
91 | pass
92 | self.assertEqual(MockView().get_view_description(), '')
93 |
94 | def test_view_description_can_be_promise(self):
95 | """
96 | Ensure a view may have a docstring that is actually a lazily evaluated
97 | class that can be converted to a string.
98 |
99 | See: https://github.com/tomchristie/django-rest-framework/issues/1708
100 | """
101 | # use a mock object instead of gettext_lazy to ensure that we can't end
102 | # up with a test case string in our l10n catalog
103 | @python_2_unicode_compatible
104 | class MockLazyStr(object):
105 | def __init__(self, string):
106 | self.s = string
107 |
108 | def __str__(self):
109 | return self.s
110 |
111 | class MockView(APIView):
112 | __doc__ = MockLazyStr("a gettext string")
113 |
114 | self.assertEqual(MockView().get_view_description(), 'a gettext string')
115 |
116 | def test_markdown(self):
117 | """
118 | Ensure markdown to HTML works as expected.
119 | """
120 | if apply_markdown:
121 | gte_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_gte_21
122 | lt_21_match = apply_markdown(DESCRIPTION) == MARKED_DOWN_lt_21
123 | self.assertTrue(gte_21_match or lt_21_match)
124 |
125 |
126 | def test_dedent_tabs():
127 | assert dedent("\tfirst string\n\n\tsecond string") == 'first string\n\n\tsecond string'
128 |
--------------------------------------------------------------------------------
/rest_framework/response.py:
--------------------------------------------------------------------------------
1 | """
2 | The Response class in REST framework is similar to HTTPResponse, except that
3 | it is initialized with unrendered data, instead of a pre-rendered string.
4 |
5 | The appropriate renderer is called during Django's template response rendering.
6 | """
7 | from __future__ import unicode_literals
8 |
9 | from django.template.response import SimpleTemplateResponse
10 | from django.utils import six
11 | from django.utils.six.moves.http_client import responses
12 |
13 | from rest_framework.serializers import Serializer
14 |
15 |
16 | class Response(SimpleTemplateResponse):
17 | """
18 | An HttpResponse that allows its data to be rendered into
19 | arbitrary media types.
20 | """
21 |
22 | def __init__(self, data=None, status=None,
23 | template_name=None, headers=None,
24 | exception=False, content_type=None):
25 | """
26 | Alters the init arguments slightly.
27 | For example, drop 'template_name', and instead use 'data'.
28 |
29 | Setting 'renderer' and 'media_type' will typically be deferred,
30 | For example being set automatically by the `APIView`.
31 | """
32 | super(Response, self).__init__(None, status=status)
33 |
34 | if isinstance(data, Serializer):
35 | msg = (
36 | 'You passed a Serializer instance as data, but '
37 | 'probably meant to pass serialized `.data` or '
38 | '`.error`. representation.'
39 | )
40 | raise AssertionError(msg)
41 |
42 | self.data = data
43 | self.template_name = template_name
44 | self.exception = exception
45 | self.content_type = content_type
46 |
47 | if headers:
48 | for name, value in six.iteritems(headers):
49 | self[name] = value
50 |
51 | @property
52 | def rendered_content(self):
53 | renderer = getattr(self, 'accepted_renderer', None)
54 | accepted_media_type = getattr(self, 'accepted_media_type', None)
55 | context = getattr(self, 'renderer_context', None)
56 |
57 | assert renderer, ".accepted_renderer not set on Response"
58 | assert accepted_media_type, ".accepted_media_type not set on Response"
59 | assert context is not None, ".renderer_context not set on Response"
60 | context['response'] = self
61 |
62 | media_type = renderer.media_type
63 | charset = renderer.charset
64 | content_type = self.content_type
65 |
66 | if content_type is None and charset is not None:
67 | content_type = "{0}; charset={1}".format(media_type, charset)
68 | elif content_type is None:
69 | content_type = media_type
70 | self['Content-Type'] = content_type
71 |
72 | ret = renderer.render(self.data, accepted_media_type, context)
73 | if isinstance(ret, six.text_type):
74 | assert charset, (
75 | 'renderer returned unicode, and did not specify '
76 | 'a charset value.'
77 | )
78 | return bytes(ret.encode(charset))
79 |
80 | if not ret:
81 | del self['Content-Type']
82 |
83 | return ret
84 |
85 | @property
86 | def status_text(self):
87 | """
88 | Returns reason text corresponding to our HTTP response status code.
89 | Provided for convenience.
90 | """
91 | # TODO: Deprecate and use a template tag instead
92 | # TODO: Status code text for RFC 6585 status codes
93 | return responses.get(self.status_code, '')
94 |
95 | def __getstate__(self):
96 | """
97 | Remove attributes from the response that shouldn't be cached.
98 | """
99 | state = super(Response, self).__getstate__()
100 | for key in (
101 | 'accepted_renderer', 'renderer_context', 'resolver_match',
102 | 'client', 'request', 'json', 'wsgi_request'
103 | ):
104 | if key in state:
105 | del state[key]
106 | state['_closable_objects'] = []
107 | return state
108 |
--------------------------------------------------------------------------------
/docs/topics/3.3-announcement.md:
--------------------------------------------------------------------------------
1 | # Django REST framework 3.3
2 |
3 | The 3.3 release marks the final work in the Kickstarter funded series. We'd like to offer a final resounding **thank you** to all our wonderful sponsors and supporters.
4 |
5 | The amount of work that has been achieved as a direct result of the funding is immense. We've added a huge amounts of new functionality, resolved nearly 2,000 tickets, and redesigned & refined large parts of the project.
6 |
7 | In order to continue driving REST framework forward, we'll shortly be announcing a new set of funding plans. Follow [@_tomchristie](https://twitter.com/_tomchristie) to keep up to date with these announcements, and be among the first set of sign ups.
8 |
9 | We strongly believe that collaboratively funded software development yields outstanding results for a relatively low investment-per-head. If you or your company use REST framework commercially, then we would strongly urge you to participate in this latest funding drive, and help us continue to build an increasingly polished & professional product.
10 |
11 | ---
12 |
13 | ## Release notes
14 |
15 | Significant new functionality in the 3.3 release includes:
16 |
17 | * Filters presented as HTML controls in the browsable API.
18 | * A [forms API][forms-api], allowing serializers to be rendered as HTML forms.
19 | * Django 1.9 support.
20 | * A [`JSONField` serializer field][jsonfield], corresponding to Django 1.9's Postgres `JSONField` model field.
21 | * Browsable API support [via AJAX][ajax-form], rather than server side request overloading.
22 |
23 | 
24 |
25 | *Example of the new filter controls*
26 |
27 | ---
28 |
29 | ## Supported versions
30 |
31 | This release drops support for Django 1.5 and 1.6. Django 1.7, 1.8 or 1.9 are now required.
32 |
33 | This brings our supported versions into line with Django's [currently supported versions][django-supported-versions]
34 |
35 | ## Deprecations
36 |
37 | The AJAX based support for the browsable API means that there are a number of internal cleanups in the `request` class. For the vast majority of developers this should largely remain transparent:
38 |
39 | * To support form based `PUT` and `DELETE`, or to support form content types such as JSON, you should now use the [AJAX forms][ajax-form] javascript library. This replaces the previous 'method and content type overloading' that required significant internal complexity to the request class.
40 | * The `accept` query parameter is no longer supported by the default content negotiation class. If you require it then you'll need to [use a custom content negotiation class](browser-enhancements.md#url-based-accept-headers).
41 | * The custom `HTTP_X_HTTP_METHOD_OVERRIDE` header is no longer supported by default. If you require it then you'll need to [use custom middleware](browser-enhancements.md#http-header-based-method-overriding).
42 |
43 | The following pagination view attributes and settings have been moved into attributes on the pagination class since 3.1. Their usage was formerly deprecated, and has now been removed entirely, in line with the deprecation policy.
44 |
45 | * `view.paginate_by` - Use `paginator.page_size` instead.
46 | * `view.page_query_param` - Use `paginator.page_query_param` instead.
47 | * `view.paginate_by_param` - Use `paginator.page_size_query_param` instead.
48 | * `view.max_paginate_by` - Use `paginator.max_page_size` instead.
49 | * `settings.PAGINATE_BY` - Use `paginator.page_size` instead.
50 | * `settings.PAGINATE_BY_PARAM` - Use `paginator.page_size_query_param` instead.
51 | * `settings.MAX_PAGINATE_BY` - Use `paginator.max_page_size` instead.
52 |
53 | The `ModelSerializer` and `HyperlinkedModelSerializer` classes should now include either a `fields` or `exclude` option, although the `fields = '__all__'` shortcut may be used. Failing to include either of these two options is currently pending deprecation, and will be removed entirely in the 3.5 release. This behavior brings `ModelSerializer` more closely in line with Django's `ModelForm` behavior.
54 |
55 | [forms-api]: html-and-forms.md
56 | [ajax-form]: https://github.com/tomchristie/ajax-form
57 | [jsonfield]: ../../api-guide/fields#jsonfield
58 | [django-supported-versions]: https://www.djangoproject.com/download/#supported-versions
--------------------------------------------------------------------------------