Browse Source

Add support for Django 3, drop support for Django 1.11

master
Samir Shah 5 years ago
parent
commit
93e7e66c2b

+ 7
- 9
.travis.yml View File

@@ -10,20 +10,20 @@ addons:
10 10
 matrix:
11 11
   fast_finish: true
12 12
   include:
13
-    - python: 3.5
14
-      env: TOXENV=py35-django111
15
-    - python: 3.6
16
-      env: TOXENV=py36-django111
17
-    - python: 3.7
18
-      env: TOXENV=py37-django111
19 13
     - python: 3.5
20 14
       env: TOXENV=py35-django22
21 15
     - python: 3.6
22 16
       env: TOXENV=py36-django22
23 17
     - python: 3.7
24 18
       env: TOXENV=py37-django22
19
+    - python: 3.8
20
+      env: TOXENV=py38-django22
21
+    - python: 3.6
22
+      env: TOXENV=py36-django30
25 23
     - python: 3.7
26
-      env: TOXENV=py37-djangomaster
24
+      env: TOXENV=py37-django30
25
+    - python: 3.8
26
+      env: TOXENV=py38-django30
27 27
 
28 28
     - python: 3.7
29 29
       env: TOXENV=lint
@@ -31,8 +31,6 @@ matrix:
31 31
       env: TOXENV=sandbox
32 32
     - python: 3.7
33 33
       env: TOXENV=docs
34
-  allow_failures:
35
-    - env: TOXENV=py37-djangomaster
36 34
 
37 35
 branches:
38 36
   only:

+ 3
- 3
docs/source/conf.py View File

@@ -22,7 +22,7 @@ import os
22 22
 import sys
23 23
 
24 24
 import django
25
-from django.utils.encoding import force_text
25
+from django.utils.encoding import force_str
26 26
 from django.utils.html import strip_tags
27 27
 
28 28
 from oscar import get_version, get_short_version
@@ -290,11 +290,11 @@ def process_docstring(app, what, name, obj, options, lines):
290 290
 
291 291
         for field in fields:
292 292
             # Decode and strip any html out of the field's help text
293
-            help_text = strip_tags(force_text(field.help_text))
293
+            help_text = strip_tags(force_str(field.help_text))
294 294
 
295 295
             # Decode and capitalize the verbose name, for use if there isn't
296 296
             # any help text
297
-            verbose_name = force_text(field.verbose_name).capitalize()
297
+            verbose_name = force_str(field.verbose_name).capitalize()
298 298
 
299 299
             if help_text:
300 300
                 # Add the model field to the end of the docstring as a param

+ 5
- 5
requirements.txt View File

@@ -1,9 +1,9 @@
1 1
 # These requirements are only necessary when developing on Oscar.
2 2
 
3 3
 # development
4
-Werkzeug==0.16.1
5
-django-debug-toolbar==2.1
6
-django-extensions==2.2.6
4
+Werkzeug>=1.0,<1.1
5
+django-debug-toolbar>=2.2,<2.3
6
+django-extensions>=2.2,<2.3
7 7
 psycopg2-binary>=2.8,<2.9
8 8
 
9 9
 # Sandbox
@@ -26,7 +26,7 @@ isort==4.3.21
26 26
 # Helpers
27 27
 pyprof2calltree==1.4.4
28 28
 ipdb==0.12.3
29
-ipython==7.11.1
29
+ipython>=7.12,<7.13
30 30
 
31 31
 # Country data
32
-pycountry==19.8.18
32
+pycountry>=19.8,<19.9

+ 8
- 8
setup.py View File

@@ -18,13 +18,13 @@ sys.path.append(os.path.join(PROJECT_DIR, 'src'))
18 18
 from oscar import get_version  # noqa isort:skip
19 19
 
