diff --git a/.travis.yml b/.travis.yml index dd3ffbd..5558ac0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: python python: - - "2.7" - "3.6" addons: postgresql: "9.4" diff --git a/README.rst b/README.rst index e89c187..c5ad279 100644 --- a/README.rst +++ b/README.rst @@ -11,6 +11,11 @@ Django app that uses JWT to manage one-time and expiring tokens to protect URLs. This app currently requires the use of PostgreSQL. +Compatibility +============= + +This library is now Python3 and Django1.11 and above only. If you are on Python2 then you will have to refer to the python2 branch. + Background ========== diff --git a/request_token/__init__.py b/request_token/__init__.py index 2cf15f4..067ee38 100644 --- a/request_token/__init__.py +++ b/request_token/__init__.py @@ -1,2 +1 @@ -# -*- coding: utf-8 -*- default_app_config = 'request_token.apps.RequestTokenAppConfig' diff --git a/request_token/admin.py b/request_token/admin.py index 100f5dc..66cd513 100644 --- a/request_token/admin.py +++ b/request_token/admin.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import json from django.contrib import admin diff --git a/request_token/apps.py b/request_token/apps.py index 2d30524..aee569c 100644 --- a/request_token/apps.py +++ b/request_token/apps.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.apps import AppConfig from django.core.exceptions import ImproperlyConfigured from django.template import loader, TemplateDoesNotExist diff --git a/request_token/compat.py b/request_token/compat.py deleted file mode 100644 index 9847d2b..0000000 --- a/request_token/compat.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- coding: utf-8 -*- -# try: -# from django.urls import reverse, resolve -# except ImportError: -# from django.core.urlresolvers import reverse, resolve # noqa - -try: - from unittest import mock -except ImportError: - import mock # noqa - -try: - from django.utils.deprecation import MiddlewareMixin -except ImportError: - MiddlewareMixin = object diff --git a/request_token/decorators.py b/request_token/decorators.py index b9c0657..bba252c 100644 --- a/request_token/decorators.py +++ b/request_token/decorators.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import functools import logging diff --git a/request_token/exceptions.py b/request_token/exceptions.py index 5fbee6f..0e4a0cc 100644 --- a/request_token/exceptions.py +++ b/request_token/exceptions.py @@ -1,4 +1,3 @@ -# -*- coding: utf8 -*- """ Local exceptions related to tokens inherit from the PyJWT base InvalidTokenError. diff --git a/request_token/middleware.py b/request_token/middleware.py index 20cb346..715a45f 100644 --- a/request_token/middleware.py +++ b/request_token/middleware.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import logging from django.http import HttpResponseForbidden, HttpResponseNotAllowed @@ -6,7 +5,6 @@ from jwt.exceptions import InvalidTokenError -from .compat import MiddlewareMixin from .models import RequestToken from .settings import JWT_QUERYSTRING_ARG, FOUR03_TEMPLATE from .utils import decode @@ -14,7 +12,7 @@ logger = logging.getLogger(__name__) -class RequestTokenMiddleware(MiddlewareMixin): +class RequestTokenMiddleware: """ Extract and verify request tokens from incoming GET requests. @@ -24,7 +22,10 @@ class RequestTokenMiddleware(MiddlewareMixin): """ - def process_request(self, request): + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): """Verify JWT request querystring arg. If a token is found (using JWT_QUERYSTRING_ARG), then it is decoded, @@ -54,7 +55,7 @@ def process_request(self, request): token = request.GET.get(JWT_QUERYSTRING_ARG) if token is None: - return + return self.get_response(request) if request.method != 'GET': return HttpResponseNotAllowed(['GET']) @@ -72,6 +73,8 @@ def process_request(self, request): request.token = None logger.exception("RequestToken cannot be decoded: %s", token) + return self.get_response(request) + def process_exception(self, request, exception): """Handle all InvalidTokenErrors.""" if isinstance(exception, InvalidTokenError): diff --git a/request_token/migrations/0001_initial.py b/request_token/migrations/0001_initial.py index dacfbbc..bc4802a 100644 --- a/request_token/migrations/0001_initial.py +++ b/request_token/migrations/0001_initial.py @@ -23,7 +23,7 @@ class Migration(migrations.Migration): ('issued_at', models.DateTimeField(help_text='Time the token was created, set in the initial save.', null=True, blank=True)), ('max_uses', models.IntegerField(default=1, help_text='Cap on the number of times the token can be used, defaults to 1 (single use).')), ('used_to_date', models.IntegerField(default=0, help_text='Denormalised count of the number times the token has been used.')), - ('user', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, help_text='Intended recipient of the JWT.', null=True)), + ('user', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, help_text='Intended recipient of the JWT.', null=True, on_delete=models.deletion.CASCADE)), ], ), migrations.CreateModel( @@ -33,8 +33,8 @@ class Migration(migrations.Migration): ('user_agent', models.TextField(help_text='User-agent of client used to make the request.', blank=True)), ('client_ip', models.CharField(help_text='Client IP of device used to make the request.', max_length=15)), ('timestamp', models.DateTimeField(help_text='Time the request was logged.')), - ('token', models.ForeignKey(help_text='The RequestToken that was used.', to='request_token.RequestToken')), - ('user', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, help_text='The user who made the request (None if anonymous).', null=True)), + ('token', models.ForeignKey(help_text='The RequestToken that was used.', to='request_token.RequestToken', on_delete=models.deletion.CASCADE)), + ('user', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, help_text='The user who made the request (None if anonymous).', null=True, on_delete=models.deletion.CASCADE)), ], ), ] diff --git a/request_token/migrations/0003_auto_20151229_1105.py b/request_token/migrations/0003_auto_20151229_1105.py index 55b9454..bb05943 100644 --- a/request_token/migrations/0003_auto_20151229_1105.py +++ b/request_token/migrations/0003_auto_20151229_1105.py @@ -46,6 +46,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='requesttoken', name='user', - field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, help_text='Intended recipient of the JWT (can be used by anyone if not set).', null=True), + field=models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, help_text='Intended recipient of the JWT (can be used by anyone if not set).', null=True, on_delete=models.deletion.CASCADE), ), ] diff --git a/request_token/migrations/0008_convert_token_data_to_jsonfield.py b/request_token/migrations/0008_convert_token_data_to_jsonfield.py index feff160..c261e18 100644 --- a/request_token/migrations/0008_convert_token_data_to_jsonfield.py +++ b/request_token/migrations/0008_convert_token_data_to_jsonfield.py @@ -4,7 +4,6 @@ import django.contrib.postgres.fields.jsonb from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): @@ -22,6 +21,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='requesttokenlog', name='token', - field=models.ForeignKey(help_text='The RequestToken that was used.', on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='request_token.RequestToken'), + field=models.ForeignKey(help_text='The RequestToken that was used.', on_delete=models.deletion.CASCADE, related_name='logs', to='request_token.RequestToken'), ), ] diff --git a/request_token/migrations/0010_auto_20170521_1944.py b/request_token/migrations/0010_auto_20170521_1944.py index eddd909..3e47aac 100644 --- a/request_token/migrations/0010_auto_20170521_1944.py +++ b/request_token/migrations/0010_auto_20170521_1944.py @@ -4,7 +4,6 @@ from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): @@ -17,6 +16,6 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='requesttoken', name='user', - field=models.ForeignKey(blank=True, help_text='Intended recipient of the JWT (can be used by anyone if not set).', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='request_tokens', to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(blank=True, help_text='Intended recipient of the JWT (can be used by anyone if not set).', null=True, on_delete=models.deletion.CASCADE, related_name='request_tokens', to=settings.AUTH_USER_MODEL), ), ] diff --git a/request_token/models.py b/request_token/models.py index f08a8bd..fc7789b 100644 --- a/request_token/models.py +++ b/request_token/models.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """request_token models.""" from __future__ import unicode_literals @@ -86,6 +85,7 @@ class RequestToken(models.Model): settings.AUTH_USER_MODEL, related_name="request_tokens", blank=True, null=True, + on_delete=models.CASCADE, help_text="Intended recipient of the JWT (can be used by anyone if not set)." ) scope = models.CharField( @@ -238,7 +238,8 @@ def validate_max_uses(self): def _auth_is_anonymous(self, request): """Authenticate anonymous requests.""" - assert request.user.is_anonymous(), 'User is authenticated.' + if request.user.is_authenticated: + raise InvalidAudienceError('Token requires anonymous user.') if self.login_mode == RequestToken.LOGIN_MODE_NONE: pass @@ -265,7 +266,8 @@ def _auth_is_anonymous(self, request): def _auth_is_authenticated(self, request): """Authenticate requests with existing users.""" - assert request.user.is_authenticated(), 'User is anonymous.' + if request.user.is_anonymous: + raise InvalidAudienceError('Token requires authenticated user.') if self.login_mode == RequestToken.LOGIN_MODE_NONE: return request @@ -285,7 +287,7 @@ def authenticate(self, request): has a user assigned, then this will be added to the request. """ - if request.user.is_anonymous(): + if request.user.is_anonymous: return self._auth_is_anonymous(request) else: return self._auth_is_authenticated(request) @@ -312,7 +314,7 @@ def rmg(key, default=None): log = RequestTokenLog( token=self, - user=None if request.user.is_anonymous() else request.user, + user=None if request.user.is_anonymous else request.user, user_agent=rmg('HTTP_USER_AGENT', 'unknown'), client_ip=parse_xff(rmg('HTTP_X_FORWARDED_FOR')) or rmg('REMOTE_ADDR', None), status_code=response.status_code @@ -355,11 +357,13 @@ class RequestTokenLog(models.Model): RequestToken, related_name='logs', help_text="The RequestToken that was used.", + on_delete=models.CASCADE, db_index=True ) user = models.ForeignKey( settings.AUTH_USER_MODEL, blank=True, null=True, + on_delete=models.CASCADE, help_text="The user who made the request (None if anonymous)." ) user_agent = models.TextField( @@ -421,12 +425,14 @@ class RequestTokenErrorLog(models.Model): token = models.ForeignKey( RequestToken, related_name='errors', + on_delete=models.CASCADE, help_text="The RequestToken that was used.", db_index=True ) log = models.OneToOneField( RequestTokenLog, related_name='error', + on_delete=models.CASCADE, help_text="The token use against which the error occurred.", db_index=True ) diff --git a/request_token/settings.py b/request_token/settings.py index 1d60fd6..740f00b 100644 --- a/request_token/settings.py +++ b/request_token/settings.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from os import getenv from django.conf import settings diff --git a/request_token/tests/test_admin.py b/request_token/tests/test_admin.py index 98c0200..e044deb 100644 --- a/request_token/tests/test_admin.py +++ b/request_token/tests/test_admin.py @@ -1,5 +1,5 @@ -# -*- coding: utf-8 -*- import datetime +from unittest import mock from jwt.exceptions import MissingRequiredClaimError @@ -7,7 +7,6 @@ from django.utils.timezone import now as tz_now from ..admin import pretty_print, RequestTokenAdmin -from ..compat import mock from ..models import RequestToken diff --git a/request_token/tests/test_apps.py b/request_token/tests/test_apps.py index 3100c5d..b3f9b2d 100644 --- a/request_token/tests/test_apps.py +++ b/request_token/tests/test_apps.py @@ -1,9 +1,9 @@ -# -*- coding: utf-8 -*- +from unittest import mock + from django.template import TemplateDoesNotExist from django.test import TestCase from ..apps import check_template, ImproperlyConfigured -from ..compat import mock class AppTests(TestCase): diff --git a/request_token/tests/test_decorators.py b/request_token/tests/test_decorators.py index f4142a3..1b1276f 100644 --- a/request_token/tests/test_decorators.py +++ b/request_token/tests/test_decorators.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.contrib.auth.models import AnonymousUser from django.http import HttpResponse, HttpRequest from django.test import TestCase, RequestFactory @@ -41,14 +40,14 @@ class DecoratorTests(TestCase): def setUp(self): self.factory = RequestFactory() - self.middleware = RequestTokenMiddleware() + self.middleware = RequestTokenMiddleware(get_response=lambda r: r) def _request(self, path, token, user): path = path + "?%s=%s" % (JWT_QUERYSTRING_ARG, token) if token else path request = self.factory.get(path) request.session = MockSession() request.user = user - self.middleware.process_request(request) + self.middleware(request) return request def test_no_token(self): diff --git a/request_token/tests/test_middleware.py b/request_token/tests/test_middleware.py index 5b16235..4d3fde3 100644 --- a/request_token/tests/test_middleware.py +++ b/request_token/tests/test_middleware.py @@ -1,12 +1,13 @@ -# -*- coding: utf-8 -*- +from unittest import mock + +from jwt import exceptions + from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser -from django.http import HttpResponseNotAllowed, HttpResponseForbidden +from django.http import HttpResponseNotAllowed from django.test import TestCase, RequestFactory -from jwt import exceptions -from ..compat import mock from ..middleware import RequestTokenMiddleware from ..models import RequestToken from ..settings import JWT_QUERYSTRING_ARG @@ -28,43 +29,37 @@ class MiddlewareTests(TestCase): def setUp(self): self.user = get_user_model().objects.create_user('zoidberg') self.factory = RequestFactory() - self.middleware = RequestTokenMiddleware() + self.middleware = RequestTokenMiddleware(get_response=lambda r: r) self.token = RequestToken.objects.create_token(scope="foo") def get_request(self): request = self.factory.get('/?%s=%s' % (JWT_QUERYSTRING_ARG, self.token.jwt())) request.user = self.user request.session = MockSession() - # request.token = self.token return request def test_process_request_assertions(self): request = self.factory.get('/') - middleware = self.middleware - - process_request = middleware.process_request - self.assertRaises(AssertionError, process_request, request) + self.assertRaises(AssertionError, self.middleware, request) request.user = AnonymousUser() - self.assertRaises(AssertionError, process_request, request) + self.assertRaises(AssertionError, self.middleware, request) request.session = MockSession() - self.assertIsNone(process_request(request)) + self.middleware(request) self.assertFalse(hasattr(request, 'token')) def test_process_request_without_token(self): request = self.factory.post('/') - process_request = self.middleware.process_request request = self.factory.get('/') request.user = AnonymousUser() request.session = MockSession() - self.assertIsNone(process_request(request)) + self.middleware(request) self.assertFalse(hasattr(request, 'token')) def test_process_request_with_valid_token(self): request = self.get_request() - response = self.middleware.process_request(request) - self.assertIsNone(response) + self.middleware(request) self.assertEqual(request.token, self.token) def test_process_request_not_allowed(self): @@ -72,7 +67,7 @@ def test_process_request_not_allowed(self): request = self.factory.post('/?rt=foo') request.user = self.user request.session = MockSession() - response = self.middleware.process_request(request) + response = self.middleware(request) self.assertIsInstance(response, HttpResponseNotAllowed) self.assertFalse(hasattr(request, 'token')) self.assertEqual(response.status_code, 405) @@ -83,8 +78,7 @@ def test_process_request_token_error(self, mock_logger): request = self.factory.get('/?rt=foo') request.user = self.user request.session = MockSession() - response = self.middleware.process_request(request) - self.assertIsNone(response) + self.middleware(request) self.assertIsNone(request.token) self.assertEqual(mock_logger.exception.call_count, 1) @@ -92,8 +86,7 @@ def test_process_request_token_error(self, mock_logger): def test_process_request_token_does_not_exist(self, mock_logger): request = self.get_request() self.token.delete() - response = self.middleware.process_request(request) - self.assertIsNone(response) + self.middleware(request) self.assertIsNone(request.token) self.assertEqual(mock_logger.exception.call_count, 1) diff --git a/request_token/tests/test_migrations.py b/request_token/tests/test_migrations.py index 2425e63..2548a8e 100644 --- a/request_token/tests/test_migrations.py +++ b/request_token/tests/test_migrations.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.apps import apps from django.db import connection from django.db.migrations.autodetector import MigrationAutodetector diff --git a/request_token/tests/test_models.py b/request_token/tests/test_models.py index 64c29ea..fa7811d 100644 --- a/request_token/tests/test_models.py +++ b/request_token/tests/test_models.py @@ -1,6 +1,5 @@ -# -*- coding: utf-8 -*- import datetime -import six +from unittest import mock from jwt.exceptions import InvalidAudienceError @@ -13,7 +12,6 @@ from django.test import TestCase, RequestFactory from django.utils.timezone import now as tz_now -from ..compat import mock from ..exceptions import MaxUseError from ..models import ( parse_xff, @@ -54,8 +52,6 @@ def test_string_repr(self): token = RequestToken(user=self.user) self.assertIsNotNone(str(token)) self.assertIsNotNone(repr(token)) - if six.PY2: - self.assertIsNotNone(unicode(token)) def test_save(self): token = RequestToken().save() @@ -267,6 +263,10 @@ def test__auth_is_anonymous(self): self.assertEqual(request.user, user1) self.assertEqual(token.user.backend, 'django.contrib.auth.backends.ModelBackend') + # authenticated user fails + request.user = user1 + self.assertRaises(InvalidAudienceError, token._auth_is_anonymous, request) + def test__auth_is_authenticated(self): factory = RequestFactory() middleware = SessionMiddleware() @@ -300,6 +300,10 @@ def test__auth_is_authenticated(self): token.user = get_user_model().objects.create_user(username="Hyde") self.assertRaises(InvalidAudienceError, token._auth_is_authenticated, request) + # anonymous user fails + request.user = AnonymousUser() + self.assertRaises(InvalidAudienceError, token._auth_is_authenticated, request) + def test_authenticate(self): factory = RequestFactory() middleware = SessionMiddleware() @@ -399,8 +403,6 @@ def test_defaults(self): token = RequestToken(user=self.user) self.assertIsNotNone(str(token)) self.assertIsNotNone(repr(token)) - if six.PY2: - self.assertIsNotNone(unicode(token)) def test_string_repr(self): log = RequestTokenLog( @@ -409,14 +411,10 @@ def test_string_repr(self): ) self.assertIsNotNone(str(log)) self.assertIsNotNone(repr(log)) - if six.PY2: - self.assertIsNotNone(unicode(log)) log.user = None self.assertIsNotNone(str(log)) self.assertIsNotNone(repr(log)) - if six.PY2: - self.assertIsNotNone(unicode(log)) def test_save(self): log = RequestTokenLog( diff --git a/request_token/tests/test_utils.py b/request_token/tests/test_utils.py index 0819293..4fcbb1d 100644 --- a/request_token/tests/test_utils.py +++ b/request_token/tests/test_utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import datetime from jwt import decode as jwt_decode, encode as jwt_encode diff --git a/request_token/utils.py b/request_token/utils.py index d69cf47..24f9c68 100644 --- a/request_token/utils.py +++ b/request_token/utils.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Basic encode/decode utils, taken from PyJWT.""" import calendar diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9695d63 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +# no versions pinned - recommendation is to install your +# preferred Django version first, then run this requirements file. +Django +PyJWT +sqlparse +psycopg2 \ No newline at end of file diff --git a/settings.py b/settings.py index 079b1af..b809509 100644 --- a/settings.py +++ b/settings.py @@ -7,6 +7,7 @@ DJANGO_VERSION = StrictVersion(django.get_version()) +assert DJANGO_VERSION >= StrictVersion('1.11') DEBUG = True @@ -33,20 +34,15 @@ 'test_app' ) -_MIDDLEWARE_CLASSES = [ - 'django.contrib.sessions.middleware.SessionMiddleware', +MIDDLEWARE = [ 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'request_token.middleware.RequestTokenMiddleware', ] -if DJANGO_VERSION < StrictVersion('1.10.0'): - MIDDLEWARE_CLASSES = _MIDDLEWARE_CLASSES -else: - MIDDLEWARE = _MIDDLEWARE_CLASSES - TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', @@ -70,6 +66,8 @@ }, ] +ALLOWED_HOSTS = ['localhost', '127.0.0.1'] + SECRET_KEY = "request_token" ROOT_URLCONF = 'urls' diff --git a/setup.py b/setup.py index 93a9b2e..babd040 100644 --- a/setup.py +++ b/setup.py @@ -9,10 +9,10 @@ setup( name="django-request-token", - version="0.7.5", + version="0.8", packages=find_packages(), install_requires=[ - 'Django>=1.9', + 'Django>=1.11', 'PyJWT>=1.4', 'sqlparse>=0.2', 'psycopg2>=2.7' @@ -29,14 +29,12 @@ classifiers=[ 'Environment :: Web Environment', 'Framework :: Django', - 'Framework :: Django :: 1.9', - 'Framework :: Django :: 1.10', 'Framework :: Django :: 1.11', + 'Framework :: Django :: 2.0', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.6', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', diff --git a/test_app/tests.py b/test_app/tests.py index f576049..56a6c82 100644 --- a/test_app/tests.py +++ b/test_app/tests.py @@ -1,11 +1,10 @@ -# -*- coding: utf-8 -*- """request_token decorator tests.""" from datetime import datetime, timedelta from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser -from django.core.urlresolvers import reverse from django.test import TransactionTestCase, Client +from django.urls import reverse from request_token.models import RequestToken, RequestTokenLog from request_token.settings import JWT_QUERYSTRING_ARG, JWT_SESSION_TOKEN_EXPIRY @@ -13,7 +12,7 @@ def get_url(url_name, token): """Helper to format urls with tokens.""" - url = reverse('testing:%s' % url_name) + url = reverse('test_app:%s' % url_name) if token: url += '?%s=%s' % (JWT_QUERYSTRING_ARG, token.jwt()) return url diff --git a/test_app/urls.py b/test_app/urls.py index 74e215c..7313752 100644 --- a/test_app/urls.py +++ b/test_app/urls.py @@ -1,9 +1,13 @@ -# -*- coding: utf-8 -*- -from django.conf.urls import url +try: + from django.urls import re_path +except ImportError: + from django.conf.urls import url as re_path from .views import decorated, undecorated +app_name = 'test_app' + urlpatterns = [ - url(r'^decorated/$', decorated, name="decorated"), - url(r'^undecorated/$', undecorated, name="undecorated"), + re_path(r'^decorated/$', decorated, name="decorated"), + re_path(r'^undecorated/$', undecorated, name="undecorated"), ] diff --git a/tox.ini b/tox.ini index a312af6..95a014f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,16 +1,14 @@ [tox] -envlist = py{27,36}-django{19,110,111} +envlist = py{36}-django{111,20} [testenv] deps = coverage==4.2 - mock==2.0 - django19: Django==1.9 - django110: Django==1.10 django111: Django==1.11 + django20: Django==2.0 commands = python --version coverage erase - coverage run --branch manage.py test test_app request_token + coverage run --branch manage.py test request_token test_app coverage report -m diff --git a/urls.py b/urls.py index c6ad269..6939c55 100644 --- a/urls.py +++ b/urls.py @@ -1,10 +1,13 @@ -# -*- coding: utf-8 -*- -from django.conf.urls import url, include from django.contrib import admin +try: + from django.urls import re_path, include +except ImportError: + from django.conf.urls import url as re_path + from django.conf.urls import include admin.autodiscover() urlpatterns = [ - url(r'^admin/', include(admin.site.urls)), - url(r'^testing/', include('test_app.urls', namespace="testing")), + re_path(r'^admin/', admin.site.urls), + re_path(r'^testing/', include('test_app.urls')), ]