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