angrybeanie_wagtail/env/lib/python3.12/site-packages/wagtail/admin/viewsets/model.py
2025-07-25 21:32:16 +10:00

658 lines
24 KiB
Python

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.urls import path
from django.utils.functional import cached_property
from django.utils.text import capfirst
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 .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
#: 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,
**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 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 offered 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.LocaleColumn(), wagtail.admin.ui.tables.UpdatedAtColumn()]``.
Note that the ``LocaleColumn`` is only included if the model is translatable.
"""
return self.UNDEFINED
@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.UNDEFINED
@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 capfirst(self.model_opts.verbose_name_plural)
@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"))
return urlpatterns
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 capfirst(app_label)
return ""
@cached_property
def menu_label(self):
return self.get_app_label_from_subitems()