diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0014fe3c89..7de8c6db69 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,9 +17,8 @@ on: # - test runs with USE_EMAIL_USER_MODEL=yes and DISABLE_TIMEZONE=yes # Current configuration: -# - django 2.2, python 3.6, mysql -# - django 3.0, python 3.7, sqlite -# - django 3.1, python 3.8, postgres +# - django 3.0, python 3.6, sqlite +# - django 3.1, python 3.7, postgres # - django 3.2, python 3.8, postgres # - django 3.2, python 3.9, mysql # - django 3.2, python 3.9, sqlite @@ -27,9 +26,8 @@ on: # - django 3.2, python 3.9, postgres, DISABLE_TIMEZONE=yes # - django stable/3.2.x, python 3.9, postgres (allow failures) # - django main, python 3.9, postgres (allow failures) -# - elasticsearch 5, django 2.2, python 3.6, sqlite -# - elasticsearch 6, django 3.0, python 3.7, postgres -# - elasticsearch 7, django 3.1, python 3.8, postgres +# - elasticsearch 5, django 3.0, python 3.6, sqlite +# - elasticsearch 6, django 3.1, python 3.7, postgres # - elasticsearch 7, django 3.2, python 3.8, postgres # - elasticsearch 7, django 3.2, python 3.9, sqlite, USE_EMAIL_USER_MODEL=yes @@ -39,7 +37,7 @@ jobs: strategy: matrix: include: - - python: 3.7 + - python: 3.6 django: "Django>=3.0,<3.1" - python: 3.9 django: "Django>=3.1,<3.2" @@ -67,7 +65,7 @@ jobs: strategy: matrix: include: - - python: 3.8 + - python: 3.7 django: "Django>=3.1,<3.2" experimental: false - python: 3.8 @@ -123,8 +121,6 @@ jobs: strategy: matrix: include: - - python: 3.6 - django: "Django>=2.2,<3.0" - python: 3.9 django: "Django>=3.2,<3.3" @@ -166,7 +162,7 @@ jobs: matrix: include: - python: 3.6 - django: "Django>=2.2,<3.0" + django: "Django>=3.0,<3.1" steps: - name: Configure sysctl limits run: | @@ -250,7 +246,7 @@ jobs: matrix: include: - python: 3.7 - django: "Django>=3.0,<3.1" + django: "Django>=3.1,<3.2" services: postgres: @@ -297,8 +293,6 @@ jobs: strategy: matrix: include: - - python: 3.8 - django: "Django>=3.1,<3.2" - python: 3.8 django: "Django>=3.2,<3.3" diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 6a6be300eb..7243bfbbed 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -4,6 +4,7 @@ Changelog 2.14 (xx.xx.xxxx) - IN DEVELOPMENT ~~~~~~~~~~~~~~~~~ + * Removed support for Django 2.2 * Added ``ancestor_of`` API filter (Jaap Roes) * Fix: Invalid filter values for foreign key fields in the API now give an error instead of crashing (Tidjani Dia) * Fix: Ordering specified in `construct_explorer_page_queryset` hook is now taken into account again by the page explorer API (Andre Fonseca) diff --git a/README.md b/README.md index aef7a9f812..990f771892 100644 --- a/README.md +++ b/README.md @@ -56,11 +56,11 @@ _(If you are reading this on GitHub, the details here may not be indicative of t Wagtail supports: -* Django 2.2.x, 3.0.x, 3.1.x and 3.2.x +* Django 3.0.x, 3.1.x and 3.2.x * Python 3.6, 3.7, 3.8 and 3.9 * PostgreSQL, MySQL and SQLite as database backends -[Previous versions of Wagtail](https://docs.wagtail.io/en/stable/releases/upgrading.html#compatible-django-python-versions) additionally supported Python 2.7 and Django 1.x. +[Previous versions of Wagtail](https://docs.wagtail.io/en/stable/releases/upgrading.html#compatible-django-python-versions) additionally supported Python 2.7 and Django 1.x - 2.x. --- diff --git a/docs/getting_started/integrating_into_django.md b/docs/getting_started/integrating_into_django.md index 9b9c898341..d98bf72810 100644 --- a/docs/getting_started/integrating_into_django.md +++ b/docs/getting_started/integrating_into_django.md @@ -5,7 +5,7 @@ Wagtail provides the `wagtail start` command and project template to get you started with a new Wagtail project as quickly as possible, but it's easy to integrate Wagtail into an existing Django project too. -Wagtail is currently compatible with Django 2.2, 3.0, 3.1 and 3.2. First, install the `wagtail` package from PyPI: +Wagtail is currently compatible with Django 3.0, 3.1 and 3.2. First, install the `wagtail` package from PyPI: ```sh $ pip install wagtail diff --git a/docs/releases/2.14.rst b/docs/releases/2.14.rst index 94b172a559..4058573152 100644 --- a/docs/releases/2.14.rst +++ b/docs/releases/2.14.rst @@ -24,3 +24,8 @@ Bug fixes Upgrade considerations ====================== + +Removed support for Django 2.2 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Django 2.2 is no longer supported as of this release; please upgrade to Django 3.0 or above before upgrading Wagtail. diff --git a/setup.py b/setup.py index a85ec9c371..6ff451fa94 100755 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ except ImportError: install_requires = [ - "Django>=2.2,<3.3", + "Django>=3.0,<3.3", "django-modelcluster>=5.1,<6.0", "django-taggit>=1.0,<2.0", "django-treebeard>=4.2.0,<5.0,!=4.5", @@ -109,7 +109,6 @@ https://github.com/wagtail/wagtail/.", 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Framework :: Django', - 'Framework :: Django :: 2.2', 'Framework :: Django :: 3.0', 'Framework :: Django :: 3.1', 'Framework :: Django :: 3.2', diff --git a/tox.ini b/tox.ini index 53f4a296a4..9caa5fd104 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ skipsdist = True usedevelop = True -envlist = py{36,37,38,39}-dj{22,30,31,32,32stable,main}-{sqlite,postgres,mysql,mssql}-{elasticsearch7,elasticsearch6,elasticsearch5,noelasticsearch}-{customuser,emailuser}-{tz,notz}, +envlist = py{36,37,38,39}-dj{30,31,32,32stable,main}-{sqlite,postgres,mysql,mssql}-{elasticsearch7,elasticsearch6,elasticsearch5,noelasticsearch}-{customuser,emailuser}-{tz,notz}, [testenv] install_command = pip install -e ".[testing]" -U {opts} {packages} @@ -22,7 +22,6 @@ deps = django-sendfile==0.3.6 Embedly - dj22: Django~=2.2.0 dj30: Django~=3.0.0 dj31: Django~=3.1.0 dj32: Django~=3.2.0 diff --git a/wagtail/admin/tests/pages/test_copy_page.py b/wagtail/admin/tests/pages/test_copy_page.py index 2121236777..52a61e5d3c 100644 --- a/wagtail/admin/tests/pages/test_copy_page.py +++ b/wagtail/admin/tests/pages/test_copy_page.py @@ -1,4 +1,3 @@ -from django import VERSION as DJANGO_VERSION from django.contrib.auth.models import Group, Permission from django.http import HttpRequest, HttpResponse from django.test import TestCase @@ -319,14 +318,9 @@ class TestPageCopy(TestCase, WagtailTestUtils): self.assertEqual(response.status_code, 200) # Check that a form error was raised - if DJANGO_VERSION >= (3, 0): - self.assertFormError( - response, 'form', 'new_slug', "Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or hyphens." - ) - else: - self.assertFormError( - response, 'form', 'new_slug', "Enter a valid 'slug' consisting of Unicode letters, numbers, underscores, or hyphens." - ) + self.assertFormError( + response, 'form', 'new_slug', "Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or hyphens." + ) def test_page_copy_post_valid_unicode_slug(self): post_data = { diff --git a/wagtail/admin/tests/test_account_management.py b/wagtail/admin/tests/test_account_management.py index 65b586f898..5ea21e86ae 100644 --- a/wagtail/admin/tests/test_account_management.py +++ b/wagtail/admin/tests/test_account_management.py @@ -2,7 +2,6 @@ import unittest import pytz -from django import VERSION as DJANGO_VERSION from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth import views as auth_views @@ -344,10 +343,7 @@ class TestAccountSection(TestCase, WagtailTestUtils, TestAccountSectionUtilsMixi # Check that a validation error was raised password_form = password_panel.get_form() self.assertTrue('new_password2' in password_form.errors.keys()) - if DJANGO_VERSION >= (3, 0): - self.assertTrue("The two password fields didn’t match." in password_form.errors['new_password2']) - else: - self.assertTrue("The two password fields didn't match." in password_form.errors['new_password2']) + self.assertTrue("The two password fields didn’t match." in password_form.errors['new_password2']) # Check that the password was not changed self.user.refresh_from_db() @@ -722,10 +718,7 @@ class TestPasswordReset(TestCase, WagtailTestUtils): self.password_reset_uid = force_str(urlsafe_base64_encode(force_bytes(self.user.pk))) # Create url_args - if DJANGO_VERSION >= (3, 0): - token = auth_views.PasswordResetConfirmView.reset_url_token - else: - token = auth_views.INTERNAL_RESET_URL_TOKEN + token = auth_views.PasswordResetConfirmView.reset_url_token self.url_kwargs = dict(uidb64=self.password_reset_uid, token=token) @@ -807,11 +800,7 @@ class TestPasswordReset(TestCase, WagtailTestUtils): # Check that a validation error was raised self.assertTrue('new_password2' in response.context['form'].errors.keys()) - - if DJANGO_VERSION >= (3, 0): - self.assertTrue("The two password fields didn’t match." in response.context['form'].errors['new_password2']) - else: - self.assertTrue("The two password fields didn't match." in response.context['form'].errors['new_password2']) + self.assertTrue("The two password fields didn’t match." in response.context['form'].errors['new_password2']) # Check that the password was not changed self.assertTrue(get_user_model().objects.get(email='test@email.com').check_password('password')) diff --git a/wagtail/admin/tests/test_audit_log.py b/wagtail/admin/tests/test_audit_log.py index 44358e74de..3984402d0b 100644 --- a/wagtail/admin/tests/test_audit_log.py +++ b/wagtail/admin/tests/test_audit_log.py @@ -1,6 +1,5 @@ from datetime import timedelta -from django import VERSION as DJANGO_VERSION from django.contrib.auth.models import Group, Permission from django.test import TestCase from django.urls import reverse @@ -85,30 +84,17 @@ class TestAuditLogAdmin(TestCase, WagtailTestUtils): self.assertContains(response, "Page scheduled for publishing", 1) self.assertContains(response, "Published", 1) - if DJANGO_VERSION >= (3, 0): - self.assertContains( - response, "Added the 'Private, accessible to logged-in users' view restriction" - ) - self.assertContains( - response, - "Updated the view restriction to 'Private, accessible with the following password'" - ) - self.assertContains( - response, - "Removed the 'Private, accessible with the following password' view restriction" - ) - else: - self.assertContains( - response, "Added the 'Private, accessible to logged-in users' view restriction" - ) - self.assertContains( - response, - "Updated the view restriction to 'Private, accessible with the following password'" - ) - self.assertContains( - response, - "Removed the 'Private, accessible with the following password' view restriction" - ) + self.assertContains( + response, "Added the 'Private, accessible to logged-in users' view restriction" + ) + self.assertContains( + response, + "Updated the view restriction to 'Private, accessible with the following password'" + ) + self.assertContains( + response, + "Removed the 'Private, accessible with the following password' view restriction" + ) self.assertContains(response, 'system', 2) # create without a user + remove restriction self.assertContains(response, 'the_editor', 9) # 7 entries by editor + 1 in sidebar menu + 1 in filter diff --git a/wagtail/contrib/modeladmin/options.py b/wagtail/contrib/modeladmin/options.py index 2ddaffb603..5e1d96bbef 100644 --- a/wagtail/contrib/modeladmin/options.py +++ b/wagtail/contrib/modeladmin/options.py @@ -126,7 +126,7 @@ class ModelAdmin(WagtailRegisterable): self.model, self.inspect_view_enabled) self.url_helper = self.get_url_helper_class()(self.model) - # Needed to support RelatedFieldListFilter in Django 2.2+ + # Needed to support RelatedFieldListFilter # See: https://github.com/wagtail/wagtail/issues/5105 self.admin_site = default_django_admin_site diff --git a/wagtail/contrib/modeladmin/tests/test_page_modeladmin.py b/wagtail/contrib/modeladmin/tests/test_page_modeladmin.py index 3160f2c151..ca8a31f4a6 100644 --- a/wagtail/contrib/modeladmin/tests/test_page_modeladmin.py +++ b/wagtail/contrib/modeladmin/tests/test_page_modeladmin.py @@ -1,4 +1,3 @@ -from django import VERSION as DJANGO_VERSION from django.contrib.auth.models import Group, Permission from django.test import TestCase @@ -65,21 +64,13 @@ class TestExcludeFromExplorer(TestCase, WagtailTestUtils): def test_attribute_effects_explorer(self): # The two VenuePages should appear in the venuepage list response = self.client.get('/admin/modeladmintest/venuepage/') - if DJANGO_VERSION >= (3, 0): - self.assertContains(response, "Santa's Grotto") - self.assertContains(response, "Santa's Workshop") - else: - self.assertContains(response, "Santa's Grotto") - self.assertContains(response, "Santa's Workshop") + self.assertContains(response, "Santa's Grotto") + self.assertContains(response, "Santa's Workshop") # But when viewing the children of 'Christmas' event in explorer response = self.client.get('/admin/pages/4/') - if DJANGO_VERSION >= (3, 0): - self.assertNotContains(response, "Santa's Grotto") - self.assertNotContains(response, "Santa's Workshop") - else: - self.assertNotContains(response, "Santa's Grotto") - self.assertNotContains(response, "Santa's Workshop") + self.assertNotContains(response, "Santa's Grotto") + self.assertNotContains(response, "Santa's Workshop") # But the other test page should... self.assertContains(response, "Claim your free present!") diff --git a/wagtail/contrib/modeladmin/views.py b/wagtail/contrib/modeladmin/views.py index 5160c654a4..16a8127b9a 100644 --- a/wagtail/contrib/modeladmin/views.py +++ b/wagtail/contrib/modeladmin/views.py @@ -33,16 +33,12 @@ from wagtail.admin.views.mixins import SpreadsheetExportMixin from .forms import ParentChooserForm -try: - from django.db.models.sql.constants import QUERY_TERMS -except ImportError: - # Django 2.1+ does not have QUERY_TERMS anymore - QUERY_TERMS = { - 'contains', 'day', 'endswith', 'exact', 'gt', 'gte', 'hour', - 'icontains', 'iendswith', 'iexact', 'in', 'iregex', 'isnull', - 'istartswith', 'lt', 'lte', 'minute', 'month', 'range', 'regex', - 'search', 'second', 'startswith', 'week_day', 'year', - } +QUERY_TERMS = { + 'contains', 'day', 'endswith', 'exact', 'gt', 'gte', 'hour', + 'icontains', 'iendswith', 'iexact', 'in', 'iregex', 'isnull', + 'istartswith', 'lt', 'lte', 'minute', 'month', 'range', 'regex', + 'search', 'second', 'startswith', 'week_day', 'year', +} class WMABaseView(TemplateView): @@ -255,7 +251,7 @@ class IndexView(SpreadsheetExportMixin, WMABaseView): IGNORED_PARAMS = (ORDER_VAR, ORDER_TYPE_VAR, SEARCH_VAR, EXPORT_VAR) # sortable_by is required by the django.contrib.admin.templatetags.admin_list.result_headers - # template tag as of Django 2.1 - see https://docs.djangoproject.com/en/2.1/ref/contrib/admin/#django.contrib.admin.ModelAdmin.sortable_by + # template tag - see https://docs.djangoproject.com/en/stable/ref/contrib/admin/#django.contrib.admin.ModelAdmin.sortable_by sortable_by = None @method_decorator(login_required) diff --git a/wagtail/core/models.py b/wagtail/core/models.py index c02bf5fb56..e7376ba8f8 100644 --- a/wagtail/core/models.py +++ b/wagtail/core/models.py @@ -7,7 +7,6 @@ from collections import namedtuple from io import StringIO from urllib.parse import urlparse -from django import VERSION as DJANGO_VERSION from django import forms from django.apps import apps from django.conf import settings @@ -96,17 +95,13 @@ def _extract_field_data(source, exclude_fields=None): if isinstance(field, models.OneToOneField) and field.remote_field.parent_link: continue - if DJANGO_VERSION >= (3, 0) and isinstance(field, models.ForeignKey): + if isinstance(field, models.ForeignKey): # Use attname to copy the ID instead of retrieving the instance # Note: We first need to set the field to None to unset any object # that's there already just setting _id on its own won't change the # field until its saved. - # Before Django 3.0, Django won't find the new object if the field - # was set to None in this way, so this optimisation isn't available - # for Django 2.x. - data_dict[field.name] = None data_dict[field.attname] = getattr(source, field.attname) diff --git a/wagtail/core/views.py b/wagtail/core/views.py index 63583487d2..8f5c735534 100644 --- a/wagtail/core/views.py +++ b/wagtail/core/views.py @@ -2,18 +2,13 @@ from django.conf import settings from django.http import Http404, HttpResponse from django.shortcuts import get_object_or_404, redirect from django.urls import reverse +from django.utils.http import url_has_allowed_host_and_scheme from wagtail.core import hooks from wagtail.core.forms import PasswordViewRestrictionForm from wagtail.core.models import Page, PageViewRestriction, Site -try: - from django.utils.http import url_has_allowed_host_and_scheme -except ImportError: # fallback for Django 2.2 - from django.utils.http import is_safe_url as url_has_allowed_host_and_scheme - - def serve(request, path): # we need a valid Site object corresponding to this request in order to proceed site = Site.find_for_request(request) diff --git a/wagtail/documents/views/serve.py b/wagtail/documents/views/serve.py index de3b9606bc..3ab8ea47ae 100644 --- a/wagtail/documents/views/serve.py +++ b/wagtail/documents/views/serve.py @@ -5,6 +5,7 @@ from django.http import Http404, HttpResponse, StreamingHttpResponse from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.urls import reverse +from django.utils.http import url_has_allowed_host_and_scheme from django.views.decorators.cache import cache_control from django.views.decorators.http import etag @@ -17,12 +18,6 @@ from wagtail.utils import sendfile_streaming_backend from wagtail.utils.sendfile import sendfile -try: - from django.utils.http import url_has_allowed_host_and_scheme -except ImportError: # fallback for Django 2.2 - from django.utils.http import is_safe_url as url_has_allowed_host_and_scheme - - def document_etag(request, document_id, document_filename): Document = get_document_model() if hasattr(Document, 'file_hash'):