Initial commit
This commit is contained in:
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},
|
||||
)
|
||||
Reference in New Issue
Block a user