Skip to content

Commit

Permalink
feat: django 5 support for cms 3.11 (django-cms#7724)
Browse files Browse the repository at this point in the history
* feat: django 5 support (django-cms#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 (django-cms#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 <[email protected]>

* Do not deprecate LazyChoiceField

Co-authored-by: Fabian Braun <[email protected]>

* Do not deprecate SuperLazyIterator

Co-authored-by: Fabian Braun <[email protected]>

* Remove unused warning import

---------

Co-authored-by: Fabian Braun <[email protected]>
  • Loading branch information
protoroto and fsbraun authored Dec 21, 2023
1 parent 6310a9b commit 0950714
Show file tree
Hide file tree
Showing 12 changed files with 95 additions and 29 deletions.
24 changes: 21 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ concurrency:
cancel-in-progress: true

jobs:
database-postgres:
postgres:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
Expand All @@ -18,10 +18,16 @@ jobs:
django-4.0.txt,
django-4.1.txt,
django-4.2.txt,
django-5.0.txt
]
os: [
ubuntu-20.04,
]
exclude:
- requirements-file: django-5.0.txt
python-version: 3.8
- requirements-file: django-5.0.txt
python-version: 3.9

services:
postgres:
Expand Down Expand Up @@ -59,7 +65,7 @@ jobs:
DATABASE_URL: postgres://postgres:[email protected]/postgres


database-mysql:
mysql:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
Expand All @@ -70,10 +76,16 @@ jobs:
django-4.0.txt,
django-4.1.txt,
django-4.2.txt,
django-5.0.txt
]
os: [
ubuntu-20.04,
]
exclude:
- requirements-file: django-5.0.txt
python-version: 3.8
- requirements-file: django-5.0.txt
python-version: 3.9

services:
mysql:
Expand Down Expand Up @@ -111,7 +123,7 @@ jobs:
env:
DATABASE_URL: mysql://[email protected]/djangocms_test

database-sqlite:
sqlite:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
Expand All @@ -122,10 +134,16 @@ jobs:
django-4.0.txt,
django-4.1.txt,
django-4.2.txt,
django-5.0.txt
]
os: [
ubuntu-20.04,
]
exclude:
- requirements-file: django-5.0.txt
python-version: 3.8
- requirements-file: django-5.0.txt
python-version: 3.9

steps:
- uses: actions/checkout@v3
Expand Down
3 changes: 2 additions & 1 deletion cms/cms_toolbars.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from cms.toolbar_base import CMSToolbar
from cms.toolbar_pool import toolbar_pool
from cms.utils import get_language_from_request, page_permissions
from cms.utils.compat import DJANGO_4_2
from cms.utils.conf import get_cms_setting
from cms.utils.i18n import get_language_dict, get_language_tuple
from cms.utils.page_permissions import user_can_change_page, user_can_delete_page, user_can_publish_page
Expand Down Expand Up @@ -225,7 +226,7 @@ def add_logout_button(self, parent):
action=admin_reverse('logout'),
active=True,
on_success=on_success,
method='GET',
method='GET' if DJANGO_4_2 else 'POST',
)

def add_language_menu(self):
Expand Down
25 changes: 17 additions & 8 deletions cms/forms/fields.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from django import forms
from django.contrib.admin.widgets import RelatedFieldWidgetWrapper
from django.core.validators import EMPTY_VALUES
from django.forms import ChoiceField
from django.utils.translation import gettext_lazy as _

from cms.forms.utils import get_page_choices, get_site_choices
from cms.forms.validators import validate_url_extra
from cms.forms.widgets import PageSelectWidget, PageSmartLinkWidget
from cms.models.pagemodel import Page
from cms.utils.compat import DJANGO_4_2


class SuperLazyIterator:
Expand All @@ -18,11 +20,20 @@ def __iter__(self):


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

choices = property(forms.ChoiceField._get_choices, _set_choices)

@property
def choices(self):
return super().choices()

@choices.setter
def choices(self, value):
# we overwrite this function so no list(value) or normalize_choices(value) is called
# also, do not call the widget's setter as of Django 5
if DJANGO_4_2:
self._choices = self.widget.choices = value
else:
self._choices = self.widget._choices = value


