Initial commit

This commit is contained in:
2024-08-27 20:33:44 +02:00
commit 1f1832267d
14794 changed files with 1599592 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
from django.urls import include, path
from wagtail import hooks
from wagtail.admin.viewsets.base import ViewSetGroup
class ViewSetRegistry:
def __init__(self):
self.viewsets = []
def populate(self):
for fn in hooks.get_hooks("register_admin_viewset"):
viewset = fn()
if isinstance(viewset, (list, tuple)):
for vs in viewset:
self.register(vs)
else:
self.register(viewset)
def register(self, viewset):
# Allow registering a ViewSetGroup, which will register all of its
# registerables.
if isinstance(viewset, ViewSetGroup):
for vs in viewset.registerables:
self.register(vs)
viewset.on_register()
return
self.viewsets.append(viewset)
viewset.on_register()
return viewset
def get_urlpatterns(self):
urlpatterns = []
for viewset in self.viewsets:
vs_urlpatterns = viewset.get_urlpatterns()
if vs_urlpatterns:
urlpatterns.append(
path(
f"{viewset.url_prefix}/",
include(
(vs_urlpatterns, viewset.url_namespace),
namespace=viewset.url_namespace,
),
)
)
return urlpatterns
viewsets = ViewSetRegistry()

View File

@@ -0,0 +1,148 @@
from django.core.exceptions import ImproperlyConfigured
from django.urls import reverse
from django.utils.functional import cached_property
from wagtail.admin.menu import WagtailMenuRegisterable, WagtailMenuRegisterableGroup
class ViewSet(WagtailMenuRegisterable):
"""
Defines a viewset to be registered with the Wagtail admin.
All properties of the viewset can be defined as class-level attributes, or passed as
keyword arguments to the constructor (in which case they will override any class-level
attributes). Additionally, the :attr:`name` property can be passed as the first positional
argument to the constructor.
For more information on how to use this class, see :ref:`using_base_viewset`.
"""
#: A special value that, when passed in a kwargs dict to construct a view, indicates that
#: the attribute should not be written and should instead be left as the view's initial value
UNDEFINED = object()
#: A name for this viewset, used as the default URL prefix and namespace.
name = None
#: The icon to use across the views.
icon = ""
def __init__(self, name=None, **kwargs):
if name:
self.__dict__["name"] = name
for key, value in kwargs.items():
self.__dict__[key] = value
def get_common_view_kwargs(self, **kwargs):
"""
Returns a dictionary of keyword arguments to be passed to all views within this viewset.
"""
return kwargs
def construct_view(self, view_class, **kwargs):
"""
Wrapper for view_class.as_view() which passes the kwargs returned from get_common_view_kwargs
in addition to any kwargs passed to this method. Items from get_common_view_kwargs will be
filtered to only include those that are valid for the given view_class.
"""
merged_kwargs = self.get_common_view_kwargs()
merged_kwargs.update(kwargs)
filtered_kwargs = {
key: value
for key, value in merged_kwargs.items()
if hasattr(view_class, key) and value is not self.UNDEFINED
}
return view_class.as_view(**filtered_kwargs)
def inject_view_methods(self, view_class, method_names):
"""
Check for the presence of any of the named methods on this viewset. If any are found,
create a subclass of view_class that overrides those methods to call the implementation
on this viewset instead. Otherwise, return view_class unmodified.
"""
viewset = self
overrides = {}
for method_name in method_names:
viewset_method = getattr(viewset, method_name, None)
if viewset_method:
def view_method(self, *args, **kwargs):
return viewset_method(*args, **kwargs)
view_method.__name__ = method_name
overrides[method_name] = view_method
if overrides:
return type(view_class.__name__, (view_class,), overrides)
else:
return view_class
@cached_property
def url_prefix(self):
"""
The preferred URL prefix for views within this viewset. When registered through
Wagtail's :ref:`register_admin_viewset` hook, this will be used as the URL path component
following ``/admin/``. Other URL registration mechanisms (e.g. editing ``urls.py`` manually)
may disregard this and use a prefix of their own choosing.
Defaults to the viewset's ``name``.
"""
if not self.name:
raise ImproperlyConfigured(
"ViewSet %r must provide a `name` property" % self
)
return self.name
@cached_property
def url_namespace(self):
"""
The URL namespace for views within this viewset. Will be used internally as the
application namespace for the viewset's URLs, and generally be the instance namespace
too.
Defaults to the viewset's ``name``.
"""
if not self.name:
raise ImproperlyConfigured(
"ViewSet %r must provide a `name` property" % self
)
return self.name
def on_register(self):
"""
Called when the viewset is registered; subclasses can override this to perform additional setup.
"""
self.register_menu_item()
def get_urlpatterns(self):
"""
Returns a set of URL routes to be registered with the Wagtail admin.
"""
return []
def get_url_name(self, view_name):
"""
Returns the namespaced URL name for the given view.
"""
return self.url_namespace + ":" + view_name
@cached_property
def menu_icon(self):
return self.icon
@cached_property
def menu_url(self):
return reverse(self.get_url_name(self.get_urlpatterns()[0].name))
class ViewSetGroup(WagtailMenuRegisterableGroup):
"""
A container for grouping together multiple :class:`ViewSet` instances.
Creates a menu item with a submenu for accessing the main URL for each instances.
For more information on how to use this class, see :ref:`using_base_viewsetgroup`.
"""
def on_register(self):
self.register_menu_item()

