8000 feat: django 5 support for cms 3.11 (#7724) · django-cms/django-cms@0950714 · GitHub
[go: up one dir, main page]

Skip to content

Commit 0950714

Browse files
protorotofsbraun
andauthored
feat: django 5 support for cms 3.11 (#7724)
* feat: django 5 support (#7648) * Support for Django 5.0 * Fix: test.yml and django Promise handling * Shorten github action names * Update test.yml * Update _cms.scss * Update setup.py * fix: django 5's choice widget is not lazy (#7707) * Support for Django 5.0 * Fix: test.yml and django Promise handling * Shorten github action names * Update test.yml * Update _cms.scss * Update setup.py * Fix: Django 5 choice widget is not lazy either * Add test * Fix: Swapped underscore * Deprecate SuperLazyIterator and LazyChoiceField * Fix toolbar tests * Fix import sorting * Remove Django 2.2 as it was before * Remove useless DJANGO_5_0 compat variable * Pin correct Django 5.0 version Co-authored-by: Fabian Braun <fsbraun@gmx.de> * Do not deprecate LazyChoiceField Co-authored-by: Fabian Braun <fsbraun@gmx.de> * Do not deprecate SuperLazyIterator Co-authored-by: Fabian Braun <fsbraun@gmx.de> * Remove unused warning import --------- Co-authored-by: Fabian Braun <fsbraun@gmx.de>
1 parent 6310a9b commit 0950714

File tree

12 files changed

+95
-29
lines changed

12 files changed

+95
-29
lines changed

.github/workflows/test.yml

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ concurrency:
77
cancel-in-progress: true
88

99
jobs:
10-
database-postgres:
10+
postgres:
1111
runs-on: ${{ matrix.os }}
1212
strategy:
1313
fail-fast: false
@@ -18,10 +18,16 @@ jobs:
1818
django-4.0.txt,
1919
django-4.1.txt,
2020
django-4.2.txt,
21+
django-5.0.txt
2122
]
2223
os: [
2324
ubuntu-20.04,
2425
]
26+
exclude:
27+
- requirements-file: django-5.0.txt
28+
python-version: 3.8
29+
- requirements-file: django-5.0.txt
30+
python-version: 3.9
2531

2632
services:
2733
postgres:
@@ -59,7 +65,7 @@ jobs:
5965
DATABASE_URL: postgres://postgres:postgres@127.0.0.1/postgres
6066

6167

62- database-mysql:
68+
mysql:
6369
runs-on: ${{ matrix.os }}
6470
strategy:
6571
fail-fast: false
@@ -70,10 +76,16 @@ jobs:
7076
django-4.0.txt,
7177
django-4.1.txt,
7278
django-4.2.txt,
79+
django-5.0.txt
7380
]
7481
os: [
7582
ubuntu-20.04,
7683
]
84+
exclude:
85+
- requirements-file: django-5.0.txt
86+
python-version: 3.8
87+
- requirements-file: django-5.0.txt
88+
python-version: 3.9
7789

7890
services:
7991
mysql:
@@ -111,7 +123,7 @@ jobs:
111123
env:
112124
DATABASE_URL: mysql://root@127.0.0.1/djangocms_test
113125

114-
database-sqlite:
126+
sqlite:
115127
runs-on: ${{ matrix.os }}
116128
strategy:
117129
fail-fast: false
@@ -122,10 +134,16 @@ jobs:
122134
django-4.0.txt,
123135
django-4.1.txt,
124136
django-4.2.txt,
137+
django-5.0.txt
125138
]
126139
os: [
127140
ubuntu-20.04,
128141
]
142+
exclude:
143+
- requirements-file: django-5.0.txt
144+
python-version: 3.8
145+
- requirements-file: django-5.0.txt
146+
python-version: 3.9
129147

130148
steps:
131149
- uses: actions/checkout@v3

cms/cms_toolbars.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from cms.toolbar_base import CMSToolbar
1717
from cms.toolbar_pool import toolbar_pool
1818
from cms.utils import get_language_from_request, page_permissions
19+
from cms.utils.compat import DJANGO_4_2
1920
from cms.utils.conf import get_cms_setting
2021
from cms.utils.i18n import get_language_dict, get_language_tuple
2122
from cms.utils.page_permissions import user_can_change_page, user_can_delete_page, user_can_publish_page
@@ -225,7 +226,7 @@ def add_logout_button(self, parent):
225226
action=admin_reverse('logout'),
226227
active=True,
227228
on_success=on_success,
228-
method='GET',
229+
method='GET' if DJANGO_4_2 else 'POST',
229230
)
230231

