Skip to content

Commit

Permalink
Feature/django 3.2 (#1624)
Browse files Browse the repository at this point in the history
* tests: Test against Django 3.1 and 3.2

* flake8: Move doctest import to top of file

* django: Set SECURE_REFERRER_POLICY = None to restore Django<3.1 behavior

* tests: Don't use private method _headers

* Update authentication, remove old salt/unsalt compat from django 1.10

* Drop python 2, django 1.11 support

Yet another travis fix

Fix travis

* Compat shim for CSRF token comparison.

* Remove is_authenticated compat shim

* Silence several warnings

Silence path warning

* Firehose the travis/tox config

* flake8

Co-authored-by: James McKinney <26463+jpmckinney@users.noreply.github.com>
  • Loading branch information
georgedorn and jpmckinney committed Jun 3, 2021
1 parent 2700cdc commit 3b435be
Show file tree
Hide file tree
Showing 9 changed files with 65 additions and 73 deletions.
21 changes: 13 additions & 8 deletions .travis.yml
Expand Up @@ -4,33 +4,38 @@ dist: bionic
language: python

python:
- "2.7"
- "3.5"
- "3.6"
- "3.7"
- "3.8"

env:
- MODE=flake8
- MODE=flake8-strict
- MODE=docs
- DJANGO_VERSION=dj111
- DJANGO_VERSION=dj22
- DJANGO_VERSION=dj30
- DJANGO_VERSION=dj31
- DJANGO_VERSION=dj32
- DJANGO_VERSION=djdev

matrix:
allow_failures:
- env: DJANGO_VERSION=djdev
- env: MODE=flake8-strict
exclude:
- python: "2.7"
env: DJANGO_VERSION=djdev
- python: "2.7"
env: DJANGO_VERSION=dj22
- python: "2.7"
env: DJANGO_VERSION=dj30
- python: "3.5"
env: MODE=flake8
- python: "3.5"
env: MODE=flake8-strict
- python: "3.5"
env: MODE=flake8-docs
- python: "3.5"
env: DJANGO_VERSION=dj30
- python: "3.5"
env: DJANGO_VERSION=dj31
- python: "3.5"
env: DJANGO_VERSION=dj32
- python: "3.5"
env: DJANGO_VERSION=djdev

Expand Down
3 changes: 2 additions & 1 deletion tastypie/api.py
Expand Up @@ -9,6 +9,7 @@
from tastypie.utils import is_valid_jsonp_callback_value, string_to_python, trailing_slash
from tastypie.utils.mime import determine_format, build_content_type
from tastypie.resources import Resource
from django.urls.conf import re_path


class Api(object):
Expand Down Expand Up @@ -103,7 +104,7 @@ def urls(self):
``Resources`` beneath it.
"""
pattern_list = [
url(r"^(?P<api_name>%s)%s$" % (self.api_name, trailing_slash), self.wrap_view('top_level'), name="api_%s_top_level" % self.api_name),
re_path(r"^(?P<api_name>%s)%s$" % (self.api_name, trailing_slash), self.wrap_view('top_level'), name="api_%s_top_level" % self.api_name),
]

for name in sorted(self._registry.keys()):
Expand Down
13 changes: 6 additions & 7 deletions tastypie/authentication.py
Expand Up @@ -9,13 +9,13 @@
from django.conf import settings
from django.contrib.auth import authenticate
from django.core.exceptions import ImproperlyConfigured
from django.middleware.csrf import _sanitize_token, constant_time_compare
from django.middleware.csrf import _sanitize_token
from django.utils.translation import ugettext as _

from six.moves.urllib.parse import urlparse

from tastypie.compat import (
get_user_model, get_username_field, unsalt_token, is_authenticated
get_user_model, get_username_field, compare_sanitized_tokens
)
from tastypie.http import HttpUnauthorized

Expand Down Expand Up @@ -306,10 +306,10 @@ def is_authenticated(self, request, **kwargs):
# the serialized bodies.

if request.method in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
return is_authenticated(request.user)
return request.user.is_authenticated

if getattr(request, '_dont_enforce_csrf_checks', False):
return is_authenticated(request.user)
return request.user.is_authenticated

csrf_token = _sanitize_token(request.COOKIES.get(settings.CSRF_COOKIE_NAME, ''))

Expand All @@ -327,11 +327,10 @@ def is_authenticated(self, request, **kwargs):
request_csrf_token = request.META.get('HTTP_X_CSRFTOKEN', '')
request_csrf_token = _sanitize_token(request_csrf_token)

if not constant_time_compare(unsalt_token(request_csrf_token),
unsalt_token(csrf_token)):
if not compare_sanitized_tokens(request_csrf_token, csrf_token):
return False

return is_authenticated(request.user)
return request.user.is_authenticated

def get_identifier(self, request):
"""
Expand Down
36 changes: 12 additions & 24 deletions tastypie/compat.py
Expand Up @@ -13,16 +13,6 @@
AUTH_USER_MODEL = settings.AUTH_USER_MODEL


def is_authenticated(user):
"""
Django is changing User.is_authenticated into a property. Calling it
will be deprecated by Django 2.0 and a warning in 1.10+.
"""
if django.VERSION < (1, 10):
return bool(user.is_authenticated())
return bool(user.is_authenticated)


def get_username_field():
return get_user_model().USERNAME_FIELD

Expand All @@ -33,24 +23,22 @@ def get_module_name(meta):

atomic_decorator = django.db.transaction.atomic

# Compatability for salted vs unsalted CSRF tokens;
# Django 1.10's _sanitize_token also hashes it, so it can't be compared directly.
# Solution is to call _sanitize_token on both tokens, then unsalt or noop both
try:
from django.middleware.csrf import _unsalt_cipher_token

def unsalt_token(token):
return _unsalt_cipher_token(token)
except ImportError:

def unsalt_token(token):
return token


# force_text deprecated in 2.2, removed in 3.0
# note that in 1.1.x, force_str and force_text both exist, but force_str behaves
# much differently on python 3 than python 2.
if django.VERSION < (2, 2):
from django.utils.encoding import force_text as force_str # noqa
else:
from django.utils.encoding import force_str # noqa


# compat between django 3.0 and 3.2's csrf token comparison
try:
from django.middleware.csrf import _compare_masked_tokens
compare_sanitized_tokens = _compare_masked_tokens
except ImportError:
from django.middleware.csrf import _unsalt_cipher_token, constant_time_compare

def compare_sanitized_tokens(request_csrf_token, csrf_token):
return constant_time_compare(_unsalt_cipher_token(request_csrf_token),
_unsalt_cipher_token(csrf_token))
2 changes: 1 addition & 1 deletion tests/core/tests/__init__.py
@@ -1,6 +1,6 @@
import doctest
import warnings
warnings.simplefilter('ignore', Warning) # noqa
import doctest

from core.tests.api import * # noqa
from core.tests.authentication import * # noqa
Expand Down
8 changes: 4 additions & 4 deletions tests/core/tests/resources.py
Expand Up @@ -36,7 +36,7 @@
UnsupportedSerializationFormat, UnsupportedDeserializationFormat,
)
from tastypie import fields, http
from tastypie.compat import is_authenticated, force_str
from tastypie.compat import force_str
from tastypie.paginator import Paginator
from tastypie.resources import (
ALL, ALL_WITH_RELATIONS, convert_post_to_put, convert_post_to_patch,
Expand Down Expand Up @@ -1338,7 +1338,7 @@ class Meta:
class PerUserAuthorization(Authorization):
def read_list(self, object_list, bundle):
if bundle.request and hasattr(bundle.request, 'user'):
if is_authenticated(bundle.request.user):
if bundle.request.user.is_authenticated:
object_list = object_list.filter(author=bundle.request.user)
else:
object_list = object_list.none()
Expand Down Expand Up @@ -4937,15 +4937,15 @@ def test_browser_cache(self):
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode('utf-8'), '{"content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": 1, "is_active": true, "resource_uri": "/api/v1/notes/1/", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00"}')
self.assertTrue(resp.has_header('Cache-Control'))
self.assertEqual(resp._headers['cache-control'], ('Cache-Control', 'no-cache'))
self.assertEqual(resp['Cache-Control'], 'no-cache')

