浏览代码

Add support for Django 3, drop support for Django 1.11

master
Samir Shah 5 年前
父节点
当前提交
93e7e66c2b

+ 7
- 9
.travis.yml 查看文件

10
 matrix:
10
 matrix:
11
   fast_finish: true
11
   fast_finish: true
12
   include:
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
     - python: 3.5
13
     - python: 3.5
20
       env: TOXENV=py35-django22
14
       env: TOXENV=py35-django22
21
     - python: 3.6
15
     - python: 3.6
22
       env: TOXENV=py36-django22
16
       env: TOXENV=py36-django22
23
     - python: 3.7
17
     - python: 3.7
24
       env: TOXENV=py37-django22
18
       env: TOXENV=py37-django22
19
+    - python: 3.8
20
+      env: TOXENV=py38-django22
21
+    - python: 3.6
22
+      env: TOXENV=py36-django30
25
     - python: 3.7
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
     - python: 3.7
28
     - python: 3.7
29
       env: TOXENV=lint
29
       env: TOXENV=lint
31
       env: TOXENV=sandbox
31
       env: TOXENV=sandbox
32
     - python: 3.7
32
     - python: 3.7
33
       env: TOXENV=docs
33
       env: TOXENV=docs
34
-  allow_failures:
35
-    - env: TOXENV=py37-djangomaster
36
 
34
 
37
 branches:
35
 branches:
38
   only:
36
   only:

+ 3
- 3
docs/source/conf.py 查看文件

22
 import sys
22
 import sys
23
 
23
 
24
 import django
24
 import django
25
-from django.utils.encoding import force_text
25
+from django.utils.encoding import force_str
26
 from django.utils.html import strip_tags
26
 from django.utils.html import strip_tags
27
 
27
 
28
 from oscar import get_version, get_short_version
28
 from oscar import get_version, get_short_version
290
 
290
 
291
         for field in fields:
291
         for field in fields:
292
             # Decode and strip any html out of the field's help text
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
             # Decode and capitalize the verbose name, for use if there isn't
295
             # Decode and capitalize the verbose name, for use if there isn't
296
             # any help text
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
             if help_text:
299
             if help_text:
300
                 # Add the model field to the end of the docstring as a param
300
                 # Add the model field to the end of the docstring as a param

+ 5
- 5
requirements.txt 查看文件

1
 # These requirements are only necessary when developing on Oscar.
1
 # These requirements are only necessary when developing on Oscar.
2
 
2
 
3
 # development
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
 psycopg2-binary>=2.8,<2.9
7
 psycopg2-binary>=2.8,<2.9
8
 
8
 
9
 # Sandbox
9
 # Sandbox
26
 # Helpers
26
 # Helpers
27
 pyprof2calltree==1.4.4
27
 pyprof2calltree==1.4.4
28
 ipdb==0.12.3
28
 ipdb==0.12.3
29
-ipython==7.11.1
29
+ipython>=7.12,<7.13
30
 
30
 
31
 # Country data
31
 # Country data
32
-pycountry==19.8.18
32
+pycountry>=19.8,<19.9

+ 8
- 8
setup.py 查看文件

18
 from oscar import get_version  # noqa isort:skip
18
 from oscar import get_version  # noqa isort:skip
19
 
19
 
