1# -*- coding: utf-8 -*- 2""" 3Copy of ``django.contrib.admin.utils.get_deleted_objects`` and a subclass of 4``django.contrib.admin.utils.NestedObjects`` that work with djongo_polymorphic 5querysets. 6Ultimatly these should go directly into django_polymorphic or, in a more 7generic way, into django itself. 8 9This code has been copied from Django 1.9.4. 10 11At all locations where something has been changed, there are inline comments 12in the code. 13""" 14from __future__ import absolute_import, unicode_literals 15 16from collections import defaultdict 17 18from django.contrib.admin.utils import quote 19from django.contrib.auth import get_permission_codename 20from django.db import models 21from django.db.models.deletion import Collector 22from django.urls import NoReverseMatch, reverse 23from django.utils.html import format_html 24from django.utils.text import capfirst 25 26 27try: 28 from django.utils.encoding import force_text 29except ImportError: 30 # Django < 1.5 31 from django.utils.encoding import force_unicode as force_text 32 33 34def get_deleted_objects(objs, opts, user, admin_site, using): 35 """ 36 Find all objects related to ``objs`` that should also be deleted. ``objs`` 37 must be a homogeneous iterable of objects (e.g. a QuerySet). 38 Returns a nested list of strings suitable for display in the 39 template with the ``unordered_list`` filter. 40 """ 41 # --- begin patch --- 42 collector = PolymorphicAwareNestedObjects(using=using) 43 # --- end patch --- 44 collector.collect(objs) 45 perms_needed = set() 46 47 def format_callback(obj): 48 has_admin = obj.__class__ in admin_site._registry 49 opts = obj._meta 50 51 no_edit_link = '%s: %s' % (capfirst(opts.verbose_name), 52 force_text(obj)) 53 54 if has_admin: 55 try: 56 admin_url = reverse('%s:%s_%s_change' 57 % (admin_site.name, 58 opts.app_label, 59 opts.model_name), 60 None, (quote(obj._get_pk_val()),)) 61 except NoReverseMatch: 62 # Change url doesn't exist -- don't display link to edit 63 return no_edit_link 64 65 p = '%s.%s' % (opts.app_label, 66 get_permission_codename('delete', opts)) 67 if not user.has_perm(p): 68 perms_needed.add(opts.verbose_name) 69 # Display a link to the admin page. 70 return format_html('{}: <a href="{}">{}</a>', 71 capfirst(opts.verbose_name), 72 admin_url, 73 obj) 74 else: 75 # Don't display link to edit, because it either has no 76 # admin or is edited inline. 77 return no_edit_link 78 79 to_delete = collector.nested(format_callback) 80 81 protected = [format_callback(obj) for obj in collector.protected] 82 model_count = {model._meta.verbose_name_plural: len(objs) for model, objs in collector.model_objs.items()} 83 84 return to_delete, model_count, perms_needed, protected 85 86 87class NestedObjects(Collector): 88 def __init__(self, *args, **kwargs): 89 super(NestedObjects, self).__init__(*args, **kwargs) 90 self.edges = {} # {from_instance: [to_instances]} 91 self.protected = set() 92 self.model_objs = defaultdict(set) 93 94 def add_edge(self, source, target): 95 self.edges.setdefault(source, []).append(target) 96 97 def collect(self, objs, source=None, source_attr=None, **kwargs): 98 for obj in objs: 99 if source_attr and not source_attr.endswith('+'): 100 related_name = source_attr % { 101 'class': source._meta.model_name, 102 'app_label': source._meta.app_label, 103 } 104 self.add_edge(getattr(obj, related_name), obj) 105 else: 106 self.add_edge(None, obj) 107 self.model_objs[obj._meta.model].add(obj) 108 try: 109 return super(NestedObjects, self).collect(objs, source_attr=source_attr, **kwargs) 110 except models.ProtectedError as e: 111 self.protected.update(e.protected_objects) 112 113 def related_objects(self, related, objs): 114 qs = super(NestedObjects, self).related_objects(related, objs) 115 return qs.select_related(related.field.name) 116 117 def _nested(self, obj, seen, format_callback): 118 if obj in seen: 119 return [] 120 seen.add(obj) 121 children = [] 122 for child in self.edges.get(obj, ()): 123 children.extend(self._nested(child, seen, format_callback)) 124 if format_callback: 125 ret = [format_callback(obj)] 126 else: 127 ret = [obj] 128 if children: 129 ret.append(children) 130 return ret 131 132 def nested(self, format_callback=None): 133 """ 134 Return the graph as a nested list. 135 """ 136 seen = set() 137 roots = [] 138 for root in self.edges.get(None, ()): 139 roots.extend(self._nested(root, seen, format_callback)) 140 return roots 141 142 def can_fast_delete(self, *args, **kwargs): 143 """ 144 We always want to load the objects into memory so that we can display 145 them to the user in confirm page. 146 """ 147 return False 148 149 150class PolymorphicAwareNestedObjects(NestedObjects): 151 def collect(self, objs, source_attr=None, **kwargs): 152 if hasattr(objs, 'non_polymorphic'): 153 # .filter() is needed, because there may already be cached 154 # polymorphic results in the queryset 155 objs = objs.non_polymorphic().filter() 156 return super(PolymorphicAwareNestedObjects, self).collect( 157 objs, source_attr=source_attr, **kwargs) 158