소스 검색

Make all usage reports use the reference index

Karl Hobley 2 년 전
부모
커밋
8691b19967

+ 2 - 3
wagtail/documents/models.py

@@ -12,8 +12,7 @@ from django.urls import reverse
 from django.utils.translation import gettext_lazy as _
 from taggit.managers import TaggableManager
 
-from wagtail.admin.models import get_object_usage
-from wagtail.models import CollectionMember
+from wagtail.models import CollectionMember, ReferenceIndex
 from wagtail.search import index
 from wagtail.search.queryset import SearchableQuerySetMixin
 
@@ -168,7 +167,7 @@ class AbstractDocument(CollectionMember, index.Indexed, models.Model):
         return reverse("wagtaildocs_serve", args=[self.id, self.filename])
 
     def get_usage(self):
-        return get_object_usage(self)
+        return ReferenceIndex.get_references_to(self).group_by_source_object()
 
     @property
     def usage_url(self):

+ 16 - 18
wagtail/documents/templates/wagtaildocs/documents/usage.html

@@ -7,34 +7,32 @@
 
     <div class="nice-padding">
         <table class="listing">
-            <col />
-            <col width="30%"/>
-            <col width="15%"/>
-            <col width="15%"/>
             <thead>
                 <tr>
                     <th class="title">{% trans "Title" %}</th>
-                    <th>{% trans "Parent" %}</th>
-                    <th>{% trans "Type" %}</th>
-                    <th>{% trans "Status" %}</th>
+                    <th>Field</th>
                 </tr>
             </thead>
             <tbody>
-                {% for page in used_by %}
+                {% for object, references in used_by %}
                     <tr>
                         <td class="title" valign="top">
-                            <div class="title-wrapper"><a href="{% url 'wagtailadmin_pages:edit' page.id %}" title="{% trans 'Edit this page' %}">{{ page.title }}</a></div>
+                            <div class="title-wrapper">
+                                {% if object.edit_url %}<a href="{{ object.edit_url }}" title="{% trans 'Edit this page' %}">{% endif %}
+                                {{ object }}
+                                {% if object.edit_url %}</a>{% endif %}
+                            </div>
                         </td>
                         <td>
-                            {% if page.get_parent %}
-                                <a href="{% url 'wagtailadmin_explore' page.get_parent.id %}">{{ page.get_parent.title }}</a>
-                            {% endif %}
-                        </td>
-                        <td>
-                            {{ page.page_type_display_name }}
-                        </td>
-                        <td>
-                            {% include "wagtailadmin/shared/page_status_tag.html" with page=page %}
+                            <ul>
+                                {% for reference in references %}
+                                    <li>
+                                        {% if object.edit_url %}<a href="{{ object.edit_url }}#content-path-{{ reference.content_path }}">{% endif %}
+                                        {{ reference.describe_source_field }}
+                                        {% if object.edit_url %}</a>{% endif %}
+                                    </li>
+                                {% endfor %}
+                            </ul>
                         </td>
                     </tr>
                 {% endfor %}

+ 6 - 2
wagtail/documents/tests/test_admin_views.py

@@ -12,7 +12,7 @@ from django.utils.http import urlencode
 from wagtail.admin.admin_url_finder import AdminURLFinder
 from wagtail.documents import get_document_model, models
 from wagtail.documents.tests.utils import get_test_document_file
