Merge branch 'master' into localizedfloatfield

This commit is contained in:
kgeorgy 2017-08-16 08:35:21 +02:00 committed by GitHub
commit f389833701
22 changed files with 195 additions and 65 deletions

View File

@ -26,10 +26,9 @@ The initial aim is to provide a single full-time position on REST framework.
<a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf"><img src="https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/stream-readme.png"/></a> <a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf"><img src="https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/stream-readme.png"/></a>
<a href="https://hello.machinalis.co.uk/"><img src="https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/machinalis-readme.png"/></a> <a href="https://hello.machinalis.co.uk/"><img src="https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/machinalis-readme.png"/></a>
<a href="https://rollbar.com/"><img src="https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/rollbar-readme.png"/></a> <a href="https://rollbar.com/"><img src="https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/rollbar-readme.png"/></a>
<a href="https://micropyramid.com/django-rest-framework-development-services/"><img src="https://raw.githubusercontent.com/encode/django-rest-framework/master/docs/img/premium/micropyramid-readme.png"/></a>
</p> </p>
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Machinalis](https://hello.machinalis.co.uk/), [Rollbar](https://rollbar.com), and [MicroPyramid](https://micropyramid.com/django-rest-framework-development-services/).* *Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Machinalis](https://hello.machinalis.co.uk/), and [Rollbar](https://rollbar.com/).*
--- ---

View File

@ -222,6 +222,21 @@ It is also possible to create Tokens manually through admin interface. In case y
TokenAdmin.raw_id_fields = ('user',) TokenAdmin.raw_id_fields = ('user',)
#### Using Django manage.py command
Since version 3.6.4 it's possible to generate a user token using the following command:
./manage.py drf_create_token <username>
this command will return the API token for the given user, creating it if it doesn't exist:
Generated token 9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b for user user1
In case you want to regenerate the token (for example if it has been compromised or leaked) you can pass an additional parameter:
./manage.py drf_create_token -r <username>
## SessionAuthentication ## SessionAuthentication
This authentication scheme uses Django's default session backend for authentication. Session authentication is appropriate for AJAX clients that are running in the same session context as your website. This authentication scheme uses Django's default session backend for authentication. Session authentication is appropriate for AJAX clients that are running in the same session context as your website.
@ -239,6 +254,28 @@ If you're using an AJAX style API with SessionAuthentication, you'll need to mak
CSRF validation in REST framework works slightly differently to standard Django due to the need to support both session and non-session based authentication to the same views. This means that only authenticated requests require CSRF tokens, and anonymous requests may be sent without CSRF tokens. This behaviour is not suitable for login views, which should always have CSRF validation applied. CSRF validation in REST framework works slightly differently to standard Django due to the need to support both session and non-session based authentication to the same views. This means that only authenticated requests require CSRF tokens, and anonymous requests may be sent without CSRF tokens. This behaviour is not suitable for login views, which should always have CSRF validation applied.
## RemoteUserAuthentication
This authentication scheme allows you to delegate authentication to your web server, which sets the `REMOTE_USER`
environment variable.
To use it, you must have `django.contrib.auth.backends.RemoteUserBackend` (or a subclass) in your
`AUTHENTICATION_BACKENDS` setting. By default, `RemoteUserBackend` creates `User` objects for usernames that don't
already exist. To change this and other behaviour, consult the
[Django documentation](https://docs.djangoproject.com/en/stable/howto/auth-remote-user/).
If successfully authenticated, `RemoteUserAuthentication` provides the following credentials:
* `request.user` will be a Django `User` instance.
* `request.auth` will be `None`.
Consult your web server's documentation for information about configuring an authentication method, e.g.:
* [Apache Authentication How-To](https://httpd.apache.org/docs/2.4/howto/auth.html)
* [NGINX (Restricting Access)](https://www.nginx.com/resources/admin-guide/#restricting_access)
# Custom authentication # Custom authentication
To implement a custom authentication scheme, subclass `BaseAuthentication` and override the `.authenticate(self, request)` method. The method should return a two-tuple of `(user, auth)` if authentication succeeds, or `None` otherwise. To implement a custom authentication scheme, subclass `BaseAuthentication` and override the `.authenticate(self, request)` method. The method should return a two-tuple of `(user, auth)` if authentication succeeds, or `None` otherwise.

View File

@ -641,7 +641,7 @@ The `.fail()` method is a shortcut for raising `ValidationError` that takes a me
return Color(red, green, blue) return Color(red, green, blue)
This style keeps you error messages more cleanly separated from your code, and should be preferred. This style keeps your error messages cleaner and more separated from your code, and should be preferred.
# Third party packages # Third party packages

View File

@ -242,29 +242,6 @@ We'd then need to setup the custom class in our configuration:
Note that if you care about how the ordering of keys is displayed in responses in the browsable API you might choose to use an `OrderedDict` when constructing the body of paginated responses, but this is optional. Note that if you care about how the ordering of keys is displayed in responses in the browsable API you might choose to use an `OrderedDict` when constructing the body of paginated responses, but this is optional.
## Header based pagination
Let's modify the built-in `PageNumberPagination` style, so that instead of include the pagination links in the body of the response, we'll instead include a `Link` header, in a [similar style to the GitHub API][github-link-pagination].
class LinkHeaderPagination(pagination.PageNumberPagination):
def get_paginated_response(self, data):
next_url = self.get_next_link()
previous_url = self.get_previous_link()
if next_url is not None and previous_url is not None:
link = '<{next_url}>; rel="next", <{previous_url}>; rel="prev"'
elif next_url is not None:
link = '<{next_url}>; rel="next"'
elif previous_url is not None:
link = '<{previous_url}>; rel="prev"'
else:
link = ''
link = link.format(next_url=next_url, previous_url=previous_url)
headers = {'Link': link} if link else {}
return Response(data, headers=headers)
## Using your custom pagination class ## Using your custom pagination class
To have your custom pagination class be used by default, use the `DEFAULT_PAGINATION_CLASS` setting: To have your custom pagination class be used by default, use the `DEFAULT_PAGINATION_CLASS` setting:
@ -328,10 +305,15 @@ The [`DRF-extensions` package][drf-extensions] includes a [`PaginateByMaxMixin`
The [`drf-proxy-pagination` package][drf-proxy-pagination] includes a `ProxyPagination` class which allows to choose pagination class with a query parameter. The [`drf-proxy-pagination` package][drf-proxy-pagination] includes a `ProxyPagination` class which allows to choose pagination class with a query parameter.
## link-header-pagination
The [`django-rest-framework-link-header-pagination` package][drf-link-header-pagination] includes a `LinkHeaderPagination` class which provides pagination via an HTTP `Link` header as desribed in [Github's developer documentation](github-link-pagination).
[cite]: https://docs.djangoproject.com/en/stable/topics/pagination/ [cite]: https://docs.djangoproject.com/en/stable/topics/pagination/
[github-link-pagination]: https://developer.github.com/guides/traversing-with-pagination/ [github-link-pagination]: https://developer.github.com/guides/traversing-with-pagination/
[link-header]: ../img/link-header-pagination.png [link-header]: ../img/link-header-pagination.png
[drf-extensions]: http://chibisov.github.io/drf-extensions/docs/ [drf-extensions]: http://chibisov.github.io/drf-extensions/docs/
[paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin [paginate-by-max-mixin]: http://chibisov.github.io/drf-extensions/docs/#paginatebymaxmixin
[drf-proxy-pagination]: https://github.com/tuffnatty/drf-proxy-pagination [drf-proxy-pagination]: https://github.com/tuffnatty/drf-proxy-pagination
[drf-link-header-pagination]: https://github.com/tbeadle/django-rest-framework-link-header-pagination
[disqus-cursor-api]: http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api [disqus-cursor-api]: http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api

View File

@ -82,7 +82,7 @@ For example, when forcibly authenticating using a token, you might do something
user = User.objects.get(username='olivia') user = User.objects.get(username='olivia')
request = factory.get('/accounts/django-superstars/') request = factory.get('/accounts/django-superstars/')
force_authenticate(request, user=user, token=user.token) force_authenticate(request, user=user, token=user.auth_token)
--- ---

View File

@ -23,6 +23,7 @@ For example:
from rest_framework.views import APIView from rest_framework.views import APIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import authentication, permissions from rest_framework import authentication, permissions
from django.contrib.auth.models import User
class ListUsers(APIView): class ListUsers(APIView):
""" """

View File

@ -75,11 +75,10 @@ continued development by **[signing up for a paid plan][funding]**.
<li><a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li> <li><a href="https://getstream.io/try-the-api/?utm_source=drf&utm_medium=banner&utm_campaign=drf" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/stream-130.png)">Stream</a></li>
<li><a href="https://hello.machinalis.co.uk/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/Machinalis130.png)">Machinalis</a></li> <li><a href="https://hello.machinalis.co.uk/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/Machinalis130.png)">Machinalis</a></li>
<li><a href="https://rollbar.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rollbar.png)">Rollbar</a></li> <li><a href="https://rollbar.com" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/rollbar.png)">Rollbar</a></li>
<li><a href="https://micropyramid.com/django-rest-framework-development-services/" style="background-image: url(https://fund-rest-framework.s3.amazonaws.com/mp-text-logo.png)">MicroPyramid</a></li>
</ul> </ul>
<div style="clear: both; padding-bottom: 20px;"></div> <div style="clear: both; padding-bottom: 20px;"></div>
*Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Machinalis](https://hello.machinalis.co.uk/), [Rollbar](https://rollbar.com), and [MicroPyramid](https://micropyramid.com/django-rest-framework-development-services/).* *Many thanks to all our [wonderful sponsors][sponsors], and in particular to our premium backers, [Rover](http://jobs.rover.com/), [Sentry](https://getsentry.com/welcome/), [Stream](https://getstream.io/?utm_source=drf&utm_medium=banner&utm_campaign=drf), [Machinalis](https://hello.machinalis.co.uk/), and [Rollbar](https://rollbar.com).*
--- ---

View File

@ -408,7 +408,7 @@ The `coreapi` library, and the `schema` object will now both be available on the
In order to interact with the API you'll need a client instance. In order to interact with the API you'll need a client instance.
var client = coreapi.Client() var client = new coreapi.Client()
Typically you'll also want to provide some authentication credentials when Typically you'll also want to provide some authentication credentials when
instantiating the client. instantiating the client.
@ -423,7 +423,7 @@ the user to login, and then instantiate a client using session authentication:
csrfCookieName: 'csrftoken', csrfCookieName: 'csrftoken',
csrfHeaderName: 'X-CSRFToken' csrfHeaderName: 'X-CSRFToken'
}) })
let client = coreapi.Client({auth: auth}) let client = new coreapi.Client({auth: auth})
The authentication scheme will handle including a CSRF header in any outgoing The authentication scheme will handle including a CSRF header in any outgoing
requests for unsafe HTTP methods. requests for unsafe HTTP methods.
@ -437,7 +437,7 @@ The `TokenAuthentication` class can be used to support REST framework's built-in
scheme: 'JWT' scheme: 'JWT'
token: '<token>' token: '<token>'
}) })
let client = coreapi.Client({auth: auth}) let client = new coreapi.Client({auth: auth})
When using TokenAuthentication you'll probably need to implement a login flow When using TokenAuthentication you'll probably need to implement a login flow
using the CoreAPI client. using the CoreAPI client.
@ -448,7 +448,7 @@ request to an "obtain token" endpoint
For example, using the "Django REST framework JWT" package For example, using the "Django REST framework JWT" package
// Setup some globally accessible state // Setup some globally accessible state
window.client = coreapi.Client() window.client = new coreapi.Client()
window.loggedIn = false window.loggedIn = false
function loginUser(username, password) { function loginUser(username, password) {
@ -475,7 +475,7 @@ The `BasicAuthentication` class can be used to support HTTP Basic Authentication
username: '<username>', username: '<username>',
password: '<password>' password: '<password>'
}) })
let client = coreapi.Client({auth: auth}) let client = new coreapi.Client({auth: auth})
## Using the client ## Using the client

