diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 55bd8a566..74ee428a5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,9 +16,9 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ 3.7, 3.8, 3.9 ] + python-version: [ 3.8, 3.9, '3.10' ] requirements-file: [ - dj22_cms40.txt, + dj42_cms40.txt, dj32_cms40.txt, ] diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2864e5f85..bce3eb798 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,10 @@ Changelog Unreleased ========== +* Python 3.10 support added +* Python 3.7 support removed +* Django 4.2 support added +* Django 2.2 support removed 1.8.3 (2024-03-06) ================== diff --git a/README.rst b/README.rst index d65bc4796..eca4dc87f 100644 --- a/README.rst +++ b/README.rst @@ -1,5 +1,5 @@ ==================== -djangocms-navigation +djangocms-navigation ==================== Installation diff --git a/djangocms_navigation/admin.py b/djangocms_navigation/admin.py index d925ba9e6..e6f6846be 100644 --- a/djangocms_navigation/admin.py +++ b/djangocms_navigation/admin.py @@ -13,7 +13,7 @@ from django.shortcuts import get_object_or_404, redirect, render from django.template.loader import render_to_string from django.template.response import TemplateResponse -from django.urls import re_path, reverse, reverse_lazy +from django.urls import path, re_path, reverse, reverse_lazy from django.utils.html import format_html, format_html_join from django.utils.text import slugify from django.utils.translation import gettext_lazy as _ @@ -26,6 +26,7 @@ from djangocms_versioning.models import Version from treebeard.admin import TreeAdmin +from .compat import TREEBEARD_4_5 from .conf import TREE_MAX_RESULT_PER_PAGE_COUNT from .filters import LanguageFilter from .forms import MenuContentForm, MenuItemForm @@ -66,6 +67,7 @@ def url_for_result(self, result): ) +@admin.register(MenuContent) class MenuContentAdmin(ExtendedVersionAdminMixin, admin.ModelAdmin): form = MenuContentForm menu_model = Menu @@ -130,14 +132,15 @@ def get_list_display(self, request): return menu_content_list_display + @admin.display( + description=_("Lock State") + ) def is_locked(self, obj): version = self.get_version(obj) if version.state == DRAFT and version_is_locked(version): return render_to_string("djangocms_version_locking/admin/locked_icon.html") return "" - is_locked.short_description = _("Lock State") - def _get_references_link(self, obj, request): menu_content_type = ContentType.objects.get( app_label=self.model._meta.app_label, model=Menu._meta.model_name, @@ -153,6 +156,10 @@ def _get_references_link(self, obj, request): {"url": url} ) + @admin.display( + description="Main Navigation", + boolean=True, + ) def get_main_navigation(self, obj): """ Return main_navigation field from Menu associated with MenuContent. @@ -161,9 +168,6 @@ def get_main_navigation(self, obj): """ return obj.menu.main_navigation - get_main_navigation.short_description = "Main Navigation" - get_main_navigation.boolean = True - def _get_main_navigation_link(self, obj, request, disabled=False): """ Return an admin link to the confirmation page for setting main confirmations @@ -237,6 +241,9 @@ def change_view(self, request, object_id, form_url="", extra_context=None): ) ) + @admin.display( + description=_("Menu Items") + ) def get_menuitem_link(self, obj): object_menuitem_url = reverse( "admin:{app}_{model}_list".format( @@ -252,9 +259,9 @@ def get_menuitem_link(self, obj): object_menuitem_url, _("Items"), ) - get_menuitem_link.short_description = _("Menu Items") +@admin.register(MenuItem) class MenuItemAdmin(TreeAdmin): form = MenuItemForm menu_content_model = MenuContent @@ -277,47 +284,47 @@ class Media: def get_urls(self): info = self.model._meta.app_label, self.model._meta.model_name return [ - re_path( - r"^$", + path( + "", self.admin_site.admin_view(self.changelist_view), name="{}_{}_changelist".format(*info), ), - re_path( - r"^(?P\d+)/$", + path( + "/", self.admin_site.admin_view(self.changelist_view), name="{}_{}_list".format(*info), ), - re_path( - r"^(?P\d+)/preview/$", + path( + "/preview/", self.admin_site.admin_view(self.preview_view), name="{}_{}_preview".format(*info), ), - re_path( - r"^(?P\d+)/add/$", + path( + "/add/", self.admin_site.admin_view(self.add_view), name="{}_{}_add".format(*info), ), - re_path( - r"^(?P\d+)/(?P\d+)/change/$", + path( + "//change/", self.admin_site.admin_view(self.change_view), name="{}_{}_change".format(*info), ), - re_path( - r"^(?P\d+)/(?P\d+)/delete/$", + path( + "//delete/", self.admin_site.admin_view(self.delete_view), name="{}_{}_delete".format(*info), ), - re_path( - r"^(?P\d+)/move/$", + path( + "/move/", self.admin_site.admin_view(self.move_node), name="{}_{}_move_node".format(*info), ), - re_path( - r"^(?P\d+)/jsi18n/$", + path( + "/jsi18n/", JavaScriptCatalog.as_view(packages=["treebeard"]), ), - re_path( - r"^select2/$", + path( + "select2/", self.admin_site.admin_view(ContentObjectSelect2View.as_view( menu_content_model=self.menu_content_model, )), @@ -325,8 +332,8 @@ def get_urls(self): self.model._meta.app_label ) ), - re_path( - r"^(?P\d+)/messages/$", + path( + "/messages/", self.admin_site.admin_view(MessageStorageView.as_view()), name="{}_{}_message_storage".format(*info), ), @@ -442,7 +449,7 @@ def change_view(self, request, object_id, menu_content_id=None, form_url="", ext kwargs={"menu_content_id": menu_content_id, "object_id": object_id}, ) return super().change_view( - request, object_id, form_url="", extra_context=extra_context + request, str(object_id), form_url="", extra_context=extra_context ) def add_view(self, request, menu_content_id=None, form_url="", extra_context=None): @@ -492,7 +499,10 @@ def get_changelist_template(self, request): """ if is_preview_url(request=request): return "admin/djangocms_navigation/menuitem/preview.html" - return "admin/djangocms_navigation/menuitem/change_list.html" + elif TREEBEARD_4_5: + return "admin/djangocms_navigation/menuitem/change_list.html" + else: + return "admin/djangocms_navigation/menuitem/tree_change_list.html" def changelist_view(self, request, menu_content_id=None, extra_context=None): self.change_list_template = self.get_changelist_template(request=request) @@ -632,7 +642,7 @@ def delete_view(self, request, object_id, menu_content_id=None, form_url="", ext to_be_deleted = [f"Menu item: {menu_item}"] extra_context["deleted_objects"] = self._get_to_be_deleted(menu_item.get_children(), to_be_deleted) - return super().delete_view(request, object_id, extra_context) + return super().delete_view(request, str(object_id), extra_context) def response_delete(self, request, obj_display, obj_id): """ @@ -784,13 +794,14 @@ def __new__(cls, *args, **kwargs): return Form + @admin.display( + description=_("URL") + ) def get_object_url(self, obj): if obj.content: obj_url = obj.content.get_absolute_url() return format_html("{0}", obj_url) - get_object_url.short_description = _("URL") - @property def _versioning_enabled(self): """Helper property to check if versioning is enabled for navigation""" @@ -798,7 +809,3 @@ def _versioning_enabled(self): return apps.get_app_config( self.model._meta.app_label ).cms_config.djangocms_versioning_enabled - - -admin.site.register(MenuItem, MenuItemAdmin) -admin.site.register(MenuContent, MenuContentAdmin) diff --git a/djangocms_navigation/compat.py b/djangocms_navigation/compat.py new file mode 100644 index 000000000..3517f7d59 --- /dev/null +++ b/djangocms_navigation/compat.py @@ -0,0 +1,5 @@ +from packaging.version import Version +from treebeard import __version__ as treebeard_version + + +TREEBEARD_4_5 = Version(treebeard_version) < Version('4.6') diff --git a/djangocms_navigation/static/djangocms_navigation/js/navigation-tree-admin.js b/djangocms_navigation/static/djangocms_navigation/js/navigation-tree-admin.js index f8513ce95..3f8aef78e 100644 --- a/djangocms_navigation/static/djangocms_navigation/js/navigation-tree-admin.js +++ b/djangocms_navigation/static/djangocms_navigation/js/navigation-tree-admin.js @@ -14,6 +14,26 @@ Original code found in treebeard-admin.js const EXPANDED_SESSION_KEY = 'expanded-'; + // Add jQuery util for disabling selection + // Originally taken from jquery-ui (where it is deprecated) + // https://api.jqueryui.com/disableSelection/ + if($.fn.disableSelection == undefined) { + $.fn.extend( { + disableSelection: ( function() { + var eventType = "onselectstart" in document.createElement( "div" ) ? "selectstart" : "mousedown"; + return function() { + return this.on( eventType + ".ui-disableSelection", function( event ) { + event.preventDefault(); + } ); + }; + } )(), + + enableSelection: function() { + return this.off( ".ui-disableSelection" ); + } + } ); + } + // This is the basic Node class, which handles UI tree operations for each 'row' var Node = function (elem) { var $elem = $(elem); @@ -146,7 +166,7 @@ Original code found in treebeard-admin.js if (document.cookie && document.cookie != '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { - var cookie = jQuery.trim(cookies[i]); + var cookie = $.trim(cookies[i]); // Does this cookie string begin with the name we want? if (cookie.substring(0, name.length + 1) == (name + '=')) { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); diff --git a/djangocms_navigation/templates/admin/djangocms_navigation/menuitem/tree_change_list.html b/djangocms_navigation/templates/admin/djangocms_navigation/menuitem/tree_change_list.html new file mode 100644 index 000000000..e3300135e --- /dev/null +++ b/djangocms_navigation/templates/admin/djangocms_navigation/menuitem/tree_change_list.html @@ -0,0 +1,45 @@ +{# copy from djangocms-treebeard tree_change_list.html to override media files #} +{% extends "admin/change_list.html" %} +{% load static admin_list admin_tree admin_urls navigation_admin_tree i18n djangocms_versioning %} + +{% block extrastyle %} + {{ block.super }} + + +{% endblock %} + +{% block extrahead %} + {{ block.super }} + + +{% endblock %} + +{% block result_list %} + {% if action_form and actions_on_top and cl.full_result_count %} + {% admin_actions %} + {% endif %} + {% result_tree cl request %} + {% if action_form and actions_on_bottom and cl.full_result_count %} + {% admin_actions %} + {% endif %} +{% endblock %} + +{% block object-tools-items %} +
  • + + {% blocktrans with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktrans %} + +
  • +
  • + + {% trans "Preview" %} + +
  • +{% if versioning_enabled_for_nav %} +
  • + + {% trans "Versions" %} + +
  • +{% endif %} +{% endblock %} diff --git a/djangocms_navigation/templatetags/navigation_admin_tree.py b/djangocms_navigation/templatetags/navigation_admin_tree.py index e13f0191d..58467bd26 100644 --- a/djangocms_navigation/templatetags/navigation_admin_tree.py +++ b/djangocms_navigation/templatetags/navigation_admin_tree.py @@ -5,6 +5,7 @@ result_hidden_fields, ) from django.templatetags.static import static +from django.urls import reverse from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ @@ -104,7 +105,7 @@ def treebeard_js(): """ js_file = static('djangocms_navigation/js/navigation-tree-admin.js') - + jsi18n_url = reverse('admin:jsi18n') jquery_ui = static('treebeard/jquery-ui-1.8.5.custom.min.js') # Jquery UI is needed to call disableSelection() on drag and drop so @@ -118,7 +119,7 @@ def treebeard_js(): '' '') return format_html( - TEMPLATE, "jsi18n", mark_safe(js_file), mark_safe(jquery_ui)) + TEMPLATE, jsi18n_url, mark_safe(js_file), mark_safe(jquery_ui)) admin_tree.treebeard_js = treebeard_js diff --git a/setup.py b/setup.py index a79dfab59..4873208bd 100644 --- a/setup.py +++ b/setup.py @@ -4,8 +4,8 @@ INSTALL_REQUIREMENTS = [ - "Django>=1.11,<3.3", - "django-treebeard>=4.3,<4.6.0", + "Django>=3.2,<5.0", + "django-treebeard>=4.3", "django-cms", ] diff --git a/tests/requirements/dj22_cms40.txt b/tests/requirements/dj22_cms40.txt deleted file mode 100644 index cf2c0d830..000000000 --- a/tests/requirements/dj22_cms40.txt +++ /dev/null @@ -1,5 +0,0 @@ --r ./requirements_base.txt - -Django>=2.2,<3.0 -django-classy-tags<2.0.0 -django-sekizai<2.0.0 diff --git a/tests/requirements/dj32_cms40.txt b/tests/requirements/dj32_cms40.txt index c5e1fc937..1ab41c37f 100644 --- a/tests/requirements/dj32_cms40.txt +++ b/tests/requirements/dj32_cms40.txt @@ -1,5 +1,4 @@ -r ./requirements_base.txt Django>=3.2,<4.0 -django-classy-tags -django-sekizai +django-treebeard<=4.5.1 \ No newline at end of file diff --git a/tests/requirements/dj42_cms40.txt b/tests/requirements/dj42_cms40.txt new file mode 100644 index 000000000..6552862ad --- /dev/null +++ b/tests/requirements/dj42_cms40.txt @@ -0,0 +1,4 @@ +-r ./requirements_base.txt + +Django>=4.2,<5.0 +django-treebeard>=4.6.0 \ No newline at end of file diff --git a/tests/requirements/requirements_base.txt b/tests/requirements/requirements_base.txt index 329a0a83e..0d28bff53 100644 --- a/tests/requirements/requirements_base.txt +++ b/tests/requirements/requirements_base.txt @@ -1,15 +1,19 @@ beautifulsoup4 coverage django-app-helper +django-classy-tags +django-sekizai factory_boy flake8 isort lxml tox -https://github.com/django-cms/django-cms/tarball/4.0.0#egg=django-cms -https://github.com/divio/djangocms-text-ckeditor/tarball/support/4.0.x#egg=djangocms-text-ckeditor -https://github.com/django-cms/djangocms-versioning/tarball/1.2.2#egg=djangocms-versioning + +# Unreleased django-cms 4.0 compatible packages +https://github.com/django-cms/django-cms/tarball/release/4.0.1.x#egg=django-cms +https://github.com/divio/djangocms-text-ckeditor/tarball/master#egg=djangocms-text-ckeditor +https://github.com/django-cms/djangocms-versioning/tarball/support/django-cms-4.0.x#egg=djangocms-versioning https://github.com/FidelityInternational/djangocms-version-locking/tarball/master#egg=djangocms-version-locking https://github.com/django-cms/djangocms-moderation/tarball/master#egg=djangocms-moderation -https://github.com/FidelityInternational/djangocms-references/tarball/1.4.3#egg=djangocms-references -https://github.com/django-cms/djangocms-alias/tarball/1.11.0#egg=djangocms-alias +https://github.com/FidelityInternational/djangocms-references/tarball/master#egg=djangocms-references +https://github.com/django-cms/djangocms-alias/tarball/support/django-cms-4.0.x#egg=djangocms-alias diff --git a/tests/settings.py b/tests/settings.py index 39c6d0f69..b55fb8341 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -42,6 +42,10 @@ def run(): from app_helper import runner runner.cms("djangocms_navigation", extra_args=[]) + from cms.test_utils.testcases import CMSTestCase + from cms.utils.compat import DJANGO_4_1 + if DJANGO_4_1: + CMSTestCase.assertQuerySetEqual = CMSTestCase.assertQuerysetEqual if __name__ == "__main__": diff --git a/tests/test_admin.py b/tests/test_admin.py index 08480c429..8a0466c40 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -2,9 +2,9 @@ import importlib import json import sys +from unittest import skipIf, skipUnless from unittest.mock import patch -import django from django.contrib import admin from django.contrib.contenttypes.models import ContentType from django.contrib.messages import get_messages @@ -17,6 +17,7 @@ from cms.api import add_plugin, create_page, create_title from cms.test_utils.testcases import CMSTestCase from cms.toolbar.utils import get_object_preview_url +from cms.utils.compat import DJANGO_4_1 from bs4 import BeautifulSoup from djangocms_versioning.constants import DRAFT, PUBLISHED, UNPUBLISHED @@ -29,16 +30,13 @@ MenuItemAdmin, MenuItemChangeList, ) +from djangocms_navigation.compat import TREEBEARD_4_5 from djangocms_navigation.models import Menu, MenuContent, MenuItem from djangocms_navigation.test_utils import factories from .utils import UsefulAssertsMixin, disable_versioning_for_navigation -version = list(map(int, django.__version__.split('.'))) -GTE_DJ21 = version[0] >= 2 and version[1] >= 1 - - class MenuItemChangelistTestCase(CMSTestCase): def setUp(self): self.user = self.get_superuser() @@ -69,8 +67,9 @@ def _get_changelist_instance(self, menu_content): model_admin, # model_admin admin_field, # sortable_by ] - if not GTE_DJ21: - args.pop() + if not DJANGO_4_1: + search_help_text = model_admin.search_help_text + args.append(search_help_text) return MenuItemChangeList(*args) @@ -297,7 +296,7 @@ def test_get_queryset_filters_by_content_id(self): queryset = self.model_admin.get_queryset(request) - self.assertQuerysetEqual( + self.assertQuerySetEqual( queryset, [menu_contents[0].root.pk, child_item.pk], lambda o: o.pk ) @@ -333,7 +332,19 @@ def test_get_list_display(self): ['__str__', 'get_object_url', 'soft_root', 'hide_node', "list_actions"] ) - def test_get_changelist_template(self): + @skipIf(TREEBEARD_4_5, "Test relevant only for treebeard>=4.6") + def test_get_changelist_template_for_old_treebeard(self): + """ + Check the template is the standard change list template when the request is for the changelist endpoint + """ + request = self.get_request("/admin/djangocms_navigation/menuitem/1/") + + result = self.model_admin.get_changelist_template(request=request) + + self.assertEqual(result, "admin/djangocms_navigation/menuitem/tree_change_list.html") + + @skipUnless(TREEBEARD_4_5, "Test relevant only for treebeard<4.6") + def test_get_changelist_template_for_new_treebeard(self): """ Check the template is the standard change list template when the request is for the changelist endpoint """ diff --git a/tests/test_forms.py b/tests/test_forms.py index 54730ec0a..5d43b8284 100644 --- a/tests/test_forms.py +++ b/tests/test_forms.py @@ -3,6 +3,7 @@ from cms.models import Page from cms.test_utils.testcases import CMSTestCase +from cms.utils.compat import DJANGO_4_1 from cms.utils.urlutils import admin_reverse from djangocms_navigation.constants import SELECT2_CONTENT_OBJECT_URL_NAME @@ -16,6 +17,10 @@ from djangocms_navigation.test_utils.polls.models import PollContent +if DJANGO_4_1: + CMSTestCase.assertQuerySetEqual = CMSTestCase.assertQuerysetEqual + + class MenuContentFormTestCase(CMSTestCase): def setUp(self): self.menu_root = factories.RootMenuItemFactory() @@ -396,7 +401,7 @@ def test_only_display_supported_content_types(self): queryset = form.fields["content_type"].queryset expected_content_type_pks = [ct.pk for ct in content_types.values()] - self.assertQuerysetEqual( + self.assertQuerySetEqual( queryset, expected_content_type_pks, lambda o: o.pk, ordered=False ) diff --git a/tests/urls.py b/tests/urls.py index 97f32f83d..0c670d269 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,8 +1,8 @@ from django.conf import settings -from django.conf.urls import include, re_path from django.conf.urls.i18n import i18n_patterns from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns +from django.urls import include, re_path from django.views.static import serve diff --git a/tox.ini b/tox.ini index 77efdb657..bbf18d355 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ envlist = flake8 isort - py{37,38,39}-dj{22,32}-sqlite-cms40 + py{38,39,310}-dj{32,42}-sqlite-cms40 skip_missing_interpreters=True @@ -11,13 +11,13 @@ deps = flake8: -r{toxinidir}/tests/requirements/requirements_base.txt isort: -r{toxinidir}/tests/requirements/requirements_base.txt - dj22: -r{toxinidir}/tests/requirements/dj22_cms40.txt dj32: -r{toxinidir}/tests/requirements/dj32_cms40.txt + dj42: -r{toxinidir}/tests/requirements/dj42_cms40.txt basepython = - py37: python3.7 py38: python3.8 py39: python3.9 + py310: python3.10 commands = {envpython} --version @@ -27,8 +27,8 @@ commands = [testenv:flake8] commands = flake8 -basepython = python3.9 +basepython = python3.10 [testenv:isort] commands = isort --recursive --check-only --diff {toxinidir} -basepython = python3.9 +basepython = python3.10