-from wagtail.models import Collection, GroupCollectionPermission, Page
+from wagtail.models import Collection, GroupCollectionPermission, Page, ReferenceIndex
 from wagtail.test.testapp.models import (
     CustomDocument,
     CustomDocumentWithAuthor,
@@ -1802,7 +1802,11 @@ class TestGetUsage(TestCase, WagtailTestUtils):
         event_page_related_link.page = page
         event_page_related_link.link_document = doc
         event_page_related_link.save()
-        self.assertTrue(issubclass(Page, type(doc.get_usage()[0])))
+
+        self.assertIsInstance(doc.get_usage()[0], tuple)
+        self.assertIsInstance(doc.get_usage()[0][0], Page)
+        self.assertIsInstance(doc.get_usage()[0][1], list)
+        self.assertIsInstance(doc.get_usage()[0][1][0], ReferenceIndex)
 
     @override_settings(WAGTAIL_USAGE_COUNT_ENABLED=True)
     def test_usage_page(self):

+ 6 - 0
wagtail/documents/views/documents.py

@@ -11,6 +11,7 @@ from django.utils.translation import gettext as _
 from django.views.generic import TemplateView
 
 from wagtail.admin import messages
+from wagtail.admin.admin_url_finder import AdminURLFinder
 from wagtail.admin.auth import PermissionPolicyChecker
 from wagtail.admin.forms.search import SearchForm
 from wagtail.admin.models import popular_tags_for_model
@@ -266,6 +267,11 @@ def usage(request, document_id):
     paginator = Paginator(doc.get_usage(), per_page=20)
     used_by = paginator.get_page(request.GET.get("p"))
 
+    # Add edit URLs to each source object
+    url_finder = AdminURLFinder(request.user)
+    for object, references in used_by:
+        object.edit_url = url_finder.get_edit_url(object)
+
     return TemplateResponse(
         request,
         "wagtaildocs/documents/usage.html",

+ 4 - 3
wagtail/images/models.py

@@ -24,7 +24,6 @@ from taggit.managers import TaggableManager
 from willow.image import Image as WillowImage
 
 from wagtail import hooks
-from wagtail.admin.models import get_object_usage
 from wagtail.coreutils import string_to_ascii
 from wagtail.images.exceptions import (
     InvalidFilterSpecError,
@@ -36,7 +35,7 @@ from wagtail.images.image_operations import (
     TransformOperation,
 )
 from wagtail.images.rect import Rect
-from wagtail.models import CollectionMember
+from wagtail.models import CollectionMember, ReferenceIndex
 from wagtail.search import index
 from wagtail.search.queryset import SearchableQuerySetMixin
 
@@ -270,7 +269,7 @@ class AbstractImage(ImageFileMixin, CollectionMember, index.Indexed, models.Mode
         return full_path
 
     def get_usage(self):
-        return get_object_usage(self)
+        return ReferenceIndex.get_references_to(self).group_by_source_object()
 
     @property
     def usage_url(self):
@@ -756,6 +755,8 @@ class AbstractRendition(ImageFileMixin, models.Model):
         max_length=16, blank=True, default="", editable=False
     )
 
+    wagtail_reference_index_ignore = True
+
     @property
     def url(self):
         return self.file.url

+ 16 - 18
wagtail/images/templates/wagtailimages/images/usage.html

@@ -7,34 +7,32 @@
 
     <div class="nice-padding">
         <table class="listing">
-            <col />
-            <col width="30%"/>
-            <col width="15%"/>
-            <col width="15%"/>
             <thead>
                 <tr>
                     <th class="title">{% trans "Title" %}</th>
-                    <th>{% trans "Parent" %}</th>
-                    <th>{% trans "Type" %}</th>
-                    <th>{% trans "Status" %}</th>
+                    <th>Field</th>
                 </tr>
             </thead>
             <tbody>
-                {% for page in used_by %}
+                {% for object, references in used_by %}
                     <tr>
                         <td class="title" valign="top">
-                            <div class="title-wrapper"><a href="{% url 'wagtailadmin_pages:edit' page.id %}" title="{% trans 'Edit this page' %}">{{ page.title }}</a></div>
+                            <div class="title-wrapper">
+                                {% if object.edit_url %}<a href="{{ object.edit_url }}" title="{% trans 'Edit this page' %}">{% endif %}
+                                {{ object }}
+                                {% if object.edit_url %}</a>{% endif %}
+                            </div>
                         </td>
                         <td>
-                            {% if page.get_parent %}
-                                <a href="{% url 'wagtailadmin_explore' page.get_parent.id %}">{{ page.get_parent.title }}</a>
-                            {% endif %}
-                        </td>
-                        <td>
-                            {{ page.page_type_display_name }}
-                        </td>
-                        <td>
-                            {% include "wagtailadmin/shared/page_status_tag.html" with page=page %}
+                            <ul>
+                                {% for reference in references %}
+                                    <li>
+                                        {% if object.edit_url %}<a href="{{ object.edit_url }}#content-path-{{ reference.content_path }}">{% endif %}
+                                        {{ reference.describe_source_field }}
+                                        {% if object.edit_url %}</a>{% endif %}
+                                    </li>
+                                {% endfor %}
+                            </ul>
                         </td>
                     </tr>
                 {% endfor %}

+ 6 - 2
wagtail/images/tests/test_models.py

@@ -14,7 +14,7 @@ from willow.image import Image as WillowImage
 
 from wagtail.images.models import Rendition, SourceImageIOError, get_rendition_storage
 from wagtail.images.rect import Rect
-from wagtail.models import Collection, GroupCollectionPermission, Page
+from wagtail.models import Collection, GroupCollectionPermission, Page, ReferenceIndex
 from wagtail.test.testapp.models import (
     EventPage,
     EventPageCarouselItem,
@@ -567,7 +567,11 @@ class TestGetUsage(TestCase):
         event_page_carousel_item.page = page
         event_page_carousel_item.image = self.image
         event_page_carousel_item.save()
-        self.assertTrue(issubclass(Page, type(self.image.get_usage()[0])))
+
+        self.assertIsInstance(self.image.get_usage()[0], tuple)
+        self.assertIsInstance(self.image.get_usage()[0][0], Page)
+        self.assertIsInstance(self.image.get_usage()[0][1], list)
+        self.assertIsInstance(self.image.get_usage()[0][1][0], ReferenceIndex)
 
 
 class TestGetWillowImage(TestCase):

+ 6 - 0
wagtail/images/views/images.py

@@ -14,6 +14,7 @@ from django.utils.translation import gettext as _
 from django.views.generic import TemplateView
 
 from wagtail.admin import messages
+from wagtail.admin.admin_url_finder import AdminURLFinder
 from wagtail.admin.auth import PermissionPolicyChecker
 from wagtail.admin.forms.search import SearchForm
 from wagtail.admin.models import popular_tags_for_model
@@ -402,6 +403,11 @@ def usage(request, image_id):
     paginator = Paginator(image.get_usage(), per_page=USAGE_PAGE_SIZE)
     used_by = paginator.get_page(request.GET.get("p"))
 
+    # Add edit URLs to each source object
+    url_finder = AdminURLFinder(request.user)
+    for object, references in used_by:
+        object.edit_url = url_finder.get_edit_url(object)
+
     return TemplateResponse(
         request, "wagtailimages/images/usage.html", {"image": image, "used_by": used_by}
     )

+ 61 - 0
wagtail/models/__init__.py

@@ -4633,6 +4633,46 @@ class PageSubscription(models.Model):
         ]
 
 
+class ReferenceGroups:
+    def __init__(self, qs):
+        self.qs = qs
+
+    def __iter__(self):
+        object = None
+        references = []
+        for reference in self.qs.order_by("base_content_type", "object_id"):
+            if object != (reference.base_content_type, reference.object_id):
+                if object is not None:
+                    yield object[0].get_object_for_this_type(pk=object[1]), references
+                    references = []
+
+                object = (reference.base_content_type, reference.object_id)
+
+            references.append(reference)
+
+        if references:
+            yield object[0].get_object_for_this_type(pk=object[1]), references
+
+    def __len__(self):
+        return (
+            self.qs.order_by("base_content_type", "object_id")
+            .values("base_content_type", "object_id")
+            .distinct()
+            .count()
+        )
+
+    def count(self):
+        return len(self)
+
+    def __getitem__(self, key):
+        return list(self)[key]
+
+
+class ReferenceIndexQuerySet(models.QuerySet):
+    def group_by_source_object(self):
+        return ReferenceGroups(self)
+
+
 class ReferenceIndex(models.Model):
     """
     Records references between objects for quick retrieval of object usage.
@@ -4699,6 +4739,8 @@ class ReferenceIndex(models.Model):
     # MySQL has a limit to the size of fields that are included in unique keys
     content_path_hash = models.UUIDField()
 
+    objects = ReferenceIndexQuerySet.as_manager()
+
     wagtail_reference_index_ignore = True
 
     class Meta:
@@ -4870,3 +4912,22 @@ class ReferenceIndex(models.Model):
         cls.objects.filter(
             id__in=[existing_references[reference] for reference in deleted_references]
         ).delete()
+
+    @classmethod
+    def get_references_to(cls, object):
+        return cls.objects.filter(
+            to_content_type_id=cls._get_base_content_type(object),
+            to_object_id=object.pk,
+        )
+
+    def describe_source_field(self):
+        model_path_components = self.model_path.split(".")
+        field_name = model_path_components[0]
+        field = self.content_type.model_class()._meta.get_field(field_name)
+
+        # ManyToOneRel (reverse accessor for ParentalKey) does not have a verbose name. So get the name of the child field instead
+        if isinstance(field, models.ManyToOneRel):
+            child_field = field.related_model._meta.get_field(model_path_components[2])
+            return capfirst(child_field.verbose_name)
+        else:
+            return capfirst(field.verbose_name)

+ 4 - 2
wagtail/snippets/models.py

@@ -6,8 +6,8 @@ from django.utils.module_loading import import_string
 
 from wagtail.admin.checks import check_panels_in_model
 from wagtail.admin.forms.models import register_form_field_override
-from wagtail.admin.models import get_object_usage
 from wagtail.admin.viewsets import viewsets
+from wagtail.models import ReferenceIndex
 
 from .widgets import AdminSnippetChooser
 
@@ -46,7 +46,9 @@ def register_snippet(model, viewset=None):
         from wagtail.snippets.views.chooser import SnippetChooserViewSet
         from wagtail.snippets.views.snippets import SnippetViewSet
 
-        model.get_usage = get_object_usage
+        model.get_usage = lambda obj: ReferenceIndex.get_references_to(
+            obj
+        ).group_by_source_object()
         model.usage_url = get_snippet_usage_url
         model.get_admin_base_path = get_admin_base_path
         model.get_admin_url_namespace = get_admin_url_namespace

+ 17 - 19
wagtail/snippets/templates/wagtailsnippets/snippets/usage.html

@@ -1,5 +1,5 @@
 {% extends "wagtailadmin/base.html" %}
-{% load i18n wagtailadmin_tags %}
+{% load i18n %}
 {% block titletag %}{% blocktrans trimmed with title=object %}Usage of {{ title }}{% endblocktrans %}{% endblock %}
 {% block bodyclass %}model-{{ model_opts.model_name }}{% endblock %}
 
@@ -11,34 +11,32 @@
 
     <div class="nice-padding">
         <table class="listing">
-            <col />
-            <col width="30%"/>
-            <col width="15%"/>
-            <col width="15%"/>
             <thead>
                 <tr>
                     <th class="title">{% trans "Title" %}</th>
-                    <th>{% trans "Parent" %}</th>
-                    <th>{% trans "Type" %}</th>
-                    <th>{% trans "Status" %}</th>
+                    <th>Field</th>
                 </tr>
             </thead>
             <tbody>
-                {% for page in used_by %}
+                {% for object, references in used_by %}
                     <tr>
                         <td class="title" valign="top">
-                            <div class="title-wrapper"><a href="{% url 'wagtailadmin_pages:edit' page.id %}" title="{% trans 'Edit this page' %}">{{ page.title }}</a></div>
+                            <div class="title-wrapper">
+                                {% if object.edit_url %}<a href="{{ object.edit_url }}" title="{% trans 'Edit this page' %}">{% endif %}
+                                {{ object }}
+                                {% if object.edit_url %}</a>{% endif %}
+                            </div>
                         </td>
                         <td>
-                            {% if page.get_parent %}
-                                <a href="{% url 'wagtailadmin_explore' page.get_parent.id %}">{{ page.get_parent.title }}</a>
-                            {% endif %}
-                        </td>
-                        <td>
-                            {{ page.specific_class.get_verbose_name }}
-                        </td>
-                        <td>
-                            {% include "wagtailadmin/shared/page_status_tag.html" with page=page %}
+                            <ul>
+                                {% for reference in references %}
+                                    <li>
+                                        {% if object.edit_url %}<a href="{{ object.edit_url }}#content-path-{{ reference.content_path }}">{% endif %}
+                                        {{ reference.describe_source_field }}
+                                        {% if object.edit_url %}</a>{% endif %}
+                                    </li>
+                                {% endfor %}
+                            </ul>
                         </td>
                     </tr>
                 {% endfor %}

+ 19 - 3
wagtail/snippets/tests/test_snippets.py

@@ -6,7 +6,7 @@ from django.contrib.admin.utils import quote
 from django.contrib.auth import get_user_model
 from django.contrib.auth.models import AnonymousUser, Permission
 from django.contrib.contenttypes.models import ContentType
-from django.core import checks
+from django.core import checks, management
 from django.core.exceptions import ValidationError
 from django.core.files.base import ContentFile
 from django.core.files.uploadedfile import SimpleUploadedFile
@@ -24,7 +24,7 @@ from wagtail.admin.admin_url_finder import AdminURLFinder
 from wagtail.admin.forms import WagtailAdminModelForm
 from wagtail.admin.panels import FieldPanel, ObjectList, Panel, get_edit_handler
 from wagtail.blocks.field_block import FieldBlockAdapter
-from wagtail.models import Locale, ModelLogEntry, Page, Revision
+from wagtail.models import Locale, ModelLogEntry, Page, ReferenceIndex, Revision
 from wagtail.signals import published, unpublished
 from wagtail.snippets.action_menu import (
     ActionMenuItem,
@@ -2804,6 +2804,8 @@ class TestSnippetDelete(TestCase, WagtailTestUtils):
 
     @override_settings(WAGTAIL_USAGE_COUNT_ENABLED=True)
     def test_usage_link(self):
+        management.call_command("rebuild_references_index")
+
         response = self.client.get(
             reverse(
                 "wagtailsnippets_tests_advert:delete",
@@ -3043,6 +3045,11 @@ class TestSnippetOrdering(TestCase):
 class TestUsageCount(TestCase):
     fixtures = ["test.json"]
 
+    @classmethod
+    def setUpTestData(cls):
+        super().setUpTestData()
+        management.call_command("rebuild_references_index")
+
     @override_settings(WAGTAIL_USAGE_COUNT_ENABLED=True)
     def test_snippet_usage_count(self):
         advert = Advert.objects.get(pk=1)
@@ -3052,10 +3059,19 @@ class TestUsageCount(TestCase):
 class TestUsedBy(TestCase):
     fixtures = ["test.json"]
 
+    @classmethod
+    def setUpTestData(cls):
+        super().setUpTestData()
+        management.call_command("rebuild_references_index")
+
     @override_settings(WAGTAIL_USAGE_COUNT_ENABLED=True)
     def test_snippet_used_by(self):
         advert = Advert.objects.get(pk=1)
-        self.assertEqual(type(advert.get_usage()[0]), Page)
+
+        self.assertIsInstance(advert.get_usage()[0], tuple)
+        self.assertIsInstance(advert.get_usage()[0][0], Page)
+        self.assertIsInstance(advert.get_usage()[0][1], list)
+        self.assertIsInstance(advert.get_usage()[0][1][0], ReferenceIndex)
 
 
 @override_settings(WAGTAIL_USAGE_COUNT_ENABLED=True)

+ 7 - 1
wagtail/snippets/views/snippets.py

@@ -16,7 +16,7 @@ from django.utils.translation import gettext as _
 from django.utils.translation import gettext_lazy, ngettext
 
 from wagtail.admin import messages
-from wagtail.admin.admin_url_finder import register_admin_url_finder
+from wagtail.admin.admin_url_finder import AdminURLFinder, register_admin_url_finder
 from wagtail.admin.filters import DateRangePickerWidget, WagtailFilterSet
 from wagtail.admin.panels import get_edit_handler
 from wagtail.admin.ui.tables import (
@@ -572,6 +572,12 @@ class UsageView(generic.IndexView):
 
         page_number = self.request.GET.get(self.page_kwarg)
         page = paginator.get_page(page_number)
+
+        # Add edit URLs to each source object
+        url_finder = AdminURLFinder(self.request.user)
+        for object, references in page:
+            object.edit_url = url_finder.get_edit_url(object)
+
         return (paginator, page, page.object_list, page.has_other_pages())
 
     def get_context_data(self, **kwargs):