View File

@ -42,7 +42,7 @@ For example:
class UserList(generics.ListAPIView): class UserList(generics.ListAPIView):
""" """
Return a list of all the existing users. Return a list of all the existing users.
"""" """
If a view supports multiple methods, you should split your documentation using `method:` style delimiters. If a view supports multiple methods, you should split your documentation using `method:` style delimiters.

View File

@ -329,7 +329,7 @@ For further enquires please contact <a href=mailto:funding@django-rest-framework
## Accountability ## Accountability
In an effort to keep the project as transparent as possible, we are releasing [monthly progress reports](http://www.encode.io/reports/june-2017) and regularly include financial reports and cost breakdowns. In an effort to keep the project as transparent as possible, we are releasing [monthly progress reports](http://www.encode.io/reports/july-2017) and regularly include financial reports and cost breakdowns.
<!-- Begin MailChimp Signup Form --> <!-- Begin MailChimp Signup Form -->
<link href="//cdn-images.mailchimp.com/embedcode/classic-10_7.css" rel="stylesheet" type="text/css"> <link href="//cdn-images.mailchimp.com/embedcode/classic-10_7.css" rel="stylesheet" type="text/css">

View File

@ -36,15 +36,15 @@ API schema.
We can now include a schema for our API, by including an autogenerated schema We can now include a schema for our API, by including an autogenerated schema
view in our URL configuration. view in our URL configuration.
``` ```python
from rest_framework.schemas import get_schema_view from rest_framework.schemas import get_schema_view
schema_view = get_schema_view(title='Pastebin API') schema_view = get_schema_view(title='Pastebin API')
urlpatterns = [ urlpatterns = [
   url(r'^schema/$', schema_view),    url(r'^schema/$', schema_view),
... ...
] ]
``` ```
If you visit the API root endpoint in a browser you should now see `corejson` If you visit the API root endpoint in a browser you should now see `corejson`

View File

@ -201,3 +201,24 @@ class TokenAuthentication(BaseAuthentication):
def authenticate_header(self, request): def authenticate_header(self, request):
return self.keyword return self.keyword
class RemoteUserAuthentication(BaseAuthentication):
"""
REMOTE_USER authentication.
To use this, set up your web server to perform authentication, which will
set the REMOTE_USER environment variable. You will need to have
'django.contrib.auth.backends.RemoteUserBackend in your
AUTHENTICATION_BACKENDS setting
"""
# Name of request header to grab username from. This will be the key as
# used in the request.META dictionary, i.e. the normalization of headers to
# all uppercase and the addition of "HTTP_" prefix apply.
header = "REMOTE_USER"
def authenticate(self, request):
user = authenticate(remote_user=request.META.get(self.header))
if user and user.is_active:
return (user, None)

View File

@ -129,8 +129,8 @@ class ValidationError(APIException):
if code is None: if code is None:
code = self.default_code code = self.default_code
# For validation failures, we may collect may errors together, so the # For validation failures, we may collect many errors together,
# details should always be coerced to a list if not already. # so the details should always be coerced to a list if not already.
if not isinstance(detail, dict) and not isinstance(detail, list): if not isinstance(detail, dict) and not isinstance(detail, list):
detail = [detail] detail = [detail]

View File

@ -25,10 +25,7 @@ from django.utils.dateparse import (
) )
from django.utils.duration import duration_string from django.utils.duration import duration_string
from django.utils.encoding import is_protected_type, smart_text from django.utils.encoding import is_protected_type, smart_text
from django.utils.formats import ( from django.utils.formats import localize_input, number_format, sanitize_separators
localize_input, number_format, sanitize_separators
)
from django.utils.functional import cached_property
from django.utils.ipv6 import clean_ipv6_address from django.utils.ipv6 import clean_ipv6_address
from django.utils.timezone import utc from django.utils.timezone import utc
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -588,7 +585,7 @@ class Field(object):
message_string = msg.format(**kwargs) message_string = msg.format(**kwargs)
raise ValidationError(message_string, code=key) raise ValidationError(message_string, code=key)
@cached_property @property
def root(self): def root(self):
""" """
Returns the top-level serializer for this field. Returns the top-level serializer for this field.
@ -598,7 +595,7 @@ class Field(object):
root = root.parent root = root.parent
return root return root
@cached_property @property
def context(self): def context(self):
""" """
Returns the context as passed to the root serializer on initialization. Returns the context as passed to the root serializer on initialization.

