mirror of
https://github.com/encode/django-rest-framework.git
synced 2025-08-04 12:30:11 +03:00
Merge branch 'master' into localizedfloatfield
This commit is contained in:
commit
f389833701
|
@ -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/).*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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).*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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`
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.')
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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.
|
||||||
# ----------------------------------------
|
# ----------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user