825 lines
29 KiB
Python
825 lines
29 KiB
Python
import json
|
|
|
|
from django.conf import settings
|
|
from django.contrib.admin.utils import quote
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.db import models, transaction
|
|
from django.forms import Media
|
|
from django.shortcuts import get_object_or_404
|
|
from django.template.loader import render_to_string
|
|
from django.urls import reverse
|
|
from django.utils import timezone
|
|
from django.utils.functional import cached_property
|
|
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 import hooks
|
|
from wagtail.admin import messages
|
|
from wagtail.admin.models import EditingSession
|
|
from wagtail.admin.templatetags.wagtailadmin_tags import user_display_name
|
|
from wagtail.admin.ui.editing_sessions import EditingSessionsModule
|
|
from wagtail.admin.ui.tables import TitleColumn
|
|
from wagtail.admin.utils import get_latest_str, set_query_params
|
|
from wagtail.locks import BasicLock, ScheduledForPublishLock, WorkflowLock
|
|
from wagtail.log_actions import log
|
|
from wagtail.log_actions import registry as log_registry
|
|
from wagtail.models import (
|
|
DraftStateMixin,
|
|
Locale,
|
|
LockableMixin,
|
|
PreviewableMixin,
|
|
RevisionMixin,
|
|
TranslatableMixin,
|
|
WorkflowMixin,
|
|
WorkflowState,
|
|
)
|
|
from wagtail.utils.timestamps import render_timestamp
|
|
|
|
|
|
class HookResponseMixin:
|
|
"""
|
|
A mixin for class-based views to run hooks by `hook_name`.
|
|
"""
|
|
|
|
def run_hook(self, hook_name, *args, **kwargs):
|
|
"""
|
|
Run the named hook, passing args and kwargs to each function registered under that hook name.
|
|
If any return an HttpResponse, stop processing and return that response
|
|
"""
|
|
for fn in hooks.get_hooks(hook_name):
|
|
result = fn(*args, **kwargs)
|
|
if hasattr(result, "status_code"):
|
|
return result
|
|
return None
|
|
|
|
|
|
class BeforeAfterHookMixin(HookResponseMixin):
|
|
"""
|
|
A mixin for class-based views to support hooks like `before_edit_page` and
|
|
`after_edit_page`, which are triggered during execution of some operation and
|
|
can return a response to halt that operation and/or change the view response.
|
|
"""
|
|
|
|
def run_before_hook(self):
|
|
"""
|
|
Define how to run the hooks before the operation is executed.
|
|
The `self.run_hook(hook_name, *args, **kwargs)` from HookResponseMixin
|
|
can be utilised to call the hooks.
|
|
|
|
If this method returns a response, the operation will be aborted and the
|
|
hook response will be returned as the view response, skipping the default
|
|
response.
|
|
"""
|
|
return None
|
|
|
|
def run_after_hook(self):
|
|
"""
|
|
Define how to run the hooks after the operation is executed.
|
|
The `self.run_hook(hook_name, *args, **kwargs)` from HookResponseMixin
|
|
can be utilised to call the hooks.
|
|
|
|
If this method returns a response, it will be returned as the view
|
|
response immediately after the operation finishes, skipping the default
|
|
response.
|
|
"""
|
|
return None
|
|
|
|
def dispatch(self, *args, **kwargs):
|
|
hooks_result = self.run_before_hook()
|
|
if hooks_result is not None:
|
|
return hooks_result
|
|
|
|
return super().dispatch(*args, **kwargs)
|
|
|
|
def form_valid(self, form):
|
|
response = super().form_valid(form)
|
|
|
|
hooks_result = self.run_after_hook()
|
|
if hooks_result is not None:
|
|
return hooks_result
|
|
|
|
return response
|
|
|
|
|
|
class LocaleMixin:
|
|
@cached_property
|
|
def locale(self):
|
|
return self.get_locale()
|
|
|
|
@cached_property
|
|
def translations(self):
|
|
return self.get_translations() if self.locale else []
|
|
|
|
def get_locale(self):
|
|
if not getattr(self, "model", None):
|
|
return None
|
|
|
|
i18n_enabled = getattr(settings, "WAGTAIL_I18N_ENABLED", False)
|
|
if not i18n_enabled or not issubclass(self.model, TranslatableMixin):
|
|
return None
|
|
|
|
if hasattr(self, "object") and self.object:
|
|
return self.object.locale
|
|
|
|
selected_locale = self.request.GET.get("locale")
|
|
if selected_locale:
|
|
return get_object_or_404(Locale, language_code=selected_locale)
|
|
return Locale.get_default()
|
|
|
|
def get_translations(self):
|
|
# Return a list of {"locale": Locale, "url": str} objects for available locales
|
|
return []
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
if not self.locale:
|
|
return context
|
|
|
|
context["locale"] = self.locale
|
|
context["translations"] = self.translations
|
|
return context
|
|
|
|
def _set_locale_query_param(self, url, locale=None):
|
|
if not (locale := locale or self.locale):
|
|
return url
|
|
return set_query_params(url, {"locale": locale.language_code})
|
|
|
|
|
|
class PanelMixin:
|
|
panel = None
|
|
|
|
def setup(self, request, *args, **kwargs):
|
|
super().setup(request, *args, **kwargs)
|
|
self.panel = self.get_panel()
|
|
|
|
def get_panel(self):
|
|
return self.panel
|
|
|
|
def get_bound_panel(self, form):
|
|
if not self.panel:
|
|
return None
|
|
return self.panel.get_bound_panel(
|
|
request=self.request, instance=form.instance, form=form
|
|
)
|
|
|
|
def get_form_class(self):
|
|
# The form_class takes precedence if specified
|
|
if self.form_class or not self.panel:
|
|
return super().get_form_class()
|
|
return self.panel.get_form_class()
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
form = context.get("form")
|
|
panel = self.get_bound_panel(form)
|
|
|
|
media = context.get("media", Media())
|
|
if form:
|
|
media += form.media
|
|
if panel:
|
|
media += panel.media
|
|
|
|
context.update(
|
|
{
|
|
"panel": panel,
|
|
"media": media,
|
|
}
|
|
)
|
|
|
|
return context
|
|
|
|
|
|
class IndexViewOptionalFeaturesMixin:
|
|
"""
|
|
A mixin for generic IndexView to support optional features that are applied
|
|
to the model as mixins (e.g. DraftStateMixin, RevisionMixin).
|
|
"""
|
|
|
|
def _get_title_column(self, field_name, column_class=TitleColumn, **kwargs):
|
|
accessor = kwargs.pop("accessor", None)
|
|
|
|
if not accessor and field_name == "__str__":
|
|
accessor = get_latest_str
|
|
|
|
return super()._get_title_column(
|
|
field_name, column_class, accessor=accessor, **kwargs
|
|
)
|
|
|
|
def _annotate_queryset_updated_at(self, queryset):
|
|
if issubclass(queryset.model, RevisionMixin):
|
|
# Use the latest revision's created_at
|
|
queryset = queryset.select_related("latest_revision")
|
|
queryset = queryset.annotate(
|
|
_updated_at=models.F("latest_revision__created_at")
|
|
)
|
|
return queryset
|
|
return super()._annotate_queryset_updated_at(queryset)
|
|
|
|
|
|
class CreateEditViewOptionalFeaturesMixin:
|
|
"""
|
|
A mixin for generic CreateView/EditView to support optional features that
|
|
are applied to the model as mixins (e.g. DraftStateMixin, RevisionMixin).
|
|
"""
|
|
|
|
view_name = "create"
|
|
preview_url_name = None
|
|
lock_url_name = None
|
|
unlock_url_name = None
|
|
revisions_unschedule_url_name = None
|
|
revisions_compare_url_name = None
|
|
workflow_history_url_name = None
|
|
confirm_workflow_cancellation_url_name = None
|
|
|
|
def setup(self, request, *args, **kwargs):
|
|
# Need to set these here as they are used in get_object()
|
|
self.request = request
|
|
self.args = args
|
|
self.kwargs = kwargs
|
|
|
|
self.preview_enabled = self.model and issubclass(self.model, PreviewableMixin)
|
|
self.revision_enabled = self.model and issubclass(self.model, RevisionMixin)
|
|
self.draftstate_enabled = self.model and issubclass(self.model, DraftStateMixin)
|
|
self.locking_enabled = (
|
|
self.model
|
|
and issubclass(self.model, LockableMixin)
|
|
and self.view_name != "create"
|
|
)
|
|
|
|
# Set the object before super().setup() as LocaleMixin.setup() needs it
|
|
self.object = self.get_object()
|
|
self.lock = self.get_lock()
|
|
self.locked_for_user = self.lock and self.lock.for_user(request.user)
|
|
super().setup(request, *args, **kwargs)
|
|
|
|
@cached_property
|
|
def workflow(self):
|
|
if not self.model or not issubclass(self.model, WorkflowMixin):
|
|
return None
|
|
if self.object:
|
|
return self.object.get_workflow()
|
|
return self.model.get_default_workflow()
|
|
|
|
@cached_property
|
|
def workflow_enabled(self):
|
|
return self.workflow is not None
|
|
|
|
@cached_property
|
|
def workflow_state(self):
|
|
if not self.workflow_enabled or not self.object:
|
|
return None
|
|
return (
|
|
self.object.current_workflow_state
|
|
or self.object.workflow_states.order_by("created_at").last()
|
|
)
|
|
|
|
@cached_property
|
|
def current_workflow_task(self):
|
|
if not self.workflow_enabled or not self.object:
|
|
return None
|
|
return self.object.current_workflow_task
|
|
|
|
@cached_property
|
|
def workflow_tasks(self):
|
|
if not self.workflow_state:
|
|
return []
|
|
return self.workflow_state.all_tasks_with_status()
|
|
|
|
def user_has_permission(self, permission):
|
|
user = self.request.user
|
|
# Workflow lock/unlock methods take precedence before the base
|
|
# "lock" and "unlock" permissions -- see PagePermissionTester for reference
|
|
if permission == "lock" and self.current_workflow_task:
|
|
# Follow the logic in PagePermissionTester.user_can_lock()
|
|
# (superusers can always lock)
|
|
if user.is_superuser:
|
|
return True
|
|
return self.current_workflow_task.user_can_lock(self.object, user)
|
|
if permission == "unlock":
|
|
# Follow the logic in PagePermissionTester.user_can_unlock()
|
|
# (superusers can always unlock)
|
|
if user.is_superuser:
|
|
return True
|
|
# Allow unlocking even if the user does not have the 'unlock' permission
|
|
# if they are the user who locked the object
|
|
if self.object.locked_by_id == user.pk:
|
|
return True
|
|
if self.current_workflow_task:
|
|
return self.current_workflow_task.user_can_unlock(self.object, user)
|
|
|
|
# Check with base PermissionCheckedMixin logic
|
|
has_base_permission = super().user_has_permission(permission)
|
|
if has_base_permission:
|
|
return True
|
|
|
|
# Allow access to the editor if the current workflow task allows it,
|
|
# even if the user does not normally have edit access. Users with edit
|
|
# permissions can always edit regardless what this method returns --
|
|
# see Task.user_can_access_editor() for reference
|
|
if (
|
|
permission == "change"
|
|
and self.current_workflow_task
|
|
and self.current_workflow_task.user_can_access_editor(
|
|
self.object, self.request.user
|
|
)
|
|
):
|
|
return True
|
|
|
|
return False
|
|
|
|
def workflow_action_is_valid(self):
|
|
if not self.current_workflow_task:
|
|
return False
|
|
self.workflow_action = self.request.POST.get("workflow-action-name")
|
|
available_actions = self.current_workflow_task.get_actions(
|
|
self.object, self.request.user
|
|
)
|
|
available_action_names = [
|
|
name for name, verbose_name, modal in available_actions
|
|
]
|
|
return self.workflow_action in available_action_names
|
|
|
|
def get_available_actions(self):
|
|
actions = [*super().get_available_actions()]
|
|
|
|
if self.request.method != "POST":
|
|
return actions
|
|
|
|
if self.draftstate_enabled and (
|
|
not self.permission_policy
|
|
or self.permission_policy.user_has_permission(self.request.user, "publish")
|
|
):
|
|
actions.append("publish")
|
|
|
|
if self.workflow_enabled:
|
|
actions.append("submit")
|
|
|
|
if self.workflow_state and (
|
|
self.workflow_state.user_can_cancel(self.request.user)
|
|
):
|
|
actions.append("cancel-workflow")
|
|
if self.object and not self.object.workflow_in_progress:
|
|
actions.append("restart-workflow")
|
|
|
|
if self.workflow_action_is_valid():
|
|
actions.append("workflow-action")
|
|
|
|
return actions
|
|
|
|
def get_object(self, queryset=None):
|
|
if self.view_name == "create":
|
|
return None
|
|
self.live_object = super().get_object(queryset)
|
|
if self.draftstate_enabled:
|
|
return self.live_object.get_latest_revision_as_object()
|
|
return self.live_object
|
|
|
|
def get_lock(self):
|
|
if not self.locking_enabled:
|
|
return None
|
|
return self.object.get_lock()
|
|
|
|
def get_lock_url(self):
|
|
if not self.locking_enabled or not self.lock_url_name:
|
|
return None
|
|
return reverse(self.lock_url_name, args=[quote(self.object.pk)])
|
|
|
|
def get_unlock_url(self):
|
|
if not self.locking_enabled or not self.unlock_url_name:
|
|
return None
|
|
return reverse(self.unlock_url_name, args=[quote(self.object.pk)])
|
|
|
|
def get_preview_url(self):
|
|
if not self.preview_enabled or not self.preview_url_name:
|
|
return None
|
|
args = [] if self.view_name == "create" else [quote(self.object.pk)]
|
|
return reverse(self.preview_url_name, args=args)
|
|
|
|
def get_workflow_history_url(self):
|
|
if not self.workflow_enabled or not self.workflow_history_url_name:
|
|
return None
|
|
return reverse(self.workflow_history_url_name, args=[quote(self.object.pk)])
|
|
|
|
def get_confirm_workflow_cancellation_url(self):
|
|
if not self.workflow_enabled or not self.confirm_workflow_cancellation_url_name:
|
|
return None
|
|
return reverse(
|
|
self.confirm_workflow_cancellation_url_name, args=[quote(self.object.pk)]
|
|
)
|
|
|
|
def get_error_message(self):
|
|
if self.action == "cancel-workflow":
|
|
return None
|
|
if self.locked_for_user:
|
|
return capfirst(
|
|
_("The %(model_name)s could not be saved as it is locked")
|
|
% {"model_name": self.model._meta.verbose_name}
|
|
)
|
|
return super().get_error_message()
|
|
|
|
def get_success_message(self, instance=None):
|
|
object = instance or self.object
|
|
|
|
message = _("%(model_name)s '%(object)s' updated.")
|
|
if self.view_name == "create":
|
|
message = _("%(model_name)s '%(object)s' created.")
|
|
|
|
if self.action == "publish":
|
|
# Scheduled publishing
|
|
if object.go_live_at and object.go_live_at > timezone.now():
|
|
message = _(
|
|
"%(model_name)s '%(object)s' has been scheduled for publishing."
|
|
)
|
|
|
|
if self.view_name == "create":
|
|
message = _(
|
|
"%(model_name)s '%(object)s' created and scheduled for publishing."
|
|
)
|
|
elif object.live:
|
|
message = _(
|
|
"%(model_name)s '%(object)s' is live and this version has been scheduled for publishing."
|
|
)
|
|
|
|
# Immediate publishing
|
|
else:
|
|
message = _("%(model_name)s '%(object)s' updated and published.")
|
|
if self.view_name == "create":
|
|
message = _("%(model_name)s '%(object)s' created and published.")
|
|
|
|
if self.action == "submit":
|
|
message = _(
|
|
"%(model_name)s '%(object)s' has been submitted for moderation."
|
|
)
|
|
|
|
if self.view_name == "create":
|
|
message = _(
|
|
"%(model_name)s '%(object)s' created and submitted for moderation."
|
|
)
|
|
|
|
if self.action == "restart-workflow":
|
|
message = _("Workflow on %(model_name)s '%(object)s' has been restarted.")
|
|
|
|
if self.action == "cancel-workflow":
|
|
message = _("Workflow on %(model_name)s '%(object)s' has been cancelled.")
|
|
|
|
return message % {
|
|
"model_name": capfirst(self.model._meta.verbose_name),
|
|
"object": get_latest_str(object),
|
|
}
|
|
|
|
def get_success_url(self):
|
|
# If DraftStateMixin is enabled and the action is saving a draft
|
|
# or cancelling a workflow, remain on the edit view
|
|
remain_actions = {"create", "edit", "cancel-workflow"}
|
|
if self.draftstate_enabled and self.action in remain_actions:
|
|
return self.get_edit_url()
|
|
return super().get_success_url()
|
|
|
|
def save_instance(self):
|
|
"""
|
|
Called after the form is successfully validated - saves the object to the db
|
|
and returns the new object. Override this to implement custom save logic.
|
|
"""
|
|
if self.draftstate_enabled:
|
|
instance = self.form.save(
|
|
commit=self.view_name == "edit" and not self.object.live
|
|
)
|
|
|
|
# If DraftStateMixin is applied, only save to the database in CreateView,
|
|
# and make sure the live field is set to False.
|
|
if self.view_name == "create":
|
|
instance.live = False
|
|
instance.save()
|
|
self.form.save_m2m()
|
|
else:
|
|
instance = self.form.save()
|
|
|
|
self.has_content_changes = self.view_name == "create" or self.form.has_changed()
|
|
|
|
# Save revision if the model inherits from RevisionMixin
|
|
self.new_revision = None
|
|
if self.revision_enabled:
|
|
self.new_revision = instance.save_revision(user=self.request.user)
|
|
|
|
log(
|
|
instance=instance,
|
|
action="wagtail.create" if self.view_name == "create" else "wagtail.edit",
|
|
revision=self.new_revision,
|
|
content_changed=self.has_content_changes,
|
|
)
|
|
|
|
return instance
|
|
|
|
def publish_action(self):
|
|
hook_response = self.run_hook("before_publish", self.request, self.object)
|
|
if hook_response is not None:
|
|
return hook_response
|
|
|
|
# Skip permission check as it's already done in get_available_actions
|
|
self.new_revision.publish(user=self.request.user, skip_permission_checks=True)
|
|
|
|
hook_response = self.run_hook("after_publish", self.request, self.object)
|
|
if hook_response is not None:
|
|
return hook_response
|
|
|
|
return None
|
|
|
|
def submit_action(self):
|
|
if (
|
|
self.workflow_state
|
|
and self.workflow_state.status == WorkflowState.STATUS_NEEDS_CHANGES
|
|
):
|
|
# If the workflow was in the needs changes state, resume the existing workflow on submission
|
|
self.workflow_state.resume(self.request.user)
|
|
else:
|
|
# Otherwise start a new workflow
|
|
self.workflow.start(self.object, self.request.user)
|
|
|
|
return None
|
|
|
|
def restart_workflow_action(self):
|
|
self.workflow_state.cancel(user=self.request.user)
|
|
self.workflow.start(self.object, self.request.user)
|
|
return None
|
|
|
|
def cancel_workflow_action(self):
|
|
self.workflow_state.cancel(user=self.request.user)
|
|
return None
|
|
|
|
def workflow_action_action(self):
|
|
extra_workflow_data_json = self.request.POST.get(
|
|
"workflow-action-extra-data", "{}"
|
|
)
|
|
extra_workflow_data = json.loads(extra_workflow_data_json)
|
|
self.object.current_workflow_task.on_action(
|
|
self.object.current_workflow_task_state,
|
|
self.request.user,
|
|
self.workflow_action,
|
|
**extra_workflow_data,
|
|
)
|
|
return None
|
|
|
|
def run_action_method(self):
|
|
action_method = getattr(self, self.action.replace("-", "_") + "_action", None)
|
|
if action_method:
|
|
return action_method()
|
|
return None
|
|
|
|
def form_valid(self, form):
|
|
self.form = form
|
|
with transaction.atomic():
|
|
self.object = self.save_instance()
|
|
|
|
response = self.run_action_method()
|
|
if response is not None:
|
|
return response
|
|
|
|
response = self.save_action()
|
|
|
|
hook_response = self.run_after_hook()
|
|
if hook_response is not None:
|
|
return hook_response
|
|
|
|
return response
|
|
|
|
def form_invalid(self, form):
|
|
# Even if the object is locked due to not having permissions,
|
|
# the original submitter can still cancel the workflow
|
|
if self.action == "cancel-workflow":
|
|
self.cancel_workflow_action()
|
|
messages.success(
|
|
self.request,
|
|
self.get_success_message(),
|
|
buttons=self.get_success_buttons(),
|
|
)
|
|
# Refresh the lock object as now WorkflowLock no longer applies
|
|
self.lock = self.get_lock()
|
|
self.locked_for_user = self.lock and self.lock.for_user(self.request.user)
|
|
return super().form_invalid(form)
|
|
|
|
def get_last_updated_info(self):
|
|
# Create view doesn't have last updated info
|
|
if self.view_name == "create":
|
|
return None
|
|
|
|
# DraftStateMixin is applied but object is not live
|
|
if self.draftstate_enabled and not self.object.live:
|
|
return None
|
|
|
|
revision = None
|
|
# DraftStateMixin is applied and object is live
|
|
if self.draftstate_enabled and self.object.live_revision:
|
|
revision = self.object.live_revision
|
|
# RevisionMixin is applied, so object is assumed to be live
|
|
elif self.revision_enabled and self.object.latest_revision:
|
|
revision = self.object.latest_revision
|
|
|
|
# No mixin is applied or no revision exists, fall back to latest log entry
|
|
if not revision:
|
|
return log_registry.get_logs_for_instance(self.object).first()
|
|
|
|
return {
|
|
"timestamp": revision.created_at,
|
|
"user_display_name": user_display_name(revision.user),
|
|
}
|
|
|
|
def get_lock_context(self):
|
|
if not self.locking_enabled:
|
|
return {}
|
|
|
|
user_can_lock = (
|
|
not self.lock or isinstance(self.lock, WorkflowLock)
|
|
) and self.user_has_permission("lock")
|
|
user_can_unlock = (
|
|
isinstance(self.lock, BasicLock)
|
|
) and self.user_has_permission("unlock")
|
|
user_can_unschedule = (
|
|
isinstance(self.lock, ScheduledForPublishLock)
|
|
) and self.user_has_permission("publish")
|
|
|
|
context = {
|
|
"lock": self.lock,
|
|
"locked_for_user": self.locked_for_user,
|
|
"lock_url": self.get_lock_url(),
|
|
"unlock_url": self.get_unlock_url(),
|
|
"user_can_lock": user_can_lock,
|
|
"user_can_unlock": user_can_unlock,
|
|
}
|
|
|
|
# Do not add lock message if the request method is not GET,
|
|
# as POST request may add success/validation error messages already
|
|
if not self.lock or self.request.method != "GET":
|
|
return context
|
|
|
|
lock_message = self.lock.get_message(self.request.user)
|
|
if lock_message:
|
|
if user_can_unlock:
|
|
lock_message = format_html(
|
|
'{} <span class="buttons"><button type="button" class="button button-small button-secondary" data-action="w-action#post" data-controller="w-action" data-w-action-url-value="{}">{}</button></span>',
|
|
lock_message,
|
|
self.get_unlock_url(),
|
|
_("Unlock"),
|
|
)
|
|
|
|
if user_can_unschedule:
|
|
lock_message = format_html(
|
|
'{} <span class="buttons"><button type="button" class="button button-small button-secondary" data-action="w-action#post" data-controller="w-action" data-w-action-url-value="{}">{}</button></span>',
|
|
lock_message,
|
|
reverse(
|
|
self.revisions_unschedule_url_name,
|
|
args=[quote(self.object.pk), self.object.scheduled_revision.id],
|
|
),
|
|
_("Cancel scheduled publish"),
|
|
)
|
|
|
|
if (
|
|
not isinstance(self.lock, ScheduledForPublishLock)
|
|
and self.locked_for_user
|
|
):
|
|
messages.warning(self.request, lock_message, extra_tags="lock")
|
|
else:
|
|
messages.info(self.request, lock_message, extra_tags="lock")
|
|
|
|
return context
|
|
|
|
def get_editing_sessions(self):
|
|
if self.view_name == "create":
|
|
return None
|
|
EditingSession.cleanup()
|
|
content_type = ContentType.objects.get_for_model(self.model)
|
|
session = EditingSession.objects.create(
|
|
user=self.request.user,
|
|
content_type=content_type,
|
|
object_id=self.object.pk,
|
|
last_seen_at=timezone.now(),
|
|
)
|
|
revision_id = self.object.latest_revision_id if self.revision_enabled else None
|
|
return EditingSessionsModule(
|
|
session,
|
|
reverse(
|
|
"wagtailadmin_editing_sessions:ping",
|
|
args=(
|
|
self.model._meta.app_label,
|
|
self.model._meta.model_name,
|
|
quote(self.object.pk),
|
|
session.id,
|
|
),
|
|
),
|
|
reverse(
|
|
"wagtailadmin_editing_sessions:release",
|
|
args=(session.id,),
|
|
),
|
|
[],
|
|
revision_id,
|
|
)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update(self.get_lock_context())
|
|
context["revision_enabled"] = self.revision_enabled
|
|
context["draftstate_enabled"] = self.draftstate_enabled
|
|
context["workflow_enabled"] = self.workflow_enabled
|
|
context["workflow_history_url"] = self.get_workflow_history_url()
|
|
context[
|
|
"confirm_workflow_cancellation_url"
|
|
] = self.get_confirm_workflow_cancellation_url()
|
|
context["publishing_will_cancel_workflow"] = getattr(
|
|
settings, "WAGTAIL_WORKFLOW_CANCEL_ON_PUBLISH", True
|
|
) and bool(self.workflow_tasks)
|
|
context["revisions_compare_url_name"] = self.revisions_compare_url_name
|
|
context["editing_sessions"] = self.get_editing_sessions()
|
|
return context
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
form = self.get_form()
|
|
# Make sure object is not locked
|
|
if not self.locked_for_user and form.is_valid():
|
|
return self.form_valid(form)
|
|
else:
|
|
return self.form_invalid(form)
|
|
|
|
|
|
class RevisionsRevertMixin:
|
|
revision_id_kwarg = "revision_id"
|
|
revisions_revert_url_name = None
|
|
|
|
def setup(self, request, *args, **kwargs):
|
|
self.revision_id = kwargs.get(self.revision_id_kwarg)
|
|
super().setup(request, *args, **kwargs)
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
self._add_warning_message()
|
|
return super().get(request, *args, **kwargs)
|
|
|
|
def get_revisions_revert_url(self):
|
|
return reverse(
|
|
self.revisions_revert_url_name,
|
|
args=[quote(self.object.pk), self.revision_id],
|
|
)
|
|
|
|
def get_warning_message(self):
|
|
user_avatar = render_to_string(
|
|
"wagtailadmin/shared/user_avatar.html", {"user": self.revision.user}
|
|
)
|
|
message_string = _(
|
|
"You are viewing a previous version of this %(model_name)s from <b>%(created_at)s</b> by %(user)s"
|
|
)
|
|
message_data = {
|
|
"model_name": capfirst(self.model._meta.verbose_name),
|
|
"created_at": render_timestamp(self.revision.created_at),
|
|
"user": user_avatar,
|
|
}
|
|
message = mark_safe(message_string % message_data)
|
|
return message
|
|
|
|
def _add_warning_message(self):
|
|
messages.warning(self.request, self.get_warning_message())
|
|
|
|
def get_object(self, queryset=None):
|
|
object = super().get_object(queryset)
|
|
self.revision = get_object_or_404(object.revisions, id=self.revision_id)
|
|
return self.revision.as_object()
|
|
|
|
def save_instance(self):
|
|
commit = not issubclass(self.model, DraftStateMixin) or not self.object.live
|
|
instance = self.form.save(commit=commit)
|
|
|
|
self.has_content_changes = self.form.has_changed()
|
|
|
|
self.new_revision = instance.save_revision(
|
|
user=self.request.user,
|
|
log_action=True,
|
|
previous_revision=self.revision,
|
|
)
|
|
|
|
return instance
|
|
|
|
def get_success_message(self):
|
|
message = _(
|
|
"%(model_name)s '%(object)s' has been replaced with version from %(timestamp)s."
|
|
)
|
|
if self.draftstate_enabled and self.action == "publish":
|
|
message = _(
|
|
"Version from %(timestamp)s of %(model_name)s '%(object)s' has been published."
|
|
)
|
|
|
|
if self.object.go_live_at and self.object.go_live_at > timezone.now():
|
|
message = _(
|
|
"Version from %(timestamp)s of %(model_name)s '%(object)s' has been scheduled for publishing."
|
|
)
|
|
|
|
return message % {
|
|
"model_name": capfirst(self.model._meta.verbose_name),
|
|
"object": self.object,
|
|
"timestamp": render_timestamp(self.revision.created_at),
|
|
}
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["revision"] = self.revision
|
|
context["action_url"] = self.get_revisions_revert_url()
|
|
return context
|