From f726bc203c06ccbb8ba79dc9defb98d1b4e06a9d Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Fri, 29 Mar 2024 14:30:09 +0100 Subject: [PATCH 01/20] chore: bump version (#398) * chore: bump version * Fix md format in CHANGELOG.rst --- CHANGELOG.rst | 20 ++++++++++++++++++++ djangocms_versioning/__init__.py | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f7ab8665..f21c5790 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,26 @@ Changelog ========= +2.0.1 (2024-03-29) +================== + +* feat: Add content object level publish permissions by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/390 +* fix: Create missing __init__.py in management folder by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/366 +* fix #363: Better UX in versioning listview by @jrief in https://github.com/django-cms/djangocms-versioning/pull/364 +* fix: Several fixes for the versioning forms: #382, #383, #384 by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/386 +* fix: For Django CMS 4.1.1 and later do not automatically register versioned CMS Menu by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/388 +* fix: Post requests from the side frame were sent to wrong URL by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/396 +* fix: Consistent use of action buttons by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/392 +* fix: Avoid duplication of placeholder checks for locked versions by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/393 +* ci: Add testing against django main by @marksweb in https://github.com/django-cms/djangocms-versioning/pull/353 +* ci: Improve efficiency of ruff workflow by @marksweb in https://github.com/django-cms/djangocms-versioning/pull/378 +* Chore: update ruff and pre-commit hook by @raffaellasuardini in https://github.com/django-cms/djangocms-versioning/pull/381 +* build(deps): bump actions/cache from 4.0.1 to 4.0.2 by @dependabot in https://github.com/django-cms/djangocms-versioning/pull/397 + +New Contributors + +* @raffaellasuardini made their first contribution in https://github.com/django-cms/djangocms-versioning/pull/381 +* @jrief made their first contribution in https://github.com/django-cms/djangocms-versioning/pull/364 2.0.0 (2023-12-29) ================== diff --git a/djangocms_versioning/__init__.py b/djangocms_versioning/__init__.py index 8c0d5d5b..159d48b8 100644 --- a/djangocms_versioning/__init__.py +++ b/djangocms_versioning/__init__.py @@ -1 +1 @@ -__version__ = "2.0.0" +__version__ = "2.0.1" From 4c80aa52e5ebd2b9fbf33b83b093f4d31e1f873a Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Fri, 3 May 2024 16:05:53 +0200 Subject: [PATCH 02/20] fix: Do not show edit action for version objects where editing is not possible (#405) * Fix: Do not show edit action for version objects where editing is not possible * fix ruff errors * Fix tests * Fix linting --- .github/workflows/lint.yml | 2 +- djangocms_versioning/admin.py | 8 ++++++-- tests/test_admin.py | 12 ++++++------ tests/test_extensions.py | 6 +++--- tests/test_handlers.py | 9 ++++++++- tests/test_locking.py | 4 ++-- tests/test_toolbars.py | 14 ++++++++++++++ 7 files changed, 40 insertions(+), 15 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index da24e88e..8878fd89 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,4 +14,4 @@ jobs: - run: python -Im pip install --user ruff - name: Run ruff - run: ruff --output-format=github djangocms_versioning tests + run: ruff check --output-format=github djangocms_versioning tests diff --git a/djangocms_versioning/admin.py b/djangocms_versioning/admin.py index 00898e44..0641bcb3 100644 --- a/djangocms_versioning/admin.py +++ b/djangocms_versioning/admin.py @@ -491,7 +491,7 @@ def get_actions_list(self): actions = [ self._get_preview_link, self._get_edit_link, - ] + ] if "state_indicator" not in self.versioning_list_display: # State indicator mixin loaded? actions.append(self._get_manage_versions_link) @@ -729,6 +729,10 @@ def _get_unpublish_link(self, obj, request, disabled=False): def _get_edit_link(self, obj, request, disabled=False): """Helper function to get the html link to the edit action """ + + if not obj.check_edit_redirect.as_bool(request.user): + return "" + # Only show if no draft exists if obj.state == PUBLISHED: pks_for_grouper = obj.versionable.for_content_grouping_values( @@ -758,7 +762,7 @@ def _get_edit_link(self, obj, request, disabled=False): title=_("Edit") if icon == "pencil" else _("New Draft"), name="edit", action="post", - disabled=not obj.check_edit_redirect.as_bool(request.user) or disabled, + disabled=disabled, keepsideframe=keepsideframe, ) diff --git a/tests/test_admin.py b/tests/test_admin.py index cb59fc33..56560c9e 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -528,9 +528,9 @@ def test_revert_action_link_enable_state(self): 'cms-action-revert ' 'js-action ' 'js-keep-sideframe" ' - 'href="%s" ' + f'href="{draft_revert_url}" ' 'title="Revert">' - ) % draft_revert_url + ) self.assertIn(expected_enabled_state, actual_enabled_control.replace("\n", "")) def test_revert_action_link_for_draft_state(self): @@ -599,9 +599,9 @@ def test_discard_action_link_enabled_state(self): 'cms-action-discard ' 'js-action ' 'js-keep-sideframe" ' - 'href="%s" ' + f'href="{draft_discard_url}" ' 'title="Discard">' - ) % draft_discard_url + ) self.assertIn(expected_enabled_state, actual_enabled_control.replace("\n", "")) def test_discard_action_link_for_archive_state(self): @@ -664,11 +664,11 @@ def test_revert_action_link_for_archive_state(self): 'cms-action-revert ' 'js-action ' 'js-keep-sideframe" ' - 'href="%s" ' + f'href="{draft_revert_url}" ' 'title="Revert">' '' '' - ) % draft_revert_url + ) self.assertIn( expected_disabled_control, actual_disabled_control.replace("\n", "") diff --git a/tests/test_extensions.py b/tests/test_extensions.py index bcb6c35e..9d1b1a01 100644 --- a/tests/test_extensions.py +++ b/tests/test_extensions.py @@ -117,7 +117,7 @@ def test_title_extension_admin_monkey_patch_save(self): poll_extension = PollTitleExtensionFactory(extended_object=self.version.content) model_site = PollExtensionAdmin(admin_site=admin.AdminSite(), model=PollPageContentExtension) test_url = admin_reverse("extended_polls_pollpagecontentextension_change", args=(poll_extension.pk,)) - test_url += "?extended_object=%s" % self.version.content.pk + test_url += f"?extended_object={self.version.content.pk}" request = RequestFactory().post(path=test_url) request.user = self.get_superuser() @@ -137,7 +137,7 @@ def test_title_extension_admin_monkey_patch_save_date_modified_updated(self): model_site = PollExtensionAdmin(admin_site=admin.AdminSite(), model=PollPageContentExtension) pre_changes_date_modified = Version.objects.get(id=self.version.pk).modified test_url = admin_reverse("extended_polls_pollpagecontentextension_change", args=(poll_extension.pk,)) - test_url += "?extended_object=%s" % self.version.content.pk + test_url += f"?extended_object={self.version.content.pk}" request = RequestFactory().post(path=test_url) request.user = self.get_superuser() @@ -155,7 +155,7 @@ def test_title_extension_admin_monkeypatch_add_view(self): with self.login_user_context(self.get_superuser()): response = self.client.get( admin_reverse("extended_polls_pollpagecontentextension_add") + - "?extended_object=%s" % self.version.content.pk, + f"?extended_object={self.version.content.pk}", follow=True ) self.assertEqual(response.status_code, 200) diff --git a/tests/test_handlers.py b/tests/test_handlers.py index c9cf0a90..06d426b3 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -21,6 +21,7 @@ def test_modified_date(self): def test_add_plugin(self): version = factories.PageVersionFactory() placeholder = factories.PlaceholderFactory(source=version.content) + placeholder.page.get_absolute_url = lambda *args, **kwargs: "/test_page/" # Fake URL needed for URI poll = factories.PollFactory() dt = datetime(2016, 6, 6) with freeze_time(dt): @@ -45,6 +46,7 @@ def test_change_plugin(self): plugin = add_plugin( placeholder, "PollPlugin", version.content.language, poll=poll ) + plugin.page.get_absolute_url = lambda *args, **kwargs: "/test_page/" # Fake URL needed for URI dt = datetime(2016, 6, 6) with freeze_time(dt): @@ -61,6 +63,7 @@ def test_change_plugin(self): def test_clear_placeholder(self): version = factories.PageVersionFactory() placeholder = factories.PlaceholderFactory(source=version.content) + placeholder.page.get_absolute_url = lambda *args, **kwargs: "/test_page/" # Fake URL needed for URI dt = datetime(2016, 6, 6) with freeze_time(dt): @@ -81,6 +84,7 @@ def test_delete_plugin(self): plugin = add_plugin( placeholder, "PollPlugin", version.content.language, poll=poll ) + plugin.page.get_absolute_url = lambda *args, **kwargs: "/test_page/" # Fake URL needed for URI dt = datetime(2016, 6, 6) with freeze_time(dt): @@ -103,6 +107,7 @@ def test_add_plugins_from_placeholder(self): plugin = add_plugin( source_placeholder, "PollPlugin", version.content.language, poll=poll ) + plugin.page.get_absolute_url = lambda *args, **kwargs: "/test_page/" # Fake URL needed for URI dt = datetime(2016, 6, 6) with freeze_time(dt): @@ -165,7 +170,7 @@ def test_paste_plugin(self): plugin = add_plugin( source_placeholder, "PollPlugin", version.content.language, poll=poll ) - + plugin.page.get_absolute_url = lambda *args, **kwargs: "/test_page/" dt = datetime(2016, 6, 6) with freeze_time(dt): endpoint = self.get_move_plugin_uri(plugin) @@ -197,6 +202,7 @@ def test_cut_plugin(self): plugin = add_plugin( placeholder, "PollPlugin", version.content.language, poll=poll ) + plugin.page.get_absolute_url = lambda *args, **kwargs: "/test_page/" # Fake URL needed for URI dt = datetime(2016, 6, 6) with freeze_time(dt): @@ -223,6 +229,7 @@ def test_move_plugin(self): plugin = add_plugin( source_placeholder, "PollPlugin", version.content.language, poll=poll ) + plugin.page.get_absolute_url = lambda *args, **kwargs: "/test_page/" # Fake URL needed for URI dt = datetime(2016, 6, 6) with freeze_time(dt): diff --git a/tests/test_locking.py b/tests/test_locking.py index bbc72d94..7b016778 100644 --- a/tests/test_locking.py +++ b/tests/test_locking.py @@ -394,12 +394,12 @@ def test_edit_action_link_disabled_state(self): author_request.user = self.user_author otheruser_request = RequestFactory() otheruser_request.user = self.superuser - expected_disabled_state = "inactive" + expected_disabled_state = "" actual_disabled_state = self.version_admin._get_edit_link(version, otheruser_request) self.assertFalse(version.check_edit_redirect.as_bool(self.superuser)) - self.assertIn(expected_disabled_state, actual_disabled_state) + self.assertEqual(expected_disabled_state, actual_disabled_state) @override_settings(DJANGOCMS_VERSIONING_LOCK_VERSIONS=True) diff --git a/tests/test_toolbars.py b/tests/test_toolbars.py index deb1f038..66339249 100644 --- a/tests/test_toolbars.py +++ b/tests/test_toolbars.py @@ -341,6 +341,13 @@ def test_view_published_in_toolbar_in_edit_mode_for_published_page(self): are published """ published_version = PageVersionFactory(content__language="en", state=PUBLISHED) + # Create URL + PageUrlFactory( + page=published_version.content.page, + language=published_version.content.language, + path=slugify("test_page"), + slug=slugify("test_page"), + ) toolbar = get_toolbar(published_version.content, edit_mode=True) toolbar.post_template_populate() @@ -353,6 +360,13 @@ def test_view_published_in_toolbar_in_preview_mode_for_published_page(self): are published """ published_version = PageVersionFactory(content__language="en", state=PUBLISHED) + # Create URL + PageUrlFactory( + page=published_version.content.page, + language=published_version.content.language, + path=slugify("test_page"), + slug=slugify("test_page"), + ) toolbar = get_toolbar(published_version.content, preview_mode=True) toolbar.post_template_populate() From ec3e5b9e41ca937039ad1dc9a6fb90af1c93b2a1 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Fri, 3 May 2024 22:00:57 +0200 Subject: [PATCH 03/20] fix: Avoid unnecessary loading of `actions.js` asset into the toolbar (#403) * fix: as of django-cms 4.1.1, the `actions.js` asset needs not be loaded for the toolbar * Fix test --- djangocms_versioning/cms_toolbars.py | 10 +++------- setup.py | 4 ++-- tests/test_locking.py | 2 +- tests/test_toolbars.py | 4 ++-- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/djangocms_versioning/cms_toolbars.py b/djangocms_versioning/cms_toolbars.py index 153f43ba..62a5231a 100644 --- a/djangocms_versioning/cms_toolbars.py +++ b/djangocms_versioning/cms_toolbars.py @@ -35,9 +35,6 @@ class VersioningToolbar(PlaceholderToolbar): - class Media: - js = ("cms/js/admin/actions.js",) - def _get_versionable(self): """Helper method to get the versionable for the content type of the version @@ -79,7 +76,7 @@ def _add_publish_button(self): _("Publish"), url=publish_url, disabled=False, - extra_classes=["cms-btn-action", "js-action", "cms-form-post-method", "cms-versioning-js-publish-btn"], + extra_classes=["cms-btn-action", "cms-form-post-method", "cms-versioning-js-publish-btn"], ) self.toolbar.add_item(item) @@ -115,7 +112,7 @@ def _add_edit_button(self, disabled=False): _("Edit") if draft_exists else _("New Draft"), url=edit_url, disabled=disabled, - extra_classes=["cms-btn-action", "js-action", "cms-form-post-method", "cms-versioning-js-edit-btn"], + extra_classes=["cms-btn-action", "cms-form-post-method", "cms-versioning-js-edit-btn"], ) self.toolbar.add_item(item) @@ -135,7 +132,6 @@ def _add_unlock_button(self): if can_unlock: extra_classes = [ "cms-btn-action", - "js-action", "cms-form-post-method", "cms-versioning-js-unlock-btn", ] @@ -316,7 +312,7 @@ def override_language_menu(self): # Only override the menu if it exists and a page can be found language_menu = self.toolbar.get_menu(LANGUAGE_MENU_IDENTIFIER, _("Language")) if settings.USE_I18N and language_menu and self.page: - # remove_item uses `items` attribute so we have to copy object + # remove_item uses `items` attribute, so we have to copy object for _item in copy(language_menu.items): language_menu.remove_item(item=_item) diff --git a/setup.py b/setup.py index 9ddc378b..a1f934f3 100644 --- a/setup.py +++ b/setup.py @@ -3,8 +3,8 @@ import djangocms_versioning INSTALL_REQUIREMENTS = [ - "Django>=1.11", - "django-cms", + "Django>=3.2", + "django-cms>=4.1.1", "django-fsm" ] diff --git a/tests/test_locking.py b/tests/test_locking.py index 7b016778..f8199f57 100644 --- a/tests/test_locking.py +++ b/tests/test_locking.py @@ -839,7 +839,7 @@ def test_enable_edit_button_when_content_is_locked(self): self.assertFalse(edit_button.disabled) self.assertListEqual( edit_button.extra_classes, - ["cms-btn-action", "js-action", "cms-form-post-method", "cms-versioning-js-edit-btn"] + ["cms-btn-action", "cms-form-post-method", "cms-versioning-js-edit-btn"] ) def test_lock_message_when_content_is_locked(self): diff --git a/tests/test_toolbars.py b/tests/test_toolbars.py index 66339249..1c51d838 100644 --- a/tests/test_toolbars.py +++ b/tests/test_toolbars.py @@ -65,7 +65,7 @@ def test_publish_in_toolbar_in_edit_mode(self): self.assertFalse(publish_button.disabled) self.assertListEqual( publish_button.extra_classes, - ["cms-btn-action", "js-action", "cms-form-post-method", "cms-versioning-js-publish-btn"], + ["cms-btn-action", "cms-form-post-method", "cms-versioning-js-publish-btn"], ) def test_revert_in_toolbar_in_preview_mode(self): @@ -150,7 +150,7 @@ def test_edit_in_toolbar_in_preview_mode(self): self.assertFalse(edit_button.disabled) self.assertListEqual( edit_button.extra_classes, - ["cms-btn-action", "js-action", "cms-form-post-method", "cms-versioning-js-edit-btn"] + ["cms-btn-action", "cms-form-post-method", "cms-versioning-js-edit-btn"] ) def test_edit_not_in_toolbar_in_edit_mode(self): From 5b07387b31bce7dcbe41863aeeb6ea2c3849ec33 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Sat, 4 May 2024 04:47:16 +0200 Subject: [PATCH 04/20] Translate django.po in ar (#407) 100% translated source file: 'django.po' on 'ar'. Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com> --- .../locale/ar/LC_MESSAGES/django.po | 502 ++++++++++++++++++ 1 file changed, 502 insertions(+) create mode 100644 djangocms_versioning/locale/ar/LC_MESSAGES/django.po diff --git a/djangocms_versioning/locale/ar/LC_MESSAGES/django.po b/djangocms_versioning/locale/ar/LC_MESSAGES/django.po new file mode 100644 index 00000000..7de35d10 --- /dev/null +++ b/djangocms_versioning/locale/ar/LC_MESSAGES/django.po @@ -0,0 +1,502 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +# Translators: +# Seraj Adden Baltu, 2024 +# Fabian Braun , 2024 +# Mohammad Alsakhawy, 2024 +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-10-02 09:37+0200\n" +"PO-Revision-Date: 2023-01-10 15:29+0000\n" +"Last-Translator: Mohammad Alsakhawy, 2024\n" +"Language-Team: Arabic (https://app.transifex.com/divio/teams/58664/ar/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: ar\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" + +#: admin.py:164 admin.py:301 admin.py:377 +msgid "State" +msgstr "الحالة" + +#: admin.py:192 constants.py:27 +msgid "Empty" +msgstr "فارغ" + +#: admin.py:315 admin.py:387 +msgid "Author" +msgstr "المؤلف" + +#: admin.py:329 admin.py:401 models.py:87 +msgid "Modified" +msgstr "تعديل" + +#: admin.py:437 admin.py:667 +#: templates/djangocms_versioning/admin/icons/preview.html:3 +#: templates/djangocms_versioning/admin/preview.html:3 +msgid "Preview" +msgstr "معاينة" + +#: admin.py:470 admin.py:758 cms_toolbars.py:115 +#: templates/djangocms_versioning/admin/icons/edit_icon.html:3 +msgid "Edit" +msgstr "تحرير" + +#: admin.py:482 +#: templates/djangocms_versioning/admin/icons/manage_versions.html:3 +msgid "Manage versions" +msgstr "إدارة الإصدارات " + +#: admin.py:631 +msgid "Content" +msgstr "المحتوى" + +#: admin.py:647 +msgid "locked" +msgstr "مقفول" + +#: admin.py:683 templates/djangocms_versioning/admin/icons/archive_icon.html:3 +msgid "Archive" +msgstr "أرشيف" + +#: admin.py:701 cms_toolbars.py:79 indicators.py:34 +#: templates/djangocms_versioning/admin/icons/publish_icon.html:3 +msgid "Publish" +msgstr "نشر" + +#: admin.py:721 indicators.py:54 indicators.py:60 +#: templates/djangocms_versioning/admin/icons/unpublish_icon.html:3 +msgid "Unpublish" +msgstr "الغاء النشر " + +#: admin.py:758 cms_toolbars.py:115 +msgid "New Draft" +msgstr "مسودة جديدة " + +#: admin.py:779 cms_toolbars.py:177 +#: templates/djangocms_versioning/admin/icons/revert_icon.html:3 +msgid "Revert" +msgstr "استرجاع" + +#: admin.py:798 templates/djangocms_versioning/admin/icons/discard_icon.html:3 +msgid "Discard" +msgstr "تجاهل" + +#: admin.py:821 cms_toolbars.py:145 +msgid "Unlock" +msgstr "إلغاء القفل " + +#: admin.py:856 +msgid "Compare versions" +msgstr "مقارنة الإصدارات " + +#: admin.py:866 +msgid "Exactly two versions need to be selected." +msgstr "يجب تحديد إثنين من الإصدارات بالضبط" + +#: admin.py:903 +msgid "Version cannot be archived" +msgstr "لا يمكن أرشفة الإصدار " + +#: admin.py:929 +msgid "Version archived" +msgstr "تمت أرشفة الإصدار " + +#: admin.py:940 admin.py:1059 admin.py:1235 +msgid "This view only supports POST method." +msgstr "هذا العرض يدعم فقط طريقة POST" + +#: admin.py:951 +msgid "Version cannot be published" +msgstr "لا يمكن نشر الإصدار " + +#: admin.py:962 +msgid "Version published" +msgstr "تم نشر الإصدار " + +#: admin.py:979 +msgid "Version cannot be unpublished" +msgstr "لا يمكن إلغاء نشر الإصدار" + +#: admin.py:1017 +msgid "Version unpublished" +msgstr "تم إلغاء نشر الإصدار" + +#: admin.py:1163 +msgid "The last version has been deleted" +msgstr "تم حذف الإصدار السابق" + +#: admin.py:1249 +msgid "You do not have permission to remove the version lock" +msgstr "ليس لديك صلاحيات لحذف قفل الإصدار" + +#: admin.py:1254 +msgid "Version unlocked" +msgstr "تم إلغاء قفل الإصدار" + +#: admin.py:1303 +#, python-brace-format +msgid "Displaying versions of \"{grouper}\"" +msgstr "عرض إصدارات \"{grouper}\"" + +#: apps.py:8 +msgid "django CMS Versioning" +msgstr "إصدارات ن.إ.م. چانجو" + +#: cms_config.py:246 +msgid "No available title" +msgstr "بدون عنوان" + +#: cms_config.py:248 constants.py:12 constants.py:25 +msgid "Unpublished" +msgstr "غير منشور" + +#: cms_config.py:342 +msgid "Language must be set to a supported language!" +msgstr "يجب تحديد لغة ضمن اللغات المدعومة!" + +#: cms_config.py:360 +msgid "You do not have permission to copy these plugins." +msgstr "ليس لديك صلاحيات لنسخ هذه الملحقات." + +#: cms_toolbars.py:207 +msgid "Manage Versions" +msgstr "إدارة الإصدارات" + +#: cms_toolbars.py:210 +#, python-brace-format +msgid "Compare to {source}" +msgstr "قارن ب {source}" + +#: cms_toolbars.py:226 indicators.py:66 +msgid "Discard Changes" +msgstr " تجاهل التغييرات" + +#: cms_toolbars.py:262 +msgid "View Published" +msgstr "عرض المنشور" + +#: cms_toolbars.py:317 +msgid "Language" +msgstr "اللغة" + +#: cms_toolbars.py:364 +msgid "Add Translation" +msgstr "إضافة ترجمة" + +#: cms_toolbars.py:377 +msgid "Copy all plugins" +msgstr "نسخ كل الملحقات" + +#: cms_toolbars.py:379 +#, python-format +msgid "from %s" +msgstr "من %s" + +#: cms_toolbars.py:380 +#, python-format +msgid "Are you sure you want to copy all plugins from %s?" +msgstr "هل أنت متأكد من أنك تريد نسخ كل الملحقات من %s؟" + +#: cms_toolbars.py:395 +msgid "No other language available" +msgstr "لا توجد لغة أخرى متوفرة" + +#: constants.py:10 constants.py:24 +msgid "Draft" +msgstr "مسودة" + +#: constants.py:11 constants.py:22 +msgid "Published" +msgstr "منشور" + +#: constants.py:13 constants.py:26 +msgid "Archived" +msgstr "مُؤرشَف" + +#: constants.py:23 +msgid "Changed" +msgstr "مُعدّل" + +#: emails.py:39 +msgid "Unlocked" +msgstr "أُلغيَ القفل" + +#: indicators.py:28 +#, python-format +msgid "Unlock (%(message)s)" +msgstr "إلغاء قفل (%(message)s)" + +#: indicators.py:40 +msgid "Create new draft" +msgstr "إنشاء مسودة جديدة" + +#: indicators.py:46 +msgid "Revert from Unpublish" +msgstr "تراجع عن إلغاء النشر" + +#: indicators.py:66 +msgid "Delete Draft" +msgstr "حذف المسودة" + +#: indicators.py:72 +msgid "Compare Draft to Published..." +msgstr "قارن المسودة بالمنشور..." + +#: indicators.py:82 +msgid "Manage Versions..." +msgstr "إدارة الإصدارات..." + +#: models.py:29 +msgid "Version is not a draft" +msgstr "الإصدار ليس مسودة" + +#: models.py:30 +#, python-brace-format +msgid "Action Denied. The latest version is locked by {user}" +msgstr "تم رفض الإجراء. أحدث إصدار مقفول بواسطة {user}" + +#: models.py:31 +#, python-brace-format +msgid "Action Denied. The draft version is locked by {user}" +msgstr "تم رفض الإجراء. إصدار المسودة مقفول بواسطة {user}" + +#: models.py:86 +msgid "Created" +msgstr "أُنشئ" + +#: models.py:89 +msgid "author" +msgstr "المؤلف" + +#: models.py:102 +msgid "status" +msgstr "الحالة" + +#: models.py:110 +msgid "locked by" +msgstr "مقفول بواسطة" + +#: models.py:119 +msgid "source" +msgstr "المصدر" + +#: models.py:133 +#, python-brace-format +msgid "Version #{number} ({state} {date})" +msgstr "الإصدار #{number} ({state} {date})" + +#: models.py:140 +#, python-brace-format +msgid "Version #{number} ({state})" +msgstr "الإصدار #{number} ({state})" + +#: models.py:146 +#, python-format +msgid "Locked by %(user)s" +msgstr "مقفول بواسطة %(user)s" + +#: models.py:278 models.py:327 +msgid "Version is not in draft state" +msgstr "الإصدار ليس في حالة مسودة" + +#: models.py:387 +msgid "Version is not in published state" +msgstr "الإصدار ليس في حالة منشور" + +#: models.py:444 +msgid "Version is not in archived or unpublished state" +msgstr "الإصدار ليس في حالة أرشفة أو حالة غير منشور" + +#: models.py:459 +msgid "Version is not in draft or published state" +msgstr "الإصدار ليس في حالة مسودة أو حالة منشور" + +#: models.py:467 +msgid "Version is already locked" +msgstr "تم قفل الإصدار بالفعل" + +#: models.py:473 +msgid "Draft version is not locked" +msgstr "إصدار المسودة غير مقفول" + +#: templates/admin/djangocms_versioning/versioning_breadcrumbs.html:3 +#: templates/djangocms_versioning/admin/grouper_form.html:9 +msgid "Home" +msgstr "الرئيسية" + +#: templates/admin/djangocms_versioning/versioning_breadcrumbs.html:7 +#: templates/djangocms_versioning/admin/mixin/change_form.html:7 +msgid "Versions" +msgstr "الإصدارات" + +#: templates/djangocms_versioning/admin/archive_confirmation.html:3 +msgid "Archive Confirmation" +msgstr "تأكيد الأرشفة" + +#: templates/djangocms_versioning/admin/archive_confirmation.html:15 +msgid "Are you sure you want to archive the following version?" +msgstr "هل أنت متأكد أنك تريد أرشفة الإصدار التالي؟" + +#: templates/djangocms_versioning/admin/archive_confirmation.html:17 +#: templates/djangocms_versioning/admin/unpublish_confirmation.html:17 +#, python-format +msgid " Version number: %(version_number)s" +msgstr "إصدار رقم: %(version_number)s" + +#: templates/djangocms_versioning/admin/archive_confirmation.html:22 +#: templates/djangocms_versioning/admin/discard_confirmation.html:23 +#: templates/djangocms_versioning/admin/revert_confirmation.html:40 +#: templates/djangocms_versioning/admin/unpublish_confirmation.html:27 +msgid "Yes, I'm sure" +msgstr "نعم، أنا متأكد" + +#: templates/djangocms_versioning/admin/archive_confirmation.html:26 +#: templates/djangocms_versioning/admin/discard_confirmation.html:27 +#: templates/djangocms_versioning/admin/revert_confirmation.html:45 +#: templates/djangocms_versioning/admin/unpublish_confirmation.html:31 +msgid "No, take me back" +msgstr "لا، تراجع للخلف" + +#: templates/djangocms_versioning/admin/compare.html:8 +#, python-format +msgid "" +"\n" +" Compare %(left)s to %(right)s\n" +" " +msgstr "" +"\n" +"قارن %(left)s بـ %(right)s" + +#: templates/djangocms_versioning/admin/compare.html:12 +#, python-format +msgid "" +"\n" +" Compare %(left)s\n" +" " +msgstr "" +"\n" +"قارن %(left)s" + +#: templates/djangocms_versioning/admin/compare.html:16 +#, python-format +msgid "" +"\n" +" Compare %(right)s\n" +" " +msgstr "" +"\n" +"قارن %(right)s" + +#: templates/djangocms_versioning/admin/compare.html:37 +msgid "Back" +msgstr "رجوع" + +#: templates/djangocms_versioning/admin/compare.html:40 +#, python-format +msgid "" +"\n" +" Comparing %(left)s with\n" +" " +msgstr "" +"\n" +"مقارنة %(left)s بـ" + +#: templates/djangocms_versioning/admin/compare.html:45 +msgid "Pick a version to compare to" +msgstr "اختر إصدار للمقارنة" + +#: templates/djangocms_versioning/admin/compare.html:56 +msgid "Visual" +msgstr "مرئي" + +#: templates/djangocms_versioning/admin/compare.html:59 +msgid "Source" +msgstr "مصدر" + +#: templates/djangocms_versioning/admin/discard_confirmation.html:3 +msgid "Discard Confirmation" +msgstr "تأكيد التجاهل" + +#: templates/djangocms_versioning/admin/discard_confirmation.html:15 +msgid "Are you sure you want to discard following version?" +msgstr "هل أنت متأكد أنك تريد تجاهل الإصدار التالي؟" + +#: templates/djangocms_versioning/admin/discard_confirmation.html:17 +#: templates/djangocms_versioning/admin/revert_confirmation.html:24 +#, python-format +msgid "Version number: %(version_number)s" +msgstr "إصدار رقم: %(version_number)s" + +#: templates/djangocms_versioning/admin/grouper_form.html:27 +#, python-format +msgid "Add %(name)s" +msgstr "إضافة %(name)s" + +#: templates/djangocms_versioning/admin/grouper_form.html:37 +msgid "Submit" +msgstr "تأكيد" + +#: templates/djangocms_versioning/admin/icons/view.html:3 +msgid "View on site" +msgstr "عرض على الموقع" + +#: templates/djangocms_versioning/admin/revert_confirmation.html:3 +#: templates/djangocms_versioning/admin/unpublish_confirmation.html:3 +msgid "Revert Confirmation" +msgstr "تأكيد الاسترجاع" + +#: templates/djangocms_versioning/admin/revert_confirmation.html:18 +msgid "" +"Reverting to this version may cause loss of an existing draft version. " +"Please select an option to continue" +msgstr "" +"الاسترجاع لهذا الإصدار قد يتسبب في خسارة إصدار مسودة متواجدة. يرجى تحديد " +"اختيار للإستمرار" + +#: templates/djangocms_versioning/admin/revert_confirmation.html:20 +msgid "Are you sure you want to revert to the following version?" +msgstr "هل أنت متأكد أنك تريد استرجاع الإصدار التالي؟" + +#: templates/djangocms_versioning/admin/revert_confirmation.html:31 +msgid "Discard existing draft and Revert" +msgstr "تجاهل المسودة المتواجدة واسترجع" + +#: templates/djangocms_versioning/admin/revert_confirmation.html:35 +msgid "Archive existing draft and Revert" +msgstr "أرشف المسودة المتواجدة واسترجع" + +#: templates/djangocms_versioning/admin/unpublish_confirmation.html:15 +msgid "" +"Unpublishing will remove this version from live. Are you sure you want to " +"unpublish?" +msgstr "" +"إلغاء النشر سيؤدي إلى حذف هذا الإصدار من مباشر الموقع. هل أنت متأكد أنك تريد" +" إلغاء النشر؟" + +#: templates/djangocms_versioning/emails/unlock-notification.txt:2 +#, python-format +msgid "" +"\n" +"The following draft version has been unlocked by %(by_user)s for their use.\n" +"%(version_link)s\n" +"\n" +"Please note you will not be able to further edit this draft. Kindly reach out to %(by_user)s in case of any concerns.\n" +"\n" +"This is an automated notification from Django CMS.\n" +msgstr "" +"\n" +"تم إلغاء قفل المسودة التالية بواسطة %(by_user)s لاستخدامهم.\n" +"%(version_link)s\n" +"\n" +"برجاء العلم أنك لن تستطيع إجراء المزيد من التعديلات على هذه المسودة. يرجى التواصل مع %(by_user)s في حالة وجود أي استفسارات.\n" +"\n" +"هذا إشعار تلقائي من ن.إ.م. چانجو.\n" From 7860a6d4a79068899defa42f227ed9b25f517494 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Sat, 4 May 2024 05:26:43 +0200 Subject: [PATCH 05/20] feat: Update locales (#406) * feat: Update locales * Update mo files --- CHANGELOG.rst | 6 ++ djangocms_versioning/__init__.py | 2 +- .../locale/ar/LC_MESSAGES/django.mo | Bin 0 -> 9074 bytes .../locale/de/LC_MESSAGES/django.mo | Bin 8257 -> 8270 bytes .../locale/de/LC_MESSAGES/django.po | 12 ++-- .../locale/fr/LC_MESSAGES/django.mo | Bin 6571 -> 8298 bytes .../locale/fr/LC_MESSAGES/django.po | 15 ++--- .../locale/nl/LC_MESSAGES/django.mo | Bin 8121 -> 8121 bytes .../locale/nl/LC_MESSAGES/django.po | 15 ++--- .../locale/sq/LC_MESSAGES/django.mo | Bin 6454 -> 6454 bytes .../locale/sq/LC_MESSAGES/django.po | 53 +++++------------- 11 files changed, 38 insertions(+), 65 deletions(-) create mode 100644 djangocms_versioning/locale/ar/LC_MESSAGES/django.mo diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f21c5790..1b54acd2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,12 @@ Changelog ========= +2.0.2 (2024-05-03) +================== + +* fix: Do not show edit action for version objects where editing is not possible by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/405 +* feat: Add Arabic locale + 2.0.1 (2024-03-29) ================== diff --git a/djangocms_versioning/__init__.py b/djangocms_versioning/__init__.py index 159d48b8..0309ae29 100644 --- a/djangocms_versioning/__init__.py +++ b/djangocms_versioning/__init__.py @@ -1 +1 @@ -__version__ = "2.0.1" +__version__ = "2.0.2" diff --git a/djangocms_versioning/locale/ar/LC_MESSAGES/django.mo b/djangocms_versioning/locale/ar/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..15abc8cde9a88dc3bf492c31f4c2932c9fca2bb5 GIT binary patch literal 9074 zcmb7{du$v>9ml7%rPM%a3kAw^`fzL0-r3HhBo1zp#!d;%!z6BK3#F{@*7i1cyXN*T zsYwG(8YgZ91dsqBBv6&Kaoi@Un?7(r6%qo0S{3+j7o-9L{y;)hs>FZ1KEK)7y*<5>D z4};6WGvI3Q&!C9*VK%)4yb+ZBI>8TvJ>a{+ZQzw)$YwLGTt(RiN;@5fr`Ff#R=EgQCB0aW^RIz6^?;ra-aBQSch@82BJK zXXkH32+`*jQ0RL=(RUSiBY2-3{|YGfI0TA(-v@=yaS)fN)8G(z4ir1R3n4|`WuVBr zmA?;w57_Z9f#R?4fY*Z0f$s%h1-rn%f*%8~xhkPs2Z~&u2W9_9Kz`~Ne~_x@LDBOU zpvd<-5Ea#*L0qTa07bw5f|ypl6({t7?*S!FH-a+%Fer9<6coFD&yGJ1ioVmf|E$G1 z@Z3d8{S*{^_hKYo)ptRWcLo%>UIC||`y<#+-$U6U@F=(y{0k^_%iovqzY`=y)cv5? zXAG41KLCneKLNLaFN52`557O?```}xFM*=>)hrf!d=!*^2Y3s(5=0c`fEwHl%D&$P zC9a;d<39sMj=z9nm)C9oQk?k_`d5MssV;C0xCs=!CqY?v0u+AF+Wu>x$n!UFJ@^JF z{#$|Zgnj_L9UKHTcodX4dB*l%1%>bHpwM4|uo5@df?~gJ@MB;<$WI0QNgRI_6#ZvG z@$*Zdtp7PEdjAF#K7RnkKmP`ggEufJ^8ON(^}hvW-D}{@;NL)rqpJ`~k@-DbcJ@D)(x{wG)jKcEwSn?Qc*i~MZ`zXje2 z{s|PiPBsy|2NXRHgP2O4vi)CKd>xcHy$0su*A5U@sM|r&_g>o{2G`Nw2ljz4fntY$ zfl}`;=TGva0E%5!f};02Q0VTl=zM_NNQVc} z&G{(r_tJW4_tDnTG)?Nl1GF+NeWb>GiYB%cKgc8g-$}!z=J`1PpSADCclKTKQgR9x zWsmrJ3+-0g08L_jBkl7vd6v;Oo3|u>2wxMIGIRxP1MLg6Pt(NDJ81F{z9v2-25y!C z9?1`fCh^fvqoik#)SSC%1)9{ai)okAN;IiM;&XZKqDeg5PP>Vge^%u>_wY_CnCCO{ z-=-IMHU!mir{?*iy1k>~j?_Cty~nGMok!bTovd&(`iyjO5?v181C7wFp~^^5)Acde zt7!&`ZS5U)TS5E{DbgxvT$F|xTuDBxkNTbFcHSLzYI=gvc zB1KW(<@x2xgs!>N zyc%leCbj#J7uK6jb^Nm4?xIpXT~_8LaZA{+FMAx{nAu4fh;wt%VePSY@pYwOd_p_; zc)ZdW^?V{k?A#tMv83#UC8t*Yzu44_9ug0W3D~HQ1vPbtQ`)6AjNxUstkT5lH!;rI z!NzdK3&-$ev6#&=zBddSwUWCp8?$stp7wA?~|-l7O%SW!3LiTqX=p z)t{)EqD~|WoBg~SE%6d=gkrqnOo)q92_+Y<-#c0h8smiC^~&;X$!elaGBAl#H9*#> zf$DgDLJd6RlJ?Wp6qwqQvTsr_cdM>DyIfs$^{~XrpjX4hRUa2`>pc1jEmrd2xWa(0Iv(Y(_O8%{;t>xQ@LO*dDG(weL8<%&@bOnDn~ z2q=nR)eFsC#-s&Rn{q&Y+2g5rffZR?x>;F}vZ=UK$njFn?ho3kN<=GrTG&)B*u}kiaQ%k6*55gxcMfddv1!}Zw(YKz&%%~))GHV6XpDx1 zp`cf{4Q?w?T0GIK&`)gl>Sf)_mKVBv3O(JrXJzlQ)wgu>v~4EM3+cTf;7YS47;~ys zr>xgkLTA^QvuEN~S-Jwsq?Q+kT&LPg!a2iUNq3CZ>*HZ>SC=zBUaZUg!y9oQDpD4^ z%HD1-=&C~+ifeloLX0B+m=mR2Ab;h6uaB(ky#j;7)Q>!`%t}$L(B8m=+c-| z3*CC(-9sA-tD1DeX2h)(2K*8=-XHDNtA@RLCSflbz_qebb1H?6L9H70Y9DXX4f}3e zBaY>@&+qH&)@yaQX11@dhu5Cubs4YAcQb$k3 z2cvWFdR)f`;{EYK9i4~|#*@+6cq%#rcj-nmX7Gq~rx`yHosC{Jk~J^OP|5I2bed%+ zP~dPpxzLS0nvQ0prwv21^_+0i(XseY{HW&rkY*3r=f(I)G>3c;&t~KZ_AKLPGz_3j zWSfi5#77MO86C-fkLmbu^h|tso-RerQkXNJtTd`TkIrm6hq-dv?2nJ==#(KlsiAwC zc1FZx*ytJWM@n2oo8gN<7)B>V+~=Y>8AlBy5nnyY8Y6Ces2Dv_#4P%EhuHa$Y|4iL zYn~UI$(mW5FuCBTLE^1h*uIcRpBbhoT(ccoy$4_M>Xi63?>YPFu;ix4(Em*IRDSmh zZTCd<0*dW7pEY z(+9ArspkiGH}) ze#XhdCci06&qk*;8m6g5q@FT?A;J*F?cw5E&fg6o$elzV3I0~cUs%NR_Dga$$w;zo z$vut!lhJXkfZN2CQ>m>b1-}^|R9SJdv{@CIP6n{XbF7+`D|eH2y7@^Y5hkesp{+fc ziVV+aWj8)W=uX9tsin-Tq9=qO>nLuyiblxbMv5B|79TYvPbz}_nRtGvNpMex92&C}7eHdDqz!uKh{`3O;JLRF04r_+r)=JjiVL$Sa%M{TC<3=qxE77_OY}X)895tsNEz2-{#h}P zoS$D6<4ZhI#N;qM7@ba%?F37h(MXxrB$*bYi1Q>{L@rJqah7x>@bN7LCdoKlVUAUs z>Y`9f**$MI6+_g4uMu7{$+#xjU0z$a6`}1(*b*vSF2n#w;CdEAO>4rClk~JbLkWVM zVom!d7J`MT^7->y!q>*Au?<_x0V+meD^3EIiUwi=W3pKji>wm42+T}RNlTbYgieY!2cj1>mm*FQUQe2DSvB8k$ptGvZ0}sHpSPwe5i4x< z^~Bs}VWL2CS8Fr(k3O3i`B3;hT;~KD4kcVsk?-Z#(3nngi<2NBoTff zn!^)^-`tV9kR%*;#;{FZOMFyckh0S_P-@1a$G1r?q7RiGm0*6egh>(^&2vF9f48QJ z3(_a?WgR_F8!;}P*iR50oNp7xsiNWmdx$PZ&~jDH>vk-jYTjWNh?033YWcNn*W`}X z^4m#tWQE1#T$(ef+>7k1mpC>$- z!NG5jXmMoDD0VdY95G(DxudYDIU=P-Q#_>T%hmLRxY}M#39-ca2hEwo9hI*XxSe8D zjz^~LwKD&m1Bty@a=E(D25IguYz(7JWN&(z2NK>3e8r#>dDR{Y%7%PzRT06`2jVGxx-2368IjNmHjB)h1SyvHDx(T`Qsm3?>H=eR=K$9-v|HB^O) zsCBk5ip~x}A3+6m!c(LO_8&Dde5+Q0F66NxUSIJ!YJq5b?VlJ$ek03aH@?IeZsUEd zpw|84+I-vW5&PR+0u3mj2JE0t{1G{X{XpI288%~(@+grAD!~*|RU1Q9Y#MbHOQ_O2 zcn>#m1V5p!%+slg^TkJ?(nL_D=|Pn=?zU5?@1vzzZ!zG1nk~#MW@qxr&KLR8-JitpBbMy6m>X^eq6*K z+{OxArLzqW(7_jE$Vvicr5M2+>_hE4gfX1KY&^nFJPVj>-Bdp5=)g#c*(gq88s1_C z-lG;iVFX`M2W3+zz!0Wmhu3zHDK>(cIEw|ifLXYSTDM#3CMF)!(N4!T*5DuN;4;E5 z#~>?4C!Z61AWa6@Y_E=@*Kvm+Oh#|j`jbIq3umQKR6)#cyet4!, YEAR. -# +# # Translators: # Fabian Braun , 2023 -# +# #, fuzzy msgid "" msgstr "" @@ -15,10 +15,10 @@ msgstr "" "PO-Revision-Date: 2023-01-10 15:29+0000\n" "Last-Translator: Fabian Braun , 2023\n" "Language-Team: German (https://app.transifex.com/divio/teams/58664/de/)\n" -"Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Language: de\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: admin.py:164 admin.py:301 admin.py:377 @@ -490,8 +490,7 @@ msgid "" "The following draft version has been unlocked by %(by_user)s for their use.\n" "%(version_link)s\n" "\n" -"Please note you will not be able to further edit this draft. Kindly reach " -"out to %(by_user)s in case of any concerns.\n" +"Please note you will not be able to further edit this draft. Kindly reach out to %(by_user)s in case of any concerns.\n" "\n" "This is an automated notification from Django CMS.\n" msgstr "" @@ -500,7 +499,6 @@ msgstr "" "\n" "%(version_link)s\n" "\n" -"Bitte beachte, dass Du diesen Entwurf nicht mehr ändern kannst. Bitte " -"kontaktiere %(by_user)s für Rückfragen.\n" +"Bitte beachte, dass Du diesen Entwurf nicht mehr ändern kannst. Bitte kontaktiere %(by_user)s für Rückfragen.\n" "\n" "Dies ist eine automatisierte Nachricht von Django CMS.\n" diff --git a/djangocms_versioning/locale/fr/LC_MESSAGES/django.mo b/djangocms_versioning/locale/fr/LC_MESSAGES/django.mo index 92cd88bbaf4bc329314c166d2a40475d32837532..1315279b0a49e4ff899380ca5d80be430380c508 100644 GIT binary patch literal 8298 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 RRxRAOzGrtqTcwt${{mq3EhGQ{ delta 2033 zcmXxldu)?c7{~Eb#vG2WyOhCNMq4f$!%dX66Nc=DtALws>Vgn3qnd)-*b2L_WD*-u zgNZINIWuU)#6J*BG{)@x10}}9U?OO|MvM?68gGd)3;rWUqw)J|&rbUE^Sh7O# z&Y%){)jgj@C2|gx$Y;2U@y#DxsMsaN`GLZy`zWr%8rR>0lIEHXyl}1gi2@3}O;1aW@WP#y!7+O7sVJ|0}Ap zIb4Hfl&{}wLb^;FYMHxGOZf_BmDzbNl-X4b<26+O;*$JSuf?U@CsAv^7jMF6-Se}k zfiIzc_&SzjIFK)(0W~A-$jj{DjUmjg0Q0X(({%71a}YP+6srFXROKFOhAz7PFPz`t zYd#XkHl9CC721>+@K*dCH4`hyrVh6vU1kVX(9v?{Uzu4tRPjk{!naZVbEr*u9knUz zS!iw6H0pCNpc4Jmc?tEsZ&5S#Gpc|})=>r2<1&mRFVmIfU77TwK5zu}g~u>~GpGtK zqSkT_^|`-Lr$Sp=0~MnBn@|b0U^{jqA2m;*#(B}*zlBOP`+JoqS{%k2oWVLghuVZcVgnXd<&SA2>V6-t!)a6kXR%D@|0)+0ZLXoF zq%f4l^}p;qh3rwYjhJs*Qng;4f32gYSV`#k zsohW9Luf!%sWrZj(6Q4F-$dL?Bni!iDpXU2^R3Tas0huFPRM-Q!NqNa#?Q4<-Zg!9 z5n2vSxmrOUbEi`*Aas72Fmne%fw|+>gB^t0dLl|_k8~1hnx!s6v!^9sRig!YCun*L6xbragKYWfEiCpHiw;zh;`|R|hcWiHQjrV==q|XldqkJzIXHFmb*$w+GC{F%(aSgbd7aCjg+8r!n}Q2S8o(D?e{%tHgM zHW{ofn4FxnBf(mKcWUIoMCw3#b8P>pwZT2!Kf#dC7qkCH>bwg}J)eE9s>5EcirUK1 wl${Mt+uh;qo)`Yo=e=Lu=1iP4ACW^6n)J~3|o2QH<|g8%>k diff --git a/djangocms_versioning/locale/fr/LC_MESSAGES/django.po b/djangocms_versioning/locale/fr/LC_MESSAGES/django.po index 0316829c..02035cba 100644 --- a/djangocms_versioning/locale/fr/LC_MESSAGES/django.po +++ b/djangocms_versioning/locale/fr/LC_MESSAGES/django.po @@ -2,11 +2,11 @@ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. -# +# # Translators: # François Palmier , 2023 # Frédéric Roland, 2023 -# +# #, fuzzy msgid "" msgstr "" @@ -16,12 +16,11 @@ msgstr "" "PO-Revision-Date: 2023-01-10 15:29+0000\n" "Last-Translator: Frédéric Roland, 2023\n" "Language-Team: French (https://app.transifex.com/divio/teams/58664/fr/)\n" -"Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % " -"1000000 == 0 ? 1 : 2;\n" +"Language: fr\n" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #: admin.py:164 admin.py:301 admin.py:377 msgid "State" @@ -493,8 +492,7 @@ msgid "" "The following draft version has been unlocked by %(by_user)s for their use.\n" "%(version_link)s\n" "\n" -"Please note you will not be able to further edit this draft. Kindly reach " -"out to %(by_user)s in case of any concerns.\n" +"Please note you will not be able to further edit this draft. Kindly reach out to %(by_user)s in case of any concerns.\n" "\n" "This is an automated notification from Django CMS.\n" msgstr "" @@ -502,7 +500,6 @@ msgstr "" "Le brouillon suivant a été déverrouillé par %(by_user)s pour son usage.\n" "%(version_link)s\n" "\n" -"Notez que vous ne pourrez pas continuer a modifier ce brouillon. Vous êtes " -"prié de contacter %(by_user)s en cas de soucis.\n" +"Notez que vous ne pourrez pas continuer a modifier ce brouillon. Vous êtes prié de contacter %(by_user)s en cas de soucis.\n" "\n" "C'est une notification automatique de Django CMS.\n" diff --git a/djangocms_versioning/locale/nl/LC_MESSAGES/django.mo b/djangocms_versioning/locale/nl/LC_MESSAGES/django.mo index a1ffd899b3435bb042b75fdd2779b2c8899e994c..7bc611d5d6dba6ad16c521e3951d8b2084995a32 100644 GIT binary patch delta 29 lcmdmKzteuhI^oH?g`;?U67$ka6Vp?z6!LO5i-;`c1^~7X3k(1N delta 29 lcmdmKzteuhI$<84#Ju#<#Pn1vg}j`}YlWjXi;67a1^}?Q3jY8A diff --git a/djangocms_versioning/locale/nl/LC_MESSAGES/django.po b/djangocms_versioning/locale/nl/LC_MESSAGES/django.po index ccc036a8..fd9c6b65 100644 --- a/djangocms_versioning/locale/nl/LC_MESSAGES/django.po +++ b/djangocms_versioning/locale/nl/LC_MESSAGES/django.po @@ -2,11 +2,11 @@ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. -# +# # Translators: # Fabian Braun , 2023 # Stefan van den Eertwegh , 2023 -# +# #, fuzzy msgid "" msgstr "" @@ -16,10 +16,10 @@ msgstr "" "PO-Revision-Date: 2023-01-10 15:29+0000\n" "Last-Translator: Stefan van den Eertwegh , 2023\n" "Language-Team: Dutch (https://app.transifex.com/divio/teams/58664/nl/)\n" -"Language: nl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Language: nl\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: admin.py:164 admin.py:301 admin.py:377 @@ -492,16 +492,13 @@ msgid "" "The following draft version has been unlocked by %(by_user)s for their use.\n" "%(version_link)s\n" "\n" -"Please note you will not be able to further edit this draft. Kindly reach " -"out to %(by_user)s in case of any concerns.\n" +"Please note you will not be able to further edit this draft. Kindly reach out to %(by_user)s in case of any concerns.\n" "\n" "This is an automated notification from Django CMS.\n" msgstr "" "\n" -"De volgende concept versie is van het slot af door%(by_user)svoor hun " -"gebruik.\n" +"De volgende concept versie is van het slot af door%(by_user)svoor hun gebruik.\n" " %(version_link)s\n" -"Let op: je kunt niet verder bewerken in dit concept. Neem asjeblieft contact " -"op met %(by_user)s in geval van enige zorgen. \n" +"Let op: je kunt niet verder bewerken in dit concept. Neem asjeblieft contact op met %(by_user)s in geval van enige zorgen. \n" "\n" "Dit is een geautomatiseerde notificatie van Django CMS.\n" diff --git a/djangocms_versioning/locale/sq/LC_MESSAGES/django.mo b/djangocms_versioning/locale/sq/LC_MESSAGES/django.mo index 625df0675edc9842df3f3171f3276238351b5ef2..1929104de5013565f25686c9476cc736211674da 100644 GIT binary patch delta 37 tcmdmHw9RP4B7Ww?f`Z9Q`PC-h;E&?*Nz6+xO-xU=s_4JQBq delta 37 tcmdmHw9RP4B7Wxb^76?``PF!Q67$ka6Vp?z6p9NcpW=_+Y$U+K2>=lj4L$$> diff --git a/djangocms_versioning/locale/sq/LC_MESSAGES/django.po b/djangocms_versioning/locale/sq/LC_MESSAGES/django.po index 263ae0c5..9a638674 100644 --- a/djangocms_versioning/locale/sq/LC_MESSAGES/django.po +++ b/djangocms_versioning/locale/sq/LC_MESSAGES/django.po @@ -2,10 +2,10 @@ # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. -# +# # Translators: # Besnik Bleta , 2023 -# +# #, fuzzy msgid "" msgstr "" @@ -14,11 +14,11 @@ msgstr "" "POT-Creation-Date: 2023-10-02 09:37+0200\n" "PO-Revision-Date: 2023-01-10 15:29+0000\n" "Last-Translator: Besnik Bleta , 2023\n" -"Language-Team: Albanian (https://www.transifex.com/divio/teams/58664/sq/)\n" -"Language: sq\n" +"Language-Team: Albanian (https://app.transifex.com/divio/teams/58664/sq/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +"Language: sq\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: admin.py:164 admin.py:301 admin.py:377 @@ -76,10 +76,8 @@ msgid "Unpublish" msgstr "Hiqe nga të botuar" #: admin.py:758 cms_toolbars.py:115 -#, fuzzy -#| msgid "Draft" msgid "New Draft" -msgstr "Skicë" +msgstr "" #: admin.py:779 cms_toolbars.py:177 #: templates/djangocms_versioning/admin/icons/revert_icon.html:3 @@ -135,16 +133,12 @@ msgid "The last version has been deleted" msgstr "Versioni i fundit është fshirë" #: admin.py:1249 -#, fuzzy -#| msgid "You do not have permission to copy these plugins." msgid "You do not have permission to remove the version lock" -msgstr "S’keni leje të kopjoni këto shtojca." +msgstr "" #: admin.py:1254 -#, fuzzy -#| msgid "Version unpublished" msgid "Version unlocked" -msgstr "Versioni u shbotua" +msgstr "" #: admin.py:1303 #, python-brace-format @@ -176,16 +170,13 @@ msgid "Manage Versions" msgstr "Administroni Versione" #: cms_toolbars.py:210 -#, fuzzy, python-brace-format -#| msgid "Compare to {state} source" +#, python-brace-format msgid "Compare to {source}" -msgstr "Krahasoje me burimin {state}" +msgstr "" #: cms_toolbars.py:226 indicators.py:66 -#, fuzzy -#| msgid "Discard" msgid "Discard Changes" -msgstr "Hidhe tej" +msgstr "" #: cms_toolbars.py:262 msgid "View Published" @@ -277,10 +268,8 @@ msgid "Action Denied. The draft version is locked by {user}" msgstr "" #: models.py:86 -#, fuzzy -#| msgid "Create new draft" msgid "Created" -msgstr "Krijoni një skicë të re" +msgstr "" #: models.py:89 msgid "author" @@ -330,16 +319,12 @@ msgid "Version is not in draft or published state" msgstr "Versioni s’është nën gjendjen “skicë” ose “i botuar”" #: models.py:467 -#, fuzzy -#| msgid "Version archived" msgid "Version is already locked" -msgstr "Versioni u arkivua" +msgstr "" #: models.py:473 -#, fuzzy -#| msgid "Version is not a draft" msgid "Draft version is not locked" -msgstr "Versioni s’është skicë" +msgstr "" #: templates/admin/djangocms_versioning/versioning_breadcrumbs.html:3 #: templates/djangocms_versioning/admin/grouper_form.html:9 @@ -506,17 +491,7 @@ msgid "" "The following draft version has been unlocked by %(by_user)s for their use.\n" "%(version_link)s\n" "\n" -"Please note you will not be able to further edit this draft. Kindly reach " -"out to %(by_user)s in case of any concerns.\n" +"Please note you will not be able to further edit this draft. Kindly reach out to %(by_user)s in case of any concerns.\n" "\n" "This is an automated notification from Django CMS.\n" msgstr "" - -#~ msgid "actions" -#~ msgstr "veprime" - -#~ msgid "version number" -#~ msgstr "numër versioni" - -#~ msgid "Delete Changes" -#~ msgstr "Fshiji Ndryshimet" From adce88065509c84212e432c41c05a121c246ed1f Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Sat, 4 May 2024 13:50:20 +0200 Subject: [PATCH 06/20] fix: pin django fsm to < 3 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a1f934f3..aa90ca36 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ INSTALL_REQUIREMENTS = [ "Django>=3.2", "django-cms>=4.1.1", - "django-fsm" + "django-fsm<3" ] setup( From 34577f055a14f1a434e1884320d6f692c80a67f8 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Fri, 24 May 2024 00:02:04 +0200 Subject: [PATCH 07/20] feat: Add versioning actions to settings (admin change view) of versioned objects (#408) * fix: Add versioning actions to change forms * Fix ruff errors * Fix settings button * Add tests * Only offer publish button publishing technically is possible * fix: Unify edit icons * Fix: Only offer settings button if the admin change view exists. * Fix ruff issue * Improve DRY in tests --- djangocms_versioning/admin.py | 61 +++++++++-- djangocms_versioning/cms_config.py | 11 +- djangocms_versioning/cms_toolbars.py | 3 + djangocms_versioning/helpers.py | 6 +- .../djangocms_versioning/css/object-tools.css | 8 ++ .../djangocms_versioning/js/object-tools.js | 13 +++ .../page/change_form.html | 100 ++++++++++++++++++ .../versioning_buttons.html | 30 ++++++ .../admin/mixin/change_form.html | 18 ++-- .../templatetags/djangocms_versioning.py | 38 +++++++ tests/test_admin.py | 74 +++++++++++++ 11 files changed, 336 insertions(+), 26 deletions(-) create mode 100644 djangocms_versioning/static/djangocms_versioning/css/object-tools.css create mode 100644 djangocms_versioning/static/djangocms_versioning/js/object-tools.js create mode 100644 djangocms_versioning/templates/admin/djangocms_versioning/page/change_form.html create mode 100644 djangocms_versioning/templates/admin/djangocms_versioning/versioning_buttons.html diff --git a/djangocms_versioning/admin.py b/djangocms_versioning/admin.py index 0641bcb3..77b1748b 100644 --- a/djangocms_versioning/admin.py +++ b/djangocms_versioning/admin.py @@ -8,6 +8,7 @@ from cms.models import PageContent from cms.utils import get_language_from_request from cms.utils.conf import get_cms_setting +from cms.utils.helpers import is_editable_model from cms.utils.urlutils import add_url_parameters, static_with_version from django.conf import settings from django.contrib import admin, messages @@ -464,10 +465,26 @@ def _get_edit_link(self, obj, request, disabled=False): f"admin:{version._meta.app_label}_{version._meta.model_name}_edit_redirect", args=(version.pk,), ) + # Only show if no draft exists + if version.state == PUBLISHED: + pks_for_grouper = version.versionable.for_content_grouping_values( + obj + ).values_list("pk", flat=True) + drafts = Version.objects.filter( + object_id__in=pks_for_grouper, + content_type=version.content_type, + state=DRAFT, + ) + if drafts.exists(): + return "" + icon = "edit-new" + else: + icon = "edit" + return self.admin_action_button( url, - icon="pencil", - title=_("Edit"), + icon=icon, + title=_("Edit") if icon == "edit" else _("New Draft"), name="edit", disabled=disabled, action="post", @@ -747,7 +764,7 @@ def _get_edit_link(self, obj, request, disabled=False): return "" icon = "edit-new" else: - icon = "pencil" + icon = "edit" # Don't open in the sideframe if the item is not sideframe compatible keepsideframe = obj.versionable.content_model_is_sideframe_editable @@ -759,7 +776,7 @@ def _get_edit_link(self, obj, request, disabled=False): return self.admin_action_button( edit_url, icon=icon, - title=_("Edit") if icon == "pencil" else _("New Draft"), + title=_("Edit") if icon == "edit" else _("New Draft"), name="edit", action="post", disabled=disabled, @@ -822,6 +839,32 @@ def _get_unlock_link(self, obj, request): disabled=not obj.check_unlock.as_bool(request.user), ) + def _get_settings_link(self, obj, request): + """ + Generate a settings button for the Versioning Admin + """ + + # If the content object is not registered for frontend editing no action should be present + # Also, the content object must be registered with the admin site + content_model = obj.versionable.content_model + if not is_editable_model(content_model): + return "" + + try: + settings_url = reverse( + f"admin:{content_model._meta.app_label}_{content_model._meta.model_name}_change", + args=(obj.content.pk,) + ) + except Resolver404: + return "" + + return self.admin_action_button( + settings_url, + icon="settings", + title=_("Settings"), + name="settings", + ) + def get_actions_list(self): """Returns all action links as a list""" return self.get_state_actions() @@ -848,6 +891,7 @@ def get_state_actions(self): self._get_revert_link, self._get_discard_link, self._get_unlock_link, + self._get_settings_link, ] @admin.action( @@ -945,6 +989,7 @@ def publish_view(self, request, object_id): request, self.model._meta, object_id ) + requested_redirect = request.GET.get("next", None) if conf.ON_PUBLISH_REDIRECT in ("preview", "published"): redirect_url=get_preview_url(version.content) else: @@ -952,12 +997,12 @@ def publish_view(self, request, object_id): if not version.can_be_published(): self.message_user(request, _("Version cannot be published"), messages.ERROR) - return redirect(redirect_url) + return redirect(requested_redirect or redirect_url) try: version.check_publish(request.user) except ConditionFailed as e: self.message_user(request, force_str(e), messages.ERROR) - return redirect(redirect_url) + return redirect(requested_redirect or redirect_url) # Publish the version version.publish(request.user) @@ -970,7 +1015,7 @@ def publish_view(self, request, object_id): if hasattr(version.content, "get_absolute_url"): redirect_url = version.content.get_absolute_url() or redirect_url - return redirect(redirect_url) + return redirect(requested_redirect or redirect_url) def unpublish_view(self, request, object_id): """Unpublishes the specified version and redirects back to the @@ -1085,7 +1130,7 @@ def edit_redirect_view(self, request, object_id): return redirect(version_list_url(version.content)) # Redirect - return redirect(get_editable_url(target.content)) + return redirect(get_editable_url(target.content, request.GET.get("force_admin"))) def revert_view(self, request, object_id): """Reverts to the specified version i.e. creates a draft from it.""" diff --git a/djangocms_versioning/cms_config.py b/djangocms_versioning/cms_config.py index e2894084..39dbc70a 100644 --- a/djangocms_versioning/cms_config.py +++ b/djangocms_versioning/cms_config.py @@ -268,6 +268,8 @@ def on_page_content_archive(version): class VersioningCMSPageAdminMixin(VersioningAdminMixin): + change_form_template = "admin/djangocms_versioning/page/change_form.html" + def get_readonly_fields(self, request, obj=None): fields = super().get_readonly_fields(request, obj) if obj: @@ -281,15 +283,6 @@ def get_readonly_fields(self, request, obj=None): fields.remove(f_name) return fields - def get_form(self, request, obj=None, **kwargs): - form = super().get_form(request, obj, **kwargs) - if obj: - version = Version.objects.get_for_content(obj) - if not version.check_modify.as_bool(request.user): - for f_name in ["slug", "overwrite_url"]: - form.declared_fields[f_name].widget.attrs["readonly"] = True - return form - def get_queryset(self, request): urls = ("cms_pagecontent_get_tree",) queryset = super().get_queryset(request) diff --git a/djangocms_versioning/cms_toolbars.py b/djangocms_versioning/cms_toolbars.py index 62a5231a..73d18594 100644 --- a/djangocms_versioning/cms_toolbars.py +++ b/djangocms_versioning/cms_toolbars.py @@ -292,6 +292,9 @@ def get_page_content(self, language=None): if not language: language = self.current_lang + toolbar_obj = self.toolbar.get_object() + if toolbar_obj and toolbar_obj.language == language: + return self.toolbar.get_object() return get_latest_admin_viewable_content(self.page, language=language) def populate(self): diff --git a/djangocms_versioning/helpers.py b/djangocms_versioning/helpers.py index 8d2213d8..76636e14 100644 --- a/djangocms_versioning/helpers.py +++ b/djangocms_versioning/helpers.py @@ -233,11 +233,11 @@ def is_content_editable(placeholder, user): return version.state == DRAFT -def get_editable_url(content_obj): +def get_editable_url(content_obj, force_admin=False): """If the object is editable the cms editable view should be used, with the toolbar. - This method is provides the URL for it. + This method provides the URL for it. """ - if is_editable_model(content_obj.__class__): + if is_editable_model(content_obj.__class__) and not force_admin: language = getattr(content_obj, "language", None) url = get_object_edit_url(content_obj, language) # Or else, the standard edit view should be used diff --git a/djangocms_versioning/static/djangocms_versioning/css/object-tools.css b/djangocms_versioning/static/djangocms_versioning/css/object-tools.css new file mode 100644 index 00000000..6b7b671e --- /dev/null +++ b/djangocms_versioning/static/djangocms_versioning/css/object-tools.css @@ -0,0 +1,8 @@ +.object-tools a.accent { + background-color: var(--accent) !important; +} +.object-tools a.accent:hover, +.object-tools a.accent:active, +.object-tools a.accent:hover:active { + background-color: color-mix(in srgb, var(--accent) 90%, var(--dca-black)) !important; +} diff --git a/djangocms_versioning/static/djangocms_versioning/js/object-tools.js b/djangocms_versioning/static/djangocms_versioning/js/object-tools.js new file mode 100644 index 00000000..ca7e1e44 --- /dev/null +++ b/djangocms_versioning/static/djangocms_versioning/js/object-tools.js @@ -0,0 +1,13 @@ +(function($) { + $(document).ready(function() { + $('.cms-form-post-method').on('click', function(e) { + e.preventDefault(); + var csrf_token = document.querySelector('form input[name="csrfmiddlewaretoken"]').value; + var url = this.href; + var $form = $('
'); + var $csrf = $(``); + $form.append($csrf); + $form.appendTo('body').submit(); + }); + }); +})(django.jQuery); diff --git a/djangocms_versioning/templates/admin/djangocms_versioning/page/change_form.html b/djangocms_versioning/templates/admin/djangocms_versioning/page/change_form.html new file mode 100644 index 00000000..92d00d0e --- /dev/null +++ b/djangocms_versioning/templates/admin/djangocms_versioning/page/change_form.html @@ -0,0 +1,100 @@ +{% extends "admin/cms/page/change_form.html" %} +{% load static admin_urls admin_modify djangocms_versioning i18n cms_admin %} + +{% block extrahead %} + {{ block.super }} + +{% endblock %} +{% block extrastyle %} + {{ block.super }} + +{% endblock %} + +{% block content_title %} + {% if title %}

{{ title }}{% if original %} - {{ original.versions.first.short_name }}{% endif %}

{% endif %} + {% block object-tools %} + {% if not popup and not add %} +
    + {% block object-tools-items %} + {% include "admin/djangocms_versioning/versioning_buttons.html" %} +
  • + {% get_preview_url original as admin_url %} + {% trans "Preview" %} +
  • + {% endblock %} +
+ {% endif %} + {% endblock %} +{% endblock %} + +{% block content %} +
+ + + +
+{% csrf_token %} +{% block form_top %}{% endblock %} + +{% if show_language_tabs and not show_permissions %} +
+ {% for lang_code, lang_name in language_tabs %} + + {% endfor %} +
+
+{% endif %} + +
+{% if is_popup %}{% endif %} +{% if save_on_top %}{% submit_row %}{% endif %} +{% if errors %} +

+{% blocktrans count errors|length as counter %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktrans %} +

+
    {% for error in adminform.form.non_field_errors %}
  • {{ error }}
  • {% endfor %}
+{% endif %} + +{% for fieldset in adminform %} + {% include "admin/cms/page/includes/fieldset.html" %} +{% endfor %} + +{% for inline_admin_formset in inline_admin_formsets %} + {% include inline_admin_formset.opts.template %} +{% endfor %} + +{% if show_permissions %} +
+ +
+{% endif %} + +{% block after_related_objects %}{% endblock %} + +{% if add %} +
+ + +
+{% else %} + {% page_submit_row %} +{% endif %} +
+
+
+ +{% block admin_change_form_document_ready %} +{{ block.super }} +{% endblock %} + +{# JavaScript for prepopulated fields #} +{% prepopulated_fields_js %} + +{% endblock %} diff --git a/djangocms_versioning/templates/admin/djangocms_versioning/versioning_buttons.html b/djangocms_versioning/templates/admin/djangocms_versioning/versioning_buttons.html new file mode 100644 index 00000000..f08c1d12 --- /dev/null +++ b/djangocms_versioning/templates/admin/djangocms_versioning/versioning_buttons.html @@ -0,0 +1,30 @@ +{% load djangocms_versioning i18n %} +{% with url=original|url_publish_version:request.user %} + {% if url %} +
  • + {% trans "Publish" %} +
  • + {% endif %} +{% endwith %} +{% with url=original|url_new_draft:request.user %} + {% if url %} +
  • + {% trans "New Draft" %} +
  • + {% endif %} +{% endwith %} +{% with url=original|url_revert_version:request.user %} + {% if url %} +
  • + {% trans "Revert" %} +
  • + {% endif %} +{% endwith %} + +{% with url=original|url_version_list %} + {% if url %} +
  • + {% trans "Versions" %} +
  • + {% endif %} +{% endwith %} diff --git a/djangocms_versioning/templates/djangocms_versioning/admin/mixin/change_form.html b/djangocms_versioning/templates/djangocms_versioning/admin/mixin/change_form.html index 6fab19b2..32908d92 100644 --- a/djangocms_versioning/templates/djangocms_versioning/admin/mixin/change_form.html +++ b/djangocms_versioning/templates/djangocms_versioning/admin/mixin/change_form.html @@ -1,12 +1,18 @@ {% extends versioning_fallback_change_form_template|default:"admin/change_form.html" %} -{% load i18n admin_urls djangocms_versioning %} +{% load static %} + +{% block extrahead %} + {{ block.super }} + +{% endblock %} + +{% block extrastyle %} + {{ block.super }} + +{% endblock %} {% block object-tools-items %} -
  • - - {% translate "Versions" %} - -
  • + {% include "admin/djangocms_versioning/versioning_buttons.html" %} {{ block.super }} {% endblock %} diff --git a/djangocms_versioning/templatetags/djangocms_versioning.py b/djangocms_versioning/templatetags/djangocms_versioning.py index e6dae706..2c07cd12 100644 --- a/djangocms_versioning/templatetags/djangocms_versioning.py +++ b/djangocms_versioning/templatetags/djangocms_versioning.py @@ -1,5 +1,7 @@ from django import template +from django.urls import reverse +from .. import constants, versionables from ..helpers import version_list_url register = template.Library() @@ -8,3 +10,39 @@ @register.filter def url_version_list(content): return version_list_url(content) + +@register.filter +def url_publish_version(content, user): + version = content.versions.first() + if version: + if version.check_publish.as_bool(user) and version.can_be_published(): + proxy_model = versionables.for_content(content).version_model_proxy + return reverse( + f"admin:{proxy_model._meta.app_label}_{proxy_model.__name__.lower()}_publish", + args=(version.pk,), + ) + return "" + +@register.filter +def url_new_draft(content, user): + version = content.versions.first() + if version: + if version.state == constants.PUBLISHED: + proxy_model = versionables.for_content(content).version_model_proxy + return reverse( + f"admin:{proxy_model._meta.app_label}_{proxy_model.__name__.lower()}_edit_redirect", + args=(version.pk,), + ) + return "" + +@register.filter +def url_revert_version(content, user): + version = content.versions.first() + if version: + if version.check_revert.as_bool(user): + proxy_model = versionables.for_content(content).version_model_proxy + return reverse( + f"admin:{proxy_model._meta.app_label}_{proxy_model.__name__.lower()}_revert", + args=(version.pk,), + ) + return "" diff --git a/tests/test_admin.py b/tests/test_admin.py index 56560c9e..2244e460 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -51,6 +51,7 @@ BlogContentFactory, BlogPostFactory, BlogPostVersionFactory, + PollVersionFactory, ) from djangocms_versioning.test_utils.incorrectly_configured_blogpost.models import ( IncorrectBlogContent, @@ -3171,3 +3172,76 @@ def test_fake_back_link(self): self.assertNotContains(response, "hijack_url") self.assertContains(response, version_list_url(version.content)) +class VersioningAdminButtonsTestCase(CMSTestCase): + def _get_versioning_url(self, version, action, versionable=PollsCMSConfig.versioning[0]): + """Helper method to return the expected action url + """ + admin_url = self.get_admin_url( + versionable.version_model_proxy, action, version.pk + ) + return admin_url + + def get_change_view_url(self, content): + return self.get_admin_url( + content.__class__, + "change", + content.pk, + ) + + def test_buttons_in_draft_changeview(self): + """Only publish button should be visible in draft mode""" + version = PollVersionFactory(state=constants.DRAFT) + action_url = self._get_versioning_url(version, "publish") + next_url = self.get_change_view_url(version.content) + expected_button = ('Publish') + + with self.login_user_context(self.get_superuser()): + response = self.client.get(self.get_change_view_url(version.content)) + + self.assertContains(response, expected_button) + self.assertNotContains(response, "Revert") + self.assertNotContains(response, "New Draft") + + def test_buttons_in_published_changeview(self): + """Only revert button should be visible in published mode""" + version = PollVersionFactory(state=constants.PUBLISHED) + action_url = self._get_versioning_url(version, "edit_redirect") + expected_button = ('New Draft') + + with self.login_user_context(self.get_superuser()): + response = self.client.get(self.get_change_view_url(version.content)) + + self.assertContains(response, expected_button) + self.assertNotContains(response, "Revert") + self.assertNotContains(response, "Publish") + + def test_buttons_in_unpublished_changeview(self): + """Only revert button should be visible in unpublished mode""" + version = PollVersionFactory(state=constants.UNPUBLISHED) + action_url = self._get_versioning_url(version, "revert") + next_url = self.get_change_view_url(version.content) + expected_button = f'Revert' + + with self.login_user_context(self.get_superuser()): + response = self.client.get(self.get_change_view_url(version.content)) + + self.assertContains(response, expected_button) + self.assertNotContains(response, "New Draft") + self.assertNotContains(response, "Publish") + + def test_buttons_in_archived_changeview(self): + """Only revert button should be visible in archived mode""" + version = PollVersionFactory(state=constants.ARCHIVED) + action_url = self._get_versioning_url(version, "revert") + next_url = self.get_change_view_url(version.content) + expected_button = f'Revert' + + with self.login_user_context(self.get_superuser()): + response = self.client.get(self.get_change_view_url(version.content)) + + self.assertContains(response, expected_button) + self.assertNotContains(response, "New Draft") + self.assertNotContains(response, "Publish") + From 1c8b14b3dc8e61d4ad3863c62665bc14136e7ebb Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Thu, 30 May 2024 10:11:09 +0200 Subject: [PATCH 08/20] fix: Remove workaround for page-specific rendering (#411) --- djangocms_versioning/plugin_rendering.py | 46 +++++++++++++----------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/djangocms_versioning/plugin_rendering.py b/djangocms_versioning/plugin_rendering.py index 7571bbaf..cad0be24 100644 --- a/djangocms_versioning/plugin_rendering.py +++ b/djangocms_versioning/plugin_rendering.py @@ -1,5 +1,6 @@ from functools import lru_cache +from cms import __version__ as cms_version from cms.plugin_rendering import ContentRenderer, StructureRenderer from cms.utils.placeholder import rescan_placeholders_for_obj @@ -41,29 +42,32 @@ def render_plugin(self, instance, context, placeholder=None, editable=False): prefetch_versioned_related_objects(instance, self.toolbar) return super().render_plugin(instance, context, placeholder, editable) - def render_obj_placeholder( - self, slot, context, inherit, nodelist=None, editable=True - ): - # FIXME This is an ad-hoc solution for page-specific rendering - # code, which by default doesn't work well with versioning. - # Remove this method once the issue is fixed. - from cms.models import Placeholder + if cms_version in ("4.1.0", "4.1.1"): + # Only needed for CMS 4.1.0 and 4.1.1 which have fix #7952 not merged + # With #7952, page-specific rendering works well with versioning. + def render_obj_placeholder( + self, slot, context, inherit, nodelist=None, editable=True + ): + # FIXME This is an ad-hoc solution for page-specific rendering + # code, which by default doesn't work well with versioning. + # Remove this method once the issue is fixed. + from cms.models import Placeholder - current_obj = self.toolbar.get_object() + current_obj = self.toolbar.get_object() - # Not page, therefore we will use toolbar object as - # the current object and render the placeholder - rescan_placeholders_for_obj(current_obj) - placeholder = Placeholder.objects.get_for_obj(current_obj).get(slot=slot) - content = self.render_placeholder( - placeholder, - context=context, - page=current_obj, - editable=editable, - use_cache=True, - nodelist=None, - ) - return content + # Not page, therefore we will use toolbar object as + # the current object and render the placeholder + rescan_placeholders_for_obj(current_obj) + placeholder = Placeholder.objects.get_for_obj(current_obj).get(slot=slot) + content = self.render_placeholder( + placeholder, + context=context, + page=current_obj, + editable=editable, + use_cache=True, + nodelist=None, + ) + return content class VersionStructureRenderer(StructureRenderer): From f31b5e0fb89ccccb486df4014eec53d6322aec5b Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Thu, 30 May 2024 12:28:38 +0200 Subject: [PATCH 09/20] fix: Compare versions' back button sometimes returns to invalid URL (#413) --- djangocms_versioning/cms_toolbars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangocms_versioning/cms_toolbars.py b/djangocms_versioning/cms_toolbars.py index 73d18594..007cac53 100644 --- a/djangocms_versioning/cms_toolbars.py +++ b/djangocms_versioning/cms_toolbars.py @@ -212,7 +212,7 @@ def _add_versioning_menu(self): url += "?" + urlencode({ "compare_to": version.pk, - "back": self.request.get_full_path(), + "back": self.toolbar.request_path, }) versioning_menu.add_link_item(name, url=url) # Discard changes menu entry (wrt to source) From 778254f03efc49c7b1d2f8ebcc86804f584af1b6 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Sun, 14 Jul 2024 14:08:12 +0200 Subject: [PATCH 10/20] feat: Optimize db evaluation (#416) * Cache `page_content` in toolbar * Avoid repeated db hits * Fix signature evaluation * Avoid double asignment * Add test for number of queries! * Fix ruff issue --- djangocms_versioning/cms_toolbars.py | 18 +++++++++++++++--- djangocms_versioning/helpers.py | 19 ++++++++++--------- djangocms_versioning/indicators.py | 21 +++++++++++---------- tests/test_indicators.py | 10 ++++++++++ 4 files changed, 46 insertions(+), 22 deletions(-) diff --git a/djangocms_versioning/cms_toolbars.py b/djangocms_versioning/cms_toolbars.py index 007cac53..f7da4b5c 100644 --- a/djangocms_versioning/cms_toolbars.py +++ b/djangocms_versioning/cms_toolbars.py @@ -1,5 +1,6 @@ from collections import OrderedDict from copy import copy +from typing import Optional from cms.cms_toolbars import ( ADD_PAGE_LANGUAGE_BREAK, @@ -288,18 +289,29 @@ class VersioningPageToolbar(PageToolbar): Overriding the original Page toolbar to ensure that draft and published pages can be accessed and to allow full control over the Page toolbar for versioned pages. """ - def get_page_content(self, language=None): + + def __init__(self, *args, **kwargs): + self.page_content: Optional[PageContent] = None + super().__init__(*args, **kwargs) + + def get_page_content(self, language: Optional[str] = None) -> PageContent: if not language: language = self.current_lang + if self.page_content and self.page_content.language == language: + # Already known - no need to query it again + return self.page_content toolbar_obj = self.toolbar.get_object() if toolbar_obj and toolbar_obj.language == language: + # Already in the toolbar, then use it! return self.toolbar.get_object() - return get_latest_admin_viewable_content(self.page, language=language) + else: + # Get it from the DB + return get_latest_admin_viewable_content(self.page, language=language) def populate(self): self.page = self.request.current_page - self.title = self.get_page_content() if self.page else None + self.page_content = self.get_page_content() if self.page else None self.permissions_activated = get_cms_setting("PERMISSION") self.override_language_menu() diff --git a/djangocms_versioning/helpers.py b/djangocms_versioning/helpers.py index 76636e14..19abd78d 100644 --- a/djangocms_versioning/helpers.py +++ b/djangocms_versioning/helpers.py @@ -304,7 +304,7 @@ def remove_published_where(queryset): def get_latest_admin_viewable_content( - grouper: type, + grouper: models.Model, include_unpublished_archived: bool = False, **extra_grouping_fields, ) -> models.Model: @@ -425,15 +425,16 @@ def send_email( def get_latest_draft_version(version): - """Get latest draft version of version object + """Get latest draft version of version object and caches it """ from djangocms_versioning.constants import DRAFT from djangocms_versioning.models import Version - drafts = ( - Version.objects - .filter_by_content_grouping_values(version.content) - .filter(state=DRAFT) - ) - - return drafts.first() + if not hasattr(version, "_latest_draft_version"): + drafts = ( + Version.objects + .filter_by_content_grouping_values(version.content) + .filter(state=DRAFT) + ) + version._latest_draft_version = drafts.first() + return version._latest_draft_version diff --git a/djangocms_versioning/indicators.py b/djangocms_versioning/indicators.py index 0b625d63..7424d97c 100644 --- a/djangocms_versioning/indicators.py +++ b/djangocms_versioning/indicators.py @@ -97,25 +97,26 @@ def content_indicator(content_obj): versions = Version.objects.filter_by_content_grouping_values( content_obj ).order_by("-pk") + version_states = dict(VERSION_STATES) signature = { - state: versions.filter(state=state) - for state, name in VERSION_STATES + version.state: version + for version in versions if version.state in version_states } - if signature[DRAFT] and not signature[PUBLISHED]: + if DRAFT in signature and PUBLISHED not in signature: content_obj._indicator_status = "draft" - content_obj._version = signature[DRAFT] - elif signature[DRAFT] and signature[PUBLISHED]: + content_obj._version = signature[DRAFT], + elif DRAFT in signature and PUBLISHED in signature: content_obj._indicator_status = "dirty" - content_obj._version = (signature[DRAFT][0], signature[PUBLISHED][0]) - elif signature[PUBLISHED]: + content_obj._version = (signature[DRAFT], signature[PUBLISHED]) + elif PUBLISHED in signature: content_obj._indicator_status = "published" - content_obj._version = signature[PUBLISHED] + content_obj._version = signature[PUBLISHED], elif versions[0].state == UNPUBLISHED: content_obj._indicator_status = "unpublished" - content_obj._version = signature[UNPUBLISHED] + content_obj._version = signature[UNPUBLISHED], elif versions[0].state == ARCHIVED: content_obj._indicator_status = "archived" - content_obj._version = signature[ARCHIVED] + content_obj._version = signature[ARCHIVED], else: # pragma: no cover content_obj._indicator_status = None content_obj._version = [None] diff --git a/tests/test_indicators.py b/tests/test_indicators.py index dabce266..551e3316 100644 --- a/tests/test_indicators.py +++ b/tests/test_indicators.py @@ -219,3 +219,13 @@ def test_mixin_factory(self): self.assertContains(response, "cms.pagetree.css"), # JS loadeD? self.assertContains(response, "indicators.js") + + def test_page_indicator_db_queries(self): + """Only one query should be executed to get the indicator""" + version = PageVersionFactory( + content__language="en", + ) + with self.assertNumQueries(1): + from djangocms_versioning.indicators import content_indicator + + content_indicator(version.content) From 3af4fc00223b9d459e284b19f8779457b70e6ec5 Mon Sep 17 00:00:00 2001 From: Jacob Rief Date: Wed, 24 Jul 2024 16:21:17 +0200 Subject: [PATCH 11/20] fix-permissions-adding-page (#419) * fix-permissions-adding-page * Update djangocms_versioning/templates/admin/djangocms_versioning/page/change_form.html Co-authored-by: Fabian Braun --------- Co-authored-by: Fabian Braun --- .../templates/admin/djangocms_versioning/page/change_form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/djangocms_versioning/templates/admin/djangocms_versioning/page/change_form.html b/djangocms_versioning/templates/admin/djangocms_versioning/page/change_form.html index 92d00d0e..925c1845 100644 --- a/djangocms_versioning/templates/admin/djangocms_versioning/page/change_form.html +++ b/djangocms_versioning/templates/admin/djangocms_versioning/page/change_form.html @@ -32,7 +32,7 @@ -
    + {% csrf_token %} {% block form_top %}{% endblock %} From ad0024c728898200bf91ce8c103a112a73dda4fd Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Fri, 26 Jul 2024 11:27:13 +0200 Subject: [PATCH 12/20] chore: Prepare release 2.1.0 (#415) * Prepare release 2.1.0 * Update tests for django-cms@develop-4 * Fix ruff issues * Add warning to `CMSMenu` class --- CHANGELOG.rst | 10 +++ djangocms_versioning/__init__.py | 2 +- djangocms_versioning/cms_menus.py | 14 +++- djangocms_versioning/plugin_rendering.py | 4 +- djangocms_versioning/test_utils/factories.py | 37 +++++++---- tests/test_indicators.py | 3 +- tests/test_integration_with_core.py | 3 +- tests/test_menus.py | 67 ++++++++------------ 8 files changed, 78 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1b54acd2..3b6d6f82 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,16 @@ Changelog ========= +2.1.0 (2024-07-12) +================== + +* feat: Add versioning actions to settings (admin change view) of versioned objects by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/408 +* fix: Remove workaround for page-specific rendering by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/411 +* fix: Compare versions' back button sometimes returns to invalid URL by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/413 + + +**Full Changelog**: https://github.com/django-cms/djangocms-versioning/compare/2.0.2...2.1.0 + 2.0.2 (2024-05-03) ================== diff --git a/djangocms_versioning/__init__.py b/djangocms_versioning/__init__.py index 0309ae29..9aa3f903 100644 --- a/djangocms_versioning/__init__.py +++ b/djangocms_versioning/__init__.py @@ -1 +1 @@ -__version__ = "2.0.2" +__version__ = "2.1.0" diff --git a/djangocms_versioning/cms_menus.py b/djangocms_versioning/cms_menus.py index e11cb109..51fd54ec 100644 --- a/djangocms_versioning/cms_menus.py +++ b/djangocms_versioning/cms_menus.py @@ -2,6 +2,11 @@ from cms.apphook_pool import apphook_pool from cms.cms_menus import CMSMenu as OriginalCMSMenu, get_visible_nodes from cms.models import Page + +try: + from cms.models import TreeNode +except ImportError: + TreeNode = None from cms.toolbar.utils import get_object_preview_url, get_toolbar_from_request from cms.utils.page import get_page_queryset from django.apps import apps @@ -76,6 +81,11 @@ def _get_attrs_for_node(renderer, page_content): class CMSMenu(Menu): + """This is a legacy class used by django CMS 4.0 and django CMS 4.1.0 only. Its language + fallback mechanism does not comply with django CMS' core's. Also, it is by far slower + than django CMS core's. As of django CMS 4.1.1, this class is by default deactivated. + + See https://discord.com/channels/800813886689247262/1204047551570120755 for more information.""" def get_nodes(self, request): site = self.renderer.site language = self.renderer.request_language @@ -106,8 +116,8 @@ def get_nodes(self, request): versionable_item.content_model._base_manager.filter( language=language, page__in=pages_qs, versions__state__in=states ) - .order_by("page__node__path", "versions__state") - .select_related("page", "page__node") + .order_by("page__node__path" if TreeNode else "page__path", "versions__state") + .select_related("page", "page__node" if TreeNode else "page") .prefetch_related("versions") ) added_pages = [] diff --git a/djangocms_versioning/plugin_rendering.py b/djangocms_versioning/plugin_rendering.py index cad0be24..2ed05652 100644 --- a/djangocms_versioning/plugin_rendering.py +++ b/djangocms_versioning/plugin_rendering.py @@ -43,8 +43,8 @@ def render_plugin(self, instance, context, placeholder=None, editable=False): return super().render_plugin(instance, context, placeholder, editable) if cms_version in ("4.1.0", "4.1.1"): - # Only needed for CMS 4.1.0 and 4.1.1 which have fix #7952 not merged - # With #7952, page-specific rendering works well with versioning. + # Only needed for CMS 4.1.0 and 4.1.1 which have fix #7924 not merged + # With #7924, page-specific rendering works well with versioning. def render_obj_placeholder( self, slot, context, inherit, nodelist=None, editable=True ): diff --git a/djangocms_versioning/test_utils/factories.py b/djangocms_versioning/test_utils/factories.py index 15a8a364..0b62dc59 100644 --- a/djangocms_versioning/test_utils/factories.py +++ b/djangocms_versioning/test_utils/factories.py @@ -2,7 +2,12 @@ import factory from cms import constants -from cms.models import Page, PageContent, PageUrl, Placeholder, TreeNode +from cms.models import Page, PageContent, PageUrl, Placeholder + +try: + from cms.models import TreeNode +except ImportError: + TreeNode = None from django.contrib.auth.models import User from django.contrib.contenttypes.models import ContentType from django.contrib.sites.models import Site @@ -170,18 +175,19 @@ def version(self, create, extracted, **kwargs): IncorrectBlogPostVersionFactory(content=self, **kwargs) -class TreeNodeFactory(factory.django.DjangoModelFactory): - site = factory.fuzzy.FuzzyChoice(Site.objects.all()) - depth = 0 - # NOTE: Generating path this way is probably not a good way of - # doing it, but seems to work for our present tests which only - # really need a tree node to exist and not throw unique constraint - # errors on this field. If the data in this model starts mattering - # in our tests then something more will need to be done here. - path = FuzzyText(length=8, chars=string.digits) +if TreeNode: + class TreeNodeFactory(factory.django.DjangoModelFactory): + site = factory.fuzzy.FuzzyChoice(Site.objects.all()) + depth = 0 + # NOTE: Generating path this way is probably not a good way of + # doing it, but seems to work for our present tests which only + # really need a tree node to exist and not throw unique constraint + # errors on this field. If the data in this model starts mattering + # in our tests then something more will need to be done here. + path = FuzzyText(length=8, chars=string.digits) - class Meta: - model = TreeNode + class Meta: + model = TreeNode class PageUrlFactory(factory.django.DjangoModelFactory): @@ -195,7 +201,12 @@ class Meta: class PageFactory(factory.django.DjangoModelFactory): - node = factory.SubFactory(TreeNodeFactory) + if TreeNode: + node = factory.SubFactory(TreeNodeFactory) + else: + site = factory.fuzzy.FuzzyChoice(Site.objects.all()) + depth = 0 + path = FuzzyText(length=8, chars=string.digits) class Meta: model = Page diff --git a/tests/test_indicators.py b/tests/test_indicators.py index 551e3316..f3601bed 100644 --- a/tests/test_indicators.py +++ b/tests/test_indicators.py @@ -11,6 +11,7 @@ BlogPostVersionFactory, PageFactory, PageVersionFactory, + TreeNode, ) @@ -86,7 +87,7 @@ def test_latest_admin_viewable_archive_on_top_of_published(self): class TestVersionState(CMSTestCase): def test_page_indicators(self): """The page content indicators render correctly""" - page = PageFactory(node__depth=1) + page = PageFactory(node__depth=1) if TreeNode else PageFactory(depth=1) version1 = PageVersionFactory( content__page=page, content__language="en", diff --git a/tests/test_integration_with_core.py b/tests/test_integration_with_core.py index a4b8a8bd..8dd9cc1b 100644 --- a/tests/test_integration_with_core.py +++ b/tests/test_integration_with_core.py @@ -9,6 +9,7 @@ PlaceholderFactory, PollVersionFactory, TextPluginFactory, + TreeNode, ) @@ -190,7 +191,7 @@ def test_default_cms_page_changelist_view_language_with_multi_language_content(s language filters / additional grouping values are set using the default CMS PageContent view """ - page = PageFactory(node__depth=1) + page = PageFactory(node__depth=1) if TreeNode else PageFactory(depth=1) en_version1 = PageVersionFactory( content__page=page, content__language="en", diff --git a/tests/test_menus.py b/tests/test_menus.py index a8c02b1a..5340d82d 100644 --- a/tests/test_menus.py +++ b/tests/test_menus.py @@ -19,48 +19,31 @@ class CMSVersionedMenuTestCase(CMSTestCase): def setUp(self): super().setUp() - self._page_1 = PageVersionFactory( - content__title="page_content_1", - content__menu_title="", - content__in_navigation=True, - content__limit_visibility_in_menu=None, - content__language="en", - content__page__node__path="0001", - ) - self._page_2 = PageVersionFactory( - content__title="page_content_2", - content__menu_title="", - content__in_navigation=True, - content__limit_visibility_in_menu=None, - content__language="en", - content__page__node__path="0002", - ) - self._page_2_1 = PageVersionFactory( - content__title="page_content_2_1", - content__menu_title="", - content__in_navigation=True, - content__limit_visibility_in_menu=None, - content__language="en", - content__page__node__path="00020001", - content__page__node__parent=self._page_2.content.page.node, - ) - self._page_2_2 = PageVersionFactory( - content__title="page_content_2_2", - content__menu_title="", - content__in_navigation=True, - content__limit_visibility_in_menu=None, - content__language="en", - content__page__node__path="00020002", - content__page__node__parent=self._page_2.content.page.node, - ) - self._page_3 = PageVersionFactory( - content__title="page_content_3", - content__menu_title="", - content__in_navigation=True, - content__limit_visibility_in_menu=None, - content__language="en", - content__page__node__path="0003", - ) + from djangocms_versioning.test_utils.factories import TreeNode + + def get_page(title, path, parent=None): + return { + "content__title": title, + "content__menu_title": "", + "content__in_navigation": True, + "content__limit_visibility_in_menu": None, + "content__language": "en", + "content__page__node__path" if TreeNode else "content__page__path": path, + "content__page__node__parent" if TreeNode else "content__page__parent": parent, + } + self._page_1 = PageVersionFactory(**get_page("page_content_1", "0001")) + self._page_2 = PageVersionFactory(**get_page("page_content_2", "0002")) + self._page_2_1 = PageVersionFactory(**get_page( + "page_content_2_1", + "00020001", + self._page_2.content.page.node if TreeNode else self._page_2.content.page, + )) + self._page_2_2 = PageVersionFactory(**get_page( + "page_content_2_2", + "00020002", + self._page_2.content.page.node if TreeNode else self._page_2.content.page, + )) + self._page_3 = PageVersionFactory(**get_page("page_content_3", "0003")) def _render_menu(self, user=None, **kwargs): request = RequestFactory().get("/") From 5bc66e5bbff42e7ece33c6afe3c90251c3c6e929 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Mon, 29 Jul 2024 18:48:23 +0200 Subject: [PATCH 13/20] fix: Run tests without setup tools (#420) * Replace `setup.py test` in github actions * Update test requirements * Undo some unnecessary changes --- .github/workflows/test.yml | 10 +++++----- tests/requirements/dj32_cms41.txt | 2 +- tests/requirements/dj40_cms41.txt | 2 +- tests/requirements/dj41_cms41.txt | 2 +- tests/requirements/dj42_cms41.txt | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ff49a8aa..f2d50066 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,7 +34,7 @@ jobs: python setup.py install - name: Run coverage - run: coverage run setup.py test + run: coverage run ./test_settings.py - name: Upload Coverage to Codecov uses: codecov/codecov-action@v4 @@ -78,7 +78,7 @@ jobs: python setup.py install - name: Run coverage - run: coverage run setup.py test + run: coverage run ./test_settings.py env: DATABASE_URL: postgres://postgres:postgres@127.0.0.1/postgres @@ -122,7 +122,7 @@ jobs: python setup.py install - name: Run coverage - run: coverage run setup.py test + run: coverage run ./test_settings.py env: DATABASE_URL: mysql://root@127.0.0.1/djangocms_test @@ -158,7 +158,7 @@ jobs: python setup.py install - name: Run coverage - run: coverage run setup.py test + run: coverage run ./test_settings.py - name: Upload Coverage to Codecov uses: codecov/codecov-action@v4 @@ -194,7 +194,7 @@ jobs: python setup.py install - name: Run coverage - run: coverage run setup.py test + run: coverage run ./test_settings.py - name: Upload Coverage to Codecov uses: codecov/codecov-action@v4 diff --git a/tests/requirements/dj32_cms41.txt b/tests/requirements/dj32_cms41.txt index 24060eaf..b81e33fd 100644 --- a/tests/requirements/dj32_cms41.txt +++ b/tests/requirements/dj32_cms41.txt @@ -1,6 +1,6 @@ -r requirements_base.txt -django-cms>=4.1.0rc2 +django-cms>=4.1,<4.2 Django>=3.2,<4.0 django-classy-tags diff --git a/tests/requirements/dj40_cms41.txt b/tests/requirements/dj40_cms41.txt index 7b1ccb33..08f469ca 100644 --- a/tests/requirements/dj40_cms41.txt +++ b/tests/requirements/dj40_cms41.txt @@ -1,6 +1,6 @@ -r requirements_base.txt -django-cms>=4.1.0rc2 +django-cms>=4.1,<4.2 Django>=4.0,<4.1 django-classy-tags diff --git a/tests/requirements/dj41_cms41.txt b/tests/requirements/dj41_cms41.txt index 5c1aa2b8..08e4d41b 100644 --- a/tests/requirements/dj41_cms41.txt +++ b/tests/requirements/dj41_cms41.txt @@ -1,6 +1,6 @@ -r requirements_base.txt -django-cms>=4.1.0rc2 +django-cms>=4.1,<4.2 Django>=4.1,<4.2 django-classy-tags diff --git a/tests/requirements/dj42_cms41.txt b/tests/requirements/dj42_cms41.txt index 1e78584a..3546934e 100644 --- a/tests/requirements/dj42_cms41.txt +++ b/tests/requirements/dj42_cms41.txt @@ -1,6 +1,6 @@ -r requirements_base.txt -django-cms>=4.1.0rc2 +django-cms>=4.1,<4.2 Django>=4.2,<5 django-classy-tags From 9b8abd5d786770b746a7732e06738c70aecf4608 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Tue, 30 Jul 2024 14:27:32 +0200 Subject: [PATCH 14/20] fix: Unnecessary complexity in `current_content` query set (#417) * Fix: Linear in stead of quadratic complexity in `current_content` queryset method. * Update tests * Add test for latest_content issue in core --- djangocms_versioning/managers.py | 15 +++------- tests/test_content_models.py | 10 +++---- tests/test_integration_with_core.py | 45 +++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/djangocms_versioning/managers.py b/djangocms_versioning/managers.py index 55d615e8..5d323b76 100644 --- a/djangocms_versioning/managers.py +++ b/djangocms_versioning/managers.py @@ -60,23 +60,16 @@ def _chain(self): clone._group_by_key = self._group_by_key return clone - def current_content_iterator(self, **kwargs): - """Returns generator (not a queryset) over current content versions. Current versions are either draft - versions or published versions (in that order)""" - warnings.warn("current_content_iterator is deprecated in favour of current_conent", - DeprecationWarning, stacklevel=2) - return iter(self.current_content(**kwargs)) - def current_content(self, **kwargs): """Returns a queryset current content versions. Current versions are either draft versions or published versions (in that order). This optimized query assumes that draft versions always have a higher pk than any other version type. This is true as long as no other version type can be converted to draft without creating a new version.""" - qs = self.filter(versions__state__in=(constants.DRAFT, constants.PUBLISHED), **kwargs) - pk_filter = qs.values(*self._group_by_key)\ + pk_filter = self.filter(versions__state__in=(constants.DRAFT, constants.PUBLISHED))\ + .values(*self._group_by_key)\ .annotate(vers_pk=models.Max("versions__pk"))\ - .values_list("vers_pk", flat=True) - return qs.filter(versions__pk__in=pk_filter) + .values("vers_pk") + return self.filter(versions__pk__in=pk_filter, **kwargs) def latest_content(self, **kwargs): """Returns the "latest" content object which is in this order diff --git a/tests/test_content_models.py b/tests/test_content_models.py index 0a4d07a1..4117446c 100644 --- a/tests/test_content_models.py +++ b/tests/test_content_models.py @@ -68,7 +68,7 @@ def setUp(self) -> None: self.create_page_content(page, "it", constants.ARCHIVED) self.create_page_content(page, "it", constants.PUBLISHED) - def test_current_content_iterator(self): + def test_current_content(self): # 12 PageContent versions in total self.assertEqual(len(list( PageContent.admin_manager.all() @@ -79,11 +79,11 @@ def test_current_content_iterator(self): self.assertEqual(len(qs), 4) self.assertEqual(qs._group_by_key, ["page", "language"]) self.assertEqual(len(list( - PageContent.admin_manager.filter(page__in=self.pages1).current_content_iterator() - )), 4, f"{list(PageContent.admin_manager.filter(page__in=self.pages1).current_content_iterator())}") + PageContent.admin_manager.filter(page__in=self.pages1).current_content() + )), 4, f"{list(PageContent.admin_manager.filter(page__in=self.pages1).current_content())}") # 2 current PageContent versions for self.pages2 self.assertEqual(len(list( - PageContent.admin_manager.filter(page__in=self.pages2).current_content_iterator() + PageContent.admin_manager.filter(page__in=self.pages2).current_content() )), 4) # Now unpublish all published in pages2 @@ -93,5 +93,5 @@ def test_current_content_iterator(self): # 2 current PageContent versions for self.pages2 self.assertEqual(len(list( - PageContent.admin_manager.filter(page__in=self.pages2).current_content_iterator() + PageContent.admin_manager.filter(page__in=self.pages2).current_content() )), 2) diff --git a/tests/test_integration_with_core.py b/tests/test_integration_with_core.py index 8dd9cc1b..d4afcfb4 100644 --- a/tests/test_integration_with_core.py +++ b/tests/test_integration_with_core.py @@ -1,7 +1,12 @@ +from unittest import skipIf + +from cms import __version__ as cms_version from cms.test_utils.testcases import CMSTestCase from cms.toolbar.toolbar import CMSToolbar from cms.utils.urlutils import admin_reverse +from django.template import Context +from djangocms_versioning import constants from djangocms_versioning.plugin_rendering import VersionContentRenderer from djangocms_versioning.test_utils.factories import ( PageFactory, @@ -256,3 +261,43 @@ def test_success_url_for_cms_wizard(self): poll_wizard.get_success_url(version.content), version.content.get_absolute_url(), ) + + +class AdminManagerIntegrationTestCase(CMSTestCase): + def setUp(self): + self.page = PageFactory(node__depth=1) if TreeNode else PageFactory(depth=1) + self.en_version = PageVersionFactory( + content__page=self.page, + content__language="en", + state=constants.UNPUBLISHED, + ) + self.fr_version = PageVersionFactory( + content__page=self.page, + content__language="fr", + state=constants.ARCHIVED, + ) + self.page.languages = "en,fr" + self.page.save() + + + @skipIf(cms_version < "4.1.3", "Bug only fixed in django CMS 4.1.3") + def test_get_admin_url_for_language(self): + """Regression fixed that made unpublished and archived versions invisivle to get_admin_url_for_language + template tag. See: https://github.com/django-cms/django-cms/pull/7967""" + from django.template import Template + + # Test English page with unpublished version + context = Context({"page": self.page}) + template = Template("{% load cms_admin %}{% get_admin_url_for_language page 'en' %}") + + result = template.render(context) + + self.assertIn(f"/admin/cms/pagecontent/{self.en_version.content.pk}/", result) + + # Test French page with archived version + template = Template("{% load cms_admin %}{% get_admin_url_for_language page 'fr' %}") + + result = template.render(context) + + self.assertIn(f"/admin/cms/pagecontent/{self.fr_version.content.pk}/", result) + From ac763d9843aa075b01caf2de1ad52fea99dc5f3e Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Wed, 4 Sep 2024 07:44:18 +0200 Subject: [PATCH 15/20] Allow prefetched version objects for page contents (#418) --- djangocms_versioning/cms_config.py | 31 ++++++++++--------- djangocms_versioning/helpers.py | 16 ++++++---- djangocms_versioning/indicators.py | 16 +++++++--- djangocms_versioning/models.py | 4 +-- .../templatetags/djangocms_versioning.py | 15 +++++++-- 5 files changed, 52 insertions(+), 30 deletions(-) diff --git a/djangocms_versioning/cms_config.py b/djangocms_versioning/cms_config.py index 39dbc70a..ae864f2b 100644 --- a/djangocms_versioning/cms_config.py +++ b/djangocms_versioning/cms_config.py @@ -3,7 +3,6 @@ from cms.app_base import CMSAppConfig, CMSAppExtension from cms.extensions.models import BaseExtension from cms.models import PageContent, Placeholder -from cms.utils import get_language_from_request from cms.utils.i18n import get_language_list, get_language_tuple from cms.utils.plugins import copy_plugins_to_placeholder from cms.utils.urlutils import admin_reverse @@ -14,6 +13,7 @@ ObjectDoesNotExist, PermissionDenied, ) +from django.db.models import Prefetch from django.http import ( HttpResponse, HttpResponseBadRequest, @@ -23,7 +23,7 @@ from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ -from . import indicators, versionables +from . import indicators from .admin import VersioningAdminMixin from .constants import INDICATOR_DESCRIPTIONS from .datastructures import BaseVersionableItem, VersionableItem @@ -284,18 +284,8 @@ def get_readonly_fields(self, request, obj=None): return fields def get_queryset(self, request): - urls = ("cms_pagecontent_get_tree",) - queryset = super().get_queryset(request) - if request.resolver_match.url_name in urls: - versionable = versionables.for_content(queryset.model) - - # TODO: Improve the grouping filters to use anything defined in the - # apps versioning config extra_grouping_fields - grouping_filters = {} - if "language" in versionable.extra_grouping_fields: - grouping_filters["language"] = get_language_from_request(request) - - return queryset.filter(pk__in=versionable.distinct_groupers(**grouping_filters)) + queryset = super().get_queryset(request)\ + .prefetch_related(Prefetch("versions", to_attr="prefetched_versions")) return queryset # CAVEAT: @@ -361,7 +351,18 @@ def get_indicator_menu(cls, request, page_content): """Get the indicator menu for PageContent object taking into account the currently available versions""" menu_template = "admin/cms/page/tree/indicator_menu.html" - status = page_content.content_indicator() + if hasattr(page_content.page, "filtered_translations") and hasattr(page_content, "prefetched_versions"): + # get_tree has prefetched versions + versions = sorted( + [content.prefetched_versions[0] for content in page_content.page.filtered_translations], + key=lambda version: -version.pk, + ) + for content in page_content.page.filtered_translations: + content.__dict__["content"] = content + status = page_content.content_indicator(versions) + else: + # No prefetched versions available, get them ourselves + status = page_content.content_indicator() if not status or status == "empty": # pragma: no cover return super().get_indicator_menu(request, page_content) versions = page_content._version # Cache from .content_indicator() diff --git a/djangocms_versioning/helpers.py b/djangocms_versioning/helpers.py index 19abd78d..9afa7140 100644 --- a/djangocms_versioning/helpers.py +++ b/djangocms_versioning/helpers.py @@ -387,7 +387,11 @@ def content_is_unlocked_for_user(content: models.Model, user: settings.AUTH_USER """Check if lock doesn't exist or object is locked to provided user. """ try: - return version_is_unlocked_for_user(content.versions.first(), user) + if hasattr(content, "prefetched_versions"): + version = content.prefetched_versions[0] + else: + version = content.versions.first() + return version_is_unlocked_for_user(version, user) except AttributeError: return True @@ -425,16 +429,16 @@ def send_email( def get_latest_draft_version(version): - """Get latest draft version of version object and caches it - """ + """Get latest draft version of version object and caches it in the + content object""" from djangocms_versioning.constants import DRAFT from djangocms_versioning.models import Version - if not hasattr(version, "_latest_draft_version"): + if not hasattr(version.content, "_latest_draft_version"): drafts = ( Version.objects .filter_by_content_grouping_values(version.content) .filter(state=DRAFT) ) - version._latest_draft_version = drafts.first() - return version._latest_draft_version + version.content._latest_draft_version = drafts.first() + return version.content._latest_draft_version diff --git a/djangocms_versioning/indicators.py b/djangocms_versioning/indicators.py index 7424d97c..a23ebd13 100644 --- a/djangocms_versioning/indicators.py +++ b/djangocms_versioning/indicators.py @@ -1,5 +1,8 @@ +import typing + from cms.utils.urlutils import admin_reverse from django.contrib.auth import get_permission_codename +from django.db import models from django.utils.http import urlencode from django.utils.translation import gettext_lazy as _ @@ -87,16 +90,21 @@ def content_indicator_menu(request, status, versions, back=""): return menu -def content_indicator(content_obj): +def content_indicator( + content_obj: models.Model, + versions: typing.Optional[list[Version]] = None +) -> typing.Optional[str]: """Translates available versions into status to be reflected by the indicator. Function caches the result in the page_content object""" if not content_obj: return None # pragma: no cover elif not hasattr(content_obj, "_indicator_status"): - versions = Version.objects.filter_by_content_grouping_values( - content_obj - ).order_by("-pk") + if versions is None: + # Get all versions for the content object if not available + versions = Version.objects.filter_by_content_grouping_values( + content_obj + ).order_by("-pk") version_states = dict(VERSION_STATES) signature = { version.state: version diff --git a/djangocms_versioning/models.py b/djangocms_versioning/models.py index 0f4a3956..08ac8079 100644 --- a/djangocms_versioning/models.py +++ b/djangocms_versioning/models.py @@ -74,8 +74,8 @@ def filter_by_grouping_values(self, versionable, **kwargs): def filter_by_content_grouping_values(self, content): """Returns a list of Version objects for grouping values taken - from provided content object. In other words: - it uses the content instance property values as filter parameters + from provided content object. In other words: + it uses the content instance property values as filter parameters """ versionable = versionables.for_content(content) content_objects = versionable.for_content_grouping_values(content) diff --git a/djangocms_versioning/templatetags/djangocms_versioning.py b/djangocms_versioning/templatetags/djangocms_versioning.py index 2c07cd12..6b9bbcfe 100644 --- a/djangocms_versioning/templatetags/djangocms_versioning.py +++ b/djangocms_versioning/templatetags/djangocms_versioning.py @@ -13,7 +13,10 @@ def url_version_list(content): @register.filter def url_publish_version(content, user): - version = content.versions.first() + if hasattr(content, "prefetched_versions"): + version = content.prefetched_versions[0] + else: + version = content.versions.first() if version: if version.check_publish.as_bool(user) and version.can_be_published(): proxy_model = versionables.for_content(content).version_model_proxy @@ -25,7 +28,10 @@ def url_publish_version(content, user): @register.filter def url_new_draft(content, user): - version = content.versions.first() + if hasattr(content, "prefetched_versions"): + version = content.prefetched_versions[0] + else: + version = content.versions.first() if version: if version.state == constants.PUBLISHED: proxy_model = versionables.for_content(content).version_model_proxy @@ -37,7 +43,10 @@ def url_new_draft(content, user): @register.filter def url_revert_version(content, user): - version = content.versions.first() + if hasattr(content, "prefetched_versions"): + version = content.prefetched_versions[0] + else: + version = content.versions.first() if version: if version.check_revert.as_bool(user): proxy_model = versionables.for_content(content).version_model_proxy From 07222a40ba50661808ebdb720d9606c6c81d97c5 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Sun, 22 Sep 2024 12:30:38 +0200 Subject: [PATCH 16/20] fix: get_page_content retrieved non page-content objects from the toolbar (#423) * fix: get_page_content retrieved non page-content objects from the toolbar * Fix: Lint error * Add regression test * Same test but simpler --- djangocms_versioning/cms_toolbars.py | 8 +++++--- djangocms_versioning/test_utils/test_helpers.py | 2 +- tests/test_toolbars.py | 17 +++++++++++++++++ 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/djangocms_versioning/cms_toolbars.py b/djangocms_versioning/cms_toolbars.py index f7da4b5c..fc635d00 100644 --- a/djangocms_versioning/cms_toolbars.py +++ b/djangocms_versioning/cms_toolbars.py @@ -295,16 +295,18 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def get_page_content(self, language: Optional[str] = None) -> PageContent: + # This method overwrites the method in django CMS core. Not necessary + # for django CMS 4.2+ if not language: language = self.current_lang - if self.page_content and self.page_content.language == language: + if isinstance(self.page_content, PageContent) and self.page_content.language == language: # Already known - no need to query it again return self.page_content toolbar_obj = self.toolbar.get_object() - if toolbar_obj and toolbar_obj.language == language: + if isinstance(toolbar_obj, PageContent) and toolbar_obj.language == language: # Already in the toolbar, then use it! - return self.toolbar.get_object() + return toolbar_obj else: # Get it from the DB return get_latest_admin_viewable_content(self.page, language=language) diff --git a/djangocms_versioning/test_utils/test_helpers.py b/djangocms_versioning/test_utils/test_helpers.py index 51db206f..bca487fc 100644 --- a/djangocms_versioning/test_utils/test_helpers.py +++ b/djangocms_versioning/test_utils/test_helpers.py @@ -20,7 +20,7 @@ def get_toolbar(content_obj, user=None, **kwargs): request = kwargs.get("request", RequestFactory().get("/")) request.user = user request.session = kwargs.get("session", {}) - request.current_page = getattr(content_obj, "page", None) + request.current_page = kwargs.get("current_page", getattr(content_obj, "page", None)) request.toolbar = CMSToolbar(request) # Set the toolbar class if kwargs.get("toolbar_class", False): diff --git a/tests/test_toolbars.py b/tests/test_toolbars.py index 1c51d838..e4674560 100644 --- a/tests/test_toolbars.py +++ b/tests/test_toolbars.py @@ -6,12 +6,14 @@ from django.utils.text import slugify from djangocms_versioning.cms_config import VersioningCMSConfig +from djangocms_versioning.cms_toolbars import VersioningPageToolbar from djangocms_versioning.constants import ARCHIVED, DRAFT, PUBLISHED from djangocms_versioning.helpers import version_list_url from djangocms_versioning.test_utils.factories import ( BlogPostVersionFactory, FancyPollFactory, PageContentWithVersionFactory, + PageFactory, PageUrlFactory, PageVersionFactory, PollVersionFactory, @@ -615,3 +617,18 @@ def test_page_toolbar_wo_language_menu(self): language_menu = request.toolbar.get_menu(LANGUAGE_MENU_IDENTIFIER, _("Language")) self.assertIsNone(language_menu) + + def test_toolbar_only_catches_page_content_objects(self): + """Regression test to ensure that the toolbar only catches PageContent objects and not + other toolbar objects.""" + + version = PollVersionFactory() # Not a page content model + page = PageFactory() # Get a page, e.g. where an apphook is configured + toolbar = get_toolbar(version.content, edit_mode=True, toolbar_class=VersioningPageToolbar, current_page=page) + + # Did page get detected? Otherwise, page_content never will be detected + self.assertIs(toolbar.page, page) + # Check regression does not happen + self.assertNotIsInstance(toolbar.page_content, version.content.__class__) + # Check for correct result + self.assertIsNone(toolbar.page_content) From 85b0182a50234b423e681d757c66f82cb3535e7a Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Tue, 1 Oct 2024 20:24:37 +0200 Subject: [PATCH 17/20] Update README.rst (#424) * Update README.rst * Update README.rst --------- Co-authored-by: Vinit Kumar --- README.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index e4c7bb1e..1556a22e 100644 --- a/README.rst +++ b/README.rst @@ -32,9 +32,11 @@ Add ``djangocms_versioning`` to your project's ``INSTALLED_APPS``. Run:: - python manage.py migrate djangocms_versioning + python -m manage migrate djangocms_versioning + python -m manage create_versions --user-id -to perform the application's database migrations. +to perform the application's database migrations and (only if you have an existing database) add version objects +needed to mark existing versions as draft. ===== From 07f9ccbcbdb6f665df466c3ad24eac82a15c5b95 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Mon, 28 Oct 2024 22:00:43 +0100 Subject: [PATCH 18/20] feat: add support for Django 5.0 and 5.1 (#429) * Replace `setup.py test` in github actions * fix: added `exclude_parameters` to `ChangeList.get_queryset` * Delay test since tested fix has not been ported back to django-cms 4.1.3 * Update tests for django 5.0, 5.1 * Update test.yml for all database setups * Update to postgres 13 for django 5.x tests * Update Changelog * Update page content factory to deliver valid x frame options * fix: Close sideframe when clicking preview button * Fix: `FuzzyInteger`'s higher limit is inclusive it turns out --- .github/workflows/test.yml | 31 ++++++++++++++----- CHANGELOG.rst | 11 +++++++ djangocms_versioning/admin.py | 18 ++++++++--- .../page/change_form.html | 4 ++- djangocms_versioning/test_utils/factories.py | 2 +- tests/requirements/dj32_cms41.txt | 2 +- tests/requirements/dj42_cms41.txt | 2 +- .../{dj40_cms41.txt => dj50_cms41.txt} | 4 +-- .../{dj41_cms41.txt => dj51_cms41.txt} | 4 +-- tests/test_cms_config.py | 2 +- tests/test_integration_with_core.py | 4 +-- 11 files changed, 61 insertions(+), 23 deletions(-) rename tests/requirements/{dj40_cms41.txt => dj50_cms41.txt} (69%) rename tests/requirements/{dj41_cms41.txt => dj51_cms41.txt} (69%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f2d50066..c4315c7a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,10 +15,15 @@ jobs: python-version: [ 3.9, "3.10", "3.11", "3.12" ] # latest release minus two requirements-file: [ dj32_cms41.txt, - dj40_cms41.txt, - dj41_cms41.txt, dj42_cms41.txt, + dj50_cms41.txt, + dj51_cms41.txt, ] + exclude: + - requirements-file: dj50_cms41.txt + python-version: 3.9 + - requirements-file: dj51_cms41.txt + python-version: 3.9 steps: - uses: actions/checkout@v4 @@ -47,14 +52,19 @@ jobs: python-version: [ 3.9, "3.10", "3.11", "3.12" ] # latest release minus two requirements-file: [ dj32_cms41.txt, - dj40_cms41.txt, - dj41_cms41.txt, dj42_cms41.txt, + dj50_cms41.txt, + dj51_cms41.txt, ] + exclude: + - requirements-file: dj50_cms41.txt + python-version: 3.9 + - requirements-file: dj51_cms41.txt + python-version: 3.9 services: postgres: - image: postgres:12 + image: postgres:13 env: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres @@ -93,10 +103,15 @@ jobs: python-version: [ 3.9, "3.10", "3.11", "3.12" ] # latest release minus two requirements-file: [ dj32_cms41.txt, - dj40_cms41.txt, - dj41_cms41.txt, dj42_cms41.txt, + dj50_cms41.txt, + dj51_cms41.txt, ] + exclude: + - requirements-file: dj50_cms41.txt + python-version: 3.9 + - requirements-file: dj51_cms41.txt + python-version: 3.9 services: mysql: @@ -135,7 +150,7 @@ jobs: fail-fast: false matrix: python-version: ['3.11'] - requirements-file: ['dj42_cms41.txt'] + requirements-file: ['dj51_cms41.txt'] cms-version: [ 'https://github.com/django-cms/django-cms/archive/develop-4.tar.gz' ] diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3b6d6f82..92f7272c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,11 +5,22 @@ Changelog 2.1.0 (2024-07-12) ================== +* feat: add support for Django 5.0 and 5.1 (#429) by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/429 * feat: Add versioning actions to settings (admin change view) of versioned objects by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/408 * fix: Remove workaround for page-specific rendering by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/411 * fix: Compare versions' back button sometimes returns to invalid URL by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/413 +* feat: Add versioning actions to settings (admin change view) of versioned objects by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/408 +* feat: Optimize db evaluation by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/416 +* feat: Prefetch page content version objects for faster page tree by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/418 +* fix: Remove workaround for page-specific rendering by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/411 +* fix: Compare versions' back button sometimes returns to invalid URL by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/413 +* fix: Preparation for changes in django CMS 4.2 by @jrief in https://github.com/django-cms/djangocms-versioning/pull/419 +* fix: Unnecessary complexity in ``current_content`` query set by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/417 +* fix: get_page_content retrieved non page-content objects from the toolbar by @fsbraun in https://github.com/django-cms/djangocms-versioning/pull/423 + + **Full Changelog**: https://github.com/django-cms/djangocms-versioning/compare/2.0.2...2.1.0 2.0.2 (2024-05-03) diff --git a/djangocms_versioning/admin.py b/djangocms_versioning/admin.py index 77b1748b..84e89ce3 100644 --- a/djangocms_versioning/admin.py +++ b/djangocms_versioning/admin.py @@ -61,9 +61,14 @@ class VersioningChangeListMixin: """Mixin used for ChangeList classes of content models.""" - def get_queryset(self, request): + def get_queryset(self, request, exclude_parameters=None): """Limit the content model queryset to the latest versions only.""" - queryset = super().get_queryset(request) + if exclude_parameters: + # Django 5.0+ (facet support) + queryset = super().get_queryset(request, exclude_parameters) + else: + # Django 4.2 compatible get_queryset + queryset = super().get_queryset(request) versionable = versionables.for_content(queryset.model) """Check if there is a method "self.get__from_request" for each extra grouping field. @@ -557,7 +562,7 @@ def get_grouping_field_filters(self, request): if value is not None: yield field, value - def get_queryset(self, request): + def get_queryset(self, request, exclude_parameters=None): """Adds support for querying the version model by grouping fields. Filters by the value of grouping fields (specified in VersionableItem @@ -567,7 +572,12 @@ def get_queryset(self, request): for specifying filters that work without being shown in the UI along with filter choices. """ - queryset = super().get_queryset(request) + if exclude_parameters: + # Django 5.0+ (facet support) + queryset = super().get_queryset(request, exclude_parameters) + else: + # Django 4.2 compatible get_queryset + queryset = super().get_queryset(request) content_model = self.model_admin.model._source_model versionable = versionables.for_content(content_model) filters = dict(self.get_grouping_field_filters(request)) diff --git a/djangocms_versioning/templates/admin/djangocms_versioning/page/change_form.html b/djangocms_versioning/templates/admin/djangocms_versioning/page/change_form.html index 925c1845..43039a22 100644 --- a/djangocms_versioning/templates/admin/djangocms_versioning/page/change_form.html +++ b/djangocms_versioning/templates/admin/djangocms_versioning/page/change_form.html @@ -19,7 +19,9 @@ {% include "admin/djangocms_versioning/versioning_buttons.html" %}
  • {% get_preview_url original as admin_url %} - {% trans "Preview" %} + + {% trans "Preview" %} +
  • {% endblock %} diff --git a/djangocms_versioning/test_utils/factories.py b/djangocms_versioning/test_utils/factories.py index 0b62dc59..2eb97417 100644 --- a/djangocms_versioning/test_utils/factories.py +++ b/djangocms_versioning/test_utils/factories.py @@ -226,7 +226,7 @@ class PageContentFactory(AbstractContentFactory): soft_root = FuzzyChoice([True, False]) limit_visibility_in_menu = constants.VISIBILITY_USERS template = "page.html" - xframe_options = FuzzyInteger(0, 25) + xframe_options = FuzzyInteger(0, 3) class Meta: model = PageContent diff --git a/tests/requirements/dj32_cms41.txt b/tests/requirements/dj32_cms41.txt index b81e33fd..aaafdfa5 100644 --- a/tests/requirements/dj32_cms41.txt +++ b/tests/requirements/dj32_cms41.txt @@ -4,5 +4,5 @@ django-cms>=4.1,<4.2 Django>=3.2,<4.0 django-classy-tags -django-fsm>=2.6 +django-fsm>=2.6,<3 django-sekizai diff --git a/tests/requirements/dj42_cms41.txt b/tests/requirements/dj42_cms41.txt index 3546934e..bf600a57 100644 --- a/tests/requirements/dj42_cms41.txt +++ b/tests/requirements/dj42_cms41.txt @@ -4,5 +4,5 @@ django-cms>=4.1,<4.2 Django>=4.2,<5 django-classy-tags -django-fsm>=2.6 +django-fsm>=2.6,<3 django-sekizai diff --git a/tests/requirements/dj40_cms41.txt b/tests/requirements/dj50_cms41.txt similarity index 69% rename from tests/requirements/dj40_cms41.txt rename to tests/requirements/dj50_cms41.txt index 08f469ca..4326bfd7 100644 --- a/tests/requirements/dj40_cms41.txt +++ b/tests/requirements/dj50_cms41.txt @@ -2,7 +2,7 @@ django-cms>=4.1,<4.2 -Django>=4.0,<4.1 +Django>=5.0,<5.1 django-classy-tags -django-fsm>=2.6 +django-fsm>=2.6,<3 django-sekizai diff --git a/tests/requirements/dj41_cms41.txt b/tests/requirements/dj51_cms41.txt similarity index 69% rename from tests/requirements/dj41_cms41.txt rename to tests/requirements/dj51_cms41.txt index 08e4d41b..14b5770e 100644 --- a/tests/requirements/dj41_cms41.txt +++ b/tests/requirements/dj51_cms41.txt @@ -2,7 +2,7 @@ django-cms>=4.1,<4.2 -Django>=4.1,<4.2 +Django>=5.1,<5.2 django-classy-tags -django-fsm>=2.6 +django-fsm>=2.6,<3 django-sekizai diff --git a/tests/test_cms_config.py b/tests/test_cms_config.py index 13153e12..001f4520 100644 --- a/tests/test_cms_config.py +++ b/tests/test_cms_config.py @@ -119,7 +119,7 @@ def test_changing_slug_changes_page_url(self): form = ChangePageForm(data, instance=self.content) form._request = request form._site = self.site - self.assertEqual(form.is_valid(), True) + self.assertTrue(form.is_valid(), f"Form errors: {form.errors}") form.save() page = Page.objects.get(pk=self.page.pk) diff --git a/tests/test_integration_with_core.py b/tests/test_integration_with_core.py index d4afcfb4..ff66110e 100644 --- a/tests/test_integration_with_core.py +++ b/tests/test_integration_with_core.py @@ -280,9 +280,9 @@ def setUp(self): self.page.save() - @skipIf(cms_version < "4.1.3", "Bug only fixed in django CMS 4.1.3") + @skipIf(cms_version < "4.1.4", "Bug only fixed in django CMS 4.1.4") def test_get_admin_url_for_language(self): - """Regression fixed that made unpublished and archived versions invisivle to get_admin_url_for_language + """Regression fixed that made unpublished and archived versions invisible to get_admin_url_for_language template tag. See: https://github.com/django-cms/django-cms/pull/7967""" from django.template import Template From 76a7cc402a857f29d1b454dda2169500dee0326f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 08:22:25 +0100 Subject: [PATCH 19/20] build(deps): bump actions/cache from 4.0.2 to 4.1.2 (#431) Bumps [actions/cache](https://github.com/actions/cache) from 4.0.2 to 4.1.2. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v4.0.2...v4.1.2) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 2fceee19..f2dee668 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -19,7 +19,7 @@ jobs: python-version: '3.11' cache: 'pip' - name: Cache dependencies - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('docs/requirements.txt') }} @@ -44,7 +44,7 @@ jobs: python-version: '3.11' cache: 'pip' - name: Cache dependencies - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.2 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('docs/requirements.txt') }} From d51f80672eca1498567f60a763e6ad61d48fc59c Mon Sep 17 00:00:00 2001 From: Constantina <23738423+polyccon@users.noreply.github.com> Date: Tue, 29 Oct 2024 16:49:31 +0000 Subject: [PATCH 20/20] feat: Added bulk delete to version change view (#338) * test: adds bulk delete failing test * feat: adds first functional draft of delete_selected method * test: adds new test case to check there is warning when published version is selected * Update test_admin.py for non sqlite testing * Add error messages, and update delete permission to include content object * fix: bugs in test_admin * Update test to new expectation: Do not delete anything if a published or draft version is amongst the selected objects * Delegate the content delete to the `delete_selected` method Update the queryset to contain content elements * Add test for confirmation message * Update tests/test_admin.py * Update admin.py * Update test_admin.py * Update test_admin.py * Update admin.py * Update djangocms_versioning/admin.py --------- Co-authored-by: Fabian Braun --- djangocms_versioning/admin.py | 62 +++++++++++++++++++++++------ tests/test_admin.py | 75 +++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 12 deletions(-) diff --git a/djangocms_versioning/admin.py b/djangocms_versioning/admin.py index 84e89ce3..223d3e9e 100644 --- a/djangocms_versioning/admin.py +++ b/djangocms_versioning/admin.py @@ -12,11 +12,12 @@ from cms.utils.urlutils import add_url_parameters, static_with_version from django.conf import settings from django.contrib import admin, messages +from django.contrib.admin.actions import delete_selected from django.contrib.admin.options import IncorrectLookupParameters from django.contrib.admin.utils import unquote from django.contrib.admin.views.main import ChangeList from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist +from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist, PermissionDenied from django.db import models from django.db.models import OuterRef, Subquery from django.db.models.functions import Cast, Lower @@ -614,7 +615,7 @@ class VersionAdmin(ChangeListActionsMixin, admin.ModelAdmin, metaclass=MediaDefi """ # register custom actions - actions = ["compare_versions"] + actions = ["compare_versions", "delete_selected"] list_display = ( "number", "created", @@ -649,14 +650,6 @@ def get_list_filter(self, request): for field in versionable.extra_grouping_fields ] - def get_actions(self, request): - """Removes the standard django admin delete action.""" - actions = super().get_actions(request) - # disable delete action - if "delete_selected" in actions and not conf.ALLOW_DELETING_VERSIONS: - del actions["delete_selected"] - return actions - @admin.display( description=_("Content"), ordering="content", @@ -927,6 +920,44 @@ def compare_versions(self, request, queryset): return redirect(url) + def delete_view(self, request, object_id, extra_context=None): + """Do not allow deleting single version objects. Use discard instead.""" + raise PermissionDenied + + @admin.action( + permissions=["delete"], + description=_("Delete selected %(verbose_name_plural)s"), + ) + def delete_selected(self, request, queryset): + """ + Redirects to a delete versions view based on a users choice + """ + # Do not allow deleting single version objects. Use discard instead. + forbidden = queryset.filter(state__in=(PUBLISHED, DRAFT)) + if forbidden.exists(): + self.message_user( + request, + _("Draft or published versions cannot be deleted. First unpublish or use discard for drafts."), + messages.ERROR + ) + return None + + if request.POST.get("post"): + # When the user confirms, delete the content objects + queryset = self.get_content_queryset(queryset) + return delete_selected(self, request, queryset) + + def get_deleted_objects(self, objs, request): + """Return the content objects to be deleted""" + if issubclass(objs.model, Version): + objs = self.get_content_queryset(objs) + return super().get_deleted_objects(objs, request) + + def get_content_queryset(self, queryset): + return self.model._source_model._base_manager.filter( + pk__in=queryset.values_list("object_id", flat=True) + ) + def grouper_form_view(self, request): """Displays an intermediary page to select a grouper object to show versions of. @@ -1388,7 +1419,7 @@ def changelist_view(self, request, extra_context=None): .latest("created") .content ) - except ObjectDoesNotExist: + except (ObjectDoesNotExist, KeyError): pass return response @@ -1452,4 +1483,11 @@ def has_change_permission(self, request, obj=None): return super().has_change_permission(request, obj) def has_delete_permission(self, request, obj=None): - return False + if obj is None: + return conf.ALLOW_DELETING_VERSIONS and super().has_delete_permission(request, obj) + content_admin = self.admin_site._registry[self.model._source_model] + return all(( + conf.ALLOW_DELETING_VERSIONS, + super().has_delete_permission(request, obj), + content_admin.has_delete_permission(request, obj.content), + )) diff --git a/tests/test_admin.py b/tests/test_admin.py index 2244e460..551007fd 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -2652,6 +2652,81 @@ def test_change_view_action_compare_versions_three_selected(self): self.assertContains(response, "Exactly two versions need to be selected.") +class VersionBulkDeleteViewTestCase(CMSTestCase): + def setUp(self): + self.versionable = PollsCMSConfig.versioning[0] + self.superuser = self.get_superuser() + + @patch("djangocms_versioning.conf.ALLOW_DELETING_VERSIONS", True) + def test_change_view_action_bulk_delete_versions_three_selected(self): + """ + Query returns 1 versions when three versioning options are selected + to delete + """ + poll = factories.PollFactory() + versions = factories.PollVersionFactory.create_batch(4, content__poll=poll, state=constants.ARCHIVED) + querystring = f"?poll={poll.pk}" + endpoint = ( + self.get_admin_url(self.versionable.version_model_proxy, "changelist") + + querystring + ) + + with self.login_user_context(self.superuser): + data = { + "action": "delete_selected", + ACTION_CHECKBOX_NAME: [str(version.pk) for version in versions[1:]], + "post": "yes", + } + response = self.client.post(endpoint, data, follow=True) + + self.assertEqual(response.status_code, 200) + self.assertEqual(PollContent._base_manager.all().count(), 1) + + + @patch("djangocms_versioning.conf.ALLOW_DELETING_VERSIONS", True) + def test_change_view_action_bulk_delete_versions_gives_warning_when_published_selected(self): + """ + Nothing is deleted if a published (or draft) version is amongst the selected objects + """ + poll = factories.PollFactory() + published = factories.PollVersionFactory(state=constants.PUBLISHED) + versions = factories.PollVersionFactory.create_batch(4, content__poll=poll) + querystring = f"?poll={poll.pk}" + endpoint = ( + self.get_admin_url(self.versionable.version_model_proxy, "changelist") + + querystring + ) + + with self.login_user_context(self.superuser): + data = { + "action": "delete_selected", + ACTION_CHECKBOX_NAME: [published.pk] + [version.pk for version in versions], + "post": "yes", + } + response = self.client.post(endpoint, data, follow=True) + + self.assertEqual(response.status_code, 200) + self.assertEqual(PollContent._base_manager.all().count(), 1 + 4) + + @patch("djangocms_versioning.conf.ALLOW_DELETING_VERSIONS", True) + def test_bulk_delete_action_confirmation(self): + version = factories.PollVersionFactory(state=constants.ARCHIVED) + url = self.get_admin_url(self.versionable.version_model_proxy, "changelist") + url += f"?poll={version.content.poll.pk}" + data = { + "action": "delete_selected", + ACTION_CHECKBOX_NAME: [version.pk], + } + with self.login_user_context(self.superuser): + response = self.client.post(url, data, follow=True) + + # Check that the confirmation page is displayed + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Are you sure you want to delete the selected poll content version?") + # Check that the poll content is contained in the confirmation + self.assertContains(response, str(version)) + + class ExtendedVersionAdminTestCase(CMSTestCase): def test_extended_version_change_list_display_renders_from_provided_list_display(self):