View File

@@ -0,0 +1,226 @@
from django.db.models import ForeignKey
from django.urls import path
from django.utils.functional import cached_property
from django.utils.translation import gettext as _
from wagtail.admin.forms.models import register_form_field_override
from wagtail.admin.views.generic import chooser as chooser_views
from wagtail.admin.widgets.chooser import BaseChooser
from wagtail.blocks import ChooserBlock
from wagtail.telepath import register as register_telepath_adapter
from .base import ViewSet
class ChooserViewSet(ViewSet):
"""
A viewset that creates a chooser modal interface for choosing model instances.
"""
model = None
icon = "snippet" #: The icon to use in the header of the chooser modal, and on the chooser widget
choose_one_text = _(
"Choose"
) #: Label for the 'choose' button in the chooser widget when choosing an initial item
page_title = None #: Title text for the chooser modal (defaults to the same as ``choose_one_text``)`
choose_another_text = _(
"Choose another"
) #: Label for the 'choose' button in the chooser widget, when an item has already been chosen
edit_item_text = _("Edit") #: Label for the 'edit' button in the chooser widget
per_page = ViewSet.UNDEFINED #: Number of results to show per page
#: A list of URL query parameters that should be passed on unmodified as part of any links or
#: form submissions within the chooser modal workflow.
preserve_url_parameters = ["multiple"]
#: A list of URL query parameters that, if present in the url, should be applied as filters to the queryset.
#: (These should also be listed in `preserve_url_parameters`.)
url_filter_parameters = []
#: The view class to use for the overall chooser modal; must be a subclass of ``wagtail.admin.views.generic.chooser.ChooseView``.
choose_view_class = chooser_views.ChooseView
#: The view class used to render just the results panel within the chooser modal; must be a subclass of ``wagtail.admin.views.generic.chooser.ChooseResultsView``.
choose_results_view_class = chooser_views.ChooseResultsView
#: The view class used after an item has been chosen; must be a subclass of ``wagtail.admin.views.generic.chooser.ChosenView``.
chosen_view_class = chooser_views.ChosenView
#: The view class used after multiple items have been chosen; must be a subclass of ``wagtail.admin.views.generic.chooser.ChosenMultipleView``.
chosen_multiple_view_class = chooser_views.ChosenMultipleView
#: The view class used to handle submissions of the 'create' form; must be a subclass of ``wagtail.admin.views.generic.chooser.CreateView``.
create_view_class = chooser_views.CreateView
#: The base Widget class that the chooser widget will be derived from.
base_widget_class = BaseChooser
#: The adapter class used to map the widget class to its JavaScript implementation - see :ref:`streamfield_widget_api`.
#: Only required if the chooser uses custom JavaScript code.
widget_telepath_adapter_class = None
#: The base ChooserBlock class that the StreamField chooser block will be derived from.
base_block_class = ChooserBlock
#: Defaults to True; if False, the chooser widget will not automatically be registered for use in admin forms.
register_widget = True
#: Form class to use for the form in the "Create" tab of the modal.
creation_form_class = None
#: List of model fields that should be included in the creation form, if creation_form_class is not specified.
form_fields = None
#: List of model fields that should be excluded from the creation form, if creation_form_class.
#: If none of ``creation_form_class``, ``form_fields`` or ``exclude_form_fields`` are specified, the "Create" tab will be omitted.
exclude_form_fields = None
search_tab_label = _("Search") #: Label for the 'search' tab in the chooser modal
create_action_label = _(
"Create"
) #: Label for the submit button on the 'create' form
create_action_clicked_label = None #: Alternative text to display on the submit button after it has been clicked
creation_tab_label = None #: Label for the 'create' tab in the chooser modal (defaults to the same as create_action_label)
permission_policy = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.page_title is None:
self.page_title = self.choose_one_text
def get_common_view_kwargs(self, **kwargs):
return super().get_common_view_kwargs(
**{
"model": self.model,
"permission_policy": self.permission_policy,
"preserve_url_parameters": self.preserve_url_parameters,
"url_filter_parameters": self.url_filter_parameters,
"create_action_label": self.create_action_label,
"create_action_clicked_label": self.create_action_clicked_label,
"creation_form_class": self.creation_form_class,
"form_fields": self.form_fields,
"exclude_form_fields": self.exclude_form_fields,
"chosen_url_name": self.get_url_name("chosen"),
"chosen_multiple_url_name": self.get_url_name("chosen_multiple"),
"results_url_name": self.get_url_name("choose_results"),
"create_url_name": self.get_url_name("create"),
"per_page": self.per_page,
**kwargs,
}
)
@property
def choose_view(self):
view_class = self.inject_view_methods(
self.choose_view_class, ["get_object_list"]
)
return self.construct_view(
view_class,
icon=self.icon,
page_title=self.page_title,
search_tab_label=self.search_tab_label,
creation_tab_label=self.creation_tab_label,
)
@property
def choose_results_view(self):
view_class = self.inject_view_methods(
self.choose_results_view_class, ["get_object_list"]
)
return self.construct_view(view_class)
@property
def chosen_view(self):
return self.construct_view(self.chosen_view_class)
@property
def chosen_multiple_view(self):
return self.construct_view(self.chosen_multiple_view_class)
@property
def create_view(self):
return self.construct_view(self.create_view_class)
@cached_property
def model_name(self):
if isinstance(self.model, str):
return self.model.split(".")[-1]
else:
return self.model.__name__
@cached_property
def widget_class(self):
"""
Returns the form widget class for this chooser.
"""
if self.model is None:
widget_class_name = "ChooserWidget"
else:
if isinstance(self.model, str):
model_name = self.model.split(".")[-1]
else:
model_name = self.model.__name__
widget_class_name = "%sChooserWidget" % model_name
return type(
widget_class_name,
(self.base_widget_class,),
{
"model": self.model,
"choose_one_text": self.choose_one_text,
"choose_another_text": self.choose_another_text,
"link_to_chosen_text": self.edit_item_text,
"chooser_modal_url_name": self.get_url_name("choose"),
"icon": self.icon,
},
)
def get_block_class(self, name=None, module_path=None):
"""
Returns a StreamField ChooserBlock class using this chooser.
:param name: Name to give to the class; defaults to the model name with "ChooserBlock" appended
:param module_path: The dotted path of the module where the class can be imported from; used when
deconstructing the block definition for migration files.
"""
meta = type(
"Meta",
(self.base_block_class._meta_class,),
{
"icon": self.icon,
},
)
cls = type(
name or "%sChooserBlock" % self.model_name,
(self.base_block_class,),
{
"target_model": self.model,
"widget": self.widget_class(),
"Meta": meta,
},
)
if module_path:
cls.__module__ = module_path
return cls
def get_urlpatterns(self):
return super().get_urlpatterns() + [
path("", self.choose_view, name="choose"),
path("results/", self.choose_results_view, name="choose_results"),
path("chosen/<str:pk>/", self.chosen_view, name="chosen"),
path("chosen-multiple/", self.chosen_multiple_view, name="chosen_multiple"),
path("create/", self.create_view, name="create"),
]
def on_register(self):
if self.model and self.register_widget:
register_form_field_override(
ForeignKey, to=self.model, override={"widget": self.widget_class}
)
if self.widget_telepath_adapter_class:
adapter = self.widget_telepath_adapter_class()
register_telepath_adapter(adapter, self.widget_class)