class PageSelectFormField(forms.MultiValueField):
Expand All @@ -38,14 +49,12 @@ def __init__(self, queryset=None, empty_label="---------", cache_choices=False,
errors = self.default_error_messages.copy()
if 'error_messages' in kwargs:
errors.update(kwargs['error_messages'])
site_choices = SuperLazyIterator(get_site_choices)
page_choices = SuperLazyIterator(get_page_choices)
self.limit_choices_to = limit_choices_to
kwargs['required'] = required
kwargs.pop('blank', None)
fields = (
LazyChoiceField(choices=site_choices, required=False, error_messages={'invalid': errors['invalid_site']}),
LazyChoiceField(choices=page_choices, required=False, error_messages={'invalid': errors['invalid_page']}),
ChoiceField(choices=get_site_choices, required=False, error_messages={'invalid': errors['invalid_site']}),
ChoiceField(choices=get_page_choices, required=False, error_messages={'invalid': errors['invalid_page']}),
)
super().__init__(fields, *args, **kwargs)

Expand Down
13 changes: 12 additions & 1 deletion cms/tests/test_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
ViewRestrictionInlineAdminForm,
)
from cms.api import assign_user_to_page, create_page, create_title
from cms.forms.fields import PageSelectFormField, SuperLazyIterator
from cms.forms.fields import LazyChoiceField, PageSelectFormField, SuperLazyIterator
from cms.forms.utils import get_page_choices, get_site_choices, update_site_and_page_choices
from cms.forms.widgets import ApplicationConfigSelect
from cms.models import ACCESS_PAGE, ACCESS_PAGE_AND_CHILDREN
Expand Down Expand Up @@ -238,6 +238,17 @@ def test_superlazy_iterator_behaves_properly_for_pages(self):

self.assertEqual(normal_result, list(lazy_result))

def test_lazy_choice_field_behaves_properly(self):
"""Ensure LazyChoiceField is really lazy"""
choices_called = False
def get_choices():
nonlocal choices_called
choices_called = True
return ("", "-----"),

LazyChoiceField(choices=SuperLazyIterator(get_choices))
self.assertFalse(choices_called, "Lazy choice function called")


class PermissionFormTestCase(CMSTestCase):

Expand Down
8 changes: 8 additions & 0 deletions cms/tests/test_page_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
CMSTestCase,
)
from cms.test_utils.util.context_managers import LanguageOverride, UserLoginContext
from cms.utils.compat import DJANGO_4_2
from cms.utils.compat.dj import installed_apps
from cms.utils.conf import get_cms_setting
from cms.utils.page import get_page_from_request
Expand Down Expand Up @@ -1373,6 +1374,10 @@ def test_set_overwrite_url(self):
expected = (
'<input id="id_overwrite_url" maxlength="255" '
'value="new-url" name="overwrite_url" type="text" />'
) if DJANGO_4_2 else (
'<input type="text" name="overwrite_url" value="new-url" '
'maxlength="255" aria-describedby="id_overwrite_url_helptext" '
'id="id_overwrite_url">'
)
changelist = self.get_admin_url(Page, 'changelist')
endpoint = self.get_admin_url(Page, 'advanced', cms_page.pk)
Expand Down Expand Up @@ -1425,6 +1430,9 @@ def test_remove_overwrite_url(self):
expected = (
'<input id="id_overwrite_url" maxlength="255" '
'name="overwrite_url" type="text" />'
) if DJANGO_4_2 else (
'<input type="text" name="overwrite_url" maxlength="255" '
'aria-describedby="id_overwrite_url_helptext" id="id_overwrite_url">'
)
changelist = self.get_admin_url(Page, 'changelist')
endpoint = self.get_admin_url(Page, 'advanced', cms_page.pk)
Expand Down
3 changes: 2 additions & 1 deletion cms/tests/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -884,8 +884,9 @@ def test_empty_plugin_description(self):
plugin_type='TextPlugin',
placeholder=placeholder,
position=1,
language=self.FIRST_LANG
language=self.FIRST_LANG,
)
a.save()

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

Expand Down
16 changes: 9 additions & 7 deletions cms/tests/test_sitemap.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
from cms.models import Page, Title
from cms.sitemaps import CMSSitemap
from cms.test_utils.testcases import CMSTestCase
from cms.utils.compat import DJANGO_4_2
from cms.utils.conf import get_cms_setting

protocol = "http" if DJANGO_4_2 else "https"

class SitemapTestCase(CMSTestCase):
def setUp(self):
Expand Down Expand Up @@ -93,9 +95,9 @@ def test_sitemap_items_location(self):
urlset = sitemap.get_urls()
for item in urlset:
if item['item'].path:
url = 'http://example.com/{}/{}/'.format(item['item'].language, item['item'].path)
url = f'{protocol}://example.com/{item["item"].language}/{item["item"].path}/'
else:
url = 'http://example.com/{}/{}'.format(item['item'].language, item['item'].path)
url = f'{protocol}://example.com/{item["item"].language}/'
self.assertEqual(item['location'], url)

