kopia lustrzana https://github.com/wagtail/wagtail
Add docs and tests for snippets inspect view
rodzic
4b9f7df4fd
commit
0bebe532e8
|
@ -96,6 +96,9 @@ Viewsets are Wagtail's mechanism for defining a group of related admin views wit
|
|||
.. autoattribute:: chooser_per_page
|
||||
.. autoattribute:: export_filename
|
||||
.. autoattribute:: ordering
|
||||
.. autoattribute:: inspect_view_enabled
|
||||
.. autoattribute:: inspect_view_fields
|
||||
.. autoattribute:: inspect_view_fields_exclude
|
||||
.. autoattribute:: admin_url_namespace
|
||||
.. autoattribute:: base_url_path
|
||||
.. autoattribute:: chooser_admin_url_namespace
|
||||
|
@ -106,6 +109,7 @@ Viewsets are Wagtail's mechanism for defining a group of related admin views wit
|
|||
.. autoattribute:: delete_view_class
|
||||
.. autoattribute:: usage_view_class
|
||||
.. autoattribute:: history_view_class
|
||||
.. autoattribute:: inspect_view_class
|
||||
.. autoattribute:: revisions_view_class
|
||||
.. autoattribute:: revisions_revert_view_class
|
||||
.. autoattribute:: revisions_compare_view_class
|
||||
|
@ -137,6 +141,7 @@ Viewsets are Wagtail's mechanism for defining a group of related admin views wit
|
|||
.. automethod:: get_edit_template
|
||||
.. automethod:: get_delete_template
|
||||
.. automethod:: get_history_template
|
||||
.. automethod:: get_inspect_template
|
||||
.. automethod:: get_admin_url_namespace
|
||||
.. automethod:: get_admin_base_path
|
||||
.. automethod:: get_chooser_admin_url_namespace
|
||||
|
|
|
@ -53,6 +53,7 @@ class MemberViewSet(SnippetViewSet):
|
|||
icon = "user"
|
||||
list_display = ["name", "shirt_size", "get_shirt_size_display", UpdatedAtColumn()]
|
||||
list_per_page = 50
|
||||
inspect_view_enabled = True
|
||||
admin_url_namespace = "member_views"
|
||||
base_url_path = "internal/member"
|
||||
filterset_class = MemberFilterSet
|
||||
|
@ -99,6 +100,18 @@ You can add the ability to export the listing view to a spreadsheet by setting t
|
|||
The ability to export the listing view was added.
|
||||
```
|
||||
|
||||
## Inspect view
|
||||
|
||||
```{versionadded} 5.1
|
||||
The ability to enable inspect view was added.
|
||||
```
|
||||
|
||||
The inspect view is disabled by default, as it's not often useful for most models. However, if you need a view that enables users to view more detailed information about an instance without the option to edit it, you can enable the inspect view by setting {attr}`~wagtail.snippets.views.snippets.SnippetViewSet.inspect_view_enabled` on your `SnippetViewSet` class.
|
||||
|
||||
When inspect view is enabled, an 'Inspect' button will automatically appear for each row on the listing view, which takes you to a view that shows a list of field values for that particular snippet.
|
||||
|
||||
By default, all 'concrete' fields (where the field value is stored as a column in the database table for your model) will be shown. You can customise what values are displayed by specifying the {attr}`~wagtail.snippets.views.snippets.SnippetViewSet.inspect_view_fields` or the {attr}`~wagtail.snippets.views.snippets.SnippetViewSet.inspect_view_fields_exclude` attributes to your `SnippetViewSet` class.
|
||||
|
||||
## Templates
|
||||
|
||||
For all views that are used for a snippet model, Wagtail looks for templates in the following directories within your project or app, before resorting to the defaults:
|
||||
|
|
|
@ -8,8 +8,9 @@ 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.template.defaultfilters import date
|
||||
from django.test import TestCase, TransactionTestCase
|
||||
from django.urls import reverse
|
||||
from django.urls import NoReverseMatch, resolve, reverse
|
||||
from django.utils.timezone import now
|
||||
from openpyxl import load_workbook
|
||||
|
||||
|
@ -20,6 +21,10 @@ from wagtail.admin.staticfiles import versioned_static
|
|||
from wagtail.admin.views.mixins import ExcelDateFormatter
|
||||
from wagtail.blocks.field_block import FieldBlockAdapter
|
||||
from wagtail.coreutils import get_dummy_request
|
||||
from wagtail.documents import get_document_model
|
||||
from wagtail.documents.tests.utils import get_test_document_file
|
||||
from wagtail.images import get_image_model
|
||||
from wagtail.images.tests.utils import get_test_image_file
|
||||
from wagtail.models import Locale, Workflow, WorkflowContentType
|
||||
from wagtail.snippets.blocks import SnippetChooserBlock
|
||||
from wagtail.snippets.models import register_snippet
|
||||
|
@ -33,6 +38,7 @@ from wagtail.test.testapp.models import (
|
|||
RevisableChildModel,
|
||||
RevisableModel,
|
||||
SnippetChooserModel,
|
||||
VariousOnDeleteModel,
|
||||
)
|
||||
from wagtail.test.utils import WagtailTestUtils
|
||||
|
||||
|
@ -1073,3 +1079,222 @@ class TestCustomFormClass(BaseSnippetViewSetTests):
|
|||
edit_view = self.client.get(self.get_url("edit", args=(quote(obj.pk),)))
|
||||
self.assertContains(edit_view, '<input type="text" name="text"')
|
||||
self.assertNotContains(edit_view, '<textarea name="text"')
|
||||
|
||||
|
||||
class TestInspectViewConfiguration(BaseSnippetViewSetTests):
|
||||
model = FullFeaturedSnippet
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.viewset = self.model.snippet_viewset
|
||||
self.object = self.model.objects.create(text="Perkedel", country_code="ID")
|
||||
|
||||
def test_enabled(self):
|
||||
self.model = FullFeaturedSnippet
|
||||
url = self.get_url("inspect", args=(quote(self.object.pk),))
|
||||
response = self.client.get(url)
|
||||
self.assertContains(
|
||||
response,
|
||||
"<dt>Text</dt> <dd>Perkedel</dd>",
|
||||
html=True,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
"<dt>Country code</dt> <dd>Indonesia</dd>",
|
||||
html=True,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f"<dt>Some date</dt> <dd>{date(self.object.some_date)}</dd>",
|
||||
html=True,
|
||||
)
|
||||
self.assertNotContains(
|
||||
response,
|
||||
"<dt>Some attribute</dt> <dd>some value</dd>",
|
||||
html=True,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
self.get_url("edit", args=(quote(self.object.pk),)),
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
self.get_url("delete", args=(quote(self.object.pk),)),
|
||||
)
|
||||
|
||||
def test_disabled(self):
|
||||
self.model = Advert
|
||||
object = self.model.objects.create(text="ad")
|
||||
with self.assertRaises(NoReverseMatch):
|
||||
self.get_url("inspect", args=(quote(object.pk),))
|
||||
|
||||
def test_only_add_permission(self):
|
||||
self.model = FullFeaturedSnippet
|
||||
|
||||
self.user.is_superuser = False
|
||||
self.user.user_permissions.add(
|
||||
Permission.objects.get(
|
||||
content_type__app_label="wagtailadmin", codename="access_admin"
|
||||
),
|
||||
Permission.objects.get(
|
||||
content_type__app_label=self.model._meta.app_label,
|
||||
codename=get_permission_codename("add", self.model._meta),
|
||||
),
|
||||
)
|
||||
self.user.save()
|
||||
|
||||
url = self.get_url("inspect", args=(quote(self.object.pk),))
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertContains(
|
||||
response,
|
||||
"<dt>Text</dt> <dd>Perkedel</dd>",
|
||||
html=True,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
"<dt>Country code</dt> <dd>Indonesia</dd>",
|
||||
html=True,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f"<dt>Some date</dt> <dd>{date(self.object.some_date)}</dd>",
|
||||
html=True,
|
||||
)
|
||||
self.assertNotContains(
|
||||
response,
|
||||
self.get_url("edit", args=(quote(self.object.pk),)),
|
||||
)
|
||||
self.assertNotContains(
|
||||
response,
|
||||
self.get_url("delete", args=(quote(self.object.pk),)),
|
||||
)
|
||||
|
||||
def test_custom_fields(self):
|
||||
self.model = FullFeaturedSnippet
|
||||
url = self.get_url("inspect", args=(quote(self.object.pk),))
|
||||
view_func = resolve(url).func
|
||||
|
||||
adverts = [Advert.objects.create(text=f"advertisement {i}") for i in range(3)]
|
||||
queryset = Advert.objects.filter(pk=adverts[0].pk)
|
||||
|
||||
mock_manager = mock.patch.object(
|
||||
self.model, "adverts", Advert.objects, create=True
|
||||
)
|
||||
|
||||
mock_queryset = mock.patch.object(
|
||||
self.model, "some_queryset", queryset, create=True
|
||||
)
|
||||
|
||||
mock_fields = mock.patch.dict(
|
||||
view_func.view_initkwargs,
|
||||
{
|
||||
"fields": [
|
||||
"country_code", # Field with choices (thus get_FOO_display method)
|
||||
"some_date", # DateField
|
||||
"some_attribute", # Model attribute
|
||||
"adverts", # Manager
|
||||
"some_queryset", # QuerySet
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
# We need to mock the view's init kwargs instead of the viewset's
|
||||
# attributes, because the viewset's attributes are only used when the
|
||||
# view is instantiated, and the view is instantiated once at startup.
|
||||
with mock_manager, mock_queryset, mock_fields:
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertNotContains(
|
||||
response,
|
||||
"<dt>Text</dt> <dd>Perkedel</dd>",
|
||||
html=True,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
"<dt>Country code</dt> <dd>Indonesia</dd>",
|
||||
html=True,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
f"<dt>Some date</dt> <dd>{date(self.object.some_date)}</dd>",
|
||||
html=True,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
"<dt>Some attribute</dt> <dd>some value</dd>",
|
||||
html=True,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
"""
|
||||
<dt>Adverts</dt>
|
||||
<dd>advertisement 0, advertisement 1, advertisement 2</dd>
|
||||
""",
|
||||
html=True,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
"<dt>Some queryset</dt> <dd>advertisement 0</dd>",
|
||||
html=True,
|
||||
)
|
||||
|
||||
def test_exclude_fields(self):
|
||||
self.model = FullFeaturedSnippet
|
||||
url = self.get_url("inspect", args=(quote(self.object.pk),))
|
||||
view_func = resolve(url).func
|
||||
|
||||
# We need to mock the view's init kwargs instead of the viewset's
|
||||
# attributes, because the viewset's attributes are only used when the
|
||||
# view is instantiated, and the view is instantiated once at startup.
|
||||
with mock.patch.dict(
|
||||
view_func.view_initkwargs,
|
||||
{"fields_exclude": ["some_date"]},
|
||||
):
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertContains(
|
||||
response,
|
||||
"<dt>Text</dt> <dd>Perkedel</dd>",
|
||||
html=True,
|
||||
)
|
||||
self.assertContains(
|
||||
response,
|
||||
"<dt>Country code</dt> <dd>Indonesia</dd>",
|
||||
html=True,
|
||||
)
|
||||
self.assertNotContains(
|
||||
response,
|
||||
f"<dt>Some date</dt> <dd>{date(self.object.some_date)}</dd>",
|
||||
html=True,
|
||||
)
|
||||
self.assertNotContains(
|
||||
response,
|
||||
"<dt>Some attribute</dt> <dd>some value</dd>",
|
||||
html=True,
|
||||
)
|
||||
|
||||
def test_image_and_document_fields(self):
|
||||
self.model = VariousOnDeleteModel
|
||||
image = get_image_model().objects.create(
|
||||
title="Test image",
|
||||
file=get_test_image_file(),
|
||||
)
|
||||
document = get_document_model().objects.create(
|
||||
title="Test document", file=get_test_document_file()
|
||||
)
|
||||
object = self.model.objects.create(
|
||||
protected_image=image, protected_document=document
|
||||
)
|
||||
response = self.client.get(self.get_url("inspect", args=(quote(object.pk),)))
|
||||
|
||||
self.assertContains(
|
||||
response,
|
||||
f"<dt>Protected image</dt> <dd>{image.get_rendition('max-400x400').img_tag()}</dd>",
|
||||
html=True,
|
||||
)
|
||||
self.assertContains(response, "<dt>Protected document</dt>", html=True)
|
||||
self.assertContains(response, f'<a href="{document.url}">')
|
||||
self.assertContains(response, "Test document")
|
||||
self.assertContains(response, "TXT")
|
||||
self.assertContains(response, f"{document.file.size}\xa0bytes")
|
||||
|
|
|
@ -701,7 +701,17 @@ class SnippetViewSet(ModelViewSet):
|
|||
#: Whether to enable the inspect view. Defaults to ``False``.
|
||||
inspect_view_enabled = False
|
||||
|
||||
#: The fields to display in the inspect view.
|
||||
#: 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.
|
||||
|
|
|
@ -1117,6 +1117,8 @@ class FullFeaturedSnippet(
|
|||
)
|
||||
some_date = models.DateField(auto_now=True)
|
||||
|
||||
some_attribute = "some value"
|
||||
|
||||
search_fields = [
|
||||
index.SearchField("text"),
|
||||
index.FilterField("text"),
|
||||
|
@ -1225,9 +1227,6 @@ class VariousOnDeleteModel(models.Model):
|
|||
rich_text = RichTextField(blank=True)
|
||||
|
||||
|
||||
register_snippet(VariousOnDeleteModel)
|
||||
|
||||
|
||||
class StandardIndex(Page):
|
||||
"""Index for the site"""
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ from wagtail.test.testapp.models import (
|
|||
ModeratedModel,
|
||||
RevisableChildModel,
|
||||
RevisableModel,
|
||||
VariousOnDeleteModel,
|
||||
)
|
||||
|
||||
from .forms import FavouriteColourForm
|
||||
|
@ -342,7 +343,13 @@ class ModeratedModelViewSet(SnippetViewSet):
|
|||
}
|
||||
|
||||
|
||||
class VariousOnDeleteModelViewSet(SnippetViewSet):
|
||||
model = VariousOnDeleteModel
|
||||
inspect_view_enabled = True
|
||||
|
||||
|
||||
register_snippet(FullFeaturedSnippet, viewset=FullFeaturedSnippetViewSet)
|
||||
register_snippet(DraftStateModel, viewset=DraftStateModelViewSet)
|
||||
register_snippet(ModeratedModelViewSet)
|
||||
register_snippet(RevisableViewSetGroup)
|
||||
register_snippet(VariousOnDeleteModelViewSet)
|
||||
|
|
Ładowanie…
Reference in New Issue