Initial commit
This commit is contained in:
0
env/lib/python3.10/site-packages/wagtail/admin/__init__.py
vendored
Normal file
0
env/lib/python3.10/site-packages/wagtail/admin/__init__.py
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/action_menu.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/action_menu.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/admin_url_finder.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/admin_url_finder.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/apps.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/apps.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/auth.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/auth.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/blocks.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/blocks.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/checks.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/checks.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/compare.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/compare.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/datetimepicker.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/datetimepicker.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/filters.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/filters.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/icons.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/icons.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/jinja2tags.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/jinja2tags.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/localization.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/localization.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/mail.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/mail.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/menu.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/menu.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/messages.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/messages.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/modal_workflow.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/modal_workflow.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/models.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/models.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/navigation.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/navigation.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/search.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/search.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/signal_handlers.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/signal_handlers.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/signals.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/signals.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/site_summary.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/site_summary.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/staticfiles.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/staticfiles.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/userbar.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/userbar.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/utils.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/utils.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/wagtail_hooks.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/__pycache__/wagtail_hooks.cpython-310.pyc
vendored
Normal file
Binary file not shown.
342
env/lib/python3.10/site-packages/wagtail/admin/action_menu.py
vendored
Normal file
342
env/lib/python3.10/site-packages/wagtail/admin/action_menu.py
vendored
Normal file
@@ -0,0 +1,342 @@
|
||||
"""Handles rendering of the list of actions in the footer of the page create/edit views."""
|
||||
from django.conf import settings
|
||||
from django.forms import Media
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from wagtail import hooks
|
||||
from wagtail.admin.ui.components import Component
|
||||
|
||||
|
||||
class ActionMenuItem(Component):
|
||||
"""Defines an item in the actions drop-up on the page creation/edit view"""
|
||||
|
||||
order = 100 # default order index if one is not specified on init
|
||||
template_name = "wagtailadmin/pages/action_menu/menu_item.html"
|
||||
|
||||
label = ""
|
||||
name = None
|
||||
classname = ""
|
||||
icon_name = ""
|
||||
|
||||
def __init__(self, order=None):
|
||||
if order is not None:
|
||||
self.order = order
|
||||
|
||||
def get_user_page_permissions_tester(self, context):
|
||||
if "user_page_permissions_tester" in context:
|
||||
return context["user_page_permissions_tester"]
|
||||
return context["page"].permissions_for_user(context["request"].user)
|
||||
|
||||
def is_shown(self, context):
|
||||
"""
|
||||
Whether this action should be shown on this request; permission checks etc should go here.
|
||||
By default, actions are shown for unlocked pages, hidden for locked pages
|
||||
|
||||
context = dictionary containing at least:
|
||||
'request' = the current request object
|
||||
'view' = 'create', 'edit' or 'revisions_revert'
|
||||
'page' (if view = 'edit' or 'revisions_revert') = the page being edited
|
||||
'parent_page' (if view = 'create') = the parent page of the page being created
|
||||
'lock' = a Lock object if the page is locked, otherwise None
|
||||
'locked_for_user' = True if the lock prevents the current user from editing the page
|
||||
may also contain:
|
||||
'user_page_permissions_tester' = a PagePermissionTester for the current user and page
|
||||
"""
|
||||
return context["view"] == "create" or not context["locked_for_user"]
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
"""Defines context for the template, overridable to use more data"""
|
||||
context = parent_context.copy()
|
||||
url = self.get_url(parent_context)
|
||||
|
||||
context.update(
|
||||
{
|
||||
"label": self.label,
|
||||
"url": url,
|
||||
"name": self.name,
|
||||
"classname": self.classname,
|
||||
"icon_name": self.icon_name,
|
||||
"request": parent_context["request"],
|
||||
}
|
||||
)
|
||||
return context
|
||||
|
||||
def get_url(self, parent_context):
|
||||
return None
|
||||
|
||||
|
||||
class PublishMenuItem(ActionMenuItem):
|
||||
label = _("Publish")
|
||||
name = "action-publish"
|
||||
template_name = "wagtailadmin/pages/action_menu/publish.html"
|
||||
icon_name = "upload"
|
||||
|
||||
def is_shown(self, context):
|
||||
if context["view"] == "create":
|
||||
return (
|
||||
context["parent_page"]
|
||||
.permissions_for_user(context["request"].user)
|
||||
.can_publish_subpage()
|
||||
)
|
||||
else: # view == 'edit' or 'revisions_revert'
|
||||
perms_tester = self.get_user_page_permissions_tester(context)
|
||||
return not context["locked_for_user"] and perms_tester.can_publish()
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
context = super().get_context_data(parent_context)
|
||||
context["is_revision"] = context["view"] == "revisions_revert"
|
||||
return context
|
||||
|
||||
|
||||
class SubmitForModerationMenuItem(ActionMenuItem):
|
||||
label = _("Submit for moderation")
|
||||
name = "action-submit"
|
||||
icon_name = "resubmit"
|
||||
|
||||
def is_shown(self, context):
|
||||
if not getattr(settings, "WAGTAIL_WORKFLOW_ENABLED", True):
|
||||
return False
|
||||
|
||||
if context["view"] == "create":
|
||||
return context["parent_page"].has_workflow
|
||||
|
||||
if context["view"] == "edit":
|
||||
perms_tester = self.get_user_page_permissions_tester(context)
|
||||
return (
|
||||
perms_tester.can_submit_for_moderation()
|
||||
and not context["locked_for_user"]
|
||||
)
|
||||
# context == revisions_revert
|
||||
return False
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
context = super().get_context_data(parent_context)
|
||||
page = context.get("page")
|
||||
workflow_state = page.current_workflow_state if page else None
|
||||
if (
|
||||
workflow_state
|
||||
and workflow_state.status == workflow_state.STATUS_NEEDS_CHANGES
|
||||
):
|
||||
context["label"] = _("Resubmit to %(task_name)s") % {
|
||||
"task_name": workflow_state.current_task_state.task.name
|
||||
}
|
||||
elif page:
|
||||
workflow = page.get_workflow()
|
||||
if workflow:
|
||||
context["label"] = _("Submit to %(workflow_name)s") % {
|
||||
"workflow_name": workflow.name
|
||||
}
|
||||
return context
|
||||
|
||||
|
||||
class WorkflowMenuItem(ActionMenuItem):
|
||||
template_name = "wagtailadmin/pages/action_menu/workflow_menu_item.html"
|
||||
|
||||
def __init__(self, name, label, launch_modal, *args, **kwargs):
|
||||
self.name = name
|
||||
self.label = label
|
||||
self.launch_modal = launch_modal
|
||||
|
||||
if kwargs.get("icon_name"):
|
||||
self.icon_name = kwargs.pop("icon_name")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
context = super().get_context_data(parent_context)
|
||||
context["launch_modal"] = self.launch_modal
|
||||
context["current_task_state"] = context["page"].current_workflow_task_state
|
||||
return context
|
||||
|
||||
def is_shown(self, context):
|
||||
if context["view"] == "edit":
|
||||
return not context["locked_for_user"]
|
||||
|
||||
|
||||
class RestartWorkflowMenuItem(ActionMenuItem):
|
||||
label = _("Restart workflow ")
|
||||
name = "action-restart-workflow"
|
||||
classname = "button--icon-flipped"
|
||||
icon_name = "login"
|
||||
|
||||
def is_shown(self, context):
|
||||
if not getattr(settings, "WAGTAIL_WORKFLOW_ENABLED", True):
|
||||
return False
|
||||
elif context["view"] == "edit":
|
||||
workflow_state = context["page"].current_workflow_state
|
||||
perms_tester = self.get_user_page_permissions_tester(context)
|
||||
return (
|
||||
perms_tester.can_submit_for_moderation()
|
||||
and not context["locked_for_user"]
|
||||
and workflow_state
|
||||
and workflow_state.user_can_cancel(context["request"].user)
|
||||
)
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class CancelWorkflowMenuItem(ActionMenuItem):
|
||||
label = _("Cancel workflow ")
|
||||
name = "action-cancel-workflow"
|
||||
icon_name = "error"
|
||||
|
||||
def is_shown(self, context):
|
||||
if context["view"] == "edit":
|
||||
workflow_state = context["page"].current_workflow_state
|
||||
return workflow_state and workflow_state.user_can_cancel(
|
||||
context["request"].user
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
class UnpublishMenuItem(ActionMenuItem):
|
||||
label = _("Unpublish")
|
||||
name = "action-unpublish"
|
||||
icon_name = "download"
|
||||
classname = "action-secondary"
|
||||
|
||||
def is_shown(self, context):
|
||||
if context["view"] == "edit":
|
||||
perms_tester = self.get_user_page_permissions_tester(context)
|
||||
return not context["locked_for_user"] and perms_tester.can_unpublish()
|
||||
|
||||
def get_url(self, context):
|
||||
return reverse("wagtailadmin_pages:unpublish", args=(context["page"].id,))
|
||||
|
||||
|
||||
class SaveDraftMenuItem(ActionMenuItem):
|
||||
name = "action-save-draft"
|
||||
label = _("Save Draft")
|
||||
template_name = "wagtailadmin/pages/action_menu/save_draft.html"
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
context = super().get_context_data(parent_context)
|
||||
context["is_revision"] = context["view"] == "revisions_revert"
|
||||
return context
|
||||
|
||||
|
||||
class PageLockedMenuItem(ActionMenuItem):
|
||||
name = "action-page-locked"
|
||||
label = _("Page locked")
|
||||
template_name = "wagtailadmin/pages/action_menu/page_locked.html"
|
||||
|
||||
def is_shown(self, context):
|
||||
return "page" in context and context["locked_for_user"]
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
context = super().get_context_data(parent_context)
|
||||
context["is_revision"] = context["view"] == "revisions_revert"
|
||||
return context
|
||||
|
||||
|
||||
BASE_PAGE_ACTION_MENU_ITEMS = None
|
||||
|
||||
|
||||
def _get_base_page_action_menu_items():
|
||||
"""
|
||||
Retrieve the global list of menu items for the page action menu,
|
||||
which may then be customised on a per-request basis
|
||||
"""
|
||||
global BASE_PAGE_ACTION_MENU_ITEMS
|
||||
|
||||
if BASE_PAGE_ACTION_MENU_ITEMS is None:
|
||||
BASE_PAGE_ACTION_MENU_ITEMS = [
|
||||
SaveDraftMenuItem(order=0),
|
||||
UnpublishMenuItem(order=20),
|
||||
PublishMenuItem(order=30),
|
||||
CancelWorkflowMenuItem(order=40),
|
||||
RestartWorkflowMenuItem(order=50),
|
||||
SubmitForModerationMenuItem(order=60),
|
||||
PageLockedMenuItem(order=10000),
|
||||
]
|
||||
for hook in hooks.get_hooks("register_page_action_menu_item"):
|
||||
action_menu_item = hook()
|
||||
if action_menu_item:
|
||||
BASE_PAGE_ACTION_MENU_ITEMS.append(action_menu_item)
|
||||
|
||||
return BASE_PAGE_ACTION_MENU_ITEMS
|
||||
|
||||
|
||||
class PageActionMenu:
|
||||
template = "wagtailadmin/pages/action_menu/menu.html"
|
||||
|
||||
def __init__(self, request, **kwargs):
|
||||
self.request = request
|
||||
self.context = kwargs
|
||||
self.context["request"] = request
|
||||
page = self.context.get("page")
|
||||
if page:
|
||||
self.context["user_page_permissions_tester"] = page.permissions_for_user(
|
||||
self.request.user
|
||||
)
|
||||
|
||||
self.menu_items = []
|
||||
|
||||
if page:
|
||||
task = page.current_workflow_task
|
||||
current_workflow_state = page.current_workflow_state
|
||||
is_final_task = (
|
||||
current_workflow_state and current_workflow_state.is_at_final_task
|
||||
)
|
||||
if task:
|
||||
actions = task.get_actions(page, request.user)
|
||||
workflow_menu_items = []
|
||||
for name, label, launch_modal in actions:
|
||||
icon_name = "edit"
|
||||
if name == "approve":
|
||||
if is_final_task and not getattr(
|
||||
settings,
|
||||
"WAGTAIL_WORKFLOW_REQUIRE_REAPPROVAL_ON_EDIT",
|
||||
False,
|
||||
):
|
||||
label = _("%(label)s and Publish") % {"label": label}
|
||||
icon_name = "success"
|
||||
|
||||
item = WorkflowMenuItem(
|
||||
name, label, launch_modal, icon_name=icon_name
|
||||
)
|
||||
|
||||
if item.is_shown(self.context):
|
||||
workflow_menu_items.append(item)
|
||||
self.menu_items.extend(workflow_menu_items)
|
||||
|
||||
for menu_item in _get_base_page_action_menu_items():
|
||||
if menu_item.is_shown(self.context):
|
||||
self.menu_items.append(menu_item)
|
||||
|
||||
self.menu_items.sort(key=lambda item: item.order)
|
||||
|
||||
for hook in hooks.get_hooks("construct_page_action_menu"):
|
||||
hook(self.menu_items, self.request, self.context)
|
||||
|
||||
try:
|
||||
self.default_item = self.menu_items.pop(0)
|
||||
except IndexError:
|
||||
self.default_item = None
|
||||
|
||||
def render_html(self):
|
||||
rendered_menu_items = [
|
||||
menu_item.render_html(self.context) for menu_item in self.menu_items
|
||||
]
|
||||
|
||||
rendered_default_item = self.default_item.render_html(self.context)
|
||||
|
||||
return render_to_string(
|
||||
self.template,
|
||||
{
|
||||
"default_menu_item": rendered_default_item,
|
||||
"show_menu": bool(self.menu_items),
|
||||
"rendered_menu_items": rendered_menu_items,
|
||||
},
|
||||
request=self.request,
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def media(self):
|
||||
media = Media()
|
||||
for item in self.menu_items:
|
||||
media += item.media
|
||||
return media
|
||||
107
env/lib/python3.10/site-packages/wagtail/admin/admin_url_finder.py
vendored
Normal file
107
env/lib/python3.10/site-packages/wagtail/admin/admin_url_finder.py
vendored
Normal file
@@ -0,0 +1,107 @@
|
||||
from django.contrib.admin.utils import quote
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.urls import reverse
|
||||
|
||||
from wagtail.hooks import search_for_hooks
|
||||
from wagtail.utils.registry import ObjectTypeRegistry
|
||||
|
||||
"""
|
||||
A mechanism for finding the admin edit URL for an arbitrary object instance, optionally applying
|
||||
permission checks.
|
||||
|
||||
url_finder = AdminURLFinder(request.user)
|
||||
url_finder.get_edit_url(some_page) # => "/admin/pages/123/edit/"
|
||||
url_finder.get_edit_url(some_image) # => "/admin/images/456/"
|
||||
url_finder.get_edit_url(some_site) # => None (user does not have edit permission for sites)
|
||||
|
||||
If the user parameter is omitted, edit URLs are returned without considering permissions.
|
||||
|
||||
Handlers for new models can be registered via register_admin_url_finder:
|
||||
|
||||
class SprocketAdminURLFinder(ModelAdminURLFinder):
|
||||
edit_url_name = 'wagtailsprockets:edit'
|
||||
|
||||
register_admin_url_finder(Sprocket, SprocketAdminURLFinder)
|
||||
"""
|
||||
|
||||
|
||||
class ModelAdminURLFinder:
|
||||
"""
|
||||
Handles admin edit URL lookups for an individual model
|
||||
"""
|
||||
|
||||
edit_url_name = None
|
||||
permission_policy = None
|
||||
|
||||
def __init__(self, user=None):
|
||||
self.user = user
|
||||
|
||||
def construct_edit_url(self, instance):
|
||||
"""
|
||||
Return the edit URL for the given instance - regardless of whether the user can access it -
|
||||
or None if no edit URL is available.
|
||||
"""
|
||||
if self.edit_url_name is None:
|
||||
raise ImproperlyConfigured(
|
||||
"%r must define edit_url_name or override construct_edit_url"
|
||||
% type(self)
|
||||
)
|
||||
return reverse(self.edit_url_name, args=(quote(instance.pk),))
|
||||
|
||||
def get_edit_url(self, instance):
|
||||
"""
|
||||
Return the edit URL for the given instance if one exists and the user has permission for it,
|
||||
or None otherwise.
|
||||
"""
|
||||
if (
|
||||
self.user
|
||||
and self.permission_policy
|
||||
and not self.permission_policy.user_has_permission_for_instance(
|
||||
self.user, "change", instance
|
||||
)
|
||||
):
|
||||
return None
|
||||
else:
|
||||
return self.construct_edit_url(instance)
|
||||
|
||||
|
||||
class NullAdminURLFinder:
|
||||
"""
|
||||
A dummy AdminURLFinder that always returns None
|
||||
"""
|
||||
|
||||
def __init__(self, user=None):
|
||||
pass
|
||||
|
||||
def get_edit_url(self, instance):
|
||||
return None
|
||||
|
||||
|
||||
finder_classes = ObjectTypeRegistry()
|
||||
|
||||
|
||||
def register_admin_url_finder(model, handler):
|
||||
finder_classes.register(model, value=handler)
|
||||
|
||||
|
||||
class AdminURLFinder:
|
||||
"""
|
||||
The 'main' admin URL finder, which searches across all registered models
|
||||
"""
|
||||
|
||||
def __init__(self, user=None):
|
||||
search_for_hooks() # ensure wagtail_hooks files have been loaded
|
||||
self.user = user
|
||||
self.finders_by_model = {}
|
||||
|
||||
def get_edit_url(self, instance):
|
||||
model = type(instance)
|
||||
try:
|
||||
# do we already have a finder for this model and user?
|
||||
finder = self.finders_by_model[model]
|
||||
except KeyError:
|
||||
finder_class = finder_classes.get(instance) or NullAdminURLFinder
|
||||
finder = finder_class(self.user)
|
||||
self.finders_by_model[model] = finder
|
||||
|
||||
return finder.get_edit_url(instance)
|
||||
0
env/lib/python3.10/site-packages/wagtail/admin/api/__init__.py
vendored
Normal file
0
env/lib/python3.10/site-packages/wagtail/admin/api/__init__.py
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/__pycache__/filters.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/__pycache__/filters.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/__pycache__/serializers.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/__pycache__/serializers.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/__pycache__/urls.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/__pycache__/urls.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/__pycache__/views.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/__pycache__/views.cpython-310.pyc
vendored
Normal file
Binary file not shown.
0
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__init__.py
vendored
Normal file
0
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__init__.py
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__pycache__/base.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__pycache__/base.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__pycache__/convert_alias.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__pycache__/convert_alias.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__pycache__/copy.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__pycache__/copy.cpython-310.pyc
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__pycache__/create_alias.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__pycache__/create_alias.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__pycache__/delete.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__pycache__/delete.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__pycache__/move.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__pycache__/move.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__pycache__/publish.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__pycache__/publish.cpython-310.pyc
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__pycache__/unpublish.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/api/actions/__pycache__/unpublish.cpython-310.pyc
vendored
Normal file
Binary file not shown.
6
env/lib/python3.10/site-packages/wagtail/admin/api/actions/base.py
vendored
Normal file
6
env/lib/python3.10/site-packages/wagtail/admin/api/actions/base.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
class APIAction:
|
||||
serializer = None
|
||||
|
||||
def __init__(self, view, request):
|
||||
self.view = view
|
||||
self.request = request
|
||||
30
env/lib/python3.10/site-packages/wagtail/admin/api/actions/convert_alias.py
vendored
Normal file
30
env/lib/python3.10/site-packages/wagtail/admin/api/actions/convert_alias.py
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from rest_framework import status
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from wagtail.actions.convert_alias import ConvertAliasPageAction, ConvertAliasPageError
|
||||
from wagtail.api.v2.utils import BadRequestError
|
||||
|
||||
from .base import APIAction
|
||||
|
||||
|
||||
class ConvertAliasPageAPIAction(APIAction):
|
||||
serializer = Serializer
|
||||
|
||||
def _action_from_data(self, instance, data):
|
||||
return ConvertAliasPageAction(instance, user=self.request.user)
|
||||
|
||||
def execute(self, instance, data):
|
||||
action = self._action_from_data(instance, data)
|
||||
|
||||
try:
|
||||
new_page = action.execute()
|
||||
except DjangoValidationError as e:
|
||||
raise ValidationError(e.message_dict)
|
||||
except ConvertAliasPageError as e:
|
||||
raise BadRequestError(e.args[0])
|
||||
|
||||
serializer = self.view.get_serializer(new_page)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
67
env/lib/python3.10/site-packages/wagtail/admin/api/actions/copy.py
vendored
Normal file
67
env/lib/python3.10/site-packages/wagtail/admin/api/actions/copy.py
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import fields, status
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from wagtail.actions.copy_page import CopyPageAction, CopyPageIntegrityError
|
||||
from wagtail.api.v2.utils import BadRequestError
|
||||
from wagtail.coreutils import find_available_slug
|
||||
from wagtail.models import Page
|
||||
|
||||
from .base import APIAction
|
||||
|
||||
|
||||
class CopyPageAPIActionSerializer(Serializer):
|
||||
# Note: CopyPageAction will validate the destination page
|
||||
destination_page_id = fields.IntegerField(required=False)
|
||||
recursive = fields.BooleanField(default=False, required=False)
|
||||
keep_live = fields.BooleanField(default=True, required=False)
|
||||
slug = fields.CharField(required=False)
|
||||
title = fields.CharField(required=False)
|
||||
|
||||
|
||||
class CopyPageAPIAction(APIAction):
|
||||
serializer = CopyPageAPIActionSerializer
|
||||
|
||||
def _action_from_data(self, instance, data):
|
||||
destination_page_id = data.get("destination_page_id")
|
||||
if destination_page_id is None:
|
||||
destination = instance.get_parent()
|
||||
else:
|
||||
destination = get_object_or_404(Page, id=destination_page_id)
|
||||
|
||||
update_attrs = {}
|
||||
if "slug" in data:
|
||||
update_attrs["slug"] = data["slug"]
|
||||
else:
|
||||
# If user didn't specify a particular slug, find an available one
|
||||
available_slug = find_available_slug(destination, instance.slug)
|
||||
if available_slug != instance.slug:
|
||||
update_attrs["slug"] = available_slug
|
||||
|
||||
if "title" in data:
|
||||
update_attrs["title"] = data["title"]
|
||||
|
||||
return CopyPageAction(
|
||||
page=instance,
|
||||
to=destination,
|
||||
recursive=data["recursive"],
|
||||
keep_live=data["keep_live"],
|
||||
update_attrs=update_attrs,
|
||||
user=self.request.user,
|
||||
)
|
||||
|
||||
def execute(self, instance, data):
|
||||
action = self._action_from_data(instance, data)
|
||||
|
||||
try:
|
||||
new_page = action.execute()
|
||||
except DjangoValidationError as e:
|
||||
raise ValidationError(e.message_dict)
|
||||
except CopyPageIntegrityError as e:
|
||||
raise BadRequestError(e.args[0])
|
||||
|
||||
serializer = self.view.get_serializer(new_page)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
51
env/lib/python3.10/site-packages/wagtail/admin/api/actions/copy_for_translation.py
vendored
Normal file
51
env/lib/python3.10/site-packages/wagtail/admin/api/actions/copy_for_translation.py
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import fields, status
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from wagtail.actions.copy_for_translation import (
|
||||
CopyPageForTranslationAction,
|
||||
ParentNotTranslatedError,
|
||||
)
|
||||
from wagtail.api.v2.utils import BadRequestError
|
||||
from wagtail.models.i18n import Locale
|
||||
|
||||
from .base import APIAction
|
||||
|
||||
|
||||
class CopyForTranslationAPIActionSerializer(Serializer):
|
||||
locale = fields.CharField(max_length=100)
|
||||
copy_parents = fields.BooleanField(default=False, required=False)
|
||||
alias = fields.BooleanField(default=False, required=False)
|
||||
recursive = fields.BooleanField(default=False, required=False)
|
||||
|
||||
|
||||
class CopyForTranslationAPIAction(APIAction):
|
||||
serializer = CopyForTranslationAPIActionSerializer
|
||||
|
||||
def _action_from_data(self, instance, data):
|
||||
locale = get_object_or_404(Locale, language_code=data["locale"])
|
||||
|
||||
return CopyPageForTranslationAction(
|
||||
page=instance,
|
||||
locale=locale,
|
||||
copy_parents=data["copy_parents"],
|
||||
alias=data["alias"],
|
||||
user=self.request.user,
|
||||
include_subtree=data["recursive"],
|
||||
)
|
||||
|
||||
def execute(self, instance, data):
|
||||
action = self._action_from_data(instance, data)
|
||||
|
||||
try:
|
||||
translated_page = action.execute()
|
||||
except DjangoValidationError as e:
|
||||
raise ValidationError(e.message_dict)
|
||||
except ParentNotTranslatedError as e:
|
||||
raise BadRequestError(e.args[0])
|
||||
|
||||
serializer = self.view.get_serializer(translated_page)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
51
env/lib/python3.10/site-packages/wagtail/admin/api/actions/create_alias.py
vendored
Normal file
51
env/lib/python3.10/site-packages/wagtail/admin/api/actions/create_alias.py
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import fields, status
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from wagtail.actions.create_alias import (
|
||||
CreatePageAliasAction,
|
||||
CreatePageAliasIntegrityError,
|
||||
)
|
||||
from wagtail.api.v2.utils import BadRequestError
|
||||
from wagtail.models import Page
|
||||
|
||||
from .base import APIAction
|
||||
|
||||
|
||||
class CreatePageAliasAPIActionSerializer(Serializer):
|
||||
destination_page_id = fields.IntegerField(required=False)
|
||||
recursive = fields.BooleanField(default=False, required=False)
|
||||
update_slug = fields.CharField(required=False)
|
||||
|
||||
|
||||
class CreatePageAliasAPIAction(APIAction):
|
||||
serializer = CreatePageAliasAPIActionSerializer
|
||||
|
||||
def _action_from_data(self, instance, data):
|
||||
parent, destination_page_id = None, data.get("destination_page_id")
|
||||
if destination_page_id:
|
||||
parent = get_object_or_404(Page, id=destination_page_id).specific
|
||||
|
||||
return CreatePageAliasAction(
|
||||
page=instance,
|
||||
recursive=data["recursive"],
|
||||
parent=parent,
|
||||
update_slug=data.get("update_slug"),
|
||||
user=self.request.user,
|
||||
)
|
||||
|
||||
def execute(self, instance, data):
|
||||
action = self._action_from_data(instance, data)
|
||||
|
||||
try:
|
||||
new_page = action.execute()
|
||||
except DjangoValidationError as e:
|
||||
raise ValidationError(e.message_dict)
|
||||
except CreatePageAliasIntegrityError as e:
|
||||
raise BadRequestError(e.args[0])
|
||||
|
||||
serializer = self.view.get_serializer(new_page)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
26
env/lib/python3.10/site-packages/wagtail/admin/api/actions/delete.py
vendored
Normal file
26
env/lib/python3.10/site-packages/wagtail/admin/api/actions/delete.py
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from rest_framework import status
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from wagtail.actions.delete_page import DeletePageAction
|
||||
|
||||
from .base import APIAction
|
||||
|
||||
|
||||
class DeletePageAPIAction(APIAction):
|
||||
serializer = Serializer
|
||||
|
||||
def _action_from_data(self, instance, data):
|
||||
return DeletePageAction(page=instance, user=self.request.user)
|
||||
|
||||
def execute(self, instance, data):
|
||||
action = self._action_from_data(instance, data)
|
||||
|
||||
try:
|
||||
action.execute()
|
||||
except DjangoValidationError as e:
|
||||
raise ValidationError(e.message_dict)
|
||||
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
53
env/lib/python3.10/site-packages/wagtail/admin/api/actions/move.py
vendored
Normal file
53
env/lib/python3.10/site-packages/wagtail/admin/api/actions/move.py
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import fields, status
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from wagtail.actions.move_page import MovePageAction
|
||||
from wagtail.models import Page
|
||||
|
||||
from .base import APIAction
|
||||
|
||||
|
||||
class MovePageAPIActionSerializer(Serializer):
|
||||
destination_page_id = fields.IntegerField(required=True)
|
||||
position = fields.ChoiceField(
|
||||
required=False,
|
||||
choices=[
|
||||
"left",
|
||||
"right",
|
||||
"first-child",
|
||||
"last-child",
|
||||
"first-sibling",
|
||||
"last-sibling",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class MovePageAPIAction(APIAction):
|
||||
serializer = MovePageAPIActionSerializer
|
||||
|
||||
def _action_from_data(self, instance, data):
|
||||
destination_page_id = data["destination_page_id"]
|
||||
target = get_object_or_404(Page, id=destination_page_id)
|
||||
|
||||
return MovePageAction(
|
||||
page=instance,
|
||||
target=target,
|
||||
pos=data.get("position"),
|
||||
user=self.request.user,
|
||||
)
|
||||
|
||||
def execute(self, instance, data):
|
||||
action = self._action_from_data(instance, data)
|
||||
|
||||
try:
|
||||
action.execute()
|
||||
except DjangoValidationError as e:
|
||||
raise ValidationError(e.message_dict)
|
||||
|
||||
instance.refresh_from_db()
|
||||
serializer = self.view.get_serializer(instance)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
34
env/lib/python3.10/site-packages/wagtail/admin/api/actions/publish.py
vendored
Normal file
34
env/lib/python3.10/site-packages/wagtail/admin/api/actions/publish.py
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from rest_framework import status
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from wagtail.actions.publish_page_revision import PublishPageRevisionAction
|
||||
from wagtail.api.v2.utils import BadRequestError
|
||||
|
||||
from .base import APIAction
|
||||
|
||||
|
||||
class PublishPageAPIAction(APIAction):
|
||||
serializer = Serializer
|
||||
|
||||
def _action_from_data(self, instance, data):
|
||||
user = self.request.user
|
||||
revision = instance.get_latest_revision() or instance.save_revision(user=user)
|
||||
return PublishPageRevisionAction(revision, user=user)
|
||||
|
||||
def execute(self, instance, data):
|
||||
try:
|
||||
action = self._action_from_data(instance, data)
|
||||
except RuntimeError as e:
|
||||
raise BadRequestError(e.args[0])
|
||||
|
||||
try:
|
||||
action.execute()
|
||||
except DjangoValidationError as e:
|
||||
raise ValidationError(e.message_dict)
|
||||
|
||||
new_page = instance.specific_class.objects.get(pk=instance.pk)
|
||||
serializer = self.view.get_serializer(new_page)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
42
env/lib/python3.10/site-packages/wagtail/admin/api/actions/revert_to_page_revision.py
vendored
Normal file
42
env/lib/python3.10/site-packages/wagtail/admin/api/actions/revert_to_page_revision.py
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import fields, status
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from wagtail.actions.revert_to_page_revision import (
|
||||
RevertToPageRevisionAction,
|
||||
RevertToPageRevisionError,
|
||||
)
|
||||
from wagtail.api.v2.utils import BadRequestError
|
||||
|
||||
from .base import APIAction
|
||||
|
||||
|
||||
class RevertToPageRevisionAPIActionSerializer(Serializer):
|
||||
revision_id = fields.IntegerField()
|
||||
|
||||
|
||||
class RevertToPageRevisionAPIAction(APIAction):
|
||||
serializer = RevertToPageRevisionAPIActionSerializer
|
||||
|
||||
def _action_from_data(self, instance, data):
|
||||
revision = get_object_or_404(instance.revisions, id=data["revision_id"])
|
||||
|
||||
return RevertToPageRevisionAction(
|
||||
page=instance, revision=revision, user=self.request.user
|
||||
)
|
||||
|
||||
def execute(self, instance, data):
|
||||
action = self._action_from_data(instance, data)
|
||||
|
||||
try:
|
||||
new_revision = action.execute()
|
||||
except DjangoValidationError as e:
|
||||
raise ValidationError(e.message_dict)
|
||||
except RevertToPageRevisionError as e:
|
||||
raise BadRequestError(e.args[0])
|
||||
|
||||
serializer = self.view.get_serializer(new_revision.as_object())
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
35
env/lib/python3.10/site-packages/wagtail/admin/api/actions/unpublish.py
vendored
Normal file
35
env/lib/python3.10/site-packages/wagtail/admin/api/actions/unpublish.py
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||
from rest_framework import fields, status
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from wagtail.actions.unpublish_page import UnpublishPageAction
|
||||
|
||||
from .base import APIAction
|
||||
|
||||
|
||||
class UnpublishPageAPIActionSerializer(Serializer):
|
||||
recursive = fields.BooleanField(default=False, required=False)
|
||||
|
||||
|
||||
class UnpublishPageAPIAction(APIAction):
|
||||
serializer = UnpublishPageAPIActionSerializer
|
||||
|
||||
def _action_from_data(self, instance, data):
|
||||
return UnpublishPageAction(
|
||||
page=instance,
|
||||
user=self.request.user,
|
||||
include_descendants=data["recursive"],
|
||||
)
|
||||
|
||||
def execute(self, instance, data):
|
||||
action = self._action_from_data(instance, data)
|
||||
|
||||
try:
|
||||
action.execute()
|
||||
except DjangoValidationError as e:
|
||||
raise ValidationError(e.message_dict)
|
||||
|
||||
serializer = self.view.get_serializer(instance)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
45
env/lib/python3.10/site-packages/wagtail/admin/api/filters.py
vendored
Normal file
45
env/lib/python3.10/site-packages/wagtail/admin/api/filters.py
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
from rest_framework.filters import BaseFilterBackend
|
||||
|
||||
from wagtail import hooks
|
||||
from wagtail.api.v2.utils import BadRequestError, parse_boolean
|
||||
from wagtail.permissions import page_permission_policy
|
||||
|
||||
|
||||
class HasChildrenFilter(BaseFilterBackend):
|
||||
"""
|
||||
Filters the queryset by checking if the pages have children or not.
|
||||
This is useful when you want to get just the branches or just the leaves.
|
||||
"""
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
if "has_children" in request.GET:
|
||||
try:
|
||||
has_children_filter = parse_boolean(request.GET["has_children"])
|
||||
except ValueError:
|
||||
raise BadRequestError("has_children must be 'true' or 'false'")
|
||||
|
||||
if has_children_filter is True:
|
||||
return queryset.filter(numchild__gt=0)
|
||||
else:
|
||||
return queryset.filter(numchild=0)
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class ForExplorerFilter(BaseFilterBackend):
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
if request.GET.get("for_explorer"):
|
||||
if not hasattr(queryset, "_filtered_by_child_of"):
|
||||
raise BadRequestError(
|
||||
"filtering by for_explorer without child_of is not supported"
|
||||
)
|
||||
|
||||
parent_page = queryset._filtered_by_child_of
|
||||
for hook in hooks.get_hooks("construct_explorer_page_queryset"):
|
||||
queryset = hook(parent_page, queryset, request)
|
||||
|
||||
queryset = (
|
||||
page_permission_policy.explorable_instances(request.user) & queryset
|
||||
)
|
||||
|
||||
return queryset
|
||||
192
env/lib/python3.10/site-packages/wagtail/admin/api/serializers.py
vendored
Normal file
192
env/lib/python3.10/site-packages/wagtail/admin/api/serializers.py
vendored
Normal file
@@ -0,0 +1,192 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from rest_framework.fields import Field, ReadOnlyField
|
||||
|
||||
from wagtail.api.v2.serializers import PageSerializer, get_serializer_class
|
||||
from wagtail.api.v2.utils import get_full_url
|
||||
from wagtail.models import Page
|
||||
|
||||
|
||||
def get_model_listing_url(context, model):
|
||||
url_path = context["router"].get_model_listing_urlpath(model)
|
||||
|
||||
if url_path:
|
||||
return get_full_url(context["request"], url_path)
|
||||
|
||||
|
||||
class PageStatusField(Field):
|
||||
"""
|
||||
Serializes the "status" field.
|
||||
|
||||
Example:
|
||||
"status": {
|
||||
"status": "live",
|
||||
"live": true,
|
||||
"has_unpublished_changes": false
|
||||
},
|
||||
"""
|
||||
|
||||
def get_attribute(self, instance):
|
||||
return instance
|
||||
|
||||
def to_representation(self, page):
|
||||
return OrderedDict(
|
||||
[
|
||||
("status", page.status_string),
|
||||
("live", page.live),
|
||||
("has_unpublished_changes", page.has_unpublished_changes),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class PageChildrenField(Field):
|
||||
"""
|
||||
Serializes the "children" field.
|
||||
|
||||
Example:
|
||||
"children": {
|
||||
"count": 1,
|
||||
"listing_url": "/api/v1/pages/?child_of=2"
|
||||
}
|
||||
"""
|
||||
|
||||
def get_attribute(self, instance):
|
||||
return instance
|
||||
|
||||
def to_representation(self, page):
|
||||
return OrderedDict(
|
||||
[
|
||||
("count", self.context["base_queryset"].child_of(page).count()),
|
||||
(
|
||||
"listing_url",
|
||||
get_model_listing_url(self.context, Page)
|
||||
+ "?child_of="
|
||||
+ str(page.id),
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class PageDescendantsField(Field):
|
||||
"""
|
||||
Serializes the "descendants" field.
|
||||
|
||||
Example:
|
||||
"descendants": {
|
||||
"count": 10,
|
||||
"listing_url": "/api/v1/pages/?descendant_of=2"
|
||||
}
|
||||
"""
|
||||
|
||||
def get_attribute(self, instance):
|
||||
return instance
|
||||
|
||||
def to_representation(self, page):
|
||||
return OrderedDict(
|
||||
[
|
||||
("count", self.context["base_queryset"].descendant_of(page).count()),
|
||||
(
|
||||
"listing_url",
|
||||
get_model_listing_url(self.context, Page)
|
||||
+ "?descendant_of="
|
||||
+ str(page.id),
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class PageAncestorsField(Field):
|
||||
"""
|
||||
Serializes the page's ancestry.
|
||||
|
||||
Example:
|
||||
"ancestry": [
|
||||
{
|
||||
"id": 1,
|
||||
"meta": {
|
||||
"type": "wagtailcore.Page",
|
||||
"detail_url": "/api/v1/pages/1/"
|
||||
},
|
||||
"title": "Root"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"meta": {
|
||||
"type": "home.HomePage",
|
||||
"detail_url": "/api/v1/pages/2/"
|
||||
},
|
||||
"title": "Home"
|
||||
}
|
||||
]
|
||||
"""
|
||||
|
||||
def get_attribute(self, instance):
|
||||
return instance
|
||||
|
||||
def to_representation(self, page):
|
||||
serializer_class = get_serializer_class(
|
||||
Page,
|
||||
["id", "type", "detail_url", "html_url", "title", "admin_display_title"],
|
||||
meta_fields=["type", "detail_url", "html_url"],
|
||||
base=AdminPageSerializer,
|
||||
)
|
||||
serializer = serializer_class(context=self.context, many=True)
|
||||
return serializer.to_representation(page.get_ancestors())
|
||||
|
||||
|
||||
class PageTranslationsField(Field):
|
||||
"""
|
||||
Serializes the page's translations.
|
||||
|
||||
Example:
|
||||
"translations": [
|
||||
{
|
||||
"id": 1,
|
||||
"meta": {
|
||||
"type": "home.HomePage",
|
||||
"detail_url": "/api/v1/pages/1/",
|
||||
"locale": "es"
|
||||
},
|
||||
"title": "Casa"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"meta": {
|
||||
"type": "home.HomePage",
|
||||
"detail_url": "/api/v1/pages/2/",
|
||||
"locale": "fr"
|
||||
},
|
||||
"title": "Maison"
|
||||
}
|
||||
]
|
||||
"""
|
||||
|
||||
def get_attribute(self, instance):
|
||||
return instance
|
||||
|
||||
def to_representation(self, page):
|
||||
serializer_class = get_serializer_class(
|
||||
Page,
|
||||
[
|
||||
"id",
|
||||
"type",
|
||||
"detail_url",
|
||||
"html_url",
|
||||
"locale",
|
||||
"title",
|
||||
"admin_display_title",
|
||||
],
|
||||
meta_fields=["type", "detail_url", "html_url", "locale"],
|
||||
base=AdminPageSerializer,
|
||||
)
|
||||
serializer = serializer_class(context=self.context, many=True)
|
||||
return serializer.to_representation(page.get_translations())
|
||||
|
||||
|
||||
class AdminPageSerializer(PageSerializer):
|
||||
status = PageStatusField(read_only=True)
|
||||
children = PageChildrenField(read_only=True)
|
||||
descendants = PageDescendantsField(read_only=True)
|
||||
ancestors = PageAncestorsField(read_only=True)
|
||||
translations = PageTranslationsField(read_only=True)
|
||||
admin_display_title = ReadOnlyField(source="get_admin_display_title")
|
||||
16
env/lib/python3.10/site-packages/wagtail/admin/api/urls.py
vendored
Normal file
16
env/lib/python3.10/site-packages/wagtail/admin/api/urls.py
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.urls import path
|
||||
|
||||
from wagtail import hooks
|
||||
from wagtail.api.v2.router import WagtailAPIRouter
|
||||
|
||||
from .views import PagesAdminAPIViewSet
|
||||
|
||||
admin_api = WagtailAPIRouter("wagtailadmin_api")
|
||||
admin_api.register_endpoint("pages", PagesAdminAPIViewSet)
|
||||
|
||||
for fn in hooks.get_hooks("construct_admin_api"):
|
||||
fn(admin_api)
|
||||
|
||||
urlpatterns = [
|
||||
path("main/", admin_api.urls),
|
||||
]
|
||||
161
env/lib/python3.10/site-packages/wagtail/admin/api/views.py
vendored
Normal file
161
env/lib/python3.10/site-packages/wagtail/admin/api/views.py
vendored
Normal file
@@ -0,0 +1,161 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import Http404
|
||||
from django.urls import path
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.response import Response
|
||||
|
||||
from wagtail.api.v2.views import PagesAPIViewSet
|
||||
from wagtail.models import Page
|
||||
|
||||
from .actions.convert_alias import ConvertAliasPageAPIAction
|
||||
from .actions.copy import CopyPageAPIAction
|
||||
from .actions.copy_for_translation import CopyForTranslationAPIAction
|
||||
from .actions.create_alias import CreatePageAliasAPIAction
|
||||
from .actions.delete import DeletePageAPIAction
|
||||
from .actions.move import MovePageAPIAction
|
||||
from .actions.publish import PublishPageAPIAction
|
||||
from .actions.revert_to_page_revision import RevertToPageRevisionAPIAction
|
||||
from .actions.unpublish import UnpublishPageAPIAction
|
||||
from .filters import ForExplorerFilter, HasChildrenFilter
|
||||
from .serializers import AdminPageSerializer
|
||||
|
||||
|
||||
class PagesAdminAPIViewSet(PagesAPIViewSet):
|
||||
base_serializer_class = AdminPageSerializer
|
||||
authentication_classes = [SessionAuthentication]
|
||||
|
||||
actions = {
|
||||
"convert_alias": ConvertAliasPageAPIAction,
|
||||
"copy": CopyPageAPIAction,
|
||||
"delete": DeletePageAPIAction,
|
||||
"publish": PublishPageAPIAction,
|
||||
"unpublish": UnpublishPageAPIAction,
|
||||
"move": MovePageAPIAction,
|
||||
"copy_for_translation": CopyForTranslationAPIAction,
|
||||
"create_alias": CreatePageAliasAPIAction,
|
||||
"revert_to_page_revision": RevertToPageRevisionAPIAction,
|
||||
}
|
||||
|
||||
# Add has_children and for_explorer filters
|
||||
filter_backends = PagesAPIViewSet.filter_backends + [
|
||||
HasChildrenFilter,
|
||||
ForExplorerFilter,
|
||||
]
|
||||
|
||||
meta_fields = PagesAPIViewSet.meta_fields + [
|
||||
"latest_revision_created_at",
|
||||
"status",
|
||||
"children",
|
||||
"descendants",
|
||||
"parent",
|
||||
"ancestors",
|
||||
"translations",
|
||||
]
|
||||
|
||||
body_fields = PagesAPIViewSet.body_fields + [
|
||||
"admin_display_title",
|
||||
]
|
||||
|
||||
listing_default_fields = PagesAPIViewSet.listing_default_fields + [
|
||||
"latest_revision_created_at",
|
||||
"status",
|
||||
"children",
|
||||
"admin_display_title",
|
||||
]
|
||||
|
||||
# Allow the parent field to appear on listings
|
||||
detail_only_fields = []
|
||||
|
||||
known_query_parameters = PagesAPIViewSet.known_query_parameters.union(
|
||||
["for_explorer", "has_children"]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_detail_default_fields(cls, model):
|
||||
detail_default_fields = super().get_detail_default_fields(model)
|
||||
|
||||
# When i18n is disabled, remove "translations" from default fields
|
||||
if not getattr(settings, "WAGTAIL_I18N_ENABLED", False):
|
||||
detail_default_fields.remove("translations")
|
||||
|
||||
return detail_default_fields
|
||||
|
||||
def get_root_page(self):
|
||||
"""
|
||||
Returns the page that is used when the `&child_of=root` filter is used.
|
||||
"""
|
||||
return Page.get_first_root_node()
|
||||
|
||||
def get_base_queryset(self):
|
||||
"""
|
||||
Returns a queryset containing all pages that can be seen by this user.
|
||||
|
||||
This is used as the base for get_queryset and is also used to find the
|
||||
parent pages when using the child_of and descendant_of filters as well.
|
||||
"""
|
||||
return Page.objects.all()
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
|
||||
# Hide root page
|
||||
# TODO: Add "include_root" flag
|
||||
queryset = queryset.exclude(depth=1).defer_streamfields().specific()
|
||||
|
||||
return queryset
|
||||
|
||||
def get_type_info(self):
|
||||
types = OrderedDict()
|
||||
|
||||
for name, model in self.seen_types.items():
|
||||
types[name] = OrderedDict(
|
||||
[
|
||||
("verbose_name", model._meta.verbose_name),
|
||||
("verbose_name_plural", model._meta.verbose_name_plural),
|
||||
]
|
||||
)
|
||||
|
||||
return types
|
||||
|
||||
def listing_view(self, request):
|
||||
response = super().listing_view(request)
|
||||
response.data["__types"] = self.get_type_info()
|
||||
return response
|
||||
|
||||
def detail_view(self, request, pk):
|
||||
response = super().detail_view(request, pk)
|
||||
response.data["__types"] = self.get_type_info()
|
||||
return response
|
||||
|
||||
def action_view(self, request, pk, action_name):
|
||||
instance = self.get_object()
|
||||
|
||||
if action_name not in self.actions:
|
||||
raise Http404(f"unrecognised action '{action_name}'")
|
||||
|
||||
action = self.actions[action_name](self, request)
|
||||
action_data = action.serializer(data=request.data)
|
||||
|
||||
if not action_data.is_valid():
|
||||
return Response(action_data.errors, status=400)
|
||||
|
||||
return action.execute(instance, action_data.data)
|
||||
|
||||
@classmethod
|
||||
def get_urlpatterns(cls):
|
||||
"""
|
||||
This returns a list of URL patterns for the endpoint
|
||||
"""
|
||||
urlpatterns = super().get_urlpatterns()
|
||||
urlpatterns.extend(
|
||||
[
|
||||
path(
|
||||
"<int:pk>/action/<str:action_name>/",
|
||||
cls.as_view({"post": "action_view"}),
|
||||
name="action",
|
||||
),
|
||||
]
|
||||
)
|
||||
return urlpatterns
|
||||
16
env/lib/python3.10/site-packages/wagtail/admin/apps.py
vendored
Normal file
16
env/lib/python3.10/site-packages/wagtail/admin/apps.py
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from . import checks # NOQA: F401
|
||||
|
||||
|
||||
class WagtailAdminAppConfig(AppConfig):
|
||||
name = "wagtail.admin"
|
||||
label = "wagtailadmin"
|
||||
verbose_name = _("Wagtail admin")
|
||||
default_auto_field = "django.db.models.AutoField"
|
||||
|
||||
def ready(self):
|
||||
from wagtail.admin.signal_handlers import register_signal_handlers
|
||||
|
||||
register_signal_handlers()
|
||||
188
env/lib/python3.10/site-packages/wagtail/admin/auth.py
vendored
Normal file
188
env/lib/python3.10/site-packages/wagtail/admin/auth.py
vendored
Normal file
@@ -0,0 +1,188 @@
|
||||
import types
|
||||
from functools import wraps
|
||||
|
||||
import l18n
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import override as override_tz
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import override
|
||||
|
||||
from wagtail.admin import messages
|
||||
from wagtail.log_actions import LogContext
|
||||
from wagtail.permissions import page_permission_policy
|
||||
|
||||
|
||||
def permission_denied(request):
|
||||
"""Return a standard 'permission denied' response"""
|
||||
if request.headers.get("x-requested-with") == "XMLHttpRequest":
|
||||
raise PermissionDenied
|
||||
|
||||
from wagtail.admin import messages
|
||||
|
||||
messages.error(request, _("Sorry, you do not have permission to access this area."))
|
||||
return redirect("wagtailadmin_home")
|
||||
|
||||
|
||||
def user_passes_test(test):
|
||||
"""
|
||||
Given a test function that takes a user object and returns a boolean,
|
||||
return a view decorator that denies access to the user if the test returns false.
|
||||
"""
|
||||
|
||||
def decorator(view_func):
|
||||
# decorator takes the view function, and returns the view wrapped in
|
||||
# a permission check
|
||||
|
||||
@wraps(view_func)
|
||||
def wrapped_view_func(request, *args, **kwargs):
|
||||
if test(request.user):
|
||||
# permission check succeeds; run the view function as normal
|
||||
return view_func(request, *args, **kwargs)
|
||||
else:
|
||||
# permission check failed
|
||||
return permission_denied(request)
|
||||
|
||||
return wrapped_view_func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def permission_required(permission_name):
|
||||
"""
|
||||
Replacement for django.contrib.auth.decorators.permission_required which returns a
|
||||
more meaningful 'permission denied' response than just redirecting to the login page.
|
||||
(The latter doesn't work anyway because Wagtail doesn't define LOGIN_URL...)
|
||||
"""
|
||||
|
||||
def test(user):
|
||||
return user.has_perm(permission_name)
|
||||
|
||||
# user_passes_test constructs a decorator function specific to the above test function
|
||||
return user_passes_test(test)
|
||||
|
||||
|
||||
def any_permission_required(*perms):
|
||||
"""
|
||||
Decorator that accepts a list of permission names, and allows the user
|
||||
to pass if they have *any* of the permissions in the list
|
||||
"""
|
||||
|
||||
def test(user):
|
||||
for perm in perms:
|
||||
if user.has_perm(perm):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
return user_passes_test(test)
|
||||
|
||||
|
||||
class PermissionPolicyChecker:
|
||||
"""
|
||||
Provides a view decorator that enforces the given permission policy,
|
||||
returning the wagtailadmin 'permission denied' response if permission not granted
|
||||
"""
|
||||
|
||||
def __init__(self, policy):
|
||||
self.policy = policy
|
||||
|
||||
def require(self, action):
|
||||
def test(user):
|
||||
return self.policy.user_has_permission(user, action)
|
||||
|
||||
return user_passes_test(test)
|
||||
|
||||
def require_any(self, *actions):
|
||||
def test(user):
|
||||
return self.policy.user_has_any_permission(user, actions)
|
||||
|
||||
return user_passes_test(test)
|
||||
|
||||
|
||||
def user_has_any_page_permission(user):
|
||||
"""
|
||||
Check if a user has any permission to add, edit, or otherwise manage any
|
||||
page.
|
||||
"""
|
||||
return page_permission_policy.user_has_any_permission(
|
||||
user, {"add", "change", "publish", "bulk_delete", "lock", "unlock"}
|
||||
)
|
||||
|
||||
|
||||
def reject_request(request):
|
||||
if request.headers.get("x-requested-with") == "XMLHttpRequest":
|
||||
raise PermissionDenied
|
||||
|
||||
# import redirect_to_login here to avoid circular imports on model files that import
|
||||
# wagtail.admin.auth, specifically where custom user models are involved
|
||||
from django.contrib.auth.views import redirect_to_login as auth_redirect_to_login
|
||||
|
||||
login_url = getattr(
|
||||
settings, "WAGTAILADMIN_LOGIN_URL", reverse("wagtailadmin_login")
|
||||
)
|
||||
|
||||
return auth_redirect_to_login(request.get_full_path(), login_url=login_url)
|
||||
|
||||
|
||||
def require_admin_access(view_func):
|
||||
def decorated_view(request, *args, **kwargs):
|
||||
user = request.user
|
||||
|
||||
if user.is_anonymous:
|
||||
return reject_request(request)
|
||||
|
||||
if user.has_perms(["wagtailadmin.access_admin"]):
|
||||
try:
|
||||
preferred_language = None
|
||||
if hasattr(user, "wagtail_userprofile"):
|
||||
preferred_language = (
|
||||
user.wagtail_userprofile.get_preferred_language()
|
||||
)
|
||||
l18n.set_language(preferred_language)
|
||||
time_zone = user.wagtail_userprofile.get_current_time_zone()
|
||||
else:
|
||||
time_zone = settings.TIME_ZONE
|
||||
with override_tz(time_zone), LogContext(user=user):
|
||||
if preferred_language:
|
||||
with override(preferred_language):
|
||||
response = view_func(request, *args, **kwargs)
|
||||
else:
|
||||
response = view_func(request, *args, **kwargs)
|
||||
|
||||
if hasattr(response, "render"):
|
||||
# If the response has a render() method, Django treats it
|
||||
# like a TemplateResponse, so we should do the same
|
||||
# In this case, we need to guarantee that when the TemplateResponse
|
||||
# is rendered, it is done within the override context manager
|
||||
# or the user preferred_language/timezone will not be used
|
||||
# (this could be replaced with simply rendering the TemplateResponse
|
||||
# for simplicity but this does remove some of its middleware modification
|
||||
# potential)
|
||||
render = response.render
|
||||
|
||||
def overridden_render(response):
|
||||
with override_tz(time_zone):
|
||||
if preferred_language:
|
||||
with override(preferred_language):
|
||||
return render()
|
||||
return render()
|
||||
|
||||
response.render = types.MethodType(overridden_render, response)
|
||||
# decorate the response render method with the override context manager
|
||||
return response
|
||||
|
||||
except PermissionDenied:
|
||||
if request.headers.get("x-requested-with") == "XMLHttpRequest":
|
||||
raise
|
||||
|
||||
return permission_denied(request)
|
||||
|
||||
if not request.headers.get("x-requested-with") == "XMLHttpRequest":
|
||||
messages.error(request, _("You do not have permission to access the admin"))
|
||||
|
||||
return reject_request(request)
|
||||
|
||||
return decorated_view
|
||||
7
env/lib/python3.10/site-packages/wagtail/admin/blocks.py
vendored
Normal file
7
env/lib/python3.10/site-packages/wagtail/admin/blocks.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
import warnings
|
||||
|
||||
from wagtail.blocks import * # noqa: F403
|
||||
|
||||
warnings.warn(
|
||||
"wagtail.admin.blocks has moved to wagtail.blocks", UserWarning, stacklevel=2
|
||||
)
|
||||
278
env/lib/python3.10/site-packages/wagtail/admin/checks.py
vendored
Normal file
278
env/lib/python3.10/site-packages/wagtail/admin/checks.py
vendored
Normal file
@@ -0,0 +1,278 @@
|
||||
import os
|
||||
|
||||
from django.core.checks import Error, Tags, Warning, register
|
||||
|
||||
|
||||
@register("staticfiles")
|
||||
def css_install_check(app_configs, **kwargs):
|
||||
errors = []
|
||||
|
||||
css_path = os.path.join(
|
||||
os.path.dirname(__file__), "static", "wagtailadmin", "css", "core.css"
|
||||
)
|
||||
|
||||
if not os.path.isfile(css_path):
|
||||
error_hint = (
|
||||
"""
|
||||
Most likely you are running a development (non-packaged) copy of
|
||||
Wagtail and have not built the static assets -
|
||||
see https://docs.wagtail.org/en/latest/contributing/developing.html
|
||||
|
||||
File not found: %s
|
||||
"""
|
||||
% css_path
|
||||
)
|
||||
|
||||
errors.append(
|
||||
Warning(
|
||||
"CSS for the Wagtail admin is missing",
|
||||
hint=error_hint,
|
||||
id="wagtailadmin.W001",
|
||||
)
|
||||
)
|
||||
return errors
|
||||
|
||||
|
||||
@register(Tags.admin)
|
||||
def base_form_class_check(app_configs, **kwargs):
|
||||
from wagtail.admin.forms import WagtailAdminPageForm
|
||||
from wagtail.models import get_page_models
|
||||
|
||||
errors = []
|
||||
|
||||
for cls in get_page_models():
|
||||
if not issubclass(cls.base_form_class, WagtailAdminPageForm):
|
||||
errors.append(
|
||||
Error(
|
||||
"{}.base_form_class does not extend WagtailAdminPageForm".format(
|
||||
cls.__name__
|
||||
),
|
||||
hint="Ensure that {}.{} extends WagtailAdminPageForm".format(
|
||||
cls.base_form_class.__module__, cls.base_form_class.__name__
|
||||
),
|
||||
obj=cls,
|
||||
id="wagtailadmin.E001",
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@register(Tags.admin)
|
||||
def get_form_class_check(app_configs, **kwargs):
|
||||
from wagtail.admin.forms import WagtailAdminPageForm
|
||||
from wagtail.models import get_page_models
|
||||
|
||||
errors = []
|
||||
|
||||
for cls in get_page_models():
|
||||
edit_handler = cls.get_edit_handler()
|
||||
if not issubclass(edit_handler.get_form_class(), WagtailAdminPageForm):
|
||||
errors.append(
|
||||
Error(
|
||||
"{cls}.get_edit_handler().get_form_class() does not extend WagtailAdminPageForm".format(
|
||||
cls=cls.__name__
|
||||
),
|
||||
hint="Ensure that the panel definition for {cls} creates a subclass of WagtailAdminPageForm".format(
|
||||
cls=cls.__name__
|
||||
),
|
||||
obj=cls,
|
||||
id="wagtailadmin.E002",
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@register("panels")
|
||||
def inline_panel_model_panels_check(app_configs, **kwargs):
|
||||
from wagtail.models import get_page_models
|
||||
|
||||
errors = []
|
||||
page_models = get_page_models()
|
||||
|
||||
for cls in page_models:
|
||||
errors.extend(check_panels_in_model(cls))
|
||||
|
||||
# filter out duplicate errors found for the same model
|
||||
unique_errors = []
|
||||
for error in errors:
|
||||
if error.msg not in [e.msg for e in unique_errors]:
|
||||
unique_errors.append(error)
|
||||
return unique_errors
|
||||
|
||||
|
||||
def check_panels_in_model(cls, context="model"):
|
||||
"""Check panels configuration uses `panels` when `edit_handler` not in use."""
|
||||
from wagtail.admin.panels import InlinePanel, PanelGroup
|
||||
from wagtail.models import Page
|
||||
|
||||
errors = []
|
||||
|
||||
if hasattr(cls, "get_edit_handler"):
|
||||
# must check the InlinePanel related models
|
||||
edit_handler = cls.get_edit_handler()
|
||||
for tab in edit_handler.children:
|
||||
if isinstance(tab, PanelGroup):
|
||||
inline_panel_children = [
|
||||
panel for panel in tab.children if isinstance(panel, InlinePanel)
|
||||
]
|
||||
for inline_panel_child in inline_panel_children:
|
||||
errors.extend(
|
||||
check_panels_in_model(
|
||||
inline_panel_child.db_field.related_model,
|
||||
context="InlinePanel model",
|
||||
)
|
||||
)
|
||||
|
||||
if issubclass(cls, Page) or hasattr(cls, "edit_handler"):
|
||||
# Pages do not need to be checked for standalone tabbed_panel usage
|
||||
# if edit_handler is used on any model, assume config is correct
|
||||
return errors
|
||||
|
||||
tabbed_panels = [
|
||||
"content_panels",
|
||||
"promote_panels",
|
||||
"settings_panels",
|
||||
]
|
||||
|
||||
for panel_name in tabbed_panels:
|
||||
class_name = cls.__name__
|
||||
if not hasattr(cls, panel_name):
|
||||
continue
|
||||
|
||||
panel_name_short = panel_name.replace("_panels", "").title()
|
||||
error_title = "{}.{} will have no effect on {} editing".format(
|
||||
class_name, panel_name, context
|
||||
)
|
||||
|
||||
if "InlinePanel" in context:
|
||||
error_hint = """Ensure that {} uses `panels` instead of `{}`.
|
||||
There are no tabs on non-Page model editing within InlinePanels.""".format(
|
||||
class_name, panel_name
|
||||
)
|
||||
else:
|
||||
error_hint = """Ensure that {} uses `panels` instead of `{}`\
|
||||
or set up an `edit_handler` if you want a tabbed editing interface.
|
||||
There are no default tabs on non-Page models so there will be no \
|
||||
{} tab for the {} to render in.""".format(
|
||||
class_name, panel_name, panel_name_short, panel_name
|
||||
)
|
||||
|
||||
error = Warning(error_title, hint=error_hint, obj=cls, id="wagtailadmin.W002")
|
||||
|
||||
errors.append(error)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@register("wagtailadmin_base_url")
|
||||
def wagtail_admin_base_url_check(app_configs, **kwargs):
|
||||
from django.conf import settings
|
||||
|
||||
errors = []
|
||||
|
||||
if getattr(settings, "WAGTAILADMIN_BASE_URL", None) is None:
|
||||
errors.append(
|
||||
Warning(
|
||||
"The WAGTAILADMIN_BASE_URL setting is not defined",
|
||||
hint="This should be the base URL used to access the Wagtail admin site. "
|
||||
"Without this, URLs in notification emails will not display correctly.",
|
||||
id="wagtailadmin.W003",
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@register("file_overwrite")
|
||||
def file_overwrite_check(app_configs, **kwargs):
|
||||
from django import VERSION as DJANGO_VERSION
|
||||
from django.conf import settings
|
||||
|
||||
if DJANGO_VERSION >= (5, 1):
|
||||
file_storage = getattr(settings, "STORAGES")["default"]["BACKEND"]
|
||||
else:
|
||||
try:
|
||||
file_storage = getattr(settings, "STORAGES")["default"]["BACKEND"]
|
||||
except AttributeError:
|
||||
file_storage = getattr(settings, "DEFAULT_FILE_STORAGE", None)
|
||||
|
||||
errors = []
|
||||
|
||||
if file_storage == "storages.backends.s3boto3.S3Boto3Storage" and getattr(
|
||||
settings, "AWS_S3_FILE_OVERWRITE", True
|
||||
):
|
||||
errors.append(
|
||||
Warning(
|
||||
"The AWS_S3_FILE_OVERWRITE setting is set to True",
|
||||
hint="This should be set to False. The incorrect setting can cause documents and "
|
||||
"other user-uploaded files to be silently overwritten or deleted.",
|
||||
id="wagtailadmin.W004",
|
||||
)
|
||||
)
|
||||
if file_storage == "storages.backends.azure_storage.AzureStorage" and getattr(
|
||||
settings, "AZURE_OVERWRITE_FILES", False
|
||||
):
|
||||
errors.append(
|
||||
Warning(
|
||||
"The AZURE_OVERWRITE_FILES setting is set to True",
|
||||
hint="This should be set to False. The incorrect setting can cause documents and "
|
||||
"other user-uploaded files to be silently overwritten or deleted.",
|
||||
id="wagtailadmin.W004",
|
||||
)
|
||||
)
|
||||
if file_storage == "storages.backends.gcloud.GoogleCloudStorage" and getattr(
|
||||
settings, "GS_FILE_OVERWRITE", True
|
||||
):
|
||||
errors.append(
|
||||
Warning(
|
||||
"The GS_FILE_OVERWRITE setting is set to True",
|
||||
hint="This should be set to False. The incorrect setting can cause documents and "
|
||||
"other user-uploaded files to be silently overwritten or deleted.",
|
||||
id="wagtailadmin.W004",
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
@register("datetime_format")
|
||||
def datetime_format_check(app_configs, **kwargs):
|
||||
"""
|
||||
If L10N is enabled, check if WAGTAIL_* formats are compatible with Django input formats.
|
||||
See https://docs.djangoproject.com/en/stable/topics/i18n/formatting/#creating-custom-format-files
|
||||
See https://docs.wagtail.org/en/stable/reference/settings.html#wagtail-date-format-wagtail-datetime-format-wagtail-time-format
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils import formats
|
||||
|
||||
errors = []
|
||||
|
||||
if not getattr(settings, "USE_L10N", False):
|
||||
return errors
|
||||
|
||||
for code, label in settings.LANGUAGES:
|
||||
for wagtail_setting, django_setting in [
|
||||
("WAGTAIL_DATE_FORMAT", "DATE_INPUT_FORMATS"),
|
||||
("WAGTAIL_DATETIME_FORMAT", "DATETIME_INPUT_FORMATS"),
|
||||
("WAGTAIL_TIME_FORMAT", "TIME_INPUT_FORMATS"),
|
||||
]:
|
||||
wagtail_format_value = getattr(settings, wagtail_setting, None)
|
||||
if wagtail_format_value is None:
|
||||
# Skip the iteration if wagtail_format is not present
|
||||
continue
|
||||
|
||||
input_formats = formats.get_format(django_setting, lang=code)
|
||||
if wagtail_format_value not in input_formats:
|
||||
errors.append(
|
||||
Error(
|
||||
"Configuration error",
|
||||
hint=f"'{wagtail_format_value}' must be in {django_setting} for language {label} ({code}).",
|
||||
obj=wagtail_setting,
|
||||
id="wagtailadmin.E003",
|
||||
)
|
||||
)
|
||||
|
||||
return errors
|
||||
852
env/lib/python3.10/site-packages/wagtail/admin/compare.py
vendored
Normal file
852
env/lib/python3.10/site-packages/wagtail/admin/compare.py
vendored
Normal file
@@ -0,0 +1,852 @@
|
||||
import difflib
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import models
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.html import escape, format_html, format_html_join
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from wagtail import blocks
|
||||
from wagtail.fields import RichTextField, StreamField
|
||||
from wagtail.utils.registry import ModelFieldRegistry
|
||||
from wagtail.utils.text import text_from_html
|
||||
|
||||
comparison_class_registry = ModelFieldRegistry()
|
||||
|
||||
|
||||
def register_comparison_class(
|
||||
field_class, to=None, comparison_class=None, exact_class=False
|
||||
):
|
||||
"""
|
||||
Define parameters for form fields to be used by WagtailAdminModelForm for a given
|
||||
database field.
|
||||
"""
|
||||
|
||||
if comparison_class is None:
|
||||
raise ImproperlyConfigured(
|
||||
"register_comparison_class must be passed a 'comparison_class' keyword argument"
|
||||
)
|
||||
|
||||
if to and field_class != models.ForeignKey:
|
||||
raise ImproperlyConfigured(
|
||||
"The 'to' argument on register_comparison_class is only valid for ForeignKey fields"
|
||||
)
|
||||
|
||||
comparison_class_registry.register(
|
||||
field_class, to=to, value=comparison_class, exact_class=exact_class
|
||||
)
|
||||
|
||||
|
||||
class FieldComparison:
|
||||
is_field = True
|
||||
is_child_relation = False
|
||||
|
||||
def __init__(self, field, obj_a, obj_b):
|
||||
self.field = field
|
||||
self.val_a = field.value_from_object(obj_a)
|
||||
self.val_b = field.value_from_object(obj_b)
|
||||
|
||||
def field_label(self):
|
||||
"""
|
||||
Returns a label for this field to be displayed to the user
|
||||
"""
|
||||
verbose_name = getattr(self.field, "verbose_name", None)
|
||||
|
||||
if verbose_name is None:
|
||||
# Relations don't have a verbose_name
|
||||
verbose_name = self.field.name.replace("_", " ")
|
||||
|
||||
return capfirst(verbose_name)
|
||||
|
||||
def htmldiff(self):
|
||||
if self.val_a != self.val_b:
|
||||
return TextDiff(
|
||||
[("deletion", self.val_a), ("addition", self.val_b)]
|
||||
).to_html()
|
||||
else:
|
||||
return escape(self.val_a)
|
||||
|
||||
def has_changed(self):
|
||||
"""
|
||||
Returns True if the field has changed
|
||||
"""
|
||||
return self.val_a != self.val_b
|
||||
|
||||
|
||||
class TextFieldComparison(FieldComparison):
|
||||
def htmldiff(self):
|
||||
return diff_text(self.val_a, self.val_b).to_html()
|
||||
|
||||
|
||||
register_comparison_class(models.CharField, comparison_class=TextFieldComparison)
|
||||
register_comparison_class(models.TextField, comparison_class=TextFieldComparison)
|
||||
|
||||
|
||||
class RichTextFieldComparison(TextFieldComparison):
|
||||
def htmldiff(self):
|
||||
return diff_text(
|
||||
text_from_html(self.val_a), text_from_html(self.val_b)
|
||||
).to_html()
|
||||
|
||||
|
||||
register_comparison_class(RichTextField, comparison_class=RichTextFieldComparison)
|
||||
|
||||
|
||||
def get_comparison_class_for_block(block):
|
||||
if hasattr(block, "get_comparison_class"):
|
||||
return block.get_comparison_class()
|
||||
elif isinstance(block, (blocks.CharBlock, blocks.TextBlock)):
|
||||
return CharBlockComparison
|
||||
elif isinstance(block, blocks.RawHTMLBlock):
|
||||
# Compare raw HTML blocks as if they were plain text, so that tags are shown explicitly
|
||||
return CharBlockComparison
|
||||
elif isinstance(block, blocks.RichTextBlock):
|
||||
return RichTextBlockComparison
|
||||
elif isinstance(block, blocks.StructBlock):
|
||||
return StructBlockComparison
|
||||
elif isinstance(block, blocks.StreamBlock):
|
||||
return StreamBlockComparison
|
||||
elif isinstance(block, blocks.ListBlock):
|
||||
return ListBlockComparison
|
||||
else:
|
||||
# As all stream field blocks have a HTML representation, fall back to diffing that.
|
||||
return RichTextBlockComparison
|
||||
|
||||
|
||||
class BlockComparison:
|
||||
def __init__(self, block, exists_a, exists_b, val_a, val_b):
|
||||
self.block = block
|
||||
self.exists_a = exists_a
|
||||
self.exists_b = exists_b
|
||||
self.val_a = val_a
|
||||
self.val_b = val_b
|
||||
|
||||
def is_new(self):
|
||||
return self.exists_b and not self.exists_a
|
||||
|
||||
def is_deleted(self):
|
||||
return self.exists_a and not self.exists_b
|
||||
|
||||
def has_changed(self):
|
||||
return self.val_a != self.val_b
|
||||
|
||||
def htmlvalue(self, val):
|
||||
"""
|
||||
Return an HTML representation of this block that is safe to be included
|
||||
in comparison views
|
||||
"""
|
||||
return escape(text_from_html(self.block.render_basic(val)))
|
||||
|
||||
def htmldiff(self):
|
||||
html_val_a = self.block.render_basic(self.val_a)
|
||||
html_val_b = self.block.render_basic(self.val_b)
|
||||
return diff_text(
|
||||
text_from_html(html_val_a), text_from_html(html_val_b)
|
||||
).to_html()
|
||||
|
||||
|
||||
class CharBlockComparison(BlockComparison):
|
||||
def htmldiff(self):
|
||||
return diff_text(force_str(self.val_a), force_str(self.val_b)).to_html()
|
||||
|
||||
def htmlvalue(self, val):
|
||||
return escape(val)
|
||||
|
||||
|
||||
class RichTextBlockComparison(BlockComparison):
|
||||
pass
|
||||
|
||||
|
||||
class StructBlockComparison(BlockComparison):
|
||||
def htmlvalue(self, val):
|
||||
htmlvalues = []
|
||||
for name, block in self.block.child_blocks.items():
|
||||
label = self.block.child_blocks[name].label
|
||||
comparison_class = get_comparison_class_for_block(block)
|
||||
|
||||
htmlvalues.append(
|
||||
(
|
||||
label,
|
||||
comparison_class(block, True, True, val[name], val[name]).htmlvalue(
|
||||
val[name]
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return format_html(
|
||||
"<dl>\n{}\n</dl>",
|
||||
format_html_join("\n", " <dt>{}</dt>\n <dd>{}</dd>", htmlvalues),
|
||||
)
|
||||
|
||||
def htmldiff(self):
|
||||
htmldiffs = []
|
||||
for name, block in self.block.child_blocks.items():
|
||||
label = self.block.child_blocks[name].label
|
||||
comparison_class = get_comparison_class_for_block(block)
|
||||
|
||||
htmldiffs.append(
|
||||
(
|
||||
label,
|
||||
comparison_class(
|
||||
block,
|
||||
self.exists_a,
|
||||
self.exists_b,
|
||||
self.val_a[name],
|
||||
self.val_b[name],
|
||||
).htmldiff(),
|
||||
)
|
||||
)
|
||||
|
||||
return format_html(
|
||||
"<dl>\n{}\n</dl>",
|
||||
format_html_join("\n", " <dt>{}</dt>\n <dd>{}</dd>", htmldiffs),
|
||||
)
|
||||
|
||||
|
||||
class BaseSequenceBlockComparison(BlockComparison):
|
||||
@staticmethod
|
||||
def get_blocks_from_value(val):
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
def get_blocks_by_id(a_blocks, b_blocks):
|
||||
a_blocks_by_id = {block.id: block for block in a_blocks}
|
||||
b_blocks_by_id = {block.id: block for block in b_blocks}
|
||||
return a_blocks_by_id, b_blocks_by_id
|
||||
|
||||
def get_block_comparisons_by_id(self):
|
||||
a_blocks = self.get_blocks_from_value(self.val_a)
|
||||
b_blocks = self.get_blocks_from_value(self.val_b)
|
||||
a_blocks_by_id, b_blocks_by_id = self.get_blocks_by_id(a_blocks, b_blocks)
|
||||
|
||||
deleted_ids = a_blocks_by_id.keys() - b_blocks_by_id.keys()
|
||||
|
||||
comparisons = []
|
||||
for block in b_blocks:
|
||||
comparison_class = get_comparison_class_for_block(block.block)
|
||||
|
||||
if block.id in a_blocks_by_id:
|
||||
# Changed/existing block
|
||||
comparisons.append(
|
||||
comparison_class(
|
||||
block.block,
|
||||
True,
|
||||
True,
|
||||
a_blocks_by_id[block.id].value,
|
||||
block.value,
|
||||
)
|
||||
)
|
||||
else:
|
||||
# New block
|
||||
comparisons.append(
|
||||
comparison_class(block.block, False, True, None, block.value)
|
||||
)
|
||||
|
||||
# Insert deleted blocks at the index where they used to be
|
||||
deleted_block_indices = [
|
||||
(block, i) for i, block in enumerate(a_blocks) if block.id in deleted_ids
|
||||
]
|
||||
|
||||
for block, index in deleted_block_indices:
|
||||
comparison_class = get_comparison_class_for_block(block.block)
|
||||
comparison_to_insert = comparison_class(
|
||||
block.block, True, False, block.value, None
|
||||
)
|
||||
|
||||
# Insert the block back in where it was before it was deleted.
|
||||
# Note: we need to account for new blocks when finding the position.
|
||||
current_index = 0
|
||||
block_inserted = False
|
||||
for i, comparison in enumerate(comparisons):
|
||||
if comparison.is_new():
|
||||
continue
|
||||
|
||||
if current_index == index:
|
||||
comparisons.insert(i, comparison_to_insert)
|
||||
block_inserted = True
|
||||
break
|
||||
|
||||
current_index += 1
|
||||
|
||||
# Deleted block was from the end
|
||||
if not block_inserted:
|
||||
comparisons.append(comparison_to_insert)
|
||||
|
||||
return comparisons
|
||||
|
||||
def htmldiff(self):
|
||||
comparisons_html = []
|
||||
|
||||
for comparison in self.get_block_comparisons():
|
||||
classes = ["comparison__child-object"]
|
||||
if comparison.is_new():
|
||||
classes.append("addition")
|
||||
block_rendered = comparison.htmlvalue(comparison.val_b)
|
||||
elif comparison.is_deleted():
|
||||
classes.append("deletion")
|
||||
block_rendered = comparison.htmlvalue(comparison.val_a)
|
||||
elif comparison.has_changed():
|
||||
block_rendered = comparison.htmldiff()
|
||||
else:
|
||||
block_rendered = comparison.htmlvalue(comparison.val_a)
|
||||
|
||||
classes = " ".join(classes)
|
||||
comparisons_html.append(f'<div class="{classes}">{block_rendered}</div>')
|
||||
|
||||
return mark_safe("\n".join(comparisons_html))
|
||||
|
||||
|
||||
class StreamBlockComparison(BaseSequenceBlockComparison):
|
||||
@staticmethod
|
||||
def get_blocks_from_value(val):
|
||||
blocks = list(val) or []
|
||||
return blocks
|
||||
|
||||
def get_block_comparisons(self):
|
||||
return self.get_block_comparisons_by_id()
|
||||
|
||||
|
||||
class ListBlockComparison(BaseSequenceBlockComparison):
|
||||
@staticmethod
|
||||
def get_blocks_from_value(val):
|
||||
blocks = list(val.bound_blocks) or []
|
||||
return blocks
|
||||
|
||||
def get_block_comparisons(self):
|
||||
a_blocks = self.get_blocks_from_value(self.val_a)
|
||||
b_blocks = self.get_blocks_from_value(self.val_b)
|
||||
|
||||
# ListBlock could be in one of two formats.
|
||||
# If IDs in both a and b are all newly created (ie we're loading data in the old format)
|
||||
# there's no point in using id for identifying which blocks are the same vs moved / added / deleted.
|
||||
both_in_new_format = any(block.original_id for block in a_blocks) and any(
|
||||
block.original_id for block in b_blocks
|
||||
)
|
||||
|
||||
if both_in_new_format:
|
||||
# All blocks have ids, so we can use the BaseSequenceBlock comparison logic, which uses ids.
|
||||
return self.get_block_comparisons_by_id()
|
||||
|
||||
# We're dealing with data in the old format
|
||||
# Let's compare blocks by position - it's not perfect, but it's better than rendering to HTML
|
||||
comparisons = []
|
||||
comparison_class = get_comparison_class_for_block(self.block.child_block)
|
||||
|
||||
a_length = len(a_blocks)
|
||||
b_length = len(b_blocks)
|
||||
|
||||
for index, block in enumerate(b_blocks):
|
||||
if index < a_length:
|
||||
# Assume blocks with the same index are the same changed block
|
||||
# (not necessarily true as blocks could be moved)
|
||||
# Changed/existing block
|
||||
comparisons.append(
|
||||
comparison_class(
|
||||
block.block,
|
||||
True,
|
||||
True,
|
||||
a_blocks[index].value,
|
||||
block.value,
|
||||
)
|
||||
)
|
||||
else:
|
||||
# New block
|
||||
comparisons.append(
|
||||
comparison_class(block.block, False, True, None, block.value)
|
||||
)
|
||||
|
||||
if a_length > b_length:
|
||||
for block in a_blocks[b_length:]:
|
||||
# Deleted blocks
|
||||
comparisons.append(
|
||||
comparison_class(block.block, True, False, block.value, None)
|
||||
)
|
||||
|
||||
return comparisons
|
||||
|
||||
|
||||
class StreamFieldComparison(FieldComparison):
|
||||
def has_block_ids(self, val):
|
||||
if not val:
|
||||
return True
|
||||
|
||||
return bool(val[0].id)
|
||||
|
||||
def htmldiff(self):
|
||||
# Our method for diffing streamfields relies on the blocks in both revisions having UUIDs.
|
||||
# But as UUIDs were added in Wagtail 1.11 we can't compare revisions that were created before
|
||||
# that Wagtail version.
|
||||
if self.has_block_ids(self.val_a) and self.has_block_ids(self.val_b):
|
||||
return StreamBlockComparison(
|
||||
self.field.stream_block, True, True, self.val_a, self.val_b
|
||||
).htmldiff()
|
||||
else:
|
||||
# Fall back to diffing the HTML representation
|
||||
return diff_text(
|
||||
text_from_html(self.val_a), text_from_html(self.val_b)
|
||||
).to_html()
|
||||
|
||||
|
||||
register_comparison_class(StreamField, comparison_class=StreamFieldComparison)
|
||||
|
||||
|
||||
class ChoiceFieldComparison(FieldComparison):
|
||||
def htmldiff(self):
|
||||
val_a = force_str(
|
||||
dict(self.field.flatchoices).get(self.val_a, self.val_a), strings_only=True
|
||||
)
|
||||
val_b = force_str(
|
||||
dict(self.field.flatchoices).get(self.val_b, self.val_b), strings_only=True
|
||||
)
|
||||
|
||||
if self.val_a != self.val_b:
|
||||
diffs = []
|
||||
|
||||
if val_a:
|
||||
diffs += [("deletion", val_a)]
|
||||
if val_b:
|
||||
diffs += [("addition", val_b)]
|
||||
|
||||
return TextDiff(diffs).to_html()
|
||||
else:
|
||||
return escape(val_a)
|
||||
|
||||
|
||||
class M2MFieldComparison(FieldComparison):
|
||||
def get_items(self):
|
||||
return list(self.val_a), list(self.val_b)
|
||||
|
||||
def get_item_display(self, item):
|
||||
return str(item)
|
||||
|
||||
def htmldiff(self):
|
||||
# Get tags
|
||||
items_a, items_b = self.get_items()
|
||||
|
||||
# Calculate changes
|
||||
sm = difflib.SequenceMatcher(0, items_a, items_b)
|
||||
changes = []
|
||||
for op, i1, i2, j1, j2 in sm.get_opcodes():
|
||||
if op == "replace":
|
||||
for item in items_a[i1:i2]:
|
||||
changes.append(("deletion", self.get_item_display(item)))
|
||||
for item in items_b[j1:j2]:
|
||||
changes.append(("addition", self.get_item_display(item)))
|
||||
elif op == "delete":
|
||||
for item in items_a[i1:i2]:
|
||||
changes.append(("deletion", self.get_item_display(item)))
|
||||
elif op == "insert":
|
||||
for item in items_b[j1:j2]:
|
||||
changes.append(("addition", self.get_item_display(item)))
|
||||
elif op == "equal":
|
||||
for item in items_a[i1:i2]:
|
||||
changes.append(("equal", self.get_item_display(item)))
|
||||
|
||||
# Convert changelist to HTML
|
||||
return TextDiff(changes, separator=", ").to_html()
|
||||
|
||||
def has_changed(self):
|
||||
items_a, items_b = self.get_items()
|
||||
return items_a != items_b
|
||||
|
||||
|
||||
class TagsFieldComparison(M2MFieldComparison):
|
||||
def get_item_display(self, tag):
|
||||
return tag.slug
|
||||
|
||||
|
||||
register_comparison_class(TaggableManager, comparison_class=TagsFieldComparison)
|
||||
|
||||
|
||||
class ForeignObjectComparison(FieldComparison):
|
||||
def get_objects(self):
|
||||
model = self.field.related_model
|
||||
obj_a = model.objects.filter(pk=self.val_a).first()
|
||||
obj_b = model.objects.filter(pk=self.val_b).first()
|
||||
return obj_a, obj_b
|
||||
|
||||
def htmldiff(self):
|
||||
obj_a, obj_b = self.get_objects()
|
||||
|
||||
if obj_a != obj_b:
|
||||
if obj_a and obj_b:
|
||||
# Changed
|
||||
return TextDiff(
|
||||
[("deletion", force_str(obj_a)), ("addition", force_str(obj_b))]
|
||||
).to_html()
|
||||
elif obj_b:
|
||||
# Added
|
||||
return TextDiff([("addition", force_str(obj_b))]).to_html()
|
||||
elif obj_a:
|
||||
# Removed
|
||||
return TextDiff([("deletion", force_str(obj_a))]).to_html()
|
||||
else:
|
||||
if obj_a:
|
||||
return escape(force_str(obj_a))
|
||||
else:
|
||||
return _("None")
|
||||
|
||||
|
||||
class ChildRelationComparison:
|
||||
is_field = False
|
||||
is_child_relation = True
|
||||
|
||||
def __init__(self, field, field_comparisons, obj_a, obj_b, label=""):
|
||||
self.field = field
|
||||
self.field_comparisons = field_comparisons
|
||||
self.val_a = getattr(obj_a, field.related_name)
|
||||
self.val_b = getattr(obj_b, field.related_name)
|
||||
self.label = label
|
||||
|
||||
def field_label(self):
|
||||
"""
|
||||
Returns a label for this field to be displayed to the user
|
||||
"""
|
||||
verbose_name = getattr(self.field, "verbose_name", None)
|
||||
|
||||
if verbose_name is None:
|
||||
# Relations don't have a verbose_name
|
||||
if self.label:
|
||||
# If the panel has a label, we set it instead.
|
||||
# See InlinePanel.get_comparison for usage
|
||||
verbose_name = self.label
|
||||
else:
|
||||
verbose_name = self.field.name.replace("_", " ")
|
||||
|
||||
return capfirst(verbose_name)
|
||||
|
||||
def get_mapping(self, objs_a, objs_b):
|
||||
"""
|
||||
This bit of code attempts to match the objects in the A revision with
|
||||
their counterpart in the B revision.
|
||||
|
||||
A match is firstly attempted by PK (where a matching ID indicates they're the same).
|
||||
We compare remaining the objects by their field data; the objects with the fewest
|
||||
fields changed are matched until there are no more possible matches left.
|
||||
|
||||
This returns 4 values:
|
||||
- map_forwards => a mapping of object indexes from the B version to the A version
|
||||
- map_backwards => a mapping of object indexes from the A version to the B version
|
||||
- added => a list of indices for objects that didn't exist in the B version
|
||||
- deleted => a list of indices for objects that didn't exist in the A version
|
||||
|
||||
Note the indices are 0-based array indices indicating the location of the object in either
|
||||
the objs_a or objs_b arrays.
|
||||
|
||||
For example:
|
||||
|
||||
objs_a => A, B, C, D
|
||||
objs_b => B, C, D, E
|
||||
|
||||
Will return the following:
|
||||
|
||||
map_forwards = {
|
||||
1: 0, # B (objs_a: objs_b)
|
||||
2: 1, # C (objs_a: objs_b)
|
||||
3: 2, # D (objs_a: objs_b)
|
||||
}
|
||||
map_backwards = {
|
||||
0: 1, # B (objs_b: objs_a)
|
||||
1: 2, # C (objs_b: objs_a)
|
||||
2: 3, # D (objs_b: objs_a)
|
||||
}
|
||||
added = [4] # D in objs_b
|
||||
deleted = [0] # A in objs_a
|
||||
"""
|
||||
map_forwards = {}
|
||||
map_backwards = {}
|
||||
added = []
|
||||
deleted = []
|
||||
|
||||
# Match child objects on PK (ID)
|
||||
for a_idx, a_child in enumerate(objs_a):
|
||||
for b_idx, b_child in enumerate(objs_b):
|
||||
if b_idx in map_backwards:
|
||||
continue
|
||||
|
||||
if (
|
||||
a_child.pk is not None
|
||||
and b_child.pk is not None
|
||||
and a_child.pk == b_child.pk
|
||||
):
|
||||
map_forwards[a_idx] = b_idx
|
||||
map_backwards[b_idx] = a_idx
|
||||
|
||||
# Now try to match them by data
|
||||
matches = []
|
||||
for a_idx, a_child in enumerate(objs_a):
|
||||
if a_idx not in map_forwards:
|
||||
for b_idx, b_child in enumerate(objs_b):
|
||||
if b_idx not in map_backwards:
|
||||
# If they both have a PK (ID) that is different, they can't be the same child object
|
||||
if a_child.pk and b_child.pk and a_child.pk != b_child.pk:
|
||||
continue
|
||||
|
||||
comparison = self.get_child_comparison(
|
||||
objs_a[a_idx], objs_b[b_idx]
|
||||
)
|
||||
num_differences = comparison.get_num_differences()
|
||||
|
||||
matches.append((a_idx, b_idx, num_differences))
|
||||
|
||||
# Objects with the least differences will be matched first. So only the best possible matches are made
|
||||
matches.sort(key=lambda match: match[2])
|
||||
for a_idx, b_idx, num_differences in matches:
|
||||
# Make sure both objects were not matched previously
|
||||
if a_idx in map_forwards or b_idx in map_backwards:
|
||||
continue
|
||||
|
||||
# Match!
|
||||
map_forwards[a_idx] = b_idx
|
||||
map_backwards[b_idx] = a_idx
|
||||
|
||||
# Mark unmapped objects as added/deleted
|
||||
for a_idx, a_child in enumerate(objs_a):
|
||||
if a_idx not in map_forwards:
|
||||
deleted.append(a_idx)
|
||||
|
||||
for b_idx, b_child in enumerate(objs_b):
|
||||
if b_idx not in map_backwards:
|
||||
added.append(b_idx)
|
||||
|
||||
return map_forwards, map_backwards, added, deleted
|
||||
|
||||
def get_child_comparison(self, obj_a, obj_b):
|
||||
return ChildObjectComparison(
|
||||
self.field.related_model, self.field_comparisons, obj_a, obj_b
|
||||
)
|
||||
|
||||
def get_child_comparisons(self):
|
||||
"""
|
||||
Returns a list of ChildObjectComparison objects. Representing all child
|
||||
objects that existed in either version.
|
||||
|
||||
They are returned in the order they appear in the B version with deletions
|
||||
appended at the end.
|
||||
|
||||
All child objects are returned, regardless of whether they were actually changed.
|
||||
"""
|
||||
objs_a = list(self.val_a.all())
|
||||
objs_b = list(self.val_b.all())
|
||||
|
||||
map_forwards, map_backwards, added, deleted = self.get_mapping(objs_a, objs_b)
|
||||
objs_a = dict(enumerate(objs_a))
|
||||
objs_b = dict(enumerate(objs_b))
|
||||
|
||||
comparisons = []
|
||||
|
||||
for b_idx, b_child in objs_b.items():
|
||||
if b_idx in added:
|
||||
comparisons.append(self.get_child_comparison(None, b_child))
|
||||
else:
|
||||
comparisons.append(
|
||||
self.get_child_comparison(objs_a[map_backwards[b_idx]], b_child)
|
||||
)
|
||||
|
||||
for a_idx, a_child in objs_a.items():
|
||||
if a_idx in deleted:
|
||||
comparisons.append(self.get_child_comparison(a_child, None))
|
||||
|
||||
return comparisons
|
||||
|
||||
def has_changed(self):
|
||||
"""
|
||||
Returns true if any changes were made to any of the child objects. This includes
|
||||
adding, deleting and reordering.
|
||||
"""
|
||||
objs_a = list(self.val_a.all())
|
||||
objs_b = list(self.val_b.all())
|
||||
|
||||
map_forwards, map_backwards, added, deleted = self.get_mapping(objs_a, objs_b)
|
||||
|
||||
if added or deleted:
|
||||
return True
|
||||
|
||||
for a_idx, b_idx in map_forwards.items():
|
||||
comparison = self.get_child_comparison(objs_a[a_idx], objs_b[b_idx])
|
||||
|
||||
if comparison.has_changed():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class ChildObjectComparison:
|
||||
def __init__(self, model, field_comparisons, obj_a, obj_b):
|
||||
self.model = model
|
||||
self.field_comparisons = field_comparisons
|
||||
self.obj_a = obj_a
|
||||
self.obj_b = obj_b
|
||||
|
||||
def is_addition(self):
|
||||
"""
|
||||
Returns True if this child object was created since obj_a
|
||||
"""
|
||||
return self.obj_b and not self.obj_a
|
||||
|
||||
def is_deletion(self):
|
||||
"""
|
||||
Returns True if this child object was deleted in obj_b
|
||||
"""
|
||||
return self.obj_a and not self.obj_b
|
||||
|
||||
def get_position_change(self):
|
||||
"""
|
||||
Returns the change in position as an integer. Positive if the object
|
||||
was moved down, negative if it moved up.
|
||||
|
||||
For example: '3' indicates the object moved down three spaces. '-1'
|
||||
indicates the object moved up one space.
|
||||
"""
|
||||
if not self.is_addition() and not self.is_deletion():
|
||||
sort_a = getattr(self.obj_a, "sort_order", 0) or 0
|
||||
sort_b = getattr(self.obj_b, "sort_order", 0) or 0
|
||||
return sort_b - sort_a
|
||||
|
||||
def get_field_comparisons(self):
|
||||
"""
|
||||
Returns a list of comparisons for all the fields in this object.
|
||||
Fields that haven't changed are included as well.
|
||||
"""
|
||||
comparisons = []
|
||||
|
||||
if self.is_addition() or self.is_deletion():
|
||||
# Display the fields without diff as one of the versions are missing
|
||||
obj = self.obj_a or self.obj_b
|
||||
|
||||
for field_comparison in self.field_comparisons:
|
||||
comparisons.append(field_comparison(obj, obj))
|
||||
else:
|
||||
for field_comparison in self.field_comparisons:
|
||||
comparisons.append(field_comparison(self.obj_a, self.obj_b))
|
||||
|
||||
return comparisons
|
||||
|
||||
def has_changed(self):
|
||||
for comparison in self.get_field_comparisons():
|
||||
if comparison.has_changed():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_num_differences(self):
|
||||
"""
|
||||
Returns the number of fields that differ between the two
|
||||
objects.
|
||||
"""
|
||||
num_differences = 0
|
||||
|
||||
for comparison in self.get_field_comparisons():
|
||||
if comparison.has_changed():
|
||||
num_differences += 1
|
||||
|
||||
return num_differences
|
||||
|
||||
|
||||
class TextDiff:
|
||||
def __init__(self, changes, separator=""):
|
||||
self.changes = changes
|
||||
self.separator = separator
|
||||
|
||||
def to_html(self, tag="span", addition_class="addition", deletion_class="deletion"):
|
||||
html = []
|
||||
|
||||
for change_type, value in self.changes:
|
||||
if change_type == "equal":
|
||||
html.append(escape(value))
|
||||
elif change_type == "addition":
|
||||
html.append(
|
||||
'<{tag} class="{classname}">{value}</{tag}>'.format(
|
||||
tag=tag, classname=addition_class, value=escape(value)
|
||||
)
|
||||
)
|
||||
elif change_type == "deletion":
|
||||
html.append(
|
||||
'<{tag} class="{classname}">{value}</{tag}>'.format(
|
||||
tag=tag, classname=deletion_class, value=escape(value)
|
||||
)
|
||||
)
|
||||
|
||||
return mark_safe(self.separator.join(html))
|
||||
|
||||
|
||||
def diff_text(a, b):
|
||||
"""
|
||||
Performs a diffing algorithm on two pieces of text. Returns
|
||||
a string of HTML containing the content of both texts with
|
||||
<span> tags inserted indicating where the differences are.
|
||||
"""
|
||||
|
||||
def tokenise(text):
|
||||
"""
|
||||
Tokenises a string by splitting it into individual characters
|
||||
and grouping the alphanumeric ones together.
|
||||
|
||||
This means that punctuation, whitespace, CJK characters, etc
|
||||
become separate tokens and words/numbers are merged together
|
||||
to form bigger tokens.
|
||||
|
||||
This makes the output of the diff easier to read as words are
|
||||
not broken up.
|
||||
"""
|
||||
tokens = []
|
||||
current_token = ""
|
||||
|
||||
for c in text or "":
|
||||
if c.isalnum():
|
||||
current_token += c
|
||||
else:
|
||||
if current_token:
|
||||
tokens.append(current_token)
|
||||
current_token = ""
|
||||
|
||||
tokens.append(c)
|
||||
|
||||
if current_token:
|
||||
tokens.append(current_token)
|
||||
|
||||
return tokens
|
||||
|
||||
a_tok = tokenise(a)
|
||||
b_tok = tokenise(b)
|
||||
sm = difflib.SequenceMatcher(lambda t: len(t) <= 4, a_tok, b_tok)
|
||||
|
||||
changes = []
|
||||
|
||||
for op, i1, i2, j1, j2 in sm.get_opcodes():
|
||||
if op == "replace":
|
||||
for token in a_tok[i1:i2]:
|
||||
changes.append(("deletion", token))
|
||||
for token in b_tok[j1:j2]:
|
||||
changes.append(("addition", token))
|
||||
elif op == "delete":
|
||||
for token in a_tok[i1:i2]:
|
||||
changes.append(("deletion", token))
|
||||
elif op == "insert":
|
||||
for token in b_tok[j1:j2]:
|
||||
changes.append(("addition", token))
|
||||
elif op == "equal":
|
||||
for token in a_tok[i1:i2]:
|
||||
changes.append(("equal", token))
|
||||
|
||||
# Merge adjacent changes which have the same type. This just cleans up the HTML a bit
|
||||
merged_changes = []
|
||||
current_value = []
|
||||
current_change_type = None
|
||||
for change_type, value in changes:
|
||||
if change_type != current_change_type:
|
||||
if current_change_type is not None:
|
||||
merged_changes.append((current_change_type, "".join(current_value)))
|
||||
current_value = []
|
||||
|
||||
current_change_type = change_type
|
||||
|
||||
current_value.append(value)
|
||||
|
||||
if current_value:
|
||||
merged_changes.append((current_change_type, "".join(current_value)))
|
||||
|
||||
return TextDiff(merged_changes)
|
||||
38
env/lib/python3.10/site-packages/wagtail/admin/datetimepicker.py
vendored
Normal file
38
env/lib/python3.10/site-packages/wagtail/admin/datetimepicker.py
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# Adapted from https://djangosnippets.org/snippets/10563/
|
||||
# original author bernd-wechner
|
||||
|
||||
|
||||
def to_datetimepicker_format(python_format_string):
|
||||
"""
|
||||
Given a python datetime format string, attempts to convert it to
|
||||
the nearest PHP datetime format string possible.
|
||||
"""
|
||||
python2PHP = {
|
||||
"%a": "D",
|
||||
"%A": "l",
|
||||
"%b": "M",
|
||||
"%B": "F",
|
||||
"%c": "",
|
||||
"%d": "d",
|
||||
"%H": "H",
|
||||
"%I": "h",
|
||||
"%j": "z",
|
||||
"%m": "m",
|
||||
"%M": "i",
|
||||
"%p": "A",
|
||||
"%S": "s",
|
||||
"%U": "",
|
||||
"%w": "w",
|
||||
"%W": "W",
|
||||
"%x": "",
|
||||
"%X": "",
|
||||
"%y": "y",
|
||||
"%Y": "Y",
|
||||
"%Z": "e",
|
||||
}
|
||||
|
||||
php_format_string = python_format_string
|
||||
for py, php in python2PHP.items():
|
||||
php_format_string = php_format_string.replace(py, php)
|
||||
|
||||
return php_format_string
|
||||
262
env/lib/python3.10/site-packages/wagtail/admin/filters.py
vendored
Normal file
262
env/lib/python3.10/site-packages/wagtail/admin/filters.py
vendored
Normal file
@@ -0,0 +1,262 @@
|
||||
import django_filters
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_filters.widgets import SuffixedMultiWidget
|
||||
|
||||
from wagtail.admin.models import popular_tags_for_model
|
||||
from wagtail.admin.utils import get_user_display_name
|
||||
from wagtail.admin.widgets import AdminDateInput, BooleanRadioSelect, FilteredSelect
|
||||
from wagtail.coreutils import get_content_languages, get_content_type_label
|
||||
|
||||
|
||||
class DateRangePickerWidget(SuffixedMultiWidget):
|
||||
"""
|
||||
A widget allowing a start and end date to be picked.
|
||||
"""
|
||||
|
||||
template_name = "wagtailadmin/widgets/daterange_input.html"
|
||||
suffixes = ["from", "to"]
|
||||
|
||||
def __init__(self, attrs=None):
|
||||
widgets = (
|
||||
AdminDateInput(attrs={"placeholder": _("Date from")}),
|
||||
AdminDateInput(attrs={"placeholder": _("Date to")}),
|
||||
)
|
||||
super().__init__(widgets, attrs)
|
||||
|
||||
def decompress(self, value):
|
||||
if value:
|
||||
return [value.start, value.stop]
|
||||
return [None, None]
|
||||
|
||||
|
||||
class FilteredModelChoiceIterator(django_filters.fields.ModelChoiceIterator):
|
||||
"""
|
||||
A variant of Django's ModelChoiceIterator that, instead of yielding (value, label) tuples,
|
||||
returns (value, label, filter_value) so that FilteredSelect can drop filter_value into
|
||||
the data-filter-value attribute.
|
||||
"""
|
||||
|
||||
def choice(self, obj):
|
||||
return (
|
||||
self.field.prepare_value(obj),
|
||||
self.field.label_from_instance(obj),
|
||||
self.field.get_filter_value(obj),
|
||||
)
|
||||
|
||||
|
||||
class FilteredModelChoiceField(django_filters.fields.ModelChoiceField):
|
||||
"""
|
||||
A ModelChoiceField that uses FilteredSelect to dynamically show/hide options based on another
|
||||
ModelChoiceField of related objects; an option will be shown whenever the selected related
|
||||
object is present in the result of filter_accessor for that option.
|
||||
|
||||
filter_field - the HTML `id` of the related ModelChoiceField
|
||||
filter_accessor - either the name of a relation, property or method on the model instance which
|
||||
returns a queryset of related objects, or a function which accepts the model instance and
|
||||
returns such a queryset.
|
||||
"""
|
||||
|
||||
widget = FilteredSelect
|
||||
iterator = FilteredModelChoiceIterator
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.filter_accessor = kwargs.pop("filter_accessor")
|
||||
filter_field = kwargs.pop("filter_field")
|
||||
super().__init__(*args, **kwargs)
|
||||
self.widget.filter_field = filter_field
|
||||
|
||||
def get_filter_value(self, obj):
|
||||
# Use filter_accessor to obtain a queryset of related objects
|
||||
if callable(self.filter_accessor):
|
||||
queryset = self.filter_accessor(obj)
|
||||
else:
|
||||
# treat filter_accessor as a method/property name of obj
|
||||
queryset = getattr(obj, self.filter_accessor)
|
||||
if isinstance(queryset, models.Manager):
|
||||
queryset = queryset.all()
|
||||
elif callable(queryset):
|
||||
queryset = queryset()
|
||||
|
||||
# Turn this queryset into a list of IDs that will become the 'data-filter-value' used to
|
||||
# filter this listing
|
||||
return queryset.values_list("pk", flat=True)
|
||||
|
||||
|
||||
class FilteredModelChoiceFilter(django_filters.ModelChoiceFilter):
|
||||
field_class = FilteredModelChoiceField
|
||||
|
||||
|
||||
class LocaleFilter(django_filters.ChoiceFilter):
|
||||
def filter(self, qs, language_code):
|
||||
if language_code:
|
||||
return qs.filter(locale__language_code=language_code)
|
||||
return qs
|
||||
|
||||
|
||||
class WagtailFilterSet(django_filters.FilterSet):
|
||||
def __init__(self, data=None, queryset=None, *, request=None, prefix=None):
|
||||
super().__init__(data, queryset, request=request, prefix=prefix)
|
||||
|
||||
if getattr(settings, "WAGTAIL_I18N_ENABLED", False):
|
||||
self._add_locale_filter()
|
||||
|
||||
def _add_locale_filter(self):
|
||||
# Add a locale filter if the model is translatable
|
||||
# and there isn't one already.
|
||||
from wagtail.models.i18n import Locale, TranslatableMixin
|
||||
|
||||
if (
|
||||
self._meta.model
|
||||
and issubclass(self._meta.model, TranslatableMixin)
|
||||
and "locale" not in self.filters
|
||||
):
|
||||
# Only add the locale filter if there are multiple content languages
|
||||
# in the settings and the corresponding Locales exist.
|
||||
languages = get_content_languages()
|
||||
locales = set(Locale.objects.values_list("language_code", flat=True))
|
||||
choices = [(k, v) for k, v in languages.items() if k in locales]
|
||||
if len(choices) <= 1:
|
||||
return
|
||||
|
||||
self.filters["locale"] = LocaleFilter(
|
||||
label=_("Locale"),
|
||||
choices=choices,
|
||||
empty_label=None,
|
||||
null_label=_("All"),
|
||||
null_value=None,
|
||||
widget=forms.RadioSelect,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def filter_for_lookup(cls, field, lookup_type):
|
||||
filter_class, params = super().filter_for_lookup(field, lookup_type)
|
||||
|
||||
if filter_class == django_filters.ChoiceFilter:
|
||||
params.setdefault("widget", forms.RadioSelect)
|
||||
params.setdefault("empty_label", _("All"))
|
||||
|
||||
elif filter_class in [django_filters.DateFilter, django_filters.DateTimeFilter]:
|
||||
params.setdefault("widget", AdminDateInput)
|
||||
|
||||
elif filter_class == django_filters.DateFromToRangeFilter:
|
||||
params.setdefault("widget", DateRangePickerWidget)
|
||||
|
||||
elif filter_class == django_filters.BooleanFilter:
|
||||
params.setdefault("widget", BooleanRadioSelect)
|
||||
|
||||
return filter_class, params
|
||||
|
||||
|
||||
class ContentTypeModelChoiceField(django_filters.fields.ModelChoiceField):
|
||||
"""
|
||||
Custom ModelChoiceField for ContentType, to show the model verbose name as the label rather
|
||||
than the default 'wagtailcore | page' representation of a ContentType
|
||||
"""
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
return get_content_type_label(obj)
|
||||
|
||||
|
||||
class ContentTypeFilter(django_filters.ModelChoiceFilter):
|
||||
field_class = ContentTypeModelChoiceField
|
||||
|
||||
|
||||
class ContentTypeModelMultipleChoiceField(
|
||||
django_filters.fields.ModelMultipleChoiceField
|
||||
):
|
||||
"""
|
||||
Custom ModelMultipleChoiceField for ContentType, to show the model verbose name as the label rather
|
||||
than the default 'wagtailcore | page' representation of a ContentType
|
||||
"""
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
return get_content_type_label(obj)
|
||||
|
||||
|
||||
class MultipleContentTypeFilter(django_filters.ModelMultipleChoiceFilter):
|
||||
field_class = ContentTypeModelMultipleChoiceField
|
||||
|
||||
|
||||
class UserModelMultipleChoiceField(django_filters.fields.ModelMultipleChoiceField):
|
||||
"""
|
||||
Custom ModelMultipleChoiceField for user models, to show the result of
|
||||
get_user_display_name as the label rather than the default string representation
|
||||
"""
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
return get_user_display_name(obj)
|
||||
|
||||
|
||||
class MultipleUserFilter(django_filters.ModelMultipleChoiceFilter):
|
||||
field_class = UserModelMultipleChoiceField
|
||||
|
||||
|
||||
class CollectionChoiceIterator(django_filters.fields.ModelChoiceIterator):
|
||||
@cached_property
|
||||
def min_depth(self):
|
||||
return self.queryset.get_min_depth()
|
||||
|
||||
def choice(self, obj):
|
||||
return (obj.pk, obj.get_indented_name(self.min_depth, html=True))
|
||||
|
||||
|
||||
class CollectionChoiceField(django_filters.fields.ModelChoiceField):
|
||||
iterator = CollectionChoiceIterator
|
||||
|
||||
|
||||
class CollectionFilter(django_filters.ModelChoiceFilter):
|
||||
field_class = CollectionChoiceField
|
||||
|
||||
|
||||
class PopularTagsFilter(django_filters.MultipleChoiceFilter):
|
||||
# This uses a MultipleChoiceFilter instead of a ModelMultipleChoiceFilter
|
||||
# because the queryset has been sliced, which means ModelMultipleChoiceFilter
|
||||
# cannot do further queries to validate the selected tags.
|
||||
|
||||
def __init__(self, *args, use_subquery=False, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.use_subquery = use_subquery
|
||||
|
||||
def filter(self, qs, value):
|
||||
filtered = super().filter(qs, value)
|
||||
if not self.use_subquery:
|
||||
return filtered
|
||||
|
||||
# Workaround for https://github.com/wagtail/wagtail/issues/6616
|
||||
pks = list(filtered.values_list("pk", flat=True))
|
||||
return qs.filter(pk__in=pks)
|
||||
|
||||
|
||||
class BaseMediaFilterSet(WagtailFilterSet):
|
||||
permission_policy = None
|
||||
|
||||
def __init__(
|
||||
self, data=None, queryset=None, *, request=None, prefix=None, is_searching=None
|
||||
):
|
||||
super().__init__(data, queryset, request=request, prefix=prefix)
|
||||
collections_qs = self.permission_policy.collections_user_has_any_permission_for(
|
||||
request.user, ["add", "change"]
|
||||
)
|
||||
# Add collection filter only if there are multiple collections
|
||||
if collections_qs.count() > 1:
|
||||
self.filters["collection_id"] = CollectionFilter(
|
||||
field_name="collection_id",
|
||||
label=_("Collection"),
|
||||
queryset=collections_qs,
|
||||
)
|
||||
|
||||
popular_tags = popular_tags_for_model(self._meta.model)
|
||||
|
||||
if popular_tags:
|
||||
self.filters["tag"] = PopularTagsFilter(
|
||||
label=_("Tag"),
|
||||
field_name="tags__name",
|
||||
choices=[(tag.name, tag.name) for tag in popular_tags],
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
use_subquery=is_searching,
|
||||
help_text=_("Filter by up to ten most popular tags."),
|
||||
)
|
||||
9
env/lib/python3.10/site-packages/wagtail/admin/forms/__init__.py
vendored
Normal file
9
env/lib/python3.10/site-packages/wagtail/admin/forms/__init__.py
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# definitions which are not being deprecated from wagtail.admin.forms
|
||||
from .models import ( # NOQA: F401
|
||||
DIRECT_FORM_FIELD_OVERRIDES,
|
||||
FORM_FIELD_OVERRIDES,
|
||||
WagtailAdminModelForm,
|
||||
WagtailAdminModelFormMetaclass,
|
||||
formfield_for_dbfield,
|
||||
)
|
||||
from .pages import WagtailAdminPageForm # NOQA: F401
|
||||
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/account.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/account.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/auth.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/auth.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/choosers.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/choosers.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/collections.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/collections.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/comments.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/comments.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/models.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/models.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/pages.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/pages.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/search.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/search.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/tags.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/tags.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/view_restrictions.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/view_restrictions.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/workflows.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/forms/__pycache__/workflows.cpython-310.pyc
vendored
Normal file
Binary file not shown.
143
env/lib/python3.10/site-packages/wagtail/admin/forms/account.py
vendored
Normal file
143
env/lib/python3.10/site-packages/wagtail/admin/forms/account.py
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
import warnings
|
||||
from operator import itemgetter
|
||||
|
||||
import l18n
|
||||
from django import forms
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models.fields import BLANK_CHOICE_DASH
|
||||
from django.utils.translation import get_language_info
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from wagtail.admin.localization import (
|
||||
get_available_admin_languages,
|
||||
get_available_admin_time_zones,
|
||||
)
|
||||
from wagtail.admin.widgets import SwitchInput
|
||||
from wagtail.permissions import page_permission_policy
|
||||
from wagtail.users.models import UserProfile
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class NotificationPreferencesForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
permission_policy = page_permission_policy
|
||||
if not permission_policy.user_has_permission(self.instance.user, "publish"):
|
||||
del self.fields["submitted_notifications"]
|
||||
if not permission_policy.user_has_permission(self.instance.user, "change"):
|
||||
del self.fields["approved_notifications"]
|
||||
del self.fields["rejected_notifications"]
|
||||
del self.fields["updated_comments_notifications"]
|
||||
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = [
|
||||
"submitted_notifications",
|
||||
"approved_notifications",
|
||||
"rejected_notifications",
|
||||
"updated_comments_notifications",
|
||||
]
|
||||
widgets = {
|
||||
"submitted_notifications": SwitchInput(),
|
||||
"approved_notifications": SwitchInput(),
|
||||
"rejected_notifications": SwitchInput(),
|
||||
"updated_comments_notifications": SwitchInput(),
|
||||
}
|
||||
|
||||
|
||||
def _get_language_choices():
|
||||
language_choices = [
|
||||
(lang_code, get_language_info(lang_code)["name_local"])
|
||||
for lang_code, lang_name in get_available_admin_languages()
|
||||
]
|
||||
return sorted(
|
||||
BLANK_CHOICE_DASH + language_choices,
|
||||
key=lambda language_choice: language_choice[1].lower(),
|
||||
)
|
||||
|
||||
|
||||
def _get_time_zone_choices():
|
||||
time_zones = [
|
||||
(tz, str(l18n.tz_fullnames.get(tz, tz)))
|
||||
for tz in get_available_admin_time_zones()
|
||||
]
|
||||
time_zones.sort(key=itemgetter(1))
|
||||
return BLANK_CHOICE_DASH + time_zones
|
||||
|
||||
|
||||
class LocalePreferencesForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if len(get_available_admin_languages()) <= 1:
|
||||
del self.fields["preferred_language"]
|
||||
|
||||
if len(get_available_admin_time_zones()) <= 1:
|
||||
del self.fields["current_time_zone"]
|
||||
|
||||
preferred_language = forms.ChoiceField(
|
||||
required=False, choices=_get_language_choices, label=_("Preferred language")
|
||||
)
|
||||
|
||||
current_time_zone = forms.ChoiceField(
|
||||
required=False, choices=_get_time_zone_choices, label=_("Current time zone")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = ["preferred_language", "current_time_zone"]
|
||||
|
||||
|
||||
class NameEmailForm(forms.ModelForm):
|
||||
first_name = forms.CharField(required=True, label=_("First Name"))
|
||||
last_name = forms.CharField(required=True, label=_("Last Name"))
|
||||
email = forms.EmailField(required=True, label=_("Email"))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
from wagtail.admin.views.account import email_management_enabled
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if not email_management_enabled():
|
||||
del self.fields["email"]
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["first_name", "last_name", "email"]
|
||||
|
||||
|
||||
class AvatarPreferencesForm(forms.ModelForm):
|
||||
avatar = forms.ImageField(label=_("Upload a profile picture"), required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._original_avatar = self.instance.avatar
|
||||
|
||||
def save(self, commit=True):
|
||||
if (
|
||||
commit
|
||||
and self._original_avatar
|
||||
and (self._original_avatar != self.cleaned_data["avatar"])
|
||||
):
|
||||
# Call delete() on the storage backend directly, as calling self._original_avatar.delete()
|
||||
# will clear the now-updated field on self.instance too
|
||||
try:
|
||||
self._original_avatar.storage.delete(self._original_avatar.name)
|
||||
except OSError:
|
||||
# failure to delete the old avatar shouldn't prevent us from continuing
|
||||
warnings.warn(
|
||||
"Failed to delete old avatar file: %s" % self._original_avatar.name
|
||||
)
|
||||
super().save(commit=commit)
|
||||
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = ["avatar"]
|
||||
|
||||
|
||||
class ThemePreferencesForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = UserProfile
|
||||
fields = ["theme", "density"]
|
||||
81
env/lib/python3.10/site-packages/wagtail/admin/forms/auth.py
vendored
Normal file
81
env/lib/python3.10/site-packages/wagtail/admin/forms/auth.py
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from django.contrib.auth.forms import PasswordChangeForm as DjangoPasswordChangeForm
|
||||
from django.contrib.auth.forms import PasswordResetForm as DjangoPasswordResetForm
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
|
||||
class LoginForm(AuthenticationForm):
|
||||
username = forms.CharField(max_length=254, widget=forms.TextInput())
|
||||
|
||||
password = forms.CharField(
|
||||
widget=forms.PasswordInput(
|
||||
attrs={
|
||||
"placeholder": gettext_lazy("Enter password"),
|
||||
}
|
||||
),
|
||||
strip=False,
|
||||
)
|
||||
|
||||
remember = forms.BooleanField(required=False)
|
||||
|
||||
error_messages = {
|
||||
**AuthenticationForm.error_messages,
|
||||
"invalid_login": gettext_lazy(
|
||||
"Your %(username_field)s and password didn't match. Please try again."
|
||||
),
|
||||
}
|
||||
|
||||
def __init__(self, request=None, *args, **kwargs):
|
||||
super().__init__(request=request, *args, **kwargs)
|
||||
self.fields["username"].widget.attrs["placeholder"] = gettext_lazy(
|
||||
"Enter your %(username_field_name)s"
|
||||
) % {"username_field_name": self.username_field.verbose_name}
|
||||
self.fields["username"].widget.attrs["autofocus"] = ""
|
||||
|
||||
@property
|
||||
def extra_fields(self):
|
||||
for field_name in self.fields.keys():
|
||||
if field_name not in ["username", "password", "remember"]:
|
||||
yield field_name, self[field_name]
|
||||
|
||||
def get_invalid_login_error(self):
|
||||
return forms.ValidationError(
|
||||
self.error_messages["invalid_login"],
|
||||
code="invalid_login",
|
||||
params={"username_field": self.username_field.verbose_name},
|
||||
)
|
||||
|
||||
|
||||
class PasswordResetForm(DjangoPasswordResetForm):
|
||||
email = forms.EmailField(
|
||||
label=gettext_lazy("Enter your email address to reset your password"),
|
||||
max_length=254,
|
||||
required=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_fields(self):
|
||||
for field_name in self.fields.keys():
|
||||
if field_name not in ["email"]:
|
||||
yield field_name, self[field_name]
|
||||
|
||||
|
||||
class PasswordChangeForm(DjangoPasswordChangeForm):
|
||||
"""
|
||||
Since this is displayed as part of a larger form, this differs from the vanilla Django
|
||||
PasswordChangeForm as follows:
|
||||
* the old-password field is not auto-focused
|
||||
* Fields are not marked as required
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
try:
|
||||
del self.fields["old_password"].widget.attrs["autofocus"]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self.fields["old_password"].required = False
|
||||
self.fields["new_password1"].required = False
|
||||
self.fields["new_password2"].required = False
|
||||
151
env/lib/python3.10/site-packages/wagtail/admin/forms/choosers.py
vendored
Normal file
151
env/lib/python3.10/site-packages/wagtail/admin/forms/choosers.py
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
import warnings
|
||||
|
||||
from django import forms
|
||||
from django.core import validators
|
||||
from django.forms.widgets import TextInput
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from wagtail.models import Locale
|
||||
from wagtail.search.backends import get_search_backend
|
||||
|
||||
|
||||
class URLOrAbsolutePathValidator(validators.URLValidator):
|
||||
@staticmethod
|
||||
def is_absolute_path(value):
|
||||
return value.startswith("/")
|
||||
|
||||
def __call__(self, value):
|
||||
if URLOrAbsolutePathValidator.is_absolute_path(value):
|
||||
return None
|
||||
else:
|
||||
return super().__call__(value)
|
||||
|
||||
|
||||
class URLOrAbsolutePathField(forms.URLField):
|
||||
widget = TextInput
|
||||
default_validators = [URLOrAbsolutePathValidator()]
|
||||
|
||||
def to_python(self, value):
|
||||
if not URLOrAbsolutePathValidator.is_absolute_path(value):
|
||||
value = super().to_python(value)
|
||||
return value
|
||||
|
||||
|
||||
class ExternalLinkChooserForm(forms.Form):
|
||||
url = URLOrAbsolutePathField(required=True, label=_("URL"))
|
||||
link_text = forms.CharField(required=False)
|
||||
|
||||
|
||||
class AnchorLinkChooserForm(forms.Form):
|
||||
url = forms.CharField(required=True, label="#")
|
||||
link_text = forms.CharField(required=False)
|
||||
|
||||
|
||||
class EmailLinkChooserForm(forms.Form):
|
||||
email_address = forms.EmailField(required=True)
|
||||
link_text = forms.CharField(required=False)
|
||||
subject = forms.CharField(required=False)
|
||||
body = forms.CharField(required=False, widget=forms.Textarea(attrs={"rows": 3}))
|
||||
|
||||
|
||||
class PhoneLinkChooserForm(forms.Form):
|
||||
phone_number = forms.CharField(required=True)
|
||||
link_text = forms.CharField(required=False)
|
||||
|
||||
|
||||
class BaseFilterForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.is_searching = False
|
||||
self.is_filtering_by_collection = False
|
||||
self.search_query = None
|
||||
|
||||
def filter(self, objects):
|
||||
return objects
|
||||
|
||||
|
||||
class SearchFilterMixin(forms.Form):
|
||||
"""
|
||||
Mixin for a chooser listing filter form, to provide a search field
|
||||
"""
|
||||
|
||||
q = forms.CharField(
|
||||
label=_("Search term"),
|
||||
widget=forms.TextInput(attrs={"placeholder": _("Search")}),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def filter(self, objects):
|
||||
objects = super().filter(objects)
|
||||
search_query = self.cleaned_data.get("q")
|
||||
if search_query:
|
||||
search_backend = get_search_backend()
|
||||
if objects.model.get_autocomplete_search_fields():
|
||||
objects = search_backend.autocomplete(search_query, objects)
|
||||
else:
|
||||
# fall back on non-autocompleting search
|
||||
warnings.warn(
|
||||
f"{objects.model} is defined as Indexable but does not specify "
|
||||
"any AutocompleteFields. Searches within the chooser will only "
|
||||
"respond to complete words.",
|
||||
category=RuntimeWarning,
|
||||
)
|
||||
|
||||
objects = search_backend.search(search_query, objects)
|
||||
self.is_searching = True
|
||||
self.search_query = search_query
|
||||
return objects
|
||||
|
||||
|
||||
class CollectionFilterMixin(forms.Form):
|
||||
"""
|
||||
Mixin for a chooser listing filter form, to provide a collection filter field.
|
||||
The view must pass a `collections` keyword argument when constructing the form
|
||||
"""
|
||||
|
||||
def __init__(self, *args, collections=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if collections:
|
||||
collection_choices = [
|
||||
("", _("All collections"))
|
||||
] + collections.get_indented_choices()
|
||||
self.fields["collection_id"] = forms.ChoiceField(
|
||||
label=_("Collection"),
|
||||
choices=collection_choices,
|
||||
required=False,
|
||||
widget=forms.Select(attrs={"data-chooser-modal-search-filter": True}),
|
||||
)
|
||||
|
||||
def filter(self, objects):
|
||||
collection_id = self.cleaned_data.get("collection_id")
|
||||
if collection_id:
|
||||
self.is_filtering_by_collection = True
|
||||
objects = objects.filter(collection=collection_id)
|
||||
return super().filter(objects)
|
||||
|
||||
|
||||
class LocaleFilterMixin(forms.Form):
|
||||
"""
|
||||
Mixin for a chooser listing filter form, to provide a locale filter field.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
locales = Locale.objects.all()
|
||||
if locales:
|
||||
self.fields["locale"] = forms.ChoiceField(
|
||||
choices=[
|
||||
(locale.language_code, locale.get_display_name())
|
||||
for locale in locales
|
||||
],
|
||||
required=False,
|
||||
widget=forms.Select(attrs={"data-chooser-modal-search-filter": True}),
|
||||
)
|
||||
|
||||
def filter(self, objects):
|
||||
selected_locale_code = self.cleaned_data.get("locale")
|
||||
if selected_locale_code:
|
||||
selected_locale = Locale.objects.get(language_code=selected_locale_code)
|
||||
objects = objects.filter(locale=selected_locale)
|
||||
return super().filter(objects)
|
||||
384
env/lib/python3.10/site-packages/wagtail/admin/forms/collections.py
vendored
Normal file
384
env/lib/python3.10/site-packages/wagtail/admin/forms/collections.py
vendored
Normal file
@@ -0,0 +1,384 @@
|
||||
from itertools import groupby
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import Group, Permission
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.db.models import Min
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from wagtail.models import (
|
||||
Collection,
|
||||
CollectionViewRestriction,
|
||||
GroupCollectionPermission,
|
||||
)
|
||||
|
||||
from .view_restrictions import BaseViewRestrictionForm
|
||||
|
||||
|
||||
class CollectionViewRestrictionForm(BaseViewRestrictionForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if not getattr(settings, "WAGTAILDOCS_PRIVATE_COLLECTION_OPTIONS", {}).get(
|
||||
"SHARED_PASSWORD",
|
||||
True,
|
||||
):
|
||||
self.fields["restriction_type"].choices = [
|
||||
choice
|
||||
for choice in CollectionViewRestriction.RESTRICTION_CHOICES
|
||||
if choice[0] != CollectionViewRestriction.PASSWORD
|
||||
]
|
||||
del self.fields["password"]
|
||||
|
||||
class Meta:
|
||||
model = CollectionViewRestriction
|
||||
fields = ("restriction_type", "password", "groups")
|
||||
|
||||
|
||||
class SelectWithDisabledOptions(forms.Select):
|
||||
"""
|
||||
Subclass of Django's select widget that allows disabling options.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.disabled_values = ()
|
||||
|
||||
def create_option(self, name, value, *args, **kwargs):
|
||||
option_dict = super().create_option(name, value, *args, **kwargs)
|
||||
if value in self.disabled_values:
|
||||
option_dict["attrs"]["disabled"] = "disabled"
|
||||
return option_dict
|
||||
|
||||
|
||||
class CollectionChoiceField(forms.ModelChoiceField):
|
||||
widget = SelectWithDisabledOptions
|
||||
|
||||
def __init__(self, *args, disabled_queryset=None, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._indentation_start_depth = 2
|
||||
self.disabled_queryset = disabled_queryset
|
||||
|
||||
def _get_disabled_queryset(self):
|
||||
return self._disabled_queryset
|
||||
|
||||
def _set_disabled_queryset(self, queryset):
|
||||
self._disabled_queryset = queryset
|
||||
if queryset is None:
|
||||
self.widget.disabled_values = ()
|
||||
else:
|
||||
self.widget.disabled_values = queryset.values_list(
|
||||
self.to_field_name or "pk", flat=True
|
||||
)
|
||||
|
||||
disabled_queryset = property(_get_disabled_queryset, _set_disabled_queryset)
|
||||
|
||||
def _set_queryset(self, queryset):
|
||||
min_depth = self.queryset.aggregate(Min("depth"))["depth__min"]
|
||||
if min_depth is None:
|
||||
self._indentation_start_depth = 2
|
||||
else:
|
||||
self._indentation_start_depth = min_depth + 1
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
return obj.get_indented_name(self._indentation_start_depth, html=True)
|
||||
|
||||
|
||||
class CollectionForm(forms.ModelForm):
|
||||
parent = CollectionChoiceField(
|
||||
label=gettext_lazy("Parent"),
|
||||
queryset=Collection.objects.all(),
|
||||
required=True,
|
||||
help_text=gettext_lazy(
|
||||
"Select hierarchical position. Note: a collection cannot become a child of itself or one of its "
|
||||
"descendants."
|
||||
),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Collection
|
||||
fields = ("name",)
|
||||
|
||||
def clean_parent(self):
|
||||
"""
|
||||
Our rules about where a user may add or move a collection are as follows:
|
||||
1. The user must have 'add' permission on the parent collection (or its ancestors)
|
||||
2. We are not moving a collection used to assign permissions for this user
|
||||
3. We are not trying to move a collection to be parented by one of their descendants
|
||||
|
||||
The first 2 items are taken care in the Create and Edit views by deleting the 'parent' field
|
||||
from the edit form if the user cannot move the collection. This causes Django's form
|
||||
machinery to ignore the parent field for parent regardless of what the user submits.
|
||||
This methods enforces rule #3 when we are editing an existing collection.
|
||||
"""
|
||||
parent = self.cleaned_data["parent"]
|
||||
if not self.instance._state.adding and not parent.pk == self.initial.get(
|
||||
"parent"
|
||||
):
|
||||
old_descendants = list(
|
||||
self.instance.get_descendants(inclusive=True).values_list(
|
||||
"pk", flat=True
|
||||
)
|
||||
)
|
||||
if parent.pk in old_descendants:
|
||||
raise ValidationError(gettext_lazy("Please select another parent"))
|
||||
return parent
|
||||
|
||||
|
||||
class BaseCollectionMemberForm(forms.ModelForm):
|
||||
"""
|
||||
Abstract form handler for editing models that belong to a collection,
|
||||
such as documents and images. These forms are (optionally) instantiated
|
||||
with a 'user' kwarg, and take care of populating the 'collection' field's
|
||||
choices with the collections the user has permission for, as well as
|
||||
hiding the field when only one collection is available.
|
||||
|
||||
Subclasses must define a 'permission_policy' attribute.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
user = kwargs.pop("user", None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if user is None:
|
||||
self.collections = Collection.objects.all()
|
||||
else:
|
||||
self.collections = (
|
||||
self.permission_policy.collections_user_has_permission_for(user, "add")
|
||||
)
|
||||
|
||||
if self.instance.pk:
|
||||
# editing an existing document; ensure that the list of available collections
|
||||
# includes its current collection
|
||||
self.collections = self.collections | Collection.objects.filter(
|
||||
id=self.instance.collection_id
|
||||
)
|
||||
|
||||
if len(self.collections) == 0:
|
||||
raise Exception(
|
||||
"Cannot construct %s for a user with no collection permissions"
|
||||
% type(self)
|
||||
)
|
||||
elif len(self.collections) == 1:
|
||||
# don't show collection field if only one collection is available
|
||||
del self.fields["collection"]
|
||||
else:
|
||||
self.fields["collection"].queryset = self.collections
|
||||
|
||||
def save(self, commit=True):
|
||||
if len(self.collections) == 1:
|
||||
# populate the instance's collection field with the one available collection
|
||||
self.instance.collection = self.collections[0]
|
||||
|
||||
return super().save(commit=commit)
|
||||
|
||||
|
||||
class BaseGroupCollectionMemberPermissionFormSet(forms.BaseFormSet):
|
||||
"""
|
||||
A base formset class for managing GroupCollectionPermissions for a
|
||||
model with CollectionMember behaviour. Subclasses should provide attributes:
|
||||
permission_types - a list of (codename, short_label, long_label) tuples for the permissions
|
||||
being managed here
|
||||
permission_queryset - a queryset of Permission objects for the above permissions
|
||||
default_prefix - prefix to use on form fields if one is not specified in __init__
|
||||
template = template filename
|
||||
"""
|
||||
|
||||
def __init__(self, data=None, files=None, instance=None, prefix=None):
|
||||
if prefix is None:
|
||||
prefix = self.default_prefix
|
||||
|
||||
if instance is None:
|
||||
instance = Group()
|
||||
|
||||
if instance.pk is None:
|
||||
full_collection_permissions = []
|
||||
else:
|
||||
full_collection_permissions = (
|
||||
instance.collection_permissions.filter(
|
||||
permission__in=self.permission_queryset
|
||||
)
|
||||
.select_related("permission__content_type", "collection")
|
||||
.order_by("collection")
|
||||
)
|
||||
|
||||
self.instance = instance
|
||||
|
||||
initial_data = []
|
||||
|
||||
for collection, collection_permissions in groupby(
|
||||
full_collection_permissions,
|
||||
lambda cp: cp.collection,
|
||||
):
|
||||
initial_data.append(
|
||||
{
|
||||
"collection": collection,
|
||||
"permissions": [cp.permission for cp in collection_permissions],
|
||||
}
|
||||
)
|
||||
|
||||
super().__init__(data, files, initial=initial_data, prefix=prefix)
|
||||
for form in self.forms:
|
||||
form.fields["DELETE"].widget = forms.HiddenInput()
|
||||
|
||||
@property
|
||||
def empty_form(self):
|
||||
empty_form = super().empty_form
|
||||
empty_form.fields["DELETE"].widget = forms.HiddenInput()
|
||||
return empty_form
|
||||
|
||||
def clean(self):
|
||||
"""Checks that no two forms refer to the same collection object"""
|
||||
if any(self.errors):
|
||||
# Don't bother validating the formset unless each form is valid on its own
|
||||
return
|
||||
|
||||
collections = [
|
||||
form.cleaned_data["collection"]
|
||||
for form in self.forms
|
||||
# need to check for presence of 'collection' in cleaned_data,
|
||||
# because a completely blank form passes validation
|
||||
if form not in self.deleted_forms and "collection" in form.cleaned_data
|
||||
]
|
||||
if len(set(collections)) != len(collections):
|
||||
# collections list contains duplicates
|
||||
raise forms.ValidationError(
|
||||
_(
|
||||
"You cannot have multiple permission records for the same collection."
|
||||
)
|
||||
)
|
||||
|
||||
@transaction.atomic
|
||||
def save(self):
|
||||
if self.instance.pk is None:
|
||||
raise Exception(
|
||||
"Cannot save a GroupCollectionMemberPermissionFormSet "
|
||||
"for an unsaved group instance"
|
||||
)
|
||||
|
||||
# get a set of (collection, permission) tuples for all ticked permissions
|
||||
forms_to_save = [
|
||||
form
|
||||
for form in self.forms
|
||||
if form not in self.deleted_forms and "collection" in form.cleaned_data
|
||||
]
|
||||
|
||||
final_permission_records = set()
|
||||
for form in forms_to_save:
|
||||
for permission in form.cleaned_data["permissions"]:
|
||||
final_permission_records.add(
|
||||
(form.cleaned_data["collection"], permission)
|
||||
)
|
||||
|
||||
# fetch the group's existing collection permission records for this model,
|
||||
# and from that, build a list of records to be created / deleted
|
||||
permission_ids_to_delete = []
|
||||
permission_records_to_keep = set()
|
||||
|
||||
for cp in self.instance.collection_permissions.filter(
|
||||
permission__in=self.permission_queryset,
|
||||
):
|
||||
if (cp.collection, cp.permission) in final_permission_records:
|
||||
permission_records_to_keep.add((cp.collection, cp.permission))
|
||||
else:
|
||||
permission_ids_to_delete.append(cp.id)
|
||||
|
||||
self.instance.collection_permissions.filter(
|
||||
id__in=permission_ids_to_delete
|
||||
).delete()
|
||||
|
||||
permissions_to_add = final_permission_records - permission_records_to_keep
|
||||
GroupCollectionPermission.objects.bulk_create(
|
||||
[
|
||||
GroupCollectionPermission(
|
||||
group=self.instance, collection=collection, permission=permission
|
||||
)
|
||||
for (collection, permission) in permissions_to_add
|
||||
]
|
||||
)
|
||||
|
||||
def as_admin_panel(self):
|
||||
return render_to_string(
|
||||
self.template,
|
||||
{"formset": self},
|
||||
)
|
||||
|
||||
|
||||
def collection_member_permission_formset_factory(
|
||||
model, permission_types, template, default_prefix=None
|
||||
):
|
||||
permission_queryset = Permission.objects.filter(
|
||||
content_type__app_label=model._meta.app_label,
|
||||
codename__in=[
|
||||
codename for codename, short_label, long_label in permission_types
|
||||
],
|
||||
).select_related("content_type")
|
||||
|
||||
if default_prefix is None:
|
||||
default_prefix = "%s_permissions" % model._meta.model_name
|
||||
|
||||
class PermissionMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||
"""
|
||||
Allows the custom labels from ``permission_types`` to be applied to
|
||||
permission checkboxes for the ``CollectionMemberPermissionsForm`` below
|
||||
"""
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
for codename, short_label, long_label in permission_types:
|
||||
if codename == obj.codename:
|
||||
return long_label
|
||||
return str(obj)
|
||||
|
||||
class CollectionMemberPermissionsForm(forms.Form):
|
||||
"""
|
||||
For a given model with CollectionMember behaviour,
|
||||
defines the permissions that are assigned to an entity
|
||||
(such as a group or user) for a specific collection
|
||||
"""
|
||||
|
||||
collection = CollectionChoiceField(
|
||||
label=_("Collection"),
|
||||
queryset=Collection.objects.all().prefetch_related("group_permissions"),
|
||||
empty_label=None,
|
||||
)
|
||||
permissions = PermissionMultipleChoiceField(
|
||||
queryset=permission_queryset,
|
||||
required=False,
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
)
|
||||
|
||||
GroupCollectionMemberPermissionFormSet = type(
|
||||
"GroupCollectionMemberPermissionFormSet",
|
||||
(BaseGroupCollectionMemberPermissionFormSet,),
|
||||
{
|
||||
"permission_types": permission_types,
|
||||
"permission_queryset": permission_queryset,
|
||||
"default_prefix": default_prefix,
|
||||
"template": template,
|
||||
},
|
||||
)
|
||||
|
||||
return forms.formset_factory(
|
||||
CollectionMemberPermissionsForm,
|
||||
formset=GroupCollectionMemberPermissionFormSet,
|
||||
extra=0,
|
||||
can_delete=True,
|
||||
)
|
||||
|
||||
|
||||
GroupCollectionManagementPermissionFormSet = (
|
||||
collection_member_permission_formset_factory(
|
||||
Collection,
|
||||
[
|
||||
("add_collection", _("Add"), _("Add collections")),
|
||||
("change_collection", _("Edit"), _("Edit collections")),
|
||||
("delete_collection", _("Delete"), _("Delete collections")),
|
||||
],
|
||||
"wagtailadmin/permissions/includes/collection_management_permissions_form.html",
|
||||
)
|
||||
)
|
||||
87
env/lib/python3.10/site-packages/wagtail/admin/forms/comments.py
vendored
Normal file
87
env/lib/python3.10/site-packages/wagtail/admin/forms/comments.py
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
from django.forms import BooleanField, ValidationError
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
from modelcluster.forms import BaseChildFormSet
|
||||
|
||||
from .models import WagtailAdminModelForm
|
||||
|
||||
|
||||
class CommentReplyForm(WagtailAdminModelForm):
|
||||
class Meta:
|
||||
fields = ("text",)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
user = self.for_user
|
||||
|
||||
if not self.instance.pk:
|
||||
self.instance.user = user
|
||||
elif self.instance.user != user:
|
||||
# trying to edit someone else's comment reply
|
||||
if any(field for field in self.changed_data):
|
||||
# includes DELETION_FIELD_NAME, as users cannot delete each other's individual comment replies
|
||||
# if deleting a whole thread, this should be done by deleting the parent Comment instead
|
||||
self.add_error(
|
||||
None, ValidationError(_("You cannot edit another user's comment."))
|
||||
)
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class CommentForm(WagtailAdminModelForm):
|
||||
"""
|
||||
This is designed to be subclassed and have the user overridden to enable user-based validation within the edit handler system
|
||||
"""
|
||||
|
||||
resolved = BooleanField(required=False)
|
||||
|
||||
class Meta:
|
||||
formsets = {
|
||||
"replies": {
|
||||
"form": CommentReplyForm,
|
||||
"inherit_kwargs": ["for_user"],
|
||||
}
|
||||
}
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
user = self.for_user
|
||||
|
||||
if not self.instance.pk:
|
||||
self.instance.user = user
|
||||
elif self.instance.user != user:
|
||||
# trying to edit someone else's comment
|
||||
if (
|
||||
any(
|
||||
field
|
||||
for field in self.changed_data
|
||||
if field not in ["resolved", "position", "contentpath"]
|
||||
)
|
||||
or cleaned_data["contentpath"].split(".")[0]
|
||||
!= self.instance.contentpath.split(".")[0]
|
||||
):
|
||||
# users can resolve each other's base comments and change their positions within a field, or move a comment between blocks in a StreamField
|
||||
self.add_error(
|
||||
None, ValidationError(_("You cannot edit another user's comment."))
|
||||
)
|
||||
return cleaned_data
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.cleaned_data.get("resolved", False):
|
||||
if not getattr(self.instance, "resolved_at"):
|
||||
self.instance.resolved_at = now()
|
||||
self.instance.resolved_by = self.for_user
|
||||
else:
|
||||
self.instance.resolved_by = None
|
||||
self.instance.resolved_at = None
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class CommentFormSet(BaseChildFormSet):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
valid_comment_ids = [
|
||||
comment.id
|
||||
for comment in self.queryset
|
||||
if comment.has_valid_contentpath(self.instance)
|
||||
]
|
||||
self.queryset = self.queryset.filter(id__in=valid_comment_ids)
|
||||
169
env/lib/python3.10/site-packages/wagtail/admin/forms/models.py
vendored
Normal file
169
env/lib/python3.10/site-packages/wagtail/admin/forms/models.py
vendored
Normal file
@@ -0,0 +1,169 @@
|
||||
import copy
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from modelcluster.forms import ClusterForm, ClusterFormMetaclass, ClusterFormOptions
|
||||
from permissionedforms import (
|
||||
PermissionedForm,
|
||||
PermissionedFormMetaclass,
|
||||
PermissionedFormOptionsMixin,
|
||||
)
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
from wagtail.admin import widgets
|
||||
from wagtail.admin.forms.tags import TagField
|
||||
from wagtail.models import Page
|
||||
from wagtail.utils.registry import ModelFieldRegistry
|
||||
|
||||
# Define a registry of form field properties to override for a given model field
|
||||
registry = ModelFieldRegistry()
|
||||
|
||||
# Aliases to lookups in the overrides registry, for backwards compatibility
|
||||
FORM_FIELD_OVERRIDES = registry.values_by_class
|
||||
DIRECT_FORM_FIELD_OVERRIDES = registry.values_by_exact_class
|
||||
|
||||
|
||||
def register_form_field_override(
|
||||
db_field_class, to=None, override=None, exact_class=False
|
||||
):
|
||||
"""
|
||||
Define parameters for form fields to be used by WagtailAdminModelForm for a given
|
||||
database field.
|
||||
"""
|
||||
|
||||
if override is None:
|
||||
raise ImproperlyConfigured(
|
||||
"register_form_field_override must be passed an 'override' keyword argument"
|
||||
)
|
||||
|
||||
if to and db_field_class != models.ForeignKey:
|
||||
raise ImproperlyConfigured(
|
||||
"The 'to' argument on register_form_field_override is only valid for ForeignKey fields"
|
||||
)
|
||||
|
||||
registry.register(db_field_class, to=to, value=override, exact_class=exact_class)
|
||||
|
||||
|
||||
# Define built-in overrides
|
||||
|
||||
# Date / time fields
|
||||
register_form_field_override(
|
||||
models.DateField, override={"widget": widgets.AdminDateInput}
|
||||
)
|
||||
register_form_field_override(
|
||||
models.TimeField, override={"widget": widgets.AdminTimeInput}
|
||||
)
|
||||
register_form_field_override(
|
||||
models.DateTimeField, override={"widget": widgets.AdminDateTimeInput}
|
||||
)
|
||||
|
||||
# Auto-height text fields (defined as exact_class=True so that it doesn't take effect for RichTextField)
|
||||
register_form_field_override(
|
||||
models.TextField,
|
||||
override={"widget": widgets.AdminAutoHeightTextInput},
|
||||
exact_class=True,
|
||||
)
|
||||
|
||||
# Page chooser
|
||||
register_form_field_override(
|
||||
models.ForeignKey,
|
||||
to=Page,
|
||||
override=lambda db_field: {
|
||||
"widget": widgets.AdminPageChooser(target_models=[db_field.remote_field.model])
|
||||
},
|
||||
)
|
||||
|
||||
# Tag fields
|
||||
register_form_field_override(
|
||||
TaggableManager,
|
||||
override=(
|
||||
lambda db_field: {"form_class": TagField, "tag_model": db_field.related_model}
|
||||
),
|
||||
)
|
||||
|
||||
# Slug fields
|
||||
register_form_field_override(
|
||||
models.SlugField,
|
||||
override={"widget": widgets.SlugInput},
|
||||
)
|
||||
|
||||
|
||||
# Callback to allow us to override the default form fields provided for each model field.
|
||||
def formfield_for_dbfield(db_field, **kwargs):
|
||||
overrides = registry.get(db_field)
|
||||
if overrides:
|
||||
kwargs = dict(copy.deepcopy(overrides), **kwargs)
|
||||
|
||||
return db_field.formfield(**kwargs)
|
||||
|
||||
|
||||
class WagtailAdminModelFormOptions(PermissionedFormOptionsMixin, ClusterFormOptions):
|
||||
# Container for the options set in the inner 'class Meta' of a model form, supporting
|
||||
# extensions for both ClusterForm ('formsets') and PermissionedForm ('field_permissions').
|
||||
pass
|
||||
|
||||
|
||||
class WagtailAdminModelFormMetaclass(PermissionedFormMetaclass, ClusterFormMetaclass):
|
||||
options_class = WagtailAdminModelFormOptions
|
||||
|
||||
# set extra_form_count to 0, as we're creating extra forms in JS
|
||||
extra_form_count = 0
|
||||
|
||||
@classmethod
|
||||
def child_form(cls):
|
||||
return WagtailAdminModelForm
|
||||
|
||||
|
||||
class WagtailAdminModelForm(
|
||||
PermissionedForm, ClusterForm, metaclass=WagtailAdminModelFormMetaclass
|
||||
):
|
||||
def __init__(self, *args, **kwargs):
|
||||
# keep hold of the `for_user` kwarg as well as passing it on to PermissionedForm
|
||||
self.for_user = kwargs.get("for_user")
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
formfield_callback = formfield_for_dbfield
|
||||
|
||||
|
||||
# Now, any model forms built off WagtailAdminModelForm instead of ModelForm should pick up
|
||||
# the nice form fields defined in FORM_FIELD_OVERRIDES.
|
||||
|
||||
|
||||
class WagtailAdminDraftStateFormMixin:
|
||||
@property
|
||||
def show_schedule_publishing_toggle(self):
|
||||
return "go_live_at" in self.__class__.base_fields
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Check scheduled publishing fields
|
||||
go_live_at = self.cleaned_data.get("go_live_at")
|
||||
expire_at = self.cleaned_data.get("expire_at")
|
||||
|
||||
# Go live must be before expire
|
||||
if go_live_at and expire_at:
|
||||
if go_live_at > expire_at:
|
||||
msg = _("Go live date/time must be before expiry date/time")
|
||||
self.add_error("go_live_at", forms.ValidationError(msg))
|
||||
self.add_error("expire_at", forms.ValidationError(msg))
|
||||
|
||||
# Expire at must be in the future
|
||||
if expire_at and expire_at < timezone.now():
|
||||
self.add_error(
|
||||
"expire_at",
|
||||
forms.ValidationError(_("Expiry date/time must be in the future")),
|
||||
)
|
||||
|
||||
# Don't allow an existing first_published_at to be unset by clearing the field
|
||||
if (
|
||||
"first_published_at" in self.cleaned_data
|
||||
and not self.cleaned_data["first_published_at"]
|
||||
):
|
||||
del self.cleaned_data["first_published_at"]
|
||||
|
||||
return self.cleaned_data
|
||||
276
env/lib/python3.10/site-packages/wagtail/admin/forms/pages.py
vendored
Normal file
276
env/lib/python3.10/site-packages/wagtail/admin/forms/pages.py
vendored
Normal file
@@ -0,0 +1,276 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import ngettext
|
||||
|
||||
from wagtail.admin import widgets
|
||||
from wagtail.models import Page, PageViewRestriction
|
||||
|
||||
from .models import WagtailAdminModelForm
|
||||
from .view_restrictions import BaseViewRestrictionForm
|
||||
|
||||
|
||||
class CopyForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
# CopyPage must be passed a 'page' kwarg indicating the page to be copied
|
||||
self.page = kwargs.pop("page")
|
||||
self.user = kwargs.pop("user", None)
|
||||
can_publish = kwargs.pop("can_publish")
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["new_title"] = forms.CharField(
|
||||
initial=self.page.title, label=_("New title")
|
||||
)
|
||||
allow_unicode = getattr(settings, "WAGTAIL_ALLOW_UNICODE_SLUGS", True)
|
||||
self.fields["new_slug"] = forms.SlugField(
|
||||
initial=self.page.slug,
|
||||
label=_("New slug"),
|
||||
allow_unicode=allow_unicode,
|
||||
widget=widgets.SlugInput,
|
||||
)
|
||||
self.fields["new_parent_page"] = forms.ModelChoiceField(
|
||||
initial=self.page.get_parent(),
|
||||
queryset=Page.objects.all(),
|
||||
widget=widgets.AdminPageChooser(can_choose_root=True, user_perms="copy_to"),
|
||||
label=_("New parent page"),
|
||||
help_text=_("This copy will be a child of this given parent page."),
|
||||
)
|
||||
pages_to_copy = self.page.get_descendants(inclusive=True)
|
||||
subpage_count = pages_to_copy.count() - 1
|
||||
if subpage_count > 0:
|
||||
self.fields["copy_subpages"] = forms.BooleanField(
|
||||
required=False,
|
||||
initial=True,
|
||||
label=_("Copy subpages"),
|
||||
help_text=ngettext(
|
||||
"This will copy %(count)s subpage.",
|
||||
"This will copy %(count)s subpages.",
|
||||
subpage_count,
|
||||
)
|
||||
% {"count": subpage_count},
|
||||
)
|
||||
|
||||
if can_publish:
|
||||
pages_to_publish_count = pages_to_copy.live().count()
|
||||
if pages_to_publish_count > 0:
|
||||
# In the specific case that there are no subpages, customise the field label and help text
|
||||
if subpage_count == 0:
|
||||
label = _("Publish copied page")
|
||||
help_text = _(
|
||||
"This page is live. Would you like to publish its copy as well?"
|
||||
)
|
||||
else:
|
||||
label = _("Publish copies")
|
||||
help_text = ngettext(
|
||||
"%(count)s of the pages being copied is live. Would you like to publish its copy?",
|
||||
"%(count)s of the pages being copied are live. Would you like to publish their copies?",
|
||||
pages_to_publish_count,
|
||||
) % {"count": pages_to_publish_count}
|
||||
|
||||
self.fields["publish_copies"] = forms.BooleanField(
|
||||
required=False, initial=False, label=label, help_text=help_text
|
||||
)
|
||||
|
||||
# Note that only users who can publish in the new parent page can create an alias.
|
||||
# This is because alias pages must always match their original page's state.
|
||||
self.fields["alias"] = forms.BooleanField(
|
||||
required=False,
|
||||
initial=False,
|
||||
label=_("Alias"),
|
||||
help_text=_("Keep the new pages updated with future changes"),
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
# Make sure the slug isn't already in use
|
||||
slug = cleaned_data.get("new_slug")
|
||||
|
||||
# New parent page given in form or parent of source, if parent_page is empty
|
||||
parent_page = cleaned_data.get("new_parent_page") or self.page.get_parent()
|
||||
|
||||
# check if user is allowed to create a page at given location.
|
||||
if not parent_page.permissions_for_user(self.user).can_add_subpage():
|
||||
self._errors["new_parent_page"] = self.error_class(
|
||||
[
|
||||
_('You do not have permission to copy to page "%(page_title)s"')
|
||||
% {
|
||||
"page_title": parent_page.specific_deferred.get_admin_display_title()
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
# Count the pages with the same slug within the context of our copy's parent page
|
||||
if slug and parent_page.get_children().filter(slug=slug).count():
|
||||
self._errors["new_slug"] = self.error_class(
|
||||
[
|
||||
_(
|
||||
'This slug is already in use within the context of its parent page "%(parent_page_title)s"'
|
||||
)
|
||||
% {"parent_page_title": parent_page}
|
||||
]
|
||||
)
|
||||
# The slug is no longer valid, hence remove it from cleaned_data
|
||||
del cleaned_data["new_slug"]
|
||||
|
||||
# Don't allow recursive copies into self
|
||||
if cleaned_data.get("copy_subpages") and (
|
||||
self.page == parent_page or parent_page.is_descendant_of(self.page)
|
||||
):
|
||||
self._errors["new_parent_page"] = self.error_class(
|
||||
[_("You cannot copy a page into itself when copying subpages")]
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class PageViewRestrictionForm(BaseViewRestrictionForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
# get the list of private page options from the page
|
||||
private_page_options = kwargs.pop("private_page_options", [])
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if not getattr(settings, "WAGTAIL_PRIVATE_PAGE_OPTIONS", {}).get(
|
||||
"SHARED_PASSWORD", True
|
||||
):
|
||||
self.fields["restriction_type"].choices = [
|
||||
choice
|
||||
for choice in PageViewRestriction.RESTRICTION_CHOICES
|
||||
if choice[0] != PageViewRestriction.PASSWORD
|
||||
]
|
||||
del self.fields["password"]
|
||||
# Remove the fields that are not allowed for the page
|
||||
self.fields["restriction_type"].choices = [
|
||||
choice
|
||||
for choice in self.fields["restriction_type"].choices
|
||||
if choice[0] in private_page_options
|
||||
or choice[0] == PageViewRestriction.NONE
|
||||
]
|
||||
|
||||
class Meta:
|
||||
model = PageViewRestriction
|
||||
fields = ("restriction_type", "password", "groups")
|
||||
|
||||
|
||||
class WagtailAdminPageForm(WagtailAdminModelForm):
|
||||
comment_notifications = forms.BooleanField(
|
||||
widget=forms.CheckboxInput(), required=False
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data=None,
|
||||
files=None,
|
||||
parent_page=None,
|
||||
subscription=None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
self.subscription = subscription
|
||||
|
||||
initial = kwargs.pop("initial", {})
|
||||
if self.subscription:
|
||||
initial["comment_notifications"] = subscription.comment_notifications
|
||||
|
||||
super().__init__(data, files, *args, initial=initial, **kwargs)
|
||||
|
||||
self.parent_page = parent_page
|
||||
|
||||
if not self.show_comments_toggle:
|
||||
del self.fields["comment_notifications"]
|
||||
|
||||
@property
|
||||
def show_comments_toggle(self):
|
||||
return "comments" in self.__class__.formsets
|
||||
|
||||
def save(self, commit=True):
|
||||
# Save comment notifications updates to PageSubscription
|
||||
if self.show_comments_toggle and self.subscription:
|
||||
self.subscription.comment_notifications = self.cleaned_data[
|
||||
"comment_notifications"
|
||||
]
|
||||
if commit:
|
||||
self.subscription.save()
|
||||
|
||||
return super().save(commit=commit)
|
||||
|
||||
def is_valid(self):
|
||||
comments = self.formsets.get("comments")
|
||||
# Remove the comments formset if the management form is invalid
|
||||
if comments and not comments.management_form.is_valid():
|
||||
del self.formsets["comments"]
|
||||
return super().is_valid()
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
if "slug" in self.cleaned_data:
|
||||
page_slug = cleaned_data["slug"]
|
||||
if not Page._slug_is_available(page_slug, self.parent_page, self.instance):
|
||||
self.add_error(
|
||||
"slug",
|
||||
forms.ValidationError(
|
||||
_(
|
||||
"The slug '%(page_slug)s' is already in use within the parent page"
|
||||
)
|
||||
% {"page_slug": page_slug}
|
||||
),
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
class MoveForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.page_to_move = kwargs.pop("page_to_move")
|
||||
self.target_parent_models = kwargs.pop("target_parent_models")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields["new_parent_page"] = forms.ModelChoiceField(
|
||||
initial=self.page_to_move.get_parent(),
|
||||
queryset=Page.objects.all(),
|
||||
widget=widgets.AdminPageMoveChooser(
|
||||
can_choose_root=True,
|
||||
user_perms="move_to",
|
||||
target_models=self.target_parent_models,
|
||||
pages_to_move=[self.page_to_move.pk],
|
||||
),
|
||||
label=_("New parent page"),
|
||||
help_text=_("Select a new parent for this page."),
|
||||
)
|
||||
|
||||
|
||||
class ParentChooserForm(forms.Form):
|
||||
def __init__(self, child_page_type, user, *args, **kwargs):
|
||||
self.child_page_type = child_page_type
|
||||
self.user = user
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["parent_page"] = forms.ModelChoiceField(
|
||||
queryset=Page.objects.all(),
|
||||
widget=widgets.AdminPageChooser(
|
||||
target_models=self.child_page_type.allowed_parent_page_models(),
|
||||
can_choose_root=True,
|
||||
user_perms="add_subpage",
|
||||
),
|
||||
label=_("Parent page"),
|
||||
help_text=_("The new page will be a child of this given parent page."),
|
||||
)
|
||||
|
||||
def clean_parent_page(self):
|
||||
parent_page = self.cleaned_data["parent_page"].specific_deferred
|
||||
if not parent_page.permissions_for_user(self.user).can_add_subpage():
|
||||
raise forms.ValidationError(
|
||||
_('You do not have permission to create a page under "%(page_title)s".')
|
||||
% {"page_title": parent_page.get_admin_display_title()}
|
||||
)
|
||||
if not self.child_page_type.can_create_at(parent_page):
|
||||
raise forms.ValidationError(
|
||||
_(
|
||||
'You cannot create a page of type "%(page_type)s" under "%(page_title)s".'
|
||||
)
|
||||
% {
|
||||
"page_type": self.child_page_type.get_verbose_name(),
|
||||
"page_title": parent_page.get_admin_display_title(),
|
||||
}
|
||||
)
|
||||
return parent_page
|
||||
19
env/lib/python3.10/site-packages/wagtail/admin/forms/search.py
vendored
Normal file
19
env/lib/python3.10/site-packages/wagtail/admin/forms/search.py
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
from django import forms
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
|
||||
class SearchForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
placeholder = kwargs.pop("placeholder", _("Search…"))
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["q"].widget.attrs = {
|
||||
"placeholder": placeholder,
|
||||
"data-w-swap-target": "input",
|
||||
}
|
||||
|
||||
q = forms.CharField(
|
||||
label=gettext_lazy("Search term"),
|
||||
widget=forms.TextInput(),
|
||||
required=False,
|
||||
)
|
||||
68
env/lib/python3.10/site-packages/wagtail/admin/forms/tags.py
vendored
Normal file
68
env/lib/python3.10/site-packages/wagtail/admin/forms/tags.py
vendored
Normal file
@@ -0,0 +1,68 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from taggit.forms import TagField as TaggitTagField
|
||||
from taggit.models import Tag, TagBase
|
||||
|
||||
from wagtail.admin.widgets import AdminTagWidget
|
||||
|
||||
|
||||
def validate_tag_length(
|
||||
value, max_tag_length=TagBase._meta.get_field("name").max_length
|
||||
):
|
||||
if not value:
|
||||
return
|
||||
value_too_long = ""
|
||||
for val in value:
|
||||
if len(val) > max_tag_length:
|
||||
if value_too_long:
|
||||
value_too_long += ", "
|
||||
value_too_long += val
|
||||
if value_too_long:
|
||||
raise ValidationError(
|
||||
_("Tag(s) %(value_too_long)s are over %(max_tag_length)d characters")
|
||||
% {
|
||||
"value_too_long": value_too_long,
|
||||
"max_tag_length": max_tag_length,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class TagField(TaggitTagField):
|
||||
"""
|
||||
Extends taggit's TagField with the option to prevent creating tags that do not already exist
|
||||
"""
|
||||
|
||||
widget = AdminTagWidget
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.tag_model = kwargs.pop("tag_model", None)
|
||||
self.free_tagging = kwargs.pop("free_tagging", None)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# pass on tag_model and free_tagging kwargs to the widget,
|
||||
# if (and only if) they have been passed explicitly here.
|
||||
# Otherwise, set default values for clean() to use
|
||||
if self.tag_model is None:
|
||||
self.tag_model = Tag
|
||||
else:
|
||||
self.widget.tag_model = self.tag_model
|
||||
|
||||
if self.free_tagging is None:
|
||||
self.free_tagging = getattr(self.tag_model, "free_tagging", True)
|
||||
else:
|
||||
self.widget.free_tagging = self.free_tagging
|
||||
|
||||
def clean(self, value):
|
||||
value = super().clean(value)
|
||||
validate_tag_length(value, self.tag_model.name.field.max_length)
|
||||
|
||||
if not self.free_tagging:
|
||||
# filter value to just the tags that already exist in tag_model
|
||||
value = list(
|
||||
self.tag_model.objects.filter(name__in=value).values_list(
|
||||
"name", flat=True
|
||||
)
|
||||
)
|
||||
|
||||
return value
|
||||
44
env/lib/python3.10/site-packages/wagtail/admin/forms/view_restrictions.py
vendored
Normal file
44
env/lib/python3.10/site-packages/wagtail/admin/forms/view_restrictions.py
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
from django import forms
|
||||
from django.contrib.auth.models import Group
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from wagtail.models import BaseViewRestriction
|
||||
|
||||
|
||||
class BaseViewRestrictionForm(forms.ModelForm):
|
||||
restriction_type = forms.ChoiceField(
|
||||
label=gettext_lazy("Visibility"),
|
||||
choices=BaseViewRestriction.RESTRICTION_CHOICES,
|
||||
widget=forms.RadioSelect,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields["groups"].widget = forms.CheckboxSelectMultiple()
|
||||
self.fields["groups"].queryset = Group.objects.all()
|
||||
|
||||
def clean_password(self):
|
||||
password = self.cleaned_data.get("password")
|
||||
if (
|
||||
self.cleaned_data.get("restriction_type") == BaseViewRestriction.PASSWORD
|
||||
and not password
|
||||
):
|
||||
raise forms.ValidationError(_("This field is required."), code="invalid")
|
||||
return password
|
||||
|
||||
def clean_groups(self):
|
||||
groups = self.cleaned_data.get("groups")
|
||||
if (
|
||||
self.cleaned_data.get("restriction_type") == BaseViewRestriction.GROUPS
|
||||
and not groups
|
||||
):
|
||||
raise forms.ValidationError(
|
||||
_("Please select at least one group."), code="invalid"
|
||||
)
|
||||
return groups
|
||||
|
||||
class Meta:
|
||||
model = BaseViewRestriction
|
||||
fields = ("restriction_type", "password", "groups")
|
||||
326
env/lib/python3.10/site-packages/wagtail/admin/forms/workflows.py
vendored
Normal file
326
env/lib/python3.10/site-packages/wagtail/admin/forms/workflows.py
vendored
Normal file
@@ -0,0 +1,326 @@
|
||||
from django import forms
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from wagtail.admin import widgets
|
||||
from wagtail.admin.forms import WagtailAdminModelForm
|
||||
from wagtail.admin.panels import FieldPanel, InlinePanel, ObjectList
|
||||
from wagtail.admin.widgets.workflows import AdminTaskChooser
|
||||
from wagtail.coreutils import get_content_type_label, get_model_string
|
||||
from wagtail.models import Page, Task, Workflow, WorkflowContentType, WorkflowPage
|
||||
from wagtail.snippets.models import get_workflow_enabled_models
|
||||
|
||||
|
||||
class TaskChooserSearchForm(forms.Form):
|
||||
q = forms.CharField(
|
||||
label=gettext_lazy("Search term"), widget=forms.TextInput(), required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, task_type_choices=None, **kwargs):
|
||||
placeholder = kwargs.pop("placeholder", _("Search…"))
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["q"].widget.attrs = {"placeholder": placeholder}
|
||||
|
||||
# Add task type filter if there is more than one task type option
|
||||
if task_type_choices and len(task_type_choices) > 1:
|
||||
self.fields["task_type"] = forms.ChoiceField(
|
||||
choices=(
|
||||
# Append an "All types" choice to the beginning
|
||||
[(None, _("All types"))]
|
||||
# The task type choices that are passed in use the models as values, we need
|
||||
# to convert these to something that can be represented in HTML
|
||||
+ [
|
||||
(get_model_string(model), verbose_name)
|
||||
for model, verbose_name in task_type_choices
|
||||
]
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
|
||||
# Save a mapping of task_type values back to the model that we can reference later
|
||||
self.task_type_choices = {
|
||||
get_model_string(model): model for model, verbose_name in task_type_choices
|
||||
}
|
||||
|
||||
def is_searching(self):
|
||||
"""
|
||||
Returns True if the user typed a search query
|
||||
"""
|
||||
return self.is_valid() and bool(self.cleaned_data.get("q"))
|
||||
|
||||
@cached_property
|
||||
def task_model(self):
|
||||
"""
|
||||
Returns the selected task model.
|
||||
|
||||
This looks for the task model in the following order:
|
||||
1) If there's only one task model option, return it
|
||||
2) If a task model has been selected, return it
|
||||
3) Return the generic Task model
|
||||
"""
|
||||
models = list(self.task_type_choices.values())
|
||||
if len(models) == 1:
|
||||
return models[0]
|
||||
|
||||
elif self.is_valid():
|
||||
model_name = self.cleaned_data.get("task_type")
|
||||
if model_name and model_name in self.task_type_choices:
|
||||
return self.task_type_choices[model_name]
|
||||
|
||||
return Task
|
||||
|
||||
def specific_task_model_selected(self):
|
||||
return self.task_model is not Task
|
||||
|
||||
|
||||
class WorkflowPageForm(forms.ModelForm):
|
||||
page = forms.ModelChoiceField(
|
||||
queryset=Page.objects.all(),
|
||||
widget=widgets.AdminPageChooser(target_models=[Page], can_choose_root=True),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = WorkflowPage
|
||||
fields = ["page"]
|
||||
|
||||
def clean(self):
|
||||
page = self.cleaned_data.get("page")
|
||||
try:
|
||||
existing_workflow = page.workflowpage.workflow
|
||||
if not self.errors and existing_workflow != self.cleaned_data["workflow"]:
|
||||
# If the form has no errors, Page has an existing Workflow assigned, that Workflow is not
|
||||
# the selected Workflow, and overwrite_existing is not True, add a new error. This should be used to
|
||||
# trigger the confirmation message in the view. This is why this error is only added if there are no
|
||||
# other errors - confirmation should be the final step.
|
||||
self.add_error(
|
||||
"page",
|
||||
ValidationError(
|
||||
_(
|
||||
"This page already has workflow '%(workflow_name)s' assigned."
|
||||
)
|
||||
% {"workflow_name": existing_workflow},
|
||||
code="existing_workflow",
|
||||
),
|
||||
)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def save(self, commit=False):
|
||||
page = self.cleaned_data["page"]
|
||||
|
||||
if commit:
|
||||
WorkflowPage.objects.update_or_create(
|
||||
page=page,
|
||||
defaults={"workflow": self.cleaned_data["workflow"]},
|
||||
)
|
||||
|
||||
|
||||
class BaseWorkflowPagesFormSet(forms.BaseInlineFormSet):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
for form in self.forms:
|
||||
form.fields["DELETE"].widget = forms.HiddenInput()
|
||||
|
||||
@property
|
||||
def empty_form(self):
|
||||
empty_form = super().empty_form
|
||||
empty_form.fields["DELETE"].widget = forms.HiddenInput()
|
||||
return empty_form
|
||||
|
||||
def clean(self):
|
||||
"""Checks that no two forms refer to the same page object"""
|
||||
if any(self.errors):
|
||||
# Don't bother validating the formset unless each form is valid on its own
|
||||
return
|
||||
|
||||
pages = [
|
||||
form.cleaned_data["page"]
|
||||
for form in self.forms
|
||||
# need to check for presence of 'page' in cleaned_data,
|
||||
# because a completely blank form passes validation
|
||||
if form not in self.deleted_forms and "page" in form.cleaned_data
|
||||
]
|
||||
if len(set(pages)) != len(pages):
|
||||
# pages list contains duplicates
|
||||
raise forms.ValidationError(
|
||||
_("You cannot assign this workflow to the same page multiple times.")
|
||||
)
|
||||
|
||||
|
||||
class WorkflowContentTypeForm(forms.Form):
|
||||
class ContentTypeMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||
def label_from_instance(self, obj):
|
||||
return get_content_type_label(obj)
|
||||
|
||||
class CheckboxSelectMultiple(forms.CheckboxSelectMultiple):
|
||||
"""Custom CheckboxSelectMultiple widget that renders errors for each content type ID"""
|
||||
|
||||
option_template_name = (
|
||||
"wagtailadmin/workflows/includes/workflow_content_types_checkbox.html"
|
||||
)
|
||||
|
||||
def get_errors_by_id(self, errors):
|
||||
errors_by_id = {}
|
||||
for error in errors.as_data():
|
||||
ct_id = error.params and error.params.get("content_type_id")
|
||||
errors_by_id.setdefault(ct_id, []).append(error)
|
||||
return errors_by_id
|
||||
|
||||
def render_with_errors(
|
||||
self, name, value, attrs=None, renderer=None, errors=None
|
||||
):
|
||||
context = {
|
||||
**self.get_context(name, value, attrs),
|
||||
"errors_by_id": self.get_errors_by_id(errors),
|
||||
}
|
||||
return self._render(self.template_name, context, renderer)
|
||||
|
||||
content_types = ContentTypeMultipleChoiceField(
|
||||
queryset=ContentType.objects.none(),
|
||||
widget=CheckboxSelectMultiple(),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, workflow=None, **kwargs):
|
||||
self.workflow = workflow
|
||||
if workflow and "initial" not in kwargs:
|
||||
kwargs["initial"] = {"content_types": workflow.workflow_content_types.all()}
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Start with an always-false query, as Django can optimise it by
|
||||
# returning an empty queryset without running any database queries.
|
||||
workflow_enabled_q = Q(pk__in=[])
|
||||
|
||||
# Then union the query for each workflow-enabled model.
|
||||
for model in get_workflow_enabled_models():
|
||||
workflow_enabled_q |= Q(
|
||||
app_label=model._meta.app_label, model=model._meta.model_name
|
||||
)
|
||||
|
||||
self.fields["content_types"].queryset = ContentType.objects.filter(
|
||||
workflow_enabled_q
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
content_types = self.cleaned_data.get("content_types")
|
||||
if not content_types:
|
||||
return
|
||||
|
||||
existing_assignments = WorkflowContentType.objects.filter(
|
||||
content_type__in=content_types,
|
||||
workflow__active=True,
|
||||
).exclude(workflow=self.workflow)
|
||||
for assignment in existing_assignments:
|
||||
self.add_error(
|
||||
"content_types",
|
||||
ValidationError(
|
||||
_(
|
||||
"Snippet '%(content_type)s' already has workflow '%(workflow_name)s' assigned."
|
||||
)
|
||||
% {
|
||||
"content_type": capfirst(assignment.content_type.name),
|
||||
"workflow_name": assignment.workflow,
|
||||
},
|
||||
code="existing_workflow_content_type",
|
||||
params={"content_type_id": assignment.content_type_id},
|
||||
),
|
||||
)
|
||||
|
||||
def save(self, commit=True):
|
||||
if not commit:
|
||||
return
|
||||
|
||||
content_types = self.cleaned_data["content_types"]
|
||||
|
||||
with transaction.atomic():
|
||||
# Remove any content types that are no longer selected
|
||||
WorkflowContentType.objects.filter(workflow=self.workflow).exclude(
|
||||
content_type__in=content_types
|
||||
).delete()
|
||||
|
||||
# Add any new content types, ignoring conflicts with existing ones
|
||||
# to avoid additional query for existing content types
|
||||
objects = [
|
||||
WorkflowContentType(workflow=self.workflow, content_type=ct)
|
||||
for ct in content_types
|
||||
]
|
||||
WorkflowContentType.objects.bulk_create(objects, ignore_conflicts=True)
|
||||
|
||||
|
||||
WorkflowPagesFormSet = forms.inlineformset_factory(
|
||||
Workflow,
|
||||
WorkflowPage,
|
||||
form=WorkflowPageForm,
|
||||
formset=BaseWorkflowPagesFormSet,
|
||||
extra=1,
|
||||
can_delete=True,
|
||||
fields=["page"],
|
||||
)
|
||||
|
||||
|
||||
class BaseTaskForm(forms.ModelForm):
|
||||
pass
|
||||
|
||||
|
||||
def get_task_form_class(task_model, for_edit=False):
|
||||
"""
|
||||
Generates a form class for the given task model.
|
||||
|
||||
If the form is to edit an existing task, set for_edit to True. This applies
|
||||
the readonly restrictions on fields defined in admin_form_readonly_on_edit_fields.
|
||||
"""
|
||||
fields = task_model.admin_form_fields
|
||||
|
||||
form_class = forms.modelform_factory(
|
||||
task_model,
|
||||
form=BaseTaskForm,
|
||||
fields=fields,
|
||||
widgets=getattr(task_model, "admin_form_widgets", {}),
|
||||
)
|
||||
|
||||
if for_edit:
|
||||
for field_name in getattr(task_model, "admin_form_readonly_on_edit_fields", []):
|
||||
if field_name not in form_class.base_fields:
|
||||
raise ImproperlyConfigured(
|
||||
"`%s.admin_form_readonly_on_edit_fields` contains the field "
|
||||
"'%s' that doesn't exist. Did you forget to add "
|
||||
"it to `%s.admin_form_fields`?"
|
||||
% (task_model.__name__, field_name, task_model.__name__)
|
||||
)
|
||||
|
||||
form_class.base_fields[field_name].disabled = True
|
||||
|
||||
return form_class
|
||||
|
||||
|
||||
def get_workflow_edit_handler():
|
||||
"""
|
||||
Returns an edit handler which provides the "name" and "tasks" fields for workflow.
|
||||
"""
|
||||
# Note. It's a bit of a hack that we use edit handlers here. Ideally, it should be
|
||||
# made easier to reuse the inline panel templates for any formset.
|
||||
# Since this form is internal, we're OK with this for now. We might want to revisit
|
||||
# this decision later if we decide to allow custom fields on Workflows.
|
||||
|
||||
panels = [
|
||||
FieldPanel("name", heading=_("Give your workflow a name")),
|
||||
InlinePanel(
|
||||
"workflow_tasks",
|
||||
[
|
||||
FieldPanel("task", widget=AdminTaskChooser(show_clear_link=False)),
|
||||
],
|
||||
heading=_("Add tasks to your workflow"),
|
||||
label=_("Task"),
|
||||
icon="thumbtack",
|
||||
),
|
||||
]
|
||||
edit_handler = ObjectList(panels, base_form_class=WagtailAdminModelForm)
|
||||
return edit_handler.bind_to_model(Workflow)
|
||||
43
env/lib/python3.10/site-packages/wagtail/admin/icons.py
vendored
Normal file
43
env/lib/python3.10/site-packages/wagtail/admin/icons.py
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
import hashlib
|
||||
import itertools
|
||||
import re
|
||||
from functools import lru_cache
|
||||
|
||||
from django.conf import settings
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
|
||||
from wagtail import hooks
|
||||
|
||||
icon_comment_pattern = re.compile(r"<!--.*?-->")
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_icons():
|
||||
icon_hooks = hooks.get_hooks("register_icons")
|
||||
all_icons = sorted(itertools.chain.from_iterable(hook([]) for hook in icon_hooks))
|
||||
combined_icon_markup = ""
|
||||
for icon in all_icons:
|
||||
symbol = (
|
||||
render_to_string(icon)
|
||||
.replace('xmlns="http://www.w3.org/2000/svg"', "")
|
||||
.replace("svg", "symbol")
|
||||
)
|
||||
symbol = icon_comment_pattern.sub("", symbol)
|
||||
combined_icon_markup += symbol
|
||||
|
||||
return render_to_string(
|
||||
"wagtailadmin/shared/icons.html", {"icons": combined_icon_markup}
|
||||
)
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_icon_sprite_hash():
|
||||
# SECRET_KEY is used to prevent exposing the Wagtail version
|
||||
return hashlib.sha1(
|
||||
(get_icons() + settings.SECRET_KEY).encode("utf-8")
|
||||
).hexdigest()[:8]
|
||||
|
||||
|
||||
def get_icon_sprite_url():
|
||||
return reverse("wagtailadmin_sprite") + f"?h={get_icon_sprite_hash()}"
|
||||
19
env/lib/python3.10/site-packages/wagtail/admin/jinja2tags.py
vendored
Normal file
19
env/lib/python3.10/site-packages/wagtail/admin/jinja2tags.py
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
import jinja2
|
||||
from jinja2.ext import Extension
|
||||
|
||||
from .templatetags.wagtailuserbar import wagtailuserbar
|
||||
|
||||
|
||||
class WagtailUserbarExtension(Extension):
|
||||
def __init__(self, environment):
|
||||
super().__init__(environment)
|
||||
|
||||
self.environment.globals.update(
|
||||
{
|
||||
"wagtailuserbar": jinja2.pass_context(wagtailuserbar),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Nicer import names
|
||||
userbar = WagtailUserbarExtension
|
||||
BIN
env/lib/python3.10/site-packages/wagtail/admin/locale/af/LC_MESSAGES/django.mo
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/locale/af/LC_MESSAGES/django.mo
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/locale/af/LC_MESSAGES/djangojs.mo
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/locale/af/LC_MESSAGES/djangojs.mo
vendored
Normal file
Binary file not shown.
15
env/lib/python3.10/site-packages/wagtail/admin/locale/af/LC_MESSAGES/djangojs.po
vendored
Normal file
15
env/lib/python3.10/site-packages/wagtail/admin/locale/af/LC_MESSAGES/djangojs.po
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# Translators:
|
||||
# Jaco du Plessis <jaco@jacoduplessis.co.za>, 2022
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Last-Translator: Jaco du Plessis <jaco@jacoduplessis.co.za>, 2022\n"
|
||||
"Language-Team: Afrikaans (https://app.transifex.com/torchbox/teams/8009/"
|
||||
"af/)\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Language: af\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
msgid "Save"
|
||||
msgstr "Stoor"
|
||||
BIN
env/lib/python3.10/site-packages/wagtail/admin/locale/ar/LC_MESSAGES/django.mo
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/locale/ar/LC_MESSAGES/django.mo
vendored
Normal file
Binary file not shown.
1128
env/lib/python3.10/site-packages/wagtail/admin/locale/ar/LC_MESSAGES/django.po
vendored
Normal file
1128
env/lib/python3.10/site-packages/wagtail/admin/locale/ar/LC_MESSAGES/django.po
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user