Skip to content

Commit

Permalink
Merge branch 'master' into feat/improve_default_copy
Browse files Browse the repository at this point in the history
  • Loading branch information
fsbraun authored Mar 28, 2024
2 parents 6c0a1cb + b2d46f6 commit b04488e
Show file tree
Hide file tree
Showing 21 changed files with 534 additions and 120 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
python-version: '3.11'
cache: 'pip'
- name: Cache dependencies
uses: actions/[email protected].1
uses: actions/[email protected].2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('docs/requirements.txt') }}
Expand All @@ -44,7 +44,7 @@ jobs:
python-version: '3.11'
cache: 'pip'
- name: Cache dependencies
uses: actions/[email protected].1
uses: actions/[email protected].2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('docs/requirements.txt') }}
Expand Down
36 changes: 12 additions & 24 deletions djangocms_versioning/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -685,13 +685,13 @@ def _get_archive_link(self, obj, request, disabled=False):
icon="archive",
title=_("Archive"),
name="archive",
disabled=not obj.can_be_archived(),
disabled=not obj.check_archive.as_bool(request.user),
)

def _get_publish_link(self, obj, request):
"""Helper function to get the html link to the publish action
"""
if not obj.check_publish.as_bool(request.user):
if not obj.can_be_published():
# Don't display the link if it can't be published
return ""
publish_url = reverse(
Expand All @@ -704,14 +704,14 @@ def _get_publish_link(self, obj, request):
title=_("Publish"),
name="publish",
action="post",
disabled=not obj.can_be_published(),
disabled=not obj.check_publish.as_bool(request.user),
keepsideframe=False,
)

def _get_unpublish_link(self, obj, request, disabled=False):
"""Helper function to get the html link to the unpublish action
"""
if not obj.check_unpublish.as_bool(request.user):
if not obj.can_be_unpublished():
# Don't display the link if it can't be unpublished
return ""
unpublish_url = reverse(
Expand All @@ -723,15 +723,12 @@ def _get_unpublish_link(self, obj, request, disabled=False):
icon="unpublish",
title=_("Unpublish"),
name="unpublish",
disabled=not obj.can_be_unpublished(),
disabled=not obj.check_unpublish.as_bool(request.user),
)

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(
Expand Down Expand Up @@ -761,14 +758,14 @@ def _get_edit_link(self, obj, request, disabled=False):
title=_("Edit") if icon == "pencil" else _("New Draft"),
name="edit",
action="post",
disabled=disabled,
disabled=not obj.check_edit_redirect.as_bool(request.user) or disabled,
keepsideframe=keepsideframe,
)

def _get_revert_link(self, obj, request, disabled=False):
"""Helper function to get the html link to the revert action
"""
if not obj.check_revert.as_bool(request.user):
if obj.state in (PUBLISHED, DRAFT):
# Don't display the link if it's a draft or published
return ""

Expand All @@ -781,13 +778,13 @@ def _get_revert_link(self, obj, request, disabled=False):
icon="undo",
title=_("Revert"),
name="revert",
disabled=disabled,
disabled=not obj.check_revert.as_bool(request.user) or disabled,
)

def _get_discard_link(self, obj, request, disabled=False):
"""Helper function to get the html link to the discard action
"""
if not obj.check_discard.as_bool(request.user):
if obj.state != DRAFT:
# Don't display the link if it's not a draft
return ""

Expand All @@ -800,7 +797,7 @@ def _get_discard_link(self, obj, request, disabled=False):
icon="bin",
title=_("Discard"),
name="discard",
disabled=disabled,
disabled=not obj.check_discard.as_bool(request.user) or disabled,
)

def _get_unlock_link(self, obj, request):
Expand All @@ -811,20 +808,14 @@ def _get_unlock_link(self, obj, request):
if not conf.LOCK_VERSIONS or obj.state != DRAFT or not version_is_locked(obj):
return ""

disabled = True
# Check whether the lock can be removed
# Check that the user has unlock permission
if request.user.has_perm("djangocms_versioning.delete_versionlock"):
disabled = False

unlock_url = reverse(f"admin:{obj._meta.app_label}_{self.model._meta.model_name}_unlock", args=(obj.pk,))
return self.admin_action_button(
unlock_url,
icon="unlock",
title=_("Unlock"),
name="unlock",
action="post",
disabled=disabled,
disabled=not obj.check_unlock.as_bool(request.user),
)