View File

@ -140,12 +140,14 @@ class SearchFilter(BaseFilterBackend):
] ]
base = queryset base = queryset
conditions = []
for search_term in search_terms: for search_term in search_terms:
queries = [ queries = [
models.Q(**{orm_lookup: search_term}) models.Q(**{orm_lookup: search_term})
for orm_lookup in orm_lookups for orm_lookup in orm_lookups
] ]
queryset = queryset.filter(reduce(operator.or_, queries)) conditions.append(reduce(operator.or_, queries))
queryset = queryset.filter(reduce(operator.and_, conditions))
if self.must_call_distinct(queryset, search_fields): if self.must_call_distinct(queryset, search_fields):
# Filtering against a many-to-many field requires us to # Filtering against a many-to-many field requires us to

View File

@ -31,7 +31,7 @@ def _positive_int(integer_string, strict=False, cutoff=None):
if ret < 0 or (ret == 0 and strict): if ret < 0 or (ret == 0 and strict):
raise ValueError() raise ValueError()
if cutoff: if cutoff:
ret = min(ret, cutoff) return min(ret, cutoff)
return ret return ret
@ -95,7 +95,7 @@ def _get_displayed_page_numbers(current, final):
# Now sort the page numbers and drop anything outside the limits. # Now sort the page numbers and drop anything outside the limits.
included = [ included = [
idx for idx in sorted(list(included)) idx for idx in sorted(list(included))
if idx > 0 and idx <= final if 0 < idx <= final
] ]
# Finally insert any `...` breaks # Finally insert any `...` breaks
@ -473,7 +473,7 @@ class CursorPagination(BasePagination):
""" """
The cursor pagination implementation is necessarily complex. The cursor pagination implementation is necessarily complex.
For an overview of the position/offset style we use, see this post: For an overview of the position/offset style we use, see this post:
http://cramer.io/2011/03/08/building-cursors-for-the-disqus-api http://cra.mr/2011/03/08/building-cursors-for-the-disqus-api
""" """
cursor_query_param = 'cursor' cursor_query_param = 'cursor'
cursor_query_description = _('The pagination cursor value.') cursor_query_description = _('The pagination cursor value.')