View File

@@ -0,0 +1,704 @@
from warnings import warn
from django.contrib.auth import get_permission_codename
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ImproperlyConfigured
from django.forms.models import modelform_factory
from django.shortcuts import redirect
from django.urls import path
from django.utils.functional import cached_property
from wagtail import hooks
from wagtail.admin.admin_url_finder import (
ModelAdminURLFinder,
register_admin_url_finder,
)
from wagtail.admin.panels.group import ObjectList
from wagtail.admin.views import generic
from wagtail.admin.views.generic import history, usage
from wagtail.models import ReferenceIndex
from wagtail.permissions import ModelPermissionPolicy
from wagtail.utils.deprecation import RemovedInWagtail70Warning
from .base import ViewSet, ViewSetGroup
class ModelViewSet(ViewSet):
"""
A viewset to allow listing, creating, editing and deleting model instances.
All attributes and methods from :class:`~wagtail.admin.viewsets.base.ViewSet`
are available.
For more information on how to use this class, see :ref:`generic_views`.
"""
#: Register the model to the reference index to track its usage.
#: For more details, see :ref:`managing_the_reference_index`.
add_to_reference_index = True
#: The view class to use for the index view; must be a subclass of ``wagtail.admin.views.generic.IndexView``.
index_view_class = generic.IndexView
#: The view class to use for the create view; must be a subclass of ``wagtail.admin.views.generic.CreateView``.
add_view_class = generic.CreateView
#: The view class to use for the edit view; must be a subclass of ``wagtail.admin.views.generic.EditView``.
edit_view_class = generic.EditView
#: The view class to use for the delete view; must be a subclass of ``wagtail.admin.views.generic.DeleteView``.
delete_view_class = generic.DeleteView
#: The view class to use for the history view; must be a subclass of ``wagtail.admin.views.generic.history.HistoryView``.
history_view_class = history.HistoryView
#: The view class to use for the usage view; must be a subclass of ``wagtail.admin.views.generic.usage.UsageView``.
usage_view_class = usage.UsageView
#: The view class to use for the copy view; must be a subclass of ``wagtail.admin.views.generic.CopyView``.
copy_view_class = generic.CopyView
#: The view class to use for the inspect view; must be a subclass of ``wagtail.admin.views.generic.InspectView``.
inspect_view_class = generic.InspectView
# Breadcrumbs can be turned off until we have a design that can be consistently applied
_show_breadcrumbs = True
#: The prefix of template names to look for when rendering the admin views.
template_prefix = ""
#: The number of items to display per page in the index view. Defaults to 20.
list_per_page = 20
#: The default ordering to use for the index view.
#: Can be a string or a list/tuple in the same format as Django's
#: :attr:`~django.db.models.Options.ordering`.
ordering = None
#: Whether to enable the inspect view. Defaults to ``False``.
inspect_view_enabled = False
#: The model fields or attributes to display in the inspect view.
#:
#: If the field has a corresponding :meth:`~django.db.models.Model.get_FOO_display`
#: method on the model, the method's return value will be used instead.
#:
#: If you have ``wagtail.images`` installed, and the field's value is an instance of
#: ``wagtail.images.models.AbstractImage``, a thumbnail of that image will be rendered.
#:
#: If you have ``wagtail.documents`` installed, and the field's value is an instance of
#: ``wagtail.docs.models.AbstractDocument``, a link to that document will be rendered,
#: along with the document title, file extension and size.
inspect_view_fields = []
#: The fields to exclude from the inspect view.
inspect_view_fields_exclude = []
#: Whether to enable the copy view. Defaults to ``True``.
copy_view_enabled = True
def __init__(self, name=None, **kwargs):
super().__init__(name=name, **kwargs)
if not self.model:
raise ImproperlyConfigured(
"ModelViewSet %r must define a `model` attribute or pass a `model` argument"
% self
)
self.model_opts = self.model._meta
self.app_label = self.model_opts.app_label
self.model_name = self.model_opts.model_name
@property
def permission_policy(self):
return ModelPermissionPolicy(self.model)
@cached_property
def name(self):
"""
Viewset name, to use as the URL prefix and namespace.
Defaults to the :attr:`~django.db.models.Options.model_name`.
"""
return self.model_name
def get_common_view_kwargs(self, **kwargs):
view_kwargs = super().get_common_view_kwargs(
**{
"model": self.model,
"permission_policy": self.permission_policy,
"index_url_name": self.get_url_name("index"),
"index_results_url_name": self.get_url_name("index_results"),
"history_url_name": self.get_url_name("history"),
"usage_url_name": self.get_url_name("usage"),
"add_url_name": self.get_url_name("add"),
"edit_url_name": self.get_url_name("edit"),
"delete_url_name": self.get_url_name("delete"),
"header_icon": self.icon,
"_show_breadcrumbs": self._show_breadcrumbs,
**kwargs,
}
)
if self.copy_view_enabled:
view_kwargs["copy_url_name"] = self.get_url_name("copy")
if self.inspect_view_enabled:
view_kwargs["inspect_url_name"] = self.get_url_name("inspect")
return view_kwargs
def get_index_view_kwargs(self, **kwargs):
view_kwargs = {
"template_name": self.index_template_name,
"results_template_name": self.index_results_template_name,
"list_display": self.list_display,
"list_filter": self.list_filter,
"list_export": self.list_export,
"export_headings": self.export_headings,
"export_filename": self.export_filename,
"filterset_class": self.filterset_class,
"search_fields": self.search_fields,
"search_backend_name": self.search_backend_name,
"paginate_by": self.list_per_page,
**kwargs,
}
if self.ordering:
view_kwargs["default_ordering"] = self.ordering
return view_kwargs
def get_add_view_kwargs(self, **kwargs):
return {
"panel": self._edit_handler,
"form_class": self.get_form_class(),
"template_name": self.create_template_name,
**kwargs,
}
def get_edit_view_kwargs(self, **kwargs):
return {
"panel": self._edit_handler,
"form_class": self.get_form_class(for_update=True),
"template_name": self.edit_template_name,
**kwargs,
}
def get_delete_view_kwargs(self, **kwargs):
return {
"template_name": self.delete_template_name,
**kwargs,
}
def get_history_view_kwargs(self, **kwargs):
return {
"template_name": self.history_template_name,
"history_results_url_name": self.get_url_name("history_results"),
"header_icon": "history",
**kwargs,
}
def get_usage_view_kwargs(self, **kwargs):
return {
"template_name": self.get_templates(
"usage", fallback=self.usage_view_class.template_name
),
**kwargs,
}
def get_inspect_view_kwargs(self, **kwargs):
return {
"template_name": self.inspect_template_name,
"fields": self.inspect_view_fields,
"fields_exclude": self.inspect_view_fields_exclude,
**kwargs,
}
def get_copy_view_kwargs(self, **kwargs):
return self.get_add_view_kwargs(**kwargs)
@property
def index_view(self):
return self.construct_view(
self.index_view_class, **self.get_index_view_kwargs()
)
@property
def index_results_view(self):
return self.construct_view(
self.index_view_class, **self.get_index_view_kwargs(), results_only=True
)
@property
def add_view(self):
return self.construct_view(self.add_view_class, **self.get_add_view_kwargs())
@property
def edit_view(self):
return self.construct_view(self.edit_view_class, **self.get_edit_view_kwargs())
@property
def delete_view(self):
return self.construct_view(
self.delete_view_class, **self.get_delete_view_kwargs()
)
@property
def redirect_to_edit_view(self):
def redirect_to_edit(request, pk):
warn(
(
"%s's `/<pk>/` edit view URL pattern has been "
"deprecated in favour of /edit/<pk>/."
)
% (self.__class__.__name__),
category=RemovedInWagtail70Warning,
)
return redirect(self.get_url_name("edit"), pk, permanent=True)
return redirect_to_edit
@property
def redirect_to_delete_view(self):
def redirect_to_delete(request, pk):
warn(
(
"%s's `/<pk>/delete/` delete view URL pattern has been "
"deprecated in favour of /delete/<pk>/."
)
% (self.__class__.__name__),
category=RemovedInWagtail70Warning,
)
return redirect(self.get_url_name("delete"), pk, permanent=True)
return redirect_to_delete
@property
def history_view(self):
return self.construct_view(
self.history_view_class, **self.get_history_view_kwargs()
)
@property
def history_results_view(self):
return self.construct_view(
self.history_view_class, **self.get_history_view_kwargs(), results_only=True
)
@property
def usage_view(self):
return self.construct_view(
self.usage_view_class, **self.get_usage_view_kwargs()
)
@property
def inspect_view(self):
return self.construct_view(
self.inspect_view_class, **self.get_inspect_view_kwargs()
)
@property
def copy_view(self):
return self.construct_view(self.copy_view_class, **self.get_copy_view_kwargs())
def get_templates(self, name="index", fallback=""):
"""
Utility function that provides a list of templates to try for a given
view, when the template isn't overridden by one of the template
attributes on the class.
"""
if not self.template_prefix:
return [fallback]
templates = [
f"{self.template_prefix}{self.app_label}/{self.model_name}/{name}.html",
f"{self.template_prefix}{self.app_label}/{name}.html",
f"{self.template_prefix}{name}.html",
]
if fallback:
templates.append(fallback)
return templates
@cached_property
def index_template_name(self):
"""
A template to be used when rendering ``index_view``.
Default: if :attr:`template_prefix` is specified, an ``index.html``
template in the prefix directory and its ``{app_label}/{model_name}/``
or ``{app_label}/`` subdirectories will be used. Otherwise, the
``index_view_class.template_name`` will be used.
"""
return self.get_templates(
"index",
fallback=self.index_view_class.template_name,
)
@cached_property
def index_results_template_name(self):
"""
A template to be used when rendering ``index_results_view``.
Default: if :attr:`template_prefix` is specified, a ``index_results.html``
template in the prefix directory and its ``{app_label}/{model_name}/``
or ``{app_label}/`` subdirectories will be used. Otherwise, the
``index_view_class.results_template_name`` will be used.
"""
return self.get_templates(
"index_results",
fallback=self.index_view_class.results_template_name,
)
@cached_property
def create_template_name(self):
"""
A template to be used when rendering ``add_view``.
Default: if :attr:`template_prefix` is specified, a ``create.html``
template in the prefix directory and its ``{app_label}/{model_name}/``
or ``{app_label}/`` subdirectories will be used. Otherwise, the
``add_view_class.template_name`` will be used.
"""
return self.get_templates(
"create",
fallback=self.add_view_class.template_name,
)
@cached_property
def edit_template_name(self):
"""
A template to be used when rendering ``edit_view``.
Default: if :attr:`template_prefix` is specified, an ``edit.html``
template in the prefix directory and its ``{app_label}/{model_name}/``
or ``{app_label}/`` subdirectories will be used. Otherwise, the
``edit_view_class.template_name`` will be used.
"""
return self.get_templates(
"edit",
fallback=self.edit_view_class.template_name,
)
@cached_property
def delete_template_name(self):
"""
A template to be used when rendering ``delete_view``.
Default: if :attr:`template_prefix` is specified, a ``confirm_delete.html``
template in the prefix directory and its ``{app_label}/{model_name}/``
or ``{app_label}/`` subdirectories will be used. Otherwise, the
``delete_view_class.template_name`` will be used.
"""
return self.get_templates(
"confirm_delete",
fallback=self.delete_view_class.template_name,
)
@cached_property
def history_template_name(self):
"""
A template to be used when rendering ``history_view``.
Default: if :attr:`template_prefix` is specified, a ``history.html``
template in the prefix directory and its ``{app_label}/{model_name}/``
or ``{app_label}/`` subdirectories will be used. Otherwise, the
``history_view_class.template_name`` will be used.
"""
return self.get_templates(
"history",
fallback=self.history_view_class.template_name,
)
@cached_property
def inspect_template_name(self):
"""
A template to be used when rendering ``inspect_view``.
Default: if :attr:`template_prefix` is specified, an ``inspect.html``
template in the prefix directory and its ``{app_label}/{model_name}/``
or ``{app_label}/`` subdirectories will be used. Otherwise, the
``inspect_view_class.template_name`` will be used.
"""
return self.get_templates(
"inspect",
fallback=self.inspect_view_class.template_name,
)
@cached_property
def list_display(self):
"""
A list or tuple, where each item is either:
- The name of a field on the model;
- The name of a callable or property on the model that accepts a single
parameter for the model instance; or
- An instance of the ``wagtail.admin.ui.tables.Column`` class.
If the name refers to a database field, the ability to sort the listing
by the database column will be offerred and the field's verbose name
will be used as the column header.
If the name refers to a callable or property, an ``admin_order_field``
attribute can be defined on it to point to the database column for
sorting. A ``short_description`` attribute can also be defined on the
callable or property to be used as the column header.
This list will be passed to the ``list_display`` attribute of the index
view. If left unset, the ``list_display`` attribute of the index view
will be used instead, which by default is defined as
``["__str__", wagtail.admin.ui.tables.UpdatedAtColumn()]``.
"""
return self.index_view_class.list_display
@cached_property
def list_filter(self):
"""
A list or tuple, where each item is the name of model fields of type
``BooleanField``, ``CharField``, ``DateField``, ``DateTimeField``,
``IntegerField`` or ``ForeignKey``.
Alternatively, it can also be a dictionary that maps a field name to a
list of lookup expressions.
This will be passed as django-filter's ``FilterSet.Meta.fields``
attribute. See
`its documentation <https://django-filter.readthedocs.io/en/stable/guide/usage.html#generating-filters-with-meta-fields>`_
for more details.
If ``filterset_class`` is set, this attribute will be ignored.
"""
return self.index_view_class.list_filter
@cached_property
def filterset_class(self):
"""
A subclass of ``wagtail.admin.filters.WagtailFilterSet``, which is a
subclass of `django_filters.FilterSet <https://django-filter.readthedocs.io/en/stable/ref/filterset.html>`_.
This will be passed to the ``filterset_class`` attribute of the index view.
"""
return self.index_view_class.filterset_class
@cached_property
def search_fields(self):
"""
The fields to use for the search in the index view.
If set to ``None`` and :attr:`search_backend_name` is set to use a Wagtail search backend,
the ``search_fields`` attribute of the model will be used instead.
"""
return self.index_view_class.search_fields
@cached_property
def search_backend_name(self):
"""
The name of the Wagtail search backend to use for the search in the index view.
If set to a falsy value, the search will fall back to use Django's QuerySet API.
"""
return self.index_view_class.search_backend_name
@cached_property
def list_export(self):
"""
A list or tuple, where each item is the name of a field, an attribute,
or a single-argument callable on the model to be exported.
"""
return self.index_view_class.list_export
@cached_property
def export_headings(self):
"""
A dictionary of export column heading overrides in the format
``{field_name: heading}``.
"""
return self.index_view_class.export_headings
@cached_property
def export_filename(self):
"""
The base file name for the exported listing, without extensions.
If unset, the model's :attr:`~django.db.models.Options.db_table` will be
used instead.
"""
return self.model._meta.db_table
@cached_property
def menu_label(self):
return self.model_opts.verbose_name_plural.title()
@cached_property
def menu_item_class(self):
from wagtail.admin.menu import MenuItem
def is_shown(_self, request):
return self.permission_policy.user_has_any_permission(
request.user, self.index_view_class.any_permission_required
)
return type(
f"{self.model.__name__}MenuItem",
(MenuItem,),
{"is_shown": is_shown},
)
def formfield_for_dbfield(self, db_field, **kwargs):
return db_field.formfield(**kwargs)
def get_form_class(self, for_update=False):
"""
Returns the form class to use for the create / edit forms.
"""
# If an edit handler is defined, use it to construct the form class.
if self._edit_handler:
return self._edit_handler.get_form_class()
# Otherwise, use Django's modelform_factory.
fields = self.get_form_fields()
exclude = self.get_exclude_form_fields()
if fields is None and exclude is None:
raise ImproperlyConfigured(
"Subclasses of ModelViewSet must specify 'get_form_class', 'form_fields' "
"or 'exclude_form_fields'."
)
return modelform_factory(
self.model,
formfield_callback=self.formfield_for_dbfield,
fields=fields,
exclude=exclude,
)
def get_form_fields(self):
"""
Returns a list or tuple of field names to be included in the create / edit forms.
"""
return getattr(self, "form_fields", None)
def get_exclude_form_fields(self):
"""
Returns a list or tuple of field names to be excluded from the create / edit forms.
"""
return getattr(self, "exclude_form_fields", None)
def get_edit_handler(self):
"""
Returns the appropriate edit handler for this ``ModelViewSet`` class.
It can be defined either on the model itself or on the ``ModelViewSet``,
as the ``edit_handler`` or ``panels`` properties. If none of these are
defined, it will return ``None`` and the form will be constructed as
a Django form using :meth:`get_form_class` (without using
:ref:`forms_panels_overview`).
"""
if hasattr(self, "edit_handler"):
edit_handler = self.edit_handler
elif hasattr(self, "panels"):
panels = self.panels
edit_handler = ObjectList(panels)
elif hasattr(self.model, "edit_handler"):
edit_handler = self.model.edit_handler
elif hasattr(self.model, "panels"):
panels = self.model.panels
edit_handler = ObjectList(panels)
else:
return None
return edit_handler.bind_to_model(self.model)
@cached_property
def _edit_handler(self):
"""
An edit handler that has been bound to the model class,
to be used across views.
"""
return self.get_edit_handler()
@property
def url_finder_class(self):
return type(
"_ModelAdminURLFinder",
(ModelAdminURLFinder,),
{
"permission_policy": self.permission_policy,
"edit_url_name": self.get_url_name("edit"),
},
)
def register_admin_url_finder(self):
register_admin_url_finder(self.model, self.url_finder_class)
def register_reference_index(self):
if self.add_to_reference_index:
ReferenceIndex.register_model(self.model)
def get_permissions_to_register(self):
"""
Returns a queryset of :class:`~django.contrib.auth.models.Permission`
objects to be registered with the :ref:`register_permissions` hook. By
default, it returns all permissions for the model if
:attr:`inspect_view_enabled` is set to ``True``. Otherwise, the "view"
permission is excluded.
"""
content_type = ContentType.objects.get_for_model(self.model)
permissions = Permission.objects.filter(content_type=content_type)
# Only register the "view" permission if the inspect view is enabled
if not self.inspect_view_enabled:
permissions = permissions.exclude(
codename=get_permission_codename("view", self.model_opts)
)
return permissions
def register_permissions(self):
hooks.register("register_permissions", self.get_permissions_to_register)
def get_urlpatterns(self):
urlpatterns = [
path("", self.index_view, name="index"),
path("results/", self.index_results_view, name="index_results"),
path("new/", self.add_view, name="add"),
path("edit/<str:pk>/", self.edit_view, name="edit"),
path("delete/<str:pk>/", self.delete_view, name="delete"),
path("history/<str:pk>/", self.history_view, name="history"),
path(
"history-results/<str:pk>/",
self.history_results_view,
name="history_results",
),
path("usage/<str:pk>/", self.usage_view, name="usage"),
]
if self.inspect_view_enabled:
urlpatterns.append(
path("inspect/<str:pk>/", self.inspect_view, name="inspect")
)
if self.copy_view_enabled:
urlpatterns.append(path("copy/<str:pk>/", self.copy_view, name="copy"))
# RemovedInWagtail70Warning: Remove legacy URL patterns
urlpatterns += self._legacy_urlpatterns
return urlpatterns
@cached_property
def _legacy_urlpatterns(self):
# RemovedInWagtail70Warning: Remove legacy URL patterns
return [
path("<str:pk>/", self.redirect_to_edit_view),
path("<str:pk>/delete/", self.redirect_to_delete_view),
]
def on_register(self):
super().on_register()
self.register_admin_url_finder()
self.register_reference_index()
self.register_permissions()
class ModelViewSetGroup(ViewSetGroup):
"""
A container for grouping together multiple
:class:`~wagtail.admin.viewsets.model.ModelViewSet` instances.
All attributes and methods from
:class:`~wagtail.admin.viewsets.base.ViewSetGroup` are available.
"""
def get_app_label_from_subitems(self):
for instance in self.registerables:
if app_label := getattr(instance, "app_label", ""):
return app_label.title()
return ""
@cached_property
def menu_label(self):
return self.get_app_label_from_subitems()