def get_actions_list(self):
Expand Down Expand Up @@ -1316,10 +1307,7 @@ def changelist_view(self, request, extra_context=None):
# Check if custom breadcrumb template defined, otherwise
# fallback on default
breadcrumb_templates = [
"admin/djangocms_versioning/{app_label}/{model_name}/versioning_breadcrumbs.html".format(
app_label=breadcrumb_opts.app_label,
model_name=breadcrumb_opts.model_name,
),
f"admin/djangocms_versioning/{breadcrumb_opts.app_label}/{breadcrumb_opts.model_name}/versioning_breadcrumbs.html",
"admin/djangocms_versioning/versioning_breadcrumbs.html",
]
extra_context["breadcrumb_template"] = select_template(breadcrumb_templates)
Expand Down
5 changes: 4 additions & 1 deletion djangocms_versioning/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,18 @@ def ready(self):
from cms.models import contentmodels, fields
from cms.signals import post_obj_operation, post_placeholder_operation

from .conf import LOCK_VERSIONS
from .handlers import (
update_modified_date,
update_modified_date_for_pagecontent,
update_modified_date_for_placeholder_source,
)
from .helpers import is_content_editable
from .helpers import is_content_editable, placeholder_content_is_unlocked_for_user

# Add check to PlaceholderRelationField
fields.PlaceholderRelationField.default_checks += [is_content_editable]
if LOCK_VERSIONS:
fields.PlaceholderRelationField.default_checks += [placeholder_content_is_unlocked_for_user]