def test_sitemap_published_titles(self):
Expand All @@ -110,9 +112,9 @@ def test_sitemap_published_titles(self):
for title in Title.objects.public():
page = title.page.get_public_object()
if title.path:
url = f'http://example.com/{title.language}/{title.path}/'
url = f'{protocol}://example.com/{title.language}/{title.path}/'
else:
url = f'http://example.com/{title.language}/{title.path}'
url = f'{protocol}://example.com/{title.language}/{title.path}'
if page.is_published('en') and not page.publisher_is_draft:
self.assertTrue(url in locations)
else:
Expand Down Expand Up @@ -142,9 +144,9 @@ def test_sitemap_unpublished_titles(self):
for path in unpublished_titles:
title = Title.objects.get(path=path)
if title.path:
url = f'http://example.com/{title.language}/{title.path}/'
url = f'{protocol}://example.com/{title.language}/{title.path}/'
else:
url = f'http://example.com/{title.language}/{title.path}'
url = f'{protocol}://example.com/{title.language}/{title.path}'
self.assertFalse(url in locations)

def test_sitemap_uses_public_languages_only(self):
Expand All @@ -159,7 +161,7 @@ def test_sitemap_uses_public_languages_only(self):

with self.settings(CMS_LANGUAGES=lang_settings):
for item in CMSSitemap().get_urls():
url = 'http://example.com/en/'
url = f'{protocol}://example.com/en/'

if item['item'].path:
url += item['item'].path + '/'
Expand Down
8 changes: 6 additions & 2 deletions cms/tests/test_toolbar.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from cms.toolbar.items import AjaxItem, Break, ItemSearchResult, LinkItem, SubMenu, ToolbarAPIMixin
from cms.toolbar.toolbar import CMSToolbar
from cms.toolbar_pool import toolbar_pool
from cms.utils.compat import DJANGO_4_2
from cms.utils.conf import get_cms_setting
from cms.utils.i18n import get_language_tuple
from cms.utils.urlutils import admin_reverse
Expand Down Expand Up @@ -640,7 +641,10 @@ def test_hide_toolbar_login_nonstaff(self):
def test_admin_logout_staff(self):
with override_settings(CMS_PERMISSION=True):
with self.login_user_context(self.get_staff()):
response = self.client.get('/en/admin/logout/')
if DJANGO_4_2:
response = self.client.get('/en/admin/logout/')
else:
response = self.client.post('/en/admin/logout/')
self.assertEqual(response.status_code, 200)

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

template_text = '''{% extends "base.html" %}
{% load cms_tags %}
Expand Down
18 changes: 13 additions & 5 deletions cms/toolbar/items.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.utils.functional import Promise

from cms.constants import LEFT, REFRESH_PAGE, RIGHT, URL_CHANGE
from cms.utils.compat import DJANGO_4_2


class ItemSearchResult:
Expand All @@ -24,11 +25,18 @@ def __int__(self):
return self.index


def may_be_lazy(thing):
if isinstance(thing, Promise):
return thing._proxy____args[0]
else:
return thing
if DJANGO_4_2:
def may_be_lazy(thing):
if isinstance(thing, Promise):
return thing._proxy____args[0]
else:
return thing
else:
def may_be_lazy(thing):
if isinstance(thing, Promise):
return thing._args[0]
else:
return thing


class ToolbarAPIMixin(metaclass=ABCMeta):
Expand Down
1 change: 1 addition & 0 deletions cms/utils/compat/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
DJANGO_3_2 = Version(DJANGO_VERSION) < Version('4.0')
DJANGO_3 = DJANGO_3_2
DJANGO_4_1 = Version(DJANGO_VERSION) < Version('4.2')
DJANGO_4_2 = Version(DJANGO_VERSION) < Version('4.3')
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


REQUIREMENTS = [
'Django>=3.2,<5.0',
'Django>=3.2',
'django-classy-tags>=0.7.2',
'django-formtools>=2.1',
'django-treebeard>=4.3',
Expand Down Expand Up @@ -35,6 +35,7 @@
'Framework :: Django :: 4.0',
'Framework :: Django :: 4.1',
'Framework :: Django :: 4.2',
'Framework :: Django :: 5.0',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
'Topic :: Software Development',
Expand Down
2 changes: 2 additions & 0 deletions test_requirements/django-5.0.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-r requirements_base.txt
Django>=5.0,<5.1

0 comments on commit 0950714

Please sign in to comment.