# Now as Ajax.
request.META = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
resp = resource.wrap_view('dispatch_detail')(request, pk=1)
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.content.decode('utf-8'), '{"content": "This is my very first post using my shiny new API. Pretty sweet, huh?", "created": "2010-03-30T20:05:00", "id": 1, "is_active": true, "resource_uri": "/api/v1/notes/1/", "slug": "first-post", "title": "First Post!", "updated": "2010-03-30T20:05:00"}')
self.assertTrue(resp.has_header('cache-control'))
self.assertEqual(resp._headers['cache-control'], ('Cache-Control', 'no-cache'))
self.assertEqual(resp['Cache-Control'], 'no-cache')

def test_custom_paginator(self):
mock_request = MockRequest()
Expand Down
2 changes: 1 addition & 1 deletion tests/requirements.txt
Expand Up @@ -4,5 +4,5 @@ coverage
defusedxml
lxml
mock<1.1.0
pytz==2013b0
pytz==2021.1
PyYAML
3 changes: 3 additions & 0 deletions tests/settings.py
Expand Up @@ -38,6 +38,7 @@
'NAME': DATABASE_NAME,
}
}
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'

ALLOWED_HOSTS = ['example.com']

Expand Down Expand Up @@ -91,3 +92,5 @@

if DJANGO_VERSION >= DJANGO_20:
MIDDLEWARE.remove('django.contrib.auth.middleware.SessionAuthenticationMiddleware')