231232
def add_language_menu(self):

cms/forms/fields.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
from django import forms
22
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
33
from django.core.validators import EMPTY_VALUES
4+
from django.forms import ChoiceField
45
from django.utils.translation import gettext_lazy as _
56

67
from cms.forms.utils import get_page_choices, get_site_choices
78
from cms.forms.validators import validate_url_extra
89
from cms.forms.widgets import PageSelectWidget, PageSmartLinkWidget
910
from cms.models.pagemodel import Page
11+
from cms.utils.compat import DJANGO_4_2
1012

1113

1214
class SuperLazyIterator:
@@ -18,11 +20,20 @@ def __iter__(self):
1820

1921

2022
class LazyChoiceField(forms.ChoiceField):
21-
def _set_choices(self, value):
22-
# we overwrite this function so no list(value) is called
23-
self._choices = self.widget.choices = value
2423

25-
choices = property(forms.ChoiceField._get_choices, _set_choices)
24+
25+
@property
26+
def choices(self):
27+
return super().choices()
28+
29+
@choices.setter
30+
def choices(self, value):
31+
# we overwrite this function so no list(value) or normalize_choices(value) is called
32+
# also, do not call the widget's setter as of Django 5
33+
if DJANGO_4_2:
34+
self._choices = self.widget.choices = value
35+
else:
36+
self._choices = self.widget._choices = value
2637

2738

2839
class PageSelectFormField(forms.MultiValueField):
@@ -38,14 +49,12 @@ def __init__(self, queryset=None, empty_label="---------", cache_choices=False,
3849
errors = self.default_error_messages.copy()
3950
if 'error_messages' in kwargs:
4051
errors.update(kwargs['error_messages'])
41-
site_choices = SuperLazyIterator(get_site_choices)
42-
page_choices = SuperLazyIterator(get_page_choices)
4352
self.limit_choices_to = limit_choices_to
4453
kwargs['required'] = required
4554
kwargs.pop('blank', None)
4655
fields = (
47-
LazyChoiceField(choices=site_choices, required=False, error_messages={'invalid': errors['invalid_site']}),
48-
LazyChoiceField(choices=page_choices, required=False, error_messages={'invalid': errors['invalid_page']}),
56+
ChoiceField(choices=get_site_choices, required=False, error_messages={'invalid': errors['invalid_site']}),
57+
ChoiceField(choices=get_page_choices, required=False, error_messages={'invalid': errors['invalid_page']}),
4958
)
5059
super().__init__(fields, *args, **kwargs)
5160

cms/tests/test_forms.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
ViewRestrictionInlineAdminForm,
1414
)
1515
from cms.api import assign_user_to_page, create_page, create_title
16-
from cms.forms.fields import PageSelectFormField, SuperLazyIterator
16+
from cms.forms.fields import LazyChoiceField, PageSelectFormField, SuperLazyIterator
1717
from cms.forms.utils import get_page_choices, get_site_choices, update_site_and_page_choices
1818
from cms.forms.widgets import ApplicationConfigSelect
1919
from cms.models import ACCESS_PAGE, ACCESS_PAGE_AND_CHILDREN
@@ -238,6 +238,17 @@ def test_superlazy_iterator_behaves_properly_for_pages(self):
238238

239239
self.assertEqual(normal_result, list(lazy_result))
240240

241+
def test_lazy_choice_field_behaves_properly(self):
242+
"""Ensure LazyChoiceField is really lazy"""
243+
choices_called = False
244+
def get_choices():
245+
nonlocal choices_called
246+
choices_called = True
247+
return ("", "-----"),
248+
249+
LazyChoiceField(choices=SuperLazyIterator(get_choices))
250+
self.assertFalse(choices_called, "Lazy choice function called")
251+
241252

242253
class PermissionFormTestCase(CMSTestCase):
243254

