diff --git a/docs/api-guide/authentication.md b/docs/api-guide/authentication.md index 2344c68e3..3dc2acbc4 100644 --- a/docs/api-guide/authentication.md +++ b/docs/api-guide/authentication.md @@ -300,7 +300,7 @@ Add the package to your `INSTALLED_APPS` and modify your REST framework settings REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'oauth2_provider.ext.rest_framework.OAuth2Authentication', + 'oauth2_provider.contrib.rest_framework.OAuth2Authentication', ) } diff --git a/docs/api-guide/exceptions.md b/docs/api-guide/exceptions.md index 03f16222d..ba3d5af9b 100644 --- a/docs/api-guide/exceptions.md +++ b/docs/api-guide/exceptions.md @@ -230,5 +230,5 @@ The generic views use the `raise_exception=True` flag, which means that you can By default this exception results in a response with the HTTP status code "400 Bad Request". -[cite]: http://www.doughellmann.com/articles/how-tos/python-exception-handling/index.html +[cite]: https://doughellmann.com/blog/2009/06/19/python-exception-handling-techniques/ [authentication]: authentication.md diff --git a/docs/api-guide/generic-views.md b/docs/api-guide/generic-views.md index ae2705080..0170256f2 100644 --- a/docs/api-guide/generic-views.md +++ b/docs/api-guide/generic-views.md @@ -390,4 +390,4 @@ The [django-rest-framework-bulk package][django-rest-framework-bulk] implements [UpdateModelMixin]: #updatemodelmixin [DestroyModelMixin]: #destroymodelmixin [django-rest-framework-bulk]: https://github.com/miki725/django-rest-framework-bulk -[django-rest-multiple-models]: https://github.com/Axiologue/DjangoRestMultipleModels +[django-rest-multiple-models]: https://github.com/MattBroach/DjangoRestMultipleModels diff --git a/docs/api-guide/routers.md b/docs/api-guide/routers.md index 948bf8989..0c6aab152 100644 --- a/docs/api-guide/routers.md +++ b/docs/api-guide/routers.md @@ -194,7 +194,7 @@ As with `SimpleRouter` the trailing slashes on the URL routes can be removed by # Custom Routers -Implementing a custom router isn't something you'd need to do very often, but it can be useful if you have specific requirements about how the your URLs for your API are structured. Doing so allows you to encapsulate the URL structure in a reusable way that ensures you don't have to write your URL patterns explicitly for each new view. +Implementing a custom router isn't something you'd need to do very often, but it can be useful if you have specific requirements about how the URLs for your API are structured. Doing so allows you to encapsulate the URL structure in a reusable way that ensures you don't have to write your URL patterns explicitly for each new view. The simplest way to implement a custom router is to subclass one of the existing router classes. The `.routes` attribute is used to template the URL patterns that will be mapped to each viewset. The `.routes` attribute is a list of `Route` named tuples. diff --git a/docs/api-guide/schemas.md b/docs/api-guide/schemas.md index afa058d94..836ad4b6a 100644 --- a/docs/api-guide/schemas.md +++ b/docs/api-guide/schemas.md @@ -107,6 +107,8 @@ add a schema to your API, depending on exactly what you need. The simplest way to include a schema in your project is to use the `get_schema_view()` function. + from rest_framework.schemas import get_schema_view + schema_view = get_schema_view(title="Server Monitoring API") urlpatterns = [ @@ -161,6 +163,7 @@ ROOT_URLCONF setting. May be used to pass the set of renderer classes that can be used to render the API root endpoint. + from rest_framework.schemas import get_schema_view from rest_framework.renderers import CoreJSONRenderer from my_custom_package import APIBlueprintRenderer @@ -185,6 +188,12 @@ to be exposed in the schema: patterns=schema_url_patterns, ) +#### `generator_class` + +May be used to specify a `SchemaGenerator` subclass to be passed to the +`SchemaView`. + + ## Using an explicit schema view diff --git a/docs/topics/funding.md b/docs/topics/funding.md index 20273f5c9..b4c3f9b4d 100644 --- a/docs/topics/funding.md +++ b/docs/topics/funding.md @@ -329,7 +329,7 @@ For further enquires please contact @@ -360,6 +360,25 @@ In an effort to keep the project as transparent as possible, we are releasing [m --- +## Frequently asked questions + +**Q: Can you issue monthly invoices?** +A: Yes, we are happy to issue monthly invoices. Please just email us and let us know who to issue the invoice to (name and address) and which email address to send it to each month. + +**Q: Does sponsorship include VAT?** +A: Sponsorship is VAT exempt. + +**Q: Do I have to sign up for a certain time period?** +A: No, we appreciate your support for any time period that is convenient for you. Also, you can cancel your sponsorship anytime. + +**Q: Can I pay yearly? Can I pay upfront fox X amount of months at a time?** +A: We are currently only set up to accept monthly payments. However, if you'd like to support Django REST framework and you can only do yearly/upfront payments, we are happy to work with you and figure out a convenient solution. + +**Q: Are you only looking for corporate sponsors?** +A: No, we value individual sponsors just as much as corporate sponsors and appreciate any kind of support. + +--- + ## Our sponsors
diff --git a/docs/topics/third-party-packages.md b/docs/topics/third-party-packages.md index 5d5fa3c68..44639f3d2 100644 --- a/docs/topics/third-party-packages.md +++ b/docs/topics/third-party-packages.md @@ -296,7 +296,7 @@ To submit new content, [open an issue][drf-create-issue] or [create a pull reque [drf-compound-fields]: https://github.com/estebistec/drf-compound-fields [django-extra-fields]: https://github.com/Hipo/drf-extra-fields [djangorestframework-bulk]: https://github.com/miki725/django-rest-framework-bulk -[django-rest-multiple-models]: https://github.com/Axiologue/DjangoRestMultipleModels +[django-rest-multiple-models]: https://github.com/MattBroach/DjangoRestMultipleModels [drf-nested-routers]: https://github.com/alanjds/drf-nested-routers [wq.db.rest]: http://wq.io/docs/about-rest [djangorestframework-msgpack]: https://github.com/juanriaza/django-rest-framework-msgpack diff --git a/docs/topics/tutorials-and-resources.md b/docs/topics/tutorials-and-resources.md index 48719a618..245392f40 100644 --- a/docs/topics/tutorials-and-resources.md +++ b/docs/topics/tutorials-and-resources.md @@ -35,6 +35,7 @@ There are a wide range of resources available for learning and using Django REST ### Talks +* [Level Up! Rethinking the Web API Framework][pycon-us-2017] * [How to Make a Full Fledged REST API with Django OAuth Toolkit][full-fledged-rest-api-with-django-oauth-tookit] * [Django REST API - So Easy You Can Learn It in 25 Minutes][django-rest-api-so-easy] * [Tom Christie about Django Rest Framework at Django: Under The Hood][django-under-hood-2014] @@ -114,3 +115,4 @@ Want your Django REST Framework talk/tutorial/article to be added to our website [building-a-restful-api-with-drf]: http://agiliq.com/blog/2014/12/building-a-restful-api-with-django-rest-framework/ [submit-pr]: https://github.com/encode/django-rest-framework [anna-email]: mailto:anna@django-rest-framework.org +[pycon-us-2017]: https://www.youtube.com/watch?v=Rk6MHZdust4 diff --git a/rest_framework/filters.py b/rest_framework/filters.py index aea9d3a57..bdab97b58 100644 --- a/rest_framework/filters.py +++ b/rest_framework/filters.py @@ -48,7 +48,7 @@ if django_filters: warnings.warn( "The built in 'rest_framework.filters.FilterSet' is deprecated. " "You should use 'django_filters.rest_framework.FilterSet' instead.", - DeprecationWarning + DeprecationWarning, stacklevel=2 ) return super(FilterSet, self).__init__(*args, **kwargs) @@ -72,7 +72,7 @@ class DjangoFilterBackend(DFBase): warnings.warn( "The built in 'rest_framework.filters.DjangoFilterBackend' is deprecated. " "You should use 'django_filters.rest_framework.DjangoFilterBackend' instead.", - DeprecationWarning + DeprecationWarning, stacklevel=2 ) return super(DjangoFilterBackend, cls).__new__(cls, *args, **kwargs) diff --git a/rest_framework/parsers.py b/rest_framework/parsers.py index 238382364..0e40e1a7a 100644 --- a/rest_framework/parsers.py +++ b/rest_framework/parsers.py @@ -6,6 +6,7 @@ on the request, such as form content or json encoded data. """ from __future__ import unicode_literals +import codecs import json from django.conf import settings @@ -61,8 +62,8 @@ class JSONParser(BaseParser): encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET) try: - data = stream.read().decode(encoding) - return json.loads(data) + decoded_stream = codecs.getreader(encoding)(stream) + return json.load(decoded_stream) except ValueError as exc: raise ParseError('JSON parse error - %s' % six.text_type(exc)) diff --git a/rest_framework/relations.py b/rest_framework/relations.py index 54e67cd16..4d3bdba1d 100644 --- a/rest_framework/relations.py +++ b/rest_framework/relations.py @@ -75,7 +75,8 @@ class PKOnlyObject(object): # rather than the parent serializer. MANY_RELATION_KWARGS = ( 'read_only', 'write_only', 'required', 'default', 'initial', 'source', - 'label', 'help_text', 'style', 'error_messages', 'allow_empty' + 'label', 'help_text', 'style', 'error_messages', 'allow_empty', + 'html_cutoff', 'html_cutoff_text' ) @@ -86,10 +87,12 @@ class RelatedField(Field): def __init__(self, **kwargs): self.queryset = kwargs.pop('queryset', self.queryset) - self.html_cutoff = kwargs.pop( - 'html_cutoff', - self.html_cutoff or int(api_settings.HTML_SELECT_CUTOFF) - ) + + cutoff_from_settings = api_settings.HTML_SELECT_CUTOFF + if cutoff_from_settings is not None: + cutoff_from_settings = int(cutoff_from_settings) + self.html_cutoff = kwargs.pop('html_cutoff', cutoff_from_settings) + self.html_cutoff_text = kwargs.pop( 'html_cutoff_text', self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT) @@ -466,10 +469,12 @@ class ManyRelatedField(Field): def __init__(self, child_relation=None, *args, **kwargs): self.child_relation = child_relation self.allow_empty = kwargs.pop('allow_empty', True) - self.html_cutoff = kwargs.pop( - 'html_cutoff', - self.html_cutoff or int(api_settings.HTML_SELECT_CUTOFF) - ) + + cutoff_from_settings = api_settings.HTML_SELECT_CUTOFF + if cutoff_from_settings is not None: + cutoff_from_settings = int(cutoff_from_settings) + self.html_cutoff = kwargs.pop('html_cutoff', cutoff_from_settings) + self.html_cutoff_text = kwargs.pop( 'html_cutoff_text', self.html_cutoff_text or _(api_settings.HTML_SELECT_CUTOFF_TEXT) diff --git a/rest_framework/request.py b/rest_framework/request.py index 9428708ed..ffbe4a367 100644 --- a/rest_framework/request.py +++ b/rest_framework/request.py @@ -335,7 +335,6 @@ class Request(object): """ Attempt to authenticate the request using each authentication instance in turn. - Returns a three-tuple of (authenticator, user, authtoken). """ for authenticator in self.authenticators: try: diff --git a/rest_framework/routers.py b/rest_framework/routers.py index fce968aa0..a04bffc1a 100644 --- a/rest_framework/routers.py +++ b/rest_framework/routers.py @@ -35,6 +35,15 @@ DynamicDetailRoute = namedtuple('DynamicDetailRoute', ['url', 'name', 'initkwarg DynamicListRoute = namedtuple('DynamicListRoute', ['url', 'name', 'initkwargs']) +def escape_curly_brackets(url_path): + """ + Double brackets in regex of url_path for escape string formatting + """ + if ('{' and '}') in url_path: + url_path = url_path.replace('{', '{{').replace('}', '}}') + return url_path + + def replace_methodname(format_string, methodname): """ Partially format a format_string, swapping out any @@ -178,6 +187,7 @@ class SimpleRouter(BaseRouter): initkwargs = route.initkwargs.copy() initkwargs.update(method_kwargs) url_path = initkwargs.pop("url_path", None) or methodname + url_path = escape_curly_brackets(url_path) url_name = initkwargs.pop("url_name", None) or url_path ret.append(Route( url=replace_methodname(route.url, url_path), @@ -323,7 +333,7 @@ class DefaultRouter(SimpleRouter): warnings.warn( "Including a schema directly via a router is now deprecated. " "Use `get_schema_view()` instead.", - DeprecationWarning + DeprecationWarning, stacklevel=2 ) if 'schema_renderers' in kwargs: assert 'schema_title' in kwargs, 'Missing "schema_title" argument.' diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index e27610178..a4b51ae9d 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1159,6 +1159,11 @@ class ModelSerializer(Serializer): field_class = field_mapping[model_field] field_kwargs = get_field_kwargs(field_name, model_field) + # Special case to handle when a OneToOneField is also the primary key + if model_field.one_to_one and model_field.primary_key: + field_class = self.serializer_related_field + field_kwargs['queryset'] = model_field.related_model.objects + if 'choices' in field_kwargs: # Fields with choices get coerced into `ChoiceField` # instead of using their regular typed field. diff --git a/rest_framework/static/rest_framework/docs/js/api.js b/rest_framework/static/rest_framework/docs/js/api.js index 045e95edd..3c5b7180d 100644 --- a/rest_framework/static/rest_framework/docs/js/api.js +++ b/rest_framework/static/rest_framework/docs/js/api.js @@ -102,7 +102,7 @@ $(function () { var entry = entries[i] var paramKey = entry[0] var paramValue = entry[1] - var $elem = $form.find('[name=' + paramKey + ']') + var $elem = $form.find('[name="' + paramKey + '"]') var dataType = $elem.data('type') || 'string' if (dataType === 'integer' && paramValue) { diff --git a/rest_framework/static/rest_framework/js/ajax-form.js b/rest_framework/static/rest_framework/js/ajax-form.js index ce17729d1..1483305ff 100644 --- a/rest_framework/static/rest_framework/js/ajax-form.js +++ b/rest_framework/static/rest_framework/js/ajax-form.js @@ -37,6 +37,21 @@ function doAjaxSubmit(e) { if (contentType) { data = form.find('[data-override="content"]').val() || '' + + if (contentType === 'multipart/form-data') { + // We need to add a boundary parameter to the header + // We assume the first valid-looking boundary line in the body is correct + // regex is from RFC 2046 appendix A + var boundaryCharNoSpace = "0-9A-Z'()+_,-./:=?"; + var boundaryChar = boundaryCharNoSpace + ' '; + var re = new RegExp('^--([' + boundaryChar + ']{0,69}[' + boundaryCharNoSpace + '])[\\s]*?$', 'im'); + var boundary = data.match(re); + if (boundary !== null) { + contentType += '; boundary="' + boundary[1] + '"'; + } + // Fix textarea.value EOL normalisation (multipart/form-data should use CR+NL, not NL) + data = data.replace(/\n/g, '\r\n'); + } } else { contentType = form.attr('enctype') || form.attr('encoding') diff --git a/rest_framework/templates/rest_framework/base.html b/rest_framework/templates/rest_framework/base.html index bd87ba8a6..2587567d7 100644 --- a/rest_framework/templates/rest_framework/base.html +++ b/rest_framework/templates/rest_framework/base.html @@ -32,7 +32,8 @@
{% block navbar %} -