diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8bc4fe0..6193409 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,13 +11,17 @@ jobs: matrix: python-version: ["3.11", "3.10", "3.9"] django: [42, 41, 32] - cms: [311, 39] + cms: [4, 311, 39] continue-on-error: [true] exclude: - django: 41 cms: 39 - django: 42 cms: 39 + - django: 41 + cms: 4 + - django: 42 + cms: 4 steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} diff --git a/changes/82.feature b/changes/82.feature new file mode 100644 index 0000000..7ae4c66 --- /dev/null +++ b/changes/82.feature @@ -0,0 +1 @@ +Add support for django CMS 4+ diff --git a/changes/910.feature b/changes/910.feature new file mode 100644 index 0000000..9d140fd --- /dev/null +++ b/changes/910.feature @@ -0,0 +1,3 @@ +* Update tooling and ci test suite to Github Actions +* Add compatibility with Django 3.2 +* Drop compatibility with Django < 2.2 diff --git a/cms_helper.py b/cms_helper.py index 4dae60a..a9fabc7 100755 --- a/cms_helper.py +++ b/cms_helper.py @@ -6,9 +6,6 @@ def gettext(s): HELPER_SETTINGS = { - "NOSE_ARGS": [ - "-s", - ], "CACHES": { "default": { "BACKEND": "django.core.cache.backends.locmem.LocMemCache", @@ -19,6 +16,7 @@ def gettext(s): "content": 10, "permissions": 10, }, + "CMS_CONFIRM_VERSION4": True, "ROOT_URLCONF": "tests.test_utils.urls", "INSTALLED_APPS": [ "django.contrib.sitemaps", @@ -53,6 +51,12 @@ def gettext(s): }, }, } +try: + import djangocms_versioning # noqa: F401 + + HELPER_SETTINGS["INSTALLED_APPS"].append("djangocms_versioning") +except ImportError: + pass def run(): diff --git a/djangocms_page_sitemap/cms_toolbars.py b/djangocms_page_sitemap/cms_toolbars.py index 01c8a13..df7fa50 100644 --- a/djangocms_page_sitemap/cms_toolbars.py +++ b/djangocms_page_sitemap/cms_toolbars.py @@ -1,7 +1,3 @@ -from cms.api import get_page_draft -from cms.cms_toolbars import PAGE_MENU_THIRD_BREAK -from cms.toolbar.items import Break -from cms.toolbar_base import CMSToolbar from cms.toolbar_pool import toolbar_pool from cms.utils.conf import get_cms_setting from cms.utils.permissions import has_page_permission @@ -10,14 +6,20 @@ from .models import PageSitemapProperties +# Handle versioned toolbar if it exists, otherwise just use the normal CMS toolbar +try: + from djangocms_versioning.cms_toolbars import VersioningPageToolbar as PageToolbar +except ImportError: + from cms.cms_toolbars import PageToolbar + + PAGE_SITEMAP_MENU_TITLE = _("Sitemap properties") @toolbar_pool.register -class PageSitemapPropertiesMeta(CMSToolbar): +class PageSitemapPropertiesMeta(PageToolbar): def populate(self): - # always use draft if we have a page - self.page = get_page_draft(self.request.current_page) + self.page = self.request.current_page if not self.page: return if self.page.is_page_type: @@ -36,7 +38,6 @@ def populate(self): if has_global_current_page_change_permission or can_change: not_edit_mode = not self.toolbar.edit_mode_active current_page_menu = self.toolbar.get_or_create_menu("page") - position = current_page_menu.find_first(Break, identifier=PAGE_MENU_THIRD_BREAK) - 1 # Page tags try: page_extension = PageSitemapProperties.objects.get(extended_object_id=self.page.pk) @@ -45,21 +46,14 @@ def populate(self): try: if page_extension: url = reverse( - "admin:djangocms_page_sitemap_pagesitemapproperties_change", - args=(page_extension.pk,), + "admin:djangocms_page_sitemap_pagesitemapproperties_change", args=(page_extension.pk,) ) else: url = "{}?extended_object={}".format( - reverse("admin:djangocms_page_sitemap_pagesitemapproperties_add"), - self.page.pk, + reverse("admin:djangocms_page_sitemap_pagesitemapproperties_add"), self.page.pk ) except NoReverseMatch: # pragma: no cover # not in urls pass else: - current_page_menu.add_modal_item( - PAGE_SITEMAP_MENU_TITLE, - url=url, - disabled=not_edit_mode, - position=position, - ) + current_page_menu.add_modal_item(PAGE_SITEMAP_MENU_TITLE, url=url, disabled=not_edit_mode) diff --git a/djangocms_page_sitemap/models.py b/djangocms_page_sitemap/models.py index ea2fb99..15db55a 100644 --- a/djangocms_page_sitemap/models.py +++ b/djangocms_page_sitemap/models.py @@ -14,10 +14,7 @@ @extension_pool.register class PageSitemapProperties(PageExtension): changefreq = models.CharField( - _("Change frequency"), - max_length=20, - default="monthly", - choices=PAGE_SITEMAP_CHANGEFREQ_LIST.items(), + _("Change frequency"), max_length=20, default="monthly", choices=PAGE_SITEMAP_CHANGEFREQ_LIST.items() ) priority = models.DecimalField( _("Priority"), @@ -28,14 +25,10 @@ class PageSitemapProperties(PageExtension): ) include_in_sitemap = models.BooleanField(_("Include in sitemap"), default=True) noindex = models.BooleanField( - _("Mark as no index"), - default=False, - help_text=_("Add meta tag robots with value noindex"), + _("Mark as no index"), default=False, help_text=_("Add meta tag robots with value noindex") ) noarchive = models.BooleanField( - _("Mark as no archive"), - default=False, - help_text=_("Add meta tag robots with value noarchive"), + _("Mark as no archive"), default=False, help_text=_("Add meta tag robots with value noarchive") ) robots_extra = models.CharField( _("Extra robots value"), diff --git a/djangocms_page_sitemap/sitemap.py b/djangocms_page_sitemap/sitemap.py index eb9a47c..676459b 100644 --- a/djangocms_page_sitemap/sitemap.py +++ b/djangocms_page_sitemap/sitemap.py @@ -1,9 +1,12 @@ from cms.sitemaps import CMSSitemap +from cms.utils import get_current_site +from cms.utils.i18n import get_public_languages from django.core.cache import cache +from django.db.models import Prefetch from .models import PageSitemapProperties from .settings import PAGE_SITEMAP_CACHE_DURATION, PAGE_SITEMAP_DEFAULT_CHANGEFREQ -from .utils import get_cache_key +from .utils import get_cache_key, is_versioning_enabled class ExtendedSitemap(CMSSitemap): @@ -11,7 +14,41 @@ class ExtendedSitemap(CMSSitemap): default_priority = CMSSitemap.priority def items(self): - return super().items().exclude(page__pagesitemapproperties__include_in_sitemap=False) + try: + from cms.models import PageContent, PageUrl + + # FIXME:This method was created from this commit: + # https://github.com/divio/django-cms/blob/2894ae8bcf92092d947a097499c01ab2bbb0e6df/cms/sitemaps/cms_sitemap.py + site = get_current_site() + languages = get_public_languages(site_id=site.pk) + page_content_prefetch = Prefetch( + "page__pagecontent_set", + queryset=PageContent.objects.filter( + language__in=languages, + ), + ) + all_urls = ( + PageUrl.objects.get_for_site(site) + .prefetch_related(page_content_prefetch) + .filter( + language__in=languages, + path__isnull=False, + page__login_required=False, + page__node__site=site, + ) + .exclude(page__pagesitemapproperties__include_in_sitemap=False) + .order_by("page__node__path") + ) + valid_urls = [] + for page_url in all_urls: + for page_content in page_url.page.pagecontent_set.all(): + if page_url.language == page_content.language: + valid_urls.append(page_url) + break + + return valid_urls + except ImportError: + return super().items().exclude(page__pagesitemapproperties__include_in_sitemap=False) def priority(self, title): ext_key = get_cache_key(title.page) @@ -44,3 +81,22 @@ def changefreq(self, title): return title.page.pagesitemapproperties.changefreq except PageSitemapProperties.DoesNotExist: return self.default_changefreq + + def lastmod(self, page_url): + # if versioning is enabled we return the latest version modified using the versioning + # modified date. if versioning is disabled we return the page changed_date + if is_versioning_enabled(): + from cms.models import PageContent + + site = get_current_site() + page_contents = PageContent.objects.filter( + page=page_url.page, + language=page_url.language, + page__node__site=site, + ).first() + + if page_contents: + published_version = page_contents.versions.first() + return published_version.modified + + return page_url.page.changed_date diff --git a/djangocms_page_sitemap/utils.py b/djangocms_page_sitemap/utils.py index e7b4c9a..2428efb 100644 --- a/djangocms_page_sitemap/utils.py +++ b/djangocms_page_sitemap/utils.py @@ -1,4 +1,5 @@ from cms.cache import _get_cache_key +from django.apps import apps def get_cache_key(page): @@ -7,3 +8,17 @@ def get_cache_key(page): """ site_id = page.node.site_id return _get_cache_key("page_sitemap", page, "default", site_id) + + +def is_versioning_enabled(): + """Check if djangocms-versioning plugin is installed.""" + try: + from cms.models import PageContent + + try: + app_config = apps.get_app_config("djangocms_versioning") + return app_config.cms_extension.is_content_model_versioned(PageContent) + except LookupError: # pragma: no cover + return False + except ImportError: + return False diff --git a/tests/base.py b/tests/base.py index e2d8a04..4611dcf 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1,10 +1,13 @@ from io import StringIO +from cms.api import create_page, create_title from cms.utils.i18n import get_language_list from django.contrib.auth.models import User from django.http import HttpResponse, SimpleCookie from django.test import RequestFactory, TestCase +from djangocms_page_sitemap.utils import is_versioning_enabled + class BaseTest(TestCase): """ @@ -27,8 +30,14 @@ def setUpClass(cls): cls.user_normal = User.objects.create(username="normal") def get_pages(self): - from cms.api import create_page, create_title + try: + from cms.models import PageContent # noqa: F401 + + return self._cms_4_pages() + except ImportError: + return self._cms_3_pages() + def _cms_3_pages(self): page_1 = create_page("page one", "page.html", language="en") page_2 = create_page("page two", "page.html", language="en") page_3 = create_page("page three", "page.html", language="en") @@ -49,6 +58,29 @@ def get_pages(self): page_3.get_draft_object(), ) + def _cms_4_pages(self): + from cms.models import PageContent # noqa: F401 + + page_1 = create_page("page one", "page.html", language="en", created_by=self.user) + page_2 = create_page("page two", "page.html", language="en", created_by=self.user) + page_3 = create_page("page three", "page.html", language="en", created_by=self.user) + page_content1 = PageContent._base_manager.get(page=page_1, language="en") + page_content2 = PageContent._base_manager.get(page=page_2, language="en") + page_content3 = PageContent._base_manager.get(page=page_3, language="en") + page_1_content_fr = create_title(language="fr", title="page un", page=page_1, created_by=self.user) + page_1_content_it = create_title(language="it", title="pagina uno", page=page_1, created_by=self.user) + page_3_content_fr = create_title(language="fr", title="page trois", page=page_3, created_by=self.user) + if is_versioning_enabled(): + page_content1.versions.first().publish(self.user) + page_content2.versions.first().publish(self.user) + page_content3.versions.first().publish(self.user) + page_1_content_fr.versions.first().publish(self.user) + page_1_content_it.versions.first().publish(self.user) + page_3_content_fr.versions.first().publish(self.user) + if hasattr(page_1, "set_as_homepage"): + page_1.set_as_homepage() + return page_1, page_2, page_3 + def get_request(self, page, lang): request = self.request_factory.get(page.get_path(lang)) request.current_page = page @@ -75,7 +107,7 @@ def get_page_request(self, page, user, path=None, edit=False, lang_code="en"): request.GET = {"edit_off": None} request.current_page = page if hasattr(ToolbarMiddleware, "process_request"): - mid = ToolbarMiddleware() + mid = ToolbarMiddleware(lambda req: HttpResponse()) mid.process_request(request) else: mid = ToolbarMiddleware(lambda req: HttpResponse()) diff --git a/tests/test_models.py b/tests/test_models.py index 42f72b7..4aee45a 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -56,17 +56,19 @@ def test_robots_options(self): def test_robots_page_parameter(self): page1, page2, page3 = self.get_pages() extension = PageSitemapProperties.objects.create(extended_object=page1, priority="0.2", changefreq="never") - page1.publish("en") + if hasattr(page1, "publish"): + page1.publish("en") extension.refresh_from_db() template = "{% load robots_index %}{% page_robots %}" expected = "" - context = {"request": self.get_page_request(page2.get_public_object(), AnonymousUser())} + context = {"request": self.get_page_request(page2, AnonymousUser())} self._test_robots_tag(template, context, expected) extension.noindex = True extension.save() - page1.publish("en") + if hasattr(page1, "publish"): + page1.publish("en") expected = "" self._test_robots_tag(template, context, expected) @@ -76,46 +78,51 @@ def test_robots_page_parameter(self): extension.noarchive = True extension.save() - page1.publish("en") + if hasattr(page1, "publish"): + page1.publish("en") expected = '' self._test_robots_tag(template, context, expected) extension.robots_extra = "nodmoz" extension.save() - page1.publish("en") + if hasattr(page1, "publish"): + page1.publish("en") expected = '' self._test_robots_tag(template, context, expected) def test_robots_page_no_site(self): page1, page2, page3 = self.get_pages() extension = PageSitemapProperties.objects.create(extended_object=page1, priority="0.2", changefreq="never") - page1.publish("en") + if hasattr(page1, "publish"): + page1.publish("en") extension.refresh_from_db() template = '{% load robots_index %}{% page_robots None "abc" %}' expected = "" - context = {"request": self.get_page_request(page2.get_public_object(), AnonymousUser())} + context = {"request": self.get_page_request(page2, AnonymousUser())} self._test_robots_tag(template, context, expected) def test_robots_page_no_page(self): page1, page2, page3 = self.get_pages() extension = PageSitemapProperties.objects.create(extended_object=page1, priority="0.2", changefreq="never") - page1.publish("en") + if hasattr(page1, "publish"): + page1.publish("en") extension.refresh_from_db() template = '{% load robots_index %}{% page_robots "abc" %}' expected = "" - context = {"request": self.get_page_request(page2.get_public_object(), AnonymousUser())} + context = {"request": self.get_page_request(page2, AnonymousUser())} self._test_robots_tag(template, context, expected) def test_robots_page_other_site(self): site_2 = Site.objects.create(domain="http://othersite.com") page1, page2, page3 = self.get_pages() extension = PageSitemapProperties.objects.create(extended_object=page1, priority="0.2", changefreq="never") - page1.publish("en") + if hasattr(page1, "publish"): + page1.publish("en") extension.refresh_from_db() template = "{%% load robots_index %%}{%% page_robots None %s %%}" % site_2.pk expected = "" - context = {"request": self.get_page_request(page2.get_public_object(), AnonymousUser())} + context = {"request": self.get_page_request(page2, AnonymousUser())} self._test_robots_tag(template, context, expected) diff --git a/tests/test_sitemap.py b/tests/test_sitemap.py index 23208e0..facd270 100644 --- a/tests/test_sitemap.py +++ b/tests/test_sitemap.py @@ -1,38 +1,55 @@ from decimal import Decimal +from unittest import skipIf +import cms +from cms.api import create_page, create_title +from cms.test_utils.util.fuzzy_int import FuzzyInt from django.core.cache import cache from django.utils.timezone import now from djangocms_page_sitemap.models import PageSitemapProperties from djangocms_page_sitemap.sitemap import ExtendedSitemap -from djangocms_page_sitemap.utils import get_cache_key +from djangocms_page_sitemap.utils import get_cache_key, is_versioning_enabled from .base import BaseTest class SitemapTest(BaseTest): def test_sitemap_base(self): + page1, page2, page3 = self.get_pages() + + try: + title_language = page1.get_title_obj().language + except AttributeError: + title_language = page1.get_page_content_obj_attribute("language") + + sitemap = self.client.get("/sitemap.xml") test_string = ( - "http://example.com/it/" - "%smonthly0.5" % now().strftime("%Y-%m-%d") + "http://example.com/%s/%s" + "monthly0.5" % (title_language, now().strftime("%Y-%m-%d")) ) - self.get_pages() - sitemap = self.client.get("/sitemap.xml") self.assertContains(sitemap, test_string) def test_sitemap_extended(self): - test_string = ( - "http://example.com/it/" - "%snever0.2" % now().strftime("%Y-%m-%d") - ) page1, page2, page3 = self.get_pages() PageSitemapProperties.objects.create(extended_object=page1, priority="0.2", changefreq="never") - page1.publish("it") + if hasattr(page1, "publish"): + page1.publish("it") + + try: + title_language = page1.get_title_obj().language + except AttributeError: + title_language = page1.get_page_content_obj_attribute("language") + test_string = ( + "http://example.com/%s/%s" + "never0.2" % (title_language, now().strftime("%Y-%m-%d")) + ) sitemap = self.client.get("/sitemap.xml") self.assertContains(sitemap, test_string) def test_sitemap_exclude(self): page1, page2, page3 = self.get_pages() + PageSitemapProperties.objects.create( extended_object=page3, priority="0.2", @@ -40,25 +57,30 @@ def test_sitemap_exclude(self): include_in_sitemap=False, ) sitemap = ExtendedSitemap() - # unpublished since change, still in the sitemap - self.assertEqual(len(sitemap.items()), 6) - - page3.publish("en") - page3.publish("fr") - sitemap = ExtendedSitemap() - # published, then no longer in the sitemap - self.assertEqual(len(sitemap.items()), 4) + # If publish is available, page must be published for property to be recognized + if hasattr(page1, "publish"): + # unpublished since change, still in the sitemap + self.assertEqual(len(sitemap.items()), 6) + page3.publish("en") + page3.publish("fr") + sitemap = ExtendedSitemap() + # published, then no longer in the sitemap + self.assertEqual(len(sitemap.items()), 4) + else: + self.assertEqual(len(sitemap.items()), 4) def test_sitemap_cache(self): page1, page2, page3 = self.get_pages() PageSitemapProperties.objects.create(extended_object=page1, priority="0.2", changefreq="never") PageSitemapProperties.objects.create(extended_object=page3, priority="0.8", changefreq="hourly") - page1.publish("fr") - page1 = page1.get_public_object() - page3.publish("fr") - page3 = page3.get_public_object() + if hasattr(page1, "publish"): + page1.publish("fr") + page1 = page1.get_public_object() + page3.publish("fr") + page3 = page3.get_public_object() sitemap = ExtendedSitemap() self.assertEqual(len(sitemap.items()), 6) + for item in sitemap.items(): if item.page.pk == page1.pk: self.assertEqual(sitemap.changefreq(item), "never") @@ -76,3 +98,36 @@ def test_sitemap_cache(self): ext_key = get_cache_key(page3) page3.delete() self.assertEqual(cache.get(ext_key), None) + + @skipIf(not is_versioning_enabled(), "This test can only run when versioning is installed") + def test_pageurl_lastmod_with_cms4_versioning(self): + # Check the latest version modified date for the page is checked for lastmod() + # if versioning is enabled, Currenly test is skipped , as this require changes in testsuite + page_1 = create_page("page-one", "page.html", language="en", created_by=self.user) + page_content = create_title(title="page un", language="en", page=page_1, created_by=self.user) + if is_versioning_enabled(): + page_content.versions.first().publish(self.user) + last_modified_date = "%s" % (page_content.versions.first().modified.strftime("%Y-%m-%d")) + expected_string = ( + "http://example.com%s%smonthly" + "0.5" + % (page_1.get_absolute_url(language="en"), last_modified_date) + ) + sitemap = self.client.get("/sitemap.xml") + + self.assertContains(sitemap, expected_string) + + def test_sitemap_items_query_performance(self): + page1, page2, page3 = self.get_pages() + + PageSitemapProperties.objects.create(extended_object=page1, priority="0.2", changefreq="never") + PageSitemapProperties.objects.create(extended_object=page3, priority="0.8", changefreq="hourly") + sitemap = ExtendedSitemap() + + if cms.__version__ < "4.0": + with self.assertNumQueries(1): + self.assertEqual(len(sitemap.items()), 6) + else: + max_queries = 4 + with self.assertNumQueries(FuzzyInt(3, max_queries)): + self.assertEqual(len(sitemap.items()), 6) diff --git a/tests/test_toolbar.py b/tests/test_toolbar.py index 1270511..1ec9857 100644 --- a/tests/test_toolbar.py +++ b/tests/test_toolbar.py @@ -1,3 +1,8 @@ +from unittest import skipIf + +import cms +from cms.api import create_page +from cms.test_utils.testcases import CMSTestCase from cms.toolbar.items import Menu, ModalItem from django.contrib.auth.models import Permission, User from django.test.utils import override_settings @@ -7,10 +12,24 @@ from djangocms_page_sitemap.cms_toolbars import PAGE_SITEMAP_MENU_TITLE from djangocms_page_sitemap.models import PageSitemapProperties +from djangocms_page_sitemap.utils import is_versioning_enabled from .base import BaseTest +def find_toolbar_buttons(button_name, toolbar): + """ + Taken from: from djangocms_versioning.test_utils.test_helpers import find_toolbar_buttons + + CAVEAT: This test helper is not currently accesible due to the fact that it would then enforce + versioning test packages and factory boy on this test suite. + """ + found = [] + for button_list in toolbar.get_right_items(): + found = found + [button for button in button_list.buttons if button.name == button_name] + return found + + class ToolbarTest(BaseTest): def test_no_page(self): """ @@ -144,10 +163,40 @@ def test_toolbar_with_items(self): meta_menu = page_menu.find_items(ModalItem, name="%s ..." % force_str(PAGE_SITEMAP_MENU_TITLE))[0].item self.assertTrue( meta_menu.url.startswith( - reverse( - "admin:djangocms_page_sitemap_pagesitemapproperties_change", - args=(page_ext.pk,), - ) + reverse("admin:djangocms_page_sitemap_pagesitemapproperties_change", args=(page_ext.pk,)) ) ) self.assertEqual(force_str(page_ext), force_str(_("Sitemap values for Page %s") % page1.pk)) + + +class VersioningToolbarTest(CMSTestCase): + @skipIf(cms.__version__ < "4.0", "Versioning not available if django CMS < 4") + def test_toolbar_buttons_are_not_duplicated(self): + """ + The toolbar for djangocms-page-sitemap doesn't affect the toolbar buttons. + + This test Can be ran with or without versioning and should return the same result! + """ + from cms.models import PageContent + from cms.toolbar.utils import get_object_preview_url + + user = self.get_superuser() + page_1 = create_page("page-one", "page.html", language="en", created_by=user) + page_content = PageContent._base_manager.get(page=page_1, language="en") + + if is_versioning_enabled(): + page_content.versions.first().publish(user) + preview_endpoint = get_object_preview_url(page_content, language="en") + + with self.login_user_context(self.get_superuser()): + response = self.client.post(preview_endpoint) + + edit_button_list = find_toolbar_buttons("Edit", response.wsgi_request.toolbar) + new_draft_button_list = find_toolbar_buttons("New Draft", response.wsgi_request.toolbar) + create_button_list = find_toolbar_buttons("Create", response.wsgi_request.toolbar) + + self.assertEqual(len(create_button_list), 1) + if is_versioning_enabled(): + self.assertEqual(len(new_draft_button_list), 1) + else: + self.assertEqual(len(edit_button_list), 1) diff --git a/tests/test_utils/urls.py b/tests/test_utils/urls.py index 01e4af4..401c1da 100644 --- a/tests/test_utils/urls.py +++ b/tests/test_utils/urls.py @@ -11,6 +11,11 @@ admin.autodiscover() urlpatterns = [ + re_path(r"^media/(?P.*)$", serve, {"document_root": settings.MEDIA_ROOT, "show_indexes": True}), + re_path( + r"^media/cms/(?P.*)$", serve, {"document_root": get_cms_setting("MEDIA_ROOT"), "show_indexes": True} + ), + path("", include(sitemap_urls)), re_path( r"^media/(?P.*)$", serve, diff --git a/tox.ini b/tox.ini index 97ce37b..460a976 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ envlist = ruff pypi-description towncrier + py{311,310,39}-django{42}-cms{4} py{311,310,39}-django{42,41}-cms{311} py{311,310,39}-django{32}-cms{311,39} @@ -19,6 +20,8 @@ deps = django42: Django~=4.2.0 cms39: https://github.com/django-cms/django-cms/archive/release/3.9.x.zip cms311: https://github.com/yakky/django-cms/archive/release/3.11.x.zip + cms4: https://github.com/django-cms/django-cms/archive/release/4.1.x.zip + cms4: https://github.com/django-cms/djangocms-versioning/archive/refs/heads/master.zip -r{toxinidir}/requirements-test.txt passenv = COMMAND