From b099c9a694665deed936ea91a81ec9e70f075a17 Mon Sep 17 00:00:00 2001 From: Fabian Braun Date: Thu, 28 Dec 2023 12:22:29 +0100 Subject: [PATCH] feat: Add configuration to manage redirect on publish (#358) * Remove remove_published_where * Fix language issue with preview links * Add setuptools to requirements for * feat: Add configurable redirect on publish * fix lint errors * Fix tests * Test redirects * Extend tests to django-cms@develop-4 * Update tests * Update tests.yml * Use Py3.11 for latest cms branch * Redirects for unpublish * Update docs * Update toolbar test * Fix toolbar tests prt 2 --- .github/workflows/test.yml | 40 ++++++++- djangocms_versioning/admin.py | 34 +++++-- djangocms_versioning/cms_menus.py | 2 +- djangocms_versioning/cms_toolbars.py | 2 +- djangocms_versioning/conf.py | 5 ++ djangocms_versioning/helpers.py | 4 + docs/settings.rst | 25 ++++++ tests/requirements/dj32_cms41.txt | 2 + tests/requirements/dj40_cms41.txt | 2 + tests/requirements/dj41_cms41.txt | 2 + tests/requirements/dj42_cms41.txt | 2 + tests/requirements/requirements_base.txt | 3 +- tests/test_admin.py | 110 +++++++++++++++++++++-- tests/test_toolbars.py | 7 +- 14 files changed, 214 insertions(+), 26 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 96aa6836..654f4793 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ 3.9, "3.10", "3.11", "3.12" ] # latest release minus two + python-version: [ 3.9, "3.10", "3.11", ] # latest release minus two requirements-file: [ dj32_cms41.txt, dj40_cms41.txt, @@ -44,7 +44,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ 3.9, "3.10", "3.11", "3.12" ] # latest release minus two + python-version: [ 3.9, "3.10", "3.11", ] # latest release minus two requirements-file: [ dj32_cms41.txt, dj40_cms41.txt, @@ -90,7 +90,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ 3.9, "3.10", "3.11", "3.12" ] # latest release minus two + python-version: [ 3.9, "3.10", "3.11", ] # latest release minus two requirements-file: [ dj32_cms41.txt, dj40_cms41.txt, @@ -128,3 +128,37 @@ jobs: - name: Upload Coverage to Codecov uses: codecov/codecov-action@v2 + + cms-develop-sqlite: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: ['3.11'] + requirements-file: ['dj42_cms41.txt'] + cms-version: [ + 'https://github.com/django-cms/django-cms/archive/develop-4.tar.gz' + ] + os: [ + ubuntu-20.04, + ] + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r tests/requirements/${{ matrix.requirements-file }} + pip install ${{ matrix.cms-version }} + python setup.py install + + - name: Run coverage + run: coverage run setup.py test + + - name: Upload Coverage to Codecov + uses: codecov/codecov-action@v2 diff --git a/djangocms_versioning/admin.py b/djangocms_versioning/admin.py index 424fe96a..16008576 100644 --- a/djangocms_versioning/admin.py +++ b/djangocms_versioning/admin.py @@ -947,21 +947,33 @@ def publish_view(self, request, object_id): request, self.model._meta, object_id ) + if conf.ON_PUBLISH_REDIRECT in ("preview", "published"): + redirect_url=get_preview_url(version.content) + else: + redirect_url=version_list_url(version.content) + if not version.can_be_published(): self.message_user(request, _("Version cannot be published"), messages.ERROR) - return redirect(version_list_url(version.content)) + return redirect(redirect_url) try: version.check_publish(request.user) except ConditionFailed as e: self.message_user(request, force_str(e), messages.ERROR) - return redirect(version_list_url(version.content)) + return redirect(redirect_url) # Publish the version version.publish(request.user) + # Display message self.message_user(request, _("Version published")) - # Redirect - return redirect(version_list_url(version.content)) + + # Redirect to published? + if conf.ON_PUBLISH_REDIRECT == "published": + redirect_url = None + if hasattr(version.content, "get_absolute_url"): + redirect_url = version.content.get_absolute_url() or redirect_url + + return redirect(redirect_url) def unpublish_view(self, request, object_id): """Unpublishes the specified version and redirects back to the @@ -974,16 +986,21 @@ def unpublish_view(self, request, object_id): request, self.model._meta, object_id ) + if conf.ON_PUBLISH_REDIRECT in ("preview", "published"): + redirect_url=get_preview_url(version.content) + else: + redirect_url=version_list_url(version.content) + if not version.can_be_unpublished(): self.message_user( request, _("Version cannot be unpublished"), messages.ERROR ) - return redirect(version_list_url(version.content)) + return redirect(redirect_url) try: version.check_unpublish(request.user) except ConditionFailed as e: self.message_user(request, force_str(e), messages.ERROR) - return redirect(version_list_url(version.content)) + return redirect(redirect_url) if request.method != "POST": context = { @@ -1016,7 +1033,7 @@ def unpublish_view(self, request, object_id): # Display message self.message_user(request, _("Version unpublished")) # Redirect - return redirect(version_list_url(version.content)) + return redirect(redirect_url) def _get_edit_redirect_version(self, request, version): """Helper method to get the latest draft or create one if one does not exist.""" @@ -1202,11 +1219,10 @@ def compare_view(self, request, object_id): ) else: v2_preview_url = get_preview_url(v2.content) - v2_preview_url = add_url_parameters(v2_preview_url, **persist_params) context.update( { "v2": v2, - "v2_preview_url": v2_preview_url, + "v2_preview_url": add_url_parameters(v2_preview_url, **persist_params), } ) return TemplateResponse( diff --git a/djangocms_versioning/cms_menus.py b/djangocms_versioning/cms_menus.py index eba612de..e11cb109 100644 --- a/djangocms_versioning/cms_menus.py +++ b/djangocms_versioning/cms_menus.py @@ -117,7 +117,7 @@ def get_nodes(self, request): if page not in visible_pages_for_user: # The page is restricted for the user. - # Therefore we avoid adding it to the menu. + # Therefore, we avoid adding it to the menu. continue version = page_content.versions.all()[0] diff --git a/djangocms_versioning/cms_toolbars.py b/djangocms_versioning/cms_toolbars.py index d4fed366..c9d3838a 100644 --- a/djangocms_versioning/cms_toolbars.py +++ b/djangocms_versioning/cms_toolbars.py @@ -299,7 +299,7 @@ def get_page_content(self, language=None): return get_latest_admin_viewable_content(self.page, language=language) def populate(self): - self.page = self.request.current_page or getattr(self.toolbar.obj, "page", None) + self.page = self.request.current_page self.title = self.get_page_content() if self.page else None self.permissions_activated = get_cms_setting("PERMISSION") diff --git a/djangocms_versioning/conf.py b/djangocms_versioning/conf.py index 67634898..40030005 100644 --- a/djangocms_versioning/conf.py +++ b/djangocms_versioning/conf.py @@ -27,3 +27,8 @@ EMAIL_NOTIFICATIONS_FAIL_SILENTLY = getattr( settings, "EMAIL_NOTIFICATIONS_FAIL_SILENTLY", False ) + +ON_PUBLISH_REDIRECT = getattr( + settings, "DJANGOCMS_VERISONING_ON_PUBLISH_REDIRECT", "published" +) +# Allowed values: "versions", "published", "preview" diff --git a/djangocms_versioning/helpers.py b/djangocms_versioning/helpers.py index 04272cdf..8d2213d8 100644 --- a/djangocms_versioning/helpers.py +++ b/djangocms_versioning/helpers.py @@ -15,6 +15,7 @@ from django.db import models from django.template.loader import render_to_string from django.utils.encoding import force_str +from django.utils.translation import get_language from . import versionables from .conf import EMAIL_NOTIFICATIONS_FAIL_SILENTLY @@ -272,6 +273,9 @@ def get_preview_url(content_obj: models.Model, language: typing.Union[str, None] if versionable.preview_url: return versionable.preview_url(content_obj) if is_editable_model(content_obj.__class__): + if not language: + # Use language field is content object has one to determine the language + language = getattr(content_obj, "language", get_language()) url = get_object_preview_url(content_obj, language=language) else: # Or else, the standard change view should be used diff --git a/docs/settings.rst b/docs/settings.rst index 231c2d4d..7f468aef 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -60,3 +60,28 @@ Settings for djangocms Versioning will fail. +.. py:attribute:: DJANGOCMS_VERSIONING_ON_PUBLISH_REDIRECT + + Defaults to ``"published"`` + + .. versionadded:: 2.0 + + Before version 2.0 the behavior was always ``"versions"``. + + This setting determines what happens after publication/unpublication of a + content object. Three options exist: + + * ``"versions"``: The user will be redirected to a version overview of + the current object. This is particularly useful for advanced users who + need to keep a regular overview on the existing versions. + + * ``"published"``: The user will be redirected to the content object on + the site. Its URL is determined by calling ``.get_absolute_url()`` on + the content object. If does not have an absolute url or the object was + unpublished the user is redirected to the object's preview endpoint. + This is particularly useful if users only want to interact with versions + if necessary. + + * ``"preview"``: The user will be redirected to the content object's + preview endpoint. + diff --git a/tests/requirements/dj32_cms41.txt b/tests/requirements/dj32_cms41.txt index 8e55074c..24060eaf 100644 --- a/tests/requirements/dj32_cms41.txt +++ b/tests/requirements/dj32_cms41.txt @@ -1,5 +1,7 @@ -r requirements_base.txt +django-cms>=4.1.0rc2 + Django>=3.2,<4.0 django-classy-tags django-fsm>=2.6 diff --git a/tests/requirements/dj40_cms41.txt b/tests/requirements/dj40_cms41.txt index 53c58914..7b1ccb33 100644 --- a/tests/requirements/dj40_cms41.txt +++ b/tests/requirements/dj40_cms41.txt @@ -1,5 +1,7 @@ -r requirements_base.txt +django-cms>=4.1.0rc2 + Django>=4.0,<4.1 django-classy-tags django-fsm>=2.6 diff --git a/tests/requirements/dj41_cms41.txt b/tests/requirements/dj41_cms41.txt index a839ca44..5c1aa2b8 100644 --- a/tests/requirements/dj41_cms41.txt +++ b/tests/requirements/dj41_cms41.txt @@ -1,5 +1,7 @@ -r requirements_base.txt +django-cms>=4.1.0rc2 + Django>=4.1,<4.2 django-classy-tags django-fsm>=2.6 diff --git a/tests/requirements/dj42_cms41.txt b/tests/requirements/dj42_cms41.txt index 20ef631b..1e78584a 100644 --- a/tests/requirements/dj42_cms41.txt +++ b/tests/requirements/dj42_cms41.txt @@ -1,5 +1,7 @@ -r requirements_base.txt +django-cms>=4.1.0rc2 + Django>=4.2,<5 django-classy-tags django-fsm>=2.6 diff --git a/tests/requirements/requirements_base.txt b/tests/requirements/requirements_base.txt index 06dad753..1be2a30c 100644 --- a/tests/requirements/requirements_base.txt +++ b/tests/requirements/requirements_base.txt @@ -1,3 +1,4 @@ +setuptools beautifulsoup4 coverage django-app-helper @@ -14,5 +15,3 @@ psycopg2 setuptools djangocms-text-ckeditor>=5.1.2 -# Unreleased django-cms 4.0 compatible packages -django-cms>=4.1.0rc2 diff --git a/tests/test_admin.py b/tests/test_admin.py index f2496b58..14a4ba9e 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -75,6 +75,26 @@ def assertRedirectsToVersionList(self, response, version): }, ) + def assertRedirectsToPreview(self, response, version): + parsed = urlparse(response.url) + self.assertEqual(response.status_code, 302) + self.assertEqual( + parsed.path, + helpers.get_preview_url(version.content), + ) + + def assertRedirectsToPublished(self, response, version): + if hasattr(version.content, "get_absolute_url"): + published_url = version.content.get_absolute_url() + else: + published_url = helpers.get_preview_url(version.content) + parsed = urlparse(response.url) + self.assertEqual(response.status_code, 302) + self.assertEqual( + parsed.path, + published_url, + ) + class AdminVersioningTestCase(CMSTestCase): def test_admin_factory(self): @@ -1320,8 +1340,46 @@ def test_publish_view_sets_state_and_redirects(self, mocked_messages): self.assertEqual(mocked_messages.call_count, 1) self.assertEqual(mocked_messages.call_args[0][1], "Version published") # Redirect happened + self.assertRedirectsToPublished(response, poll_version) + + def test_publish_view_redirects_according_to_settings(self): + from djangocms_versioning import conf + + original_setting = conf.ON_PUBLISH_REDIRECT + user = self.get_staff_user_with_no_permissions() + + conf.ON_PUBLISH_REDIRECT ="published" + poll_version = factories.PollVersionFactory(state=constants.DRAFT) + url = self.get_admin_url( + self.versionable.version_model_proxy, "publish", poll_version.pk + ) + + with self.login_user_context(user): + response = self.client.post(url) + self.assertRedirectsToPublished(response, poll_version) + + conf.ON_PUBLISH_REDIRECT ="preview" + poll_version = factories.PollVersionFactory(state=constants.DRAFT) + url = self.get_admin_url( + self.versionable.version_model_proxy, "publish", poll_version.pk + ) + + with self.login_user_context(user): + response = self.client.post(url) + self.assertRedirectsToPreview(response, poll_version) + + conf.ON_PUBLISH_REDIRECT ="versions" + poll_version = factories.PollVersionFactory(state=constants.DRAFT) + url = self.get_admin_url( + self.versionable.version_model_proxy, "publish", poll_version.pk + ) + + with self.login_user_context(user): + response = self.client.post(url) self.assertRedirectsToVersionList(response, poll_version) + conf.ON_PUBLISH_REDIRECT = original_setting + def test_published_view_sets_modified_time(self): poll_version = factories.PollVersionFactory(state=constants.DRAFT) url = self.get_admin_url( @@ -1353,7 +1411,7 @@ def test_publish_view_cannot_be_accessed_for_archived_version( with self.login_user_context(self.get_staff_user_with_no_permissions()): response = self.client.post(url) - self.assertRedirectsToVersionList(response, poll_version) + self.assertRedirectsToPreview(response, poll_version) self.assertEqual(mocked_messages.call_count, 1) self.assertEqual(mocked_messages.call_args[0][1], messages.ERROR) @@ -1377,7 +1435,7 @@ def test_publish_view_cannot_be_accessed_for_published_version( with self.login_user_context(self.get_staff_user_with_no_permissions()): response = self.client.post(url) - self.assertRedirectsToVersionList(response, poll_version) + self.assertRedirectsToPreview(response, poll_version) self.assertEqual(mocked_messages.call_count, 1) self.assertEqual(mocked_messages.call_args[0][1], messages.ERROR) @@ -1401,7 +1459,7 @@ def test_publish_view_cannot_be_accessed_for_unpublished_version( with self.login_user_context(self.get_staff_user_with_no_permissions()): response = self.client.post(url) - self.assertRedirectsToVersionList(response, poll_version) + self.assertRedirectsToPreview(response, poll_version) self.assertEqual(mocked_messages.call_count, 1) self.assertEqual(mocked_messages.call_args[0][1], messages.ERROR) @@ -1506,7 +1564,7 @@ def test_unpublish_view_sets_state_and_redirects(self, mocked_messages): self.assertEqual(mocked_messages.call_count, 1) self.assertEqual(mocked_messages.call_args[0][1], "Version unpublished") # Redirect happened - self.assertRedirectsToVersionList(response, poll_version) + self.assertRedirectsToPreview(response, poll_version) def test_unpublish_view_sets_modified_time(self): poll_version = factories.PollVersionFactory(state=constants.PUBLISHED) @@ -1539,7 +1597,7 @@ def test_unpublish_view_cannot_be_accessed_for_archived_version( with self.login_user_context(self.get_staff_user_with_no_permissions()): response = self.client.post(url) - self.assertRedirectsToVersionList(response, poll_version) + self.assertRedirectsToPreview(response, poll_version) self.assertEqual(mocked_messages.call_count, 1) self.assertEqual(mocked_messages.call_args[0][1], messages.ERROR) @@ -1565,7 +1623,7 @@ def test_unpublish_view_cannot_be_accessed_for_unpublished_version( with self.login_user_context(self.get_staff_user_with_no_permissions()): response = self.client.post(url) - self.assertRedirectsToVersionList(response, poll_version) + self.assertRedirectsToPreview(response, poll_version) self.assertEqual(mocked_messages.call_count, 1) self.assertEqual(mocked_messages.call_args[0][1], messages.ERROR) @@ -1586,7 +1644,7 @@ def test_unpublish_view_cannot_be_accessed_for_draft_version(self, mocked_messag with self.login_user_context(self.get_staff_user_with_no_permissions()): response = self.client.post(url) - self.assertRedirectsToVersionList(response, poll_version) + self.assertRedirectsToPreview(response, poll_version) self.assertEqual(mocked_messages.call_count, 1) self.assertEqual(mocked_messages.call_args[0][1], messages.ERROR) @@ -1705,6 +1763,44 @@ def test_unpublish_view_doesnt_throw_exception_if_no_app_registered_extra_unpubl self.assertEqual(response.status_code, 200) + def test_unpublish_view_redirects_according_to_settings(self): + from djangocms_versioning import conf + + original_setting = conf.ON_PUBLISH_REDIRECT + user = self.get_staff_user_with_no_permissions() + + conf.ON_PUBLISH_REDIRECT ="published" + poll_version = factories.PollVersionFactory(state=constants.PUBLISHED) + url = self.get_admin_url( + self.versionable.version_model_proxy, "unpublish", poll_version.pk + ) + + with self.login_user_context(user): + response = self.client.post(url) + self.assertRedirectsToPreview(response, poll_version) + + conf.ON_PUBLISH_REDIRECT ="preview" + poll_version = factories.PollVersionFactory(state=constants.DRAFT) + url = self.get_admin_url( + self.versionable.version_model_proxy, "publish", poll_version.pk + ) + + with self.login_user_context(user): + response = self.client.post(url) + self.assertRedirectsToPreview(response, poll_version) + + conf.ON_PUBLISH_REDIRECT ="versions" + poll_version = factories.PollVersionFactory(state=constants.DRAFT) + url = self.get_admin_url( + self.versionable.version_model_proxy, "publish", poll_version.pk + ) + + with self.login_user_context(user): + response = self.client.post(url) + self.assertRedirectsToVersionList(response, poll_version) + + conf.ON_PUBLISH_REDIRECT = original_setting + class RevertViewTestCase(BaseStateTestCase): def setUp(self): diff --git a/tests/test_toolbars.py b/tests/test_toolbars.py index a98d7aba..665cdd36 100644 --- a/tests/test_toolbars.py +++ b/tests/test_toolbars.py @@ -211,10 +211,11 @@ def test_default_cms_edit_button_is_replaced_by_versioning_edit_button(self): The versioning edit button is available on the toolbar when versioning is installed and the model is versionable. """ - pagecontent = PageVersionFactory(content__template="") - url = get_object_preview_url(pagecontent.content, language="en") + page = PageVersionFactory(content__template="", content__language="en") + url = get_object_preview_url(page.content) + edit_url = self._get_edit_url( - pagecontent, VersioningCMSConfig.versioning[0] + page, VersioningCMSConfig.versioning[0] ) with self.login_user_context(self.get_superuser()):