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') }} 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/.github/workflows/test.yml b/.github/workflows/test.yml index ff49a8aa..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 @@ -34,7 +39,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 @@ -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 @@ -78,7 +88,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 @@ -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: @@ -122,7 +137,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 @@ -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' ] @@ -158,7 +173,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 +209,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/CHANGELOG.rst b/CHANGELOG.rst index f7ab8665..92f7272c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,53 @@ 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) +================== + +* 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) +================== + +* 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/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. ===== diff --git a/djangocms_versioning/__init__.py b/djangocms_versioning/__init__.py index 8c0d5d5b..9aa3f903 100644 --- a/djangocms_versioning/__init__.py +++ b/djangocms_versioning/__init__.py @@ -1 +1 @@ -__version__ = "2.0.0" +__version__ = "2.1.0" diff --git a/djangocms_versioning/admin.py b/djangocms_versioning/admin.py index 00898e44..223d3e9e 100644 --- a/djangocms_versioning/admin.py +++ b/djangocms_versioning/admin.py @@ -8,14 +8,16 @@ 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 +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 @@ -60,9 +62,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. @@ -464,10 +471,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", @@ -491,7 +514,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) @@ -540,7 +563,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 @@ -550,7 +573,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)) @@ -587,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", @@ -622,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", @@ -729,6 +749,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( @@ -743,7 +767,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 @@ -755,10 +779,10 @@ 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=not obj.check_edit_redirect.as_bool(request.user) or disabled, + disabled=disabled, keepsideframe=keepsideframe, ) @@ -818,6 +842,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() @@ -844,6 +894,7 @@ def get_state_actions(self): self._get_revert_link, self._get_discard_link, self._get_unlock_link, + self._get_settings_link, ] @admin.action( @@ -869,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. @@ -941,6 +1030,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: @@ -948,12 +1038,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) @@ -966,7 +1056,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 @@ -1081,7 +1171,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.""" @@ -1329,7 +1419,7 @@ def changelist_view(self, request, extra_context=None): .latest("created") .content ) - except ObjectDoesNotExist: + except (ObjectDoesNotExist, KeyError): pass return response @@ -1393,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/djangocms_versioning/cms_config.py b/djangocms_versioning/cms_config.py index e2894084..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 @@ -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,28 +283,9 @@ 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) - 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: @@ -368,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/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/cms_toolbars.py b/djangocms_versioning/cms_toolbars.py index 153f43ba..fc635d00 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, @@ -35,9 +36,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 +77,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 +113,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 +133,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", ] @@ -216,7 +213,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) @@ -292,15 +289,31 @@ 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: + # This method overwrites the method in django CMS core. Not necessary + # for django CMS 4.2+ if not language: language = self.current_lang - return get_latest_admin_viewable_content(self.page, 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 isinstance(toolbar_obj, PageContent) and toolbar_obj.language == language: + # Already in the toolbar, then use it! + return toolbar_obj + 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() @@ -316,7 +329,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/djangocms_versioning/helpers.py b/djangocms_versioning/helpers.py index 8d2213d8..9afa7140 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 @@ -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: @@ -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,15 +429,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 in the + content object""" 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.content, "_latest_draft_version"): + drafts = ( + Version.objects + .filter_by_content_grouping_values(version.content) + .filter(state=DRAFT) + ) + 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 0b625d63..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,35 +90,41 @@ 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 = { - 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/djangocms_versioning/locale/ar/LC_MESSAGES/django.mo b/djangocms_versioning/locale/ar/LC_MESSAGES/django.mo new file mode 100644 index 00000000..15abc8cd Binary files /dev/null and b/djangocms_versioning/locale/ar/LC_MESSAGES/django.mo differ 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" diff --git a/djangocms_versioning/locale/de/LC_MESSAGES/django.mo b/djangocms_versioning/locale/de/LC_MESSAGES/django.mo index 27db87fb..b14902d4 100644 Binary files a/djangocms_versioning/locale/de/LC_MESSAGES/django.mo and b/djangocms_versioning/locale/de/LC_MESSAGES/django.mo differ diff --git a/djangocms_versioning/locale/de/LC_MESSAGES/django.po b/djangocms_versioning/locale/de/LC_MESSAGES/django.po index 5390b07a..095da32e 100644 --- a/djangocms_versioning/locale/de/LC_MESSAGES/django.po +++ b/djangocms_versioning/locale/de/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: # 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 92cd88bb..1315279b 100644 Binary files a/djangocms_versioning/locale/fr/LC_MESSAGES/django.mo and b/djangocms_versioning/locale/fr/LC_MESSAGES/django.mo differ 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 a1ffd899..7bc611d5 100644 Binary files a/djangocms_versioning/locale/nl/LC_MESSAGES/django.mo and b/djangocms_versioning/locale/nl/LC_MESSAGES/django.mo differ 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 625df067..1929104d 100644 Binary files a/djangocms_versioning/locale/sq/LC_MESSAGES/django.mo and b/djangocms_versioning/locale/sq/LC_MESSAGES/django.mo differ 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" 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/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/plugin_rendering.py b/djangocms_versioning/plugin_rendering.py index 7571bbaf..2ed05652 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 #7924 not merged + # With #7924, 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): 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..43039a22 --- /dev/null +++ b/djangocms_versioning/templates/admin/djangocms_versioning/page/change_form.html @@ -0,0 +1,102 @@ +{% 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..6b9bbcfe 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,48 @@ @register.filter def url_version_list(content): return version_list_url(content) + +@register.filter +def url_publish_version(content, user): + 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 + 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): + 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 + 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): + 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 + return reverse( + f"admin:{proxy_model._meta.app_label}_{proxy_model.__name__.lower()}_revert", + args=(version.pk,), + ) + return "" diff --git a/djangocms_versioning/test_utils/factories.py b/djangocms_versioning/test_utils/factories.py index 15a8a364..2eb97417 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 @@ -215,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/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/setup.py b/setup.py index 9ddc378b..aa90ca36 100644 --- a/setup.py +++ b/setup.py @@ -3,9 +3,9 @@ import djangocms_versioning INSTALL_REQUIREMENTS = [ - "Django>=1.11", - "django-cms", - "django-fsm" + "Django>=3.2", + "django-cms>=4.1.1", + "django-fsm<3" ] setup( diff --git a/tests/requirements/dj32_cms41.txt b/tests/requirements/dj32_cms41.txt index 24060eaf..aaafdfa5 100644 --- a/tests/requirements/dj32_cms41.txt +++ b/tests/requirements/dj32_cms41.txt @@ -1,8 +1,8 @@ -r requirements_base.txt -django-cms>=4.1.0rc2 +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 1e78584a..bf600a57 100644 --- a/tests/requirements/dj42_cms41.txt +++ b/tests/requirements/dj42_cms41.txt @@ -1,8 +1,8 @@ -r requirements_base.txt -django-cms>=4.1.0rc2 +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 51% rename from tests/requirements/dj40_cms41.txt rename to tests/requirements/dj50_cms41.txt index 7b1ccb33..4326bfd7 100644 --- a/tests/requirements/dj40_cms41.txt +++ b/tests/requirements/dj50_cms41.txt @@ -1,8 +1,8 @@ -r requirements_base.txt -django-cms>=4.1.0rc2 +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 51% rename from tests/requirements/dj41_cms41.txt rename to tests/requirements/dj51_cms41.txt index 5c1aa2b8..14b5770e 100644 --- a/tests/requirements/dj41_cms41.txt +++ b/tests/requirements/dj51_cms41.txt @@ -1,8 +1,8 @@ -r requirements_base.txt -django-cms>=4.1.0rc2 +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_admin.py b/tests/test_admin.py index cb59fc33..551007fd 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, @@ -528,9 +529,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 +600,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 +665,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", "") @@ -2651,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): @@ -3171,3 +3247,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") + 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_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_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_indicators.py b/tests/test_indicators.py index dabce266..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", @@ -219,3 +220,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) diff --git a/tests/test_integration_with_core.py b/tests/test_integration_with_core.py index a4b8a8bd..ff66110e 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, @@ -9,6 +14,7 @@ PlaceholderFactory, PollVersionFactory, TextPluginFactory, + TreeNode, ) @@ -190,7 +196,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", @@ -255,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.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 invisible 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) + diff --git a/tests/test_locking.py b/tests/test_locking.py index bbc72d94..f8199f57 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) @@ -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_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("/") diff --git a/tests/test_toolbars.py b/tests/test_toolbars.py index deb1f038..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, @@ -65,7 +67,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 +152,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): @@ -341,6 +343,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 +362,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() @@ -601,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)