View File

@ -9,23 +9,23 @@ from __future__ import unicode_literals
def is_informational(code): def is_informational(code):
return code >= 100 and code <= 199 return 100 <= code <= 199
def is_success(code): def is_success(code):
return code >= 200 and code <= 299 return 200 <= code <= 299
def is_redirect(code): def is_redirect(code):
return code >= 300 and code <= 399 return 300 <= code <= 399
def is_client_error(code): def is_client_error(code):
return code >= 400 and code <= 499 return 400 <= code <= 499
def is_server_error(code): def is_server_error(code):
return code >= 500 and code <= 599 return 500 <= code <= 599
HTTP_100_CONTINUE = 100 HTTP_100_CONTINUE = 100

View File

@ -52,8 +52,8 @@ def camelcase_to_spaces(content):
Translate 'CamelCaseNames' to 'Camel Case Names'. Translate 'CamelCaseNames' to 'Camel Case Names'.
Used when generating names from view classes. Used when generating names from view classes.
""" """
camelcase_boundry = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))' camelcase_boundary = '(((?<=[a-z])[A-Z])|([A-Z](?![A-Z]|$)))'
content = re.sub(camelcase_boundry, ' \\1', content).strip() content = re.sub(camelcase_boundary, ' \\1', content).strip()
return ' '.join(content.split('_')).title() return ' '.join(content.split('_')).title()