SECURE_REFERRER_POLICY = None
50 changes: 23 additions & 27 deletions tox.ini
@@ -1,10 +1,10 @@
[tox]
envlist =
py{27}-dj{111}
py{36,37}-dj{22,30,dev}
py{27,37}-docs,
py{27,37}-flake8,
py{27,37}-flake8-strict
py{35}-dj{22}
py{36,37,38}-dj{22,30,31,32,dev}
py{35,36,37,38}-docs,
py{35,36,37,38}-flake8,
py{35,36,37,38}-flake8-strict

skipsdist=True

Expand All @@ -16,17 +16,17 @@ setenv =
PYTHONPATH = {toxinidir}:{toxinidir}/tests
PYTHONWARNINGS = always
commands =
dj{111,22,30,dev}: {[testenv]test-executable} test -p '*' core.tests --settings=settings_core
dj{111,22,30,dev}: {[testenv]test-executable} test basic.tests --settings=settings_basic
dj{111,22,30,dev}: {[testenv]test-executable} test related_resource.tests --settings=settings_related
dj{111,22,30,dev}: {[testenv]test-executable} test alphanumeric.tests --settings=settings_alphanumeric
dj{111,22,30,dev}: {[testenv]test-executable} test authorization.tests --settings=settings_authorization
dj{111,22,30,dev}: {[testenv]test-executable} test content_gfk.tests --settings=settings_content_gfk
dj{111,22,30,dev}: {[testenv]test-executable} test customuser.tests --settings=settings_customuser
dj{111,22,30,dev}: {[testenv]test-executable} test namespaced.tests --settings=settings_namespaced
dj{111,22,30,dev}: {[testenv]test-executable} test slashless.tests --settings=settings_slashless
dj{111,22,30,dev}: {[testenv]test-executable} test validation.tests --settings=settings_validation
dj{111,22,30,dev}: {[testenv]test-executable} test gis.tests --settings=settings_gis_spatialite
dj{22,30,31,32,dev}: {[testenv]test-executable} test -p '*' core.tests --settings=settings_core
dj{22,30,31,32,dev}: {[testenv]test-executable} test basic.tests --settings=settings_basic
dj{22,30,31,32,dev}: {[testenv]test-executable} test related_resource.tests --settings=settings_related
dj{22,30,31,32,dev}: {[testenv]test-executable} test alphanumeric.tests --settings=settings_alphanumeric
dj{22,30,31,32,dev}: {[testenv]test-executable} test authorization.tests --settings=settings_authorization
dj{22,30,31,32,dev}: {[testenv]test-executable} test content_gfk.tests --settings=settings_content_gfk
dj{22,30,31,32,dev}: {[testenv]test-executable} test customuser.tests --settings=settings_customuser
dj{22,30,31,32,dev}: {[testenv]test-executable} test namespaced.tests --settings=settings_namespaced
dj{22,30,31,32,dev}: {[testenv]test-executable} test slashless.tests --settings=settings_slashless
dj{22,30,31,32,dev}: {[testenv]test-executable} test validation.tests --settings=settings_validation
dj{22,30,31,32,dev}: {[testenv]test-executable} test gis.tests --settings=settings_gis_spatialite

docs: sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
docs: sphinx-build -W -b doctest -d {envtmpdir}/doctrees . {envtmpdir}/html
Expand All @@ -35,27 +35,23 @@ commands =

flake8-strict: {envbindir}/flake8 --ignore=E128 --max-complexity 10 .
basepython =
py27: python2.7
py35: python3.5
py36: python3.6
py37: python3.7
py38: python3.8
deps =
dj111: Django>=1.11,<1.12
dj22: Django>=2.2,<2.3
dj30: Django>=3.0,<3.1
dj31: Django>=3.1,<3.2
dj32: Django>=3.2,<3.3
djdev: https://github.com/django/django/archive/master.tar.gz

py27-dj{111}: django-oauth-plus==2.2.9
py27-dj{111}: python-digest
py27-dj{111}: oauth2
py27-dj{111}: pysqlite
py{35,36,37}-dj{111,22,30,dev}: python3-digest>=1.8b4
dj{111,22,30,dev}: -r{toxinidir}/tests/requirements.txt
py{35,36,37,38}-dj{22,30,31,32,dev}: python3-digest>=1.8b4
dj{22,30,31,32,dev}: -r{toxinidir}/tests/requirements.txt

docs: Sphinx
py27-docs: Django<2.0
py35-docs: Django<3.0
py{36,37}-docs: Django<3.1
py{35}-docs: Django~=2.2
py{36,37,38}-docs: Django<3.3
docs: mock
docs: sphinx_rtd_theme

Expand Down

0 comments on commit 3b435be

Please sign in to comment.