20
 install_requires = [
20
 install_requires = [
21
-    'django>=1.11,<2.3',
21
+    'django>=2.2,<3.1',
22
     # PIL is required for image fields, Pillow is the "friendly" PIL fork
22
     # PIL is required for image fields, Pillow is the "friendly" PIL fork
23
-    'pillow>=4.0',
23
+    'pillow>=6.0',
24
     # We use the ModelFormSetView from django-extra-views for the basket page
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
     # Search support
26
     # Search support
27
-    'django-haystack>=2.5.0,<3.0.0',
27
+    'django-haystack>=3.0b1',
28
     # Treebeard is used for categories
28
     # Treebeard is used for categories
29
     'django-treebeard>=4.3.0',
29
     'django-treebeard>=4.3.0',
30
     # Babel is used for currency formatting
30
     # Babel is used for currency formatting
37
     # Used for oscar.test.newfactories
37
     # Used for oscar.test.newfactories
38
     'factory-boy>=2.4.1,<3.0',
38
     'factory-boy>=2.4.1,<3.0',
39
     # Used for automatically building larger HTML tables
39
     # Used for automatically building larger HTML tables
40
-    'django-tables2>=1.19,<2.0',
40
+    'django-tables2>=2.2,<2.3',
41
     # Used for manipulating form field attributes in templates (eg: add
41
     # Used for manipulating form field attributes in templates (eg: add
42
     # a css class)
42
     # a css class)
43
     'django-widget-tweaks>=1.4.1',
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
 docs_requires = [
49
 docs_requires = [
50
     'Sphinx==2.2.2',
50
     'Sphinx==2.2.2',
98
         'Development Status :: 5 - Production/Stable',
98
         'Development Status :: 5 - Production/Stable',
99
         'Environment :: Web Environment',
99
         'Environment :: Web Environment',
100
         'Framework :: Django',
100
         'Framework :: Django',
101
-        'Framework :: Django :: 1.11',
102
         'Framework :: Django :: 2.2',
101
         'Framework :: Django :: 2.2',
102
+        'Framework :: Django :: 3.0',
103
         'Intended Audience :: Developers',
103
         'Intended Audience :: Developers',
104
         'License :: OSI Approved :: BSD License',
104
         'License :: OSI Approved :: BSD License',
105
         'Operating System :: Unix',
105
         'Operating System :: Unix',

+ 2
- 2
src/oscar/apps/basket/abstract_models.py 查看文件

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

+ 2
- 2
src/oscar/apps/basket/views.py 查看文件

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

+ 4
- 3
src/oscar/apps/catalogue/views.py 查看文件

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

+ 2
- 2
src/oscar/apps/checkout/views.py 查看文件

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

+ 3
- 3
src/oscar/apps/communication/notifications/views.py 查看文件

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

+ 4
- 4
src/oscar/apps/customer/forms.py 查看文件

8
 from django.contrib.sites.shortcuts import get_current_site
8
 from django.contrib.sites.shortcuts import get_current_site
9
 from django.core.exceptions import ValidationError
9
 from django.core.exceptions import ValidationError
10
 from django.utils.crypto import get_random_string
10
 from django.utils.crypto import get_random_string
11
-from django.utils.http import is_safe_url
12
 from django.utils.translation import gettext_lazy as _
11
 from django.utils.translation import gettext_lazy as _
13
 from django.utils.translation import pgettext_lazy
12
 from django.utils.translation import pgettext_lazy
14
 
13
 
15
 from oscar.apps.customer.utils import get_password_reset_url, normalise_email
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
 from oscar.core.loading import get_class, get_model, get_profile_class
17
 from oscar.core.loading import get_class, get_model, get_profile_class
18
 from oscar.forms import widgets
18
 from oscar.forms import widgets
19
 
19
 
74
 
74
 
75
     def clean_redirect_url(self):
75
     def clean_redirect_url(self):
76
         url = self.cleaned_data['redirect_url'].strip()
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
             return url
78
             return url
79
 
79
 
80
 
80
 
147
 
147
 
148
     def clean_redirect_url(self):
148
     def clean_redirect_url(self):
149
         url = self.cleaned_data['redirect_url'].strip()
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
             return url
151
             return url
152
         return settings.LOGIN_REDIRECT_URL
152
         return settings.LOGIN_REDIRECT_URL
153
 
153
 

+ 4
- 4
src/oscar/apps/dashboard/catalogue/tables.py 查看文件

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

+ 9
- 7
src/oscar/apps/dashboard/ranges/views.py 查看文件

9
 from django.template.loader import render_to_string
9
 from django.template.loader import render_to_string
10
 from django.urls import reverse
10
 from django.urls import reverse
11
 from django.utils.translation import gettext_lazy as _
11
 from django.utils.translation import gettext_lazy as _
12
-from django.utils.translation import ungettext
12
+from django.utils.translation import ngettext
13
 from django.views.generic import (
13
 from django.views.generic import (
14
     CreateView, DeleteView, ListView, UpdateView, View)
14
     CreateView, DeleteView, ListView, UpdateView, View)
15
 
15
 
128
         for product in products:
128
         for product in products:
129
             range.remove_product(product)
129
             range.remove_product(product)
130
         num_products = len(products)
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
         return HttpResponseRedirect(self.get_success_url(request))
135
         return HttpResponseRedirect(self.get_success_url(request))
135
 
136
 
136
     def add_products(self, request):
137
     def add_products(self, request):
154
             range.add_product(product)
155
             range.add_product(product)
155
 
156
 
156
         num_products = len(products)
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
         dupe_skus = form.get_duplicate_skus()
162
         dupe_skus = form.get_duplicate_skus()
161
         if dupe_skus:
163
         if dupe_skus:
162
             messages.warning(
164
             messages.warning(

+ 2
- 2
src/oscar/apps/dashboard/tables.py 查看文件

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

+ 1
- 1
src/oscar/apps/dashboard/users/tables.py 查看文件

17
     active = Column(accessor='is_active')
17
     active = Column(accessor='is_active')
18
     staff = Column(accessor='is_staff')
18
     staff = Column(accessor='is_staff')
19
     date_registered = Column(accessor='date_joined')
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
     actions = TemplateColumn(
21
     actions = TemplateColumn(
22
         template_name='oscar/dashboard/users/user_row_actions.html',
22
         template_name='oscar/dashboard/users/user_row_actions.html',
23
         verbose_name=' ')
23
         verbose_name=' ')

+ 3
- 1
src/oscar/apps/dashboard/users/views.py 查看文件

25
 
25
 
26
 class IndexView(BulkEditMixin, FormMixin, SingleTableView):
26
 class IndexView(BulkEditMixin, FormMixin, SingleTableView):
27
     template_name = 'oscar/dashboard/users/index.html'
27
     template_name = 'oscar/dashboard/users/index.html'
28
-    table_pagination = True
29
     model = User
28
     model = User
30
     actions = ('make_active', 'make_inactive', )
29
     actions = ('make_active', 'make_inactive', )
31
     form_class = UserSearchForm
30
     form_class = UserSearchForm
39
         self.form = self.get_form(form_class)
38
         self.form = self.get_form(form_class)
40
         return super().dispatch(request, *args, **kwargs)
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
     def get_form_kwargs(self):
44
     def get_form_kwargs(self):
43
         """
45
         """
44
         Only bind search form if it was submitted.
46
         Only bind search form if it was submitted.

+ 5
- 5
src/oscar/apps/offer/conditions.py 查看文件

2
 from decimal import Decimal as D
2
 from decimal import Decimal as D
3
 
3
 
4
 from django.utils.translation import gettext_lazy as _
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
 from oscar.core.loading import get_classes, get_model
7
 from oscar.core.loading import get_classes, get_model
8
 from oscar.templatetags.currency_filters import currency
8
 from oscar.templatetags.currency_filters import currency
70
     def get_upsell_message(self, offer, basket):
70
     def get_upsell_message(self, offer, basket):
71
         num_matches = self._get_num_matches(basket, offer)
71
         num_matches = self._get_num_matches(basket, offer)
72
         delta = self.value - num_matches
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
             % {'delta': delta, 'range': self.range}
75
             % {'delta': delta, 'range': self.range}
76
 
76
 
77
     def consume_items(self, offer, basket, affected_lines):
77
     def consume_items(self, offer, basket, affected_lines):
158
 
158
 
159
     def get_upsell_message(self, offer, basket):
159
     def get_upsell_message(self, offer, basket):
160
         delta = self.value - self._get_num_covered_products(basket, offer)
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
             % {'delta': delta, 'range': self.range}
163
             % {'delta': delta, 'range': self.range}
164
 
164
 
165
     def is_partially_satisfied(self, offer, basket):
165
     def is_partially_satisfied(self, offer, basket):

+ 1
- 8
src/oscar/core/application.py 查看文件

1
 from django.apps import AppConfig
1
 from django.apps import AppConfig
2
 from django.core.exceptions import ImproperlyConfigured
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
 from oscar.core.loading import feature_hidden
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
 class OscarConfigMixin(object):
8
 class OscarConfigMixin(object):
16
     """
9
     """

+ 8
- 0
src/oscar/core/compat.py 查看文件

17
                                " 'app_label.model_name'")
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
 def get_user_model():
28
 def get_user_model():
21
     """
29
     """
22
     Return the User model. Doesn't require the app cache to be fully
30
     Return the User model. Doesn't require the app cache to be fully

+ 1
- 1
src/oscar/core/loading.py 查看文件

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

+ 3
- 2
src/oscar/core/utils.py 查看文件

8
 from django.conf import settings
8
 from django.conf import settings
9
 from django.shortcuts import redirect, resolve_url
9
 from django.shortcuts import redirect, resolve_url
10
 from django.template.defaultfilters import date as date_filter
10
 from django.template.defaultfilters import date as date_filter
11
-from django.utils.http import is_safe_url
12
 from django.utils.module_loading import import_string
11
 from django.utils.module_loading import import_string
13
 from django.utils.text import slugify as django_slugify
12
 from django.utils.text import slugify as django_slugify
14
 from django.utils.timezone import get_current_timezone, is_naive, make_aware
13
 from django.utils.timezone import get_current_timezone, is_naive, make_aware
15
 from django.utils.translation import get_language, to_locale
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
 SLUGIFY_RE = re.compile(r'[^\w\s-]', re.UNICODE)
18
 SLUGIFY_RE = re.compile(r'[^\w\s-]', re.UNICODE)
18
 
19
 
19
 
20
 
140
     or a regular URL
141
     or a regular URL
141
     """
142
     """
142
     referrer = request.META.get('HTTP_REFERER')
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
         return referrer
145
         return referrer
145
     if default:
146
     if default:
146
         # Try to resolve. Can take a model instance, Django URL name or URL.
147
         # Try to resolve. Can take a model instance, Django URL name or URL.

+ 5
- 5
src/oscar/forms/widgets.py 查看文件

5
 from django.forms.models import ModelChoiceIterator
5
 from django.forms.models import ModelChoiceIterator
6
 from django.forms.widgets import FileInput
6
 from django.forms.widgets import FileInput
7
 from django.utils import formats
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
 class ImageInput(FileInput):
11
 class ImageInput(FileInput):
208
     """
208
     """
209
 
209
 
210
     def __init__(self, attrs=None, choices=(), disabled_values=()):
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
         super().__init__(attrs, choices)
212
         super().__init__(attrs, choices)
213
 
213
 
214
     def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
214
     def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
215
         option = super().create_option(name, value, label, selected, index, subindex, attrs)
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
             option['attrs']['disabled'] = True
217
             option['attrs']['disabled'] = True
218
         return option
218
         return option
219
 
219
 
254
         default = (None, [], 0)
254
         default = (None, [], 0)
255
         groups = [default]
255
         groups = [default]
256
         has_selected = False
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
         if not self.is_required and not self.allow_multiple_selected:
258
         if not self.is_required and not self.allow_multiple_selected:
259
             default[1].append(self.create_option(name, '', '', False, 0))
259
             default[1].append(self.create_option(name, '', '', False, 0))
260
 
260
 
266
             c for c in selected_choices if c not in self.choices.field.empty_values
266
             c for c in selected_choices if c not in self.choices.field.empty_values
267
         }
267
         }
268
         choices = (
268
         choices = (
269
-            (obj.pk, force_text(obj))
269
+            (obj.pk, force_str(obj))
270
             for obj in self.choices.queryset.filter(pk__in=selected_choices)
270
             for obj in self.choices.queryset.filter(pk__in=selected_choices)
271
         )
271
         )
272
         for option_value, option_label in choices:
272
         for option_value, option_label in choices:

+ 2
- 6
src/oscar/models/fields/autoslugfield.py 查看文件

27
 import re
27
 import re
28
 
28
 
29
 from django.conf import settings
29
 from django.conf import settings
30
+from django.utils.encoding import force_str
30
 
31
 
31
 from oscar.core.utils import slugify
32
 from oscar.core.utils import slugify
32
 
33
 
33
 from .slugfield import SlugField
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
 class AutoSlugField(SlugField):
37
 class AutoSlugField(SlugField):
42
     """ AutoSlugField
38
     """ AutoSlugField
169
         return slug
165
         return slug
170
 
166
 
171
     def pre_save(self, model_instance, add):
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
         setattr(model_instance, self.attname, value)
169
         setattr(model_instance, self.attname, value)
174
         return value
170
         return value
175
 
171
 

+ 1
- 2
src/oscar/templatetags/image_tags.py 查看文件

6
 from django.db.models.fields.files import ImageFieldFile
6
 from django.db.models.fields.files import ImageFieldFile
7
 from django.utils.encoding import smart_str
7
 from django.utils.encoding import smart_str
8
 from django.utils.html import escape
8
 from django.utils.html import escape
9
-from django.utils.six import text_type
10
 
9
 
11
 from oscar.core.thumbnails import get_thumbnailer
10
 from oscar.core.thumbnails import get_thumbnailer
12
 
11
 
120
         source = self.source_var.resolve(context)
119
         source = self.source_var.resolve(context)
121
         options = self.get_thumbnail_options(context)
120
         options = self.get_thumbnail_options(context)
122
         for key, expr in self.options:
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
             options[key] = value
123
             options[key] = value
125
 
124
 
126
         thumbnailer = get_thumbnailer()
125
         thumbnailer = get_thumbnailer()

+ 3
- 8
tests/functional/checkout/test_guest_checkout.py 查看文件

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

+ 2
- 2
tests/integration/core/test_compat.py 查看文件

6
 from tempfile import NamedTemporaryFile
6
 from tempfile import NamedTemporaryFile
7
 
7
 
8
 from django.test import TestCase, override_settings
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
 from oscar.core.compat import UnicodeCSVWriter, existing_user_fields
11
 from oscar.core.compat import UnicodeCSVWriter, existing_user_fields
12
 
12
 
51
             writer.writerows([row])
51
             writer.writerows([row])
52
 
52
 
53
         with open(tmp_file.name, 'r') as read_file:
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
             self.assertEqual(content, 'ünįcodē,123,foo-bar')
55
             self.assertEqual(content, 'ünįcodē,123,foo-bar')
56
 
56
 
57
         # Clean up
57
         # Clean up

+ 2
- 10
tox.ini 查看文件

1
 [tox]
1
 [tox]
2
 envlist =
2
 envlist =
3
-    py{35,36,37}-django{111,22}
4
-    py36-djangomaster
3
+    py{35,36,37,38}-django{22,30}
5
     lint
4
     lint
6
     sandbox
5
     sandbox
7
     docs
6
     docs
12
 extras = test
11
 extras = test
13
 pip_pre = true
12
 pip_pre = true
14
 deps =
13
 deps =
15
-    django111: django>=1.11,<2
16
     django22: django>=2.2,<2.3
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
 [testenv:lint]
18
 [testenv:lint]

正在加载...
取消
保存