View File

@@ -0,0 +1,77 @@
from django.urls import path
from wagtail.admin.views.pages.choose_parent import ChooseParentView
from wagtail.admin.views.pages.listing import IndexView
from wagtail.models import Page
from .base import ViewSet
class PageListingViewSet(ViewSet):
"""
A viewset to present a flat listing of all pages of a specific type.
All attributes and methods from :class:`~wagtail.admin.viewsets.base.ViewSet`
are available.
For more information on how to use this class, see :ref:`custom_page_listings`.
"""
#: The view class to use for the index view; must be a subclass of ``wagtail.admin.views.pages.listing.IndexView``.
index_view_class = IndexView
#: The view class to use for choosing the parent page when creating a new page of this page type.
choose_parent_view_class = ChooseParentView
#: Required; the page model class that this viewset will work with.
model = Page
#: A list of ``wagtail.admin.ui.tables.Column`` instances for the columns in the listing.
columns = IndexView.columns
#: A subclass of ``wagtail.admin.filters.WagtailFilterSet``, which is a
#: subclass of `django_filters.FilterSet <https://django-filter.readthedocs.io/en/stable/ref/filterset.html>`_.
#: This will be passed to the ``filterset_class`` attribute of the index view.
filterset_class = IndexView.filterset_class
def get_common_view_kwargs(self, **kwargs):
return super().get_common_view_kwargs(
**{
"_show_breadcrumbs": True,
"header_icon": self.icon,
"model": self.model,
"index_url_name": self.get_url_name("index"),
"add_url_name": self.get_url_name("choose_parent"),
**kwargs,
}
)
def get_index_view_kwargs(self, **kwargs):
return {
"index_results_url_name": self.get_url_name("index_results"),
"columns": self.columns,
"filterset_class": self.filterset_class,
**kwargs,
}
def get_choose_parent_view_kwargs(self, **kwargs):
return kwargs
@property
def index_view(self):
return self.construct_view(
self.index_view_class, **self.get_index_view_kwargs()
)
@property
def index_results_view(self):
return self.construct_view(
self.index_view_class, **self.get_index_view_kwargs(), results_only=True
)
@property
def choose_parent_view(self):
return self.construct_view(
self.choose_parent_view_class, **self.get_choose_parent_view_kwargs()
)
def get_urlpatterns(self):
return [
path("", self.index_view, name="index"),
path("results/", self.index_results_view, name="index_results"),
path("choose_parent/", self.choose_parent_view, name="choose_parent"),
]