cms/tests/test_page_admin.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
CMSTestCase,
3939
)
4040
from cms.test_utils.util.context_managers import LanguageOverride, UserLoginContext
41+
from cms.utils.compat import DJANGO_4_2
4142
from cms.utils.compat.dj import installed_apps
4243
from cms.utils.conf import get_cms_setting
4344
from cms.utils.page import get_page_from_request
@@ -1373,6 +1374,10 @@ def test_set_overwrite_url(self):
13731374
expected = (
13741375
'<input id="id_overwrite_url" maxlength="255" '
13751376
'value="new-url" name="overwrite_url" type="text" />'
1377+
) if DJANGO_4_2 else (
1378+
'<input type="text" name="overwrite_url" value="new-url" '
1379+
'maxlength="255" aria-describedby="id_overwrite_url_helptext" '
1380+
'id="id_overwrite_url">'
13761381
)
13771382
changelist = self.get_admin_url(Page, 'changelist')
13781383
endpoint = self.get_admin_url(Page, 'advanced', cms_page.pk)
@@ -1425,6 +1430,9 @@ def test_remove_overwrite_url(self):
14251430
expected = (
14261431
'<input id="id_overwrite_url" maxlength="255" '
14271432
'name="overwrite_url" type="text" />'
1433+
) if DJANGO_4_2 else (
1434+
'<input type="text" name="overwrite_url" maxlength="255" '
1435+
'aria-describedby="id_overwrite_url_helptext" id="id_overwrite_url">'
14281436
)
14291437
changelist = self.get_admin_url(Page, 'changelist')
14301438
endpoint = self.get_admin_url(Page, 'advanced', cms_page.pk)

cms/tests/test_plugins.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -884,8 +884,9 @@ def test_empty_plugin_description(self):
884884
plugin_type='TextPlugin',
885885
placeholder=placeholder,
886886
position=1,
887-
language=self.FIRST_LANG
887+
language=self.FIRST_LANG,
888888
)
889+
a.save()
889890

890891
self.assertEqual(a.get_short_description(), "<Empty>")
891892

cms/tests/test_sitemap.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
from cms.models import Page, Title
55
from cms.sitemaps import CMSSitemap
66
from cms.test_utils.testcases import CMSTestCase
7+
from cms.utils.compat import DJANGO_4_2
78
from cms.utils.conf import get_cms_setting
89

10+
protocol = "http" if DJANGO_4_2 else "https"
911

1012
class SitemapTestCase(CMSTestCase):
1113
def setUp(self):
@@ -93,9 +95,9 @@ def test_sitemap_items_location(self):
9395
urlset = sitemap.get_urls()
9496
for item in urlset:
9597
if item['item'].path:
96-
url = 'http://example.com/{}/{}/'.format(item['item'].language, item['item'].path)
98+
url = f'{protocol}://example.com/{item["item"].language}/{item["item"].path}/'
9799
else:
98-
url = 'http://example.com/{}/{}'.format(item['item'].language, item['item'].path)
100+
url = f'{protocol}://example.com/{item["item"].language}/'
99101
self.assertEqual(item['location'], url)
100102

101103
def test_sitemap_published_titles(self):
@@ -110,9 +112,9 @@ def test_sitemap_published_titles(self):
110112
for title in Title.objects.public():
111113
page = title.page.get_public_object()
112114
if title.path:
113-
url = f'http://example.com/{title.language}/{title.path}/'
115+
url = f'{protocol}://example.com/{title.language}/{title.path}/'
114116
else:
115-
url = f'http://example.com/{title.language}/{title.path}'
117+
url = f'{protocol}://example.com/{title.language}/{title.path}'
116118
if page.is_published('en') and not page.publisher_is_draft:
117119
self.assertTrue(url in locations)
118120
else:
@@ -142,9 +144,9 @@ def test_sitemap_unpublished_titles(self):
142144
for path in unpublished_titles:
143145
title = Title.objects.get(path=path)
144146
if title.path:
145-
url = f'http://example.com/{title.language}/{title.path}/'
147+
url = f'{protocol}://example.com/{title.language}/{title.path}/'
146148
else:
147-
url = f'http://example.com/{title.language}/{title.path}'
149+
url = f'{protocol}://example.com/{title.language}/{title.path}'
148150
self.assertFalse(url in locations)
149151