View File

@ -16,9 +16,8 @@ from rest_framework import (
HTTP_HEADER_ENCODING, exceptions, permissions, renderers, status HTTP_HEADER_ENCODING, exceptions, permissions, renderers, status
) )
from rest_framework.authentication import ( from rest_framework.authentication import (
BaseAuthentication, BasicAuthentication, SessionAuthentication, BaseAuthentication, BasicAuthentication, RemoteUserAuthentication, SessionAuthentication,
TokenAuthentication TokenAuthentication)
)
from rest_framework.authtoken.models import Token from rest_framework.authtoken.models import Token
from rest_framework.authtoken.views import obtain_auth_token from rest_framework.authtoken.views import obtain_auth_token
from rest_framework.compat import is_authenticated from rest_framework.compat import is_authenticated
@ -64,6 +63,10 @@ urlpatterns = [
r'^basic/$', r'^basic/$',
MockView.as_view(authentication_classes=[BasicAuthentication]) MockView.as_view(authentication_classes=[BasicAuthentication])
), ),
url(
r'^remote-user/$',
MockView.as_view(authentication_classes=[RemoteUserAuthentication])
),
url( url(
r'^token/$', r'^token/$',
MockView.as_view(authentication_classes=[TokenAuthentication]) MockView.as_view(authentication_classes=[TokenAuthentication])
@ -523,3 +526,20 @@ class BasicAuthenticationUnitTests(TestCase):
auth.authenticate_credentials('foo', 'bar') auth.authenticate_credentials('foo', 'bar')
assert 'User inactive or deleted.' in str(error) assert 'User inactive or deleted.' in str(error)
authentication.authenticate = old_authenticate authentication.authenticate = old_authenticate
@override_settings(ROOT_URLCONF='tests.test_authentication',
AUTHENTICATION_BACKENDS=('django.contrib.auth.backends.RemoteUserBackend',))
class RemoteUserAuthenticationUnitTests(TestCase):
def setUp(self):
self.username = 'john'
self.email = 'lennon@thebeatles.com'
self.password = 'password'
self.user = User.objects.create_user(
self.username, self.email, self.password
)
def test_remote_user_works(self):
response = self.client.post('/remote-user/',
REMOTE_USER=self.username)
self.assertEqual(response.status_code, status.HTTP_200_OK)

View File

@ -502,6 +502,16 @@ class TestCreateOnlyDefault:
assert serializer.validated_data['context_set'] == 'success' assert serializer.validated_data['context_set'] == 'success'
class Test5087Regression:
def test_parent_binding(self):
parent = serializers.Serializer()
field = serializers.CharField()
assert field.root is field
field.bind('name', parent)
assert field.root is parent
# Tests for field input and output values. # Tests for field input and output values.
# ---------------------------------------- # ----------------------------------------

View File

@ -5,6 +5,7 @@ import unittest
import warnings import warnings
from decimal import Decimal from decimal import Decimal
import django
import pytest import pytest
from django.conf.urls import url from django.conf.urls import url
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
@ -645,6 +646,51 @@ class SearchFilterM2MTests(TestCase):
) )
class Blog(models.Model):
name = models.CharField(max_length=20)
class Entry(models.Model):
blog = models.ForeignKey(Blog, on_delete=models.CASCADE)
headline = models.CharField(max_length=120)
pub_date = models.DateField(null=True)
class BlogSerializer(serializers.ModelSerializer):
class Meta:
model = Blog
fields = '__all__'
class SearchFilterToManyTests(TestCase):
@classmethod
def setUpTestData(cls):
b1 = Blog.objects.create(name='Blog 1')
b2 = Blog.objects.create(name='Blog 2')
# Multiple entries on Lennon published in 1979 - distinct should deduplicate
Entry.objects.create(blog=b1, headline='Something about Lennon', pub_date=datetime.date(1979, 1, 1))
Entry.objects.create(blog=b1, headline='Another thing about Lennon', pub_date=datetime.date(1979, 6, 1))
# Entry on Lennon *and* a separate entry in 1979 - should not match
Entry.objects.create(blog=b2, headline='Something unrelated', pub_date=datetime.date(1979, 1, 1))
Entry.objects.create(blog=b2, headline='Retrospective on Lennon', pub_date=datetime.date(1990, 6, 1))
@unittest.skipIf(django.VERSION < (1, 9), "Django 1.8 does not support transforms")
def test_multiple_filter_conditions(self):
class SearchListView(generics.ListAPIView):
queryset = Blog.objects.all()
serializer_class = BlogSerializer
filter_backends = (filters.SearchFilter,)
search_fields = ('=name', 'entry__headline', '=entry__pub_date__year')
view = SearchListView.as_view()
request = factory.get('/', {'search': 'Lennon,1979'})
response = view(request)
assert len(response.data) == 1
class OrderingFilterModel(models.Model): class OrderingFilterModel(models.Model):
title = models.CharField(max_length=20, verbose_name='verbose title') title = models.CharField(max_length=20, verbose_name='verbose title')
text = models.CharField(max_length=100) text = models.CharField(max_length=100)

View File

@ -469,6 +469,22 @@ class TestSerializerValidationWithCompiledRegexField:
assert serializer.errors == {} assert serializer.errors == {}
class Test2505Regression:
def test_serializer_context(self):
class NestedSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
super(NestedSerializer, self).__init__(*args, **kwargs)
# .context should not cache
self.context
class ParentSerializer(serializers.Serializer):
nested = NestedSerializer()
serializer = ParentSerializer(data={}, context={'foo': 'bar'})
assert serializer.context == {'foo': 'bar'}
assert serializer.fields['nested'].context == {'foo': 'bar'}
class Test4606Regression: class Test4606Regression:
def setup(self): def setup(self):
class ExampleSerializer(serializers.Serializer): class ExampleSerializer(serializers.Serializer):