text: """
  Copy of ``django.contrib.admin.utils.get_deleted_objects`` and a subclass of
  ``django.contrib.admin.utils.NestedObjects`` that work with django-polymorphic
  querysets.
  Ultimately these should go directly into django-polymorphic or, in a more
  generic way, into Django itself.
  
  This code has been copied from Django 1.9.4.
  
  At all locations where something has been changed, there are inline comments
  in the code.
  """
  from collections import defaultdict
  
  from django import VERSION as DJANGO_VERSION
  from django.contrib.admin.utils import quote
  from django.contrib.auth import get_permission_codename
  from django.db import models
  from django.db.models.deletion import Collector
  from django.urls import NoReverseMatch, reverse
  from django.utils.encoding import force_str
  from django.utils.html import format_html
  from django.utils.text import capfirst
  
  
  def get_deleted_objects(objs, opts, user, admin_site, using):
      """
      Find all objects related to ``objs`` that should also be deleted. ``objs``
      must be a homogeneous iterable of objects (e.g. a QuerySet).
      Returns a nested list of strings suitable for display in the
      template with the ``unordered_list`` filter.
      """
      # --- begin patch ---
      collector = PolymorphicAwareNestedObjects(using=using)
      # --- end patch ---
      collector.collect(objs)
      perms_needed = set()
  
      def format_callback(obj):
          has_admin = obj.__class__ in admin_site._registry
          opts = obj._meta
  
          no_edit_link = '{}: {}'.format(capfirst(opts.verbose_name),
                                     force_str(obj))
  
          if has_admin:
              try:
                  admin_url = reverse('%s:%s_%s_change'
                                      % (admin_site.name,
                                         opts.app_label,
                                         opts.model_name),
                                      None, (quote(obj._get_pk_val()),))
              except NoReverseMatch:
                  # Change url doesn't exist -- don't display link to edit
                  return no_edit_link
  
              p = '{}.{}'.format(opts.app_label,
                             get_permission_codename('delete', opts))
              if not user.has_perm(p):
                  perms_needed.add(opts.verbose_name)
              # Display a link to the admin page.
              return format_html('{}: <a href="{}">{}</a>',
                                 capfirst(opts.verbose_name),
                                 admin_url,
                                 obj)
          else:
              # Don't display link to edit, because it either has no
              # admin or is edited inline.
              return no_edit_link
  
      to_delete = collector.nested(format_callback)
  
      protected = [format_callback(obj) for obj in collector.protected]
      model_count = {model._meta.verbose_name_plural: len(objs) for model, objs in collector.model_objs.items()}
  
      return to_delete, model_count, perms_needed, protected
  
  
  class NestedObjects(Collector):
      def __init__(self, *args, **kwargs):
          super().__init__(*args, **kwargs)
          self.edges = {}  # {from_instance: [to_instances]}
          self.protected = set()
          self.model_objs = defaultdict(set)
  
      def add_edge(self, source, target):
          self.edges.setdefault(source, []).append(target)
  
      def collect(self, objs, source=None, source_attr=None, **kwargs):
          for obj in objs:
              if source_attr and not source_attr.endswith('+'):
                  related_name = source_attr % {
                      'class': source._meta.model_name,
                      'app_label': source._meta.app_label,
                  }
                  self.add_edge(getattr(obj, related_name), obj)
              else:
                  self.add_edge(None, obj)
              self.model_objs[obj._meta.model].add(obj)
          try:
              return super().collect(objs, source_attr=source_attr, **kwargs)
          except models.ProtectedError as e:
              self.protected.update(e.protected_objects)
  
      if DJANGO_VERSION >= (3, 1):
          def related_objects(self, related_model, related_fields, objs):
              qs = super().related_objects(related_model, related_fields, objs)
              return qs.select_related(*[related.name for related in related_fields])
      else:
          def related_objects(self, related, objs):
              qs = super().related_objects(related, objs)
              return qs.select_related(related.field.name)
  
      def _nested(self, obj, seen, format_callback):
          if obj in seen:
              return []
          seen.add(obj)
          children = []
          for child in self.edges.get(obj, ()):
              children.extend(self._nested(child, seen, format_callback))
          if format_callback:
              ret = [format_callback(obj)]
          else:
              ret = [obj]
          if children:
              ret.append(children)
          return ret
  
      def nested(self, format_callback=None):
          """
          Return the graph as a nested list.
          """
          seen = set()
          roots = []
          for root in self.edges.get(None, ()):
              roots.extend(self._nested(root, seen, format_callback))
          return roots
  
      def can_fast_delete(self, *args, **kwargs):
          """
          We always want to load the objects into memory so that we can display
          them to the user in confirm page.
          """
          return False
  
  
  class PolymorphicAwareNestedObjects(NestedObjects):
      def collect(self, objs, source_attr=None, **kwargs):
          if hasattr(objs, 'non_polymorphic'):
              # .filter() is needed, because there may already be cached
              # polymorphic results in the queryset
              objs = objs.non_polymorphic().filter()
          return super().collect(
              objs, source_attr=source_attr, **kwargs)
  