150152
def test_sitemap_uses_public_languages_only(self):
@@ -159,7 +161,7 @@ def test_sitemap_uses_public_languages_only(self):
159161

160162
with self.settings(CMS_LANGUAGES=lang_settings):
161163
for item in CMSSitemap().get_urls():
162-
url = 'http://example.com/en/'
164+
url = f'{protocol}://example.com/en/'
163165

164166
if item['item'].path:
165167
url += item['item'].path + '/'

cms/tests/test_toolbar.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from cms.toolbar.items import AjaxItem, Break, ItemSearchResult, LinkItem, SubMenu, ToolbarAPIMixin
4141
from cms.toolbar.toolbar import CMSToolbar
4242
from cms.toolbar_pool import toolbar_pool
43+
from cms.utils.compat import DJANGO_4_2
4344
from cms.utils.conf import get_cms_setting
4445
from cms.utils.i18n import get_language_tuple
4546
from cms.utils.urlutils import admin_reverse
@@ -640,7 +641,10 @@ def test_hide_toolbar_login_nonstaff(self):
640641
def test_admin_logout_staff(self):
641642
with override_settings(CMS_PERMISSION=True):
642643
with self.login_user_context(self.get_staff()):
643-
response = self.client.get('/en/admin/logout/')
644+
if DJANGO_4_2:
645+
response = self.client.get('/en/admin/logout/')
646+
else:
647+
response = self.client.post('/en/admin/logout/')
644648
self.assertEqual(response.status_code, 200)
645649

646650
def test_show_toolbar_without_edit(self):
@@ -1519,7 +1523,7 @@ def test_filters_date(self):
15191523
'<template class="cms-plugin cms-plugin-end cms-plugin-{0}-{1}-{2}-{3} cms-render-model"></template>'
15201524
'</h1>'.format(
15211525
'placeholderapp', 'example1', 'date_field', ex1.pk,
1522-
ex1.date_field.strftime("%b. %d, %Y")))
1526+
ex1.date_field.strftime("%b. %d, %Y" if DJANGO_4_2 else "%b. %-d, %Y")))
15231527

15241528
template_text = '''{% extends "base.html" %}
15251529
{% load cms_tags %}

cms/toolbar/items.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django.utils.functional import Promise
88

99
from cms.constants import LEFT, REFRESH_PAGE, RIGHT, URL_CHANGE
10+
from cms.utils.compat import DJANGO_4_2
1011

1112

1213
class ItemSearchResult:
@@ -24,11 +25,18 @@ def __int__(self):
2425
return self.index
2526

2627

27-
def may_be_lazy(thing):
28-
if isinstance(thing, Promise):
29-
return thing._proxy____args[0]
30-
else:
31-
return thing
28+
if DJANGO_4_2:
29+
def may_be_lazy(thing):
30+
if isinstance(thing, Promise):
31+
return thing._proxy____args[0]
32+
else:
33+
return thing
34+
else:
35+
def may_be_lazy(thing):
36+
if isinstance(thing, Promise):
37+
return thing._args[0]
38+
else:
39+
return thing
3240

3341

3442
class ToolbarAPIMixin(metaclass=ABCMeta):

cms/utils/compat/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313
DJANGO_3_2 = Version(DJANGO_VERSION) < Version('4.0')
1414
DJANGO_3 = DJANGO_3_2
1515
DJANGO_4_1 = Version(DJANGO_VERSION) < Version('4.2')
16+
DJANGO_4_2 = Version(DJANGO_VERSION) < Version('4.3')

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77

88
REQUIREMENTS = [
9-
'Django>=3.2,<5.0',
9+
'Django>=3.2',
1010
'django-classy-tags>=0.7.2',
1111
'django-formtools>=2.1',
1212
'django-treebeard>=4.3',
@@ -35,6 +35,7 @@
3535
'Framework :: Django :: 4.0',
3636
'Framework :: Django :: 4.1',
3737
'Framework :: Django :: 4.2',
38+
'Framework :: Django :: 5.0',
3839
'Topic :: Internet :: WWW/HTTP',
3940
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
4041
'Topic :: Software Development',

test_requirements/django-5.0.txt

Li 341A nes changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-r requirements_base.txt
2+
Django>=5.0,<5.1

0 commit comments

Comments
 (0)
0