20 20
 install_requires = [
21
-    'django>=1.11,<2.3',
21
+    'django>=2.2,<3.1',
22 22
     # PIL is required for image fields, Pillow is the "friendly" PIL fork
23
-    'pillow>=4.0',
23
+    'pillow>=6.0',
24 24
     # We use the ModelFormSetView from django-extra-views for the basket page
25
-    'django-extra-views>=0.11,<0.12',
25
+    'django-extra-views>=0.13,<0.14',
26 26
     # Search support
27
-    'django-haystack>=2.5.0,<3.0.0',
27
+    'django-haystack>=3.0b1',
28 28
     # Treebeard is used for categories
29 29
     'django-treebeard>=4.3.0',
30 30
     # Babel is used for currency formatting
@@ -37,14 +37,14 @@ install_requires = [
37 37
     # Used for oscar.test.newfactories
38 38
     'factory-boy>=2.4.1,<3.0',
39 39
     # Used for automatically building larger HTML tables
40
-    'django-tables2>=1.19,<2.0',
40
+    'django-tables2>=2.2,<2.3',
41 41
     # Used for manipulating form field attributes in templates (eg: add
42 42
     # a css class)
43 43
     'django-widget-tweaks>=1.4.1',
44 44
 ]
45 45
 
46
-sorl_thumbnail_version = 'sorl-thumbnail>=12.4.1,<12.5'
47
-easy_thumbnails_version = 'easy-thumbnails==2.5'
46
+sorl_thumbnail_version = 'sorl-thumbnail>=12.6,<12.7'
47
+easy_thumbnails_version = 'easy-thumbnails>=2.7,<2.8'
48 48
 
49 49
 docs_requires = [
50 50
     'Sphinx==2.2.2',
@@ -98,8 +98,8 @@ setup(
98 98
         'Development Status :: 5 - Production/Stable',
99 99
         'Environment :: Web Environment',
100 100
         'Framework :: Django',
101
-        'Framework :: Django :: 1.11',
102 101
         'Framework :: Django :: 2.2',
102
+        'Framework :: Django :: 3.0',
103 103
         'Intended Audience :: Developers',
104 104
         'License :: OSI Approved :: BSD License',
105 105
         'Operating System :: Unix',

+ 2
- 2
src/oscar/apps/basket/abstract_models.py View File

@@ -5,7 +5,7 @@ from django.conf import settings
5 5
 from django.core.exceptions import ObjectDoesNotExist, PermissionDenied
6 6
 from django.db import models
7 7
 from django.db.models import Sum
8
-from django.utils.encoding import smart_text
8
+from django.utils.encoding import smart_str
9 9
 from django.utils.timezone import now
10 10
 from django.utils.translation import gettext_lazy as _
11 11
 
@@ -875,7 +875,7 @@ class AbstractLine(models.Model):
875 875
 
876 876
     @property
877 877
     def description(self):
878
-        d = smart_text(self.product)
878
+        d = smart_str(self.product)
879 879
         ops = []
880 880
         for attribute in self.attributes.all():
881 881
             ops.append("%s = '%s'" % (attribute.option.name, attribute.value))

+ 2
- 2
src/oscar/apps/basket/views.py View File

@@ -5,7 +5,6 @@ from django.http import JsonResponse
5 5
 from django.shortcuts import redirect
6 6
 from django.template.loader import render_to_string
7 7
 from django.urls import reverse
8
-from django.utils.http import is_safe_url
9 8
 from django.utils.translation import gettext_lazy as _
10 9
 from django.views.generic import FormView, View
11 10
 from extra_views import ModelFormSetView
@@ -13,6 +12,7 @@ from extra_views import ModelFormSetView
13 12
 from oscar.apps.basket.signals import (
14 13
     basket_addition, voucher_addition, voucher_removal)
15 14
 from oscar.core import ajax
15
+from oscar.core.compat import url_has_allowed_host_and_scheme
16 16
 from oscar.core.loading import get_class, get_classes, get_model
17 17
 from oscar.core.utils import redirect_to_referrer, safe_referrer
18 18
 
@@ -302,7 +302,7 @@ class BasketAddView(FormView):
302 302
 
303 303
     def get_success_url(self):
304 304
         post_url = self.request.POST.get('next')
305
-        if post_url and is_safe_url(post_url, self.request.get_host()):
305
+        if post_url and url_has_allowed_host_and_scheme(post_url, self.request.get_host()):
306 306
             return post_url
307 307
         return safe_referrer(self.request, 'basket:summary')
308 308
 

+ 4
- 3
src/oscar/apps/catalogue/views.py View File

@@ -1,8 +1,9 @@
1
+from urllib.parse import quote
2
+
1 3
 from django.contrib import messages
2 4
 from django.core.paginator import InvalidPage
3 5
 from django.http import Http404, HttpResponsePermanentRedirect
4 6
 from django.shortcuts import get_object_or_404, redirect
5
-from django.utils.http import urlquote
6 7
 from django.utils.translation import gettext_lazy as _
7 8
 from django.views.generic import DetailView, TemplateView
8 9
 
@@ -66,7 +67,7 @@ class ProductDetailView(DetailView):
66 67
 
67 68
         if self.enforce_paths:
68 69
             expected_path = product.get_absolute_url()
69
-            if expected_path != urlquote(current_path):
70
+            if expected_path != quote(current_path):
70 71
                 return HttpResponsePermanentRedirect(expected_path)
71 72
 
72 73
     def get_context_data(self, **kwargs):
@@ -188,7 +189,7 @@ class ProductCategoryView(TemplateView):
188 189
             # Categories are fetched by primary key to allow slug changes.
189 190
             # If the slug has changed, issue a redirect.
190 191
             expected_path = category.get_absolute_url()
191
-            if expected_path != urlquote(current_path):
192
+            if expected_path != quote(current_path):
192 193
                 return HttpResponsePermanentRedirect(expected_path)
193 194
 
194 195
     def get_search_handler(self, *args, **kwargs):

+ 2
- 2
src/oscar/apps/checkout/views.py View File

@@ -1,11 +1,11 @@
1 1
 import logging
2
+from urllib.parse import quote
2 3
 
3 4
 from django import http
4 5
 from django.contrib import messages
5 6
 from django.contrib.auth import login
6 7
 from django.shortcuts import redirect
7 8
 from django.urls import reverse, reverse_lazy
8
-from django.utils.http import urlquote
9 9
 from django.utils.translation import gettext as _
10 10
 from django.views import generic
11 11
 
@@ -84,7 +84,7 @@ class IndexView(CheckoutSessionMixin, generic.FormView):
84 84
                 self.success_url = "%s?next=%s&email=%s" % (
85 85
                     reverse('customer:register'),
86 86
                     reverse('checkout:shipping-address'),
87
-                    urlquote(email)
87
+                    quote(email)
88 88
                 )
89 89
         else:
90 90
             user = form.get_user()

+ 3
- 3
src/oscar/apps/communication/notifications/views.py View File

@@ -3,7 +3,7 @@ from django.contrib import messages
3 3
 from django.utils.html import strip_tags
4 4
 from django.utils.timezone import now
5 5
 from django.utils.translation import gettext_lazy as _
6
-from django.utils.translation import ungettext
6
+from django.utils.translation import ngettext
7 7
 from django.views import generic
8 8
 
9 9
 from oscar.core.loading import get_class, get_model
@@ -86,7 +86,7 @@ class UpdateView(BulkEditMixin, generic.RedirectView):
86 86
     def archive(self, request, notifications):
87 87
         for notification in notifications:
88 88
             notification.archive()
89
-        msg = ungettext(
89
+        msg = ngettext(
90 90
             '%(count)d notification archived',
91 91
             '%(count)d notifications archived', len(notifications)) \
92 92
             % {'count': len(notifications)}
@@ -96,7 +96,7 @@ class UpdateView(BulkEditMixin, generic.RedirectView):
96 96
     def delete(self, request, notifications):
97 97
         for notification in notifications:
98 98
             notification.delete()
99
-        msg = ungettext(
99
+        msg = ngettext(
100 100
             '%(count)d notification deleted',
101 101
             '%(count)d notifications deleted', len(notifications)) \
102 102
             % {'count': len(notifications)}

+ 4
- 4
src/oscar/apps/customer/forms.py View File

@@ -8,12 +8,12 @@ from django.contrib.auth.password_validation import validate_password
8 8
 from django.contrib.sites.shortcuts import get_current_site
9 9
 from django.core.exceptions import ValidationError
10 10
 from django.utils.crypto import get_random_string
11
-from django.utils.http import is_safe_url
12 11
 from django.utils.translation import gettext_lazy as _
13 12
 from django.utils.translation import pgettext_lazy
14 13
 
15 14
 from oscar.apps.customer.utils import get_password_reset_url, normalise_email
16
-from oscar.core.compat import existing_user_fields, get_user_model
15
+from oscar.core.compat import (
16
+    existing_user_fields, get_user_model, url_has_allowed_host_and_scheme)
17 17
 from oscar.core.loading import get_class, get_model, get_profile_class
18 18
 from oscar.forms import widgets
19 19
 
@@ -74,7 +74,7 @@ class EmailAuthenticationForm(AuthenticationForm):
74 74
 
75 75
     def clean_redirect_url(self):
76 76
         url = self.cleaned_data['redirect_url'].strip()
77
-        if url and is_safe_url(url, self.host):
77
+        if url and url_has_allowed_host_and_scheme(url, self.host):
78 78
             return url
79 79
 
80 80
 
@@ -147,7 +147,7 @@ class EmailUserCreationForm(forms.ModelForm):
147 147
 
148 148
     def clean_redirect_url(self):
149 149
         url = self.cleaned_data['redirect_url'].strip()
150
-        if url and is_safe_url(url, self.host):
150
+        if url and url_has_allowed_host_and_scheme(url, self.host):
151 151
             return url
152 152
         return settings.LOGIN_REDIRECT_URL
153 153
 

+ 4
- 4
src/oscar/apps/dashboard/catalogue/tables.py View File

@@ -1,7 +1,7 @@
1 1
 from django.conf import settings
2 2
 from django.utils.safestring import mark_safe
3 3
 from django.utils.translation import gettext_lazy as _
4
-from django.utils.translation import ungettext_lazy
4
+from django.utils.translation import ngettext_lazy
5 5
 from django_tables2 import A, Column, LinkColumn, TemplateColumn
6 6
 
7 7
 from oscar.core.loading import get_class, get_model
@@ -67,7 +67,7 @@ class CategoryTable(DashboardTable):
67 67
         orderable=False)
68 68
 
69 69
     icon = "sitemap"
70
-    caption = ungettext_lazy("%s Category", "%s Categories")
70
+    caption = ngettext_lazy("%s Category", "%s Categories")
71 71
 
72 72
     class Meta(DashboardTable.Meta):
73 73
         model = Category
@@ -89,7 +89,7 @@ class AttributeOptionGroupTable(DashboardTable):
89 89
         orderable=False)
90 90
 
91 91
     icon = "sitemap"
92
-    caption = ungettext_lazy("%s Attribute Option Group", "%s Attribute Option Groups")
92
+    caption = ngettext_lazy("%s Attribute Option Group", "%s Attribute Option Groups")
93 93
 
94 94
     class Meta(DashboardTable.Meta):
95 95
         model = AttributeOptionGroup
@@ -109,7 +109,7 @@ class OptionTable(DashboardTable):
109 109
         orderable=False)
110 110
 
111 111
     icon = "reorder"
112
-    caption = ungettext_lazy("%s Option", "%s Options")
112
+    caption = ngettext_lazy("%s Option", "%s Options")
113 113
 
114 114
     class Meta(DashboardTable.Meta):
115 115
         model = Option

+ 9
- 7
src/oscar/apps/dashboard/ranges/views.py View File

@@ -9,7 +9,7 @@ from django.shortcuts import HttpResponse, get_object_or_404
9 9
 from django.template.loader import render_to_string
10 10
 from django.urls import reverse
11 11
 from django.utils.translation import gettext_lazy as _
12
-from django.utils.translation import ungettext
12
+from django.utils.translation import ngettext
13 13
 from django.views.generic import (
14 14
     CreateView, DeleteView, ListView, UpdateView, View)
15 15
 
@@ -128,9 +128,10 @@ class RangeProductListView(BulkEditMixin, ListView):
128 128
         for product in products:
129 129
             range.remove_product(product)
130 130
         num_products = len(products)
131
-        messages.success(request, ungettext("Removed %d product from range",
132
-                                            "Removed %d products from range",
133
-                                            num_products) % num_products)
131
+        messages.success(
132
+            request,
133
+            ngettext("Removed %d product from range", "Removed %d products from range", num_products) % num_products
134
+        )
134 135
         return HttpResponseRedirect(self.get_success_url(request))
135 136
 
136 137
     def add_products(self, request):
@@ -154,9 +155,10 @@ class RangeProductListView(BulkEditMixin, ListView):
154 155
             range.add_product(product)
155 156
 
156 157
         num_products = len(products)
157
-        messages.success(request, ungettext("%d product added to range",
158
-                                            "%d products added to range",
159
-                                            num_products) % num_products)
158
+        messages.success(
159
+            request,
160
+            ngettext("%d product added to range", "%d products added to range", num_products) % num_products
161
+        )
160 162
         dupe_skus = form.get_duplicate_skus()
161 163
         if dupe_skus:
162 164
             messages.warning(

+ 2
- 2
src/oscar/apps/dashboard/tables.py View File

@@ -1,9 +1,9 @@
1
-from django.utils.translation import ungettext_lazy
1
+from django.utils.translation import ngettext_lazy
2 2
 from django_tables2 import Table
3 3
 
4 4
 
5 5
 class DashboardTable(Table):
6
-    caption = ungettext_lazy('%d Row', '%d Rows')
6
+    caption = ngettext_lazy('%d Row', '%d Rows')
7 7
 
8 8
     def get_caption_display(self):
9 9
         # Allow overriding the caption with an arbitrary string that we cannot

+ 1
- 1
src/oscar/apps/dashboard/users/tables.py View File

@@ -17,7 +17,7 @@ class UserTable(DashboardTable):
17 17
     active = Column(accessor='is_active')
18 18
     staff = Column(accessor='is_staff')
19 19
     date_registered = Column(accessor='date_joined')
20
-    num_orders = Column(accessor='orders.count', orderable=False, verbose_name=_('Number of Orders'))
20
+    num_orders = Column(accessor='orders__count', orderable=False, verbose_name=_('Number of Orders'))
21 21
     actions = TemplateColumn(
22 22
         template_name='oscar/dashboard/users/user_row_actions.html',
23 23
         verbose_name=' ')

+ 3
- 1
src/oscar/apps/dashboard/users/views.py View File

@@ -25,7 +25,6 @@ User = get_user_model()
25 25
 
26 26
 class IndexView(BulkEditMixin, FormMixin, SingleTableView):
27 27
     template_name = 'oscar/dashboard/users/index.html'
28
-    table_pagination = True
29 28
     model = User
30 29
     actions = ('make_active', 'make_inactive', )
31 30
     form_class = UserSearchForm
@@ -39,6 +38,9 @@ class IndexView(BulkEditMixin, FormMixin, SingleTableView):
39 38
         self.form = self.get_form(form_class)
40 39
         return super().dispatch(request, *args, **kwargs)
41 40
 
41
+    def get_table_pagination(self, table):
42
+        return dict(per_page=settings.OSCAR_DASHBOARD_ITEMS_PER_PAGE)
43
+
42 44
     def get_form_kwargs(self):
43 45
         """
44 46
         Only bind search form if it was submitted.

+ 5
- 5
src/oscar/apps/offer/conditions.py View File

@@ -2,7 +2,7 @@ from decimal import ROUND_UP
2 2
 from decimal import Decimal as D
3 3
 
4 4
 from django.utils.translation import gettext_lazy as _
5
-from django.utils.translation import ungettext
5
+from django.utils.translation import ngettext
6 6
 
7 7
 from oscar.core.loading import get_classes, get_model
8 8
 from oscar.templatetags.currency_filters import currency
@@ -70,8 +70,8 @@ class CountCondition(Condition):
70 70
     def get_upsell_message(self, offer, basket):
71 71
         num_matches = self._get_num_matches(basket, offer)
72 72
         delta = self.value - num_matches
73
-        return ungettext('Buy %(delta)d more product from %(range)s',
74
-                         'Buy %(delta)d more products from %(range)s', delta) \
73
+        return ngettext('Buy %(delta)d more product from %(range)s',
74
+                        'Buy %(delta)d more products from %(range)s', delta) \
75 75
             % {'delta': delta, 'range': self.range}
76 76
 
77 77
     def consume_items(self, offer, basket, affected_lines):
@@ -158,8 +158,8 @@ class CoverageCondition(Condition):
158 158
 
159 159
     def get_upsell_message(self, offer, basket):
160 160
         delta = self.value - self._get_num_covered_products(basket, offer)
161
-        return ungettext('Buy %(delta)d more product from %(range)s',
162
-                         'Buy %(delta)d more products from %(range)s', delta) \
161
+        return ngettext('Buy %(delta)d more product from %(range)s',
162
+                        'Buy %(delta)d more products from %(range)s', delta) \
163 163
             % {'delta': delta, 'range': self.range}
164 164
 
165 165
     def is_partially_satisfied(self, offer, basket):

+ 1
- 8
src/oscar/core/application.py View File

@@ -1,16 +1,9 @@
1 1
 from django.apps import AppConfig
2 2
 from django.core.exceptions import ImproperlyConfigured
3
-from django.urls import reverse_lazy
3
+from django.urls import URLPattern, reverse_lazy
4 4
 
5 5
 from oscar.core.loading import feature_hidden
6 6
 
7
-try:
8
-    # Django 2
9
-    from django.urls import URLPattern
10
-except ImportError:
11
-    # Django 1.11
12
-    from django.urls.resolvers import RegexURLPattern as URLPattern
13
-
14 7
 
15 8
 class OscarConfigMixin(object):
16 9
     """

+ 8
- 0
src/oscar/core/compat.py View File

@@ -17,6 +17,14 @@ except ValueError:
17 17
                                " 'app_label.model_name'")
18 18
 
19 19
 
20
+# Backward-compatible import for url_has_allowed_host_and_scheme.
21
+try:
22
+    # Django 3.0 and above
23
+    from django.utils.http import url_has_allowed_host_and_scheme       # noqa F401
24
+except ImportError:
25
+    from django.utils.http import is_safe_url as url_has_allowed_host_and_scheme    # noqa F401
26
+
27
+
20 28
 def get_user_model():
21 29
     """
22 30
     Return the User model. Doesn't require the app cache to be fully

+ 1
- 1
src/oscar/core/loading.py View File

@@ -1,13 +1,13 @@
1 1
 import sys
2 2
 import traceback
3 3
 import warnings
4
+from functools import lru_cache
4 5
 from importlib import import_module
5 6
 
6 7
 from django.apps import apps
7 8
 from django.apps.config import MODELS_MODULE_NAME
8 9
 from django.conf import settings
9 10
 from django.core.exceptions import AppRegistryNotReady
10
-from django.utils.lru_cache import lru_cache
11 11
 from django.utils.module_loading import import_string
12 12
 
13 13
 from oscar.core.exceptions import (

+ 3
- 2
src/oscar/core/utils.py View File

@@ -8,12 +8,13 @@ from babel.dates import format_timedelta as format_td
8 8
 from django.conf import settings
9 9
 from django.shortcuts import redirect, resolve_url
10 10
 from django.template.defaultfilters import date as date_filter
11
-from django.utils.http import is_safe_url
12 11
 from django.utils.module_loading import import_string
13 12
 from django.utils.text import slugify as django_slugify
14 13
 from django.utils.timezone import get_current_timezone, is_naive, make_aware
15 14
 from django.utils.translation import get_language, to_locale
16 15
 
16
+from oscar.core.compat import url_has_allowed_host_and_scheme
17
+
17 18
 SLUGIFY_RE = re.compile(r'[^\w\s-]', re.UNICODE)
18 19
 
19 20
 
@@ -140,7 +141,7 @@ def safe_referrer(request, default):
140 141
     or a regular URL
141 142
     """
142 143
     referrer = request.META.get('HTTP_REFERER')
143
-    if referrer and is_safe_url(referrer, request.get_host()):
144
+    if referrer and url_has_allowed_host_and_scheme(referrer, request.get_host()):
144 145
         return referrer
145 146
     if default:
146 147
         # Try to resolve. Can take a model instance, Django URL name or URL.

+ 5
- 5
src/oscar/forms/widgets.py View File

@@ -5,7 +5,7 @@ from django.core.files.uploadedfile import InMemoryUploadedFile
5 5
 from django.forms.models import ModelChoiceIterator
6 6
 from django.forms.widgets import FileInput
7 7
 from django.utils import formats
8
-from django.utils.encoding import force_text
8
+from django.utils.encoding import force_str
9 9
 
10 10
 
11 11
 class ImageInput(FileInput):
@@ -208,12 +208,12 @@ class AdvancedSelect(forms.Select):
208 208
     """
209 209
 
210 210
     def __init__(self, attrs=None, choices=(), disabled_values=()):
211
-        self.disabled_values = set(force_text(v) for v in disabled_values)
211
+        self.disabled_values = set(force_str(v) for v in disabled_values)
212 212
         super().__init__(attrs, choices)
213 213
 
214 214
     def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
215 215
         option = super().create_option(name, value, label, selected, index, subindex, attrs)
216
-        if force_text(value) in self.disabled_values:
216
+        if force_str(value) in self.disabled_values:
217 217
             option['attrs']['disabled'] = True
218 218
         return option
219 219
 
@@ -254,7 +254,7 @@ class RemoteSelect(forms.Select):
254 254
         default = (None, [], 0)
255 255
         groups = [default]
256 256
         has_selected = False
257
-        selected_choices = {force_text(v) for v in value}
257
+        selected_choices = {force_str(v) for v in value}
258 258
         if not self.is_required and not self.allow_multiple_selected:
259 259
             default[1].append(self.create_option(name, '', '', False, 0))
260 260
 
@@ -266,7 +266,7 @@ class RemoteSelect(forms.Select):
266 266
             c for c in selected_choices if c not in self.choices.field.empty_values
267 267
         }
268 268
         choices = (
269
-            (obj.pk, force_text(obj))
269
+            (obj.pk, force_str(obj))
270 270
             for obj in self.choices.queryset.filter(pk__in=selected_choices)
271 271
         )
272 272
         for option_value, option_label in choices:

+ 2
- 6
src/oscar/models/fields/autoslugfield.py View File

@@ -27,16 +27,12 @@ THE SOFTWARE.
27 27
 import re
28 28
 
29 29
 from django.conf import settings
30
+from django.utils.encoding import force_str
30 31
 
31 32
 from oscar.core.utils import slugify
32 33
 
33 34
 from .slugfield import SlugField
34 35
 
35
-try:
36
-    from django.utils.encoding import force_unicode  # NOQA
37
-except ImportError:
38
-    from django.utils.encoding import force_text as force_unicode  # NOQA
39
-
40 36
 
41 37
 class AutoSlugField(SlugField):
42 38
     """ AutoSlugField
@@ -169,7 +165,7 @@ class AutoSlugField(SlugField):
169 165
         return slug
170 166
 
171 167
     def pre_save(self, model_instance, add):
172
-        value = force_unicode(self.create_slug(model_instance, add))
168
+        value = force_str(self.create_slug(model_instance, add))
173 169
         setattr(model_instance, self.attname, value)
174 170
         return value
175 171
 

+ 1
- 2
src/oscar/templatetags/image_tags.py View File

@@ -6,7 +6,6 @@ from django.conf import settings
6 6
 from django.db.models.fields.files import ImageFieldFile
7 7
 from django.utils.encoding import smart_str
8 8
 from django.utils.html import escape
9
-from django.utils.six import text_type
10 9
 
11 10
 from oscar.core.thumbnails import get_thumbnailer
12 11
 
@@ -120,7 +119,7 @@ class ThumbnailNode(template.Node):
120 119
         source = self.source_var.resolve(context)
121 120
         options = self.get_thumbnail_options(context)
122 121
         for key, expr in self.options:
123
-            value = self.no_resolve.get(text_type(expr), expr.resolve(context))
122
+            value = self.no_resolve.get(str(expr), expr.resolve(context))
124 123
             options[key] = value
125 124
 
126 125
         thumbnailer = get_thumbnailer()

+ 3
- 8
tests/functional/checkout/test_guest_checkout.py View File

@@ -1,12 +1,13 @@
1 1
 import sys
2 2
 from http import client as http_client
3
+from imp import reload
3 4
 from importlib import import_module
4 5
 from unittest import mock
6
+from urllib.parse import quote
5 7
 
6 8
 from django.conf import settings
7 9
 from django.test.utils import override_settings
8 10
 from django.urls import clear_url_caches, reverse
9
-from django.utils.http import urlquote
10 11
 
11 12
 from oscar.apps.shipping import methods
12 13
 from oscar.core.compat import get_user_model
@@ -27,12 +28,6 @@ Basket = get_model('basket', 'Basket')
27 28
 Order = get_model('order', 'Order')
28 29
 User = get_user_model()
29 30
 
30
-# Python 3 compat
31
-try:
32
-    from imp import reload
33
-except ImportError:
34
-    pass
35
-
36 31
 
37 32
 def reload_url_conf():
38 33
     # Reload URLs to pick up the overridden settings
@@ -77,7 +72,7 @@ class TestIndexView(CheckoutMixin, WebTestCase):
77 72
         expected_url = '{register_url}?next={forward}&email={email}'.format(
78 73
             register_url=reverse('customer:register'),
79 74
             forward='/checkout/shipping-address/',
80
-            email=urlquote(new_user_email))
75
+            email=quote(new_user_email))
81 76
         self.assertRedirects(response, expected_url)
82 77
 
83 78
     def test_redirects_existing_customers_to_shipping_address_page(self):

+ 2
- 2
tests/integration/core/test_compat.py View File

@@ -6,7 +6,7 @@ import os
6 6
 from tempfile import NamedTemporaryFile
7 7
 
8 8
 from django.test import TestCase, override_settings
9
-from django.utils.encoding import smart_text
9
+from django.utils.encoding import smart_str
10 10
 
11 11
 from oscar.core.compat import UnicodeCSVWriter, existing_user_fields
12 12
 
@@ -51,7 +51,7 @@ class TestUnicodeCSVWriter(TestCase):
51 51
             writer.writerows([row])
52 52
 
53 53
         with open(tmp_file.name, 'r') as read_file:
54
-            content = smart_text(read_file.read(), encoding='utf-8').strip()
54
+            content = smart_str(read_file.read(), encoding='utf-8').strip()
55 55
             self.assertEqual(content, 'ünįcodē,123,foo-bar')
56 56
 
57 57
         # Clean up

+ 2
- 10
tox.ini View File

@@ -1,7 +1,6 @@
1 1
 [tox]
2 2
 envlist =
3
-    py{35,36,37}-django{111,22}
4
-    py36-djangomaster
3
+    py{35,36,37,38}-django{22,30}
5 4
     lint
6 5
     sandbox
7 6
     docs
@@ -12,15 +11,8 @@ commands = coverage run --parallel -m pytest {posargs}
12 11
 extras = test
13 12
 pip_pre = true
14 13
 deps =
15
-    django111: django>=1.11,<2
16 14
     django22: django>=2.2,<2.3
17
-
18
-
19
-[testenv:py36-djangomaster]
20
-commands =
21
-    # Can't specify this in deps - see https://github.com/tox-dev/tox/issues/513
22
-    pip install https://github.com/django/django/archive/master.tar.gz#egg=django
23
-    pytest {posargs}
15
+    django30: django>=3.0,<3.1
24 16
 
25 17
 
26 18
 [testenv:lint]

Loading…
Cancel
Save