263 lines
9.6 KiB
Python
263 lines
9.6 KiB
Python
from django.conf import settings
|
||
from django.utils.html import format_html
|
||
from django.utils.safestring import mark_safe
|
||
from django.utils.text import capfirst
|
||
from django.utils.translation import gettext as _
|
||
|
||
from wagtail.admin.utils import get_latest_str, get_user_display_name
|
||
from wagtail.utils.timestamps import render_timestamp
|
||
|
||
|
||
class BaseLock:
|
||
"""
|
||
Holds information about a lock on an object.
|
||
|
||
Returned by LockableMixin.get_lock() (or Page.get_lock()).
|
||
"""
|
||
|
||
def __init__(self, object):
|
||
from wagtail.models import Page
|
||
|
||
self.object = object
|
||
self.is_page = isinstance(object, Page)
|
||
# Use the base page's model name instead of the specific type for brevity
|
||
self.model_name = (Page if self.is_page else object)._meta.verbose_name
|
||
|
||
def for_user(self, user):
|
||
"""
|
||
Returns True if the lock applies to the given user.
|
||
"""
|
||
return NotImplemented
|
||
|
||
def get_message(self, user):
|
||
"""
|
||
Returns a message to display to the given user describing the lock.
|
||
"""
|
||
return None
|
||
|
||
def get_icon(self, user):
|
||
"""
|
||
Returns the name of the icon to use for the lock.
|
||
"""
|
||
return "lock"
|
||
|
||
def get_locked_by(self, user):
|
||
"""
|
||
Returns a string that represents the user or mechanism that locked the object.
|
||
"""
|
||
return _("Locked")
|
||
|
||
def get_description(self, user):
|
||
"""
|
||
Returns a description of the lock to display to the given user.
|
||
"""
|
||
return capfirst(
|
||
_("No one can make changes while the %(model_name)s is locked")
|
||
% {"model_name": self.model_name}
|
||
)
|
||
|
||
def get_context_for_user(self, user, parent_context=None):
|
||
"""
|
||
Returns a context dictionary to use in templates for the given user.
|
||
"""
|
||
return {
|
||
"locked": self.for_user(user),
|
||
"message": self.get_message(user),
|
||
"icon": self.get_icon(user),
|
||
"locked_by": self.get_locked_by(user),
|
||
"description": self.get_description(user),
|
||
}
|
||
|
||
|
||
class BasicLock(BaseLock):
|
||
"""
|
||
A lock that is enabled when the "locked" attribute of an object is True.
|
||
|
||
The object may be editable by a user depending on whether the locked_by field is set
|
||
and if WAGTAILADMIN_GLOBAL_EDIT_LOCK is not set to True.
|
||
"""
|
||
|
||
def for_user(self, user):
|
||
global_edit_lock = getattr(settings, "WAGTAILADMIN_GLOBAL_EDIT_LOCK", None)
|
||
return global_edit_lock or user.pk != self.object.locked_by_id
|
||
|
||
def get_message(self, user):
|
||
title = get_latest_str(self.object)
|
||
|
||
if self.object.locked_by_id == user.pk:
|
||
if self.object.locked_at:
|
||
return format_html(
|
||
# nosemgrep: translation-no-new-style-formatting (new-style only w/ format_html)
|
||
_(
|
||
"<b>'{title}' was locked</b> by <b>you</b> on <b>{datetime}</b>."
|
||
),
|
||
title=title,
|
||
datetime=render_timestamp(self.object.locked_at),
|
||
)
|
||
|
||
else:
|
||
return format_html(
|
||
# nosemgrep: translation-no-new-style-formatting (new-style only w/ format_html)
|
||
_("<b>'{title}' is locked</b> by <b>you</b>."),
|
||
title=title,
|
||
)
|
||
else:
|
||
if self.object.locked_by and self.object.locked_at:
|
||
return format_html(
|
||
# nosemgrep: translation-no-new-style-formatting (new-style only w/ format_html)
|
||
_(
|
||
"<b>'{title}' was locked</b> by <b>{user}</b> on <b>{datetime}</b>."
|
||
),
|
||
title=title,
|
||
user=get_user_display_name(self.object.locked_by),
|
||
datetime=render_timestamp(self.object.locked_at),
|
||
)
|
||
else:
|
||
# Object was probably locked with an old version of Wagtail, or a script
|
||
return format_html(
|
||
# nosemgrep: translation-no-new-style-formatting (new-style only w/ format_html)
|
||
_("<b>'{title}' is locked</b>."),
|
||
title=title,
|
||
)
|
||
|
||
def get_locked_by(self, user):
|
||
if self.object.locked_by_id == user.pk:
|
||
return _("Locked by you")
|
||
if self.object.locked_by_id:
|
||
return _("Locked by another user")
|
||
return super().get_locked_by(user)
|
||
|
||
def get_description(self, user):
|
||
if self.object.locked_by_id == user.pk:
|
||
return capfirst(
|
||
_("Only you can make changes while the %(model_name)s is locked")
|
||
% {"model_name": self.model_name}
|
||
)
|
||
if self.object.locked_by_id:
|
||
return capfirst(
|
||
_("Only %(user)s can make changes while the %(model_name)s is locked")
|
||
% {
|
||
"user": get_user_display_name(self.object.locked_by),
|
||
"model_name": self.model_name,
|
||
}
|
||
)
|
||
return super().get_description(user)
|
||
|
||
|
||
class WorkflowLock(BaseLock):
|
||
"""
|
||
A lock that requires the user to pass the Task.locked_for_user test on the given workflow task.
|
||
"""
|
||
|
||
def __init__(self, object, task):
|
||
super().__init__(object)
|
||
self.task = task
|
||
|
||
def for_user(self, user):
|
||
return self.task.locked_for_user(self.object, user)
|
||
|
||
def get_message(self, user):
|
||
if self.for_user(user):
|
||
current_workflow_state = self.object.current_workflow_state
|
||
if (
|
||
current_workflow_state
|
||
and len(current_workflow_state.all_tasks_with_status()) == 1
|
||
):
|
||
# If only one task in workflow, show simple message
|
||
workflow_info = capfirst(
|
||
_("This %(model_name)s is currently awaiting moderation.")
|
||
% {"model_name": self.model_name}
|
||
)
|
||
else:
|
||
workflow_info = format_html(
|
||
# nosemgrep: translation-no-new-style-formatting (new-style only w/ format_html)
|
||
_(
|
||
"This {model_name} is awaiting <b>'{task_name}'</b> in the <b>'{workflow_name}'</b> workflow."
|
||
),
|
||
model_name=self.model_name,
|
||
task_name=self.task.name,
|
||
workflow_name=current_workflow_state.workflow.name,
|
||
)
|
||
# Make sure message is correctly capitalised even if it
|
||
# starts with model_name.
|
||
workflow_info = mark_safe(capfirst(workflow_info))
|
||
|
||
reviewers_info = capfirst(
|
||
_("Only reviewers for this task can edit the %(model_name)s.")
|
||
% {"model_name": self.model_name}
|
||
)
|
||
|
||
return mark_safe(workflow_info + " " + reviewers_info)
|
||
|
||
def get_icon(self, user, can_lock=False):
|
||
if can_lock:
|
||
return "lock-open"
|
||
return super().get_icon(user)
|
||
|
||
def get_locked_by(self, user, can_lock=False):
|
||
if can_lock:
|
||
return _("Unlocked")
|
||
return _("Locked by workflow")
|
||
|
||
def get_description(self, user, can_lock=False):
|
||
if can_lock:
|
||
return capfirst(
|
||
_(
|
||
"Reviewers can edit this %(model_name)s – lock it to prevent other reviewers from editing"
|
||
)
|
||
% {"model_name": self.model_name}
|
||
)
|
||
return capfirst(
|
||
_("Only reviewers can edit and approve the %(model_name)s")
|
||
% {"model_name": self.model_name}
|
||
)
|
||
|
||
def get_context_for_user(self, user, parent_context=None):
|
||
context = super().get_context_for_user(user, parent_context)
|
||
# BasicLock can still be applied on top of WorkflowLock, so we need to
|
||
# check if the user can lock the object based on the parent context.
|
||
# We're utilising the parent context instead of self.task.user_can_lock()
|
||
# because the latter does not take into account the user's permissions,
|
||
# while the parent context does and also checks self.task.user_can_lock().
|
||
if parent_context and "user_can_lock" in parent_context:
|
||
can_lock = parent_context.get("user_can_lock", False)
|
||
context.update(
|
||
{
|
||
"icon": self.get_icon(user, can_lock),
|
||
"locked_by": self.get_locked_by(user, can_lock),
|
||
"description": self.get_description(user, can_lock),
|
||
}
|
||
)
|
||
return context
|
||
|
||
|
||
class ScheduledForPublishLock(BaseLock):
|
||
"""
|
||
A lock that occurs when something is scheduled to be published.
|
||
|
||
This prevents it becoming difficult for users to see which version is going to be published.
|
||
Nobody can edit something that's scheduled for publish.
|
||
"""
|
||
|
||
def for_user(self, user):
|
||
return True
|
||
|
||
def get_message(self, user):
|
||
scheduled_revision = self.object.scheduled_revision
|
||
|
||
message = format_html(
|
||
# nosemgrep: translation-no-new-style-formatting (new-style only w/ format_html)
|
||
_(
|
||
"{model_name} '{title}' is locked and has been scheduled to go live at {datetime}"
|
||
),
|
||
model_name=self.model_name,
|
||
title=scheduled_revision.object_str,
|
||
datetime=render_timestamp(scheduled_revision.approved_go_live_at),
|
||
)
|
||
return mark_safe(capfirst(message))
|
||
|
||
def get_locked_by(self, user):
|
||
return _("Locked by schedule")
|
||
|
||
def get_description(self, user):
|
||
return _("Currently locked and will go live on the scheduled date")
|