# Remove uniqueness constraint from PageContent model to allow for different versions
pagecontent_unique_together = tuple(
Expand Down
9 changes: 0 additions & 9 deletions djangocms_versioning/cms_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,13 @@

from . import indicators, versionables
from .admin import VersioningAdminMixin
from .conf import LOCK_VERSIONS
from .constants import INDICATOR_DESCRIPTIONS
from .datastructures import BaseVersionableItem, VersionableItem
from .exceptions import ConditionFailed
from .helpers import (
get_latest_admin_viewable_content,
inject_generic_relation_to_version,
is_editable,
placeholder_content_is_unlocked_for_user,
register_versionadmin_proxy,
replace_admin_for_models,
replace_manager,
Expand Down Expand Up @@ -160,12 +158,6 @@ def handle_admin_field_modifiers(self, cms_config):
for key in modifier.keys():
self.add_to_field_extension[key] = modifier[key]

def handle_locking(self):
if LOCK_VERSIONS:
from cms.models import fields

fields.PlaceholderRelationField.default_checks += [placeholder_content_is_unlocked_for_user]

def configure_app(self, cms_config):
if hasattr(cms_config, "extended_admin_field_modifiers"):
self.handle_admin_field_modifiers(cms_config)
Expand All @@ -188,7 +180,6 @@ def configure_app(self, cms_config):
self.handle_version_admin(cms_config)
self.handle_content_model_generic_relation(cms_config)
self.handle_content_model_manager(cms_config)
self.handle_locking()


def copy_page_content(original_content):
Expand Down
19 changes: 19 additions & 0 deletions djangocms_versioning/conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,22 @@ def inner(version, user):
else:
raise ConditionFailed(message)
return inner

def user_can_unlock(message: str) -> callable:
def inner(version, user):
if not user.has_perm("djangocms_versioning.delete_versionlock"):
raise ConditionFailed(message)
return inner

def user_can_publish(message: str) -> callable:
def inner(version, user):
if not version.has_publish_permission(user):
raise ConditionFailed(message)
return inner


def user_can_change(message: str) -> callable:
def inner(version, user):
if not version.has_change_permission(user):
raise ConditionFailed(message)
return inner
5 changes: 3 additions & 2 deletions djangocms_versioning/conf.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from cms import __version__ as CMS_VERSION
from django.conf import settings

ENABLE_MENU_REGISTRATION = getattr(
settings, "DJANGOCMS_VERSIONING_ENABLE_MENU_REGISTRATION", True
settings, "DJANGOCMS_VERSIONING_ENABLE_MENU_REGISTRATION", CMS_VERSION <= "4.1.0"
)

USERNAME_FIELD = getattr(
Expand Down Expand Up @@ -31,4 +32,4 @@
ON_PUBLISH_REDIRECT = getattr(
settings, "DJANGOCMS_VERISONING_ON_PUBLISH_REDIRECT", "published"
)
# Allowed values: "versions", "published", "preview"
#: Allowed values: "versions", "published", "preview"
61 changes: 58 additions & 3 deletions djangocms_versioning/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
draft_is_not_locked,
in_state,
is_not_locked,
user_can_change,
user_can_publish,
user_can_unlock,
)
from .conf import ALLOW_DELETING_VERSIONS, LOCK_VERSIONS
from .operations import send_post_version_operation, send_pre_version_operation
Expand All @@ -29,7 +32,7 @@
not_draft_error = _("Version is not a draft")
lock_error_message = _("Action Denied. The latest version is locked by {user}")
lock_draft_error_message = _("Action Denied. The draft version is locked by {user}")

permission_error_message = _("You do not have permission to perform this action")

def allow_deleting_versions(collector, field, sub_objs, using):
if ALLOW_DELETING_VERSIONS:
Expand Down Expand Up @@ -257,7 +260,7 @@ def copy(self, created_by):
Allows customization of how the content object will be copied
when specified in cms_config.py
This method needs to be ran in a transaction due to the fact that if
This method needs to be run in a transaction due to the fact that if
models are partially created in the copy method a version is not attached.
It needs to be that if anything goes wrong we should roll back the entire task.
We shouldn't leave this to package developers to know to add this feature
Expand All @@ -275,6 +278,7 @@ def copy(self, created_by):

check_archive = Conditions(
[
user_can_change(permission_error_message),
in_state([constants.DRAFT], _("Version is not in draft state")),
is_not_locked(lock_error_message),
]
Expand Down Expand Up @@ -324,7 +328,10 @@ def _set_archive(self, user):
pass

check_publish = Conditions(
[in_state([constants.DRAFT], _("Version is not in draft state"))]
[
user_can_publish(permission_error_message),
in_state([constants.DRAFT], _("Version is not in draft state")),
]
)

def can_be_published(self):
Expand Down Expand Up @@ -387,6 +394,7 @@ def _set_publish(self, user):
pass

check_unpublish = Conditions([
user_can_publish(permission_error_message),
in_state([constants.PUBLISHED], _("Version is not in published state")),
draft_is_not_locked(lock_draft_error_message),
])
Expand Down Expand Up @@ -437,14 +445,60 @@ def _set_unpublish(self, user):
possible to be left with inconsistent data)"""
pass

def has_publish_permission(self, user) -> bool:
"""
Check if the given user has permission to publish.
Args:
user (User): The user to check for permission.
Returns:
bool: True if the user has publish permission, False otherwise.
"""
return self._has_permission("publish", user)

def has_change_permission(self, user) -> bool:
"""
Check whether the given user has permission to change the object.
Parameters:
user (User): The user for which permission needs to be checked.
Returns:
bool: True if the user has permission to change the object, False otherwise.
"""
return self._has_permission("change", user)

def _has_permission(self, perm: str, user) -> bool:
"""
Check if the user has the specified permission for the content by
checking the content's has_publish_permission, has_placeholder_change_permission,
or has_change_permission methods.
Falls back to Djangos change permission for the content object.
"""
if perm == "publish" and hasattr(self.content, "has_publish_permission"):
# First try explicit publish permission
return self.content.has_publish_permission(user)
if hasattr(self.content, "has_change_permission"):
# First fallback: change permissions
return self.content.has_change_permission(user)
if hasattr(self.content, "has_placeholder_change_permission"):
# Second fallback: placeholder change permissions - works for PageContent
return self.content.has_placeholder_change_permission(user)
# final fallback: Django perms
return user.has_perm(f"{self.content_type.app_label}.change_{self.content_type.model}")

check_modify = Conditions(
[
in_state([constants.DRAFT], not_draft_error),
draft_is_not_locked(lock_draft_error_message),
user_can_unlock(permission_error_message),
]
)
check_revert = Conditions(
[
user_can_change(permission_error_message),
in_state(
[constants.ARCHIVED, constants.UNPUBLISHED],
_("Version is not in archived or unpublished state"),
Expand Down Expand Up @@ -477,6 +531,7 @@ def _set_unpublish(self, user):
[
in_state([constants.DRAFT, constants.PUBLISHED], not_draft_error),
draft_is_locked(_("Draft version is not locked"))

]
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
document.querySelectorAll('form.js-close-sideframe').forEach(el => {
el.addEventListener("submit", (ev) => {
ev.preventDefault();
ev.target.action = ev.target.action; // save action url
closeSideFrame();
const form = window.top.document.body.appendChild(ev.target);
const form = window.top.document.body.appendChild(ev.target); // move to top window
form.style.display = 'none';
form.submit();
form.submit(); // submit form
});
});
});
Expand Down
12 changes: 12 additions & 0 deletions djangocms_versioning/test_utils/blogpost/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ class BlogContent(models.Model):
language = models.TextField()
text = models.TextField()

def has_publish_permission(self, user):
if user.is_superuser:
return True
# Fake a simple object-dependent permission
return user.username in self.text

def has_change_permission(self, user):
if user.is_superuser:
return True
# Fake a simple object-dependent permission
return f"<{user.username}>" in self.text

def __str__(self):
return self.text

Expand Down
Loading

0 comments on commit b04488e

Please sign in to comment.