Initial commit
This commit is contained in:
0
env/lib/python3.10/site-packages/wagtail/admin/views/__init__.py
vendored
Normal file
0
env/lib/python3.10/site-packages/wagtail/admin/views/__init__.py
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/account.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/account.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/chooser.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/chooser.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/collection_privacy.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/collection_privacy.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/collections.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/collections.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/dismissibles.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/dismissibles.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/editing_sessions.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/editing_sessions.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/home.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/home.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/mixins.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/mixins.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/page_privacy.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/page_privacy.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/tags.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/tags.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/workflows.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/__pycache__/workflows.cpython-310.pyc
vendored
Normal file
Binary file not shown.
431
env/lib/python3.10/site-packages/wagtail/admin/views/account.py
vendored
Normal file
431
env/lib/python3.10/site-packages/wagtail/admin/views/account.py
vendored
Normal file
@@ -0,0 +1,431 @@
|
||||
from collections import OrderedDict
|
||||
from functools import cached_property
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.contrib.auth import views as auth_views
|
||||
from django.db import transaction
|
||||
from django.forms import Media
|
||||
from django.http import Http404
|
||||
from django.shortcuts import redirect
|
||||
from django.template.loader import render_to_string
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy, override
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
from wagtail import hooks
|
||||
from wagtail.admin.forms.account import (
|
||||
AvatarPreferencesForm,
|
||||
LocalePreferencesForm,
|
||||
NameEmailForm,
|
||||
NotificationPreferencesForm,
|
||||
ThemePreferencesForm,
|
||||
)
|
||||
from wagtail.admin.forms.auth import LoginForm, PasswordChangeForm, PasswordResetForm
|
||||
from wagtail.admin.localization import (
|
||||
get_available_admin_languages,
|
||||
get_available_admin_time_zones,
|
||||
)
|
||||
from wagtail.admin.views.generic import WagtailAdminTemplateMixin
|
||||
from wagtail.log_actions import log
|
||||
from wagtail.users.models import UserProfile
|
||||
from wagtail.utils.loading import get_custom_form
|
||||
|
||||
|
||||
def get_user_login_form():
|
||||
form_setting = "WAGTAILADMIN_USER_LOGIN_FORM"
|
||||
if hasattr(settings, form_setting):
|
||||
return get_custom_form(form_setting)
|
||||
else:
|
||||
return LoginForm
|
||||
|
||||
|
||||
def get_password_reset_form():
|
||||
form_setting = "WAGTAILADMIN_USER_PASSWORD_RESET_FORM"
|
||||
if hasattr(settings, form_setting):
|
||||
return get_custom_form(form_setting)
|
||||
else:
|
||||
return PasswordResetForm
|
||||
|
||||
|
||||
# Helper functions to check password management settings to enable/disable views as appropriate.
|
||||
# These are functions rather than class-level constants so that they can be overridden in tests
|
||||
# by override_settings
|
||||
|
||||
|
||||
def password_management_enabled():
|
||||
return getattr(settings, "WAGTAIL_PASSWORD_MANAGEMENT_ENABLED", True)
|
||||
|
||||
|
||||
def email_management_enabled():
|
||||
return getattr(settings, "WAGTAIL_EMAIL_MANAGEMENT_ENABLED", True)
|
||||
|
||||
|
||||
def password_reset_enabled():
|
||||
return getattr(
|
||||
settings, "WAGTAIL_PASSWORD_RESET_ENABLED", password_management_enabled()
|
||||
)
|
||||
|
||||
|
||||
# Tabs
|
||||
|
||||
|
||||
class SettingsTab:
|
||||
def __init__(self, name, title, order=0):
|
||||
self.name = name
|
||||
self.title = title
|
||||
self.order = order
|
||||
|
||||
|
||||
profile_tab = SettingsTab("profile", gettext_lazy("Profile"), order=100)
|
||||
notifications_tab = SettingsTab(
|
||||
"notifications", gettext_lazy("Notifications"), order=200
|
||||
)
|
||||
|
||||
|
||||
# Panels
|
||||
|
||||
|
||||
class BaseSettingsPanel:
|
||||
name = ""
|
||||
title = ""
|
||||
tab = profile_tab
|
||||
help_text = None
|
||||
template_name = "wagtailadmin/account/settings_panels/base.html"
|
||||
form_class = None
|
||||
form_object = "user"
|
||||
|
||||
def __init__(self, request, user, profile):
|
||||
self.request = request
|
||||
self.user = user
|
||||
self.profile = profile
|
||||
|
||||
def is_active(self):
|
||||
"""
|
||||
Returns True to display the panel.
|
||||
"""
|
||||
return True
|
||||
|
||||
def get_form(self):
|
||||
"""
|
||||
Returns an initialised form.
|
||||
"""
|
||||
kwargs = {
|
||||
"instance": self.profile if self.form_object == "profile" else self.user,
|
||||
"prefix": self.name,
|
||||
}
|
||||
|
||||
if self.request.method == "POST":
|
||||
return self.form_class(self.request.POST, self.request.FILES, **kwargs)
|
||||
else:
|
||||
return self.form_class(**kwargs)
|
||||
|
||||
def get_context_data(self):
|
||||
"""
|
||||
Returns the template context to use when rendering the template.
|
||||
"""
|
||||
return {"form": self.get_form()}
|
||||
|
||||
def render(self):
|
||||
"""
|
||||
Renders the panel using the template specified in .template_name and context from .get_context_data()
|
||||
"""
|
||||
return render_to_string(
|
||||
self.template_name, self.get_context_data(), request=self.request
|
||||
)
|
||||
|
||||
|
||||
class NameEmailSettingsPanel(BaseSettingsPanel):
|
||||
name = "name_email"
|
||||
order = 100
|
||||
form_class = NameEmailForm
|
||||
|
||||
@cached_property
|
||||
def title(self):
|
||||
if email_management_enabled():
|
||||
return _("Name and Email")
|
||||
return _("Name")
|
||||
|
||||
|
||||
class AvatarSettingsPanel(BaseSettingsPanel):
|
||||
name = "avatar"
|
||||
title = gettext_lazy("Profile picture")
|
||||
order = 300
|
||||
template_name = "wagtailadmin/account/settings_panels/avatar.html"
|
||||
form_class = AvatarPreferencesForm
|
||||
form_object = "profile"
|
||||
|
||||
|
||||
class NotificationsSettingsPanel(BaseSettingsPanel):
|
||||
name = "notifications"
|
||||
title = gettext_lazy("Notifications")
|
||||
tab = notifications_tab
|
||||
order = 100
|
||||
form_class = NotificationPreferencesForm
|
||||
form_object = "profile"
|
||||
|
||||
def is_active(self):
|
||||
# Hide the panel if there are no notification preferences
|
||||
return bool(self.get_form().fields)
|
||||
|
||||
|
||||
class LocaleSettingsPanel(BaseSettingsPanel):
|
||||
name = "locale"
|
||||
title = gettext_lazy("Locale")
|
||||
order = 400
|
||||
form_class = LocalePreferencesForm
|
||||
form_object = "profile"
|
||||
|
||||
def is_active(self):
|
||||
return (
|
||||
len(get_available_admin_languages()) > 1
|
||||
or len(get_available_admin_time_zones()) > 1
|
||||
)
|
||||
|
||||
|
||||
class ThemeSettingsPanel(BaseSettingsPanel):
|
||||
name = "theme"
|
||||
title = gettext_lazy("Theme preferences")
|
||||
order = 450
|
||||
form_class = ThemePreferencesForm
|
||||
form_object = "profile"
|
||||
|
||||
|
||||
class ChangePasswordPanel(BaseSettingsPanel):
|
||||
name = "password"
|
||||
title = gettext_lazy("Password")
|
||||
order = 500
|
||||
form_class = PasswordChangeForm
|
||||
|
||||
def is_active(self):
|
||||
return password_management_enabled() and self.user.has_usable_password()
|
||||
|
||||
def get_form(self):
|
||||
# Note: don't bind the form unless a field is specified
|
||||
# This prevents the validation error from displaying if the user wishes to ignore this
|
||||
bind_form = False
|
||||
if self.request.method == "POST":
|
||||
bind_form = any(
|
||||
[
|
||||
self.request.POST.get(self.name + "-new_password1"),
|
||||
self.request.POST.get(self.name + "-new_password2"),
|
||||
]
|
||||
)
|
||||
|
||||
if bind_form:
|
||||
return self.form_class(self.user, self.request.POST, prefix=self.name)
|
||||
else:
|
||||
return self.form_class(self.user, prefix=self.name)
|
||||
|
||||
|
||||
# Views
|
||||
|
||||
|
||||
@method_decorator(sensitive_post_parameters(), name="post")
|
||||
class AccountView(WagtailAdminTemplateMixin, TemplateView):
|
||||
template_name = "wagtailadmin/account/account.html"
|
||||
page_title = gettext_lazy("Account")
|
||||
header_icon = "user"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
panels = self.get_panels()
|
||||
context["panels_by_tab"] = self.get_panels_by_tab(panels)
|
||||
context["menu_items"] = self.get_menu_items()
|
||||
context["media"] = self.get_media(panels)
|
||||
context["user"] = self.request.user
|
||||
return context
|
||||
|
||||
def get_panels(self):
|
||||
request = self.request
|
||||
user = self.request.user
|
||||
profile = UserProfile.get_for_user(user)
|
||||
# Panels
|
||||
panels = [
|
||||
NameEmailSettingsPanel(request, user, profile),
|
||||
AvatarSettingsPanel(request, user, profile),
|
||||
NotificationsSettingsPanel(request, user, profile),
|
||||
LocaleSettingsPanel(request, user, profile),
|
||||
ThemeSettingsPanel(request, user, profile),
|
||||
ChangePasswordPanel(request, user, profile),
|
||||
]
|
||||
for fn in hooks.get_hooks("register_account_settings_panel"):
|
||||
panel = fn(request, user, profile)
|
||||
if panel and panel.is_active():
|
||||
panels.append(panel)
|
||||
|
||||
panels = [panel for panel in panels if panel.is_active()]
|
||||
return panels
|
||||
|
||||
def get_panels_by_tab(self, panels):
|
||||
# Get tabs and order them
|
||||
tabs = list({panel.tab for panel in panels})
|
||||
tabs.sort(key=lambda tab: tab.order)
|
||||
|
||||
# Get dict of tabs to ordered panels
|
||||
panels_by_tab = OrderedDict([(tab, []) for tab in tabs])
|
||||
for panel in panels:
|
||||
panels_by_tab[panel.tab].append(panel)
|
||||
for tab, tab_panels in panels_by_tab.items():
|
||||
tab_panels.sort(key=lambda panel: panel.order)
|
||||
return panels_by_tab
|
||||
|
||||
def get_menu_items(self):
|
||||
# Menu items
|
||||
menu_items = []
|
||||
for fn in hooks.get_hooks("register_account_menu_item"):
|
||||
item = fn(self.request)
|
||||
if item:
|
||||
menu_items.append(item)
|
||||
return menu_items
|
||||
|
||||
def get_media(self, panels):
|
||||
panel_forms = [panel.get_form() for panel in panels]
|
||||
|
||||
media = Media()
|
||||
for form in panel_forms:
|
||||
media += form.media
|
||||
return media
|
||||
|
||||
def post(self, request):
|
||||
panel_forms = [panel.get_form() for panel in self.get_panels()]
|
||||
user = self.request.user
|
||||
profile = UserProfile.get_for_user(user)
|
||||
|
||||
if all(form.is_valid() or not form.is_bound for form in panel_forms):
|
||||
with transaction.atomic():
|
||||
for form in panel_forms:
|
||||
if form.is_bound:
|
||||
form.save()
|
||||
|
||||
log(user, "wagtail.edit")
|
||||
|
||||
# Prevent a password change from logging this user out
|
||||
update_session_auth_hash(request, user)
|
||||
|
||||
# Override the language when creating the success message
|
||||
# If the user has changed their language in this request, the message should
|
||||
# be in the new language, not the existing one
|
||||
with override(profile.get_preferred_language()):
|
||||
messages.success(
|
||||
request, _("Your account settings have been changed successfully!")
|
||||
)
|
||||
|
||||
return redirect("wagtailadmin_account")
|
||||
|
||||
return TemplateResponse(request, self.template_name, self.get_context_data())
|
||||
|
||||
|
||||
class PasswordResetEnabledViewMixin:
|
||||
"""
|
||||
Class based view mixin that disables the view if password reset is disabled by one of the following settings:
|
||||
- WAGTAIL_PASSWORD_RESET_ENABLED
|
||||
- WAGTAIL_PASSWORD_MANAGEMENT_ENABLED
|
||||
"""
|
||||
|
||||
def dispatch(self, *args, **kwargs):
|
||||
if not password_reset_enabled():
|
||||
raise Http404
|
||||
|
||||
return super().dispatch(*args, **kwargs)
|
||||
|
||||
|
||||
class PasswordResetView(PasswordResetEnabledViewMixin, auth_views.PasswordResetView):
|
||||
template_name = "wagtailadmin/account/password_reset/form.html"
|
||||
email_template_name = "wagtailadmin/account/password_reset/email.txt"
|
||||
subject_template_name = "wagtailadmin/account/password_reset/email_subject.txt"
|
||||
success_url = reverse_lazy("wagtailadmin_password_reset_done")
|
||||
|
||||
def get_form_class(self):
|
||||
return get_password_reset_form()
|
||||
|
||||
|
||||
class PasswordResetDoneView(
|
||||
PasswordResetEnabledViewMixin, auth_views.PasswordResetDoneView
|
||||
):
|
||||
template_name = "wagtailadmin/account/password_reset/done.html"
|
||||
|
||||
|
||||
class PasswordResetConfirmView(
|
||||
PasswordResetEnabledViewMixin, auth_views.PasswordResetConfirmView
|
||||
):
|
||||
template_name = "wagtailadmin/account/password_reset/confirm.html"
|
||||
success_url = reverse_lazy("wagtailadmin_password_reset_complete")
|
||||
|
||||
|
||||
class PasswordResetCompleteView(
|
||||
PasswordResetEnabledViewMixin, auth_views.PasswordResetCompleteView
|
||||
):
|
||||
template_name = "wagtailadmin/account/password_reset/complete.html"
|
||||
|
||||
|
||||
class LoginView(auth_views.LoginView):
|
||||
template_name = "wagtailadmin/login.html"
|
||||
|
||||
def get_success_url(self):
|
||||
return self.get_redirect_url() or reverse("wagtailadmin_home")
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
# If user is already logged in, redirect them to the dashboard
|
||||
if self.request.user.is_authenticated and self.request.user.has_perm(
|
||||
"wagtailadmin.access_admin"
|
||||
):
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
return super().get(*args, **kwargs)
|
||||
|
||||
def get_form_class(self):
|
||||
return get_user_login_form()
|
||||
|
||||
def form_valid(self, form):
|
||||
response = super().form_valid(form)
|
||||
|
||||
remember = form.cleaned_data.get("remember")
|
||||
if remember:
|
||||
self.request.session.set_expiry(settings.SESSION_COOKIE_AGE)
|
||||
else:
|
||||
self.request.session.set_expiry(0)
|
||||
|
||||
return response
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context["show_password_reset"] = password_reset_enabled()
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
context["username_field"] = User._meta.get_field(
|
||||
User.USERNAME_FIELD
|
||||
).verbose_name
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class LogoutView(auth_views.LogoutView):
|
||||
next_page = "wagtailadmin_login"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
response = super().dispatch(request, *args, **kwargs)
|
||||
|
||||
messages.success(self.request, _("You have been successfully logged out."))
|
||||
# By default, logging out will generate a fresh sessionid cookie. We want to use the
|
||||
# absence of sessionid as an indication that front-end pages are being viewed by a
|
||||
# non-logged-in user and are therefore cacheable, so we forcibly delete the cookie here.
|
||||
response.delete_cookie(
|
||||
settings.SESSION_COOKIE_NAME,
|
||||
domain=settings.SESSION_COOKIE_DOMAIN,
|
||||
path=settings.SESSION_COOKIE_PATH,
|
||||
)
|
||||
|
||||
# HACK: pretend that the session hasn't been modified, so that SessionMiddleware
|
||||
# won't override the above and write a new cookie.
|
||||
self.request.session.modified = False
|
||||
|
||||
return response
|
||||
4
env/lib/python3.10/site-packages/wagtail/admin/views/bulk_action/__init__.py
vendored
Normal file
4
env/lib/python3.10/site-packages/wagtail/admin/views/bulk_action/__init__.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
from .base_bulk_action import BulkAction
|
||||
from .dispatcher import index
|
||||
|
||||
__all__ = ["BulkAction", "index"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
163
env/lib/python3.10/site-packages/wagtail/admin/views/bulk_action/base_bulk_action.py
vendored
Normal file
163
env/lib/python3.10/site-packages/wagtail/admin/views/bulk_action/base_bulk_action.py
vendored
Normal file
@@ -0,0 +1,163 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from django import forms
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_list_or_404, redirect
|
||||
from django.utils.functional import classproperty
|
||||
from django.views.generic import FormView
|
||||
|
||||
from wagtail import hooks
|
||||
from wagtail.admin import messages
|
||||
from wagtail.admin.utils import get_valid_next_url_from_request
|
||||
|
||||
|
||||
class BulkAction(ABC, FormView):
|
||||
@property
|
||||
@abstractmethod
|
||||
def display_name(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def action_type(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def aria_label(self):
|
||||
pass
|
||||
|
||||
extras = {}
|
||||
action_priority = 100
|
||||
classes = set()
|
||||
|
||||
form_class = forms.Form
|
||||
cleaned_form = None
|
||||
|
||||
def __init__(self, request, model):
|
||||
self.request = request
|
||||
next_url = get_valid_next_url_from_request(request)
|
||||
if not next_url:
|
||||
next_url = request.path
|
||||
self.next_url = next_url
|
||||
self.num_parent_objects = self.num_child_objects = 0
|
||||
if model in self.models:
|
||||
self.model = model
|
||||
else:
|
||||
raise Exception(
|
||||
"model {} is not among the specified list of models".format(
|
||||
model.__class__.__name__
|
||||
)
|
||||
)
|
||||
|
||||
@classproperty
|
||||
def models(cls):
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def get_queryset(cls, model, object_ids):
|
||||
return get_list_or_404(model, pk__in=object_ids)
|
||||
|
||||
def check_perm(self, obj):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def execute_action(cls, objects, **kwargs):
|
||||
raise NotImplementedError("execute_action needs to be implemented")
|
||||
|
||||
def get_success_message(self, num_parent_objects, num_child_objects):
|
||||
pass
|
||||
|
||||
def object_context(self, obj):
|
||||
return {"item": obj}
|
||||
|
||||
@classmethod
|
||||
def get_default_model(cls):
|
||||
models = cls.models
|
||||
if len(models) == 1:
|
||||
return models[0]
|
||||
raise Exception(
|
||||
"Cannot get default model if number of models is greater than 1"
|
||||
)
|
||||
|
||||
def __run_before_hooks(self, action_type, request, objects):
|
||||
for hook in hooks.get_hooks("before_bulk_action"):
|
||||
result = hook(request, action_type, objects, self)
|
||||
if hasattr(result, "status_code"):
|
||||
return result
|
||||
|
||||
def __run_after_hooks(self, action_type, request, objects):
|
||||
for hook in hooks.get_hooks("after_bulk_action"):
|
||||
result = hook(request, action_type, objects, self)
|
||||
if hasattr(result, "status_code"):
|
||||
return result
|
||||
|
||||
def get_all_objects_in_listing_query(self, parent_id):
|
||||
return self.model.objects.all().values_list("pk", flat=True)
|
||||
|
||||
def get_actionable_objects(self):
|
||||
objects = []
|
||||
items_with_no_access = []
|
||||
object_ids = self.request.GET.getlist("id")
|
||||
if "all" in object_ids:
|
||||
object_ids = self.get_all_objects_in_listing_query(
|
||||
self.request.GET.get("childOf")
|
||||
)
|
||||
|
||||
for obj in self.get_queryset(self.model, object_ids):
|
||||
if not self.check_perm(obj):
|
||||
items_with_no_access.append(obj)
|
||||
else:
|
||||
objects.append(obj)
|
||||
return objects, {"items_with_no_access": items_with_no_access}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
items, items_with_no_access = self.get_actionable_objects()
|
||||
_items = []
|
||||
for item in items:
|
||||
_items.append(self.object_context(item))
|
||||
return {
|
||||
**super().get_context_data(**kwargs),
|
||||
"items": _items,
|
||||
**items_with_no_access,
|
||||
"next": self.next_url,
|
||||
"submit_url": self.request.path + "?" + self.request.META["QUERY_STRING"],
|
||||
}
|
||||
|
||||
def prepare_action(self, objects, objects_without_access):
|
||||
return
|
||||
|
||||
def get_execution_context(self):
|
||||
return {}
|
||||
|
||||
def form_valid(self, form):
|
||||
request = self.request
|
||||
self.cleaned_form = form
|
||||
objects, objects_without_access = self.get_actionable_objects()
|
||||
self.actionable_objects = objects
|
||||
resp = self.prepare_action(objects, objects_without_access)
|
||||
if hasattr(resp, "status_code"):
|
||||
return resp
|
||||
with transaction.atomic():
|
||||
before_hook_result = self.__run_before_hooks(
|
||||
self.action_type, request, objects
|
||||
)
|
||||
if before_hook_result is not None:
|
||||
return before_hook_result
|
||||
num_parent_objects, num_child_objects = self.execute_action(
|
||||
objects, **self.get_execution_context()
|
||||
)
|
||||
after_hook_result = self.__run_after_hooks(
|
||||
self.action_type, request, objects
|
||||
)
|
||||
if after_hook_result is not None:
|
||||
return after_hook_result
|
||||
success_message = self.get_success_message(
|
||||
num_parent_objects, num_child_objects
|
||||
)
|
||||
if success_message is not None:
|
||||
messages.success(request, success_message)
|
||||
return redirect(self.next_url)
|
||||
|
||||
def form_invalid(self, form):
|
||||
return super().form_invalid(form)
|
||||
15
env/lib/python3.10/site-packages/wagtail/admin/views/bulk_action/dispatcher.py
vendored
Normal file
15
env/lib/python3.10/site-packages/wagtail/admin/views/bulk_action/dispatcher.py
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
from django.apps import apps
|
||||
from django.http import Http404
|
||||
|
||||
from wagtail.admin.views.bulk_action.registry import bulk_action_registry as registry
|
||||
|
||||
|
||||
def index(request, app_label, model_name, action):
|
||||
try:
|
||||
model = apps.get_model(app_label, model_name)
|
||||
except LookupError:
|
||||
raise Http404
|
||||
action_class = registry.get_bulk_action_class(app_label, model_name, action)
|
||||
if action_class is not None:
|
||||
return action_class(request, model).dispatch(request)
|
||||
raise Http404
|
||||
40
env/lib/python3.10/site-packages/wagtail/admin/views/bulk_action/registry.py
vendored
Normal file
40
env/lib/python3.10/site-packages/wagtail/admin/views/bulk_action/registry.py
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
from wagtail import hooks
|
||||
from wagtail.admin.views.bulk_action import BulkAction
|
||||
|
||||
|
||||
class BulkActionRegistry:
|
||||
def __init__(self):
|
||||
self.actions = {} # {app_name: {model_name: {action_name: action_class]}}
|
||||
self.has_scanned_for_bulk_actions = False
|
||||
|
||||
def _scan_for_bulk_actions(self):
|
||||
if not self.has_scanned_for_bulk_actions:
|
||||
for action_class in hooks.get_hooks("register_bulk_action"):
|
||||
if not issubclass(action_class, BulkAction):
|
||||
raise Exception(
|
||||
"{} is not a subclass of {}".format(
|
||||
action_class.__name__, BulkAction.__name__
|
||||
)
|
||||
)
|
||||
for model in action_class.models:
|
||||
self.actions.setdefault(model._meta.app_label, {})
|
||||
self.actions[model._meta.app_label].setdefault(
|
||||
model._meta.model_name, {}
|
||||
)
|
||||
self.actions[model._meta.app_label][model._meta.model_name][
|
||||
action_class.action_type
|
||||
] = action_class
|
||||
self.has_scanned_for_bulk_actions = True
|
||||
|
||||
def get_bulk_actions_for_model(self, app_label, model_name):
|
||||
self._scan_for_bulk_actions()
|
||||
return self.actions.get(app_label, {}).get(model_name, {}).values()
|
||||
|
||||
def get_bulk_action_class(self, app_label, model_name, action_type):
|
||||
self._scan_for_bulk_actions()
|
||||
return (
|
||||
self.actions.get(app_label, {}).get(model_name, {}).get(action_type, None)
|
||||
)
|
||||
|
||||
|
||||
bulk_action_registry = BulkActionRegistry()
|
||||
831
env/lib/python3.10/site-packages/wagtail/admin/views/chooser.py
vendored
Normal file
831
env/lib/python3.10/site-packages/wagtail/admin/views/chooser.py
vendored
Normal file
@@ -0,0 +1,831 @@
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from urllib.parse import parse_qs, quote, urlencode, urlsplit
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.paginator import InvalidPage, Paginator
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import NoReverseMatch
|
||||
from django.urls.base import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic.base import View
|
||||
|
||||
from wagtail import hooks
|
||||
from wagtail.admin.forms.choosers import (
|
||||
AnchorLinkChooserForm,
|
||||
EmailLinkChooserForm,
|
||||
ExternalLinkChooserForm,
|
||||
PhoneLinkChooserForm,
|
||||
)
|
||||
from wagtail.admin.forms.search import SearchForm
|
||||
from wagtail.admin.modal_workflow import render_modal_workflow
|
||||
from wagtail.admin.ui.tables import Column, DateColumn, Table
|
||||
from wagtail.coreutils import resolve_model_string
|
||||
from wagtail.models import Locale, Page, Site
|
||||
|
||||
|
||||
def shared_context(request, extra_context=None):
|
||||
context = {
|
||||
# parent_page ID is passed as a GET parameter on the external_link, anchor_link and mail_link views
|
||||
# so that it's remembered when browsing from 'Internal link' to another link type
|
||||
# and back again. On the 'browse' / 'internal link' view this will be overridden to be
|
||||
# sourced from the standard URL path parameter instead.
|
||||
"parent_page_id": request.GET.get("parent_page_id"),
|
||||
"allow_external_link": request.GET.get("allow_external_link"),
|
||||
"allow_email_link": request.GET.get("allow_email_link"),
|
||||
"allow_phone_link": request.GET.get("allow_phone_link"),
|
||||
"allow_anchor_link": request.GET.get("allow_anchor_link"),
|
||||
}
|
||||
if extra_context:
|
||||
context.update(extra_context)
|
||||
return context
|
||||
|
||||
|
||||
def page_models_from_string(string):
|
||||
page_models = []
|
||||
|
||||
for sub_string in string.split(","):
|
||||
page_model = resolve_model_string(sub_string)
|
||||
|
||||
if not issubclass(page_model, Page):
|
||||
raise ValueError("Model is not a page")
|
||||
|
||||
page_models.append(page_model)
|
||||
|
||||
return tuple(page_models)
|
||||
|
||||
|
||||
def can_choose_page(
|
||||
page,
|
||||
user,
|
||||
desired_classes,
|
||||
can_choose_root=True,
|
||||
user_perm=None,
|
||||
target_pages=None,
|
||||
match_subclass=True,
|
||||
):
|
||||
"""Returns boolean indicating of the user can choose page.
|
||||
will check if the root page can be selected and if user permissions
|
||||
should be checked.
|
||||
"""
|
||||
|
||||
if not target_pages:
|
||||
target_pages = []
|
||||
|
||||
if not match_subclass and page.specific_class not in desired_classes:
|
||||
return False
|
||||
elif (
|
||||
match_subclass
|
||||
and not issubclass(page.specific_class or Page, desired_classes)
|
||||
and not desired_classes == (Page,)
|
||||
):
|
||||
return False
|
||||
elif not can_choose_root and page.is_root():
|
||||
return False
|
||||
|
||||
if user_perm in ["move_to", "bulk_move_to"]:
|
||||
pages_to_move = target_pages
|
||||
|
||||
for page_to_move in pages_to_move:
|
||||
if page.pk == page_to_move.pk or page.is_descendant_of(page_to_move):
|
||||
return False
|
||||
|
||||
if user_perm == "move_to":
|
||||
return page_to_move.permissions_for_user(user).can_move_to(page)
|
||||
if user_perm in {"add_subpage", "copy_to"}:
|
||||
return page.permissions_for_user(user).can_add_subpage()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class PageChooserTable(Table):
|
||||
classname = "listing chooser"
|
||||
|
||||
def __init__(self, *args, show_locale_labels=False, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.show_locale_labels = show_locale_labels
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
context = super().get_context_data(parent_context)
|
||||
context["show_locale_labels"] = self.show_locale_labels
|
||||
return context
|
||||
|
||||
def get_row_classname(self, page):
|
||||
classnames = []
|
||||
if page.is_parent_page:
|
||||
classnames.append("parent-page")
|
||||
if not page.live:
|
||||
classnames.append("unpublished")
|
||||
if not page.can_choose:
|
||||
classnames.append("disabled")
|
||||
|
||||
return " ".join(classnames)
|
||||
|
||||
|
||||
class PageTitleColumn(Column):
|
||||
cell_template_name = "wagtailadmin/chooser/tables/page_title_cell.html"
|
||||
|
||||
def __init__(self, *args, is_multiple_choice=False, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.is_multiple_choice = is_multiple_choice
|
||||
|
||||
def get_value(self, instance):
|
||||
return instance.get_admin_display_title()
|
||||
|
||||
def get_cell_context_data(self, instance, parent_context):
|
||||
context = super().get_cell_context_data(instance, parent_context)
|
||||
context["page"] = instance
|
||||
# only need to show locale labels for top-level pages
|
||||
context["show_locale_labels"] = (
|
||||
parent_context.get("show_locale_labels") and instance.depth == 2
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class ParentPageColumn(Column):
|
||||
cell_template_name = "wagtailadmin/chooser/tables/parent_page_cell.html"
|
||||
|
||||
def get_value(self, instance):
|
||||
return instance.get_parent()
|
||||
|
||||
def get_cell_context_data(self, instance, parent_context):
|
||||
context = super().get_cell_context_data(instance, parent_context)
|
||||
context["show_locale_labels"] = parent_context.get("show_locale_labels")
|
||||
return context
|
||||
|
||||
|
||||
class PageStatusColumn(Column):
|
||||
cell_template_name = "wagtailadmin/chooser/tables/page_status_cell.html"
|
||||
|
||||
def get_value(self, instance):
|
||||
return instance
|
||||
|
||||
|
||||
class PageNavigateToChildrenColumn(Column):
|
||||
cell_template_name = (
|
||||
"wagtailadmin/chooser/tables/page_navigate_to_children_cell.html"
|
||||
)
|
||||
|
||||
def get_value(self, instance):
|
||||
return instance
|
||||
|
||||
|
||||
class PageCheckboxSelectColumn(Column):
|
||||
cell_template_name = "wagtailadmin/chooser/tables/page_checkbox_select_cell.html"
|
||||
|
||||
|
||||
class BrowseView(View):
|
||||
@property
|
||||
def columns(self):
|
||||
cols = [
|
||||
PageTitleColumn(
|
||||
"title",
|
||||
label=_("Title"),
|
||||
is_multiple_choice=self.is_multiple_choice,
|
||||
),
|
||||
DateColumn(
|
||||
"updated",
|
||||
label=_("Updated"),
|
||||
width="12%",
|
||||
accessor="latest_revision_created_at",
|
||||
),
|
||||
Column(
|
||||
"type",
|
||||
label=_("Type"),
|
||||
width="12%",
|
||||
accessor="page_type_display_name",
|
||||
),
|
||||
PageStatusColumn("status", label=_("Status"), width="12%"),
|
||||
PageNavigateToChildrenColumn("children", label="", width="10%"),
|
||||
]
|
||||
if self.is_multiple_choice:
|
||||
cols.insert(
|
||||
0,
|
||||
PageCheckboxSelectColumn(
|
||||
"select", label=_("Select"), width="1%", accessor="pk"
|
||||
),
|
||||
)
|
||||
return cols
|
||||
|
||||
def get_object_list(self):
|
||||
# Get children of parent page (without streamfields)
|
||||
pages = self.parent_page.get_children().defer_streamfields().specific()
|
||||
if self.i18n_enabled:
|
||||
pages = pages.select_related("locale")
|
||||
return pages
|
||||
|
||||
def filter_object_list(self, pages):
|
||||
# allow hooks to modify the queryset
|
||||
for hook in hooks.get_hooks("construct_page_chooser_queryset"):
|
||||
pages = hook(pages, self.request)
|
||||
|
||||
# Filter them by page type
|
||||
if self.desired_classes != (Page,):
|
||||
# restrict the page listing to just those pages that:
|
||||
# - are of the given content type (taking into account class inheritance)
|
||||
# - or can be navigated into (i.e. have children)
|
||||
choosable_pages = pages.type(*self.desired_classes)
|
||||
descendable_pages = pages.filter(numchild__gt=0)
|
||||
pages = choosable_pages | descendable_pages
|
||||
|
||||
return pages
|
||||
|
||||
def get(self, request, parent_page_id=None):
|
||||
self.i18n_enabled = getattr(settings, "WAGTAIL_I18N_ENABLED", False)
|
||||
self.is_multiple_choice = request.GET.get("multiple")
|
||||
|
||||
# A missing or empty page_type parameter indicates 'all page types'
|
||||
# (i.e. descendants of wagtailcore.page)
|
||||
page_type_string = request.GET.get("page_type") or "wagtailcore.page"
|
||||
user_perm = request.GET.get("user_perms", False)
|
||||
|
||||
try:
|
||||
self.desired_classes = page_models_from_string(page_type_string)
|
||||
except (ValueError, LookupError):
|
||||
raise Http404
|
||||
|
||||
# Find parent page
|
||||
if parent_page_id:
|
||||
self.parent_page = get_object_or_404(Page, id=parent_page_id)
|
||||
elif self.desired_classes == (Page,):
|
||||
# Just use the root page
|
||||
self.parent_page = Page.get_first_root_node()
|
||||
else:
|
||||
# Find the highest common ancestor for the specific classes passed in
|
||||
# In many cases, such as selecting an EventPage under an EventIndex,
|
||||
# this will help the administrator find their page quicker.
|
||||
all_desired_pages = Page.objects.all().type(*self.desired_classes)
|
||||
self.parent_page = all_desired_pages.first_common_ancestor()
|
||||
|
||||
self.parent_page = self.parent_page.specific
|
||||
|
||||
pages = self.get_object_list()
|
||||
pages = self.filter_object_list(pages)
|
||||
|
||||
can_choose_root = request.GET.get("can_choose_root", False)
|
||||
target_pages = Page.objects.filter(
|
||||
pk__in=[int(pk) for pk in request.GET.getlist("target_pages[]", []) if pk]
|
||||
)
|
||||
|
||||
match_subclass = request.GET.get("match_subclass", True)
|
||||
|
||||
# Parent page can be chosen if it is a instance of desired_classes
|
||||
self.parent_page.can_choose = can_choose_page(
|
||||
self.parent_page,
|
||||
request.user,
|
||||
self.desired_classes,
|
||||
can_choose_root,
|
||||
user_perm,
|
||||
target_pages=target_pages,
|
||||
match_subclass=match_subclass,
|
||||
)
|
||||
self.parent_page.is_parent_page = True
|
||||
self.parent_page.can_descend = False
|
||||
|
||||
selected_locale = None
|
||||
locale_options = []
|
||||
if self.i18n_enabled:
|
||||
# Ensure query parameters (e.g. `page_type`, `user_perms`, etc.) are
|
||||
# preserved when switching locales, but reset the pagination as the
|
||||
# number of pages might be different.
|
||||
new_params = request.GET.copy()
|
||||
new_params.pop("p", None)
|
||||
if self.parent_page.is_root():
|
||||
# 'locale' is the current value of the "Locale" selector in the UI
|
||||
if request.GET.get("locale"):
|
||||
selected_locale = get_object_or_404(
|
||||
Locale, language_code=request.GET["locale"]
|
||||
)
|
||||
active_locale_id = selected_locale.pk
|
||||
else:
|
||||
active_locale_id = Locale.get_active().pk
|
||||
|
||||
# we are at the Root level, so get the locales from the current pages
|
||||
choose_url = reverse("wagtailadmin_choose_page")
|
||||
for locale in Locale.objects.filter(
|
||||
pk__in=pages.values_list("locale_id")
|
||||
).exclude(pk=active_locale_id):
|
||||
new_params["locale"] = locale.language_code
|
||||
locale_options.append(
|
||||
{
|
||||
"locale": locale,
|
||||
"url": choose_url + "?" + new_params.urlencode(),
|
||||
}
|
||||
)
|
||||
else:
|
||||
# We have a parent page (that is not the root page). Use its locale as the selected localer
|
||||
selected_locale = self.parent_page.locale
|
||||
new_params.pop("locale", None)
|
||||
# and get the locales based on its available translations
|
||||
locales_and_parent_pages = {
|
||||
item["locale"]: item["pk"]
|
||||
for item in Page.objects.translation_of(self.parent_page).values(
|
||||
"locale", "pk"
|
||||
)
|
||||
}
|
||||
locales_and_parent_pages[selected_locale.pk] = self.parent_page.pk
|
||||
for locale in Locale.objects.filter(
|
||||
pk__in=list(locales_and_parent_pages.keys())
|
||||
).exclude(pk=selected_locale.pk):
|
||||
choose_child_url = reverse(
|
||||
"wagtailadmin_choose_page_child",
|
||||
args=[locales_and_parent_pages[locale.pk]],
|
||||
)
|
||||
locale_options.append(
|
||||
{
|
||||
"locale": locale,
|
||||
"url": choose_child_url + "?" + new_params.urlencode(),
|
||||
}
|
||||
)
|
||||
|
||||
# finally, filter the browsable pages on the selected locale
|
||||
if selected_locale:
|
||||
pages = pages.filter(locale=selected_locale)
|
||||
|
||||
# Pagination
|
||||
# We apply pagination first so we don't need to walk the entire list
|
||||
# in the block below
|
||||
paginator = Paginator(pages, per_page=25)
|
||||
try:
|
||||
pages = paginator.page(request.GET.get("p", 1))
|
||||
except InvalidPage:
|
||||
raise Http404
|
||||
|
||||
# Annotate each page with can_choose/can_descend flags
|
||||
for page in pages:
|
||||
page.can_choose = can_choose_page(
|
||||
page,
|
||||
request.user,
|
||||
self.desired_classes,
|
||||
can_choose_root,
|
||||
user_perm,
|
||||
target_pages=target_pages,
|
||||
match_subclass=match_subclass,
|
||||
)
|
||||
page.can_descend = page.get_children_count()
|
||||
page.is_parent_page = False
|
||||
|
||||
table = PageChooserTable(
|
||||
self.columns,
|
||||
[self.parent_page] + list(pages),
|
||||
show_locale_labels=self.i18n_enabled,
|
||||
)
|
||||
|
||||
# Render
|
||||
context = shared_context(
|
||||
request,
|
||||
{
|
||||
"parent_page": self.parent_page,
|
||||
"parent_page_id": self.parent_page.pk,
|
||||
"table": table,
|
||||
"pagination_page": pages,
|
||||
"search_form": SearchForm(),
|
||||
"page_type_string": page_type_string,
|
||||
"page_type_names": [
|
||||
desired_class.get_verbose_name()
|
||||
for desired_class in self.desired_classes
|
||||
],
|
||||
"page_types_restricted": (page_type_string != "wagtailcore.page"),
|
||||
"show_locale_controls": self.i18n_enabled,
|
||||
"locale_options": locale_options,
|
||||
"selected_locale": selected_locale,
|
||||
"is_multiple_choice": self.is_multiple_choice,
|
||||
},
|
||||
)
|
||||
|
||||
return render_modal_workflow(
|
||||
request,
|
||||
"wagtailadmin/chooser/browse.html",
|
||||
None,
|
||||
context,
|
||||
json_data={"step": "browse", "parent_page_id": context["parent_page_id"]},
|
||||
)
|
||||
|
||||
|
||||
class SearchView(View):
|
||||
@property
|
||||
def columns(self):
|
||||
cols = [
|
||||
PageTitleColumn("title", label=_("Title")),
|
||||
ParentPageColumn("parent", label=_("Parent")),
|
||||
DateColumn(
|
||||
"updated",
|
||||
label=_("Updated"),
|
||||
width="12%",
|
||||
accessor="latest_revision_created_at",
|
||||
),
|
||||
Column(
|
||||
"type",
|
||||
label=_("Type"),
|
||||
width="12%",
|
||||
accessor="page_type_display_name",
|
||||
),
|
||||
PageStatusColumn("status", label=_("Status"), width="12%"),
|
||||
]
|
||||
if self.is_multiple_choice:
|
||||
cols.insert(
|
||||
0,
|
||||
PageCheckboxSelectColumn(
|
||||
"select", label=_("Select"), width="1%", accessor="pk"
|
||||
),
|
||||
)
|
||||
return cols
|
||||
|
||||
def get(self, request):
|
||||
self.i18n_enabled = getattr(settings, "WAGTAIL_I18N_ENABLED", False)
|
||||
self.is_multiple_choice = request.GET.get("multiple")
|
||||
# A missing or empty page_type parameter indicates 'all page types' (i.e. descendants of wagtailcore.page)
|
||||
page_type_string = request.GET.get("page_type") or "wagtailcore.page"
|
||||
|
||||
try:
|
||||
desired_classes = page_models_from_string(page_type_string)
|
||||
except (ValueError, LookupError):
|
||||
raise Http404
|
||||
|
||||
pages = Page.objects.all()
|
||||
if self.i18n_enabled:
|
||||
pages = pages.select_related("locale")
|
||||
|
||||
# allow hooks to modify the queryset
|
||||
for hook in hooks.get_hooks("construct_page_chooser_queryset"):
|
||||
pages = hook(pages, request)
|
||||
|
||||
search_form = SearchForm(request.GET)
|
||||
if search_form.is_valid() and search_form.cleaned_data["q"]:
|
||||
pages = pages.exclude(depth=1) # never include root
|
||||
pages = pages.type(*desired_classes)
|
||||
pages = pages.specific()
|
||||
pages = pages.autocomplete(search_form.cleaned_data["q"])
|
||||
|
||||
else:
|
||||
pages = pages.none()
|
||||
|
||||
paginator = Paginator(pages, per_page=25)
|
||||
pages = paginator.get_page(request.GET.get("p"))
|
||||
|
||||
for page in pages:
|
||||
page.can_choose = True
|
||||
page.is_parent_page = False
|
||||
|
||||
table = PageChooserTable(
|
||||
self.columns,
|
||||
pages,
|
||||
show_locale_labels=self.i18n_enabled,
|
||||
)
|
||||
|
||||
return TemplateResponse(
|
||||
request,
|
||||
"wagtailadmin/chooser/_search_results.html",
|
||||
shared_context(
|
||||
request,
|
||||
{
|
||||
"searchform": search_form,
|
||||
"table": table,
|
||||
"pages": pages,
|
||||
"page_type_string": page_type_string,
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class ChosenMultipleView(View):
|
||||
"""
|
||||
A view that takes a list of 'id' URL parameters and returns a modal workflow response indicating
|
||||
that those objects have been chosen
|
||||
"""
|
||||
|
||||
def render_chosen_response(self, result):
|
||||
return render_modal_workflow(
|
||||
self.request,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
json_data={"step": "page_chosen", "result": result},
|
||||
)
|
||||
|
||||
def get(self, request):
|
||||
pks = request.GET.getlist("id")
|
||||
pages = Page.objects.filter(pk__in=pks).specific()
|
||||
result = [
|
||||
{
|
||||
"id": page.pk,
|
||||
"parentId": page.get_parent().pk,
|
||||
"adminTitle": page.get_admin_display_title(),
|
||||
"editUrl": reverse("wagtailadmin_pages:edit", args=(page.pk,)),
|
||||
"url": page.url,
|
||||
}
|
||||
for page in pages
|
||||
]
|
||||
return self.render_chosen_response(result)
|
||||
|
||||
|
||||
class BaseLinkFormView(View):
|
||||
def get_initial_data(self):
|
||||
return {
|
||||
self.link_url_field_name: self.request.GET.get("link_url", ""),
|
||||
"link_text": self.request.GET.get("link_text", ""),
|
||||
}
|
||||
|
||||
def get_url_from_field_value(self, value):
|
||||
return value
|
||||
|
||||
def get_result_data(self):
|
||||
url_field_value = self.form.cleaned_data[self.link_url_field_name]
|
||||
return {
|
||||
"url": self.get_url_from_field_value(url_field_value),
|
||||
"title": self.form.cleaned_data["link_text"].strip() or url_field_value,
|
||||
# If the user has explicitly entered / edited something in the link_text field,
|
||||
# always use that text. If not, we should favour keeping the existing link/selection
|
||||
# text, where applicable.
|
||||
# (Normally this will match the link_text passed in the URL here anyhow,
|
||||
# but that won't account for non-text content such as images.)
|
||||
"prefer_this_title_as_link_text": ("link_text" in self.form.changed_data),
|
||||
}
|
||||
|
||||
def get(self, request):
|
||||
self.form = self.form_class(
|
||||
initial=self.get_initial_data(), prefix=self.form_prefix
|
||||
)
|
||||
return self.render_form_response()
|
||||
|
||||
def post(self, request):
|
||||
self.form = self.form_class(
|
||||
request.POST, initial=self.get_initial_data(), prefix=self.form_prefix
|
||||
)
|
||||
|
||||
if self.form.is_valid():
|
||||
result = self.get_result_data()
|
||||
return self.render_chosen_response(result)
|
||||
else: # form invalid
|
||||
return self.render_form_response()
|
||||
|
||||
def render_form_response(self):
|
||||
return render_modal_workflow(
|
||||
self.request,
|
||||
self.template_name,
|
||||
None,
|
||||
shared_context(
|
||||
self.request,
|
||||
{
|
||||
"form": self.form,
|
||||
},
|
||||
),
|
||||
json_data={"step": self.step_name},
|
||||
)
|
||||
|
||||
def render_chosen_response(self, result):
|
||||
return render_modal_workflow(
|
||||
self.request,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
json_data={"step": "external_link_chosen", "result": result},
|
||||
)
|
||||
|
||||
|
||||
LINK_CONVERSION_ALL = "all"
|
||||
LINK_CONVERSION_EXACT = "exact"
|
||||
LINK_CONVERSION_CONFIRM = "confirm"
|
||||
|
||||
|
||||
class ExternalLinkView(BaseLinkFormView):
|
||||
form_prefix = "external-link-chooser"
|
||||
form_class = ExternalLinkChooserForm
|
||||
template_name = "wagtailadmin/chooser/external_link.html"
|
||||
step_name = "external_link"
|
||||
link_url_field_name = "url"
|
||||
|
||||
def post(self, request):
|
||||
self.form = self.form_class(
|
||||
request.POST,
|
||||
initial=self.get_initial_data(),
|
||||
prefix=self.form_prefix,
|
||||
)
|
||||
|
||||
if self.form.is_valid():
|
||||
result = self.get_result_data()
|
||||
submitted_url = result["url"]
|
||||
|
||||
link_conversion = getattr(
|
||||
settings,
|
||||
"WAGTAILADMIN_EXTERNAL_LINK_CONVERSION",
|
||||
LINK_CONVERSION_ALL,
|
||||
).lower()
|
||||
|
||||
if link_conversion not in [
|
||||
LINK_CONVERSION_ALL,
|
||||
LINK_CONVERSION_EXACT,
|
||||
LINK_CONVERSION_CONFIRM,
|
||||
]:
|
||||
# We should not attempt to convert external urls to page links
|
||||
return self.render_chosen_response(result)
|
||||
|
||||
# Next, we should check if the url matches an internal page
|
||||
# Strip the url of its query/fragment link parameters - these won't match a page
|
||||
url_without_query = re.split(r"\?|#", submitted_url)[0]
|
||||
|
||||
# Start by finding any sites the url could potentially match
|
||||
sites = getattr(request, "_wagtail_cached_site_root_paths", None)
|
||||
if sites is None:
|
||||
sites = Site.get_site_root_paths()
|
||||
|
||||
try:
|
||||
# The serve view might not be routed to the root path of the domain,
|
||||
# e.g. /pages/, so we need to account for the path to the serve view
|
||||
serve_path = reverse("wagtail_serve", args=("",))
|
||||
except NoReverseMatch:
|
||||
serve_path = None
|
||||
|
||||
match_relative_paths = submitted_url.startswith("/") and len(sites) == 1
|
||||
# We should only match relative urls if there's only a single site
|
||||
# Otherwise this could get very annoying accidentally matching coincidentally
|
||||
# named pages on different sites
|
||||
|
||||
possible_sites = defaultdict(list)
|
||||
|
||||
if match_relative_paths:
|
||||
for pk, path, url, language_code in sites:
|
||||
possible_sites[pk].append(url_without_query)
|
||||
|
||||
# If the submitted URL is prefixed with the serve path,
|
||||
# also consider it without the serve path so we can match
|
||||
# the page using Page.route()
|
||||
if serve_path and url_without_query.startswith(serve_path):
|
||||
possible_sites[pk].append(
|
||||
url_without_query[len(serve_path) - 1 :]
|
||||
)
|
||||
else:
|
||||
for pk, path, url, language_code in sites:
|
||||
if not submitted_url.startswith(url):
|
||||
continue
|
||||
possible_sites[pk].append(url_without_query[len(url) :])
|
||||
|
||||
# If the submitted URL is prefixed with the serve path,
|
||||
# also consider it without the serve path so we can match
|
||||
# the page using Page.route()
|
||||
if serve_path and url_without_query.startswith(url + serve_path):
|
||||
possible_sites[pk].append(
|
||||
url_without_query[len(url) + len(serve_path) - 1 :]
|
||||
)
|
||||
|
||||
# Loop over possible sites to identify a page match
|
||||
for pk, possible_urls in possible_sites.items():
|
||||
site = Site.objects.select_related("root_page").get(pk=pk)
|
||||
root_page = site.root_page.specific
|
||||
for url in possible_urls:
|
||||
try:
|
||||
route = root_page.route(
|
||||
request,
|
||||
[component for component in url.split("/") if component],
|
||||
)
|
||||
except Http404:
|
||||
continue
|
||||
|
||||
matched_page = route.page.specific
|
||||
|
||||
internal_data = {
|
||||
"id": matched_page.pk,
|
||||
"parentId": matched_page.get_parent().pk,
|
||||
"adminTitle": matched_page.draft_title,
|
||||
"editUrl": reverse(
|
||||
"wagtailadmin_pages:edit", args=(matched_page.pk,)
|
||||
),
|
||||
"url": matched_page.url,
|
||||
}
|
||||
|
||||
# Let's check what this page's normal url would be
|
||||
normal_url = (
|
||||
matched_page.get_url_parts(request=request)[-1]
|
||||
if match_relative_paths
|
||||
else matched_page.get_full_url(request=request)
|
||||
)
|
||||
|
||||
# If that's what the user provided, great. Let's just convert the external
|
||||
# url to an internal link automatically unless we're set up tp manually check
|
||||
# all conversions
|
||||
if (
|
||||
normal_url == submitted_url
|
||||
and link_conversion != LINK_CONVERSION_CONFIRM
|
||||
):
|
||||
return self.render_chosen_response(internal_data)
|
||||
# If not, they might lose query parameters or routable page information
|
||||
|
||||
if link_conversion == LINK_CONVERSION_EXACT:
|
||||
# We should only convert exact matches
|
||||
continue
|
||||
|
||||
# Let's confirm the conversion with them explicitly
|
||||
else:
|
||||
return render_modal_workflow(
|
||||
request,
|
||||
"wagtailadmin/chooser/confirm_external_to_internal.html",
|
||||
None,
|
||||
{
|
||||
"submitted_url": submitted_url,
|
||||
"internal_url": normal_url,
|
||||
"page": matched_page.draft_title,
|
||||
},
|
||||
json_data={
|
||||
"step": "confirm_external_to_internal",
|
||||
"external": result,
|
||||
"internal": internal_data,
|
||||
},
|
||||
)
|
||||
|
||||
# Otherwise, with no internal matches, fall back to an external url
|
||||
return self.render_chosen_response(result)
|
||||
else: # form invalid
|
||||
return self.render_form_response()
|
||||
|
||||
|
||||
class AnchorLinkView(BaseLinkFormView):
|
||||
form_prefix = "anchor-link-chooser"
|
||||
form_class = AnchorLinkChooserForm
|
||||
template_name = "wagtailadmin/chooser/anchor_link.html"
|
||||
step_name = "anchor_link"
|
||||
link_url_field_name = "url"
|
||||
|
||||
def get_url_from_field_value(self, value):
|
||||
return "#" + value
|
||||
|
||||
|
||||
class EmailLinkView(BaseLinkFormView):
|
||||
form_prefix = "email-link-chooser"
|
||||
form_class = EmailLinkChooserForm
|
||||
template_name = "wagtailadmin/chooser/email_link.html"
|
||||
step_name = "email_link"
|
||||
link_url_field_name = "email_address"
|
||||
|
||||
def get_initial_data(self):
|
||||
parsed_email = self.parse_email_link(self.request.GET.get("link_url", ""))
|
||||
return {
|
||||
"email_address": parsed_email["email"],
|
||||
"link_text": self.request.GET.get("link_text", ""),
|
||||
"subject": parsed_email["subject"],
|
||||
"body": parsed_email["body"],
|
||||
}
|
||||
|
||||
def get_url_from_field_value(self, value):
|
||||
return "mailto:" + value
|
||||
|
||||
def get_result_data(self):
|
||||
params = {
|
||||
"subject": self.form.cleaned_data["subject"],
|
||||
"body": self.form.cleaned_data["body"],
|
||||
}
|
||||
encoded_params = urlencode(
|
||||
{k: v for k, v in params.items() if v is not None and v != ""},
|
||||
quote_via=quote,
|
||||
)
|
||||
|
||||
url = "mailto:" + self.form.cleaned_data["email_address"]
|
||||
if encoded_params:
|
||||
url += "?" + encoded_params
|
||||
|
||||
return {
|
||||
"url": url,
|
||||
"title": self.form.cleaned_data["link_text"].strip()
|
||||
or self.form.cleaned_data["email_address"],
|
||||
# If the user has explicitly entered / edited something in the link_text field,
|
||||
# always use that text. If not, we should favour keeping the existing link/selection
|
||||
# text, where applicable.
|
||||
"prefer_this_title_as_link_text": ("link_text" in self.form.changed_data),
|
||||
}
|
||||
|
||||
def post(self, request):
|
||||
self.form = self.form_class(
|
||||
request.POST, initial=self.get_initial_data(), prefix=self.form_prefix
|
||||
)
|
||||
|
||||
if self.form.is_valid():
|
||||
result = self.get_result_data()
|
||||
return self.render_chosen_response(result)
|
||||
else: # form invalid
|
||||
return self.render_form_response()
|
||||
|
||||
def parse_email_link(self, mailto):
|
||||
result = {}
|
||||
|
||||
mail_result = urlsplit(mailto)
|
||||
|
||||
result["email"] = mail_result.path
|
||||
|
||||
query = parse_qs(mail_result.query)
|
||||
result["subject"] = query["subject"][0] if "subject" in query else ""
|
||||
result["body"] = query["body"][0] if "body" in query else ""
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class PhoneLinkView(BaseLinkFormView):
|
||||
form_prefix = "phone-link-chooser"
|
||||
form_class = PhoneLinkChooserForm
|
||||
template_name = "wagtailadmin/chooser/phone_link.html"
|
||||
step_name = "phone_link"
|
||||
link_url_field_name = "phone_number"
|
||||
|
||||
def get_url_from_field_value(self, value):
|
||||
value = re.sub(r"\s", "", value)
|
||||
return "tel:" + value
|
||||
79
env/lib/python3.10/site-packages/wagtail/admin/views/collection_privacy.py
vendored
Normal file
79
env/lib/python3.10/site-packages/wagtail/admin/views/collection_privacy.py
vendored
Normal file
@@ -0,0 +1,79 @@
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from wagtail.admin.forms.collections import CollectionViewRestrictionForm
|
||||
from wagtail.admin.modal_workflow import render_modal_workflow
|
||||
from wagtail.models import Collection, CollectionViewRestriction
|
||||
from wagtail.permissions import collection_permission_policy
|
||||
|
||||
|
||||
def set_privacy(request, collection_id):
|
||||
collection = get_object_or_404(Collection, id=collection_id)
|
||||
if not collection_permission_policy.user_has_permission(request.user, "change"):
|
||||
raise PermissionDenied
|
||||
|
||||
# fetch restriction records in depth order so that ancestors appear first
|
||||
restrictions = collection.get_view_restrictions().order_by("collection__depth")
|
||||
if restrictions:
|
||||
restriction = restrictions[0]
|
||||
restriction_exists_on_ancestor = restriction.collection != collection
|
||||
else:
|
||||
restriction = None
|
||||
restriction_exists_on_ancestor = False
|
||||
|
||||
if request.method == "POST":
|
||||
form = CollectionViewRestrictionForm(request.POST, instance=restriction)
|
||||
if form.is_valid() and not restriction_exists_on_ancestor:
|
||||
if form.cleaned_data["restriction_type"] == CollectionViewRestriction.NONE:
|
||||
# remove any existing restriction
|
||||
if restriction:
|
||||
restriction.delete()
|
||||
else:
|
||||
restriction = form.save(commit=False)
|
||||
restriction.collection = collection
|
||||
form.save()
|
||||
|
||||
return render_modal_workflow(
|
||||
request,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
json_data={
|
||||
"step": "set_privacy_done",
|
||||
"is_public": (form.cleaned_data["restriction_type"] == "none"),
|
||||
},
|
||||
)
|
||||
|
||||
else: # request is a GET
|
||||
if not restriction_exists_on_ancestor:
|
||||
if restriction:
|
||||
form = CollectionViewRestrictionForm(instance=restriction)
|
||||
else:
|
||||
# no current view restrictions on this collection
|
||||
form = CollectionViewRestrictionForm(
|
||||
initial={"restriction_type": "none"}
|
||||
)
|
||||
|
||||
if restriction_exists_on_ancestor:
|
||||
# display a message indicating that there is a restriction at ancestor level -
|
||||
# do not provide the form for setting up new restrictions
|
||||
return render_modal_workflow(
|
||||
request,
|
||||
"wagtailadmin/collection_privacy/ancestor_privacy.html",
|
||||
None,
|
||||
{
|
||||
"collection_with_restriction": restriction.collection,
|
||||
},
|
||||
)
|
||||
else:
|
||||
# no restriction set at ancestor level - can set restrictions here
|
||||
return render_modal_workflow(
|
||||
request,
|
||||
"wagtailadmin/collection_privacy/set_privacy.html",
|
||||
None,
|
||||
{
|
||||
"collection": collection,
|
||||
"form": form,
|
||||
},
|
||||
json_data={"step": "set_privacy"},
|
||||
)
|
||||
194
env/lib/python3.10/site-packages/wagtail/admin/views/collections.py
vendored
Normal file
194
env/lib/python3.10/site-packages/wagtail/admin/views/collections.py
vendored
Normal file
@@ -0,0 +1,194 @@
|
||||
from django.http import HttpResponseForbidden
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from wagtail import hooks
|
||||
from wagtail.admin import messages
|
||||
from wagtail.admin.forms.collections import CollectionForm
|
||||
from wagtail.admin.ui.tables import TitleColumn
|
||||
from wagtail.admin.views.generic import CreateView, DeleteView, EditView, IndexView
|
||||
from wagtail.models import Collection
|
||||
from wagtail.permissions import collection_permission_policy
|
||||
|
||||
|
||||
class Index(IndexView):
|
||||
permission_policy = collection_permission_policy
|
||||
model = Collection
|
||||
context_object_name = "collections"
|
||||
results_template_name = "wagtailadmin/collections/index_results.html"
|
||||
add_url_name = "wagtailadmin_collections:add"
|
||||
index_url_name = "wagtailadmin_collections:index"
|
||||
page_title = gettext_lazy("Collections")
|
||||
add_item_label = gettext_lazy("Add a collection")
|
||||
header_icon = "folder-open-1"
|
||||
columns = [
|
||||
TitleColumn(
|
||||
"name",
|
||||
label=gettext_lazy("Name"),
|
||||
url_name="wagtailadmin_collections:edit",
|
||||
id_accessor="0",
|
||||
accessor="1",
|
||||
)
|
||||
]
|
||||
_show_breadcrumbs = True
|
||||
|
||||
def get_queryset(self):
|
||||
return self.permission_policy.instances_user_has_any_permission_for(
|
||||
self.request.user, ["add", "change", "delete"]
|
||||
).exclude(depth=1)
|
||||
|
||||
def get_table(self, object_list):
|
||||
return super().get_table(object_list.get_indented_choices())
|
||||
|
||||
|
||||
class Create(CreateView):
|
||||
permission_policy = collection_permission_policy
|
||||
model = Collection
|
||||
form_class = CollectionForm
|
||||
page_title = gettext_lazy("Add collection")
|
||||
success_message = gettext_lazy("Collection '%(object)s' created.")
|
||||
add_url_name = "wagtailadmin_collections:add"
|
||||
edit_url_name = "wagtailadmin_collections:edit"
|
||||
index_url_name = "wagtailadmin_collections:index"
|
||||
header_icon = "folder-open-1"
|
||||
_show_breadcrumbs = True
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
# Now filter collections offered in parent field by current user's add permissions
|
||||
collections = self.permission_policy.instances_user_has_permission_for(
|
||||
self.request.user, "add"
|
||||
)
|
||||
form.fields["parent"].queryset = collections
|
||||
return form
|
||||
|
||||
def save_instance(self):
|
||||
instance = self.form.save(commit=False)
|
||||
parent = self.form.cleaned_data["parent"]
|
||||
parent.add_child(instance=instance)
|
||||
return instance
|
||||
|
||||
|
||||
class Edit(EditView):
|
||||
permission_policy = collection_permission_policy
|
||||
model = Collection
|
||||
form_class = CollectionForm
|
||||
template_name = "wagtailadmin/collections/edit.html"
|
||||
success_message = gettext_lazy("Collection '%(object)s' updated.")
|
||||
error_message = gettext_lazy("The collection could not be saved due to errors.")
|
||||
delete_item_label = gettext_lazy("Delete collection")
|
||||
edit_url_name = "wagtailadmin_collections:edit"
|
||||
index_url_name = "wagtailadmin_collections:index"
|
||||
delete_url_name = "wagtailadmin_collections:delete"
|
||||
context_object_name = "collection"
|
||||
header_icon = "folder-open-1"
|
||||
_show_breadcrumbs = True
|
||||
|
||||
def _user_may_move_collection(self, user, instance):
|
||||
"""
|
||||
Is this instance used for assigning GroupCollectionPermissions to the user?
|
||||
If so, this user is not allowed do move the collection to a new part of the tree
|
||||
"""
|
||||
if user.is_active and user.is_superuser:
|
||||
return True
|
||||
else:
|
||||
permissions = (
|
||||
self.permission_policy._get_user_permission_objects_for_actions(
|
||||
user, {"add", "change", "delete"}
|
||||
)
|
||||
)
|
||||
return not {
|
||||
permission
|
||||
for permission in permissions
|
||||
if permission.collection_id == instance.pk
|
||||
}
|
||||
|
||||
def get_queryset(self):
|
||||
return self.permission_policy.instances_user_has_permission_for(
|
||||
self.request.user, "change"
|
||||
).exclude(depth=1)
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
user = self.request.user
|
||||
# if user does not have add permission anywhere, they can't move a collection
|
||||
if not self.permission_policy.user_has_permission(user, "add"):
|
||||
form.fields.pop("parent")
|
||||
# If this instance is a collection used to assign permissions for this user,
|
||||
# do not let the user move this collection.
|
||||
elif not self._user_may_move_collection(user, form.instance):
|
||||
form.fields.pop("parent")
|
||||
else:
|
||||
# Filter collections offered in parent field by current user's add permissions
|
||||
collections = self.permission_policy.instances_user_has_permission_for(
|
||||
user, "add"
|
||||
)
|
||||
form.fields["parent"].queryset = collections
|
||||
# Disable unavailable options in CollectionChoiceField select widget
|
||||
form.fields["parent"].disabled_queryset = form.instance.get_descendants(
|
||||
inclusive=True
|
||||
)
|
||||
|
||||
form.initial["parent"] = form.instance.get_parent().pk
|
||||
return form
|
||||
|
||||
def save_instance(self):
|
||||
instance = self.form.save()
|
||||
if "parent" in self.form.changed_data:
|
||||
instance.move(self.form.cleaned_data["parent"], "sorted-child")
|
||||
return instance
|
||||
|
||||
|
||||
class Delete(DeleteView):
|
||||
permission_policy = collection_permission_policy
|
||||
model = Collection
|
||||
success_message = gettext_lazy("Collection '%(object)s' deleted.")
|
||||
index_url_name = "wagtailadmin_collections:index"
|
||||
edit_url_name = "wagtailadmin_collections:edit"
|
||||
delete_url_name = "wagtailadmin_collections:delete"
|
||||
page_title = gettext_lazy("Delete collection")
|
||||
confirmation_message = gettext_lazy(
|
||||
"Are you sure you want to delete this collection?"
|
||||
)
|
||||
header_icon = "folder-open-1"
|
||||
|
||||
def get_queryset(self):
|
||||
return self.permission_policy.instances_user_has_permission_for(
|
||||
self.request.user, "delete"
|
||||
).exclude(depth=1)
|
||||
|
||||
def get_collection_contents(self):
|
||||
collection_contents = [
|
||||
hook(self.object)
|
||||
for hook in hooks.get_hooks("describe_collection_contents")
|
||||
]
|
||||
|
||||
# filter out any hook responses that report that the collection is empty
|
||||
# (by returning None, or a dict with 'count': 0)
|
||||
def is_nonempty(item_type):
|
||||
return item_type and item_type["count"] > 0
|
||||
|
||||
return list(filter(is_nonempty, collection_contents))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
collection_contents = self.get_collection_contents()
|
||||
|
||||
if collection_contents:
|
||||
# collection is non-empty; render the 'not allowed to delete' response
|
||||
self.template_name = "wagtailadmin/collections/delete_not_empty.html"
|
||||
context["collection_contents"] = collection_contents
|
||||
|
||||
return context
|
||||
|
||||
def post(self, request, pk):
|
||||
self.object = get_object_or_404(self.get_queryset(), id=pk)
|
||||
collection_contents = self.get_collection_contents()
|
||||
|
||||
if collection_contents:
|
||||
# collection is non-empty; refuse to delete it
|
||||
return HttpResponseForbidden()
|
||||
|
||||
messages.success(request, self.get_success_message())
|
||||
self.object.delete()
|
||||
return redirect(self.index_url_name)
|
||||
26
env/lib/python3.10/site-packages/wagtail/admin/views/dismissibles.py
vendored
Normal file
26
env/lib/python3.10/site-packages/wagtail/admin/views/dismissibles.py
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
import json
|
||||
|
||||
from django.http import HttpResponseBadRequest, JsonResponse
|
||||
from django.views import View
|
||||
|
||||
from wagtail.users.models import UserProfile
|
||||
|
||||
|
||||
class DismissiblesView(View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
# The UserProfile may not exist for the user, in which case return an empty object
|
||||
profile = getattr(request.user, "wagtail_userprofile", None)
|
||||
dismissibles = profile.dismissibles if profile else {}
|
||||
return JsonResponse(dismissibles)
|
||||
|
||||
def patch(self, request, *args, **kwargs):
|
||||
try:
|
||||
updates = json.loads(request.body)
|
||||
except json.JSONDecodeError:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
# Make sure the UserProfile exists
|
||||
profile = UserProfile.get_for_user(request.user)
|
||||
profile.dismissibles.update(updates)
|
||||
profile.save(update_fields=["dismissibles"])
|
||||
return JsonResponse(profile.dismissibles)
|
||||
200
env/lib/python3.10/site-packages/wagtail/admin/views/editing_sessions.py
vendored
Normal file
200
env/lib/python3.10/site-packages/wagtail/admin/views/editing_sessions.py
vendored
Normal file
@@ -0,0 +1,200 @@
|
||||
from django.apps import apps
|
||||
from django.contrib.admin.utils import unquote
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.http import Http404, JsonResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.http import require_POST
|
||||
|
||||
from wagtail.admin.models import EditingSession
|
||||
from wagtail.admin.ui.editing_sessions import EditingSessionsList
|
||||
from wagtail.admin.utils import get_user_display_name
|
||||
from wagtail.models import Page, Revision, RevisionMixin, WorkflowMixin
|
||||
|
||||
|
||||
@require_POST
|
||||
def ping(request, app_label, model_name, object_id, session_id):
|
||||
try:
|
||||
model = apps.get_model(app_label, model_name)
|
||||
except LookupError:
|
||||
raise Http404
|
||||
|
||||
unquoted_object_id = unquote(object_id)
|
||||
|
||||
content_type = ContentType.objects.get_for_model(model)
|
||||
|
||||
obj = get_object_or_404(model, pk=unquoted_object_id)
|
||||
if isinstance(obj, Page):
|
||||
can_edit = obj.permissions_for_user(request.user).can_edit()
|
||||
else:
|
||||
try:
|
||||
permission_policy = model.snippet_viewset.permission_policy
|
||||
except AttributeError:
|
||||
# model is neither a Page nor a snippet
|
||||
raise Http404
|
||||
|
||||
can_edit = permission_policy.user_has_permission_for_instance(
|
||||
request.user, "change", obj
|
||||
)
|
||||
if not can_edit and isinstance(obj, WorkflowMixin):
|
||||
workflow = obj.get_workflow()
|
||||
if workflow is not None:
|
||||
current_workflow_task = obj.current_workflow_task
|
||||
can_edit = (
|
||||
current_workflow_task
|
||||
and current_workflow_task.user_can_access_editor(obj, request.user)
|
||||
)
|
||||
|
||||
if not can_edit:
|
||||
raise Http404
|
||||
|
||||
try:
|
||||
session = EditingSession.objects.get(
|
||||
id=session_id,
|
||||
user=request.user,
|
||||
content_type=content_type,
|
||||
object_id=unquoted_object_id,
|
||||
)
|
||||
except EditingSession.DoesNotExist:
|
||||
session = EditingSession(
|
||||
content_type=content_type,
|
||||
object_id=unquoted_object_id,
|
||||
user=request.user,
|
||||
)
|
||||
|
||||
session.last_seen_at = timezone.now()
|
||||
session.is_editing = request.POST.get("is_editing", False)
|
||||
try:
|
||||
session.full_clean()
|
||||
except ValidationError:
|
||||
return JsonResponse({"error": "Invalid data"}, status=400)
|
||||
else:
|
||||
session.save()
|
||||
|
||||
other_sessions = (
|
||||
EditingSession.objects.filter(
|
||||
content_type=content_type,
|
||||
object_id=unquoted_object_id,
|
||||
last_seen_at__gte=timezone.now() - timezone.timedelta(minutes=1),
|
||||
)
|
||||
.exclude(id=session.id)
|
||||
.select_related("user", "user__wagtail_userprofile")
|
||||
.order_by("-last_seen_at")
|
||||
)
|
||||
|
||||
# create a lookup of sessions indexed by user ID. Multiple sessions from the same user
|
||||
# are merged, such that the most recently seen one is reported, but is_editing is true
|
||||
# if any session has the editing flag set (not just the latest one).
|
||||
other_sessions_lookup = {}
|
||||
for other_session in other_sessions:
|
||||
try:
|
||||
other_session_info = other_sessions_lookup[other_session.user.pk]
|
||||
except KeyError:
|
||||
other_sessions_lookup[other_session.user.pk] = {
|
||||
"session_id": other_session.id,
|
||||
"user": other_session.user,
|
||||
"last_seen_at": other_session.last_seen_at,
|
||||
"is_editing": other_session.is_editing,
|
||||
"revision_id": None,
|
||||
}
|
||||
else:
|
||||
if other_session.is_editing:
|
||||
other_session_info["is_editing"] = True
|
||||
|
||||
revision_id = request.POST.get("revision_id", None)
|
||||
if revision_id is not None and issubclass(model, RevisionMixin):
|
||||
all_revisions = obj.revisions.defer("content")
|
||||
try:
|
||||
original_revision = all_revisions.get(id=revision_id)
|
||||
except Revision.DoesNotExist:
|
||||
raise Http404
|
||||
|
||||
newest_revision = (
|
||||
all_revisions.filter(created_at__gt=original_revision.created_at)
|
||||
.order_by("-created_at", "-pk")
|
||||
.select_related("user")
|
||||
.first()
|
||||
)
|
||||
|
||||
if newest_revision:
|
||||
try:
|
||||
session_info = other_sessions_lookup[newest_revision.user_id]
|
||||
except KeyError:
|
||||
other_sessions_lookup[newest_revision.user_id] = {
|
||||
"session_id": None,
|
||||
"user": newest_revision.user,
|
||||
"last_seen_at": newest_revision.created_at,
|
||||
"is_editing": False,
|
||||
"revision_id": newest_revision.id,
|
||||
}
|
||||
else:
|
||||
session_info["revision_id"] = newest_revision.id
|
||||
if newest_revision.created_at > session_info["last_seen_at"]:
|
||||
session_info["last_seen_at"] = newest_revision.created_at
|
||||
|
||||
try:
|
||||
users_other_session = other_sessions_lookup[request.user.pk]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
# If the user has a different session that is not editing and hasn't
|
||||
# created the latest revision, hide it as it's not relevant.
|
||||
if (
|
||||
not users_other_session["is_editing"]
|
||||
and not users_other_session["revision_id"]
|
||||
):
|
||||
other_sessions_lookup.pop(request.user.pk)
|
||||
|
||||
# Sort the other sessions so that they are presented in the following order:
|
||||
# 1. Prioritise any session with the latest revision. Then,
|
||||
# 2. Prioritise any session that is currently editing. Then,
|
||||
# 3. Prioritise any session with the smallest id, so that new sessions are
|
||||
# appended to the end of the list (they're shown last). We are not sorting
|
||||
# by last_seen_at to avoid shifting the order of the sessions as they
|
||||
# ping the server.
|
||||
other_sessions = sorted(
|
||||
other_sessions_lookup.values(),
|
||||
key=lambda other_session: (
|
||||
# We want to sort revision_id and is_editing in descending order,
|
||||
# but we want to sort session_id in ascending order. To achieve this
|
||||
# in a single pass, we negate the values of revision_id and is_editing.
|
||||
# We can negate revision_id because there can only be one (at most)
|
||||
# session with revision_id, so we only care about the presence and
|
||||
# not the ID itself, thus we can treat it as a boolean flag.
|
||||
not other_session["revision_id"],
|
||||
not other_session["is_editing"],
|
||||
other_session["session_id"],
|
||||
),
|
||||
)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"session_id": session.id,
|
||||
"ping_url": reverse(
|
||||
"wagtailadmin_editing_sessions:ping",
|
||||
args=(app_label, model_name, object_id, session.id),
|
||||
),
|
||||
"release_url": reverse(
|
||||
"wagtailadmin_editing_sessions:release", args=(session.id,)
|
||||
),
|
||||
"other_sessions": [
|
||||
{
|
||||
"session_id": other_session["session_id"],
|
||||
"user": get_user_display_name(other_session["user"]),
|
||||
"last_seen_at": other_session["last_seen_at"].isoformat(),
|
||||
"is_editing": other_session["is_editing"],
|
||||
"revision_id": other_session["revision_id"],
|
||||
}
|
||||
for other_session in other_sessions
|
||||
],
|
||||
"html": EditingSessionsList(session, other_sessions).render_html(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@require_POST
|
||||
def release(request, session_id):
|
||||
EditingSession.objects.filter(id=session_id, user=request.user).delete()
|
||||
return JsonResponse({})
|
||||
30
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__init__.py
vendored
Normal file
30
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__init__.py
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
from .base import ( # noqa: F401
|
||||
BaseListingView,
|
||||
BaseObjectMixin,
|
||||
BaseOperationView,
|
||||
WagtailAdminTemplateMixin,
|
||||
)
|
||||
from .history import HistoryView # noqa: F401
|
||||
from .mixins import ( # noqa: F401
|
||||
BeforeAfterHookMixin,
|
||||
CreateEditViewOptionalFeaturesMixin,
|
||||
HookResponseMixin,
|
||||
IndexViewOptionalFeaturesMixin,
|
||||
LocaleMixin,
|
||||
PanelMixin,
|
||||
RevisionsRevertMixin,
|
||||
)
|
||||
from .models import ( # noqa: F401
|
||||
CopyView,
|
||||
CopyViewMixin,
|
||||
CreateView,
|
||||
DeleteView,
|
||||
EditView,
|
||||
IndexView,
|
||||
InspectView,
|
||||
RevisionsCompareView,
|
||||
RevisionsUnscheduleView,
|
||||
UnpublishView,
|
||||
)
|
||||
from .permissions import PermissionCheckedMixin # noqa: F401
|
||||
from .usage import UsageView # noqa: F401
|
||||
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/base.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/base.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/chooser.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/chooser.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/history.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/history.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/lock.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/lock.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/mixins.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/mixins.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/models.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/models.cpython-310.pyc
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/permissions.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/permissions.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/preview.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/preview.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/usage.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/usage.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/workflow.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/generic/__pycache__/workflow.cpython-310.pyc
vendored
Normal file
Binary file not shown.
477
env/lib/python3.10/site-packages/wagtail/admin/views/generic/base.py
vendored
Normal file
477
env/lib/python3.10/site-packages/wagtail/admin/views/generic/base.py
vendored
Normal file
@@ -0,0 +1,477 @@
|
||||
from collections import namedtuple
|
||||
|
||||
from django.contrib.admin.utils import quote, unquote
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.db import models
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.generic.base import ContextMixin, TemplateResponseMixin
|
||||
from django.views.generic.list import BaseListView
|
||||
from django_filters.filters import (
|
||||
ChoiceFilter,
|
||||
DateFromToRangeFilter,
|
||||
ModelChoiceFilter,
|
||||
ModelMultipleChoiceFilter,
|
||||
MultipleChoiceFilter,
|
||||
)
|
||||
|
||||
from wagtail.admin import messages
|
||||
from wagtail.admin.ui.tables import Column, Table
|
||||
from wagtail.admin.utils import get_valid_next_url_from_request
|
||||
from wagtail.admin.widgets.button import ButtonWithDropdown
|
||||
from wagtail.utils.utils import flatten_choices
|
||||
|
||||
|
||||
class WagtailAdminTemplateMixin(TemplateResponseMixin, ContextMixin):
|
||||
"""
|
||||
Mixin for views that render a template response using the standard Wagtail admin
|
||||
page furniture.
|
||||
Provides accessors for page title, subtitle and header icon.
|
||||
"""
|
||||
|
||||
page_title = ""
|
||||
page_subtitle = ""
|
||||
header_icon = ""
|
||||
# Breadcrumbs are opt-in until we have a design that can be consistently applied
|
||||
_show_breadcrumbs = False
|
||||
breadcrumbs_items = [{"url": reverse_lazy("wagtailadmin_home"), "label": _("Home")}]
|
||||
template_name = "wagtailadmin/generic/base.html"
|
||||
header_buttons = []
|
||||
header_more_buttons = []
|
||||
|
||||
def get_page_title(self):
|
||||
return self.page_title
|
||||
|
||||
def get_page_subtitle(self):
|
||||
return self.page_subtitle
|
||||
|
||||
def get_header_title(self):
|
||||
title = self.get_page_title()
|
||||
subtitle = self.get_page_subtitle()
|
||||
if subtitle:
|
||||
title = f"{title}: {subtitle}"
|
||||
return title
|
||||
|
||||
def get_header_icon(self):
|
||||
return self.header_icon
|
||||
|
||||
def get_breadcrumbs_items(self):
|
||||
return self.breadcrumbs_items
|
||||
|
||||
def get_header_buttons(self):
|
||||
buttons = sorted(self.header_buttons)
|
||||
|
||||
more_buttons = self.get_header_more_buttons()
|
||||
if more_buttons:
|
||||
buttons.append(
|
||||
ButtonWithDropdown(
|
||||
buttons=more_buttons,
|
||||
icon_name="dots-horizontal",
|
||||
attrs={"aria-label": _("Actions")},
|
||||
classname="w-h-slim-header",
|
||||
)
|
||||
)
|
||||
return buttons
|
||||
|
||||
def get_header_more_buttons(self):
|
||||
return sorted(self.header_more_buttons)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# These are only used for legacy header.html
|
||||
# and view templates that don't use "wagtailadmin/generic/base.html"
|
||||
context["page_title"] = self.get_page_title()
|
||||
context["page_subtitle"] = self.get_page_subtitle()
|
||||
context["header_icon"] = self.get_header_icon()
|
||||
|
||||
# Once all appropriate views use "wagtailadmin/generic/base.html" and
|
||||
# the slim_header.html, _show_breadcrumbs can be removed
|
||||
context["header_title"] = self.get_header_title()
|
||||
context["breadcrumbs_items"] = None
|
||||
if self._show_breadcrumbs:
|
||||
context["breadcrumbs_items"] = self.get_breadcrumbs_items()
|
||||
context["header_buttons"] = self.get_header_buttons()
|
||||
return context
|
||||
|
||||
def get_template_names(self):
|
||||
# Instead of always wrapping self.template_name in a list like
|
||||
# TemplateResponseMixin does, only do so if it's not already a list/tuple.
|
||||
# This allows us to use a list of template names in self.template_name.
|
||||
if isinstance(self.template_name, (list, tuple)):
|
||||
return self.template_name
|
||||
return super().get_template_names()
|
||||
|
||||
|
||||
class BaseObjectMixin:
|
||||
"""Mixin for views that make use of a model instance."""
|
||||
|
||||
model = None
|
||||
pk_url_kwarg = "pk"
|
||||
|
||||
def setup(self, request, *args, **kwargs):
|
||||
super().setup(request, *args, **kwargs)
|
||||
self.pk = self.get_pk()
|
||||
self.object = self.get_object()
|
||||
self.model_opts = self.object._meta
|
||||
|
||||
def get_pk(self):
|
||||
return unquote(str(self.kwargs[self.pk_url_kwarg]))
|
||||
|
||||
def get_base_object_queryset(self):
|
||||
return self.model._default_manager.all()
|
||||
|
||||
def get_object(self):
|
||||
if not self.model:
|
||||
raise ImproperlyConfigured(
|
||||
"Subclasses of wagtail.admin.views.generic.base.BaseObjectMixin must provide a "
|
||||
"model attribute or a get_object method"
|
||||
)
|
||||
return get_object_or_404(self.get_base_object_queryset(), pk=self.pk)
|
||||
|
||||
|
||||
class BaseOperationView(BaseObjectMixin, View):
|
||||
"""Base view to perform an operation on a model instance using a POST request."""
|
||||
|
||||
success_message = None
|
||||
success_message_extra_tags = ""
|
||||
success_url_name = None
|
||||
|
||||
def setup(self, request, *args, **kwargs):
|
||||
super().setup(request, *args, **kwargs)
|
||||
self.next_url = get_valid_next_url_from_request(request)
|
||||
|
||||
def perform_operation(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_success_message(self):
|
||||
return self.success_message
|
||||
|
||||
def add_success_message(self):
|
||||
success_message = self.get_success_message()
|
||||
if success_message:
|
||||
messages.success(
|
||||
self.request,
|
||||
success_message,
|
||||
extra_tags=self.success_message_extra_tags,
|
||||
)
|
||||
|
||||
def get_success_url(self):
|
||||
if not self.success_url_name:
|
||||
raise ImproperlyConfigured(
|
||||
"Subclasses of wagtail.admin.views.generic.base.BaseOperationView must provide a "
|
||||
"success_url_name attribute or a get_success_url method"
|
||||
)
|
||||
if self.next_url:
|
||||
return self.next_url
|
||||
return reverse(self.success_url_name, args=[quote(self.object.pk)])
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.perform_operation()
|
||||
self.add_success_message()
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
|
||||
# Represents a django-filters filter that is currently in force on a listing queryset
|
||||
ActiveFilter = namedtuple(
|
||||
"ActiveFilter", ["auto_id", "field_label", "value", "removed_filter_url"]
|
||||
)
|
||||
|
||||
|
||||
class BaseListingView(WagtailAdminTemplateMixin, BaseListView):
|
||||
template_name = "wagtailadmin/generic/listing.html"
|
||||
results_template_name = "wagtailadmin/generic/listing_results.html"
|
||||
results_only = False # If true, just render the results as an HTML fragment
|
||||
table_class = Table
|
||||
table_classname = None
|
||||
columns = [Column("__str__", label=_("Title"))]
|
||||
index_url_name = None
|
||||
index_results_url_name = None
|
||||
page_kwarg = "p"
|
||||
default_ordering = None
|
||||
filterset_class = None
|
||||
|
||||
def get_template_names(self):
|
||||
if self.results_only:
|
||||
if isinstance(self.results_template_name, (list, tuple)):
|
||||
return self.results_template_name
|
||||
return [self.results_template_name]
|
||||
else:
|
||||
return super().get_template_names()
|
||||
|
||||
@cached_property
|
||||
def filters(self):
|
||||
if self.filterset_class:
|
||||
filterset = self.filterset_class(**self.get_filterset_kwargs())
|
||||
# Don't use the filterset if it has no fields
|
||||
if filterset.form.fields:
|
||||
return filterset
|
||||
|
||||
@cached_property
|
||||
def is_filtering(self):
|
||||
# we are filtering if the filter form has changed from its default state
|
||||
return (
|
||||
self.filters and self.filters.is_valid() and self.filters.form.has_changed()
|
||||
)
|
||||
|
||||
def get_filterset_kwargs(self):
|
||||
return {
|
||||
"data": self.request.GET,
|
||||
"request": self.request,
|
||||
}
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
if self.filters and self.filters.is_valid():
|
||||
queryset = self.filters.filter_queryset(queryset)
|
||||
return queryset
|
||||
|
||||
def get_url_without_filter_param(self, param):
|
||||
"""
|
||||
Return the index URL with the given filter parameter removed from the query string
|
||||
"""
|
||||
base_url = self.index_results_url.split("?")[0]
|
||||
query_dict = self.request.GET.copy()
|
||||
query_dict.pop(self.page_kwarg, None) # reset pagination to first page
|
||||
if isinstance(param, (list, tuple)):
|
||||
for p in param:
|
||||
query_dict.pop(p, None)
|
||||
else:
|
||||
query_dict.pop(param, None)
|
||||
query_dict["_w_filter_fragment"] = 1
|
||||
return base_url + "?" + query_dict.urlencode()
|
||||
|
||||
def get_url_without_filter_param_value(self, param, value):
|
||||
"""
|
||||
Return the index URL where the filter parameter with the given value has been removed
|
||||
from the query string, preserving all other values for that parameter
|
||||
"""
|
||||
base_url = self.index_results_url.split("?")[0]
|
||||
query_dict = self.request.GET.copy()
|
||||
query_dict.pop(self.page_kwarg, None) # reset pagination to first page
|
||||
query_dict.setlist(
|
||||
param, [v for v in query_dict.getlist(param) if v != str(value)]
|
||||
)
|
||||
query_dict["_w_filter_fragment"] = 1
|
||||
return base_url + "?" + query_dict.urlencode()
|
||||
|
||||
@cached_property
|
||||
def active_filters(self):
|
||||
filters = []
|
||||
|
||||
if not self.filters:
|
||||
return filters
|
||||
|
||||
for field_name in self.filters.form.changed_data:
|
||||
filter_def = self.filters.filters[field_name]
|
||||
bound_field = self.filters.form[field_name]
|
||||
try:
|
||||
value = self.filters.form.cleaned_data[field_name]
|
||||
except KeyError:
|
||||
continue # invalid filter value
|
||||
|
||||
if value == bound_field.initial:
|
||||
continue # filter value is the same as the default
|
||||
|
||||
if isinstance(filter_def, ModelMultipleChoiceFilter):
|
||||
field = filter_def.field
|
||||
for item in value:
|
||||
filters.append(
|
||||
ActiveFilter(
|
||||
bound_field.auto_id,
|
||||
filter_def.label,
|
||||
field.label_from_instance(item),
|
||||
self.get_url_without_filter_param_value(
|
||||
field_name, item.pk
|
||||
),
|
||||
)
|
||||
)
|
||||
elif isinstance(filter_def, MultipleChoiceFilter):
|
||||
choices = flatten_choices(filter_def.field.choices)
|
||||
for item in value:
|
||||
filters.append(
|
||||
ActiveFilter(
|
||||
bound_field.auto_id,
|
||||
filter_def.label,
|
||||
choices.get(str(item), str(item)),
|
||||
self.get_url_without_filter_param_value(field_name, item),
|
||||
)
|
||||
)
|
||||
elif isinstance(filter_def, ModelChoiceFilter):
|
||||
field = filter_def.field
|
||||
filters.append(
|
||||
ActiveFilter(
|
||||
bound_field.auto_id,
|
||||
filter_def.label,
|
||||
field.label_from_instance(value),
|
||||
self.get_url_without_filter_param(field_name),
|
||||
)
|
||||
)
|
||||
elif isinstance(filter_def, DateFromToRangeFilter):
|
||||
start_date_display = date_format(value.start) if value.start else ""
|
||||
end_date_display = date_format(value.stop) if value.stop else ""
|
||||
widget = filter_def.field.widget
|
||||
filters.append(
|
||||
ActiveFilter(
|
||||
bound_field.auto_id,
|
||||
filter_def.label,
|
||||
"%s - %s" % (start_date_display, end_date_display),
|
||||
self.get_url_without_filter_param(
|
||||
[
|
||||
widget.suffixed(field_name, suffix)
|
||||
for suffix in widget.suffixes
|
||||
]
|
||||
),
|
||||
)
|
||||
)
|
||||
elif isinstance(filter_def, ChoiceFilter):
|
||||
choices = flatten_choices(filter_def.field.choices)
|
||||
filters.append(
|
||||
ActiveFilter(
|
||||
bound_field.auto_id,
|
||||
filter_def.label,
|
||||
choices.get(str(value), str(value)),
|
||||
self.get_url_without_filter_param(field_name),
|
||||
)
|
||||
)
|
||||
else:
|
||||
filters.append(
|
||||
ActiveFilter(
|
||||
bound_field.auto_id,
|
||||
filter_def.label,
|
||||
str(value),
|
||||
self.get_url_without_filter_param(field_name),
|
||||
)
|
||||
)
|
||||
|
||||
return filters
|
||||
|
||||
def get_valid_orderings(self):
|
||||
orderings = []
|
||||
for col in self.columns:
|
||||
if col.sort_key:
|
||||
orderings.append(col.sort_key)
|
||||
orderings.append("-%s" % col.sort_key)
|
||||
return orderings
|
||||
|
||||
@cached_property
|
||||
def is_explicitly_ordered(self):
|
||||
return "ordering" in self.request.GET
|
||||
|
||||
def get_ordering(self):
|
||||
ordering = self.request.GET.get("ordering", self.default_ordering)
|
||||
if ordering not in self.get_valid_orderings():
|
||||
ordering = self.default_ordering
|
||||
return ordering
|
||||
|
||||
@cached_property
|
||||
def ordering(self):
|
||||
return self.get_ordering()
|
||||
|
||||
def order_queryset(self, queryset):
|
||||
if not self.ordering:
|
||||
return queryset
|
||||
|
||||
ordering = self.ordering
|
||||
if not isinstance(ordering, (list, tuple)):
|
||||
ordering = (ordering,)
|
||||
return queryset.order_by(*ordering)
|
||||
|
||||
def get_base_queryset(self):
|
||||
if self.queryset is not None:
|
||||
queryset = self.queryset
|
||||
if isinstance(queryset, models.QuerySet):
|
||||
queryset = queryset.all()
|
||||
elif self.model is not None:
|
||||
queryset = self.model._default_manager.all()
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
"%(cls)s is missing a QuerySet. Define "
|
||||
"%(cls)s.model, %(cls)s.queryset, or override "
|
||||
"%(cls)s.get_queryset()." % {"cls": self.__class__.__name__}
|
||||
)
|
||||
return queryset
|
||||
|
||||
def get_queryset(self):
|
||||
# Instead of calling super().get_queryset(), we copy the initial logic from Django's
|
||||
# MultipleObjectMixin into get_base_queryset(). This allows us to perform additional steps
|
||||
# before the ordering step (such as annotations), and funnel the call to get_ordering()
|
||||
# through the cached property self.ordering so that we don't have to worry about calling
|
||||
# get_ordering() multiple times.
|
||||
# https://github.com/django/django/blob/stable/4.1.x/django/views/generic/list.py#L22-L47
|
||||
|
||||
queryset = self.get_base_queryset()
|
||||
queryset = self.order_queryset(queryset)
|
||||
queryset = self.filter_queryset(queryset)
|
||||
return queryset
|
||||
|
||||
def get_table_kwargs(self):
|
||||
return {
|
||||
"ordering": self.ordering,
|
||||
"classname": self.table_classname,
|
||||
"base_url": self.index_url,
|
||||
}
|
||||
|
||||
def get_table(self, object_list):
|
||||
return self.table_class(
|
||||
self.columns,
|
||||
object_list,
|
||||
**self.get_table_kwargs(),
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def index_url(self):
|
||||
return self.get_index_url()
|
||||
|
||||
def get_index_url(self):
|
||||
if self.index_url_name:
|
||||
return reverse(self.index_url_name)
|
||||
|
||||
@cached_property
|
||||
def index_results_url(self):
|
||||
return self.get_index_results_url()
|
||||
|
||||
def get_index_results_url(self):
|
||||
if self.index_results_url_name:
|
||||
return reverse(self.index_results_url_name)
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
|
||||
table = self.get_table(context["object_list"])
|
||||
|
||||
context["index_url"] = self.index_url
|
||||
context["index_results_url"] = self.index_results_url
|
||||
context["table"] = table
|
||||
context["media"] = table.media
|
||||
# On Django's BaseListView, a listing where pagination is applied, but the results
|
||||
# only run to a single page, is considered is_paginated=False. Override this to
|
||||
# always consider a listing to be paginated if pagination is applied. This ensures
|
||||
# that we output "Page 1 of 1" as is standard in Wagtail.
|
||||
context["is_paginated"] = context["page_obj"] is not None
|
||||
|
||||
if context["is_paginated"]:
|
||||
context["items_count"] = context["paginator"].count
|
||||
else:
|
||||
context["items_count"] = len(context["object_list"])
|
||||
|
||||
if self.filters:
|
||||
context["filters"] = self.filters
|
||||
context["is_filtering"] = self.is_filtering
|
||||
context["media"] += self.filters.form.media
|
||||
|
||||
# If we're rendering the results as an HTML fragment, the caller can pass a _w_filter_fragment=1
|
||||
# URL parameter to indicate that the filters should be rendered as a <template> block so that
|
||||
# we can replace the existing filters.
|
||||
context["render_filters_fragment"] = (
|
||||
self.request.GET.get("_w_filter_fragment")
|
||||
and self.filters
|
||||
and self.results_only
|
||||
)
|
||||
context["render_buttons_fragment"] = (
|
||||
context.get("header_buttons") and self.results_only
|
||||
)
|
||||
|
||||
return context
|
||||
577
env/lib/python3.10/site-packages/wagtail/admin/views/generic/chooser.py
vendored
Normal file
577
env/lib/python3.10/site-packages/wagtail/admin/views/generic/chooser.py
vendored
Normal file
@@ -0,0 +1,577 @@
|
||||
import re
|
||||
import urllib.parse
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.admin.utils import quote, unquote
|
||||
from django.core.exceptions import (
|
||||
ImproperlyConfigured,
|
||||
ObjectDoesNotExist,
|
||||
PermissionDenied,
|
||||
)
|
||||
from django.core.paginator import InvalidPage, Paginator
|
||||
from django.db.models import Model
|
||||
from django.forms.models import modelform_factory
|
||||
from django.http import Http404
|
||||
from django.template.loader import render_to_string
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic.base import ContextMixin, View
|
||||
|
||||
from wagtail import hooks
|
||||
from wagtail.admin.admin_url_finder import AdminURLFinder
|
||||
from wagtail.admin.forms.choosers import (
|
||||
BaseFilterForm,
|
||||
CollectionFilterMixin,
|
||||
LocaleFilterMixin,
|
||||
SearchFilterMixin,
|
||||
)
|
||||
from wagtail.admin.modal_workflow import render_modal_workflow
|
||||
from wagtail.admin.ui.tables import Column, Table, TitleColumn
|
||||
from wagtail.coreutils import resolve_model_string
|
||||
from wagtail.models import CollectionMember, TranslatableMixin
|
||||
from wagtail.permission_policies import BlanketPermissionPolicy, ModelPermissionPolicy
|
||||
from wagtail.search.index import class_is_indexed
|
||||
|
||||
|
||||
class ModalPageFurnitureMixin(ContextMixin):
|
||||
"""
|
||||
Add icon, page title and page subtitle to the template context
|
||||
"""
|
||||
|
||||
icon = None
|
||||
page_title = None
|
||||
page_subtitle = None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update(
|
||||
{
|
||||
"header_icon": self.icon,
|
||||
"page_title": self.page_title,
|
||||
"page_subtitle": self.page_subtitle,
|
||||
}
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class ModelLookupMixin:
|
||||
"""
|
||||
Allows a class to have a `model` attribute, which can be set as either a model class or a string,
|
||||
and then retrieve it as `model_class` to consistently get back a model class
|
||||
"""
|
||||
|
||||
model = None
|
||||
|
||||
@cached_property
|
||||
def model_class(self):
|
||||
if self.model:
|
||||
return resolve_model_string(self.model)
|
||||
|
||||
|
||||
class PreserveURLParametersMixin:
|
||||
"""
|
||||
Adds support for passing designated URL parameters from the current request when constructing URLs
|
||||
for links / form actions.
|
||||
"""
|
||||
|
||||
preserve_url_parameters = ["multiple"]
|
||||
|
||||
@cached_property
|
||||
def _preserved_param_string(self):
|
||||
params = {}
|
||||
for param in self.preserve_url_parameters:
|
||||
try:
|
||||
params[param] = self.request.GET[param]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return urllib.parse.urlencode(params)
|
||||
|
||||
def append_preserved_url_parameters(self, url):
|
||||
"""
|
||||
Given a base URL (which might already include URL parameters), append any URL parameters
|
||||
from the preserve_url_parameters list that are present in the current request URL
|
||||
"""
|
||||
if self._preserved_param_string:
|
||||
if "?" in url:
|
||||
url += "&" + self._preserved_param_string
|
||||
else:
|
||||
url += "?" + self._preserved_param_string
|
||||
|
||||
return url
|
||||
|
||||
|
||||
class CheckboxSelectColumn(Column):
|
||||
cell_template_name = "wagtailadmin/generic/chooser/checkbox_select_cell.html"
|
||||
|
||||
|
||||
class BaseChooseView(
|
||||
ModalPageFurnitureMixin,
|
||||
ModelLookupMixin,
|
||||
PreserveURLParametersMixin,
|
||||
ContextMixin,
|
||||
View,
|
||||
):
|
||||
"""
|
||||
Provides common functionality for views that present a (possibly searchable / filterable) list
|
||||
of objects to choose from
|
||||
"""
|
||||
|
||||
per_page = 10
|
||||
ordering = None
|
||||
chosen_url_name = None
|
||||
chosen_multiple_url_name = None
|
||||
results_url_name = None
|
||||
icon = "snippet"
|
||||
page_title = _("Choose")
|
||||
filter_form_class = None
|
||||
template_name = "wagtailadmin/generic/chooser/chooser.html"
|
||||
results_template_name = "wagtailadmin/generic/chooser/results.html"
|
||||
construct_queryset_hook_name = None
|
||||
url_filter_parameters = []
|
||||
|
||||
def get_object_list(self):
|
||||
return self.model_class.objects.all()
|
||||
|
||||
def apply_object_list_ordering(self, objects):
|
||||
if isinstance(self.ordering, (list, tuple)):
|
||||
objects = objects.order_by(*self.ordering)
|
||||
elif self.ordering:
|
||||
objects = objects.order_by(self.ordering)
|
||||
elif objects.ordered:
|
||||
# Preserve the model-level ordering if specified
|
||||
pass
|
||||
else:
|
||||
# fall back on PK to ensure pagination is consistent
|
||||
objects = objects.order_by("pk")
|
||||
|
||||
return objects
|
||||
|
||||
def get_filter_form_class(self):
|
||||
if self.filter_form_class:
|
||||
return self.filter_form_class
|
||||
else:
|
||||
bases = [BaseFilterForm]
|
||||
if self.model_class:
|
||||
if class_is_indexed(self.model_class):
|
||||
bases.insert(0, SearchFilterMixin)
|
||||
if issubclass(self.model_class, CollectionMember):
|
||||
bases.insert(0, CollectionFilterMixin)
|
||||
|
||||
i18n_enabled = getattr(settings, "WAGTAIL_I18N_ENABLED", False)
|
||||
if i18n_enabled and issubclass(self.model_class, TranslatableMixin):
|
||||
bases.insert(0, LocaleFilterMixin)
|
||||
|
||||
return type(
|
||||
"FilterForm",
|
||||
tuple(bases),
|
||||
{},
|
||||
)
|
||||
|
||||
def get_filter_form(self):
|
||||
FilterForm = self.get_filter_form_class()
|
||||
return FilterForm(self.request.GET)
|
||||
|
||||
def filter_object_list(self, objects):
|
||||
filters = {}
|
||||
for filter in self.url_filter_parameters:
|
||||
try:
|
||||
filters[filter] = self.request.GET[filter]
|
||||
except KeyError:
|
||||
pass
|
||||
if filters:
|
||||
objects = objects.filter(**filters)
|
||||
|
||||
if self.construct_queryset_hook_name:
|
||||
# allow hooks to modify the queryset
|
||||
for hook in hooks.get_hooks(self.construct_queryset_hook_name):
|
||||
objects = hook(objects, self.request)
|
||||
|
||||
if self.filter_form.is_valid():
|
||||
objects = self.filter_form.filter(objects)
|
||||
return objects
|
||||
|
||||
def get_results_url(self):
|
||||
return self.append_preserved_url_parameters(reverse(self.results_url_name))
|
||||
|
||||
def get_chosen_multiple_url(self):
|
||||
return self.append_preserved_url_parameters(
|
||||
reverse(self.chosen_multiple_url_name)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def is_multiple_choice(self):
|
||||
return self.request.GET.get("multiple")
|
||||
|
||||
@property
|
||||
def columns(self):
|
||||
return [self.title_column]
|
||||
|
||||
@property
|
||||
def title_column(self):
|
||||
if self.is_multiple_choice:
|
||||
return TitleColumn(
|
||||
"title",
|
||||
label=_("Title"),
|
||||
accessor=str,
|
||||
label_prefix="chooser-modal-select",
|
||||
)
|
||||
else:
|
||||
return TitleColumn(
|
||||
"title",
|
||||
label=_("Title"),
|
||||
accessor=str,
|
||||
get_url=(
|
||||
lambda obj: self.append_preserved_url_parameters(
|
||||
reverse(self.chosen_url_name, args=(quote(obj.pk),))
|
||||
)
|
||||
),
|
||||
link_attrs={"data-chooser-modal-choice": True},
|
||||
)
|
||||
|
||||
@property
|
||||
def checkbox_column(self):
|
||||
return CheckboxSelectColumn(
|
||||
"select", label=_("Select"), width="1%", accessor="pk"
|
||||
)
|
||||
|
||||
def get_results_page(self, request):
|
||||
objects = self.get_object_list()
|
||||
objects = self.apply_object_list_ordering(objects)
|
||||
objects = self.filter_object_list(objects)
|
||||
|
||||
paginator = Paginator(objects, per_page=self.per_page)
|
||||
try:
|
||||
return paginator.page(request.GET.get("p", 1))
|
||||
except InvalidPage:
|
||||
raise Http404
|
||||
|
||||
def get(self, request):
|
||||
self.filter_form = self.get_filter_form()
|
||||
self.results = self.get_results_page(request)
|
||||
|
||||
columns = self.columns
|
||||
if self.is_multiple_choice:
|
||||
columns.insert(0, self.checkbox_column)
|
||||
self.table = Table(columns, self.results)
|
||||
|
||||
return self.render_to_response()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
results_url = self.get_results_url()
|
||||
|
||||
# For result pagination links, we need a version of results_url with parameters removed,
|
||||
# so that the pagination include can append its own parameters via the {% querystring %} template tag
|
||||
results_pagination_url = re.sub(r"\?.*$", "", results_url)
|
||||
|
||||
context.update(
|
||||
{
|
||||
"results": self.results,
|
||||
"table": self.table,
|
||||
"results_url": results_url,
|
||||
"results_pagination_url": results_pagination_url,
|
||||
"is_searching": self.filter_form.is_searching,
|
||||
"is_filtering_by_collection": self.filter_form.is_filtering_by_collection,
|
||||
"is_multiple_choice": self.is_multiple_choice,
|
||||
"search_query": self.filter_form.search_query,
|
||||
"can_create": self.can_create(),
|
||||
}
|
||||
)
|
||||
if self.is_multiple_choice:
|
||||
context["chosen_multiple_url"] = self.get_chosen_multiple_url()
|
||||
return context
|
||||
|
||||
def render_to_response(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class CreationFormMixin(ModelLookupMixin, PreserveURLParametersMixin):
|
||||
"""
|
||||
Provides a form class for creating new objects
|
||||
"""
|
||||
|
||||
creation_form_class = None
|
||||
form_fields = None
|
||||
exclude_form_fields = None
|
||||
creation_form_template_name = "wagtailadmin/generic/chooser/creation_form.html"
|
||||
creation_tab_id = "create"
|
||||
create_action_label = _("Create")
|
||||
create_action_clicked_label = None
|
||||
create_url_name = None
|
||||
permission_policy = None
|
||||
|
||||
def get_permission_policy(self):
|
||||
if self.permission_policy:
|
||||
return self.permission_policy
|
||||
elif self.model_class and issubclass(self.model_class, Model):
|
||||
return ModelPermissionPolicy(self.model_class)
|
||||
else:
|
||||
return BlanketPermissionPolicy(None)
|
||||
|
||||
def can_create(self):
|
||||
return self.get_permission_policy().user_has_permission(
|
||||
self.request.user, "add"
|
||||
)
|
||||
|
||||
def get_creation_form_class(self):
|
||||
if self.creation_form_class:
|
||||
return self.creation_form_class
|
||||
elif self.form_fields is not None or self.exclude_form_fields is not None:
|
||||
return modelform_factory(
|
||||
self.model_class,
|
||||
fields=self.form_fields,
|
||||
exclude=self.exclude_form_fields,
|
||||
)
|
||||
|
||||
def get_creation_form_kwargs(self):
|
||||
kwargs = {}
|
||||
if self.request.method in ("POST", "PUT"):
|
||||
kwargs.update(
|
||||
{
|
||||
"data": self.request.POST,
|
||||
"files": self.request.FILES,
|
||||
}
|
||||
)
|
||||
return kwargs
|
||||
|
||||
def get_creation_form(self):
|
||||
form_class = self.get_creation_form_class()
|
||||
if not form_class:
|
||||
return None
|
||||
|
||||
return form_class(**self.get_creation_form_kwargs())
|
||||
|
||||
def get_create_url(self):
|
||||
if not self.create_url_name:
|
||||
raise ImproperlyConfigured(
|
||||
"%r must provide a create_url_name attribute or a get_create_url method"
|
||||
% type(self)
|
||||
)
|
||||
return self.append_preserved_url_parameters(reverse(self.create_url_name))
|
||||
|
||||
def get_creation_form_context_data(self, form):
|
||||
return {
|
||||
"creation_form": form,
|
||||
"create_action_url": self.get_create_url(),
|
||||
"create_action_label": self.create_action_label,
|
||||
"create_action_clicked_label": self.create_action_clicked_label,
|
||||
}
|
||||
|
||||
|
||||
class ChooseViewMixin:
|
||||
"""
|
||||
A view that renders a complete modal response for the chooser, including a tab for the object
|
||||
listing and (optionally) a 'create' form
|
||||
"""
|
||||
|
||||
search_tab_label = _("Search")
|
||||
creation_tab_label = None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update(
|
||||
{
|
||||
"filter_form": self.filter_form,
|
||||
"search_tab_label": self.search_tab_label,
|
||||
"creation_tab_label": self.creation_tab_label
|
||||
or self.create_action_label,
|
||||
}
|
||||
)
|
||||
|
||||
if context["can_create"]:
|
||||
creation_form = self.get_creation_form()
|
||||
if creation_form:
|
||||
context.update(self.get_creation_form_context_data(creation_form))
|
||||
|
||||
return context
|
||||
|
||||
def get_response_json_data(self):
|
||||
return {
|
||||
"step": "choose",
|
||||
}
|
||||
|
||||
# Return the choose view as a ModalWorkflow response
|
||||
def render_to_response(self):
|
||||
return render_modal_workflow(
|
||||
self.request,
|
||||
self.template_name,
|
||||
None,
|
||||
self.get_context_data(),
|
||||
json_data=self.get_response_json_data(),
|
||||
)
|
||||
|
||||
|
||||
class ChooseView(ChooseViewMixin, CreationFormMixin, BaseChooseView):
|
||||
pass
|
||||
|
||||
|
||||
class ChooseResultsViewMixin:
|
||||
"""
|
||||
A view that renders just the object listing as an HTML fragment, used to replace the listing
|
||||
when paginating or searching
|
||||
"""
|
||||
|
||||
# Return just the HTML fragment for the results
|
||||
def render_to_response(self):
|
||||
return TemplateResponse(
|
||||
self.request,
|
||||
self.results_template_name,
|
||||
self.get_context_data(),
|
||||
)
|
||||
|
||||
|
||||
class ChooseResultsView(ChooseResultsViewMixin, CreationFormMixin, BaseChooseView):
|
||||
pass
|
||||
|
||||
|
||||
class ChosenResponseMixin:
|
||||
"""
|
||||
Provides methods for returning the chosen object from the modal workflow.
|
||||
"""
|
||||
|
||||
response_data_title_key = "title"
|
||||
chosen_response_name = "chosen"
|
||||
|
||||
def get_object_id(self, instance):
|
||||
return instance.pk
|
||||
|
||||
def get_display_title(self, instance):
|
||||
"""
|
||||
Return a string representation of the given object instance
|
||||
"""
|
||||
return str(instance)
|
||||
|
||||
def get_edit_item_url(self, instance):
|
||||
return AdminURLFinder(user=self.request.user).get_edit_url(instance)
|
||||
|
||||
def get_chosen_response_data(self, item):
|
||||
"""
|
||||
Generate the result value to be returned when an object has been chosen
|
||||
"""
|
||||
return {
|
||||
"id": str(self.get_object_id(item)),
|
||||
self.response_data_title_key: self.get_display_title(item),
|
||||
"edit_url": self.get_edit_item_url(item),
|
||||
}
|
||||
|
||||
def _wrap_chosen_response_data(self, response_data):
|
||||
"""
|
||||
Wrap a response_data JSON payload in a modal workflow response
|
||||
"""
|
||||
return render_modal_workflow(
|
||||
self.request,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
json_data={"step": self.chosen_response_name, "result": response_data},
|
||||
)
|
||||
|
||||
def get_multiple_chosen_response(self, items):
|
||||
response_data = [self.get_chosen_response_data(item) for item in items]
|
||||
return self._wrap_chosen_response_data(response_data)
|
||||
|
||||
def get_chosen_response(self, item):
|
||||
"""
|
||||
Return the HTTP response to indicate that an object has been chosen
|
||||
"""
|
||||
response_data = self.get_chosen_response_data(item)
|
||||
|
||||
if self.request.GET.get("multiple"):
|
||||
# a multiple result was requested but we're only returning one,
|
||||
# so wrap as a list
|
||||
response_data = [response_data]
|
||||
|
||||
return self._wrap_chosen_response_data(response_data)
|
||||
|
||||
|
||||
class ChosenViewMixin(ModelLookupMixin):
|
||||
"""
|
||||
A view that takes an object ID in the URL and returns a modal workflow response indicating
|
||||
that object has been chosen
|
||||
"""
|
||||
|
||||
def get_object(self, pk):
|
||||
return self.model_class.objects.get(pk=pk)
|
||||
|
||||
def get(self, request, pk):
|
||||
try:
|
||||
item = self.get_object(unquote(pk))
|
||||
except ObjectDoesNotExist:
|
||||
raise Http404
|
||||
|
||||
return self.get_chosen_response(item)
|
||||
|
||||
|
||||
class ChosenView(ChosenViewMixin, ChosenResponseMixin, View):
|
||||
pass
|
||||
|
||||
|
||||
class ChosenMultipleViewMixin(ModelLookupMixin):
|
||||
"""
|
||||
A view that takes a list of 'id' URL parameters and returns a modal workflow response indicating
|
||||
that those objects have been chosen
|
||||
"""
|
||||
|
||||
def get_objects(self, pks):
|
||||
return self.model_class.objects.filter(pk__in=pks)
|
||||
|
||||
def get(self, request):
|
||||
items = self.get_objects(request.GET.getlist("id"))
|
||||
return self.get_multiple_chosen_response(items)
|
||||
|
||||
|
||||
class ChosenMultipleView(ChosenMultipleViewMixin, ChosenResponseMixin, View):
|
||||
pass
|
||||
|
||||
|
||||
class CreateViewMixin:
|
||||
"""
|
||||
A view that handles submissions of the 'create' form
|
||||
"""
|
||||
|
||||
model = None
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not self.can_create():
|
||||
raise PermissionDenied
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request):
|
||||
self.form = self.get_creation_form()
|
||||
return self.get_reshow_creation_form_response()
|
||||
|
||||
def save_form(self, form):
|
||||
return form.save()
|
||||
|
||||
def post(self, request):
|
||||
self.form = self.get_creation_form()
|
||||
if self.form.is_valid():
|
||||
object = self.save_form(self.form)
|
||||
return self.get_chosen_response(object)
|
||||
else:
|
||||
return self.get_reshow_creation_form_response()
|
||||
|
||||
def get_reshow_creation_form_response(self):
|
||||
context = {"view": self}
|
||||
context.update(self.get_creation_form_context_data(self.form))
|
||||
response_html = render_to_string(
|
||||
self.creation_form_template_name, context, self.request
|
||||
)
|
||||
return render_modal_workflow(
|
||||
self.request,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
json_data={
|
||||
"step": "reshow_creation_form",
|
||||
"htmlFragment": response_html,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class CreateView(CreateViewMixin, CreationFormMixin, ChosenResponseMixin, View):
|
||||
pass
|
||||
487
env/lib/python3.10/site-packages/wagtail/admin/views/generic/history.py
vendored
Normal file
487
env/lib/python3.10/site-packages/wagtail/admin/views/generic/history.py
vendored
Normal file
@@ -0,0 +1,487 @@
|
||||
from datetime import timedelta
|
||||
|
||||
import django_filters
|
||||
from django.contrib.admin.utils import quote
|
||||
from django.core.paginator import Paginator
|
||||
from django.forms import CheckboxSelectMultiple
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import gettext, gettext_lazy
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from wagtail.admin.filters import (
|
||||
DateRangePickerWidget,
|
||||
MultipleUserFilter,
|
||||
WagtailFilterSet,
|
||||
)
|
||||
from wagtail.admin.ui.tables import Column, DateColumn, UserColumn
|
||||
from wagtail.admin.utils import get_latest_str
|
||||
from wagtail.admin.views.generic.base import (
|
||||
BaseListingView,
|
||||
BaseObjectMixin,
|
||||
WagtailAdminTemplateMixin,
|
||||
)
|
||||
from wagtail.admin.views.generic.permissions import PermissionCheckedMixin
|
||||
from wagtail.admin.widgets.button import HeaderButton
|
||||
from wagtail.log_actions import registry as log_registry
|
||||
from wagtail.models import (
|
||||
BaseLogEntry,
|
||||
DraftStateMixin,
|
||||
PreviewableMixin,
|
||||
Revision,
|
||||
RevisionMixin,
|
||||
TaskState,
|
||||
WorkflowState,
|
||||
)
|
||||
|
||||
|
||||
def get_actions_for_filter(queryset):
|
||||
# Only return those actions used by model log entries.
|
||||
actions = set(queryset.get_actions())
|
||||
return [action for action in log_registry.get_choices() if action[0] in actions]
|
||||
|
||||
|
||||
class HistoryFilterSet(WagtailFilterSet):
|
||||
action = django_filters.MultipleChoiceFilter(
|
||||
label=gettext_lazy("Action"),
|
||||
widget=CheckboxSelectMultiple,
|
||||
# choices are set dynamically in __init__()
|
||||
)
|
||||
user = MultipleUserFilter(
|
||||
label=gettext_lazy("User"),
|
||||
widget=CheckboxSelectMultiple,
|
||||
# queryset is set dynamically in __init__()
|
||||
)
|
||||
timestamp = django_filters.DateFromToRangeFilter(
|
||||
label=gettext_lazy("Date"), widget=DateRangePickerWidget
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
actions = self.get_action_choices()
|
||||
if not actions:
|
||||
del self.filters["action"]
|
||||
else:
|
||||
self.filters["action"].extra["choices"] = actions
|
||||
|
||||
users = self.get_users_queryset()
|
||||
if not users.exists():
|
||||
del self.filters["user"]
|
||||
else:
|
||||
self.filters["user"].extra["queryset"] = users
|
||||
|
||||
def get_action_choices(self):
|
||||
return get_actions_for_filter(self.queryset)
|
||||
|
||||
def get_users_queryset(self):
|
||||
return self.queryset.get_users()
|
||||
|
||||
|
||||
class ActionColumn(Column):
|
||||
def __init__(self, *args, object, url_names, user_can_unschedule, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.object = object
|
||||
self.url_names = url_names
|
||||
self.user_can_unschedule = user_can_unschedule
|
||||
self.revision_enabled = isinstance(object, RevisionMixin)
|
||||
self.draftstate_enabled = isinstance(object, DraftStateMixin)
|
||||
|
||||
@cached_property
|
||||
def cell_template_name(self):
|
||||
if self.revision_enabled:
|
||||
return "wagtailadmin/generic/history/action_cell.html"
|
||||
return super().cell_template_name
|
||||
|
||||
def get_status(self, instance, parent_context):
|
||||
if self.draftstate_enabled:
|
||||
if (
|
||||
instance.action == "wagtail.publish"
|
||||
and instance.revision_id == self.object.live_revision_id
|
||||
):
|
||||
return gettext("Live version")
|
||||
elif (
|
||||
instance.content_changed
|
||||
and instance.revision_id == self.object.latest_revision_id
|
||||
):
|
||||
return gettext("Current draft")
|
||||
return None
|
||||
|
||||
def get_actions(self, instance, parent_context):
|
||||
actions = []
|
||||
|
||||
# Do not show the revision actions if the log entry:
|
||||
# - has no revision attached
|
||||
# - has no content changes
|
||||
# - is a "publish" action
|
||||
# (because we want to show the options on the "edit" action instead)
|
||||
if (
|
||||
not self.revision_enabled
|
||||
or not instance.revision_id
|
||||
or not instance.content_changed
|
||||
or instance.action == "wagtail.publish"
|
||||
):
|
||||
return actions
|
||||
|
||||
if (
|
||||
isinstance(self.object, PreviewableMixin)
|
||||
and self.object.is_previewable()
|
||||
and (url_name := self.url_names.get("revisions_view"))
|
||||
):
|
||||
url = reverse(url_name, args=(quote(self.object.pk), instance.revision_id))
|
||||
action = {"url": url, "label": gettext("Preview")}
|
||||
actions.append(action)
|
||||
|
||||
if instance.revision_id == self.object.latest_revision_id:
|
||||
if url_name := self.url_names.get("edit"):
|
||||
url = reverse(url_name, args=(quote(self.object.pk),))
|
||||
action = {"url": url, "label": gettext("Edit")}
|
||||
actions.append(action)
|
||||
elif url_name := self.url_names.get("revisions_revert"):
|
||||
url = reverse(url_name, args=(quote(self.object.pk), instance.revision_id))
|
||||
action = {"url": url, "label": gettext("Review this version")}
|
||||
actions.append(action)
|
||||
|
||||
if url_name := self.url_names.get("revisions_compare"):
|
||||
if instance.previous_revision_id:
|
||||
url = reverse(
|
||||
url_name,
|
||||
args=(
|
||||
quote(self.object.pk),
|
||||
instance.previous_revision_id,
|
||||
instance.revision_id,
|
||||
),
|
||||
)
|
||||
action = {"url": url, "label": gettext("Compare with previous version")}
|
||||
actions.append(action)
|
||||
if instance.revision_id != self.object.latest_revision_id:
|
||||
url = reverse(
|
||||
url_name,
|
||||
args=(quote(self.object.pk), instance.revision_id, "latest"),
|
||||
)
|
||||
action = {"url": url, "label": gettext("Compare with current version")}
|
||||
actions.append(action)
|
||||
|
||||
if (
|
||||
(url_name := self.url_names.get("revisions_unschedule"))
|
||||
and instance.revision.approved_go_live_at
|
||||
and self.user_can_unschedule
|
||||
):
|
||||
url = reverse(url_name, args=(quote(self.object.pk), instance.revision_id))
|
||||
action = {"url": url, "label": gettext("Cancel scheduled publish")}
|
||||
actions.append(action)
|
||||
|
||||
return actions
|
||||
|
||||
def get_cell_context_data(self, instance, parent_context):
|
||||
context = super().get_cell_context_data(instance, parent_context)
|
||||
context["status"] = self.get_status(instance, parent_context)
|
||||
context["actions"] = self.get_actions(instance, parent_context)
|
||||
return context
|
||||
|
||||
|
||||
class LogEntryUserColumn(UserColumn):
|
||||
def __init__(self, name, **kwargs):
|
||||
# Instead of accepting a blank_display_name arg, we'll make use of the
|
||||
# BaseLogEntry.user_display_name property which also handles the display
|
||||
# name for a deleted user (as the BaseLogEntry still stores the ID).
|
||||
super().__init__(name, blank_display_name=None, **kwargs)
|
||||
|
||||
def get_cell_context_data(self, instance, parent_context):
|
||||
context = super().get_cell_context_data(instance, parent_context)
|
||||
if not context["display_name"]:
|
||||
context["display_name"] = instance.user_display_name
|
||||
return context
|
||||
|
||||
|
||||
class HistoryView(PermissionCheckedMixin, BaseObjectMixin, BaseListingView):
|
||||
any_permission_required = ["add", "change", "delete"]
|
||||
page_title = gettext_lazy("History")
|
||||
results_template_name = "wagtailadmin/generic/history_results.html"
|
||||
header_icon = "history"
|
||||
is_searchable = False
|
||||
paginate_by = 20
|
||||
filterset_class = HistoryFilterSet
|
||||
history_url_name = None
|
||||
history_results_url_name = None
|
||||
edit_url_name = None
|
||||
revisions_view_url_name = None
|
||||
revisions_revert_url_name = None
|
||||
revisions_compare_url_name = None
|
||||
revisions_unschedule_url_name = None
|
||||
|
||||
@cached_property
|
||||
def columns(self):
|
||||
return [
|
||||
ActionColumn(
|
||||
"message",
|
||||
label=gettext_lazy("Action"),
|
||||
object=self.object,
|
||||
url_names={
|
||||
"edit": self.edit_url_name,
|
||||
"revisions_view": self.revisions_view_url_name,
|
||||
"revisions_revert": self.revisions_revert_url_name,
|
||||
"revisions_compare": self.revisions_compare_url_name,
|
||||
"revisions_unschedule": self.revisions_unschedule_url_name,
|
||||
},
|
||||
user_can_unschedule=self.user_can_unschedule(),
|
||||
),
|
||||
LogEntryUserColumn("user", label=gettext_lazy("User"), width="25%"),
|
||||
DateColumn("timestamp", label=gettext_lazy("Date"), width="15%"),
|
||||
]
|
||||
|
||||
def get_base_object_queryset(self):
|
||||
queryset = super().get_base_object_queryset()
|
||||
if issubclass(queryset.model, RevisionMixin):
|
||||
return queryset.select_related("latest_revision")
|
||||
return queryset
|
||||
|
||||
def get_page_subtitle(self):
|
||||
return get_latest_str(self.object)
|
||||
|
||||
def get_breadcrumbs_items(self):
|
||||
items = []
|
||||
if self.index_url_name:
|
||||
items.append(
|
||||
{
|
||||
"url": reverse(self.index_url_name),
|
||||
"label": capfirst(self.model._meta.verbose_name_plural),
|
||||
}
|
||||
)
|
||||
edit_url = self.get_edit_url(self.object)
|
||||
obj_name = self.get_page_subtitle()
|
||||
if edit_url:
|
||||
items.append(
|
||||
{
|
||||
"url": edit_url,
|
||||
"label": obj_name,
|
||||
}
|
||||
)
|
||||
items.append(
|
||||
{
|
||||
"url": "",
|
||||
"label": gettext("History"),
|
||||
"sublabel": obj_name,
|
||||
}
|
||||
)
|
||||
return self.breadcrumbs_items + items
|
||||
|
||||
@cached_property
|
||||
def header_buttons(self):
|
||||
return [
|
||||
HeaderButton(
|
||||
label=gettext("Edit"),
|
||||
url=self.get_edit_url(self.object),
|
||||
icon_name="edit",
|
||||
),
|
||||
]
|
||||
|
||||
def get_edit_url(self, instance):
|
||||
if self.edit_url_name:
|
||||
return reverse(self.edit_url_name, args=(quote(instance.pk),))
|
||||
|
||||
def get_history_url(self, instance):
|
||||
if self.history_url_name:
|
||||
return reverse(self.history_url_name, args=(quote(instance.pk),))
|
||||
|
||||
def get_history_results_url(self, instance):
|
||||
if self.history_results_url_name:
|
||||
return reverse(self.history_results_url_name, args=(quote(instance.pk),))
|
||||
|
||||
def get_index_url(self): # used for pagination links
|
||||
return self.get_history_url(self.object)
|
||||
|
||||
def get_index_results_url(self):
|
||||
return self.get_history_results_url(self.object)
|
||||
|
||||
def user_can_unschedule(self):
|
||||
return self.user_has_permission("publish")
|
||||
|
||||
def get_context_data(self, *args, object_list=None, **kwargs):
|
||||
context = super().get_context_data(*args, object_list=object_list, **kwargs)
|
||||
context["object"] = self.object
|
||||
context["model_opts"] = BaseLogEntry._meta
|
||||
return context
|
||||
|
||||
def get_base_queryset(self):
|
||||
queryset = log_registry.get_logs_for_instance(self.object)
|
||||
return self._annotate_queryset(queryset)
|
||||
|
||||
def _annotate_queryset(self, queryset):
|
||||
queryset = queryset.select_related("user", "user__wagtail_userprofile")
|
||||
if isinstance(self.object, RevisionMixin):
|
||||
queryset = queryset.select_related("revision").annotate(
|
||||
previous_revision_id=Revision.objects.previous_revision_id_subquery(),
|
||||
)
|
||||
return queryset
|
||||
|
||||
def get_filterset_kwargs(self):
|
||||
# Pass custom queryset so the FilterSet can use it when initialising the
|
||||
# filters, instead of using the default model.objects.all() queryset.
|
||||
kwargs = super().get_filterset_kwargs()
|
||||
kwargs["queryset"] = self.get_base_queryset()
|
||||
return kwargs
|
||||
|
||||
|
||||
class WorkflowHistoryView(BaseObjectMixin, WagtailAdminTemplateMixin, TemplateView):
|
||||
template_name = "wagtailadmin/shared/workflow_history/index.html"
|
||||
page_kwarg = "p"
|
||||
workflow_history_url_name = None
|
||||
workflow_history_detail_url_name = None
|
||||
|
||||
@cached_property
|
||||
def workflow_states(self):
|
||||
return WorkflowState.objects.for_instance(self.object).order_by("-created_at")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
paginator = Paginator(self.workflow_states, per_page=20)
|
||||
workflow_states = paginator.get_page(self.request.GET.get(self.page_kwarg))
|
||||
|
||||
context.update(
|
||||
{
|
||||
"object": self.object,
|
||||
"workflow_states": workflow_states,
|
||||
"workflow_history_url_name": self.workflow_history_url_name,
|
||||
"workflow_history_detail_url_name": self.workflow_history_detail_url_name,
|
||||
"model_opts": self.object._meta,
|
||||
}
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class WorkflowHistoryDetailView(
|
||||
BaseObjectMixin, WagtailAdminTemplateMixin, TemplateView
|
||||
):
|
||||
template_name = "wagtailadmin/shared/workflow_history/detail.html"
|
||||
workflow_state_url_kwarg = "workflow_state_id"
|
||||
workflow_history_url_name = None
|
||||
page_title = gettext_lazy("Workflow progress")
|
||||
header_icon = "list-ul"
|
||||
object_icon = "doc-empty-inverse"
|
||||
|
||||
@cached_property
|
||||
def workflow_state(self):
|
||||
return get_object_or_404(
|
||||
WorkflowState.objects.for_instance(self.object).filter(
|
||||
id=self.kwargs[self.workflow_state_url_kwarg]
|
||||
),
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def revisions(self):
|
||||
"""
|
||||
Get QuerySet of all revisions that have existed during this workflow state.
|
||||
It's possible that the object is edited while the workflow is running,
|
||||
so some tasks may be repeated. All tasks that have been completed no matter
|
||||
what revision needs to be displayed on this page.
|
||||
"""
|
||||
return (
|
||||
Revision.objects.for_instance(self.object)
|
||||
.filter(
|
||||
id__in=TaskState.objects.filter(
|
||||
workflow_state=self.workflow_state
|
||||
).values_list("revision_id", flat=True),
|
||||
)
|
||||
.order_by("-created_at")
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def tasks(self):
|
||||
return self.workflow_state.workflow.tasks.all()
|
||||
|
||||
@cached_property
|
||||
def task_states_by_revision(self):
|
||||
"""Get QuerySet of tasks completed for each revision."""
|
||||
task_states_by_revision_task = [
|
||||
(
|
||||
revision,
|
||||
{
|
||||
task_state.task: task_state
|
||||
for task_state in TaskState.objects.filter(
|
||||
workflow_state=self.workflow_state, revision=revision
|
||||
).specific()
|
||||
},
|
||||
)
|
||||
for revision in self.revisions
|
||||
]
|
||||
|
||||
# Make sure task states are always in a consistent order
|
||||
# In some cases, they can be completed in a different order to what they are defined
|
||||
task_states_by_revision = [
|
||||
(revision, [task_states_by_task.get(task, None) for task in self.tasks])
|
||||
for revision, task_states_by_task in task_states_by_revision_task
|
||||
]
|
||||
|
||||
return task_states_by_revision
|
||||
|
||||
@cached_property
|
||||
def timeline(self):
|
||||
"""Generate timeline."""
|
||||
completed_task_states = (
|
||||
TaskState.objects.filter(workflow_state=self.workflow_state)
|
||||
.exclude(finished_at__isnull=True)
|
||||
.exclude(status=TaskState.STATUS_CANCELLED)
|
||||
)
|
||||
|
||||
timeline = [
|
||||
{
|
||||
"time": self.workflow_state.created_at,
|
||||
"action": "workflow_started",
|
||||
"workflow_state": self.workflow_state,
|
||||
}
|
||||
]
|
||||
|
||||
if self.workflow_state.status not in (
|
||||
WorkflowState.STATUS_IN_PROGRESS,
|
||||
WorkflowState.STATUS_NEEDS_CHANGES,
|
||||
):
|
||||
last_task = completed_task_states.order_by("finished_at").last()
|
||||
if last_task:
|
||||
timeline.append(
|
||||
{
|
||||
"time": last_task.finished_at + timedelta(milliseconds=1),
|
||||
"action": "workflow_completed",
|
||||
"workflow_state": self.workflow_state,
|
||||
}
|
||||
)
|
||||
|
||||
for revision in self.revisions:
|
||||
timeline.append(
|
||||
{
|
||||
"time": revision.created_at,
|
||||
"action": "edited",
|
||||
"revision": revision,
|
||||
}
|
||||
)
|
||||
|
||||
for task_state in completed_task_states:
|
||||
timeline.append(
|
||||
{
|
||||
"time": task_state.finished_at,
|
||||
"action": "task_completed",
|
||||
"task_state": task_state,
|
||||
}
|
||||
)
|
||||
|
||||
timeline.sort(key=lambda t: t["time"])
|
||||
timeline.reverse()
|
||||
|
||||
return timeline
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update(
|
||||
{
|
||||
"object": self.object,
|
||||
"object_icon": self.object_icon,
|
||||
"workflow_state": self.workflow_state,
|
||||
"tasks": self.tasks,
|
||||
"task_states_by_revision": self.task_states_by_revision,
|
||||
"timeline": self.timeline,
|
||||
"workflow_history_url_name": self.workflow_history_url_name,
|
||||
}
|
||||
)
|
||||
return context
|
||||
42
env/lib/python3.10/site-packages/wagtail/admin/views/generic/lock.py
vendored
Normal file
42
env/lib/python3.10/site-packages/wagtail/admin/views/generic/lock.py
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
from django.utils import timezone
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from wagtail.admin.utils import get_latest_str
|
||||
from wagtail.admin.views.generic.base import BaseOperationView
|
||||
from wagtail.log_actions import log
|
||||
|
||||
|
||||
class LockView(BaseOperationView):
|
||||
success_message_extra_tags = "lock"
|
||||
|
||||
def perform_operation(self):
|
||||
if self.object.locked:
|
||||
return
|
||||
self.object.locked = True
|
||||
self.object.locked_by = self.request.user
|
||||
self.object.locked_at = timezone.now()
|
||||
self.object.save(update_fields=["locked", "locked_by", "locked_at"])
|
||||
log(instance=self.object, action="wagtail.lock", user=self.request.user)
|
||||
|
||||
|
||||
class UnlockView(BaseOperationView):
|
||||
success_message_extra_tags = "unlock"
|
||||
|
||||
def perform_operation(self):
|
||||
if not self.object.locked:
|
||||
return
|
||||
self.object.locked = False
|
||||
self.object.locked_by = None
|
||||
self.object.locked_at = None
|
||||
self.object.save(update_fields=["locked", "locked_by", "locked_at"])
|
||||
log(instance=self.object, action="wagtail.unlock", user=self.request.user)
|
||||
|
||||
def get_success_message(self):
|
||||
return capfirst(
|
||||
_("%(model_name)s '%(title)s' is now unlocked.")
|
||||
% {
|
||||
"model_name": self.model._meta.verbose_name,
|
||||
"title": get_latest_str(self.object),
|
||||
}
|
||||
)
|
||||
824
env/lib/python3.10/site-packages/wagtail/admin/views/generic/mixins.py
vendored
Normal file
824
env/lib/python3.10/site-packages/wagtail/admin/views/generic/mixins.py
vendored
Normal file
@@ -0,0 +1,824 @@
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.admin.utils import quote
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models, transaction
|
||||
from django.forms import Media
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from wagtail import hooks
|
||||
from wagtail.admin import messages
|
||||
from wagtail.admin.models import EditingSession
|
||||
from wagtail.admin.templatetags.wagtailadmin_tags import user_display_name
|
||||
from wagtail.admin.ui.editing_sessions import EditingSessionsModule
|
||||
from wagtail.admin.ui.tables import TitleColumn
|
||||
from wagtail.admin.utils import get_latest_str, set_query_params
|
||||
from wagtail.locks import BasicLock, ScheduledForPublishLock, WorkflowLock
|
||||
from wagtail.log_actions import log
|
||||
from wagtail.log_actions import registry as log_registry
|
||||
from wagtail.models import (
|
||||
DraftStateMixin,
|
||||
Locale,
|
||||
LockableMixin,
|
||||
PreviewableMixin,
|
||||
RevisionMixin,
|
||||
TranslatableMixin,
|
||||
WorkflowMixin,
|
||||
WorkflowState,
|
||||
)
|
||||
from wagtail.utils.timestamps import render_timestamp
|
||||
|
||||
|
||||
class HookResponseMixin:
|
||||
"""
|
||||
A mixin for class-based views to run hooks by `hook_name`.
|
||||
"""
|
||||
|
||||
def run_hook(self, hook_name, *args, **kwargs):
|
||||
"""
|
||||
Run the named hook, passing args and kwargs to each function registered under that hook name.
|
||||
If any return an HttpResponse, stop processing and return that response
|
||||
"""
|
||||
for fn in hooks.get_hooks(hook_name):
|
||||
result = fn(*args, **kwargs)
|
||||
if hasattr(result, "status_code"):
|
||||
return result
|
||||
return None
|
||||
|
||||
|
||||
class BeforeAfterHookMixin(HookResponseMixin):
|
||||
"""
|
||||
A mixin for class-based views to support hooks like `before_edit_page` and
|
||||
`after_edit_page`, which are triggered during execution of some operation and
|
||||
can return a response to halt that operation and/or change the view response.
|
||||
"""
|
||||
|
||||
def run_before_hook(self):
|
||||
"""
|
||||
Define how to run the hooks before the operation is executed.
|
||||
The `self.run_hook(hook_name, *args, **kwargs)` from HookResponseMixin
|
||||
can be utilised to call the hooks.
|
||||
|
||||
If this method returns a response, the operation will be aborted and the
|
||||
hook response will be returned as the view response, skipping the default
|
||||
response.
|
||||
"""
|
||||
return None
|
||||
|
||||
def run_after_hook(self):
|
||||
"""
|
||||
Define how to run the hooks after the operation is executed.
|
||||
The `self.run_hook(hook_name, *args, **kwargs)` from HookResponseMixin
|
||||
can be utilised to call the hooks.
|
||||
|
||||
If this method returns a response, it will be returned as the view
|
||||
response immediately after the operation finishes, skipping the default
|
||||
response.
|
||||
"""
|
||||
return None
|
||||
|
||||
def dispatch(self, *args, **kwargs):
|
||||
hooks_result = self.run_before_hook()
|
||||
if hooks_result is not None:
|
||||
return hooks_result
|
||||
|
||||
return super().dispatch(*args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
response = super().form_valid(form)
|
||||
|
||||
hooks_result = self.run_after_hook()
|
||||
if hooks_result is not None:
|
||||
return hooks_result
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class LocaleMixin:
|
||||
@cached_property
|
||||
def locale(self):
|
||||
return self.get_locale()
|
||||
|
||||
@cached_property
|
||||
def translations(self):
|
||||
return self.get_translations() if self.locale else []
|
||||
|
||||
def get_locale(self):
|
||||
if not getattr(self, "model", None):
|
||||
return None
|
||||
|
||||
i18n_enabled = getattr(settings, "WAGTAIL_I18N_ENABLED", False)
|
||||
if not i18n_enabled or not issubclass(self.model, TranslatableMixin):
|
||||
return None
|
||||
|
||||
if hasattr(self, "object") and self.object:
|
||||
return self.object.locale
|
||||
|
||||
selected_locale = self.request.GET.get("locale")
|
||||
if selected_locale:
|
||||
return get_object_or_404(Locale, language_code=selected_locale)
|
||||
return Locale.get_default()
|
||||
|
||||
def get_translations(self):
|
||||
# Return a list of {"locale": Locale, "url": str} objects for available locales
|
||||
return []
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if not self.locale:
|
||||
return context
|
||||
|
||||
context["locale"] = self.locale
|
||||
context["translations"] = self.translations
|
||||
return context
|
||||
|
||||
def _set_locale_query_param(self, url, locale=None):
|
||||
if not (locale := locale or self.locale):
|
||||
return url
|
||||
return set_query_params(url, {"locale": locale.language_code})
|
||||
|
||||
|
||||
class PanelMixin:
|
||||
panel = None
|
||||
|
||||
def setup(self, request, *args, **kwargs):
|
||||
super().setup(request, *args, **kwargs)
|
||||
self.panel = self.get_panel()
|
||||
|
||||
def get_panel(self):
|
||||
return self.panel
|
||||
|
||||
def get_bound_panel(self, form):
|
||||
if not self.panel:
|
||||
return None
|
||||
return self.panel.get_bound_panel(
|
||||
request=self.request, instance=form.instance, form=form
|
||||
)
|
||||
|
||||
def get_form_class(self):
|
||||
# The form_class takes precedence if specified
|
||||
if self.form_class or not self.panel:
|
||||
return super().get_form_class()
|
||||
return self.panel.get_form_class()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
form = context.get("form")
|
||||
panel = self.get_bound_panel(form)
|
||||
|
||||
media = context.get("media", Media())
|
||||
if form:
|
||||
media += form.media
|
||||
if panel:
|
||||
media += panel.media
|
||||
|
||||
context.update(
|
||||
{
|
||||
"panel": panel,
|
||||
"media": media,
|
||||
}
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class IndexViewOptionalFeaturesMixin:
|
||||
"""
|
||||
A mixin for generic IndexView to support optional features that are applied
|
||||
to the model as mixins (e.g. DraftStateMixin, RevisionMixin).
|
||||
"""
|
||||
|
||||
def _get_title_column(self, field_name, column_class=TitleColumn, **kwargs):
|
||||
accessor = kwargs.pop("accessor", None)
|
||||
|
||||
if not accessor and field_name == "__str__":
|
||||
accessor = get_latest_str
|
||||
|
||||
return super()._get_title_column(
|
||||
field_name, column_class, accessor=accessor, **kwargs
|
||||
)
|
||||
|
||||
def _annotate_queryset_updated_at(self, queryset):
|
||||
if issubclass(queryset.model, RevisionMixin):
|
||||
# Use the latest revision's created_at
|
||||
queryset = queryset.select_related("latest_revision")
|
||||
queryset = queryset.annotate(
|
||||
_updated_at=models.F("latest_revision__created_at")
|
||||
)
|
||||
return queryset
|
||||
return super()._annotate_queryset_updated_at(queryset)
|
||||
|
||||
|
||||
class CreateEditViewOptionalFeaturesMixin:
|
||||
"""
|
||||
A mixin for generic CreateView/EditView to support optional features that
|
||||
are applied to the model as mixins (e.g. DraftStateMixin, RevisionMixin).
|
||||
"""
|
||||
|
||||
view_name = "create"
|
||||
preview_url_name = None
|
||||
lock_url_name = None
|
||||
unlock_url_name = None
|
||||
revisions_unschedule_url_name = None
|
||||
revisions_compare_url_name = None
|
||||
workflow_history_url_name = None
|
||||
confirm_workflow_cancellation_url_name = None
|
||||
|
||||
def setup(self, request, *args, **kwargs):
|
||||
# Need to set these here as they are used in get_object()
|
||||
self.request = request
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
self.preview_enabled = self.model and issubclass(self.model, PreviewableMixin)
|
||||
self.revision_enabled = self.model and issubclass(self.model, RevisionMixin)
|
||||
self.draftstate_enabled = self.model and issubclass(self.model, DraftStateMixin)
|
||||
self.locking_enabled = (
|
||||
self.model
|
||||
and issubclass(self.model, LockableMixin)
|
||||
and self.view_name != "create"
|
||||
)
|
||||
|
||||
# Set the object before super().setup() as LocaleMixin.setup() needs it
|
||||
self.object = self.get_object()
|
||||
self.lock = self.get_lock()
|
||||
self.locked_for_user = self.lock and self.lock.for_user(request.user)
|
||||
super().setup(request, *args, **kwargs)
|
||||
|
||||
@cached_property
|
||||
def workflow(self):
|
||||
if not self.model or not issubclass(self.model, WorkflowMixin):
|
||||
return None
|
||||
if self.object:
|
||||
return self.object.get_workflow()
|
||||
return self.model.get_default_workflow()
|
||||
|
||||
@cached_property
|
||||
def workflow_enabled(self):
|
||||
return self.workflow is not None
|
||||
|
||||
@cached_property
|
||||
def workflow_state(self):
|
||||
if not self.workflow_enabled or not self.object:
|
||||
return None
|
||||
return (
|
||||
self.object.current_workflow_state
|
||||
or self.object.workflow_states.order_by("created_at").last()
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def current_workflow_task(self):
|
||||
if not self.workflow_enabled or not self.object:
|
||||
return None
|
||||
return self.object.current_workflow_task
|
||||
|
||||
@cached_property
|
||||
def workflow_tasks(self):
|
||||
if not self.workflow_state:
|
||||
return []
|
||||
return self.workflow_state.all_tasks_with_status()
|
||||
|
||||
def user_has_permission(self, permission):
|
||||
user = self.request.user
|
||||
# Workflow lock/unlock methods take precedence before the base
|
||||
# "lock" and "unlock" permissions -- see PagePermissionTester for reference
|
||||
if permission == "lock" and self.current_workflow_task:
|
||||
# Follow the logic in PagePermissionTester.user_can_lock()
|
||||
# (superusers can always lock)
|
||||
if user.is_superuser:
|
||||
return True
|
||||
return self.current_workflow_task.user_can_lock(self.object, user)
|
||||
if permission == "unlock":
|
||||
# Follow the logic in PagePermissionTester.user_can_unlock()
|
||||
# (superusers can always unlock)
|
||||
if user.is_superuser:
|
||||
return True
|
||||
# Allow unlocking even if the user does not have the 'unlock' permission
|
||||
# if they are the user who locked the object
|
||||
if self.object.locked_by_id == user.pk:
|
||||
return True
|
||||
if self.current_workflow_task:
|
||||
return self.current_workflow_task.user_can_unlock(self.object, user)
|
||||
|
||||
# Check with base PermissionCheckedMixin logic
|
||||
has_base_permission = super().user_has_permission(permission)
|
||||
if has_base_permission:
|
||||
return True
|
||||
|
||||
# Allow access to the editor if the current workflow task allows it,
|
||||
# even if the user does not normally have edit access. Users with edit
|
||||
# permissions can always edit regardless what this method returns --
|
||||
# see Task.user_can_access_editor() for reference
|
||||
if (
|
||||
permission == "change"
|
||||
and self.current_workflow_task
|
||||
and self.current_workflow_task.user_can_access_editor(
|
||||
self.object, self.request.user
|
||||
)
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def workflow_action_is_valid(self):
|
||||
if not self.current_workflow_task:
|
||||
return False
|
||||
self.workflow_action = self.request.POST.get("workflow-action-name")
|
||||
available_actions = self.current_workflow_task.get_actions(
|
||||
self.object, self.request.user
|
||||
)
|
||||
available_action_names = [
|
||||
name for name, verbose_name, modal in available_actions
|
||||
]
|
||||
return self.workflow_action in available_action_names
|
||||
|
||||
def get_available_actions(self):
|
||||
actions = [*super().get_available_actions()]
|
||||
|
||||
if self.request.method != "POST":
|
||||
return actions
|
||||
|
||||
if self.draftstate_enabled and (
|
||||
not self.permission_policy
|
||||
or self.permission_policy.user_has_permission(self.request.user, "publish")
|
||||
):
|
||||
actions.append("publish")
|
||||
|
||||
if self.workflow_enabled:
|
||||
actions.append("submit")
|
||||
|
||||
if self.workflow_state and (
|
||||
self.workflow_state.user_can_cancel(self.request.user)
|
||||
):
|
||||
actions.append("cancel-workflow")
|
||||
if self.object and not self.object.workflow_in_progress:
|
||||
actions.append("restart-workflow")
|
||||
|
||||
if self.workflow_action_is_valid():
|
||||
actions.append("workflow-action")
|
||||
|
||||
return actions
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
if self.view_name == "create":
|
||||
return None
|
||||
self.live_object = super().get_object(queryset)
|
||||
if self.draftstate_enabled:
|
||||
return self.live_object.get_latest_revision_as_object()
|
||||
return self.live_object
|
||||
|
||||
def get_lock(self):
|
||||
if not self.locking_enabled:
|
||||
return None
|
||||
return self.object.get_lock()
|
||||
|
||||
def get_lock_url(self):
|
||||
if not self.locking_enabled or not self.lock_url_name:
|
||||
return None
|
||||
return reverse(self.lock_url_name, args=[quote(self.object.pk)])
|
||||
|
||||
def get_unlock_url(self):
|
||||
if not self.locking_enabled or not self.unlock_url_name:
|
||||
return None
|
||||
return reverse(self.unlock_url_name, args=[quote(self.object.pk)])
|
||||
|
||||
def get_preview_url(self):
|
||||
if not self.preview_enabled or not self.preview_url_name:
|
||||
return None
|
||||
args = [] if self.view_name == "create" else [quote(self.object.pk)]
|
||||
return reverse(self.preview_url_name, args=args)
|
||||
|
||||
def get_workflow_history_url(self):
|
||||
if not self.workflow_enabled or not self.workflow_history_url_name:
|
||||
return None
|
||||
return reverse(self.workflow_history_url_name, args=[quote(self.object.pk)])
|
||||
|
||||
def get_confirm_workflow_cancellation_url(self):
|
||||
if not self.workflow_enabled or not self.confirm_workflow_cancellation_url_name:
|
||||
return None
|
||||
return reverse(
|
||||
self.confirm_workflow_cancellation_url_name, args=[quote(self.object.pk)]
|
||||
)
|
||||
|
||||
def get_error_message(self):
|
||||
if self.action == "cancel-workflow":
|
||||
return None
|
||||
if self.locked_for_user:
|
||||
return capfirst(
|
||||
_("The %(model_name)s could not be saved as it is locked")
|
||||
% {"model_name": self.model._meta.verbose_name}
|
||||
)
|
||||
return super().get_error_message()
|
||||
|
||||
def get_success_message(self, instance=None):
|
||||
object = instance or self.object
|
||||
|
||||
message = _("%(model_name)s '%(object)s' updated.")
|
||||
if self.view_name == "create":
|
||||
message = _("%(model_name)s '%(object)s' created.")
|
||||
|
||||
if self.action == "publish":
|
||||
# Scheduled publishing
|
||||
if object.go_live_at and object.go_live_at > timezone.now():
|
||||
message = _(
|
||||
"%(model_name)s '%(object)s' has been scheduled for publishing."
|
||||
)
|
||||
|
||||
if self.view_name == "create":
|
||||
message = _(
|
||||
"%(model_name)s '%(object)s' created and scheduled for publishing."
|
||||
)
|
||||
elif object.live:
|
||||
message = _(
|
||||
"%(model_name)s '%(object)s' is live and this version has been scheduled for publishing."
|
||||
)
|
||||
|
||||
# Immediate publishing
|
||||
else:
|
||||
message = _("%(model_name)s '%(object)s' updated and published.")
|
||||
if self.view_name == "create":
|
||||
message = _("%(model_name)s '%(object)s' created and published.")
|
||||
|
||||
if self.action == "submit":
|
||||
message = _(
|
||||
"%(model_name)s '%(object)s' has been submitted for moderation."
|
||||
)
|
||||
|
||||
if self.view_name == "create":
|
||||
message = _(
|
||||
"%(model_name)s '%(object)s' created and submitted for moderation."
|
||||
)
|
||||
|
||||
if self.action == "restart-workflow":
|
||||
message = _("Workflow on %(model_name)s '%(object)s' has been restarted.")
|
||||
|
||||
if self.action == "cancel-workflow":
|
||||
message = _("Workflow on %(model_name)s '%(object)s' has been cancelled.")
|
||||
|
||||
return message % {
|
||||
"model_name": capfirst(self.model._meta.verbose_name),
|
||||
"object": get_latest_str(object),
|
||||
}
|
||||
|
||||
def get_success_url(self):
|
||||
# If DraftStateMixin is enabled and the action is saving a draft
|
||||
# or cancelling a workflow, remain on the edit view
|
||||
remain_actions = {"create", "edit", "cancel-workflow"}
|
||||
if self.draftstate_enabled and self.action in remain_actions:
|
||||
return self.get_edit_url()
|
||||
return super().get_success_url()
|
||||
|
||||
def save_instance(self):
|
||||
"""
|
||||
Called after the form is successfully validated - saves the object to the db
|
||||
and returns the new object. Override this to implement custom save logic.
|
||||
"""
|
||||
if self.draftstate_enabled:
|
||||
instance = self.form.save(
|
||||
commit=self.view_name == "edit" and not self.object.live
|
||||
)
|
||||
|
||||
# If DraftStateMixin is applied, only save to the database in CreateView,
|
||||
# and make sure the live field is set to False.
|
||||
if self.view_name == "create":
|
||||
instance.live = False
|
||||
instance.save()
|
||||
self.form.save_m2m()
|
||||
else:
|
||||
instance = self.form.save()
|
||||
|
||||
self.has_content_changes = self.view_name == "create" or self.form.has_changed()
|
||||
|
||||
# Save revision if the model inherits from RevisionMixin
|
||||
self.new_revision = None
|
||||
if self.revision_enabled:
|
||||
self.new_revision = instance.save_revision(user=self.request.user)
|
||||
|
||||
log(
|
||||
instance=instance,
|
||||
action="wagtail.create" if self.view_name == "create" else "wagtail.edit",
|
||||
revision=self.new_revision,
|
||||
content_changed=self.has_content_changes,
|
||||
)
|
||||
|
||||
return instance
|
||||
|
||||
def publish_action(self):
|
||||
hook_response = self.run_hook("before_publish", self.request, self.object)
|
||||
if hook_response is not None:
|
||||
return hook_response
|
||||
|
||||
# Skip permission check as it's already done in get_available_actions
|
||||
self.new_revision.publish(user=self.request.user, skip_permission_checks=True)
|
||||
|
||||
hook_response = self.run_hook("after_publish", self.request, self.object)
|
||||
if hook_response is not None:
|
||||
return hook_response
|
||||
|
||||
return None
|
||||
|
||||
def submit_action(self):
|
||||
if (
|
||||
self.workflow_state
|
||||
and self.workflow_state.status == WorkflowState.STATUS_NEEDS_CHANGES
|
||||
):
|
||||
# If the workflow was in the needs changes state, resume the existing workflow on submission
|
||||
self.workflow_state.resume(self.request.user)
|
||||
else:
|
||||
# Otherwise start a new workflow
|
||||
self.workflow.start(self.object, self.request.user)
|
||||
|
||||
return None
|
||||
|
||||
def restart_workflow_action(self):
|
||||
self.workflow_state.cancel(user=self.request.user)
|
||||
self.workflow.start(self.object, self.request.user)
|
||||
return None
|
||||
|
||||
def cancel_workflow_action(self):
|
||||
self.workflow_state.cancel(user=self.request.user)
|
||||
return None
|
||||
|
||||
def workflow_action_action(self):
|
||||
extra_workflow_data_json = self.request.POST.get(
|
||||
"workflow-action-extra-data", "{}"
|
||||
)
|
||||
extra_workflow_data = json.loads(extra_workflow_data_json)
|
||||
self.object.current_workflow_task.on_action(
|
||||
self.object.current_workflow_task_state,
|
||||
self.request.user,
|
||||
self.workflow_action,
|
||||
**extra_workflow_data,
|
||||
)
|
||||
return None
|
||||
|
||||
def run_action_method(self):
|
||||
action_method = getattr(self, self.action.replace("-", "_") + "_action", None)
|
||||
if action_method:
|
||||
return action_method()
|
||||
return None
|
||||
|
||||
def form_valid(self, form):
|
||||
self.form = form
|
||||
with transaction.atomic():
|
||||
self.object = self.save_instance()
|
||||
|
||||
response = self.run_action_method()
|
||||
if response is not None:
|
||||
return response
|
||||
|
||||
response = self.save_action()
|
||||
|
||||
hook_response = self.run_after_hook()
|
||||
if hook_response is not None:
|
||||
return hook_response
|
||||
|
||||
return response
|
||||
|
||||
def form_invalid(self, form):
|
||||
# Even if the object is locked due to not having permissions,
|
||||
# the original submitter can still cancel the workflow
|
||||
if self.action == "cancel-workflow":
|
||||
self.cancel_workflow_action()
|
||||
messages.success(
|
||||
self.request,
|
||||
self.get_success_message(),
|
||||
buttons=self.get_success_buttons(),
|
||||
)
|
||||
# Refresh the lock object as now WorkflowLock no longer applies
|
||||
self.lock = self.get_lock()
|
||||
self.locked_for_user = self.lock and self.lock.for_user(self.request.user)
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_last_updated_info(self):
|
||||
# Create view doesn't have last updated info
|
||||
if self.view_name == "create":
|
||||
return None
|
||||
|
||||
# DraftStateMixin is applied but object is not live
|
||||
if self.draftstate_enabled and not self.object.live:
|
||||
return None
|
||||
|
||||
revision = None
|
||||
# DraftStateMixin is applied and object is live
|
||||
if self.draftstate_enabled and self.object.live_revision:
|
||||
revision = self.object.live_revision
|
||||
# RevisionMixin is applied, so object is assumed to be live
|
||||
elif self.revision_enabled and self.object.latest_revision:
|
||||
revision = self.object.latest_revision
|
||||
|
||||
# No mixin is applied or no revision exists, fall back to latest log entry
|
||||
if not revision:
|
||||
return log_registry.get_logs_for_instance(self.object).first()
|
||||
|
||||
return {
|
||||
"timestamp": revision.created_at,
|
||||
"user_display_name": user_display_name(revision.user),
|
||||
}
|
||||
|
||||
def get_lock_context(self):
|
||||
if not self.locking_enabled:
|
||||
return {}
|
||||
|
||||
user_can_lock = (
|
||||
not self.lock or isinstance(self.lock, WorkflowLock)
|
||||
) and self.user_has_permission("lock")
|
||||
user_can_unlock = (
|
||||
isinstance(self.lock, BasicLock)
|
||||
) and self.user_has_permission("unlock")
|
||||
user_can_unschedule = (
|
||||
isinstance(self.lock, ScheduledForPublishLock)
|
||||
) and self.user_has_permission("publish")
|
||||
|
||||
context = {
|
||||
"lock": self.lock,
|
||||
"locked_for_user": self.locked_for_user,
|
||||
"lock_url": self.get_lock_url(),
|
||||
"unlock_url": self.get_unlock_url(),
|
||||
"user_can_lock": user_can_lock,
|
||||
"user_can_unlock": user_can_unlock,
|
||||
}
|
||||
|
||||
# Do not add lock message if the request method is not GET,
|
||||
# as POST request may add success/validation error messages already
|
||||
if not self.lock or self.request.method != "GET":
|
||||
return context
|
||||
|
||||
lock_message = self.lock.get_message(self.request.user)
|
||||
if lock_message:
|
||||
if user_can_unlock:
|
||||
lock_message = format_html(
|
||||
'{} <span class="buttons"><button type="button" class="button button-small button-secondary" data-action="w-action#post" data-controller="w-action" data-w-action-url-value="{}">{}</button></span>',
|
||||
lock_message,
|
||||
self.get_unlock_url(),
|
||||
_("Unlock"),
|
||||
)
|
||||
|
||||
if user_can_unschedule:
|
||||
lock_message = format_html(
|
||||
'{} <span class="buttons"><button type="button" class="button button-small button-secondary" data-action="w-action#post" data-controller="w-action" data-w-action-url-value="{}">{}</button></span>',
|
||||
lock_message,
|
||||
reverse(
|
||||
self.revisions_unschedule_url_name,
|
||||
args=[quote(self.object.pk), self.object.scheduled_revision.id],
|
||||
),
|
||||
_("Cancel scheduled publish"),
|
||||
)
|
||||
|
||||
if (
|
||||
not isinstance(self.lock, ScheduledForPublishLock)
|
||||
and self.locked_for_user
|
||||
):
|
||||
messages.warning(self.request, lock_message, extra_tags="lock")
|
||||
else:
|
||||
messages.info(self.request, lock_message, extra_tags="lock")
|
||||
|
||||
return context
|
||||
|
||||
def get_editing_sessions(self):
|
||||
if self.view_name == "create":
|
||||
return None
|
||||
EditingSession.cleanup()
|
||||
content_type = ContentType.objects.get_for_model(self.model)
|
||||
session = EditingSession.objects.create(
|
||||
user=self.request.user,
|
||||
content_type=content_type,
|
||||
object_id=self.object.pk,
|
||||
last_seen_at=timezone.now(),
|
||||
)
|
||||
revision_id = self.object.latest_revision_id if self.revision_enabled else None
|
||||
return EditingSessionsModule(
|
||||
session,
|
||||
reverse(
|
||||
"wagtailadmin_editing_sessions:ping",
|
||||
args=(
|
||||
self.model._meta.app_label,
|
||||
self.model._meta.model_name,
|
||||
quote(self.object.pk),
|
||||
session.id,
|
||||
),
|
||||
),
|
||||
reverse(
|
||||
"wagtailadmin_editing_sessions:release",
|
||||
args=(session.id,),
|
||||
),
|
||||
[],
|
||||
revision_id,
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update(self.get_lock_context())
|
||||
context["revision_enabled"] = self.revision_enabled
|
||||
context["draftstate_enabled"] = self.draftstate_enabled
|
||||
context["workflow_enabled"] = self.workflow_enabled
|
||||
context["workflow_history_url"] = self.get_workflow_history_url()
|
||||
context[
|
||||
"confirm_workflow_cancellation_url"
|
||||
] = self.get_confirm_workflow_cancellation_url()
|
||||
context["publishing_will_cancel_workflow"] = getattr(
|
||||
settings, "WAGTAIL_WORKFLOW_CANCEL_ON_PUBLISH", True
|
||||
) and bool(self.workflow_tasks)
|
||||
context["revisions_compare_url_name"] = self.revisions_compare_url_name
|
||||
context["editing_sessions"] = self.get_editing_sessions()
|
||||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
# Make sure object is not locked
|
||||
if not self.locked_for_user and form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class RevisionsRevertMixin:
|
||||
revision_id_kwarg = "revision_id"
|
||||
revisions_revert_url_name = None
|
||||
|
||||
def setup(self, request, *args, **kwargs):
|
||||
self.revision_id = kwargs.get(self.revision_id_kwarg)
|
||||
super().setup(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self._add_warning_message()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_revisions_revert_url(self):
|
||||
return reverse(
|
||||
self.revisions_revert_url_name,
|
||||
args=[quote(self.object.pk), self.revision_id],
|
||||
)
|
||||
|
||||
def get_warning_message(self):
|
||||
user_avatar = render_to_string(
|
||||
"wagtailadmin/shared/user_avatar.html", {"user": self.revision.user}
|
||||
)
|
||||
message_string = _(
|
||||
"You are viewing a previous version of this %(model_name)s from <b>%(created_at)s</b> by %(user)s"
|
||||
)
|
||||
message_data = {
|
||||
"model_name": capfirst(self.model._meta.verbose_name),
|
||||
"created_at": render_timestamp(self.revision.created_at),
|
||||
"user": user_avatar,
|
||||
}
|
||||
message = mark_safe(message_string % message_data)
|
||||
return message
|
||||
|
||||
def _add_warning_message(self):
|
||||
messages.warning(self.request, self.get_warning_message())
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
object = super().get_object(queryset)
|
||||
self.revision = get_object_or_404(object.revisions, id=self.revision_id)
|
||||
return self.revision.as_object()
|
||||
|
||||
def save_instance(self):
|
||||
commit = not issubclass(self.model, DraftStateMixin) or not self.object.live
|
||||
instance = self.form.save(commit=commit)
|
||||
|
||||
self.has_content_changes = self.form.has_changed()
|
||||
|
||||
self.new_revision = instance.save_revision(
|
||||
user=self.request.user,
|
||||
log_action=True,
|
||||
previous_revision=self.revision,
|
||||
)
|
||||
|
||||
return instance
|
||||
|
||||
def get_success_message(self):
|
||||
message = _(
|
||||
"%(model_name)s '%(object)s' has been replaced with version from %(timestamp)s."
|
||||
)
|
||||
if self.draftstate_enabled and self.action == "publish":
|
||||
message = _(
|
||||
"Version from %(timestamp)s of %(model_name)s '%(object)s' has been published."
|
||||
)
|
||||
|
||||
if self.object.go_live_at and self.object.go_live_at > timezone.now():
|
||||
message = _(
|
||||
"Version from %(timestamp)s of %(model_name)s '%(object)s' has been scheduled for publishing."
|
||||
)
|
||||
|
||||
return message % {
|
||||
"model_name": capfirst(self.model._meta.verbose_name),
|
||||
"object": self.object,
|
||||
"timestamp": render_timestamp(self.revision.created_at),
|
||||
}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["revision"] = self.revision
|
||||
context["action_url"] = self.get_revisions_revert_url()
|
||||
return context
|
||||
1458
env/lib/python3.10/site-packages/wagtail/admin/views/generic/models.py
vendored
Normal file
1458
env/lib/python3.10/site-packages/wagtail/admin/views/generic/models.py
vendored
Normal file
File diff suppressed because it is too large
Load Diff
400
env/lib/python3.10/site-packages/wagtail/admin/views/generic/multiple_upload.py
vendored
Normal file
400
env/lib/python3.10/site-packages/wagtail/admin/views/generic/multiple_upload.py
vendored
Normal file
@@ -0,0 +1,400 @@
|
||||
import os.path
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponseBadRequest, JsonResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.vary import vary_on_headers
|
||||
from django.views.generic.base import TemplateView, View
|
||||
|
||||
from wagtail.admin.views.generic import PermissionCheckedMixin
|
||||
from wagtail.models import UploadedFile
|
||||
|
||||
|
||||
class AddView(PermissionCheckedMixin, TemplateView):
|
||||
# subclasses need to provide:
|
||||
# - permission_policy
|
||||
# - template_name
|
||||
|
||||
# - edit_object_url_name
|
||||
# - delete_object_url_name
|
||||
# - edit_object_form_prefix
|
||||
# - context_object_name
|
||||
# - context_object_id_name
|
||||
|
||||
# - edit_upload_url_name
|
||||
# - delete_upload_url_name
|
||||
# - edit_upload_form_prefix
|
||||
# - context_upload_name
|
||||
# - context_upload_id_name
|
||||
|
||||
# - get_model()
|
||||
# - get_upload_form_class()
|
||||
# - get_edit_form_class()
|
||||
|
||||
permission_required = "add"
|
||||
edit_form_template_name = "wagtailadmin/generic/multiple_upload/edit_form.html"
|
||||
|
||||
@method_decorator(vary_on_headers("X-Requested-With"))
|
||||
def dispatch(self, request):
|
||||
self.model = self.get_model()
|
||||
|
||||
return super().dispatch(request)
|
||||
|
||||
def save_object(self, form):
|
||||
return form.save()
|
||||
|
||||
def get_edit_object_form_context_data(self):
|
||||
"""
|
||||
Return the context data necessary for rendering the HTML form for editing
|
||||
an object that has been successfully uploaded
|
||||
"""
|
||||
edit_form_class = self.get_edit_form_class()
|
||||
return {
|
||||
self.context_object_name: self.object,
|
||||
"edit_action": reverse(self.edit_object_url_name, args=(self.object.pk,)),
|
||||
"delete_action": reverse(
|
||||
self.delete_object_url_name, args=(self.object.pk,)
|
||||
),
|
||||
"form": edit_form_class(
|
||||
instance=self.object,
|
||||
prefix="%s-%d" % (self.edit_object_form_prefix, self.object.pk),
|
||||
user=self.request.user,
|
||||
),
|
||||
}
|
||||
|
||||
def get_edit_object_response_data(self):
|
||||
"""
|
||||
Return the JSON response data for an object that has been successfully uploaded
|
||||
"""
|
||||
return {
|
||||
"success": True,
|
||||
self.context_object_id_name: self.object.pk,
|
||||
"form": render_to_string(
|
||||
self.edit_form_template_name,
|
||||
self.get_edit_object_form_context_data(),
|
||||
request=self.request,
|
||||
),
|
||||
}
|
||||
|
||||
def get_edit_upload_form_context_data(self):
|
||||
"""
|
||||
Return the context data necessary for rendering the HTML form for supplying the
|
||||
metadata to turn an upload object into a final object
|
||||
"""
|
||||
edit_form_class = self.get_edit_form_class()
|
||||
return {
|
||||
self.context_upload_name: self.upload_object,
|
||||
"edit_action": reverse(
|
||||
self.edit_upload_url_name, args=(self.upload_object.id,)
|
||||
),
|
||||
"delete_action": reverse(
|
||||
self.delete_upload_url_name, args=(self.upload_object.id,)
|
||||
),
|
||||
"form": edit_form_class(
|
||||
instance=self.object,
|
||||
prefix="%s-%d" % (self.edit_upload_form_prefix, self.upload_object.id),
|
||||
user=self.request.user,
|
||||
),
|
||||
}
|
||||
|
||||
def get_edit_upload_response_data(self):
|
||||
"""
|
||||
Return the JSON response data for an object that has been uploaded to an
|
||||
upload object and now needs extra metadata to become a final object
|
||||
"""
|
||||
return {
|
||||
"success": True,
|
||||
self.context_upload_id_name: self.upload_object.id,
|
||||
"form": render_to_string(
|
||||
self.edit_form_template_name,
|
||||
self.get_edit_upload_form_context_data(),
|
||||
request=self.request,
|
||||
),
|
||||
}
|
||||
|
||||
def get_invalid_response_data(self, form):
|
||||
"""
|
||||
Return the JSON response data for an invalid form submission
|
||||
"""
|
||||
return {
|
||||
"success": False,
|
||||
"error_message": "\n".join(form.errors["file"]),
|
||||
}
|
||||
|
||||
def post(self, request):
|
||||
if not request.FILES:
|
||||
return HttpResponseBadRequest("Must upload a file")
|
||||
|
||||
# Build a form for validation
|
||||
upload_form_class = self.get_upload_form_class()
|
||||
form = upload_form_class(
|
||||
{
|
||||
"title": request.POST.get("title", request.FILES["files[]"].name),
|
||||
"collection": request.POST.get("collection"),
|
||||
},
|
||||
{
|
||||
"file": request.FILES["files[]"],
|
||||
},
|
||||
user=request.user,
|
||||
)
|
||||
|
||||
if form.is_valid():
|
||||
# Save it
|
||||
self.object = self.save_object(form)
|
||||
|
||||
# Success! Send back an edit form for this object to the user
|
||||
return JsonResponse(self.get_edit_object_response_data())
|
||||
elif "file" in form.errors:
|
||||
# The uploaded file is invalid; reject it now
|
||||
return JsonResponse(self.get_invalid_response_data(form))
|
||||
else:
|
||||
# Some other field of the form has failed validation, e.g. a required metadata field
|
||||
# on a custom image model. Store the object as an UploadedFile instance instead and
|
||||
# present the edit form so that it will become a proper object when successfully filled in
|
||||
self.upload_object = UploadedFile.objects.create(
|
||||
for_content_type=ContentType.objects.get_for_model(self.get_model()),
|
||||
file=self.request.FILES["files[]"],
|
||||
uploaded_by_user=self.request.user,
|
||||
)
|
||||
self.object = self.model(
|
||||
title=self.request.FILES["files[]"].name,
|
||||
collection_id=self.request.POST.get("collection"),
|
||||
)
|
||||
|
||||
return JsonResponse(self.get_edit_upload_response_data())
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# Instantiate a dummy copy of the form that we can retrieve validation messages and media from;
|
||||
# actual rendering of forms will happen on AJAX POST rather than here
|
||||
upload_form_class = self.get_upload_form_class()
|
||||
self.form = upload_form_class(user=self.request.user)
|
||||
selected_collection_id = self.request.GET.get("collection_id")
|
||||
|
||||
collections = self.permission_policy.collections_user_has_permission_for(
|
||||
self.request.user, "add"
|
||||
)
|
||||
if len(collections) < 2:
|
||||
# no need to show a collections chooser
|
||||
collections = None
|
||||
|
||||
context.update(
|
||||
{
|
||||
"help_text": self.form.fields["file"].help_text,
|
||||
"collections": collections,
|
||||
"form_media": self.form.media,
|
||||
"selected_collection_id": selected_collection_id,
|
||||
}
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class EditView(View):
|
||||
# subclasses need to provide:
|
||||
# - permission_policy
|
||||
# - pk_url_kwarg
|
||||
# - edit_object_form_prefix
|
||||
# - context_object_name
|
||||
# - context_object_id_name
|
||||
# - edit_object_url_name
|
||||
# - delete_object_url_name
|
||||
# - get_model()
|
||||
# - get_edit_form_class()
|
||||
|
||||
http_method_names = ["post"]
|
||||
edit_form_template_name = "wagtailadmin/generic/multiple_upload/edit_form.html"
|
||||
|
||||
def save_object(self, form):
|
||||
form.save()
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
object_id = kwargs[self.pk_url_kwarg]
|
||||
self.model = self.get_model()
|
||||
self.form_class = self.get_edit_form_class()
|
||||
|
||||
self.object = get_object_or_404(self.model, pk=object_id)
|
||||
|
||||
if not self.permission_policy.user_has_permission_for_instance(
|
||||
request.user, "change", self.object
|
||||
):
|
||||
raise PermissionDenied
|
||||
|
||||
form = self.form_class(
|
||||
request.POST,
|
||||
request.FILES,
|
||||
instance=self.object,
|
||||
prefix="%s-%d" % (self.edit_object_form_prefix, object_id),
|
||||
user=request.user,
|
||||
)
|
||||
|
||||
if form.is_valid():
|
||||
self.save_object(form)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"success": True,
|
||||
self.context_object_id_name: self.object.pk,
|
||||
}
|
||||
)
|
||||
else:
|
||||
return JsonResponse(
|
||||
{
|
||||
"success": False,
|
||||
self.context_object_id_name: self.object.pk,
|
||||
"form": render_to_string(
|
||||
self.edit_form_template_name,
|
||||
{
|
||||
self.context_object_name: self.object, # only used for tests
|
||||
"edit_action": reverse(
|
||||
self.edit_object_url_name, args=(object_id,)
|
||||
),
|
||||
"delete_action": reverse(
|
||||
self.delete_object_url_name, args=(object_id,)
|
||||
),
|
||||
"form": form,
|
||||
},
|
||||
request=request,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class DeleteView(View):
|
||||
# subclasses need to provide:
|
||||
# - permission_policy
|
||||
# - pk_url_kwarg
|
||||
# - context_object_id_name
|
||||
|
||||
http_method_names = ["post"]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
object_id = kwargs[self.pk_url_kwarg]
|
||||
self.model = self.get_model()
|
||||
self.object = get_object_or_404(self.model, pk=object_id)
|
||||
object_id = (
|
||||
self.object.pk
|
||||
) # retrieve object id cast to the appropriate type (usually int)
|
||||
|
||||
if not self.permission_policy.user_has_permission_for_instance(
|
||||
request.user, "delete", self.object
|
||||
):
|
||||
raise PermissionDenied
|
||||
|
||||
self.object.delete()
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"success": True,
|
||||
self.context_object_id_name: object_id,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class CreateFromUploadView(View):
|
||||
# subclasses need to provide:
|
||||
# - edit_upload_url_name
|
||||
# - delete_upload_url_name
|
||||
# - upload_pk_url_kwarg
|
||||
# - edit_upload_form_prefix
|
||||
# - context_object_id_name
|
||||
# - context_upload_name
|
||||
# - get_model()
|
||||
# - get_edit_form_class()
|
||||
|
||||
http_method_names = ["post"]
|
||||
edit_form_template_name = "wagtailadmin/generic/multiple_upload/edit_form.html"
|
||||
|
||||
def save_object(self, form):
|
||||
self.object.file.save(
|
||||
os.path.basename(self.upload.file.name), self.upload.file.file, save=False
|
||||
)
|
||||
self.object.uploaded_by_user = self.request.user
|
||||
form.save()
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
upload_id = kwargs[self.upload_pk_url_kwarg]
|
||||
self.model = self.get_model()
|
||||
self.form_class = self.get_edit_form_class()
|
||||
|
||||
self.upload = get_object_or_404(
|
||||
UploadedFile,
|
||||
id=upload_id,
|
||||
for_content_type=ContentType.objects.get_for_model(self.model),
|
||||
)
|
||||
|
||||
if self.upload.uploaded_by_user != request.user:
|
||||
raise PermissionDenied
|
||||
|
||||
self.object = self.model()
|
||||
form = self.form_class(
|
||||
request.POST,
|
||||
request.FILES,
|
||||
instance=self.object,
|
||||
prefix="%s-%d" % (self.edit_upload_form_prefix, upload_id),
|
||||
user=request.user,
|
||||
)
|
||||
|
||||
if form.is_valid():
|
||||
self.save_object(form)
|
||||
self.upload.file.delete()
|
||||
self.upload.delete()
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"success": True,
|
||||
self.context_object_id_name: self.object.id,
|
||||
}
|
||||
)
|
||||
else:
|
||||
return JsonResponse(
|
||||
{
|
||||
"success": False,
|
||||
"form": render_to_string(
|
||||
self.edit_form_template_name,
|
||||
{
|
||||
self.context_upload_name: self.upload,
|
||||
"edit_action": reverse(
|
||||
self.edit_upload_url_name, args=(self.upload.id,)
|
||||
),
|
||||
"delete_action": reverse(
|
||||
self.delete_upload_url_name, args=(self.upload.id,)
|
||||
),
|
||||
"form": form,
|
||||
},
|
||||
request=request,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class DeleteUploadView(View):
|
||||
# subclasses need to provide:
|
||||
# - upload_pk_url_kwarg
|
||||
|
||||
http_method_names = ["post"]
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
upload_id = kwargs[self.upload_pk_url_kwarg]
|
||||
upload = get_object_or_404(
|
||||
UploadedFile,
|
||||
id=upload_id,
|
||||
for_content_type=ContentType.objects.get_for_model(self.get_model()),
|
||||
)
|
||||
|
||||
if upload.uploaded_by_user != request.user:
|
||||
raise PermissionDenied
|
||||
|
||||
upload.file.delete()
|
||||
upload.delete()
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"success": True,
|
||||
}
|
||||
)
|
||||
49
env/lib/python3.10/site-packages/wagtail/admin/views/generic/permissions.py
vendored
Normal file
49
env/lib/python3.10/site-packages/wagtail/admin/views/generic/permissions.py
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
|
||||
class PermissionCheckedMixin:
|
||||
"""
|
||||
Mixin for class-based views to enforce permission checks according to
|
||||
a permission policy (see wagtail.permission_policies).
|
||||
|
||||
To take advantage of this, subclasses should set the class property:
|
||||
* permission_policy (a policy object)
|
||||
and either of:
|
||||
* permission_required (an action name such as 'add', 'change' or 'delete')
|
||||
* any_permission_required (a list of action names - the user must have
|
||||
one or more of those permissions)
|
||||
"""
|
||||
|
||||
permission_policy = None
|
||||
permission_required = None
|
||||
any_permission_required = None
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if self.permission_required is not None:
|
||||
if not self.user_has_permission(self.permission_required):
|
||||
raise PermissionDenied
|
||||
|
||||
if self.any_permission_required is not None:
|
||||
if not self.user_has_any_permission(self.any_permission_required):
|
||||
raise PermissionDenied
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def user_has_permission(self, permission):
|
||||
return not self.permission_policy or (
|
||||
self.permission_policy.user_has_permission(self.request.user, permission)
|
||||
)
|
||||
|
||||
def user_has_permission_for_instance(self, permission, instance):
|
||||
return not self.permission_policy or (
|
||||
self.permission_policy.user_has_permission_for_instance(
|
||||
self.request.user, permission, instance
|
||||
)
|
||||
)
|
||||
|
||||
def user_has_any_permission(self, permissions):
|
||||
return not self.permission_policy or (
|
||||
self.permission_policy.user_has_any_permission(
|
||||
self.request.user, permissions
|
||||
)
|
||||
)
|
||||
165
env/lib/python3.10/site-packages/wagtail/admin/views/generic/preview.py
vendored
Normal file
165
env/lib/python3.10/site-packages/wagtail/admin/views/generic/preview.py
vendored
Normal file
@@ -0,0 +1,165 @@
|
||||
from time import time
|
||||
|
||||
from django.contrib.admin.utils import unquote
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404, JsonResponse
|
||||
from django.http.request import QueryDict
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.generic import View
|
||||
|
||||
from wagtail.admin.panels import get_edit_handler
|
||||
from wagtail.models import PreviewableMixin, RevisionMixin
|
||||
from wagtail.utils.decorators import xframe_options_sameorigin_override
|
||||
|
||||
|
||||
class PreviewOnEdit(View):
|
||||
model = None
|
||||
form_class = None
|
||||
http_method_names = ("post", "get", "delete")
|
||||
preview_expiration_timeout = 60 * 60 * 24 # seconds
|
||||
session_key_prefix = "wagtail-preview-"
|
||||
|
||||
def setup(self, request, *args, **kwargs):
|
||||
super().setup(request, *args, **kwargs)
|
||||
self.object = self.get_object()
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not isinstance(self.object, PreviewableMixin):
|
||||
raise Http404
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def remove_old_preview_data(self):
|
||||
expiration = time() - self.preview_expiration_timeout
|
||||
expired_keys = [
|
||||
k
|
||||
for k, v in self.request.session.items()
|
||||
if k.startswith(self.session_key_prefix) and v[1] < expiration
|
||||
]
|
||||
# Removes the session key gracefully
|
||||
for k in expired_keys:
|
||||
self.request.session.pop(k)
|
||||
|
||||
@property
|
||||
def session_key(self):
|
||||
app_label = self.model._meta.app_label
|
||||
model_name = self.model._meta.model_name
|
||||
unique_key = f"{app_label}-{model_name}-{self.object.pk}"
|
||||
return f"{self.session_key_prefix}{unique_key}"
|
||||
|
||||
def get_object(self):
|
||||
obj = get_object_or_404(self.model, pk=unquote(str(self.kwargs["pk"])))
|
||||
if isinstance(obj, RevisionMixin):
|
||||
obj = obj.get_latest_revision_as_object()
|
||||
return obj
|
||||
|
||||
def get_form_class(self):
|
||||
if self.form_class:
|
||||
return self.form_class
|
||||
return get_edit_handler(self.model).get_form_class()
|
||||
|
||||
def get_form(self, query_dict):
|
||||
form_class = self.get_form_class()
|
||||
|
||||
if not query_dict:
|
||||
# Query dict is empty, return null form
|
||||
return form_class(instance=self.object, for_user=self.request.user)
|
||||
|
||||
return form_class(query_dict, instance=self.object, for_user=self.request.user)
|
||||
|
||||
def _get_data_from_session(self):
|
||||
post_data, _ = self.request.session.get(self.session_key, (None, None))
|
||||
if not isinstance(post_data, str):
|
||||
post_data = ""
|
||||
return QueryDict(post_data)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.remove_old_preview_data()
|
||||
form = self.get_form(request.POST)
|
||||
is_valid = form.is_valid()
|
||||
|
||||
if is_valid:
|
||||
# TODO: Handle request.FILES.
|
||||
request.session[self.session_key] = request.POST.urlencode(), time()
|
||||
is_available = True
|
||||
else:
|
||||
# Check previous data in session to determine preview availability
|
||||
form = self.get_form(self._get_data_from_session())
|
||||
is_available = form.is_valid()
|
||||
|
||||
return JsonResponse({"is_valid": is_valid, "is_available": is_available})
|
||||
|
||||
def error_response(self):
|
||||
return TemplateResponse(
|
||||
self.request,
|
||||
"wagtailadmin/generic/preview_error.html",
|
||||
{"object": self.object},
|
||||
)
|
||||
|
||||
@method_decorator(xframe_options_sameorigin_override)
|
||||
def get(self, request, *args, **kwargs):
|
||||
form = self.get_form(self._get_data_from_session())
|
||||
|
||||
if not form.is_valid():
|
||||
return self.error_response()
|
||||
|
||||
form.save(commit=False)
|
||||
|
||||
try:
|
||||
preview_mode = request.GET.get("mode", self.object.default_preview_mode)
|
||||
except IndexError:
|
||||
raise PermissionDenied
|
||||
|
||||
extra_attrs = {
|
||||
"in_preview_panel": request.GET.get("in_preview_panel") == "true",
|
||||
"is_editing": True,
|
||||
}
|
||||
|
||||
return self.object.make_preview_request(request, preview_mode, extra_attrs)
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
request.session.pop(self.session_key, None)
|
||||
return JsonResponse({"success": True})
|
||||
|
||||
|
||||
class PreviewOnCreate(PreviewOnEdit):
|
||||
@property
|
||||
def session_key(self):
|
||||
app_label = self.model._meta.app_label
|
||||
model_name = self.model._meta.model_name
|
||||
return f"{self.session_key_prefix}{app_label}-{model_name}"
|
||||
|
||||
def get_object(self):
|
||||
return self.model()
|
||||
|
||||
|
||||
class PreviewRevision(View):
|
||||
model = None
|
||||
http_method_names = ("get",)
|
||||
|
||||
def setup(self, request, pk, revision_id, *args, **kwargs):
|
||||
super().setup(request, *args, **kwargs)
|
||||
self.pk = pk
|
||||
self.revision_id = revision_id
|
||||
self.object = self.get_object()
|
||||
self.revision_object = self.get_revision_object()
|
||||
|
||||
def get_object(self):
|
||||
if not issubclass(self.model, RevisionMixin):
|
||||
raise Http404
|
||||
return get_object_or_404(self.model, pk=unquote(str(self.pk)))
|
||||
|
||||
def get_revision_object(self):
|
||||
revision = get_object_or_404(self.object.revisions, id=self.revision_id)
|
||||
return revision.as_object()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
try:
|
||||
preview_mode = request.GET.get(
|
||||
"mode", self.revision_object.default_preview_mode
|
||||
)
|
||||
except IndexError:
|
||||
raise PermissionDenied
|
||||
|
||||
return self.revision_object.make_preview_request(request, preview_mode)
|
||||
148
env/lib/python3.10/site-packages/wagtail/admin/views/generic/usage.py
vendored
Normal file
148
env/lib/python3.10/site-packages/wagtail/admin/views/generic/usage.py
vendored
Normal file
@@ -0,0 +1,148 @@
|
||||
from django.contrib.admin.utils import quote
|
||||
from django.urls import reverse
|
||||
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.admin_url_finder import AdminURLFinder
|
||||
from wagtail.admin.ui import tables
|
||||
from wagtail.admin.utils import get_latest_str
|
||||
from wagtail.admin.widgets.button import HeaderButton
|
||||
from wagtail.models import DraftStateMixin, ReferenceIndex
|
||||
|
||||
from .base import BaseListingView, BaseObjectMixin
|
||||
from .permissions import PermissionCheckedMixin
|
||||
|
||||
|
||||
class TitleColumn(tables.TitleColumn):
|
||||
def get_link_attrs(self, instance, parent_context):
|
||||
return {"title": instance["edit_link_title"]}
|
||||
|
||||
|
||||
class UsageView(PermissionCheckedMixin, BaseObjectMixin, BaseListingView):
|
||||
paginate_by = 20
|
||||
page_title = gettext_lazy("Usage")
|
||||
index_url_name = None
|
||||
edit_url_name = None
|
||||
usage_url_name = None
|
||||
permission_required = "change"
|
||||
|
||||
@cached_property
|
||||
def describe_on_delete(self):
|
||||
return bool(self.request.GET.get("describe_on_delete"))
|
||||
|
||||
def get_object(self):
|
||||
object = super().get_object()
|
||||
if isinstance(object, DraftStateMixin):
|
||||
return object.get_latest_revision_as_object()
|
||||
return object
|
||||
|
||||
def get_edit_url(self, instance):
|
||||
if self.edit_url_name:
|
||||
return reverse(self.edit_url_name, args=(quote(instance.pk),))
|
||||
|
||||
def get_usage_url(self, instance):
|
||||
if self.usage_url_name:
|
||||
return reverse(self.usage_url_name, args=(quote(instance.pk),))
|
||||
|
||||
def get_index_url(self): # used for pagination links
|
||||
return self.get_usage_url(self.object)
|
||||
|
||||
def get_page_subtitle(self):
|
||||
return get_latest_str(self.object)
|
||||
|
||||
def get_breadcrumbs_items(self):
|
||||
items = []
|
||||
if self.index_url_name:
|
||||
items.append(
|
||||
{
|
||||
"url": reverse(self.index_url_name),
|
||||
"label": capfirst(self.object._meta.verbose_name_plural),
|
||||
}
|
||||
)
|
||||
edit_url = self.get_edit_url(self.object)
|
||||
if edit_url:
|
||||
items.append(
|
||||
{
|
||||
"url": edit_url,
|
||||
"label": get_latest_str(self.object),
|
||||
}
|
||||
)
|
||||
items.append(
|
||||
{
|
||||
"url": "",
|
||||
"label": _("Usage"),
|
||||
"sublabel": self.get_page_subtitle(),
|
||||
}
|
||||
)
|
||||
return self.breadcrumbs_items + items
|
||||
|
||||
@cached_property
|
||||
def header_buttons(self):
|
||||
edit_url = self.get_edit_url(self.object)
|
||||
buttons = []
|
||||
if edit_url:
|
||||
buttons.append(
|
||||
HeaderButton(
|
||||
label=_("Edit"),
|
||||
url=edit_url,
|
||||
icon_name="edit",
|
||||
)
|
||||
)
|
||||
return buttons
|
||||
|
||||
def get_queryset(self):
|
||||
return ReferenceIndex.get_references_to(self.object).group_by_source_object()
|
||||
|
||||
@cached_property
|
||||
def columns(self):
|
||||
return [
|
||||
TitleColumn(
|
||||
"name",
|
||||
label=_("Name"),
|
||||
accessor="label",
|
||||
get_url=lambda r: r["edit_url"],
|
||||
),
|
||||
tables.Column(
|
||||
"content_type",
|
||||
label=_("Type"),
|
||||
# Use the content type from the ReferenceIndex object instead of the
|
||||
# object itself, so we can get the specific content type without
|
||||
# having to fetch the specific object from the database.
|
||||
accessor=lambda r: capfirst(r["references"][0].model_name),
|
||||
),
|
||||
tables.ReferencesColumn(
|
||||
"field",
|
||||
label=_("If you confirm deletion")
|
||||
if self.describe_on_delete
|
||||
else _("Field"),
|
||||
accessor="references",
|
||||
get_url=lambda r: r["edit_url"],
|
||||
describe_on_delete=self.describe_on_delete,
|
||||
),
|
||||
]
|
||||
|
||||
def get_table(self, object_list, **kwargs):
|
||||
url_finder = AdminURLFinder(self.request.user)
|
||||
results = []
|
||||
for object, references in object_list:
|
||||
row = {"object": object, "references": references}
|
||||
row["edit_url"] = url_finder.get_edit_url(object)
|
||||
if row["edit_url"] is None:
|
||||
row["label"] = _("(Private %(object)s)") % {
|
||||
"object": object._meta.verbose_name
|
||||
}
|
||||
row["edit_link_title"] = None
|
||||
else:
|
||||
row["label"] = str(object)
|
||||
row["edit_link_title"] = _("Edit this %(object)s") % {
|
||||
"object": object._meta.verbose_name
|
||||
}
|
||||
results.append(row)
|
||||
return super().get_table(results, **kwargs)
|
||||
|
||||
def get_context_data(self, *args, object_list=None, **kwargs):
|
||||
return super().get_context_data(
|
||||
*args, object_list=object_list, object=self.object, **kwargs
|
||||
)
|
||||
265
env/lib/python3.10/site-packages/wagtail/admin/views/generic/workflow.py
vendored
Normal file
265
env/lib/python3.10/site-packages/wagtail/admin/views/generic/workflow.py
vendored
Normal file
@@ -0,0 +1,265 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.admin.utils import quote
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views import View
|
||||
|
||||
from wagtail.admin import messages
|
||||
from wagtail.admin.modal_workflow import render_modal_workflow
|
||||
from wagtail.admin.utils import get_latest_str, get_valid_next_url_from_request
|
||||
from wagtail.admin.views.generic.base import BaseObjectMixin
|
||||
from wagtail.models import Task, TaskState, WorkflowState
|
||||
|
||||
|
||||
class BaseWorkflowFormView(BaseObjectMixin, View):
|
||||
"""
|
||||
Shared functionality for views that need to render the modal form to collect extra details
|
||||
for a workflow task
|
||||
"""
|
||||
|
||||
redirect_url_name = None
|
||||
submit_url_name = None
|
||||
template_name = "wagtailadmin/shared/workflow_action_modal.html"
|
||||
|
||||
def setup(self, request, *args, action_name, task_state_id, **kwargs):
|
||||
super().setup(request, *args, **kwargs)
|
||||
self.action_name = action_name
|
||||
self.task_state_id = task_state_id
|
||||
self.redirect_url = self.get_redirect_url()
|
||||
self.task_state = self.get_task_state()
|
||||
self.task = self.get_task()
|
||||
self.form_class = self.get_form_class()
|
||||
|
||||
def get_redirect_url(self):
|
||||
next_url = get_valid_next_url_from_request(self.request)
|
||||
if next_url:
|
||||
return next_url
|
||||
return reverse(self.redirect_url_name, args=(quote(self.object.pk),))
|
||||
|
||||
def get_task_state(self):
|
||||
return get_object_or_404(TaskState, id=self.task_state_id).specific
|
||||
|
||||
def get_task(self):
|
||||
return self.task_state.task.specific
|
||||
|
||||
def get_form_class(self):
|
||||
return self.task.get_form_for_action(self.action_name)
|
||||
|
||||
def add_not_in_moderation_error(self):
|
||||
messages.error(
|
||||
self.request,
|
||||
_("The %(model_name)s '%(title)s' is not currently awaiting moderation.")
|
||||
% {
|
||||
"model_name": self.model._meta.verbose_name,
|
||||
"title": get_latest_str(self.object),
|
||||
},
|
||||
)
|
||||
|
||||
def check_action(self):
|
||||
actions = self.task.get_actions(self.object, self.request.user)
|
||||
self.action_verbose_name = ""
|
||||
action_available = False
|
||||
self.action_modal = False
|
||||
|
||||
for name, verbose_name, modal in actions:
|
||||
if name == self.action_name:
|
||||
action_available = True
|
||||
if modal:
|
||||
self.action_modal = True
|
||||
# if two actions have the same name, use the verbose name
|
||||
# of the one allowing modal data entry within the modal
|
||||
self.action_verbose_name = verbose_name
|
||||
if not action_available:
|
||||
raise PermissionDenied
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not self.object.workflow_in_progress:
|
||||
self.add_not_in_moderation_error()
|
||||
return redirect(self.redirect_url)
|
||||
self.check_action()
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.render_modal_form(request, self.form_class())
|
||||
|
||||
def get_submit_url(self):
|
||||
return reverse(
|
||||
self.submit_url_name,
|
||||
args=(quote(self.object.pk), self.action_name, self.task_state.id),
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return {
|
||||
"object": self.object,
|
||||
"action": self.action_name,
|
||||
"action_verbose": self.action_verbose_name,
|
||||
"task_state": self.task_state,
|
||||
"submit_url": self.get_submit_url(),
|
||||
**kwargs,
|
||||
}
|
||||
|
||||
def render_modal_form(self, request, form):
|
||||
return render_modal_workflow(
|
||||
request,
|
||||
self.template_name,
|
||||
None,
|
||||
self.get_context_data(form=form),
|
||||
json_data={"step": "action"},
|
||||
)
|
||||
|
||||
def render_modal_json(self, request, json_data):
|
||||
return render_modal_workflow(request, "", None, {}, json_data=json_data)
|
||||
|
||||
|
||||
class WorkflowAction(BaseWorkflowFormView):
|
||||
"""Provides a modal view to enter additional data for the specified workflow action on GET,
|
||||
or perform the specified action on POST"""
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if self.form_class:
|
||||
form = self.form_class(self.request.POST)
|
||||
if form.is_valid():
|
||||
self.redirect_url = (
|
||||
self.task.on_action(
|
||||
self.task_state,
|
||||
self.request.user,
|
||||
self.action_name,
|
||||
**form.cleaned_data,
|
||||
)
|
||||
or self.redirect_url
|
||||
)
|
||||
elif (
|
||||
self.action_modal
|
||||
and self.request.headers.get("x-requested-with") == "XMLHttpRequest"
|
||||
):
|
||||
# show form errors
|
||||
return self.render_modal_form(self.request, form)
|
||||
else:
|
||||
self.redirect_url = (
|
||||
self.task.on_action(
|
||||
self.task_state, self.request.user, self.action_name
|
||||
)
|
||||
or self.redirect_url
|
||||
)
|
||||
|
||||
if self.request.headers.get("x-requested-with") == "XMLHttpRequest":
|
||||
return self.render_modal_json(
|
||||
self.request,
|
||||
{"step": "success", "redirect": self.redirect_url},
|
||||
)
|
||||
return redirect(self.redirect_url)
|
||||
|
||||
|
||||
class CollectWorkflowActionData(BaseWorkflowFormView):
|
||||
"""
|
||||
On GET, provides a modal view to enter additional data for the specified workflow action;
|
||||
on POST, return the validated form data back to the modal's caller via a JSON response, so that
|
||||
the calling view can subsequently perform the action as part of its own processing
|
||||
(for example, approving moderation while making an edit).
|
||||
"""
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.form_class(request.POST)
|
||||
if form.is_valid():
|
||||
return self.render_modal_json(
|
||||
request,
|
||||
{"step": "success", "cleaned_data": form.cleaned_data},
|
||||
)
|
||||
elif (
|
||||
self.action_modal
|
||||
and request.headers.get("x-requested-with") == "XMLHttpRequest"
|
||||
):
|
||||
# show form errors
|
||||
return self.render_modal_form(request, form)
|
||||
return redirect(self.redirect_url)
|
||||
|
||||
|
||||
class ConfirmWorkflowCancellation(BaseObjectMixin, View):
|
||||
template_name = "wagtailadmin/generic/confirm_workflow_cancellation.html"
|
||||
|
||||
def setup(self, request, *args, **kwargs):
|
||||
super().setup(request, *args, **kwargs)
|
||||
self.workflow_state = self.object.current_workflow_state
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not self.workflow_state or not getattr(
|
||||
settings, "WAGTAIL_WORKFLOW_CANCEL_ON_PUBLISH", True
|
||||
):
|
||||
return render_modal_workflow(
|
||||
request,
|
||||
"",
|
||||
None,
|
||||
{},
|
||||
json_data={"step": "no_confirmation_needed"},
|
||||
)
|
||||
|
||||
return render_modal_workflow(
|
||||
request,
|
||||
self.template_name,
|
||||
None,
|
||||
self.get_context_data(),
|
||||
json_data={"step": "confirm"},
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return {
|
||||
"needs_changes": self.workflow_state.status
|
||||
== WorkflowState.STATUS_NEEDS_CHANGES,
|
||||
"task": self.workflow_state.current_task_state.task.name,
|
||||
"workflow": self.workflow_state.workflow.name,
|
||||
"model_opts": self.model_opts,
|
||||
**kwargs,
|
||||
}
|
||||
|
||||
|
||||
class PreviewRevisionForTask(BaseObjectMixin, View):
|
||||
def setup(self, request, *args, task_id, **kwargs):
|
||||
super().setup(request, *args, **kwargs)
|
||||
self.task_id = task_id
|
||||
self.task = self.get_task()
|
||||
self.task_state = self.get_task_state()
|
||||
|
||||
def get_task(self):
|
||||
return get_object_or_404(Task, id=self.task_id).specific
|
||||
|
||||
def get_task_state(self):
|
||||
return TaskState.objects.filter(
|
||||
revision__base_content_type=self.object.get_base_content_type(),
|
||||
revision__object_id=self.pk,
|
||||
task=self.task,
|
||||
status=TaskState.STATUS_IN_PROGRESS,
|
||||
).first()
|
||||
|
||||
def add_error_message(self):
|
||||
messages.error(
|
||||
self.request,
|
||||
_(
|
||||
"The %(model_name)s '%(title)s' is not currently awaiting moderation in task '%(task_name)s'."
|
||||
)
|
||||
% {
|
||||
"model_name": self.model._meta.verbose_name,
|
||||
"title": get_latest_str(self.object),
|
||||
"task_name": self.task.name,
|
||||
},
|
||||
)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if not self.task_state:
|
||||
self.add_error_message()
|
||||
return redirect("wagtailadmin_home")
|
||||
|
||||
if not self.task.get_actions(self.object, request.user):
|
||||
raise PermissionDenied
|
||||
|
||||
revision = self.task_state.revision
|
||||
object_to_view = revision.as_object()
|
||||
|
||||
# TODO: provide workflow actions within this view
|
||||
|
||||
return object_to_view.make_preview_request(
|
||||
request,
|
||||
object_to_view.default_preview_mode,
|
||||
extra_request_attrs={"revision_id": revision.id},
|
||||
)
|
||||
352
env/lib/python3.10/site-packages/wagtail/admin/views/home.py
vendored
Normal file
352
env/lib/python3.10/site-packages/wagtail/admin/views/home.py
vendored
Normal file
@@ -0,0 +1,352 @@
|
||||
from typing import Any, Mapping, Union
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.decorators import permission_required
|
||||
from django.db.models import Exists, IntegerField, Max, OuterRef, Q
|
||||
from django.db.models.functions import Cast
|
||||
from django.forms import Media
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.utils.translation import gettext_lazy
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
from wagtail import hooks
|
||||
from wagtail.admin.icons import get_icons
|
||||
from wagtail.admin.navigation import get_site_for_user
|
||||
from wagtail.admin.site_summary import SiteSummaryPanel
|
||||
from wagtail.admin.ui.components import Component
|
||||
from wagtail.admin.views.generic import WagtailAdminTemplateMixin
|
||||
from wagtail.models import (
|
||||
Page,
|
||||
PageLogEntry,
|
||||
Revision,
|
||||
TaskState,
|
||||
WorkflowState,
|
||||
get_default_page_content_type,
|
||||
)
|
||||
from wagtail.permissions import page_permission_policy
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
# Panels for the homepage
|
||||
|
||||
|
||||
class UpgradeNotificationPanel(Component):
|
||||
name = "upgrade_notification"
|
||||
template_name = "wagtailadmin/home/upgrade_notification.html"
|
||||
order = 100
|
||||
|
||||
def get_upgrade_check_setting(self) -> Union[bool, str]:
|
||||
return getattr(settings, "WAGTAIL_ENABLE_UPDATE_CHECK", True)
|
||||
|
||||
def upgrade_check_lts_only(self) -> bool:
|
||||
upgrade_check = self.get_upgrade_check_setting()
|
||||
if isinstance(upgrade_check, str) and upgrade_check.lower() == "lts":
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_context_data(self, parent_context: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
return {"lts_only": self.upgrade_check_lts_only()}
|
||||
|
||||
def render_html(self, parent_context: Mapping[str, Any] = None) -> str:
|
||||
if (
|
||||
parent_context["request"].user.is_superuser
|
||||
and self.get_upgrade_check_setting()
|
||||
):
|
||||
return super().render_html(parent_context)
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
class WhatsNewInWagtailVersionPanel(Component):
|
||||
name = "whats_new_in_wagtail_version"
|
||||
template_name = "wagtailadmin/home/whats_new_in_wagtail_version.html"
|
||||
order = 110
|
||||
_version = "4"
|
||||
|
||||
def get_whats_new_banner_setting(self) -> Union[bool, str]:
|
||||
return getattr(settings, "WAGTAIL_ENABLE_WHATS_NEW_BANNER", True)
|
||||
|
||||
def get_dismissible_id(self) -> str:
|
||||
return f"{self.name}_{self._version}"
|
||||
|
||||
def get_context_data(self, parent_context: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
return {"dismissible_id": self.get_dismissible_id(), "version": self._version}
|
||||
|
||||
def is_shown(self, parent_context: Mapping[str, Any] = None) -> bool:
|
||||
if not self.get_whats_new_banner_setting():
|
||||
return False
|
||||
|
||||
profile = getattr(parent_context["request"].user, "wagtail_userprofile", None)
|
||||
if profile and profile.dismissibles.get(self.get_dismissible_id()):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def render_html(self, parent_context: Mapping[str, Any] = None) -> str:
|
||||
if not self.is_shown(parent_context):
|
||||
return ""
|
||||
return super().render_html(parent_context)
|
||||
|
||||
|
||||
class UserObjectsInWorkflowModerationPanel(Component):
|
||||
name = "user_objects_in_workflow_moderation"
|
||||
template_name = "wagtailadmin/home/user_objects_in_workflow_moderation.html"
|
||||
order = 210
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
request = parent_context["request"]
|
||||
context = super().get_context_data(parent_context)
|
||||
if getattr(settings, "WAGTAIL_WORKFLOW_ENABLED", True):
|
||||
# Need to cast the page ids to string because Postgres doesn't support
|
||||
# implicit type casts when querying on GenericRelations. We also need
|
||||
# to cast the object_id to integer when querying the pages for the same reason.
|
||||
# https://code.djangoproject.com/ticket/16055
|
||||
# Once the issue is resolved, this query can be removed and the
|
||||
# filter can be changed to:
|
||||
# Q(page__owner=request.user) | Q(requested_by=request.user)
|
||||
pages_owned_by_user = Q(
|
||||
base_content_type_id=get_default_page_content_type().id
|
||||
) & Exists(
|
||||
Page.objects.filter(
|
||||
owner=request.user,
|
||||
id=Cast(OuterRef("object_id"), output_field=IntegerField()),
|
||||
)
|
||||
)
|
||||
# Find in progress workflow states which are either requested by the user or on pages owned by the user
|
||||
context["workflow_states"] = (
|
||||
WorkflowState.objects.active()
|
||||
.filter(pages_owned_by_user | Q(requested_by=request.user))
|
||||
.prefetch_related(
|
||||
"content_object",
|
||||
"content_object__latest_revision",
|
||||
)
|
||||
.select_related(
|
||||
"current_task_state",
|
||||
"current_task_state__task",
|
||||
)
|
||||
.order_by("-current_task_state__started_at")
|
||||
)
|
||||
# Filter out workflow states where the GenericForeignKey points to
|
||||
# a nonexistent object. This can happen if the model does not define
|
||||
# a GenericRelation to WorkflowState and the instance is deleted.
|
||||
context["workflow_states"] = [
|
||||
state for state in context["workflow_states"] if state.content_object
|
||||
]
|
||||
else:
|
||||
context["workflow_states"] = WorkflowState.objects.none()
|
||||
context["request"] = request
|
||||
return context
|
||||
|
||||
|
||||
class WorkflowObjectsToModeratePanel(Component):
|
||||
name = "workflow_objects_to_moderate"
|
||||
template_name = "wagtailadmin/home/workflow_objects_to_moderate.html"
|
||||
order = 220
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
request = parent_context["request"]
|
||||
context = super().get_context_data(parent_context)
|
||||
context["states"] = []
|
||||
context["request"] = request
|
||||
context["csrf_token"] = parent_context["csrf_token"]
|
||||
|
||||
if not getattr(settings, "WAGTAIL_WORKFLOW_ENABLED", True):
|
||||
return context
|
||||
|
||||
states = (
|
||||
TaskState.objects.reviewable_by(request.user)
|
||||
.select_related(
|
||||
"revision",
|
||||
"revision__user",
|
||||
"workflow_state",
|
||||
"workflow_state__workflow",
|
||||
)
|
||||
.prefetch_related(
|
||||
"revision__content_object",
|
||||
"revision__content_object__latest_revision",
|
||||
)
|
||||
.order_by("-started_at")
|
||||
.annotate(
|
||||
previous_revision_id=Revision.objects.previous_revision_id_subquery(),
|
||||
)
|
||||
)
|
||||
|
||||
for state in states:
|
||||
obj = state.revision.content_object
|
||||
# Skip task states where the revision's GenericForeignKey points to
|
||||
# a nonexistent object. This can happen if the model does not define
|
||||
# a GenericRelation to WorkflowState and/or Revision and the instance
|
||||
# is deleted.
|
||||
if not obj:
|
||||
continue
|
||||
actions = state.task.specific.get_actions(obj, request.user)
|
||||
workflow_tasks = state.workflow_state.all_tasks_with_status()
|
||||
|
||||
workflow_action_url_name = "wagtailadmin_pages:workflow_action"
|
||||
workflow_preview_url_name = "wagtailadmin_pages:workflow_preview"
|
||||
revisions_compare_url_name = "wagtailadmin_pages:revisions_compare"
|
||||
|
||||
# Snippets can also have workflows
|
||||
if not isinstance(obj, Page):
|
||||
viewset = obj.snippet_viewset
|
||||
workflow_action_url_name = viewset.get_url_name("workflow_action")
|
||||
workflow_preview_url_name = viewset.get_url_name("workflow_preview")
|
||||
revisions_compare_url_name = viewset.get_url_name("revisions_compare")
|
||||
|
||||
if not getattr(obj, "is_previewable", False):
|
||||
workflow_preview_url_name = None
|
||||
|
||||
context["states"].append(
|
||||
{
|
||||
"obj": obj,
|
||||
"revision": state.revision,
|
||||
"previous_revision_id": state.previous_revision_id,
|
||||
"live_revision_id": obj.live_revision_id,
|
||||
"task_state": state,
|
||||
"actions": actions,
|
||||
"workflow_tasks": workflow_tasks,
|
||||
"workflow_action_url_name": workflow_action_url_name,
|
||||
"workflow_preview_url_name": workflow_preview_url_name,
|
||||
"revisions_compare_url_name": revisions_compare_url_name,
|
||||
}
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class LockedPagesPanel(Component):
|
||||
name = "locked_pages"
|
||||
template_name = "wagtailadmin/home/locked_pages.html"
|
||||
order = 300
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
request = parent_context["request"]
|
||||
context = super().get_context_data(parent_context)
|
||||
context.update(
|
||||
{
|
||||
"locked_pages": Page.objects.filter(
|
||||
locked=True,
|
||||
locked_by=request.user,
|
||||
)
|
||||
.order_by("-locked_at", "-latest_revision_created_at", "-pk")
|
||||
.specific(defer=True),
|
||||
"can_remove_locks": page_permission_policy.user_has_permission(
|
||||
request.user, "unlock"
|
||||
),
|
||||
"request": request,
|
||||
"csrf_token": parent_context["csrf_token"],
|
||||
}
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class RecentEditsPanel(Component):
|
||||
name = "recent_edits"
|
||||
template_name = "wagtailadmin/home/recent_edits.html"
|
||||
order = 250
|
||||
|
||||
def get_context_data(self, parent_context):
|
||||
request = parent_context["request"]
|
||||
context = super().get_context_data(parent_context)
|
||||
|
||||
# Last n edited pages
|
||||
edit_count = getattr(settings, "WAGTAILADMIN_RECENT_EDITS_LIMIT", 5)
|
||||
|
||||
# Query the audit log to get a resultset of (page ID, latest edit timestamp)
|
||||
last_edits_dates = (
|
||||
PageLogEntry.objects.filter(user=request.user, action="wagtail.edit")
|
||||
.values("page_id")
|
||||
.annotate(latest_date=Max("timestamp"))
|
||||
.order_by("-latest_date")[:edit_count]
|
||||
)
|
||||
# Retrieve the page objects for those IDs
|
||||
pages_mapping = (
|
||||
Page.objects.specific()
|
||||
.prefetch_workflow_states()
|
||||
.annotate_approved_schedule()
|
||||
.in_bulk([log["page_id"] for log in last_edits_dates])
|
||||
)
|
||||
# Compile a list of (latest edit timestamp, page object) tuples
|
||||
last_edits = []
|
||||
for log in last_edits_dates:
|
||||
page = pages_mapping.get(log["page_id"])
|
||||
if page:
|
||||
last_edits.append((log["latest_date"], page))
|
||||
|
||||
context["last_edits"] = last_edits
|
||||
context["request"] = request
|
||||
return context
|
||||
|
||||
|
||||
class HomeView(WagtailAdminTemplateMixin, TemplateView):
|
||||
template_name = "wagtailadmin/home.html"
|
||||
page_title = gettext_lazy("Dashboard")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
panels = self.get_panels()
|
||||
site_details = self.get_site_details()
|
||||
|
||||
context["media"] = self.get_media(panels)
|
||||
context["panels"] = sorted(panels, key=lambda p: p.order)
|
||||
context["user"] = self.request.user
|
||||
|
||||
return {**context, **site_details}
|
||||
|
||||
def get_media(self, panels=[]):
|
||||
media = Media()
|
||||
|
||||
for panel in panels:
|
||||
media += panel.media
|
||||
|
||||
return media
|
||||
|
||||
def get_panels(self):
|
||||
request = self.request
|
||||
panels = [
|
||||
SiteSummaryPanel(request),
|
||||
# Disabled until a release warrants the banner.
|
||||
# WhatsNewInWagtailVersionPanel(),
|
||||
UpgradeNotificationPanel(),
|
||||
WorkflowObjectsToModeratePanel(),
|
||||
UserObjectsInWorkflowModerationPanel(),
|
||||
RecentEditsPanel(),
|
||||
LockedPagesPanel(),
|
||||
]
|
||||
|
||||
for fn in hooks.get_hooks("construct_homepage_panels"):
|
||||
fn(request, panels)
|
||||
|
||||
return panels
|
||||
|
||||
def get_site_details(self):
|
||||
request = self.request
|
||||
site = get_site_for_user(request.user)
|
||||
|
||||
return {
|
||||
"root_page": site["root_page"],
|
||||
"root_site": site["root_site"],
|
||||
"site_name": site["site_name"],
|
||||
}
|
||||
|
||||
|
||||
def error_test(request):
|
||||
raise Exception("This is a test of the emergency broadcast system.")
|
||||
|
||||
|
||||
@permission_required("wagtailadmin.access_admin", login_url="wagtailadmin_login")
|
||||
def default(request):
|
||||
"""
|
||||
Called whenever a request comes in with the correct prefix (eg /admin/) but
|
||||
doesn't actually correspond to a Wagtail view.
|
||||
|
||||
For authenticated users, it'll raise a 404 error. Anonymous users will be
|
||||
redirected to the login page.
|
||||
"""
|
||||
raise Http404
|
||||
|
||||
|
||||
def sprite(request):
|
||||
return HttpResponse(get_icons(), content_type="image/svg+xml; charset=utf-8")
|
||||
333
env/lib/python3.10/site-packages/wagtail/admin/views/mixins.py
vendored
Normal file
333
env/lib/python3.10/site-packages/wagtail/admin/views/mixins.py
vendored
Normal file
@@ -0,0 +1,333 @@
|
||||
import csv
|
||||
import datetime
|
||||
from collections import OrderedDict
|
||||
from functools import partial
|
||||
from io import BytesIO
|
||||
|
||||
from django.contrib.admin.utils import label_for_field
|
||||
from django.core.exceptions import FieldDoesNotExist
|
||||
from django.http import FileResponse, StreamingHttpResponse
|
||||
from django.utils import timezone
|
||||
from django.utils.dateformat import Formatter
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.formats import get_format
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import gettext as _
|
||||
from openpyxl import Workbook
|
||||
from openpyxl.cell import WriteOnlyCell
|
||||
|
||||
from wagtail.admin.widgets.button import Button
|
||||
from wagtail.coreutils import multigetattr
|
||||
|
||||
|
||||
class Echo:
|
||||
"""An object that implements just the write method of the file-like interface."""
|
||||
|
||||
def write(self, value):
|
||||
"""Write the value by returning it, instead of storing in a buffer."""
|
||||
return value.encode("UTF-8")
|
||||
|
||||
|
||||
def list_to_str(value):
|
||||
return force_str(", ".join(value))
|
||||
|
||||
|
||||
class ExcelDateFormatter(Formatter):
|
||||
data = None
|
||||
|
||||
# From: https://docs.djangoproject.com/en/stable/ref/templates/builtins/#date
|
||||
# To: https://support.microsoft.com/en-us/office/format-numbers-as-dates-or-times-418bd3fe-0577-47c8-8caa-b4d30c528309#bm2
|
||||
_formats = {
|
||||
# Day of the month, 2 digits with leading zeros.
|
||||
"d": "dd",
|
||||
# Day of the month without leading zeros.
|
||||
"j": "d",
|
||||
# Day of the week, textual, 3 letters.
|
||||
"D": "ddd",
|
||||
# Day of the week, textual, full.
|
||||
"l": "dddd",
|
||||
# English ordinal suffix for the day of the month, 2 characters.
|
||||
"S": "", # Not supported in Excel
|
||||
# Day of the week, digits without leading zeros.
|
||||
"w": "", # Not supported in Excel
|
||||
# Day of the year.
|
||||
"z": "", # Not supported in Excel
|
||||
# ISO-8601 week number of year, with weeks starting on Monday.
|
||||
"W": "", # Not supported in Excel
|
||||
# Month, 2 digits with leading zeros.
|
||||
"m": "mm",
|
||||
# Month without leading zeros.
|
||||
"n": "m",
|
||||
# Month, textual, 3 letters.
|
||||
"M": "mmm",
|
||||
# Month, textual, 3 letters, lowercase. (Not supported in Excel)
|
||||
"b": "mmm",
|
||||
# Month, locale specific alternative representation usually used for long date representation.
|
||||
"E": "mmmm", # Not supported in Excel
|
||||
# Month, textual, full.
|
||||
"F": "mmmm",
|
||||
# Month abbreviation in Associated Press style. Proprietary extension.
|
||||
"N": "mmm.", # Approximation, wrong for May
|
||||
# Number of days in the given month.
|
||||
"t": "", # Not supported in Excel
|
||||
# Year, 2 digits with leading zeros.
|
||||
"y": "yy",
|
||||
# Year, 4 digits with leading zeros.
|
||||
"Y": "yyyy",
|
||||
# Whether it's a leap year.
|
||||
"L": "", # Not supported in Excel
|
||||
# ISO-8601 week-numbering year.
|
||||
"o": "yyyy", # Approximation, same as Y
|
||||
# Hour, 12-hour format without leading zeros.
|
||||
"g": "h", # Only works when combined with AM/PM, 24-hour format is used otherwise
|
||||
# Hour, 24-hour format without leading zeros.
|
||||
"G": "hH",
|
||||
# Hour, 12-hour format with leading zeros.
|
||||
"h": "hh", # Only works when combined with AM/PM, 24-hour format is used otherwise
|
||||
# Hour, 24-hour format with leading zeros.
|
||||
"H": "hh",
|
||||
# Minutes.
|
||||
"i": "mm",
|
||||
# Seconds.
|
||||
"s": "ss",
|
||||
# Microseconds.
|
||||
"u": ".00", # Only works when combined with ss
|
||||
# 'a.m.' or 'p.m.'.
|
||||
"a": "AM/PM", # Approximation, uses AM/PM and only works when combined with h/hh
|
||||
# AM/PM.
|
||||
"A": "AM/PM", # Only works when combined with h/hh
|
||||
# Time, in 12-hour hours and minutes, with minutes left off if they’re zero.
|
||||
"f": "h:mm", # Approximation, uses 24-hour format and minutes are never left off
|
||||
# Time, in 12-hour hours, minutes and ‘a.m.’/’p.m.’, with minutes left off if they’re zero and the special-case strings ‘midnight’ and ‘noon’ if appropriate.
|
||||
"P": "h:mm AM/PM", # Approximation, minutes are never left off, no special case strings
|
||||
# Timezone name.
|
||||
"e": "", # Not supported in Excel
|
||||
# Daylight saving time, whether it’s in effect or not.
|
||||
"I": "", # Not supported in Excel
|
||||
# Difference to Greenwich time in hours.
|
||||
"O": "", # Not supported in Excel
|
||||
# Time zone of this machine.
|
||||
"T": "", # Not supported in Excel
|
||||
# Timezone offset in seconds.
|
||||
"Z": "", # Not supported in Excel
|
||||
# ISO 8601 format.
|
||||
"c": "yyyy-mm-ddThh:mm:ss.00",
|
||||
# RFC 5322 formatted date.
|
||||
"r": "ddd, d mmm yyyy hh:mm:ss",
|
||||
# Seconds since the Unix epoch.
|
||||
"U": "", # Not supported in Excel
|
||||
}
|
||||
|
||||
def get(self):
|
||||
format = get_format("SHORT_DATETIME_FORMAT")
|
||||
return self.format(format)
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in self._formats:
|
||||
return lambda: self._formats[name]
|
||||
raise AttributeError(
|
||||
f"'{type(self).__name__}' object has no attribute '{name}'"
|
||||
)
|
||||
|
||||
|
||||
class SpreadsheetExportMixin:
|
||||
"""A mixin for views, providing spreadsheet export functionality in csv and xlsx formats"""
|
||||
|
||||
FORMAT_XLSX = "xlsx"
|
||||
FORMAT_CSV = "csv"
|
||||
FORMATS = (FORMAT_XLSX, FORMAT_CSV)
|
||||
|
||||
# A list of fields or callables (without arguments) to export from each item in the queryset (dotted paths allowed)
|
||||
list_export = []
|
||||
# A dictionary of custom preprocessing functions by field and format (expected value would be of the form {field_name: {format: function}})
|
||||
# If a valid field preprocessing function is found, any applicable value preprocessing functions will not be used
|
||||
custom_field_preprocess = {}
|
||||
# A dictionary of preprocessing functions by value class and format
|
||||
custom_value_preprocess = {
|
||||
datetime.datetime: {
|
||||
FORMAT_XLSX: lambda value: (
|
||||
value
|
||||
if timezone.is_naive(value)
|
||||
else timezone.make_naive(value, datetime.timezone.utc)
|
||||
)
|
||||
},
|
||||
(datetime.date, datetime.time): {FORMAT_XLSX: None},
|
||||
list: {FORMAT_CSV: list_to_str, FORMAT_XLSX: list_to_str},
|
||||
}
|
||||
# A dictionary of column heading overrides in the format {field: heading}
|
||||
export_headings = {}
|
||||
|
||||
export_buttons_template_name = "wagtailadmin/shared/export_buttons.html"
|
||||
|
||||
export_filename = "spreadsheet-export"
|
||||
|
||||
def setup(self, request, *args, **kwargs):
|
||||
super().setup(request, *args, **kwargs)
|
||||
self.is_export = request.GET.get("export") in self.FORMATS
|
||||
|
||||
def get_paginate_by(self, queryset):
|
||||
if self.is_export:
|
||||
return None
|
||||
return super().get_paginate_by(queryset)
|
||||
|
||||
def get_filename(self):
|
||||
"""Gets the base filename for the exported spreadsheet, without extensions"""
|
||||
return self.export_filename
|
||||
|
||||
def to_row_dict(self, item):
|
||||
"""Returns an OrderedDict (in the order given by list_export) of the exportable information for a model instance"""
|
||||
row_dict = OrderedDict(
|
||||
(field, multigetattr(item, field)) for field in self.list_export
|
||||
)
|
||||
return row_dict
|
||||
|
||||
def get_preprocess_function(self, field, value, export_format):
|
||||
"""Returns the preprocessing function for a given field name, field value, and export format"""
|
||||
|
||||
# Try to find a field specific function and return it
|
||||
format_dict = self.custom_field_preprocess.get(field, {})
|
||||
if export_format in format_dict:
|
||||
return format_dict[export_format]
|
||||
|
||||
# Otherwise check for a value class specific function
|
||||
for value_classes, format_dict in self.custom_value_preprocess.items():
|
||||
if isinstance(value, value_classes) and export_format in format_dict:
|
||||
return format_dict[export_format]
|
||||
|
||||
# Finally resort to force_str to prevent encoding errors
|
||||
return partial(force_str, strings_only=True)
|
||||
|
||||
def preprocess_field_value(self, field, value, export_format):
|
||||
"""Preprocesses a field value before writing it to the spreadsheet"""
|
||||
preprocess_function = self.get_preprocess_function(field, value, export_format)
|
||||
if preprocess_function is not None:
|
||||
return preprocess_function(value)
|
||||
else:
|
||||
return value
|
||||
|
||||
def generate_xlsx_row(self, worksheet, row_dict, date_format=None):
|
||||
"""Generate cells to append to the worksheet"""
|
||||
for field, value in row_dict.items():
|
||||
cell = WriteOnlyCell(
|
||||
worksheet, self.preprocess_field_value(field, value, self.FORMAT_XLSX)
|
||||
)
|
||||
if date_format and isinstance(value, datetime.datetime):
|
||||
cell.number_format = date_format
|
||||
yield cell
|
||||
|
||||
def write_csv_row(self, writer, row_dict):
|
||||
return writer.writerow(
|
||||
{
|
||||
field: self.preprocess_field_value(field, value, self.FORMAT_CSV)
|
||||
for field, value in row_dict.items()
|
||||
}
|
||||
)
|
||||
|
||||
def get_heading(self, queryset, field):
|
||||
"""Get the heading label for a given field for a spreadsheet generated from queryset"""
|
||||
heading_override = self.export_headings.get(field)
|
||||
if heading_override:
|
||||
return force_str(heading_override)
|
||||
try:
|
||||
return capfirst(force_str(label_for_field(field, queryset.model)))
|
||||
except (AttributeError, FieldDoesNotExist):
|
||||
return force_str(field)
|
||||
|
||||
def stream_csv(self, queryset):
|
||||
"""Generate a csv file line by line from queryset, to be used in a StreamingHTTPResponse"""
|
||||
writer = csv.DictWriter(Echo(), fieldnames=self.list_export)
|
||||
yield writer.writerow(
|
||||
{field: self.get_heading(queryset, field) for field in self.list_export}
|
||||
)
|
||||
|
||||
for item in queryset:
|
||||
yield self.write_csv_row(writer, self.to_row_dict(item))
|
||||
|
||||
def write_xlsx(self, queryset, output):
|
||||
"""Write an xlsx workbook from a queryset"""
|
||||
workbook = Workbook(write_only=True, iso_dates=True)
|
||||
|
||||
worksheet = workbook.create_sheet(title="Sheet1")
|
||||
worksheet.append(
|
||||
self.get_heading(queryset, field) for field in self.list_export
|
||||
)
|
||||
|
||||
date_format = ExcelDateFormatter().get()
|
||||
for item in queryset:
|
||||
worksheet.append(
|
||||
self.generate_xlsx_row(
|
||||
worksheet, self.to_row_dict(item), date_format=date_format
|
||||
)
|
||||
)
|
||||
|
||||
workbook.save(output)
|
||||
|
||||
def write_xlsx_response(self, queryset):
|
||||
"""Write an xlsx file from a queryset and return a FileResponse"""
|
||||
output = BytesIO()
|
||||
self.write_xlsx(queryset, output)
|
||||
output.seek(0)
|
||||
|
||||
return FileResponse(
|
||||
output,
|
||||
as_attachment=True,
|
||||
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
filename=f"{self.get_filename()}.xlsx",
|
||||
)
|
||||
|
||||
def write_csv_response(self, queryset):
|
||||
stream = self.stream_csv(queryset)
|
||||
|
||||
response = StreamingHttpResponse(stream, content_type="text/csv")
|
||||
response["Content-Disposition"] = 'attachment; filename="{}.csv"'.format(
|
||||
self.get_filename()
|
||||
)
|
||||
return response
|
||||
|
||||
def as_spreadsheet(self, queryset, spreadsheet_format):
|
||||
"""Return a response with a spreadsheet representing the exported data from queryset, in the format specified"""
|
||||
if spreadsheet_format == self.FORMAT_CSV:
|
||||
return self.write_csv_response(queryset)
|
||||
elif spreadsheet_format == self.FORMAT_XLSX:
|
||||
return self.write_xlsx_response(queryset)
|
||||
|
||||
def get_export_url(self, format):
|
||||
params = self.request.GET.copy()
|
||||
params["export"] = format
|
||||
return self.request.path + "?" + params.urlencode()
|
||||
|
||||
@property
|
||||
def xlsx_export_url(self):
|
||||
return self.get_export_url("xlsx")
|
||||
|
||||
@property
|
||||
def csv_export_url(self):
|
||||
return self.get_export_url("csv")
|
||||
|
||||
@cached_property
|
||||
def show_export_buttons(self):
|
||||
return bool(self.list_export)
|
||||
|
||||
@cached_property
|
||||
def header_more_buttons(self):
|
||||
buttons = super().header_more_buttons.copy()
|
||||
if self.show_export_buttons:
|
||||
buttons.append(
|
||||
Button(
|
||||
_("Download XLSX"),
|
||||
url=self.xlsx_export_url,
|
||||
icon_name="download",
|
||||
priority=90,
|
||||
)
|
||||
)
|
||||
buttons.append(
|
||||
Button(
|
||||
_("Download CSV"),
|
||||
url=self.csv_export_url,
|
||||
icon_name="download",
|
||||
priority=100,
|
||||
)
|
||||
)
|
||||
|
||||
return buttons
|
||||
94
env/lib/python3.10/site-packages/wagtail/admin/views/page_privacy.py
vendored
Normal file
94
env/lib/python3.10/site-packages/wagtail/admin/views/page_privacy.py
vendored
Normal file
@@ -0,0 +1,94 @@
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from wagtail.admin.forms.pages import PageViewRestrictionForm
|
||||
from wagtail.admin.modal_workflow import render_modal_workflow
|
||||
from wagtail.models import Page, PageViewRestriction
|
||||
|
||||
|
||||
def set_privacy(request, page_id):
|
||||
page = get_object_or_404(Page, id=page_id).specific_deferred
|
||||
page_perms = page.permissions_for_user(request.user)
|
||||
if not page_perms.can_set_view_restrictions():
|
||||
raise PermissionDenied
|
||||
|
||||
# fetch restriction records in depth order so that ancestors appear first
|
||||
restrictions = page.get_view_restrictions().order_by("page__depth")
|
||||
if restrictions:
|
||||
restriction = restrictions[0]
|
||||
restriction_exists_on_ancestor = restriction.page.id != page.id
|
||||
else:
|
||||
restriction = None
|
||||
restriction_exists_on_ancestor = False
|
||||
|
||||
if request.method == "POST":
|
||||
form = PageViewRestrictionForm(
|
||||
request.POST,
|
||||
instance=restriction,
|
||||
private_page_options=page.private_page_options,
|
||||
)
|
||||
if form.is_valid() and not restriction_exists_on_ancestor:
|
||||
if form.cleaned_data["restriction_type"] == PageViewRestriction.NONE:
|
||||
# remove any existing restriction
|
||||
if restriction:
|
||||
restriction.delete(user=request.user)
|
||||
else:
|
||||
restriction = form.save(commit=False)
|
||||
restriction.page = page
|
||||
restriction.save(user=request.user)
|
||||
# Save the groups many-to-many field
|
||||
form.save_m2m()
|
||||
|
||||
return render_modal_workflow(
|
||||
request,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
json_data={
|
||||
"step": "set_privacy_done",
|
||||
"is_public": (form.cleaned_data["restriction_type"] == "none"),
|
||||
},
|
||||
)
|
||||
|
||||
else: # request is a GET
|
||||
if not restriction_exists_on_ancestor:
|
||||
if restriction:
|
||||
form = PageViewRestrictionForm(
|
||||
instance=restriction, private_page_options=page.private_page_options
|
||||
)
|
||||
else:
|
||||
# no current view restrictions on this page
|
||||
form = PageViewRestrictionForm(
|
||||
initial={"restriction_type": "none"},
|
||||
private_page_options=page.private_page_options,
|
||||
)
|
||||
|
||||
if restriction_exists_on_ancestor:
|
||||
# display a message indicating that there is a restriction at ancestor level -
|
||||
# do not provide the form for setting up new restrictions
|
||||
return render_modal_workflow(
|
||||
request,
|
||||
"wagtailadmin/page_privacy/ancestor_privacy.html",
|
||||
None,
|
||||
{
|
||||
"page_with_restriction": restriction.page,
|
||||
},
|
||||
)
|
||||
elif len(page.private_page_options) == 0:
|
||||
return render_modal_workflow(
|
||||
request,
|
||||
"wagtailadmin/page_privacy/no_privacy.html",
|
||||
None,
|
||||
)
|
||||
else:
|
||||
# no restriction set at ancestor level - can set restrictions here
|
||||
return render_modal_workflow(
|
||||
request,
|
||||
"wagtailadmin/page_privacy/set_privacy.html",
|
||||
None,
|
||||
{
|
||||
"page": page,
|
||||
"form": form,
|
||||
},
|
||||
json_data={"step": "set_privacy"},
|
||||
)
|
||||
0
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__init__.py
vendored
Normal file
0
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__init__.py
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/__init__.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/choose_parent.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/choose_parent.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/convert_alias.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/convert_alias.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/copy.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/copy.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/create.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/create.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/delete.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/delete.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/edit.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/edit.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/history.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/history.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/listing.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/listing.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/lock.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/lock.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/move.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/move.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/ordering.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/ordering.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/preview.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/preview.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/revisions.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/revisions.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/search.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/search.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/unpublish.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/unpublish.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/usage.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/usage.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/utils.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/utils.cpython-310.pyc
vendored
Normal file
Binary file not shown.
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/workflow.cpython-310.pyc
vendored
Normal file
BIN
env/lib/python3.10/site-packages/wagtail/admin/views/pages/__pycache__/workflow.cpython-310.pyc
vendored
Normal file
Binary file not shown.
11
env/lib/python3.10/site-packages/wagtail/admin/views/pages/bulk_actions/__init__.py
vendored
Normal file
11
env/lib/python3.10/site-packages/wagtail/admin/views/pages/bulk_actions/__init__.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
from .delete import DeleteBulkAction
|
||||
from .move import MoveBulkAction
|
||||
from .publish import PublishBulkAction
|
||||
from .unpublish import UnpublishBulkAction
|
||||
|
||||
__all__ = [
|
||||
"DeleteBulkAction",
|
||||
"MoveBulkAction",
|
||||
"PublishBulkAction",
|
||||
"UnpublishBulkAction",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
57
env/lib/python3.10/site-packages/wagtail/admin/views/pages/bulk_actions/delete.py
vendored
Normal file
57
env/lib/python3.10/site-packages/wagtail/admin/views/pages/bulk_actions/delete.py
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import ngettext
|
||||
|
||||
from wagtail.admin.views.pages.bulk_actions.page_bulk_action import PageBulkAction
|
||||
|
||||
|
||||
class DeleteBulkAction(PageBulkAction):
|
||||
display_name = _("Delete")
|
||||
action_type = "delete"
|
||||
aria_label = _("Delete selected pages")
|
||||
template_name = "wagtailadmin/pages/bulk_actions/confirm_bulk_delete.html"
|
||||
action_priority = 30
|
||||
classes = {"serious"}
|
||||
|
||||
def check_perm(self, page):
|
||||
return page.permissions_for_user(self.request.user).can_delete()
|
||||
|
||||
def object_context(self, page):
|
||||
return {
|
||||
"item": page,
|
||||
"descendant_count": page.get_descendant_count(),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def execute_action(cls, objects, user=None, **kwargs):
|
||||
num_parent_objects, num_child_objects = 0, 0
|
||||
for page in objects:
|
||||
num_parent_objects += 1
|
||||
num_child_objects += page.get_descendant_count()
|
||||
page.delete(user=user)
|
||||
return num_parent_objects, num_child_objects
|
||||
|
||||
def get_success_message(self, num_parent_objects, num_child_objects):
|
||||
if num_parent_objects == 1:
|
||||
if num_child_objects == 0:
|
||||
success_message = _("1 page has been deleted")
|
||||
else:
|
||||
success_message = ngettext(
|
||||
"1 page and %(num_child_objects)d child page have been deleted",
|
||||
"1 page and %(num_child_objects)d child pages have been deleted",
|
||||
num_child_objects,
|
||||
) % {"num_child_objects": num_child_objects}
|
||||
else:
|
||||
if num_child_objects == 0:
|
||||
success_message = _(
|
||||
"%(num_parent_objects)d pages have been deleted"
|
||||
) % {"num_parent_objects": num_parent_objects}
|
||||
else:
|
||||
success_message = ngettext(
|
||||
"%(num_parent_objects)d pages and %(num_child_objects)d child page have been deleted",
|
||||
"%(num_parent_objects)d pages and %(num_child_objects)d child pages have been deleted",
|
||||
num_child_objects,
|
||||
) % {
|
||||
"num_child_objects": num_child_objects,
|
||||
"num_parent_objects": num_parent_objects,
|
||||
}
|
||||
return success_message
|
||||
158
env/lib/python3.10/site-packages/wagtail/admin/views/pages/bulk_actions/move.py
vendored
Normal file
158
env/lib/python3.10/site-packages/wagtail/admin/views/pages/bulk_actions/move.py
vendored
Normal file
@@ -0,0 +1,158 @@
|
||||
from django import forms
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import ngettext
|
||||
|
||||
from wagtail.admin import widgets
|
||||
from wagtail.admin.views.pages.bulk_actions.page_bulk_action import PageBulkAction
|
||||
from wagtail.models import Page
|
||||
|
||||
|
||||
class MoveForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
destination = kwargs.pop("destination")
|
||||
target_parent_models = kwargs.pop("target_parent_models")
|
||||
pages_to_move = kwargs.pop("pages_to_move")
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["chooser"] = forms.ModelChoiceField(
|
||||
initial=destination,
|
||||
queryset=Page.objects.all(),
|
||||
widget=widgets.AdminPageMoveChooser(
|
||||
can_choose_root=True,
|
||||
user_perms="bulk_move_to",
|
||||
target_models=target_parent_models,
|
||||
pages_to_move=pages_to_move,
|
||||
),
|
||||
label=_("Select a new parent page"),
|
||||
)
|
||||
|
||||
|
||||
class MoveBulkAction(PageBulkAction):
|
||||
display_name = _("Move")
|
||||
action_type = "move"
|
||||
aria_label = _("Move selected pages")
|
||||
template_name = "wagtailadmin/pages/bulk_actions/confirm_bulk_move.html"
|
||||
action_priority = 10
|
||||
form_class = MoveForm
|
||||
destination = None
|
||||
|
||||
def __init__(self, request, model):
|
||||
super().__init__(request, model)
|
||||
self.target_parent_models = set()
|
||||
self.pages_to_move = []
|
||||
|
||||
def get_form_kwargs(self):
|
||||
ctx = super().get_form_kwargs()
|
||||
ctx["destination"] = self.destination or Page.get_first_root_node()
|
||||
ctx["target_parent_models"] = self.target_parent_models
|
||||
ctx["pages_to_move"] = self.pages_to_move
|
||||
return ctx
|
||||
|
||||
def check_perm(self, page):
|
||||
return page.permissions_for_user(self.request.user).can_move()
|
||||
|
||||
def get_success_message(self, num_parent_objects, num_child_objects):
|
||||
success_message = ngettext(
|
||||
"%(num_pages)d page has been moved",
|
||||
"%(num_pages)d pages have been moved",
|
||||
num_parent_objects,
|
||||
) % {"num_pages": num_parent_objects}
|
||||
return success_message
|
||||
|
||||
def object_context(self, obj):
|
||||
context = super().object_context(obj)
|
||||
context["child_pages"] = context["item"].get_descendants().count()
|
||||
return context
|
||||
|
||||
def get_actionable_objects(self):
|
||||
objects, objects_without_access = super().get_actionable_objects()
|
||||
request = self.request
|
||||
|
||||
if objects:
|
||||
self.target_parent_models = set(
|
||||
objects[0].specific_class.allowed_parent_page_models()
|
||||
)
|
||||
for obj in objects:
|
||||
self.target_parent_models.intersection_update(
|
||||
set(obj.specific_class.allowed_parent_page_models())
|
||||
)
|
||||
|
||||
self.pages_to_move = [page.id for page in objects]
|
||||
|
||||
if self.cleaned_form is None:
|
||||
if len(self.target_parent_models) == 0:
|
||||
return [], {
|
||||
**objects_without_access,
|
||||
"pages_without_common_parent_page": [
|
||||
{
|
||||
"item": page,
|
||||
"can_edit": page.permissions_for_user(
|
||||
self.request.user
|
||||
).can_edit(),
|
||||
}
|
||||
for page in objects
|
||||
],
|
||||
}
|
||||
return objects, objects_without_access
|
||||
|
||||
destination = self.cleaned_form.cleaned_data["chooser"]
|
||||
pages = []
|
||||
pages_without_destination_access = []
|
||||
pages_with_duplicate_slugs = []
|
||||
|
||||
for page in objects:
|
||||
if not page.permissions_for_user(request.user).can_move_to(destination):
|
||||
pages_without_destination_access.append(page)
|
||||
elif not Page._slug_is_available(page.slug, destination, page=page):
|
||||
pages_with_duplicate_slugs.append(page)
|
||||
else:
|
||||
pages.append(page)
|
||||
|
||||
return pages, {
|
||||
**objects_without_access,
|
||||
"pages_without_destination_access": [
|
||||
{
|
||||
"item": page,
|
||||
"can_edit": page.permissions_for_user(self.request.user).can_edit(),
|
||||
}
|
||||
for page in pages_without_destination_access
|
||||
],
|
||||
"pages_with_duplicate_slugs": [
|
||||
{
|
||||
"item": page,
|
||||
"can_edit": page.permissions_for_user(self.request.user).can_edit(),
|
||||
}
|
||||
for page in pages_with_duplicate_slugs
|
||||
],
|
||||
}
|
||||
|
||||
def prepare_action(self, pages, pages_without_access):
|
||||
request = self.request
|
||||
destination = self.cleaned_form.cleaned_data["chooser"]
|
||||
if (
|
||||
pages_without_access["pages_without_destination_access"]
|
||||
or pages_without_access["pages_with_duplicate_slugs"]
|
||||
):
|
||||
# this will be picked up by the form
|
||||
self.destination = destination
|
||||
return TemplateResponse(
|
||||
request,
|
||||
self.template_name,
|
||||
{"destination": destination, **self.get_context_data()},
|
||||
)
|
||||
|
||||
def get_execution_context(self):
|
||||
return {
|
||||
**super().get_execution_context(),
|
||||
"destination": self.cleaned_form.cleaned_data["chooser"],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def execute_action(cls, objects, destination=None, user=None, **kwargs):
|
||||
num_parent_objects = 0
|
||||
if destination is None:
|
||||
return
|
||||
for page in objects:
|
||||
page.move(destination, pos="last-child", user=user)
|
||||
num_parent_objects += 1
|
||||
return num_parent_objects, 0
|
||||
57
env/lib/python3.10/site-packages/wagtail/admin/views/pages/bulk_actions/page_bulk_action.py
vendored
Normal file
57
env/lib/python3.10/site-packages/wagtail/admin/views/pages/bulk_actions/page_bulk_action.py
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
from django import forms
|
||||
|
||||
from wagtail.admin.views.bulk_action import BulkAction
|
||||
from wagtail.admin.views.pages.search import page_filter_search
|
||||
from wagtail.models import Page
|
||||
|
||||
|
||||
class DefaultPageForm(forms.Form):
|
||||
include_descendants = forms.BooleanField(required=False)
|
||||
|
||||
|
||||
class PageBulkAction(BulkAction):
|
||||
models = [Page]
|
||||
form_class = DefaultPageForm
|
||||
|
||||
def get_all_objects_in_listing_query(self, parent_id):
|
||||
listing_objects = self.model.objects.all()
|
||||
|
||||
q = None
|
||||
if "q" in self.request.GET:
|
||||
q = self.request.GET.get("q", "")
|
||||
|
||||
if parent_id is not None:
|
||||
listing_objects = listing_objects.get(id=parent_id)
|
||||
# If we're searching, include the descendants as well.
|
||||
# Otherwise, just include the direct children.
|
||||
if q:
|
||||
listing_objects = listing_objects.get_descendants()
|
||||
else:
|
||||
listing_objects = listing_objects.get_children()
|
||||
|
||||
listing_objects = listing_objects.values_list("pk", flat=True)
|
||||
|
||||
if q:
|
||||
listing_objects = page_filter_search(q, listing_objects)[0].results()
|
||||
|
||||
return listing_objects
|
||||
|
||||
def object_context(self, obj):
|
||||
context = super().object_context(obj)
|
||||
# Make 'item' into the specific instance, so that custom get_admin_display_title methods are respected
|
||||
context["item"] = context["item"].specific_deferred
|
||||
return context
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["items_with_no_access"] = [
|
||||
{
|
||||
"item": page,
|
||||
"can_edit": page.permissions_for_user(self.request.user).can_edit(),
|
||||
}
|
||||
for page in context["items_with_no_access"]
|
||||
]
|
||||
return context
|
||||
|
||||
def get_execution_context(self):
|
||||
return {"user": self.request.user}
|
||||
105
env/lib/python3.10/site-packages/wagtail/admin/views/pages/bulk_actions/publish.py
vendored
Normal file
105
env/lib/python3.10/site-packages/wagtail/admin/views/pages/bulk_actions/publish.py
vendored
Normal file
@@ -0,0 +1,105 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import ngettext
|
||||
|
||||
from wagtail.admin.views.pages.bulk_actions.page_bulk_action import PageBulkAction
|
||||
|
||||
|
||||
class PublishBulkAction(PageBulkAction):
|
||||
display_name = _("Publish")
|
||||
action_type = "publish"
|
||||
aria_label = _("Publish selected pages")
|
||||
template_name = "wagtailadmin/pages/bulk_actions/confirm_bulk_publish.html"
|
||||
action_priority = 40
|
||||
|
||||
def check_perm(self, page):
|
||||
return page.permissions_for_user(self.request.user).can_publish()
|
||||
|
||||
def object_context(self, obj):
|
||||
context = super().object_context(obj)
|
||||
context["draft_descendant_count"] = (
|
||||
context["item"].get_descendants().not_live().count()
|
||||
)
|
||||
return context
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["has_draft_descendants"] = any(
|
||||
item["draft_descendant_count"] for item in context["items"]
|
||||
)
|
||||
return context
|
||||
|
||||
def get_execution_context(self):
|
||||
return {
|
||||
**super().get_execution_context(),
|
||||
"include_descendants": self.cleaned_form.cleaned_data[
|
||||
"include_descendants"
|
||||
],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def execute_action(cls, objects, include_descendants=False, user=None, **kwargs):
|
||||
num_parent_objects, num_child_objects = 0, 0
|
||||
for page in objects:
|
||||
revision = page.get_latest_revision() or page.specific.save_revision(
|
||||
user=user
|
||||
)
|
||||
revision.publish(user=user)
|
||||
num_parent_objects += 1
|
||||
|
||||
if include_descendants:
|
||||
for draft_descendant_page in (
|
||||
page.get_descendants()
|
||||
.not_live()
|
||||
.defer_streamfields()
|
||||
.specific()
|
||||
.iterator()
|
||||
):
|
||||
if (
|
||||
user is None
|
||||
or draft_descendant_page.permissions_for_user(
|
||||
user
|
||||
).can_publish()
|
||||
):
|
||||
draft_descendant_revision = (
|
||||
draft_descendant_page.get_latest_revision()
|
||||
or draft_descendant_page.save_revision(user=user)
|
||||
)
|
||||
draft_descendant_revision.publish(user=user)
|
||||
num_child_objects += 1
|
||||
|
||||
return num_parent_objects, num_child_objects
|
||||
|
||||
def get_success_message(self, num_parent_objects, num_child_objects):
|
||||
include_descendants = self.cleaned_form.cleaned_data["include_descendants"]
|
||||
if num_parent_objects == 1:
|
||||
if include_descendants:
|
||||
if num_child_objects == 0:
|
||||
success_message = _("1 page has been published")
|
||||
else:
|
||||
success_message = ngettext(
|
||||
"1 page and %(num_child_objects)d child page have been published",
|
||||
"1 page and %(num_child_objects)d child pages have been published",
|
||||
num_child_objects,
|
||||
) % {"num_child_objects": num_child_objects}
|
||||
else:
|
||||
success_message = _("1 page has been published")
|
||||
else:
|
||||
if include_descendants:
|
||||
if num_child_objects == 0:
|
||||
success_message = _(
|
||||
"%(num_parent_objects)d pages have been published"
|
||||
) % {"num_parent_objects": num_parent_objects}
|
||||
else:
|
||||
success_message = ngettext(
|
||||
"%(num_parent_objects)d pages and %(num_child_objects)d child page have been published",
|
||||
"%(num_parent_objects)d pages and %(num_child_objects)d child pages have been published",
|
||||
num_child_objects,
|
||||
) % {
|
||||
"num_child_objects": num_child_objects,
|
||||
"num_parent_objects": num_parent_objects,
|
||||
}
|
||||
else:
|
||||
success_message = _(
|
||||
"%(num_parent_objects)d pages have been published"
|
||||
) % {"num_parent_objects": num_parent_objects}
|
||||
return success_message
|
||||
99
env/lib/python3.10/site-packages/wagtail/admin/views/pages/bulk_actions/unpublish.py
vendored
Normal file
99
env/lib/python3.10/site-packages/wagtail/admin/views/pages/bulk_actions/unpublish.py
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import ngettext
|
||||
|
||||
from wagtail.admin.views.pages.bulk_actions.page_bulk_action import PageBulkAction
|
||||
|
||||
|
||||
class UnpublishBulkAction(PageBulkAction):
|
||||
display_name = _("Unpublish")
|
||||
action_type = "unpublish"
|
||||
aria_label = _("Unpublish selected pages")
|
||||
template_name = "wagtailadmin/pages/bulk_actions/confirm_bulk_unpublish.html"
|
||||
action_priority = 50
|
||||
|
||||
def check_perm(self, page):
|
||||
return page.permissions_for_user(self.request.user).can_unpublish()
|
||||
|
||||
def object_context(self, page):
|
||||
return {
|
||||
**super().object_context(page),
|
||||
"live_descendant_count": page.get_descendants().live().count(),
|
||||
}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["has_live_descendants"] = any(
|
||||
item["live_descendant_count"] > 0 for item in context["items"]
|
||||
)
|
||||
return context
|
||||
|
||||
def get_execution_context(self):
|
||||
return {
|
||||
**super().get_execution_context(),
|
||||
"permission_checker": self.check_perm,
|
||||
"include_descendants": self.cleaned_form.cleaned_data[
|
||||
"include_descendants"
|
||||
],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def execute_action(
|
||||
cls,
|
||||
objects,
|
||||
include_descendants=False,
|
||||
user=None,
|
||||
permission_checker=None,
|
||||
**kwargs,
|
||||
):
|
||||
num_parent_objects, num_child_objects = 0, 0
|
||||
for page in objects:
|
||||
page.unpublish(user=user)
|
||||
num_parent_objects += 1
|
||||
|
||||
if include_descendants:
|
||||
for live_descendant_page in (
|
||||
page.get_descendants()
|
||||
.live()
|
||||
.defer_streamfields()
|
||||
.specific()
|
||||
.iterator()
|
||||
):
|
||||
if user is None or permission_checker(live_descendant_page):
|
||||
live_descendant_page.unpublish()
|
||||
num_child_objects += 1
|
||||
return num_parent_objects, num_child_objects
|
||||
|
||||
def get_success_message(self, num_parent_objects, num_child_objects):
|
||||
include_descendants = self.cleaned_form.cleaned_data["include_descendants"]
|
||||
if num_parent_objects == 1:
|
||||
if include_descendants:
|
||||
if num_child_objects == 0:
|
||||
success_message = _("1 page has been unpublished")
|
||||
else:
|
||||
success_message = ngettext(
|
||||
"1 page and %(num_child_objects)d child page have been unpublished",
|
||||
"1 page and %(num_child_objects)d child pages have been unpublished",
|
||||
num_child_objects,
|
||||
) % {"num_child_objects": num_child_objects}
|
||||
else:
|
||||
success_message = _("1 page has been unpublished")
|
||||
else:
|
||||
if include_descendants:
|
||||
if num_child_objects == 0:
|
||||
success_message = _(
|
||||
"%(num_parent_objects)d pages have been unpublished"
|
||||
) % {"num_parent_objects": num_parent_objects}
|
||||
else:
|
||||
success_message = ngettext(
|
||||
"%(num_parent_objects)d pages and %(num_child_objects)d child page have been unpublished",
|
||||
"%(num_parent_objects)d pages and %(num_child_objects)d child pages have been unpublished",
|
||||
num_child_objects,
|
||||
) % {
|
||||
"num_child_objects": num_child_objects,
|
||||
"num_parent_objects": num_parent_objects,
|
||||
}
|
||||
else:
|
||||
success_message = _(
|
||||
"%(num_parent_objects)d pages have been unpublished"
|
||||
) % {"num_parent_objects": num_parent_objects}
|
||||
return success_message
|
||||
135
env/lib/python3.10/site-packages/wagtail/admin/views/pages/choose_parent.py
vendored
Normal file
135
env/lib/python3.10/site-packages/wagtail/admin/views/pages/choose_parent.py
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
from django.contrib.admin.utils import quote
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
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 django.views.generic import FormView
|
||||
|
||||
from wagtail.admin.forms.pages import ParentChooserForm
|
||||
from wagtail.admin.views.generic.base import WagtailAdminTemplateMixin
|
||||
from wagtail.models import Page
|
||||
from wagtail.permissions import page_permission_policy
|
||||
|
||||
|
||||
class ChooseParentView(WagtailAdminTemplateMixin, FormView):
|
||||
template_name = "wagtailadmin/pages/choose_parent.html"
|
||||
model = Page
|
||||
index_url_name = None
|
||||
page_title = gettext_lazy("Choose parent")
|
||||
|
||||
def get_valid_parent_pages(self, user):
|
||||
"""
|
||||
Identifies possible parent pages for the current user by first looking
|
||||
at allowed_parent_page_models() on self.model to limit options to the
|
||||
correct type of page, then checking permissions on those individual
|
||||
pages to make sure we have permission to add a subpage to it.
|
||||
"""
|
||||
# Get queryset of pages where this page type can be added
|
||||
allowed_parent_page_content_types = list(
|
||||
ContentType.objects.get_for_models(
|
||||
*self.model.allowed_parent_page_models()
|
||||
).values()
|
||||
)
|
||||
allowed_parent_pages = Page.objects.filter(
|
||||
content_type__in=allowed_parent_page_content_types
|
||||
)
|
||||
|
||||
# Get queryset of pages where the user has permission to add subpages
|
||||
if user.is_superuser:
|
||||
pages_where_user_can_add = Page.objects.all()
|
||||
else:
|
||||
pages_where_user_can_add = Page.objects.none()
|
||||
|
||||
perms = {
|
||||
perm
|
||||
for perm in page_permission_policy.get_cached_permissions_for_user(user)
|
||||
if perm.permission.codename == "add_page"
|
||||
}
|
||||
|
||||
for perm in perms:
|
||||
# user has add permission on any subpage of perm.page
|
||||
# (including perm.page itself)
|
||||
pages_where_user_can_add |= Page.objects.descendant_of(
|
||||
perm.page, inclusive=True
|
||||
)
|
||||
|
||||
# Combine them
|
||||
return allowed_parent_pages & pages_where_user_can_add
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
parents = self.get_valid_parent_pages(request.user)
|
||||
|
||||
# Only fetch the IDs for the first two parents to check if there's only
|
||||
# one valid parent, and if so, redirect to the add page view with that
|
||||
# parent pre-selected
|
||||
parent_ids = parents.values_list("pk", flat=True)[:2]
|
||||
if len(parent_ids) == 1:
|
||||
parent_id = quote(parent_ids[0])
|
||||
model_opts = self.model._meta
|
||||
return redirect(
|
||||
"wagtailadmin_pages:add",
|
||||
model_opts.app_label,
|
||||
model_opts.model_name,
|
||||
parent_id,
|
||||
)
|
||||
|
||||
# The page can be added in multiple places, so proceed with rendering
|
||||
# the view's form so that the parent can be specified
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_form(self):
|
||||
if self.request.method == "POST":
|
||||
return ParentChooserForm(self.model, self.request.user, self.request.POST)
|
||||
return ParentChooserForm(self.model, self.request.user)
|
||||
|
||||
def get_index_url(self):
|
||||
if self.index_url_name:
|
||||
return reverse(self.index_url_name)
|
||||
|
||||
def get_breadcrumbs_items(self):
|
||||
items = []
|
||||
index_url = self.get_index_url()
|
||||
if index_url:
|
||||
items.append(
|
||||
{
|
||||
"url": index_url,
|
||||
"label": capfirst(self.model._meta.verbose_name_plural),
|
||||
}
|
||||
)
|
||||
items.append(
|
||||
{
|
||||
"url": "",
|
||||
"label": self.get_page_title(),
|
||||
"sublabel": self.get_page_subtitle(),
|
||||
}
|
||||
)
|
||||
|
||||
return self.breadcrumbs_items + items
|
||||
|
||||
def get_page_subtitle(self):
|
||||
return self.model.get_verbose_name()
|
||||
|
||||
@cached_property
|
||||
def submit_button_label(self):
|
||||
return _("Create a new %(model_name)s") % {
|
||||
"model_name": self.model._meta.verbose_name,
|
||||
}
|
||||
|
||||
def form_valid(self, form):
|
||||
model_opts = self.model._meta
|
||||
parent_id = quote(form.cleaned_data["parent_page"].pk)
|
||||
return redirect(
|
||||
"wagtailadmin_pages:add",
|
||||
model_opts.app_label,
|
||||
model_opts.model_name,
|
||||
parent_id,
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["media"] = context["form"].media
|
||||
context["submit_button_label"] = self.submit_button_label
|
||||
return context
|
||||
53
env/lib/python3.10/site-packages/wagtail/admin/views/pages/convert_alias.py
vendored
Normal file
53
env/lib/python3.10/site-packages/wagtail/admin/views/pages/convert_alias.py
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from wagtail import hooks
|
||||
from wagtail.actions.convert_alias import ConvertAliasPageAction
|
||||
from wagtail.admin import messages
|
||||
from wagtail.admin.utils import get_valid_next_url_from_request
|
||||
from wagtail.models import Page
|
||||
|
||||
|
||||
def convert_alias(request, page_id):
|
||||
page = get_object_or_404(Page, id=page_id, alias_of_id__isnull=False).specific
|
||||
if not page.permissions_for_user(request.user).can_edit():
|
||||
raise PermissionDenied
|
||||
|
||||
with transaction.atomic():
|
||||
for fn in hooks.get_hooks("before_convert_alias_page"):
|
||||
result = fn(request, page)
|
||||
if hasattr(result, "status_code"):
|
||||
return result
|
||||
|
||||
next_url = get_valid_next_url_from_request(request)
|
||||
|
||||
if request.method == "POST":
|
||||
action = ConvertAliasPageAction(page, user=request.user)
|
||||
action.execute(skip_permission_checks=True)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
_("Page '%(page_title)s' has been converted into an ordinary page.")
|
||||
% {"page_title": page.get_admin_display_title()},
|
||||
)
|
||||
|
||||
for fn in hooks.get_hooks("after_convert_alias_page"):
|
||||
result = fn(request, page)
|
||||
if hasattr(result, "status_code"):
|
||||
return result
|
||||
|
||||
if next_url:
|
||||
return redirect(next_url)
|
||||
return redirect("wagtailadmin_pages:edit", page.id)
|
||||
|
||||
return TemplateResponse(
|
||||
request,
|
||||
"wagtailadmin/pages/confirm_convert_alias.html",
|
||||
{
|
||||
"page": page,
|
||||
"next": next_url,
|
||||
},
|
||||
)
|
||||
115
env/lib/python3.10/site-packages/wagtail/admin/views/pages/copy.py
vendored
Normal file
115
env/lib/python3.10/site-packages/wagtail/admin/views/pages/copy.py
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
from django.shortcuts import redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from wagtail import hooks
|
||||
from wagtail.actions.copy_page import CopyPageAction
|
||||
from wagtail.actions.create_alias import CreatePageAliasAction
|
||||
from wagtail.admin import messages
|
||||
from wagtail.admin.auth import user_has_any_page_permission, user_passes_test
|
||||
from wagtail.admin.forms.pages import CopyForm
|
||||
from wagtail.admin.utils import get_valid_next_url_from_request
|
||||
from wagtail.models import Page
|
||||
|
||||
|
||||
@user_passes_test(user_has_any_page_permission)
|
||||
def copy(request, page_id):
|
||||
page = Page.objects.get(id=page_id)
|
||||
|
||||
# Parent page defaults to parent of source page
|
||||
parent_page = page.get_parent()
|
||||
|
||||
# Check if the user has permission to publish subpages on the parent
|
||||
can_publish = parent_page.permissions_for_user(request.user).can_publish_subpage()
|
||||
|
||||
form_class = getattr(page.specific_class, "copy_form_class", CopyForm)
|
||||
form = form_class(
|
||||
request.POST or None, user=request.user, page=page, can_publish=can_publish
|
||||
)
|
||||
|
||||
next_url = get_valid_next_url_from_request(request)
|
||||
|
||||
for fn in hooks.get_hooks("before_copy_page"):
|
||||
result = fn(request, page)
|
||||
if hasattr(result, "status_code"):
|
||||
return result
|
||||
|
||||
# Check if user is submitting
|
||||
if request.method == "POST":
|
||||
# Prefill parent_page in case the form is invalid (as prepopulated value for the form field,
|
||||
# because ModelChoiceField seems to not fall back to the user given value)
|
||||
parent_page = Page.objects.get(id=request.POST["new_parent_page"])
|
||||
|
||||
if form.is_valid():
|
||||
# Receive the parent page (this should never be empty)
|
||||
if form.cleaned_data["new_parent_page"]:
|
||||
parent_page = form.cleaned_data["new_parent_page"]
|
||||
|
||||
# Re-check if the user has permission to publish subpages on the new parent
|
||||
can_publish = parent_page.permissions_for_user(
|
||||
request.user
|
||||
).can_publish_subpage()
|
||||
keep_live = can_publish and form.cleaned_data.get("publish_copies")
|
||||
|
||||
# Copy the page
|
||||
# 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.
|
||||
if can_publish and form.cleaned_data.get("alias"):
|
||||
action = CreatePageAliasAction(
|
||||
page.specific,
|
||||
recursive=form.cleaned_data.get("copy_subpages"),
|
||||
parent=parent_page,
|
||||
update_slug=form.cleaned_data["new_slug"],
|
||||
user=request.user,
|
||||
)
|
||||
new_page = action.execute(skip_permission_checks=True)
|
||||
else:
|
||||
action = CopyPageAction(
|
||||
page=page,
|
||||
recursive=form.cleaned_data.get("copy_subpages"),
|
||||
to=parent_page,
|
||||
update_attrs={
|
||||
"title": form.cleaned_data["new_title"],
|
||||
"slug": form.cleaned_data["new_slug"],
|
||||
},
|
||||
keep_live=keep_live,
|
||||
user=request.user,
|
||||
)
|
||||
new_page = action.execute()
|
||||
|
||||
# Give a success message back to the user
|
||||
if form.cleaned_data.get("copy_subpages"):
|
||||
messages.success(
|
||||
request,
|
||||
_("Page '%(page_title)s' and %(subpages_count)s subpages copied.")
|
||||
% {
|
||||
"page_title": page.specific_deferred.get_admin_display_title(),
|
||||
"subpages_count": new_page.get_descendants().count(),
|
||||
},
|
||||
)
|
||||
else:
|
||||
messages.success(
|
||||
request,
|
||||
_("Page '%(page_title)s' copied.")
|
||||
% {"page_title": page.specific_deferred.get_admin_display_title()},
|
||||
)
|
||||
|
||||
for fn in hooks.get_hooks("after_copy_page"):
|
||||
result = fn(request, page, new_page)
|
||||
if hasattr(result, "status_code"):
|
||||
return result
|
||||
|
||||
# Redirect to explore of parent page
|
||||
if next_url:
|
||||
return redirect(next_url)
|
||||
return redirect("wagtailadmin_explore", parent_page.id)
|
||||
|
||||
return TemplateResponse(
|
||||
request,
|
||||
"wagtailadmin/pages/copy.html",
|
||||
{
|
||||
"page": page,
|
||||
"form": form,
|
||||
"next": next_url,
|
||||
},
|
||||
)
|
||||
454
env/lib/python3.10/site-packages/wagtail/admin/views/pages/create.py
vendored
Normal file
454
env/lib/python3.10/site-packages/wagtail/admin/views/pages/create.py
vendored
Normal file
@@ -0,0 +1,454 @@
|
||||
from urllib.parse import quote, urlencode
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy
|
||||
from django.views.generic.base import View
|
||||
|
||||
from wagtail.admin import messages, signals
|
||||
from wagtail.admin.action_menu import PageActionMenu
|
||||
from wagtail.admin.ui.components import MediaContainer
|
||||
from wagtail.admin.ui.side_panels import (
|
||||
ChecksSidePanel,
|
||||
CommentsSidePanel,
|
||||
PageStatusSidePanel,
|
||||
PreviewSidePanel,
|
||||
)
|
||||
from wagtail.admin.utils import get_valid_next_url_from_request
|
||||
from wagtail.admin.views.generic import HookResponseMixin
|
||||
from wagtail.admin.views.generic.base import WagtailAdminTemplateMixin
|
||||
from wagtail.models import Locale, Page, PageSubscription
|
||||
|
||||
|
||||
def add_subpage(request, parent_page_id):
|
||||
parent_page = get_object_or_404(Page, id=parent_page_id).specific
|
||||
if not parent_page.permissions_for_user(request.user).can_add_subpage():
|
||||
raise PermissionDenied
|
||||
|
||||
page_types = [
|
||||
(
|
||||
model.get_verbose_name(),
|
||||
model._meta.app_label,
|
||||
model._meta.model_name,
|
||||
model.get_page_description(),
|
||||
)
|
||||
for model in type(parent_page).creatable_subpage_models()
|
||||
if model.can_create_at(parent_page)
|
||||
]
|
||||
# sort by lower-cased version of verbose name
|
||||
page_types.sort(key=lambda page_type: page_type[0].lower())
|
||||
|
||||
if len(page_types) == 1:
|
||||
# Only one page type is available - redirect straight to the create form rather than
|
||||
# making the user choose
|
||||
verbose_name, app_label, model_name, description = page_types[0]
|
||||
return redirect("wagtailadmin_pages:add", app_label, model_name, parent_page.id)
|
||||
|
||||
return TemplateResponse(
|
||||
request,
|
||||
"wagtailadmin/pages/add_subpage.html",
|
||||
{
|
||||
"parent_page": parent_page,
|
||||
"page_types": page_types,
|
||||
"next": get_valid_next_url_from_request(request),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class CreateView(WagtailAdminTemplateMixin, HookResponseMixin, View):
|
||||
template_name = "wagtailadmin/pages/create.html"
|
||||
page_title = gettext_lazy("New")
|
||||
|
||||
def dispatch(
|
||||
self, request, content_type_app_name, content_type_model_name, parent_page_id
|
||||
):
|
||||
self.parent_page = get_object_or_404(Page, id=parent_page_id).specific
|
||||
self.parent_page_perms = self.parent_page.permissions_for_user(
|
||||
self.request.user
|
||||
)
|
||||
if not self.parent_page_perms.can_add_subpage():
|
||||
raise PermissionDenied
|
||||
|
||||
try:
|
||||
self.page_content_type = ContentType.objects.get_by_natural_key(
|
||||
content_type_app_name, content_type_model_name
|
||||
)
|
||||
except ContentType.DoesNotExist:
|
||||
raise Http404
|
||||
|
||||
# Get class
|
||||
self.page_class = self.page_content_type.model_class()
|
||||
|
||||
# Make sure the class is a descendant of Page
|
||||
if not issubclass(self.page_class, Page):
|
||||
raise Http404
|
||||
|
||||
# page must be in the list of allowed subpage types for this parent ID
|
||||
if self.page_class not in self.parent_page.creatable_subpage_models():
|
||||
raise PermissionDenied
|
||||
|
||||
if not self.page_class.can_create_at(self.parent_page):
|
||||
raise PermissionDenied
|
||||
|
||||
response = self.run_hook(
|
||||
"before_create_page", self.request, self.parent_page, self.page_class
|
||||
)
|
||||
if response:
|
||||
return response
|
||||
|
||||
self.locale = self.parent_page.locale
|
||||
self.page = self.page_class(owner=self.request.user)
|
||||
self.page.locale = self.locale
|
||||
|
||||
if getattr(settings, "WAGTAIL_I18N_ENABLED", False):
|
||||
# If the parent page is the root page. The user may specify any locale they like
|
||||
if self.parent_page.is_root():
|
||||
selected_locale = request.GET.get("locale", None) or request.POST.get(
|
||||
"locale", None
|
||||
)
|
||||
if selected_locale:
|
||||
self.locale = get_object_or_404(
|
||||
Locale, language_code=selected_locale
|
||||
)
|
||||
self.page.locale = self.locale
|
||||
self.translations = self.get_translations()
|
||||
else:
|
||||
self.locale = None
|
||||
self.translations = []
|
||||
|
||||
self.edit_handler = self.page_class.get_edit_handler()
|
||||
self.form_class = self.edit_handler.get_form_class()
|
||||
|
||||
# Note: Comment notifications should be enabled by default for pages that a user creates
|
||||
self.subscription = PageSubscription(
|
||||
page=self.page, user=self.request.user, comment_notifications=True
|
||||
)
|
||||
|
||||
self.next_url = get_valid_next_url_from_request(self.request)
|
||||
|
||||
return super().dispatch(request)
|
||||
|
||||
def post(self, request):
|
||||
self.form = self.form_class(
|
||||
self.request.POST,
|
||||
self.request.FILES,
|
||||
instance=self.page,
|
||||
subscription=self.subscription,
|
||||
parent_page=self.parent_page,
|
||||
for_user=self.request.user,
|
||||
)
|
||||
|
||||
if self.form.is_valid():
|
||||
return self.form_valid(self.form)
|
||||
else:
|
||||
return self.form_invalid(self.form)
|
||||
|
||||
def form_valid(self, form):
|
||||
if (
|
||||
bool(self.request.POST.get("action-publish"))
|
||||
and self.parent_page_perms.can_publish_subpage()
|
||||
):
|
||||
return self.publish_action()
|
||||
elif (
|
||||
bool(self.request.POST.get("action-submit"))
|
||||
and self.parent_page.has_workflow
|
||||
):
|
||||
return self.submit_action()
|
||||
else:
|
||||
return self.save_action()
|
||||
|
||||
def get_page_subtitle(self):
|
||||
return self.page_class.get_verbose_name()
|
||||
|
||||
def get_edit_message_button(self):
|
||||
return messages.button(
|
||||
reverse("wagtailadmin_pages:edit", args=(self.page.id,)), _("Edit")
|
||||
)
|
||||
|
||||
def get_view_draft_message_button(self):
|
||||
return messages.button(
|
||||
reverse("wagtailadmin_pages:view_draft", args=(self.page.id,)),
|
||||
_("View draft"),
|
||||
new_window=False,
|
||||
)
|
||||
|
||||
def get_view_live_message_button(self):
|
||||
return messages.button(self.page.url, _("View live"), new_window=False)
|
||||
|
||||
def save_action(self):
|
||||
self.page = self.form.save(commit=False)
|
||||
self.page.live = False
|
||||
|
||||
# Save page
|
||||
self.parent_page.add_child(instance=self.page)
|
||||
|
||||
# Save revision
|
||||
self.page.save_revision(user=self.request.user, log_action=True)
|
||||
|
||||
# Save subscription settings
|
||||
self.subscription.page = self.page
|
||||
self.subscription.save()
|
||||
|
||||
# Notification
|
||||
messages.success(
|
||||
self.request,
|
||||
_("Page '%(page_title)s' created.")
|
||||
% {"page_title": self.page.get_admin_display_title()},
|
||||
)
|
||||
|
||||
response = self.run_hook("after_create_page", self.request, self.page)
|
||||
if response:
|
||||
return response
|
||||
|
||||
# remain on edit page for further edits
|
||||
return self.redirect_and_remain()
|
||||
|
||||
def publish_action(self):
|
||||
self.page = self.form.save(commit=False)
|
||||
|
||||
# Save page
|
||||
self.parent_page.add_child(instance=self.page)
|
||||
|
||||
# Save revision
|
||||
revision = self.page.save_revision(user=self.request.user, log_action=True)
|
||||
|
||||
# Save subscription settings
|
||||
self.subscription.page = self.page
|
||||
self.subscription.save()
|
||||
|
||||
# Publish
|
||||
response = self.run_hook("before_publish_page", self.request, self.page)
|
||||
if response:
|
||||
return response
|
||||
|
||||
revision.publish(user=self.request.user)
|
||||
|
||||
# get a fresh copy so that any changes coming from revision.publish() are passed on
|
||||
self.page.refresh_from_db()
|
||||
|
||||
response = self.run_hook("after_publish_page", self.request, self.page)
|
||||
if response:
|
||||
return response
|
||||
|
||||
# Notification
|
||||
if self.page.go_live_at and self.page.go_live_at > timezone.now():
|
||||
messages.success(
|
||||
self.request,
|
||||
_("Page '%(page_title)s' created and scheduled for publishing.")
|
||||
% {"page_title": self.page.get_admin_display_title()},
|
||||
buttons=[self.get_edit_message_button()],
|
||||
)
|
||||
else:
|
||||
buttons = []
|
||||
if self.page.url is not None:
|
||||
buttons.append(self.get_view_live_message_button())
|
||||
buttons.append(self.get_edit_message_button())
|
||||
messages.success(
|
||||
self.request,
|
||||
_("Page '%(page_title)s' created and published.")
|
||||
% {"page_title": self.page.get_admin_display_title()},
|
||||
buttons=buttons,
|
||||
)
|
||||
|
||||
response = self.run_hook("after_create_page", self.request, self.page)
|
||||
if response:
|
||||
return response
|
||||
|
||||
return self.redirect_away()
|
||||
|
||||
def submit_action(self):
|
||||
self.page = self.form.save(commit=False)
|
||||
self.page.live = False
|
||||
|
||||
# Save page
|
||||
self.parent_page.add_child(instance=self.page)
|
||||
|
||||
# Save revision
|
||||
self.page.save_revision(user=self.request.user, log_action=True)
|
||||
|
||||
# Submit
|
||||
workflow = self.page.get_workflow()
|
||||
workflow.start(self.page, self.request.user)
|
||||
|
||||
# Save subscription settings
|
||||
self.subscription.page = self.page
|
||||
self.subscription.save()
|
||||
|
||||
# Notification
|
||||
buttons = []
|
||||
if self.page.is_previewable():
|
||||
buttons.append(self.get_view_draft_message_button())
|
||||
|
||||
buttons.append(self.get_edit_message_button())
|
||||
|
||||
messages.success(
|
||||
self.request,
|
||||
_("Page '%(page_title)s' created and submitted for moderation.")
|
||||
% {"page_title": self.page.get_admin_display_title()},
|
||||
buttons=buttons,
|
||||
)
|
||||
|
||||
response = self.run_hook("after_create_page", self.request, self.page)
|
||||
if response:
|
||||
return response
|
||||
|
||||
return self.redirect_away()
|
||||
|
||||
def redirect_away(self):
|
||||
if self.next_url:
|
||||
# redirect back to 'next' url if present
|
||||
return redirect(self.next_url)
|
||||
else:
|
||||
# redirect back to the explorer
|
||||
return redirect("wagtailadmin_explore", self.page.get_parent().id)
|
||||
|
||||
def redirect_and_remain(self):
|
||||
target_url = reverse("wagtailadmin_pages:edit", args=[self.page.id])
|
||||
if self.next_url:
|
||||
# Ensure the 'next' url is passed through again if present
|
||||
target_url += "?next=%s" % quote(self.next_url)
|
||||
return redirect(target_url)
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.validation_error(
|
||||
self.request,
|
||||
_("The page could not be created due to validation errors"),
|
||||
self.form,
|
||||
)
|
||||
self.has_unsaved_changes = True
|
||||
|
||||
return self.render_to_response(self.get_context_data())
|
||||
|
||||
def get(self, request):
|
||||
signals.init_new_page.send(
|
||||
sender=CreateView, page=self.page, parent=self.parent_page
|
||||
)
|
||||
self.form = self.form_class(
|
||||
instance=self.page,
|
||||
subscription=self.subscription,
|
||||
parent_page=self.parent_page,
|
||||
for_user=self.request.user,
|
||||
)
|
||||
self.has_unsaved_changes = False
|
||||
|
||||
return self.render_to_response(self.get_context_data())
|
||||
|
||||
def get_preview_url(self):
|
||||
return reverse(
|
||||
"wagtailadmin_pages:preview_on_add",
|
||||
args=[
|
||||
self.page_content_type.app_label,
|
||||
self.page_content_type.model,
|
||||
self.parent_page.id,
|
||||
],
|
||||
)
|
||||
|
||||
def get_side_panels(self):
|
||||
side_panels = [
|
||||
PageStatusSidePanel(
|
||||
self.page,
|
||||
self.request,
|
||||
show_schedule_publishing_toggle=self.form.show_schedule_publishing_toggle,
|
||||
locale=self.locale,
|
||||
translations=self.translations,
|
||||
),
|
||||
]
|
||||
if self.page.is_previewable():
|
||||
side_panels.append(
|
||||
PreviewSidePanel(
|
||||
self.page, self.request, preview_url=self.get_preview_url()
|
||||
)
|
||||
)
|
||||
side_panels.append(
|
||||
ChecksSidePanel(
|
||||
self.page,
|
||||
self.request,
|
||||
)
|
||||
)
|
||||
if self.form.show_comments_toggle:
|
||||
side_panels.append(CommentsSidePanel(self.page, self.request))
|
||||
return MediaContainer(side_panels)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
bound_panel = self.edit_handler.get_bound_panel(
|
||||
request=self.request, instance=self.page, form=self.form
|
||||
)
|
||||
action_menu = PageActionMenu(
|
||||
self.request,
|
||||
view="create",
|
||||
parent_page=self.parent_page,
|
||||
lock=None,
|
||||
locked_for_user=False,
|
||||
)
|
||||
side_panels = self.get_side_panels()
|
||||
|
||||
media = MediaContainer([bound_panel, self.form, action_menu, side_panels]).media
|
||||
|
||||
context.update(
|
||||
{
|
||||
"content_type": self.page_content_type,
|
||||
"page_class": self.page_class,
|
||||
"parent_page": self.parent_page,
|
||||
"edit_handler": bound_panel,
|
||||
"action_menu": action_menu,
|
||||
"side_panels": side_panels,
|
||||
"form": self.form,
|
||||
"next": self.next_url,
|
||||
"has_unsaved_changes": self.has_unsaved_changes,
|
||||
"locale": self.locale,
|
||||
"media": media,
|
||||
}
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
def get_translations(self):
|
||||
# Pages can be created in any language at the root level
|
||||
if self.parent_page.is_root():
|
||||
return [
|
||||
{
|
||||
"locale": locale,
|
||||
"url": reverse(
|
||||
"wagtailadmin_pages:add",
|
||||
args=[
|
||||
self.page_content_type.app_label,
|
||||
self.page_content_type.model,
|
||||
self.parent_page.id,
|
||||
],
|
||||
)
|
||||
+ "?"
|
||||
+ urlencode({"locale": locale.language_code}),
|
||||
}
|
||||
# Do not show the switcher for the current locale
|
||||
for locale in Locale.objects.exclude(pk=self.locale.pk)
|
||||
]
|
||||
|
||||
else:
|
||||
return [
|
||||
{
|
||||
"locale": translation.locale,
|
||||
"url": reverse(
|
||||
"wagtailadmin_pages:add",
|
||||
args=[
|
||||
self.page_content_type.app_label,
|
||||
self.page_content_type.model,
|
||||
translation.id,
|
||||
],
|
||||
),
|
||||
}
|
||||
for translation in self.parent_page.get_translations()
|
||||
.only("id", "locale")
|
||||
.select_related("locale")
|
||||
if translation.permissions_for_user(self.request.user).can_add_subpage()
|
||||
and self.page_class
|
||||
in translation.specific_class.creatable_subpage_models()
|
||||
and self.page_class.can_create_at(translation)
|
||||
]
|
||||
119
env/lib/python3.10/site-packages/wagtail/admin/views/pages/delete.py
vendored
Normal file
119
env/lib/python3.10/site-packages/wagtail/admin/views/pages/delete.py
vendored
Normal file
@@ -0,0 +1,119 @@
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from wagtail import hooks
|
||||
from wagtail.actions.delete_page import DeletePageAction
|
||||
from wagtail.admin import messages
|
||||
from wagtail.admin.utils import get_valid_next_url_from_request
|
||||
from wagtail.models import Page, ReferenceIndex
|
||||
|
||||
|
||||
def delete(request, page_id):
|
||||
page = get_object_or_404(Page, id=page_id).specific
|
||||
if not page.permissions_for_user(request.user).can_delete():
|
||||
raise PermissionDenied
|
||||
|
||||
wagtail_site_name = getattr(settings, "WAGTAIL_SITE_NAME", "wagtail")
|
||||
with transaction.atomic():
|
||||
for fn in hooks.get_hooks("before_delete_page"):
|
||||
result = fn(request, page)
|
||||
if hasattr(result, "status_code"):
|
||||
return result
|
||||
|
||||
next_url = get_valid_next_url_from_request(request)
|
||||
|
||||
pages_to_delete = {page}
|
||||
|
||||
# The `construct_translated_pages_to_cascade_actions` hook returns translation and
|
||||
# alias pages when the action is set to "delete"
|
||||
if getattr(settings, "WAGTAIL_I18N_ENABLED", False):
|
||||
for fn in hooks.get_hooks("construct_translated_pages_to_cascade_actions"):
|
||||
fn_pages = fn([page], "delete")
|
||||
if fn_pages and isinstance(fn_pages, dict):
|
||||
for additional_pages in fn_pages.values():
|
||||
pages_to_delete.update(additional_pages)
|
||||
|
||||
pages_to_delete = list(pages_to_delete)
|
||||
|
||||
if request.method == "POST":
|
||||
continue_deleting = True
|
||||
if (
|
||||
request.POST.get("confirm_site_name")
|
||||
and request.POST.get("confirm_site_name") != wagtail_site_name
|
||||
):
|
||||
messages.error(
|
||||
request, f"Please type '{wagtail_site_name}' to confirm."
|
||||
)
|
||||
continue_deleting = False
|
||||
if continue_deleting:
|
||||
parent_id = page.get_parent().id
|
||||
# Delete the source page.
|
||||
action = DeletePageAction(page, user=request.user)
|
||||
# Permission checks are done above, so skip them in execute.
|
||||
action.execute(skip_permission_checks=True)
|
||||
|
||||
# Delete translation and alias pages if they have the same parent page.
|
||||
if getattr(settings, "WAGTAIL_I18N_ENABLED", False):
|
||||
parent_page_translations = page.get_parent().get_translations()
|
||||
for page_or_alias in pages_to_delete:
|
||||
if page_or_alias.get_parent() in parent_page_translations:
|
||||
action = DeletePageAction(page_or_alias, user=request.user)
|
||||
# Permission checks are done above, so skip them in execute.
|
||||
action.execute(skip_permission_checks=True)
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
_("Page '%(page_title)s' deleted.")
|
||||
% {"page_title": page.get_admin_display_title()},
|
||||
)
|
||||
|
||||
for fn in hooks.get_hooks("after_delete_page"):
|
||||
result = fn(request, page)
|
||||
if hasattr(result, "status_code"):
|
||||
return result
|
||||
|
||||
if next_url:
|
||||
return redirect(next_url)
|
||||
return redirect("wagtailadmin_explore", parent_id)
|
||||
|
||||
usage = ReferenceIndex.get_references_to(page).group_by_source_object()
|
||||
descendant_count = page.get_descendant_count()
|
||||
return TemplateResponse(
|
||||
request,
|
||||
"wagtailadmin/pages/confirm_delete.html",
|
||||
{
|
||||
"page": page,
|
||||
"descendant_count": descendant_count,
|
||||
"next": next_url,
|
||||
"model_opts": page._meta,
|
||||
"usage_url": reverse("wagtailadmin_pages:usage", args=(page.id,))
|
||||
+ "?describe_on_delete=1",
|
||||
"usage_count": usage.count(),
|
||||
"is_protected": usage.is_protected,
|
||||
# if the number of pages ( child pages + current page) exceeds this limit, then confirm before delete.
|
||||
"confirm_before_delete": (descendant_count + 1)
|
||||
>= getattr(settings, "WAGTAILADMIN_UNSAFE_PAGE_DELETION_LIMIT", 10),
|
||||
"wagtail_site_name": wagtail_site_name,
|
||||
# note that while pages_to_delete may contain a mix of translated pages
|
||||
# and aliases, we count the "translations" only, as aliases are similar
|
||||
# to symlinks, so they should just follow the source
|
||||
"translation_count": len(
|
||||
[
|
||||
translation.id
|
||||
for translation in pages_to_delete
|
||||
if not translation.alias_of_id and translation.id != page.id
|
||||
]
|
||||
),
|
||||
"translation_descendant_count": sum(
|
||||
[
|
||||
translation.get_descendants().filter(alias_of__isnull=True).count()
|
||||
for translation in pages_to_delete
|
||||
]
|
||||
),
|
||||
},
|
||||
)
|
||||
970
env/lib/python3.10/site-packages/wagtail/admin/views/pages/edit.py
vendored
Normal file
970
env/lib/python3.10/site-packages/wagtail/admin/views/pages/edit.py
vendored
Normal file
@@ -0,0 +1,970 @@
|
||||
import json
|
||||
from urllib.parse import quote
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import Prefetch, Q
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic.base import View
|
||||
|
||||
from wagtail.actions.publish_page_revision import PublishPageRevisionAction
|
||||
from wagtail.admin import messages
|
||||
from wagtail.admin.action_menu import PageActionMenu
|
||||
from wagtail.admin.mail import send_notification
|
||||
from wagtail.admin.models import EditingSession
|
||||
from wagtail.admin.ui.components import MediaContainer
|
||||
from wagtail.admin.ui.editing_sessions import EditingSessionsModule
|
||||
from wagtail.admin.ui.side_panels import (
|
||||
ChecksSidePanel,
|
||||
CommentsSidePanel,
|
||||
PageStatusSidePanel,
|
||||
PreviewSidePanel,
|
||||
)
|
||||
from wagtail.admin.utils import get_valid_next_url_from_request
|
||||
from wagtail.admin.views.generic import HookResponseMixin
|
||||
from wagtail.admin.views.generic.base import WagtailAdminTemplateMixin
|
||||
from wagtail.exceptions import PageClassNotFoundError
|
||||
from wagtail.locks import BasicLock, ScheduledForPublishLock, WorkflowLock
|
||||
from wagtail.models import (
|
||||
COMMENTS_RELATION_NAME,
|
||||
Comment,
|
||||
CommentReply,
|
||||
Page,
|
||||
PageSubscription,
|
||||
WorkflowState,
|
||||
get_default_page_content_type,
|
||||
)
|
||||
from wagtail.utils.timestamps import render_timestamp
|
||||
|
||||
|
||||
class EditView(WagtailAdminTemplateMixin, HookResponseMixin, View):
|
||||
def get_page_title(self):
|
||||
return _("Editing %(page_type)s") % {
|
||||
"page_type": self.page_class.get_verbose_name()
|
||||
}
|
||||
|
||||
def get_page_subtitle(self):
|
||||
return self.page.get_admin_display_title()
|
||||
|
||||
def get_template_names(self):
|
||||
if self.page.alias_of_id:
|
||||
return ["wagtailadmin/pages/edit_alias.html"]
|
||||
|
||||
else:
|
||||
return ["wagtailadmin/pages/edit.html"]
|
||||
|
||||
def add_save_confirmation_message(self):
|
||||
if self.is_reverting:
|
||||
message = _(
|
||||
"Page '%(page_title)s' has been replaced "
|
||||
"with version from %(previous_revision_datetime)s."
|
||||
) % {
|
||||
"page_title": self.page.get_admin_display_title(),
|
||||
"previous_revision_datetime": render_timestamp(
|
||||
self.previous_revision.created_at
|
||||
),
|
||||
}
|
||||
else:
|
||||
message = _("Page '%(page_title)s' has been updated.") % {
|
||||
"page_title": self.page.get_admin_display_title()
|
||||
}
|
||||
|
||||
messages.success(self.request, message)
|
||||
|
||||
def get_commenting_changes(self):
|
||||
"""
|
||||
Finds comments that have been changed during this request.
|
||||
|
||||
Returns a tuple of 5 lists:
|
||||
- New comments
|
||||
- Deleted comments
|
||||
- Resolved comments
|
||||
- Edited comments
|
||||
- Replied comments (dict containing the instance and list of replies)
|
||||
"""
|
||||
# Get changes
|
||||
comments_formset = self.form.formsets["comments"]
|
||||
new_comments = comments_formset.new_objects
|
||||
deleted_comments = comments_formset.deleted_objects
|
||||
|
||||
# Assume any changed comments that are resolved were only just resolved
|
||||
resolved_comments = []
|
||||
edited_comments = []
|
||||
for changed_comment, changed_fields in comments_formset.changed_objects:
|
||||
if changed_comment.resolved_at and "resolved" in changed_fields:
|
||||
resolved_comments.append(changed_comment)
|
||||
|
||||
if "text" in changed_fields:
|
||||
edited_comments.append(changed_comment)
|
||||
|
||||
new_replies = []
|
||||
deleted_replies = []
|
||||
edited_replies = []
|
||||
for comment_form in comments_formset.forms:
|
||||
# New
|
||||
replies = getattr(comment_form.formsets["replies"], "new_objects", [])
|
||||
if replies:
|
||||
new_replies.append((comment_form.instance, replies))
|
||||
|
||||
# Deleted
|
||||
replies = getattr(comment_form.formsets["replies"], "deleted_objects", [])
|
||||
if replies:
|
||||
deleted_replies.append((comment_form.instance, replies))
|
||||
|
||||
# Edited
|
||||
replies = getattr(comment_form.formsets["replies"], "changed_objects", [])
|
||||
replies = [
|
||||
reply for reply, changed_fields in replies if "text" in changed_fields
|
||||
]
|
||||
if replies:
|
||||
edited_replies.append((comment_form.instance, replies))
|
||||
|
||||
return {
|
||||
"new_comments": new_comments,
|
||||
"deleted_comments": deleted_comments,
|
||||
"resolved_comments": resolved_comments,
|
||||
"edited_comments": edited_comments,
|
||||
"new_replies": new_replies,
|
||||
"deleted_replies": deleted_replies,
|
||||
"edited_replies": edited_replies,
|
||||
}
|
||||
|
||||
def send_commenting_notifications(self, changes):
|
||||
"""
|
||||
Sends notifications about any changes to comments to anyone who is subscribed.
|
||||
"""
|
||||
relevant_comment_ids = []
|
||||
relevant_comment_ids.extend(
|
||||
comment.pk for comment in changes["resolved_comments"]
|
||||
)
|
||||
relevant_comment_ids.extend(
|
||||
comment.pk for comment, replies in changes["new_replies"]
|
||||
)
|
||||
|
||||
# Skip if no changes were made
|
||||
# Note: We don't email about edited comments so ignore those here
|
||||
if (
|
||||
not changes["new_comments"]
|
||||
and not changes["deleted_comments"]
|
||||
and not changes["resolved_comments"]
|
||||
and not changes["new_replies"]
|
||||
):
|
||||
return
|
||||
|
||||
# Get global page comment subscribers
|
||||
subscribers = PageSubscription.objects.filter(
|
||||
page=self.page, comment_notifications=True
|
||||
).select_related("user")
|
||||
global_recipient_users = [
|
||||
subscriber.user
|
||||
for subscriber in subscribers
|
||||
if subscriber.user != self.request.user
|
||||
]
|
||||
|
||||
# Get subscribers to individual threads
|
||||
replies = CommentReply.objects.filter(comment_id__in=relevant_comment_ids)
|
||||
comments = Comment.objects.filter(id__in=relevant_comment_ids)
|
||||
thread_users = (
|
||||
get_user_model()
|
||||
.objects.exclude(pk=self.request.user.pk)
|
||||
.exclude(pk__in=subscribers.values_list("user_id", flat=True))
|
||||
.filter(
|
||||
Q(comment_replies__comment_id__in=relevant_comment_ids)
|
||||
| Q(**{("%s__pk__in" % COMMENTS_RELATION_NAME): relevant_comment_ids})
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch("comment_replies", queryset=replies),
|
||||
Prefetch(COMMENTS_RELATION_NAME, queryset=comments),
|
||||
)
|
||||
)
|
||||
|
||||
# Skip if no recipients
|
||||
if not (global_recipient_users or thread_users):
|
||||
return
|
||||
thread_users = [
|
||||
(
|
||||
user,
|
||||
set(
|
||||
list(user.comment_replies.values_list("comment_id", flat=True))
|
||||
+ list(
|
||||
getattr(user, COMMENTS_RELATION_NAME).values_list(
|
||||
"pk", flat=True
|
||||
)
|
||||
)
|
||||
),
|
||||
)
|
||||
for user in thread_users
|
||||
]
|
||||
mailed_users = set()
|
||||
|
||||
for current_user, current_threads in thread_users:
|
||||
# We are trying to avoid calling send_notification for each user for performance reasons
|
||||
# so group the users receiving the same thread notifications together here
|
||||
if current_user in mailed_users:
|
||||
continue
|
||||
users = [current_user]
|
||||
mailed_users.add(current_user)
|
||||
for user, threads in thread_users:
|
||||
if user not in mailed_users and threads == current_threads:
|
||||
users.append(user)
|
||||
mailed_users.add(user)
|
||||
send_notification(
|
||||
users,
|
||||
"updated_comments",
|
||||
{
|
||||
"page": self.page,
|
||||
"editor": self.request.user,
|
||||
"new_comments": [
|
||||
comment
|
||||
for comment in changes["new_comments"]
|
||||
if comment.pk in threads
|
||||
],
|
||||
"resolved_comments": [
|
||||
comment
|
||||
for comment in changes["resolved_comments"]
|
||||
if comment.pk in threads
|
||||
],
|
||||
"deleted_comments": [],
|
||||
"replied_comments": [
|
||||
{
|
||||
"comment": comment,
|
||||
"replies": replies,
|
||||
}
|
||||
for comment, replies in changes["new_replies"]
|
||||
if comment.pk in threads
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
return send_notification(
|
||||
global_recipient_users,
|
||||
"updated_comments",
|
||||
{
|
||||
"page": self.page,
|
||||
"editor": self.request.user,
|
||||
"new_comments": changes["new_comments"],
|
||||
"resolved_comments": changes["resolved_comments"],
|
||||
"deleted_comments": changes["deleted_comments"],
|
||||
"replied_comments": [
|
||||
{
|
||||
"comment": comment,
|
||||
"replies": replies,
|
||||
}
|
||||
for comment, replies in changes["new_replies"]
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
def log_commenting_changes(self, changes, revision):
|
||||
"""
|
||||
Generates log entries for any changes made to comments or replies.
|
||||
"""
|
||||
for comment in changes["new_comments"]:
|
||||
comment.log_create(page_revision=revision, user=self.request.user)
|
||||
|
||||
for comment in changes["edited_comments"]:
|
||||
comment.log_edit(page_revision=revision, user=self.request.user)
|
||||
|
||||
for comment in changes["resolved_comments"]:
|
||||
comment.log_resolve(page_revision=revision, user=self.request.user)
|
||||
|
||||
for comment in changes["deleted_comments"]:
|
||||
comment.log_delete(page_revision=revision, user=self.request.user)
|
||||
|
||||
for comment, replies in changes["new_replies"]:
|
||||
for reply in replies:
|
||||
reply.log_create(page_revision=revision, user=self.request.user)
|
||||
|
||||
for comment, replies in changes["edited_replies"]:
|
||||
for reply in replies:
|
||||
reply.log_edit(page_revision=revision, user=self.request.user)
|
||||
|
||||
for comment, replies in changes["deleted_replies"]:
|
||||
for reply in replies:
|
||||
reply.log_delete(page_revision=revision, user=self.request.user)
|
||||
|
||||
def get_edit_message_button(self):
|
||||
return messages.button(
|
||||
reverse("wagtailadmin_pages:edit", args=(self.page.id,)), _("Edit")
|
||||
)
|
||||
|
||||
def get_view_draft_message_button(self):
|
||||
return messages.button(
|
||||
reverse("wagtailadmin_pages:view_draft", args=(self.page.id,)),
|
||||
_("View draft"),
|
||||
new_window=False,
|
||||
)
|
||||
|
||||
def get_view_live_message_button(self):
|
||||
return messages.button(self.page.url, _("View live"), new_window=False)
|
||||
|
||||
def get_compare_with_live_message_button(self):
|
||||
return messages.button(
|
||||
reverse(
|
||||
"wagtailadmin_pages:revisions_compare",
|
||||
args=(self.page.id, "live", self.latest_revision.id),
|
||||
),
|
||||
_("Compare with live version"),
|
||||
)
|
||||
|
||||
def get_page_for_status(self):
|
||||
if self.page.live and self.page.has_unpublished_changes:
|
||||
# Page status needs to present the version of the page containing the correct live URL
|
||||
return self.real_page_record.specific
|
||||
else:
|
||||
return self.page
|
||||
|
||||
def dispatch(self, request, page_id):
|
||||
self.real_page_record = get_object_or_404(
|
||||
Page.objects.prefetch_workflow_states(), id=page_id
|
||||
)
|
||||
self.latest_revision = self.real_page_record.get_latest_revision()
|
||||
self.scheduled_revision = self.real_page_record.scheduled_revision
|
||||
self.page_content_type = self.real_page_record.cached_content_type
|
||||
self.page_class = self.real_page_record.specific_class
|
||||
|
||||
if self.page_class is None:
|
||||
raise PageClassNotFoundError(
|
||||
f"The page '{self.real_page_record}' cannot be edited because the "
|
||||
f"model class used to create it ({self.page_content_type.app_label}."
|
||||
f"{self.page_content_type.model}) can no longer be found in the codebase. "
|
||||
"This usually happens as a result of switching between git "
|
||||
"branches without running migrations to trigger the removal of "
|
||||
"unused ContentTypes. To edit the page, you will need to switch "
|
||||
"back to a branch where the model class is still present."
|
||||
)
|
||||
|
||||
self.page = self.real_page_record.get_latest_revision_as_object()
|
||||
self.parent = self.page.get_parent()
|
||||
self.scheduled_page = self.real_page_record.get_scheduled_revision_as_object()
|
||||
|
||||
self.page_perms = self.page.permissions_for_user(self.request.user)
|
||||
self.lock = self.page.get_lock()
|
||||
self.locked_for_user = self.lock is not None and self.lock.for_user(
|
||||
self.request.user
|
||||
)
|
||||
|
||||
if not self.page_perms.can_edit():
|
||||
raise PermissionDenied
|
||||
|
||||
self.next_url = get_valid_next_url_from_request(self.request)
|
||||
|
||||
response = self.run_hook("before_edit_page", self.request, self.page)
|
||||
if response:
|
||||
return response
|
||||
|
||||
self.subscription, created = PageSubscription.objects.get_or_create(
|
||||
page=self.page,
|
||||
user=self.request.user,
|
||||
defaults={
|
||||
"comment_notifications": False,
|
||||
},
|
||||
)
|
||||
|
||||
self.edit_handler = self.page_class.get_edit_handler()
|
||||
self.form_class = self.edit_handler.get_form_class()
|
||||
|
||||
if getattr(settings, "WAGTAIL_WORKFLOW_ENABLED", True):
|
||||
# Retrieve current workflow state if set, default to last workflow state
|
||||
self.workflow_state = (
|
||||
self.page.current_workflow_state
|
||||
or self.page.workflow_states.order_by("created_at").last()
|
||||
)
|
||||
else:
|
||||
self.workflow_state = None
|
||||
|
||||
if getattr(settings, "WAGTAIL_I18N_ENABLED", False):
|
||||
self.locale = self.page.locale
|
||||
self.translations = self.get_translations()
|
||||
else:
|
||||
self.locale = None
|
||||
self.translations = []
|
||||
|
||||
if self.workflow_state:
|
||||
self.workflow_tasks = self.workflow_state.all_tasks_with_status()
|
||||
else:
|
||||
self.workflow_tasks = []
|
||||
|
||||
self.errors_debug = None
|
||||
|
||||
return super().dispatch(request)
|
||||
|
||||
def get(self, request):
|
||||
if self.lock:
|
||||
lock_message = self.lock.get_message(self.request.user)
|
||||
if lock_message:
|
||||
if isinstance(self.lock, BasicLock) and self.page_perms.can_unlock():
|
||||
lock_message = format_html(
|
||||
'{} <span class="buttons"><button type="button" class="button button-small button-secondary" data-action="w-action#post" data-controller="w-action" data-w-action-url-value="{}">{}</button></span>',
|
||||
lock_message,
|
||||
reverse("wagtailadmin_pages:unlock", args=(self.page.id,)),
|
||||
_("Unlock"),
|
||||
)
|
||||
|
||||
if (
|
||||
isinstance(self.lock, ScheduledForPublishLock)
|
||||
and self.page_perms.can_unschedule()
|
||||
):
|
||||
lock_message = format_html(
|
||||
'{} <span class="buttons"><button type="button" class="button button-small button-secondary" data-action="w-action#post" data-controller="w-action" data-w-action-url-value="{}">{}</button></span>',
|
||||
lock_message,
|
||||
reverse(
|
||||
"wagtailadmin_pages:revisions_unschedule",
|
||||
args=[self.page.id, self.scheduled_revision.pk],
|
||||
),
|
||||
_("Cancel scheduled publish"),
|
||||
)
|
||||
|
||||
if (
|
||||
not isinstance(self.lock, ScheduledForPublishLock)
|
||||
and self.locked_for_user
|
||||
):
|
||||
messages.warning(self.request, lock_message, extra_tags="lock")
|
||||
else:
|
||||
messages.info(self.request, lock_message, extra_tags="lock")
|
||||
|
||||
self.form = self.form_class(
|
||||
instance=self.page,
|
||||
subscription=self.subscription,
|
||||
parent_page=self.parent,
|
||||
for_user=self.request.user,
|
||||
)
|
||||
self.has_unsaved_changes = False
|
||||
self.page_for_status = self.get_page_for_status()
|
||||
|
||||
return self.render_to_response(self.get_context_data())
|
||||
|
||||
def add_cancel_workflow_confirmation_message(self):
|
||||
message = _("Workflow on page '%(page_title)s' has been cancelled.") % {
|
||||
"page_title": self.page.get_admin_display_title()
|
||||
}
|
||||
|
||||
messages.success(
|
||||
self.request,
|
||||
message,
|
||||
buttons=[
|
||||
self.get_view_draft_message_button(),
|
||||
self.get_edit_message_button(),
|
||||
],
|
||||
)
|
||||
|
||||
def post(self, request):
|
||||
# Don't allow POST requests if the page is an alias
|
||||
if self.page.alias_of_id:
|
||||
# Return 405 "Method Not Allowed" response
|
||||
return HttpResponse(status=405)
|
||||
|
||||
self.form = self.form_class(
|
||||
self.request.POST,
|
||||
self.request.FILES,
|
||||
instance=self.page,
|
||||
subscription=self.subscription,
|
||||
parent_page=self.parent,
|
||||
for_user=self.request.user,
|
||||
)
|
||||
|
||||
self.is_cancelling_workflow = (
|
||||
bool(self.request.POST.get("action-cancel-workflow"))
|
||||
and self.workflow_state
|
||||
and self.workflow_state.user_can_cancel(self.request.user)
|
||||
)
|
||||
|
||||
if self.form.is_valid() and not self.locked_for_user:
|
||||
return self.form_valid(self.form)
|
||||
else:
|
||||
return self.form_invalid(self.form)
|
||||
|
||||
def workflow_action_is_valid(self):
|
||||
self.workflow_action = self.request.POST["workflow-action-name"]
|
||||
available_actions = self.page.current_workflow_task.get_actions(
|
||||
self.page, self.request.user
|
||||
)
|
||||
available_action_names = [
|
||||
name for name, verbose_name, modal in available_actions
|
||||
]
|
||||
return self.workflow_action in available_action_names
|
||||
|
||||
def form_valid(self, form):
|
||||
self.is_reverting = bool(self.request.POST.get("revision"))
|
||||
# If a revision ID was passed in the form, get that revision so its
|
||||
# date can be referenced in notification messages
|
||||
if self.is_reverting:
|
||||
self.previous_revision = get_object_or_404(
|
||||
self.page.revisions, id=self.request.POST.get("revision")
|
||||
)
|
||||
|
||||
self.has_content_changes = self.form.has_changed()
|
||||
|
||||
if self.request.POST.get("action-publish") and self.page_perms.can_publish():
|
||||
return self.publish_action()
|
||||
elif (
|
||||
self.request.POST.get("action-submit")
|
||||
and self.page_perms.can_submit_for_moderation()
|
||||
):
|
||||
return self.submit_action()
|
||||
elif (
|
||||
self.request.POST.get("action-restart-workflow")
|
||||
and self.page_perms.can_submit_for_moderation()
|
||||
and self.workflow_state
|
||||
and self.workflow_state.user_can_cancel(self.request.user)
|
||||
):
|
||||
return self.restart_workflow_action()
|
||||
elif (
|
||||
self.request.POST.get("action-workflow-action")
|
||||
and self.workflow_action_is_valid()
|
||||
):
|
||||
return self.perform_workflow_action()
|
||||
elif self.is_cancelling_workflow:
|
||||
return self.cancel_workflow_action()
|
||||
else:
|
||||
return self.save_action()
|
||||
|
||||
def save_action(self):
|
||||
self.page = self.form.save(commit=not self.page.live)
|
||||
self.subscription.save()
|
||||
|
||||
# Save revision
|
||||
revision = self.page.save_revision(
|
||||
user=self.request.user,
|
||||
log_action=True, # Always log the new revision on edit
|
||||
previous_revision=(self.previous_revision if self.is_reverting else None),
|
||||
)
|
||||
|
||||
self.add_save_confirmation_message()
|
||||
|
||||
if self.has_content_changes and "comments" in self.form.formsets:
|
||||
changes = self.get_commenting_changes()
|
||||
self.log_commenting_changes(changes, revision)
|
||||
self.send_commenting_notifications(changes)
|
||||
|
||||
response = self.run_hook("after_edit_page", self.request, self.page)
|
||||
if response:
|
||||
return response
|
||||
|
||||
# Just saving - remain on edit page for further edits
|
||||
return self.redirect_and_remain()
|
||||
|
||||
def publish_action(self):
|
||||
self.page = self.form.save(commit=not self.page.live)
|
||||
self.subscription.save()
|
||||
|
||||
# Save revision
|
||||
revision = self.page.save_revision(
|
||||
user=self.request.user,
|
||||
log_action=True, # Always log the new revision on edit
|
||||
previous_revision=(self.previous_revision if self.is_reverting else None),
|
||||
)
|
||||
|
||||
# store submitted go_live_at for messaging below
|
||||
go_live_at = self.page.go_live_at
|
||||
|
||||
response = self.run_hook("before_publish_page", self.request, self.page)
|
||||
if response:
|
||||
return response
|
||||
|
||||
action = PublishPageRevisionAction(
|
||||
revision,
|
||||
user=self.request.user,
|
||||
changed=self.has_content_changes,
|
||||
previous_revision=(self.previous_revision if self.is_reverting else None),
|
||||
)
|
||||
action.execute(skip_permission_checks=True)
|
||||
|
||||
if self.has_content_changes and "comments" in self.form.formsets:
|
||||
changes = self.get_commenting_changes()
|
||||
self.log_commenting_changes(changes, revision)
|
||||
self.send_commenting_notifications(changes)
|
||||
|
||||
# Need to reload the page because the URL may have changed, and we
|
||||
# need the up-to-date URL for the "View Live" button.
|
||||
self.page = self.page.specific_class.objects.get(pk=self.page.pk)
|
||||
|
||||
response = self.run_hook("after_publish_page", self.request, self.page)
|
||||
if response:
|
||||
return response
|
||||
|
||||
# Notifications
|
||||
if go_live_at and go_live_at > timezone.now():
|
||||
# Page has been scheduled for publishing in the future
|
||||
|
||||
if self.is_reverting:
|
||||
message = _(
|
||||
"Version from %(previous_revision_datetime)s "
|
||||
"of page '%(page_title)s' has been scheduled for publishing."
|
||||
) % {
|
||||
"previous_revision_datetime": render_timestamp(
|
||||
self.previous_revision.created_at
|
||||
),
|
||||
"page_title": self.page.get_admin_display_title(),
|
||||
}
|
||||
else:
|
||||
if self.page.live:
|
||||
message = _(
|
||||
"Page '%(page_title)s' is live and this version has been scheduled for publishing."
|
||||
) % {"page_title": self.page.get_admin_display_title()}
|
||||
|
||||
else:
|
||||
message = _(
|
||||
"Page '%(page_title)s' has been scheduled for publishing."
|
||||
) % {"page_title": self.page.get_admin_display_title()}
|
||||
|
||||
messages.success(
|
||||
self.request, message, buttons=[self.get_edit_message_button()]
|
||||
)
|
||||
|
||||
else:
|
||||
# Page is being published now
|
||||
|
||||
if self.is_reverting:
|
||||
message = _(
|
||||
"Version from %(datetime)s of page '%(page_title)s' has been published."
|
||||
) % {
|
||||
"datetime": render_timestamp(self.previous_revision.created_at),
|
||||
"page_title": self.page.get_admin_display_title(),
|
||||
}
|
||||
else:
|
||||
message = _("Page '%(page_title)s' has been published.") % {
|
||||
"page_title": self.page.get_admin_display_title()
|
||||
}
|
||||
|
||||
buttons = []
|
||||
if self.page.url is not None:
|
||||
buttons.append(self.get_view_live_message_button())
|
||||
buttons.append(self.get_edit_message_button())
|
||||
messages.success(self.request, message, buttons=buttons)
|
||||
|
||||
response = self.run_hook("after_edit_page", self.request, self.page)
|
||||
if response:
|
||||
return response
|
||||
|
||||
# we're done here - redirect back to the explorer
|
||||
return self.redirect_away()
|
||||
|
||||
def submit_action(self):
|
||||
self.page = self.form.save(commit=not self.page.live)
|
||||
self.subscription.save()
|
||||
|
||||
# Save revision
|
||||
revision = self.page.save_revision(
|
||||
user=self.request.user,
|
||||
log_action=True, # Always log the new revision on edit
|
||||
previous_revision=(self.previous_revision if self.is_reverting else None),
|
||||
)
|
||||
|
||||
if self.has_content_changes and "comments" in self.form.formsets:
|
||||
changes = self.get_commenting_changes()
|
||||
self.log_commenting_changes(changes, revision)
|
||||
self.send_commenting_notifications(changes)
|
||||
|
||||
if (
|
||||
self.workflow_state
|
||||
and self.workflow_state.status == WorkflowState.STATUS_NEEDS_CHANGES
|
||||
):
|
||||
# If the workflow was in the needs changes state, resume the existing workflow on submission
|
||||
self.workflow_state.resume(self.request.user)
|
||||
else:
|
||||
# Otherwise start a new workflow
|
||||
workflow = self.page.get_workflow()
|
||||
workflow.start(self.page, self.request.user)
|
||||
|
||||
message = _("Page '%(page_title)s' has been submitted for moderation.") % {
|
||||
"page_title": self.page.get_admin_display_title()
|
||||
}
|
||||
|
||||
messages.success(
|
||||
self.request,
|
||||
message,
|
||||
buttons=[
|
||||
self.get_view_draft_message_button(),
|
||||
self.get_edit_message_button(),
|
||||
],
|
||||
)
|
||||
|
||||
response = self.run_hook("after_edit_page", self.request, self.page)
|
||||
if response:
|
||||
return response
|
||||
|
||||
# we're done here - redirect back to the explorer
|
||||
return self.redirect_away()
|
||||
|
||||
def restart_workflow_action(self):
|
||||
self.page = self.form.save(commit=not self.page.live)
|
||||
self.subscription.save()
|
||||
|
||||
# save revision
|
||||
revision = self.page.save_revision(
|
||||
user=self.request.user,
|
||||
log_action=True, # Always log the new revision on edit
|
||||
previous_revision=(self.previous_revision if self.is_reverting else None),
|
||||
)
|
||||
|
||||
if self.has_content_changes and "comments" in self.form.formsets:
|
||||
changes = self.get_commenting_changes()
|
||||
self.log_commenting_changes(changes, revision)
|
||||
self.send_commenting_notifications(changes)
|
||||
|
||||
# cancel workflow
|
||||
self.workflow_state.cancel(user=self.request.user)
|
||||
# start new workflow
|
||||
workflow = self.page.get_workflow()
|
||||
workflow.start(self.page, self.request.user)
|
||||
|
||||
message = _("Workflow on page '%(page_title)s' has been restarted.") % {
|
||||
"page_title": self.page.get_admin_display_title()
|
||||
}
|
||||
|
||||
messages.success(
|
||||
self.request,
|
||||
message,
|
||||
buttons=[
|
||||
self.get_view_draft_message_button(),
|
||||
self.get_edit_message_button(),
|
||||
],
|
||||
)
|
||||
|
||||
response = self.run_hook("after_edit_page", self.request, self.page)
|
||||
if response:
|
||||
return response
|
||||
|
||||
# we're done here - redirect back to the explorer
|
||||
return self.redirect_away()
|
||||
|
||||
def perform_workflow_action(self):
|
||||
self.page = self.form.save(commit=not self.page.live)
|
||||
self.subscription.save()
|
||||
|
||||
if self.has_content_changes:
|
||||
# Save revision
|
||||
revision = self.page.save_revision(
|
||||
user=self.request.user,
|
||||
log_action=True, # Always log the new revision on edit
|
||||
previous_revision=(
|
||||
self.previous_revision if self.is_reverting else None
|
||||
),
|
||||
)
|
||||
|
||||
if "comments" in self.form.formsets:
|
||||
changes = self.get_commenting_changes()
|
||||
self.log_commenting_changes(changes, revision)
|
||||
self.send_commenting_notifications(changes)
|
||||
|
||||
extra_workflow_data_json = self.request.POST.get(
|
||||
"workflow-action-extra-data", "{}"
|
||||
)
|
||||
extra_workflow_data = json.loads(extra_workflow_data_json)
|
||||
self.page.current_workflow_task.on_action(
|
||||
self.page.current_workflow_task_state,
|
||||
self.request.user,
|
||||
self.workflow_action,
|
||||
**extra_workflow_data,
|
||||
)
|
||||
|
||||
self.add_save_confirmation_message()
|
||||
|
||||
response = self.run_hook("after_edit_page", self.request, self.page)
|
||||
if response:
|
||||
return response
|
||||
|
||||
# we're done here - redirect back to the explorer
|
||||
return self.redirect_away()
|
||||
|
||||
def cancel_workflow_action(self):
|
||||
self.workflow_state.cancel(user=self.request.user)
|
||||
self.page = self.form.save(commit=not self.page.live)
|
||||
self.subscription.save()
|
||||
|
||||
# Save revision
|
||||
revision = self.page.save_revision(
|
||||
user=self.request.user,
|
||||
log_action=True, # Always log the new revision on edit
|
||||
previous_revision=(self.previous_revision if self.is_reverting else None),
|
||||
)
|
||||
|
||||
if self.has_content_changes and "comments" in self.form.formsets:
|
||||
changes = self.get_commenting_changes()
|
||||
self.log_commenting_changes(changes, revision)
|
||||
self.send_commenting_notifications(changes)
|
||||
|
||||
# Notifications
|
||||
self.add_cancel_workflow_confirmation_message()
|
||||
|
||||
response = self.run_hook("after_edit_page", self.request, self.page)
|
||||
if response:
|
||||
return response
|
||||
|
||||
# Just saving - remain on edit page for further edits
|
||||
return self.redirect_and_remain()
|
||||
|
||||
def redirect_away(self):
|
||||
if self.next_url:
|
||||
# redirect back to 'next' url if present
|
||||
return redirect(self.next_url)
|
||||
else:
|
||||
# redirect back to the explorer
|
||||
return redirect("wagtailadmin_explore", self.page.get_parent().id)
|
||||
|
||||
def redirect_and_remain(self):
|
||||
target_url = reverse("wagtailadmin_pages:edit", args=[self.page.id])
|
||||
if self.next_url:
|
||||
# Ensure the 'next' url is passed through again if present
|
||||
target_url += "?next=%s" % quote(self.next_url)
|
||||
return redirect(target_url)
|
||||
|
||||
def form_invalid(self, form):
|
||||
# even if the page is locked due to not having permissions, the original submitter can still cancel the workflow
|
||||
if self.is_cancelling_workflow:
|
||||
self.workflow_state.cancel(user=self.request.user)
|
||||
self.add_cancel_workflow_confirmation_message()
|
||||
|
||||
# Refresh the lock object as now WorkflowLock no longer applies
|
||||
self.lock = self.page.get_lock()
|
||||
self.locked_for_user = self.lock is not None and self.lock.for_user(
|
||||
self.request.user
|
||||
)
|
||||
elif self.locked_for_user:
|
||||
messages.error(
|
||||
self.request, _("The page could not be saved as it is locked")
|
||||
)
|
||||
else:
|
||||
messages.validation_error(
|
||||
self.request,
|
||||
_("The page could not be saved due to validation errors"),
|
||||
self.form,
|
||||
)
|
||||
self.errors_debug = repr(self.form.errors) + repr(
|
||||
[
|
||||
(name, formset.errors)
|
||||
for (name, formset) in self.form.formsets.items()
|
||||
if formset.errors
|
||||
]
|
||||
)
|
||||
self.has_unsaved_changes = True
|
||||
|
||||
self.page_for_status = self.get_page_for_status()
|
||||
|
||||
return self.render_to_response(self.get_context_data())
|
||||
|
||||
def get_preview_url(self):
|
||||
return reverse("wagtailadmin_pages:preview_on_edit", args=[self.page.id])
|
||||
|
||||
def get_history_url(self):
|
||||
permissions = self.page.permissions_for_user(self.request.user)
|
||||
if permissions.can_view_revisions():
|
||||
return reverse("wagtailadmin_pages:history", args=[self.page.id])
|
||||
|
||||
def get_side_panels(self):
|
||||
side_panels = [
|
||||
PageStatusSidePanel(
|
||||
self.page,
|
||||
self.request,
|
||||
show_schedule_publishing_toggle=self.form.show_schedule_publishing_toggle,
|
||||
live_object=self.real_page_record,
|
||||
scheduled_object=self.scheduled_page,
|
||||
locale=self.locale,
|
||||
translations=self.translations,
|
||||
),
|
||||
]
|
||||
if self.page.is_previewable():
|
||||
side_panels.append(
|
||||
PreviewSidePanel(
|
||||
self.page, self.request, preview_url=self.get_preview_url()
|
||||
)
|
||||
)
|
||||
side_panels.append(ChecksSidePanel(self.page, self.request))
|
||||
if self.form.show_comments_toggle:
|
||||
side_panels.append(CommentsSidePanel(self.page, self.request))
|
||||
return MediaContainer(side_panels)
|
||||
|
||||
def get_editing_sessions(self):
|
||||
EditingSession.cleanup()
|
||||
content_type = get_default_page_content_type()
|
||||
session = EditingSession.objects.create(
|
||||
user=self.request.user,
|
||||
content_type=content_type,
|
||||
object_id=self.page.pk,
|
||||
last_seen_at=timezone.now(),
|
||||
)
|
||||
return EditingSessionsModule(
|
||||
session,
|
||||
reverse(
|
||||
"wagtailadmin_editing_sessions:ping",
|
||||
args=("wagtailcore", "page", self.page.pk, session.id),
|
||||
),
|
||||
reverse(
|
||||
"wagtailadmin_editing_sessions:release",
|
||||
args=(session.id,),
|
||||
),
|
||||
[],
|
||||
self.page.latest_revision_id,
|
||||
)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
user_perms = self.page.permissions_for_user(self.request.user)
|
||||
bound_panel = self.edit_handler.get_bound_panel(
|
||||
instance=self.page, request=self.request, form=self.form
|
||||
)
|
||||
action_menu = PageActionMenu(
|
||||
self.request,
|
||||
view="edit",
|
||||
page=self.page,
|
||||
lock=self.lock,
|
||||
locked_for_user=self.locked_for_user,
|
||||
)
|
||||
side_panels = self.get_side_panels()
|
||||
|
||||
media = MediaContainer([bound_panel, self.form, action_menu, side_panels]).media
|
||||
|
||||
context.update(
|
||||
{
|
||||
"page": self.page,
|
||||
"page_for_status": self.page_for_status,
|
||||
"content_type": self.page_content_type,
|
||||
"edit_handler": bound_panel,
|
||||
"errors_debug": self.errors_debug,
|
||||
"action_menu": action_menu,
|
||||
"side_panels": side_panels,
|
||||
"form": self.form,
|
||||
"next": self.next_url,
|
||||
"history_url": self.get_history_url(),
|
||||
"has_unsaved_changes": self.has_unsaved_changes,
|
||||
"page_locked": self.locked_for_user,
|
||||
"workflow_state": self.workflow_state
|
||||
if self.workflow_state and self.workflow_state.is_active
|
||||
else None,
|
||||
"current_task_state": self.page.current_workflow_task_state,
|
||||
"publishing_will_cancel_workflow": self.workflow_tasks
|
||||
and getattr(settings, "WAGTAIL_WORKFLOW_CANCEL_ON_PUBLISH", True),
|
||||
"confirm_workflow_cancellation_url": reverse(
|
||||
"wagtailadmin_pages:confirm_workflow_cancellation",
|
||||
args=(self.page.id,),
|
||||
),
|
||||
"user_can_lock": (not self.lock or isinstance(self.lock, WorkflowLock))
|
||||
and user_perms.can_lock(),
|
||||
"user_can_unlock": isinstance(self.lock, BasicLock)
|
||||
and user_perms.can_unlock(),
|
||||
"locale": self.locale,
|
||||
"media": media,
|
||||
"editing_sessions": self.get_editing_sessions(),
|
||||
}
|
||||
)
|
||||
|
||||
return context
|
||||
|
||||
def get_translations(self):
|
||||
return [
|
||||
{
|
||||
"locale": translation.locale,
|
||||
"url": reverse("wagtailadmin_pages:edit", args=[translation.id]),
|
||||
}
|
||||
for translation in self.page.get_translations()
|
||||
.only("id", "locale", "depth")
|
||||
.select_related("locale")
|
||||
if translation.permissions_for_user(self.request.user).can_edit()
|
||||
]
|
||||
95
env/lib/python3.10/site-packages/wagtail/admin/views/pages/history.py
vendored
Normal file
95
env/lib/python3.10/site-packages/wagtail/admin/views/pages/history.py
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
import django_filters
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import Q
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from wagtail.admin.views.generic import history
|
||||
from wagtail.admin.views.pages.utils import (
|
||||
GenericPageBreadcrumbsMixin,
|
||||
)
|
||||
from wagtail.admin.widgets import BooleanRadioSelect
|
||||
from wagtail.models import Page, PageLogEntry
|
||||
from wagtail.permissions import page_permission_policy
|
||||
|
||||
|
||||
class PageHistoryFilterSet(history.HistoryFilterSet):
|
||||
is_commenting_action = django_filters.BooleanFilter(
|
||||
label=gettext_lazy("Is commenting action"),
|
||||
method="filter_is_commenting_action",
|
||||
widget=BooleanRadioSelect,
|
||||
)
|
||||
|
||||
def filter_is_commenting_action(self, queryset, name, value):
|
||||
if value is None:
|
||||
return queryset
|
||||
|
||||
q = Q(action__startswith="wagtail.comments")
|
||||
if value is False:
|
||||
q = ~q
|
||||
|
||||
return queryset.filter(q)
|
||||
|
||||
|
||||
class PageWorkflowHistoryViewMixin:
|
||||
model = Page
|
||||
pk_url_kwarg = "page_id"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not self.object.permissions_for_user(request.user).can_edit():
|
||||
raise PermissionDenied
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return super().get_context_data(**kwargs, page=self.object)
|
||||
|
||||
|
||||
class WorkflowHistoryView(PageWorkflowHistoryViewMixin, history.WorkflowHistoryView):
|
||||
header_icon = "doc-empty-inverse"
|
||||
workflow_history_url_name = "wagtailadmin_pages:workflow_history"
|
||||
workflow_history_detail_url_name = "wagtailadmin_pages:workflow_history_detail"
|
||||
|
||||
|
||||
class WorkflowHistoryDetailView(
|
||||
PageWorkflowHistoryViewMixin, history.WorkflowHistoryDetailView
|
||||
):
|
||||
object_icon = "doc-empty-inverse"
|
||||
workflow_history_url_name = "wagtailadmin_pages:workflow_history"
|
||||
|
||||
|
||||
class PageHistoryView(GenericPageBreadcrumbsMixin, history.HistoryView):
|
||||
template_name = "wagtailadmin/pages/history.html"
|
||||
filterset_class = PageHistoryFilterSet
|
||||
model = Page
|
||||
pk_url_kwarg = "page_id"
|
||||
permission_policy = page_permission_policy
|
||||
any_permission_required = {
|
||||
"add",
|
||||
"change",
|
||||
"publish",
|
||||
"bulk_delete",
|
||||
"lock",
|
||||
"unlock",
|
||||
}
|
||||
history_url_name = "wagtailadmin_pages:history"
|
||||
history_results_url_name = "wagtailadmin_pages:history_results"
|
||||
edit_url_name = "wagtailadmin_pages:edit"
|
||||
revisions_view_url_name = "wagtailadmin_pages:revisions_view"
|
||||
revisions_revert_url_name = "wagtailadmin_pages:revisions_revert"
|
||||
revisions_compare_url_name = "wagtailadmin_pages:revisions_compare"
|
||||
revisions_unschedule_url_name = "wagtailadmin_pages:revisions_unschedule"
|
||||
|
||||
def get_object(self):
|
||||
return get_object_or_404(Page, id=self.pk).specific
|
||||
|
||||
def get_page_subtitle(self):
|
||||
return self.object.get_admin_display_title()
|
||||
|
||||
def user_can_unschedule(self):
|
||||
return self.object.permissions_for_user(self.request.user).can_unschedule()
|
||||
|
||||
def get_base_queryset(self):
|
||||
return self._annotate_queryset(PageLogEntry.objects.filter(page=self.object))
|
||||
|
||||
def _annotate_queryset(self, queryset):
|
||||
return super()._annotate_queryset(queryset).select_related("page")
|
||||
523
env/lib/python3.10/site-packages/wagtail/admin/views/pages/listing.py
vendored
Normal file
523
env/lib/python3.10/site-packages/wagtail/admin/views/pages/listing.py
vendored
Normal file
@@ -0,0 +1,523 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db.models import F
|
||||
from django.forms import CheckboxSelectMultiple, RadioSelect
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property, classproperty
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_filters.filters import (
|
||||
ChoiceFilter,
|
||||
DateFromToRangeFilter,
|
||||
ModelMultipleChoiceFilter,
|
||||
)
|
||||
|
||||
from wagtail import hooks
|
||||
from wagtail.admin.filters import (
|
||||
DateRangePickerWidget,
|
||||
MultipleContentTypeFilter,
|
||||
MultipleUserFilter,
|
||||
WagtailFilterSet,
|
||||
)
|
||||
from wagtail.admin.forms.search import SearchForm
|
||||
from wagtail.admin.ui.components import MediaContainer
|
||||
from wagtail.admin.ui.side_panels import (
|
||||
PageStatusSidePanel,
|
||||
)
|
||||
from wagtail.admin.ui.tables import Column, DateColumn
|
||||
from wagtail.admin.ui.tables.pages import (
|
||||
BulkActionsColumn,
|
||||
NavigateToChildrenColumn,
|
||||
OrderingColumn,
|
||||
PageStatusColumn,
|
||||
PageTable,
|
||||
PageTitleColumn,
|
||||
)
|
||||
from wagtail.admin.views import generic
|
||||
from wagtail.models import Page, PageLogEntry, Site, get_page_content_types
|
||||
from wagtail.permissions import page_permission_policy
|
||||
|
||||
|
||||
class SiteFilter(ModelMultipleChoiceFilter):
|
||||
def get_filter_predicate(self, v):
|
||||
return {"path__startswith": v.root_page.path}
|
||||
|
||||
|
||||
class HasChildPagesFilter(ChoiceFilter):
|
||||
def filter(self, qs, value):
|
||||
if value == "true":
|
||||
return qs.filter(numchild__gt=0)
|
||||
elif value == "false":
|
||||
return qs.filter(numchild=0)
|
||||
else: # None / empty string
|
||||
return qs
|
||||
|
||||
|
||||
class EditedByFilter(MultipleUserFilter):
|
||||
def filter(self, qs, value):
|
||||
if value:
|
||||
qs = qs.filter(
|
||||
pk__in=PageLogEntry.objects.filter(
|
||||
action="wagtail.edit", user__in=value
|
||||
)
|
||||
.order_by()
|
||||
.values_list("page_id", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class PageFilterSet(WagtailFilterSet):
|
||||
latest_revision_created_at = DateFromToRangeFilter(
|
||||
label=_("Date updated"),
|
||||
widget=DateRangePickerWidget,
|
||||
)
|
||||
owner = MultipleUserFilter(
|
||||
label=_("Owner"),
|
||||
queryset=(
|
||||
lambda request: get_user_model().objects.filter(
|
||||
pk__in=Page.objects.order_by()
|
||||
.values_list("owner_id", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
),
|
||||
widget=CheckboxSelectMultiple,
|
||||
)
|
||||
edited_by = EditedByFilter(
|
||||
label=_("Edited by"),
|
||||
queryset=(
|
||||
lambda request: get_user_model().objects.filter(
|
||||
pk__in=PageLogEntry.objects.filter(action="wagtail.edit")
|
||||
.order_by()
|
||||
.values_list("user_id", flat=True)
|
||||
.distinct()
|
||||
)
|
||||
),
|
||||
widget=CheckboxSelectMultiple,
|
||||
)
|
||||
site = SiteFilter(
|
||||
label=_("Site"),
|
||||
queryset=Site.objects.all(),
|
||||
widget=CheckboxSelectMultiple,
|
||||
)
|
||||
has_child_pages = HasChildPagesFilter(
|
||||
label=_("Has child pages"),
|
||||
empty_label=_("Any"),
|
||||
choices=[
|
||||
("true", _("Yes")),
|
||||
("false", _("No")),
|
||||
],
|
||||
widget=RadioSelect,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Page
|
||||
fields = [] # only needed for filters being generated automatically
|
||||
|
||||
|
||||
class ExplorablePageFilterSet(PageFilterSet):
|
||||
content_type = MultipleContentTypeFilter(
|
||||
label=_("Page type"),
|
||||
queryset=lambda request: get_page_content_types(include_base_page_type=False),
|
||||
widget=CheckboxSelectMultiple,
|
||||
)
|
||||
|
||||
|
||||
class IndexView(generic.IndexView):
|
||||
template_name = "wagtailadmin/pages/index.html"
|
||||
results_template_name = "wagtailadmin/pages/index_results.html"
|
||||
permission_policy = page_permission_policy
|
||||
any_permission_required = {
|
||||
"add",
|
||||
"change",
|
||||
"publish",
|
||||
"bulk_delete",
|
||||
"lock",
|
||||
"unlock",
|
||||
}
|
||||
context_object_name = "pages"
|
||||
page_kwarg = "p"
|
||||
paginate_by = 50
|
||||
table_class = PageTable
|
||||
table_classname = "listing full-width"
|
||||
filterset_class = PageFilterSet
|
||||
index_url_name = None
|
||||
index_results_url_name = None
|
||||
default_ordering = "-latest_revision_created_at"
|
||||
model = Page
|
||||
_show_breadcrumbs = True
|
||||
|
||||
columns = [
|
||||
BulkActionsColumn("bulk_actions"),
|
||||
PageTitleColumn(
|
||||
"title",
|
||||
label=_("Title"),
|
||||
sort_key="title",
|
||||
classname="title",
|
||||
),
|
||||
DateColumn(
|
||||
"latest_revision_created_at",
|
||||
label=_("Updated"),
|
||||
sort_key="latest_revision_created_at",
|
||||
width="12%",
|
||||
),
|
||||
PageStatusColumn(
|
||||
"status",
|
||||
label=_("Status"),
|
||||
sort_key="live",
|
||||
width="12%",
|
||||
),
|
||||
]
|
||||
|
||||
def get(self, request):
|
||||
# Search
|
||||
self.query_string = None
|
||||
self.is_searching = False
|
||||
if "q" in self.request.GET:
|
||||
self.search_form = SearchForm(self.request.GET)
|
||||
if self.search_form.is_valid():
|
||||
self.query_string = self.search_form.cleaned_data["q"]
|
||||
else:
|
||||
self.search_form = SearchForm()
|
||||
|
||||
if self.query_string:
|
||||
self.is_searching = True
|
||||
|
||||
return super().get(request)
|
||||
|
||||
def get_valid_orderings(self):
|
||||
valid_orderings = super().get_valid_orderings()
|
||||
|
||||
if self.is_searching:
|
||||
# ordering by content type not currently available when searching, due to
|
||||
# https://github.com/wagtail/wagtail/issues/6616
|
||||
try:
|
||||
valid_orderings.remove("content_type")
|
||||
valid_orderings.remove("-content_type")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return valid_orderings
|
||||
|
||||
def get_ordering(self):
|
||||
if self.is_searching and not self.is_explicitly_ordered:
|
||||
# default to ordering by relevance
|
||||
default_ordering = None
|
||||
else:
|
||||
default_ordering = self.default_ordering
|
||||
|
||||
ordering = self.request.GET.get("ordering", default_ordering)
|
||||
if ordering not in self.get_valid_orderings():
|
||||
ordering = default_ordering
|
||||
|
||||
return ordering
|
||||
|
||||
def get_base_queryset(self):
|
||||
pages = self.model.objects.filter(depth__gt=1)
|
||||
pages = self._annotate_queryset(pages)
|
||||
return pages
|
||||
|
||||
def _annotate_queryset(self, pages):
|
||||
pages = pages.prefetch_related("content_type", "sites_rooted_here").filter(
|
||||
pk__in=self.permission_policy.explorable_instances(
|
||||
self.request.user
|
||||
).values_list("pk", flat=True)
|
||||
)
|
||||
|
||||
# We want specific page instances, but do not need streamfield values here
|
||||
pages = pages.defer_streamfields().specific()
|
||||
|
||||
# Annotate queryset with various states to be used later for performance optimisations
|
||||
if getattr(settings, "WAGTAIL_WORKFLOW_ENABLED", True):
|
||||
pages = pages.prefetch_workflow_states()
|
||||
|
||||
pages = pages.annotate_site_root_state().annotate_approved_schedule()
|
||||
|
||||
return pages
|
||||
|
||||
def order_queryset(self, queryset):
|
||||
if self.is_searching and not self.is_explicitly_ordered:
|
||||
# search backend will order by relevance in this case, so don't bother to
|
||||
# apply an ordering on the queryset
|
||||
return queryset
|
||||
|
||||
if self.ordering == "ord":
|
||||
# preserve the native ordering from get_children()
|
||||
pass
|
||||
elif self.ordering == "latest_revision_created_at" and not self.is_searching:
|
||||
# order by oldest revision first.
|
||||
# Special case NULL entries - these should go at the top of the list.
|
||||
# Skip this special case when searching (and fall through to plain field ordering
|
||||
# instead) as search backends do not support F objects in order_by
|
||||
queryset = queryset.order_by(
|
||||
F("latest_revision_created_at").asc(nulls_first=True)
|
||||
)
|
||||
elif self.ordering == "-latest_revision_created_at" and not self.is_searching:
|
||||
# order by oldest revision first.
|
||||
# Special case NULL entries - these should go at the end of the list.
|
||||
# Skip this special case when searching (and fall through to plain field ordering
|
||||
# instead) as search backends do not support F objects in order_by
|
||||
queryset = queryset.order_by(
|
||||
F("latest_revision_created_at").desc(nulls_last=True)
|
||||
)
|
||||
else:
|
||||
queryset = super().order_queryset(queryset)
|
||||
|
||||
return queryset
|
||||
|
||||
def search_queryset(self, queryset):
|
||||
if self.is_searching:
|
||||
queryset = queryset.autocomplete(
|
||||
self.query_string, order_by_relevance=(not self.is_explicitly_ordered)
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
def get_index_url(self):
|
||||
return reverse(self.index_url_name)
|
||||
|
||||
def get_index_results_url(self):
|
||||
return reverse(self.index_results_url_name)
|
||||
|
||||
def get_breadcrumbs_items(self):
|
||||
return self.breadcrumbs_items + [{"url": "", "label": self.get_page_title()}]
|
||||
|
||||
def get_table_kwargs(self):
|
||||
kwargs = super().get_table_kwargs()
|
||||
kwargs["actions_next_url"] = self.index_url
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context.update(
|
||||
{
|
||||
"ordering": self.ordering,
|
||||
"search_form": self.search_form,
|
||||
"is_searching": self.is_searching,
|
||||
}
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class ExplorableIndexView(IndexView):
|
||||
"""
|
||||
A version of the page listing where the user is presented with a view of a specified parent page;
|
||||
normally this will show the children of that page, but it may show results from the whole tree while
|
||||
searching or filtering.
|
||||
"""
|
||||
|
||||
template_name = "wagtailadmin/pages/explorable_index.html"
|
||||
index_url_name = "wagtailadmin_explore"
|
||||
index_results_url_name = "wagtailadmin_explore_results"
|
||||
page_title = _("Exploring")
|
||||
filterset_class = ExplorablePageFilterSet
|
||||
|
||||
@classproperty
|
||||
def columns(cls):
|
||||
columns = super().columns.copy()
|
||||
columns.insert(
|
||||
3,
|
||||
Column(
|
||||
"type",
|
||||
label=_("Type"),
|
||||
accessor="page_type_display_name",
|
||||
sort_key="content_type",
|
||||
width="12%",
|
||||
),
|
||||
)
|
||||
columns.append(NavigateToChildrenColumn("navigate", width="10%"))
|
||||
return columns
|
||||
|
||||
def get(self, request, parent_page_id=None):
|
||||
if parent_page_id:
|
||||
self.parent_page = get_object_or_404(
|
||||
Page.objects.all().prefetch_workflow_states(), id=parent_page_id
|
||||
)
|
||||
else:
|
||||
self.parent_page = Page.get_first_root_node()
|
||||
|
||||
# This will always succeed because of the check performed by PermissionCheckedMixin.
|
||||
root_page = self.permission_policy.explorable_root_instance(request.user)
|
||||
|
||||
# If this page isn't a descendant of the user's explorable root page,
|
||||
# then redirect to that explorable root page instead.
|
||||
if not (
|
||||
self.parent_page.pk == root_page.pk
|
||||
or self.parent_page.is_descendant_of(root_page)
|
||||
):
|
||||
return redirect(self.index_url_name, root_page.pk)
|
||||
|
||||
self.parent_page = self.parent_page.specific
|
||||
self.scheduled_page = self.parent_page.get_scheduled_revision_as_object()
|
||||
|
||||
self.i18n_enabled = getattr(settings, "WAGTAIL_I18N_ENABLED", False)
|
||||
if self.i18n_enabled and not self.parent_page.is_root():
|
||||
self.locale = self.parent_page.locale
|
||||
self.translations = self.get_translations()
|
||||
else:
|
||||
self.locale = None
|
||||
self.translations = []
|
||||
|
||||
return super().get(request)
|
||||
|
||||
@cached_property
|
||||
def is_searching_whole_tree(self):
|
||||
return bool(self.request.GET.get("search_all")) and (
|
||||
self.is_searching or self.is_filtering
|
||||
)
|
||||
|
||||
def get_base_queryset(self):
|
||||
if self.is_searching or self.is_filtering:
|
||||
if self.is_searching_whole_tree:
|
||||
pages = Page.objects.all()
|
||||
else:
|
||||
pages = self.parent_page.get_descendants()
|
||||
else:
|
||||
pages = self.parent_page.get_children()
|
||||
|
||||
pages = self._annotate_queryset(pages)
|
||||
return pages
|
||||
|
||||
def search_queryset(self, queryset):
|
||||
# allow hooks to modify queryset. This should happen as close as possible to the
|
||||
# final queryset, but (for backward compatibility) needs to be passed an actual queryset
|
||||
# rather than a search result object
|
||||
for hook in hooks.get_hooks("construct_explorer_page_queryset"):
|
||||
queryset = hook(self.parent_page, queryset, self.request)
|
||||
return super().search_queryset(queryset)
|
||||
|
||||
def get_index_url(self):
|
||||
return reverse(self.index_url_name, args=[self.parent_page.id])
|
||||
|
||||
def get_index_results_url(self):
|
||||
return reverse(self.index_results_url_name, args=[self.parent_page.id])
|
||||
|
||||
def get_history_url(self):
|
||||
permissions = self.parent_page.permissions_for_user(self.request.user)
|
||||
if permissions.can_view_revisions():
|
||||
return reverse("wagtailadmin_pages:history", args=[self.parent_page.id])
|
||||
|
||||
def get_table_kwargs(self):
|
||||
kwargs = super().get_table_kwargs()
|
||||
kwargs["use_row_ordering_attributes"] = self.show_ordering_column
|
||||
kwargs["parent_page"] = self.parent_page
|
||||
kwargs["show_locale_labels"] = self.i18n_enabled and self.parent_page.is_root()
|
||||
|
||||
if self.show_ordering_column:
|
||||
kwargs["caption"] = _(
|
||||
"Focus on the drag button and press up or down arrows to move the item, then press enter to submit the change."
|
||||
)
|
||||
kwargs["attrs"] = {
|
||||
"data-controller": "w-orderable",
|
||||
"data-w-orderable-active-class": "w-orderable--active",
|
||||
"data-w-orderable-chosen-class": "w-orderable__item--active",
|
||||
"data-w-orderable-container-value": "tbody",
|
||||
"data-w-orderable-message-value": _(
|
||||
"'%(page_title)s' has been moved successfully."
|
||||
)
|
||||
% {"page_title": "__LABEL__"},
|
||||
"data-w-orderable-url-value": reverse(
|
||||
"wagtailadmin_pages:set_page_position", args=[999999]
|
||||
),
|
||||
}
|
||||
return kwargs
|
||||
|
||||
def get_valid_orderings(self):
|
||||
valid_orderings = super().get_valid_orderings()
|
||||
|
||||
if not self.is_searching:
|
||||
# ordering by page order is only available when not searching
|
||||
valid_orderings.append("ord")
|
||||
|
||||
return valid_orderings
|
||||
|
||||
def get_ordering(self):
|
||||
if self.is_searching and not self.is_explicitly_ordered:
|
||||
# default to ordering by relevance
|
||||
default_ordering = None
|
||||
else:
|
||||
default_ordering = self.parent_page.get_admin_default_ordering()
|
||||
|
||||
ordering = self.request.GET.get("ordering", default_ordering)
|
||||
if ordering not in self.get_valid_orderings():
|
||||
ordering = default_ordering
|
||||
|
||||
return ordering
|
||||
|
||||
def get_paginate_by(self, queryset):
|
||||
if self.ordering == "ord":
|
||||
# Don't paginate if sorting by page order - all pages must be shown to
|
||||
# allow drag-and-drop reordering
|
||||
return None
|
||||
else:
|
||||
return self.paginate_by
|
||||
|
||||
def get_page_subtitle(self):
|
||||
return self.parent_page.get_admin_display_title()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
self.show_ordering_column = self.ordering == "ord"
|
||||
if self.show_ordering_column:
|
||||
self.columns = self.columns.copy()
|
||||
self.columns[0] = OrderingColumn("ordering", width="80px", sort_key="ord")
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
if self.is_searching:
|
||||
# postprocess this page of results to annotate each result with its parent page
|
||||
parent_page_paths = {
|
||||
page.path[: -page.steplen] for page in context["object_list"]
|
||||
}
|
||||
parent_pages_by_path = {
|
||||
page.path: page
|
||||
for page in Page.objects.filter(path__in=parent_page_paths).specific()
|
||||
}
|
||||
for page in context["object_list"]:
|
||||
parent_page = parent_pages_by_path.get(page.path[: -page.steplen])
|
||||
# add annotation if parent page is found and is not the currently viewed parent
|
||||
if parent_page and parent_page != self.parent_page:
|
||||
page.annotated_parent_page = parent_page
|
||||
|
||||
context.update(
|
||||
{
|
||||
"parent_page": self.parent_page,
|
||||
"history_url": self.get_history_url(),
|
||||
"is_searching_whole_tree": self.is_searching_whole_tree,
|
||||
}
|
||||
)
|
||||
|
||||
if not self.results_only:
|
||||
side_panels = self.get_side_panels()
|
||||
context["side_panels"] = side_panels
|
||||
context["media"] += side_panels.media
|
||||
|
||||
return context
|
||||
|
||||
def get_side_panels(self):
|
||||
# Don't show side panels on the root page
|
||||
if self.parent_page.is_root():
|
||||
return MediaContainer()
|
||||
|
||||
side_panels = [
|
||||
PageStatusSidePanel(
|
||||
self.parent_page.get_latest_revision_as_object(),
|
||||
self.request,
|
||||
show_schedule_publishing_toggle=False,
|
||||
live_object=self.parent_page,
|
||||
scheduled_object=self.scheduled_page,
|
||||
locale=self.locale,
|
||||
translations=self.translations,
|
||||
),
|
||||
]
|
||||
return MediaContainer(side_panels)
|
||||
|
||||
def get_translations(self):
|
||||
return [
|
||||
{
|
||||
"locale": translation.locale,
|
||||
"url": reverse(self.index_url_name, args=[translation.id]),
|
||||
}
|
||||
for translation in self.parent_page.get_translations()
|
||||
.only("id", "locale")
|
||||
.select_related("locale")
|
||||
]
|
||||
38
env/lib/python3.10/site-packages/wagtail/admin/views/pages/lock.py
vendored
Normal file
38
env/lib/python3.10/site-packages/wagtail/admin/views/pages/lock.py
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from wagtail.admin.views.generic import lock
|
||||
from wagtail.models import Page
|
||||
|
||||
|
||||
class PageOperationViewMixin:
|
||||
model = Page
|
||||
pk_url_kwarg = "page_id"
|
||||
|
||||
def get_object(self):
|
||||
return super().get_object().specific
|
||||
|
||||
def get_success_url(self):
|
||||
if self.next_url:
|
||||
return self.next_url
|
||||
return reverse("wagtailadmin_explore", args=[self.object.get_parent().id])
|
||||
|
||||
|
||||
class LockView(PageOperationViewMixin, lock.LockView):
|
||||
def perform_operation(self):
|
||||
if not self.object.permissions_for_user(self.request.user).can_lock():
|
||||
raise PermissionDenied
|
||||
return super().perform_operation()
|
||||
|
||||
|
||||
class UnlockView(PageOperationViewMixin, lock.UnlockView):
|
||||
def perform_operation(self):
|
||||
if not self.object.permissions_for_user(self.request.user).can_unlock():
|
||||
raise PermissionDenied
|
||||
return super().perform_operation()
|
||||
|
||||
def get_success_message(self):
|
||||
return _("Page '%(page_title)s' is now unlocked.") % {
|
||||
"page_title": self.object.get_admin_display_title()
|
||||
}
|
||||
142
env/lib/python3.10/site-packages/wagtail/admin/views/pages/move.py
vendored
Normal file
142
env/lib/python3.10/site-packages/wagtail/admin/views/pages/move.py
vendored
Normal file
@@ -0,0 +1,142 @@
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from wagtail import hooks
|
||||
from wagtail.actions.move_page import MovePageAction
|
||||
from wagtail.admin import messages
|
||||
from wagtail.admin.forms.pages import MoveForm
|
||||
from wagtail.models import Page
|
||||
|
||||
|
||||
def move_choose_destination(request, page_to_move_id):
|
||||
page_to_move = get_object_or_404(Page, id=page_to_move_id)
|
||||
page_perms = page_to_move.permissions_for_user(request.user)
|
||||
if not page_perms.can_move():
|
||||
raise PermissionDenied
|
||||
|
||||
target_parent_models = set(page_to_move.specific_class.allowed_parent_page_models())
|
||||
|
||||
move_form = MoveForm(
|
||||
request.POST or None,
|
||||
page_to_move=page_to_move,
|
||||
target_parent_models=target_parent_models,
|
||||
)
|
||||
|
||||
if request.method == "POST":
|
||||
if move_form.is_valid():
|
||||
# Receive the new parent page (this should never be empty)
|
||||
if move_form.cleaned_data["new_parent_page"]:
|
||||
new_parent_page = move_form.cleaned_data["new_parent_page"]
|
||||
return redirect(
|
||||
"wagtailadmin_pages:move_confirm",
|
||||
page_to_move.id,
|
||||
new_parent_page.id,
|
||||
)
|
||||
|
||||
return TemplateResponse(
|
||||
request,
|
||||
"wagtailadmin/pages/move_choose_destination.html",
|
||||
{
|
||||
"page_to_move": page_to_move,
|
||||
"move_form": move_form,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def move_confirm(request, page_to_move_id, destination_id):
|
||||
page_to_move = get_object_or_404(Page, id=page_to_move_id).specific
|
||||
# Needs .specific_deferred because the .get_admin_display_title method is called in template
|
||||
destination = get_object_or_404(Page, id=destination_id).specific_deferred
|
||||
|
||||
if not Page._slug_is_available(page_to_move.slug, destination, page=page_to_move):
|
||||
messages.error(
|
||||
request,
|
||||
_(
|
||||
"The slug '%(page_slug)s' is already in use at the selected parent page. Make sure the slug is unique and try again"
|
||||
)
|
||||
% {"page_slug": page_to_move.slug},
|
||||
)
|
||||
return redirect(
|
||||
"wagtailadmin_pages:move",
|
||||
page_to_move.id,
|
||||
)
|
||||
|
||||
for fn in hooks.get_hooks("before_move_page"):
|
||||
result = fn(request, page_to_move, destination)
|
||||
if hasattr(result, "status_code"):
|
||||
return result
|
||||
|
||||
pages_to_move = {page_to_move}
|
||||
|
||||
# The `construct_translated_pages_to_cascade_actions` hook returns translation and
|
||||
# alias pages when the action is set to "move"
|
||||
if getattr(settings, "WAGTAIL_I18N_ENABLED", False):
|
||||
for fn in hooks.get_hooks("construct_translated_pages_to_cascade_actions"):
|
||||
fn_pages = fn([page_to_move], "move")
|
||||
if fn_pages and isinstance(fn_pages, dict):
|
||||
for additional_pages in fn_pages.values():
|
||||
pages_to_move.update(additional_pages)
|
||||
|
||||
pages_to_move = list(pages_to_move)
|
||||
|
||||
if request.method == "POST":
|
||||
# any invalid moves *should* be caught by the permission check in the action
|
||||
# class, so don't bother to catch InvalidMoveToDescendant
|
||||
action = MovePageAction(
|
||||
page_to_move, destination, pos="last-child", user=request.user
|
||||
)
|
||||
action.execute()
|
||||
|
||||
if getattr(settings, "WAGTAIL_I18N_ENABLED", False):
|
||||
# Move translation and alias pages if they have the same parent page.
|
||||
parent_page_translations = page_to_move.get_parent().get_translations()
|
||||
for translation in pages_to_move:
|
||||
if translation.get_parent() in parent_page_translations:
|
||||
# Move the translated or alias page to it's translated or
|
||||
# alias "destination" page.
|
||||
action = MovePageAction(
|
||||
translation,
|
||||
destination.get_translation(translation.locale),
|
||||
pos="last-child",
|
||||
user=request.user,
|
||||
)
|
||||
action.execute()
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
_("Page '%(page_title)s' moved.")
|
||||
% {"page_title": page_to_move.get_admin_display_title()},
|
||||
buttons=[
|
||||
messages.button(
|
||||
reverse("wagtailadmin_pages:edit", args=(page_to_move.id,)),
|
||||
_("Edit"),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
for fn in hooks.get_hooks("after_move_page"):
|
||||
result = fn(request, page_to_move)
|
||||
if hasattr(result, "status_code"):
|
||||
return result
|
||||
|
||||
return redirect("wagtailadmin_explore", destination.id)
|
||||
|
||||
return TemplateResponse(
|
||||
request,
|
||||
"wagtailadmin/pages/confirm_move.html",
|
||||
{
|
||||
"page_to_move": page_to_move,
|
||||
"destination": destination,
|
||||
"translations_to_move_count": len(
|
||||
[
|
||||
translation.id
|
||||
for translation in pages_to_move
|
||||
if not translation.alias_of_id and translation.id != page_to_move.id
|
||||
]
|
||||
),
|
||||
},
|
||||
)
|
||||
44
env/lib/python3.10/site-packages/wagtail/admin/views/pages/ordering.py
vendored
Normal file
44
env/lib/python3.10/site-packages/wagtail/admin/views/pages/ordering.py
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from wagtail.models import Page
|
||||
|
||||
|
||||
def set_page_position(request, page_to_move_id):
|
||||
page_to_move = get_object_or_404(Page, id=page_to_move_id)
|
||||
parent_page = page_to_move.get_parent()
|
||||
|
||||
if not parent_page.permissions_for_user(request.user).can_reorder_children():
|
||||
raise PermissionDenied
|
||||
|
||||
if request.method == "POST":
|
||||
# Get position parameter
|
||||
position = request.GET.get("position", None)
|
||||
|
||||
# Find page that's already in this position
|
||||
position_page = None
|
||||
if position is not None:
|
||||
try:
|
||||
position_page = parent_page.get_children()[int(position)]
|
||||
except IndexError:
|
||||
pass # No page in this position
|
||||
|
||||
# Move page
|
||||
|
||||
# any invalid moves *should* be caught by the permission check above,
|
||||
# so don't bother to catch InvalidMoveToDescendant
|
||||
|
||||
if position_page:
|
||||
# If the page has been moved to the right, insert it to the
|
||||
# right. If left, then left.
|
||||
old_position = list(parent_page.get_children()).index(page_to_move)
|
||||
if int(position) < old_position:
|
||||
page_to_move.move(position_page, pos="left", user=request.user)
|
||||
elif int(position) > old_position:
|
||||
page_to_move.move(position_page, pos="right", user=request.user)
|
||||
else:
|
||||
# Move page to end
|
||||
page_to_move.move(parent_page, pos="last-child", user=request.user)
|
||||
|
||||
return HttpResponse("")
|
||||
103
env/lib/python3.10/site-packages/wagtail/admin/views/pages/preview.py
vendored
Normal file
103
env/lib/python3.10/site-packages/wagtail/admin/views/pages/preview.py
vendored
Normal file
@@ -0,0 +1,103 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from wagtail.admin.views.generic.preview import PreviewOnEdit as GenericPreviewOnEdit
|
||||
from wagtail.models import Page
|
||||
|
||||
|
||||
def view_draft(request, page_id):
|
||||
page = get_object_or_404(Page, id=page_id).get_latest_revision_as_object()
|
||||
perms = page.permissions_for_user(request.user)
|
||||
if not (perms.can_publish() or perms.can_edit()):
|
||||
raise PermissionDenied
|
||||
|
||||
try:
|
||||
preview_mode = page.default_preview_mode
|
||||
except IndexError:
|
||||
raise PermissionDenied
|
||||
|
||||
return page.make_preview_request(request, preview_mode)
|
||||
|
||||
|
||||
class PreviewOnEdit(GenericPreviewOnEdit):
|
||||
@property
|
||||
def session_key(self):
|
||||
return "{}{}".format(self.session_key_prefix, self.kwargs["page_id"])
|
||||
|
||||
def get_object(self):
|
||||
return get_object_or_404(
|
||||
Page, id=self.kwargs["page_id"]
|
||||
).get_latest_revision_as_object()
|
||||
|
||||
def get_form(self, query_dict):
|
||||
form_class = self.object.get_edit_handler().get_form_class()
|
||||
parent_page = self.object.get_parent().specific
|
||||
|
||||
if not query_dict:
|
||||
# Query dict is empty, return null form
|
||||
return form_class(
|
||||
instance=self.object,
|
||||
parent_page=parent_page,
|
||||
for_user=self.request.user,
|
||||
)
|
||||
|
||||
return form_class(
|
||||
query_dict,
|
||||
instance=self.object,
|
||||
parent_page=parent_page,
|
||||
for_user=self.request.user,
|
||||
)
|
||||
|
||||
|
||||
class PreviewOnCreate(PreviewOnEdit):
|
||||
@property
|
||||
def session_key(self):
|
||||
return "{}{}-{}-{}".format(
|
||||
self.session_key_prefix,
|
||||
self.kwargs["content_type_app_name"],
|
||||
self.kwargs["content_type_model_name"],
|
||||
self.kwargs["parent_page_id"],
|
||||
)
|
||||
|
||||
def get_object(self):
|
||||
content_type_app_name = self.kwargs["content_type_app_name"]
|
||||
content_type_model_name = self.kwargs["content_type_model_name"]
|
||||
parent_page_id = self.kwargs["parent_page_id"]
|
||||
try:
|
||||
content_type = ContentType.objects.get_by_natural_key(
|
||||
content_type_app_name, content_type_model_name
|
||||
)
|
||||
except ContentType.DoesNotExist:
|
||||
raise Http404
|
||||
|
||||
page = content_type.model_class()()
|
||||
parent_page = get_object_or_404(Page, id=parent_page_id).specific
|
||||
# We need to populate treebeard's path / depth fields in order to
|
||||
# pass validation. We can't make these 100% consistent with the rest
|
||||
# of the tree without making actual database changes (such as
|
||||
# incrementing the parent's numchild field), but by calling treebeard's
|
||||
# internal _get_path method, we can set a 'realistic' value that will
|
||||
# hopefully enable tree traversal operations
|
||||
# to at least partially work.
|
||||
page.depth = parent_page.depth + 1
|
||||
# Puts the page at the next available path
|
||||
# for a child of `parent_page`.
|
||||
if parent_page.is_leaf():
|
||||
# set the path as the first child of parent_page
|
||||
page.path = page._get_path(parent_page.path, page.depth, 1)
|
||||
else:
|
||||
# add the new page after the last child of parent_page
|
||||
page.path = parent_page.get_last_child()._inc_path()
|
||||
|
||||
return page
|
||||
|
||||
def get_form(self, query_dict):
|
||||
form = super().get_form(query_dict)
|
||||
if form.is_valid():
|
||||
# Ensures our unsaved page has a suitable url.
|
||||
form.instance.set_url_path(form.parent_page)
|
||||
|
||||
form.instance.full_clean()
|
||||
return form
|
||||
206
env/lib/python3.10/site-packages/wagtail/admin/views/pages/revisions.py
vendored
Normal file
206
env/lib/python3.10/site-packages/wagtail/admin/views/pages/revisions.py
vendored
Normal file
@@ -0,0 +1,206 @@
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.template.loader import render_to_string
|
||||
from django.template.response import TemplateResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy
|
||||
|
||||
from wagtail.admin import messages
|
||||
from wagtail.admin.action_menu import PageActionMenu
|
||||
from wagtail.admin.auth import user_has_any_page_permission, user_passes_test
|
||||
from wagtail.admin.ui.components import MediaContainer
|
||||
from wagtail.admin.ui.side_panels import (
|
||||
ChecksSidePanel,
|
||||
CommentsSidePanel,
|
||||
PageStatusSidePanel,
|
||||
PreviewSidePanel,
|
||||
)
|
||||
from wagtail.admin.views.generic.models import (
|
||||
RevisionsCompareView,
|
||||
RevisionsUnscheduleView,
|
||||
)
|
||||
from wagtail.admin.views.generic.preview import PreviewRevision
|
||||
from wagtail.models import Page
|
||||
from wagtail.utils.timestamps import render_timestamp
|
||||
|
||||
|
||||
def revisions_index(request, page_id):
|
||||
return redirect("wagtailadmin_pages:history", page_id)
|
||||
|
||||
|
||||
def revisions_revert(request, page_id, revision_id):
|
||||
# TODO: refactor this into a class-based view that extends the EditView
|
||||
page = get_object_or_404(Page, id=page_id).specific
|
||||
page_perms = page.permissions_for_user(request.user)
|
||||
if not page_perms.can_edit():
|
||||
raise PermissionDenied
|
||||
|
||||
revision = get_object_or_404(page.revisions, id=revision_id)
|
||||
revision_page = revision.as_object()
|
||||
|
||||
scheduled_page = page.get_scheduled_revision_as_object()
|
||||
|
||||
content_type = ContentType.objects.get_for_model(page)
|
||||
page_class = content_type.model_class()
|
||||
|
||||
if getattr(settings, "WAGTAIL_I18N_ENABLED", False):
|
||||
locale = page.locale
|
||||
translations = [
|
||||
{
|
||||
"locale": translation.locale,
|
||||
"url": reverse("wagtailadmin_pages:edit", args=[translation.id]),
|
||||
}
|
||||
for translation in page.get_translations()
|
||||
.only("id", "locale", "depth")
|
||||
.select_related("locale")
|
||||
if translation.permissions_for_user(request.user).can_edit()
|
||||
]
|
||||
else:
|
||||
locale = None
|
||||
translations = []
|
||||
|
||||
edit_handler = page_class.get_edit_handler()
|
||||
form_class = edit_handler.get_form_class()
|
||||
|
||||
form = form_class(instance=revision_page, for_user=request.user)
|
||||
edit_handler = edit_handler.get_bound_panel(
|
||||
instance=revision_page, request=request, form=form
|
||||
)
|
||||
|
||||
preview_url = reverse("wagtailadmin_pages:preview_on_edit", args=[page.id])
|
||||
lock = page.get_lock()
|
||||
|
||||
action_menu = PageActionMenu(
|
||||
request,
|
||||
view="revisions_revert",
|
||||
is_revision=True,
|
||||
page=page,
|
||||
lock=lock,
|
||||
locked_for_user=lock is not None and lock.for_user(request.user),
|
||||
)
|
||||
side_panels = [
|
||||
PageStatusSidePanel(
|
||||
revision_page,
|
||||
request,
|
||||
show_schedule_publishing_toggle=form.show_schedule_publishing_toggle,
|
||||
live_object=page,
|
||||
scheduled_object=scheduled_page,
|
||||
locale=locale,
|
||||
translations=translations,
|
||||
),
|
||||
]
|
||||
if page.is_previewable():
|
||||
side_panels.append(PreviewSidePanel(page, request, preview_url=preview_url))
|
||||
side_panels.append(ChecksSidePanel(page, request))
|
||||
if form.show_comments_toggle:
|
||||
side_panels.append(CommentsSidePanel(page, request))
|
||||
side_panels = MediaContainer(side_panels)
|
||||
|
||||
media = MediaContainer([edit_handler, form, action_menu, side_panels]).media
|
||||
|
||||
user_avatar = render_to_string(
|
||||
"wagtailadmin/shared/user_avatar.html", {"user": revision.user}
|
||||
)
|
||||
|
||||
messages.warning(
|
||||
request,
|
||||
mark_safe(
|
||||
_(
|
||||
"You are viewing a previous version of this page from <b>%(created_at)s</b> by %(user)s"
|
||||
)
|
||||
% {
|
||||
"created_at": render_timestamp(revision.created_at),
|
||||
"user": user_avatar,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
page_title = _("Editing %(page_type)s") % {
|
||||
"page_type": page_class.get_verbose_name()
|
||||
}
|
||||
page_subtitle = page.get_admin_display_title()
|
||||
header_title = f"{page_title}: {page_subtitle}"
|
||||
|
||||
return TemplateResponse(
|
||||
request,
|
||||
"wagtailadmin/pages/edit.html",
|
||||
{
|
||||
"page": page,
|
||||
"revision": revision,
|
||||
"is_revision": True,
|
||||
"content_type": content_type,
|
||||
"edit_handler": edit_handler,
|
||||
"errors_debug": None,
|
||||
"action_menu": action_menu,
|
||||
"side_panels": side_panels,
|
||||
"header_title": header_title,
|
||||
"form": form, # Used in unit tests
|
||||
"media": media,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@method_decorator(user_passes_test(user_has_any_page_permission), name="dispatch")
|
||||
class RevisionsView(PreviewRevision):
|
||||
model = Page
|
||||
|
||||
def setup(self, request, page_id, revision_id, *args, **kwargs):
|
||||
# Rename path kwargs from pk to page_id
|
||||
return super().setup(request, page_id, revision_id, *args, **kwargs)
|
||||
|
||||
def get_object(self):
|
||||
page = get_object_or_404(Page, id=self.pk).specific
|
||||
|
||||
perms = page.permissions_for_user(self.request.user)
|
||||
if not (perms.can_publish() or perms.can_edit()):
|
||||
raise PermissionDenied
|
||||
|
||||
return page
|
||||
|
||||
|
||||
class RevisionsCompare(RevisionsCompareView):
|
||||
history_label = gettext_lazy("Page history")
|
||||
edit_label = gettext_lazy("Edit this page")
|
||||
history_url_name = "wagtailadmin_pages:history"
|
||||
edit_url_name = "wagtailadmin_pages:edit"
|
||||
header_icon = "doc-empty-inverse"
|
||||
|
||||
@method_decorator(user_passes_test(user_has_any_page_permission))
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(Page, id=self.pk).specific
|
||||
|
||||
def get_edit_handler(self):
|
||||
return self.object.get_edit_handler()
|
||||
|
||||
def get_page_subtitle(self):
|
||||
return self.object.get_admin_display_title()
|
||||
|
||||
|
||||
class RevisionsUnschedule(RevisionsUnscheduleView):
|
||||
model = Page
|
||||
edit_url_name = "wagtailadmin_pages:edit"
|
||||
history_url_name = "wagtailadmin_pages:history"
|
||||
revisions_unschedule_url_name = "wagtailadmin_pages:revisions_unschedule"
|
||||
header_icon = "doc-empty-inverse"
|
||||
|
||||
def setup(self, request, page_id, revision_id, *args, **kwargs):
|
||||
# Rename path kwargs from pk to page_id
|
||||
return super().setup(request, page_id, revision_id, *args, **kwargs)
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
page = get_object_or_404(Page, id=self.pk).specific
|
||||
|
||||
if not page.permissions_for_user(self.request.user).can_unschedule():
|
||||
raise PermissionDenied
|
||||
return page
|
||||
|
||||
def get_object_display_title(self):
|
||||
return self.object.get_admin_display_title()
|
||||
192
env/lib/python3.10/site-packages/wagtail/admin/views/pages/search.py
vendored
Normal file
192
env/lib/python3.10/site-packages/wagtail/admin/views/pages/search.py
vendored
Normal file
@@ -0,0 +1,192 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models.query import QuerySet
|
||||
from django.http import Http404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from wagtail.admin.forms.search import SearchForm
|
||||
from wagtail.admin.ui.tables import Column, DateColumn
|
||||
from wagtail.admin.ui.tables.pages import (
|
||||
BulkActionsColumn,
|
||||
NavigateToChildrenColumn,
|
||||
PageStatusColumn,
|
||||
PageTable,
|
||||
PageTitleColumn,
|
||||
ParentPageColumn,
|
||||
)
|
||||
from wagtail.admin.views.generic.base import BaseListingView
|
||||
from wagtail.admin.views.generic.permissions import PermissionCheckedMixin
|
||||
from wagtail.models import Page
|
||||
from wagtail.permissions import page_permission_policy
|
||||
from wagtail.search.query import MATCH_ALL
|
||||
from wagtail.search.utils import parse_query_string
|
||||
|
||||
|
||||
def page_filter_search(q, pages, all_pages=None, ordering=None):
|
||||
# Parse query
|
||||
filters, query = parse_query_string(q, operator="and", zero_terms=MATCH_ALL)
|
||||
|
||||
# Live filter
|
||||
live_filter = filters.get("live") or filters.get("published")
|
||||
live_filter = live_filter and live_filter.lower()
|
||||
|
||||
if live_filter in ["yes", "true"]:
|
||||
if all_pages is not None:
|
||||
all_pages = all_pages.filter(live=True)
|
||||
pages = pages.filter(live=True)
|
||||
elif live_filter in ["no", "false"]:
|
||||
if all_pages is not None:
|
||||
all_pages = all_pages.filter(live=False)
|
||||
pages = pages.filter(live=False)
|
||||
|
||||
# Search
|
||||
if all_pages is not None:
|
||||
all_pages = all_pages.autocomplete(query, order_by_relevance=not ordering)
|
||||
pages = pages.autocomplete(query, order_by_relevance=not ordering)
|
||||
|
||||
return pages, all_pages
|
||||
|
||||
|
||||
class BaseSearchView(PermissionCheckedMixin, BaseListingView):
|
||||
permission_policy = page_permission_policy
|
||||
any_permission_required = {
|
||||
"add",
|
||||
"change",
|
||||
"publish",
|
||||
"bulk_delete",
|
||||
"lock",
|
||||
"unlock",
|
||||
}
|
||||
paginate_by = 20
|
||||
page_kwarg = "p"
|
||||
context_object_name = "pages"
|
||||
table_class = PageTable
|
||||
index_url_name = "wagtailadmin_pages:search"
|
||||
|
||||
columns = [
|
||||
BulkActionsColumn("bulk_actions"),
|
||||
PageTitleColumn(
|
||||
"title",
|
||||
classname="title",
|
||||
label=_("Title"),
|
||||
sort_key="title",
|
||||
),
|
||||
ParentPageColumn("parent", label=_("Parent")),
|
||||
DateColumn(
|
||||
"latest_revision_created_at",
|
||||
label=_("Updated"),
|
||||
sort_key="latest_revision_created_at",
|
||||
width="12%",
|
||||
),
|
||||
Column(
|
||||
"type",
|
||||
label=_("Type"),
|
||||
accessor="page_type_display_name",
|
||||
width="12%",
|
||||
),
|
||||
PageStatusColumn(
|
||||
"status",
|
||||
label=_("Status"),
|
||||
sort_key="live",
|
||||
width="12%",
|
||||
),
|
||||
NavigateToChildrenColumn("navigate", width="10%"),
|
||||
]
|
||||
|
||||
def get(self, request):
|
||||
self.show_locale_labels = getattr(settings, "WAGTAIL_I18N_ENABLED", False)
|
||||
self.content_types = []
|
||||
self.ordering = None
|
||||
|
||||
if "ordering" in request.GET and request.GET["ordering"] in [
|
||||
"title",
|
||||
"-title",
|
||||
"latest_revision_created_at",
|
||||
"-latest_revision_created_at",
|
||||
"live",
|
||||
"-live",
|
||||
]:
|
||||
self.ordering = request.GET["ordering"]
|
||||
|
||||
if "content_type" in request.GET:
|
||||
try:
|
||||
app_label, model_name = request.GET["content_type"].split(".")
|
||||
except ValueError:
|
||||
raise Http404
|
||||
|
||||
try:
|
||||
self.selected_content_type = ContentType.objects.get_by_natural_key(
|
||||
app_label, model_name
|
||||
)
|
||||
except ContentType.DoesNotExist:
|
||||
raise Http404
|
||||
|
||||
else:
|
||||
self.selected_content_type = None
|
||||
|
||||
self.q = self.request.GET.get("q", "")
|
||||
|
||||
return super().get(request)
|
||||
|
||||
def get_queryset(self) -> QuerySet[Any]:
|
||||
pages = self.all_pages = (
|
||||
Page.objects.all().prefetch_related("content_type").specific()
|
||||
)
|
||||
if self.show_locale_labels:
|
||||
pages = pages.select_related("locale")
|
||||
|
||||
if self.ordering:
|
||||
pages = pages.order_by(self.ordering)
|
||||
|
||||
if self.selected_content_type:
|
||||
pages = pages.filter(content_type=self.selected_content_type)
|
||||
|
||||
# Parse query and filter
|
||||
pages, self.all_pages = page_filter_search(
|
||||
self.q, pages, self.all_pages, self.ordering
|
||||
)
|
||||
|
||||
# Facets
|
||||
if pages.supports_facet:
|
||||
self.content_types = [
|
||||
(ContentType.objects.get(id=content_type_id), count)
|
||||
for content_type_id, count in self.all_pages.facet(
|
||||
"content_type_id"
|
||||
).items()
|
||||
]
|
||||
|
||||
return pages
|
||||
|
||||
def get_table_kwargs(self):
|
||||
kwargs = super().get_table_kwargs()
|
||||
kwargs["show_locale_labels"] = self.show_locale_labels
|
||||
kwargs["actions_next_url"] = self.get_index_url()
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
context.update(
|
||||
{
|
||||
"all_pages": self.all_pages,
|
||||
"query_string": self.q,
|
||||
"content_types": self.content_types,
|
||||
"selected_content_type": self.selected_content_type,
|
||||
"ordering": self.ordering,
|
||||
}
|
||||
)
|
||||
return context
|
||||
|
||||
|
||||
class SearchView(BaseSearchView):
|
||||
template_name = "wagtailadmin/pages/search.html"
|
||||
|
||||
def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["search_form"] = SearchForm(self.request.GET)
|
||||
return context
|
||||
|
||||
|
||||
class SearchResultsView(BaseSearchView):
|
||||
template_name = "wagtailadmin/pages/search_results.html"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user