commit 2c537944c0690764abbbedef9e42d2aabe3a79f7
Author: Jonas Haag <jonas@lophus.org>
Date: Mon Mar 25 18:42:23 2013 +0100
patch1
diff --git a/django/contrib/admin/actions.py b/django/contrib/admin/actions.py
index d11ba3d..6ae117b 100644
a
|
b
|
def delete_selected(modeladmin, request, queryset):
|
31 | 31 | |
32 | 32 | # Populate deletable_objects, a data structure of all related objects that |
33 | 33 | # will also be deleted. |
34 | | deletable_objects, perms_needed, protected = get_deleted_objects( |
| 34 | deletable_objects, model_count, perms_needed, protected = get_deleted_objects( |
35 | 35 | queryset, opts, request.user, modeladmin.admin_site, using) |
36 | 36 | |
37 | 37 | # The user has already confirmed the deletion. |
… |
… |
def delete_selected(modeladmin, request, queryset):
|
65 | 65 | "title": title, |
66 | 66 | "objects_name": objects_name, |
67 | 67 | "deletable_objects": [deletable_objects], |
| 68 | "model_count": dict(model_count), |
68 | 69 | 'queryset': queryset, |
69 | 70 | "perms_lacking": perms_needed, |
70 | 71 | "protected": protected, |
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index f7bfca4..b4132aa 100644
a
|
b
|
class ModelAdmin(BaseModelAdmin):
|
1336 | 1336 | |
1337 | 1337 | # Populate deleted_objects, a data structure of all related objects that |
1338 | 1338 | # will also be deleted. |
1339 | | (deleted_objects, perms_needed, protected) = get_deleted_objects( |
| 1339 | (deleted_objects, model_count, perms_needed, protected) = get_deleted_objects( |
1340 | 1340 | [obj], opts, request.user, self.admin_site, using) |
1341 | 1341 | |
1342 | 1342 | if request.POST: # The user has already confirmed the deletion. |
… |
… |
class ModelAdmin(BaseModelAdmin):
|
1367 | 1367 | "object_name": object_name, |
1368 | 1368 | "object": obj, |
1369 | 1369 | "deleted_objects": deleted_objects, |
| 1370 | "model_count": dict(model_count), |
1370 | 1371 | "perms_lacking": perms_needed, |
1371 | 1372 | "protected": protected, |
1372 | 1373 | "opts": opts, |
diff --git a/django/contrib/admin/templates/admin/delete_confirmation.html b/django/contrib/admin/templates/admin/delete_confirmation.html
index c1a7115..150a813 100644
a
|
b
|
|
13 | 13 | {% endblock %} |
14 | 14 | |
15 | 15 | {% block content %} |
16 | | {% if perms_lacking or protected %} |
17 | | {% if perms_lacking %} |
18 | | <p>{% blocktrans with escaped_object=object %}Deleting the {{ object_name }} '{{ escaped_object }}' would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktrans %}</p> |
19 | | <ul> |
20 | | {% for obj in perms_lacking %} |
21 | | <li>{{ obj }}</li> |
22 | | {% endfor %} |
23 | | </ul> |
24 | | {% endif %} |
25 | | {% if protected %} |
26 | | <p>{% blocktrans with escaped_object=object %}Deleting the {{ object_name }} '{{ escaped_object }}' would require deleting the following protected related objects:{% endblocktrans %}</p> |
27 | | <ul> |
28 | | {% for obj in protected %} |
29 | | <li>{{ obj }}</li> |
30 | | {% endfor %} |
31 | | </ul> |
32 | | {% endif %} |
| 16 | {% if perms_lacking %} |
| 17 | <p>{% blocktrans with escaped_object=object %}Deleting the {{ object_name }} '{{ escaped_object }}' would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktrans %}</p> |
| 18 | <ul> |
| 19 | {% for obj in perms_lacking %} |
| 20 | <li>{{ obj }}</li> |
| 21 | {% endfor %} |
| 22 | </ul> |
| 23 | {% elif protected %} |
| 24 | <p>{% blocktrans with escaped_object=object %}Deleting the {{ object_name }} '{{ escaped_object }}' would require deleting the following protected related objects:{% endblocktrans %}</p> |
| 25 | <ul> |
| 26 | {% for obj in protected %} |
| 27 | <li>{{ obj }}</li> |
| 28 | {% endfor %} |
| 29 | </ul> |
33 | 30 | {% else %} |
34 | 31 | <p>{% blocktrans with escaped_object=object %}Are you sure you want to delete the {{ object_name }} "{{ escaped_object }}"? All of the following related items will be deleted:{% endblocktrans %}</p> |
| 32 | <h2>{% trans "Summary" %}</h2> |
| 33 | <ul> |
| 34 | {% for model_name, object_count in model_count.items %} |
| 35 | <li>{{ model_name }}: {{ object_count }}</li> |
| 36 | {% endfor %} |
| 37 | </ul> |
| 38 | <h2>{% trans "Objects" %}</h2> |
35 | 39 | <ul>{{ deleted_objects|unordered_list }}</ul> |
36 | 40 | <form action="" method="post">{% csrf_token %} |
37 | 41 | <div> |
diff --git a/django/contrib/admin/templates/admin/delete_selected_confirmation.html b/django/contrib/admin/templates/admin/delete_selected_confirmation.html
index 608052d..558bbbc 100644
a
|
b
|
|
12 | 12 | {% endblock %} |
13 | 13 | |
14 | 14 | {% block content %} |
15 | | {% if perms_lacking or protected %} |
16 | | {% if perms_lacking %} |
17 | | <p>{% blocktrans %}Deleting the selected {{ objects_name }} would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktrans %}</p> |
18 | | <ul> |
19 | | {% for obj in perms_lacking %} |
20 | | <li>{{ obj }}</li> |
21 | | {% endfor %} |
22 | | </ul> |
23 | | {% endif %} |
24 | | {% if protected %} |
25 | | <p>{% blocktrans %}Deleting the selected {{ objects_name }} would require deleting the following protected related objects:{% endblocktrans %}</p> |
26 | | <ul> |
27 | | {% for obj in protected %} |
28 | | <li>{{ obj }}</li> |
29 | | {% endfor %} |
30 | | </ul> |
31 | | {% endif %} |
| 15 | {% if perms_lacking %} |
| 16 | <p>{% blocktrans %}Deleting the selected {{ objects_name }} would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:{% endblocktrans %}</p> |
| 17 | <ul> |
| 18 | {% for obj in perms_lacking %} |
| 19 | <li>{{ obj }}</li> |
| 20 | {% endfor %} |
| 21 | </ul> |
| 22 | {% endif %} |
| 23 | {% if protected %} |
| 24 | <p>{% blocktrans %}Deleting the selected {{ objects_name }} would require deleting the following protected related objects:{% endblocktrans %}</p> |
| 25 | <ul> |
| 26 | {% for obj in protected %} |
| 27 | <li>{{ obj }}</li> |
| 28 | {% endfor %} |
| 29 | </ul> |
32 | 30 | {% else %} |
33 | 31 | <p>{% blocktrans %}Are you sure you want to delete the selected {{ objects_name }}? All of the following objects and their related items will be deleted:{% endblocktrans %}</p> |
| 32 | <h2>{% trans "Summary" %}</h2> |
| 33 | <ul> |
| 34 | {% for model_name, object_count in model_count.items %} |
| 35 | <li>{{ model_name }}: {{ object_count }}</li> |
| 36 | {% endfor %} |
| 37 | </ul> |
| 38 | <h2>{% trans "Objects" %}</h2> |
34 | 39 | {% for deletable_object in deletable_objects %} |
35 | 40 | <ul>{{ deletable_object|unordered_list }}</ul> |
36 | 41 | {% endfor %} |
diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py
index 97858e6..d74675a 100644
a
|
b
|
from __future__ import unicode_literals
|
2 | 2 | |
3 | 3 | import datetime |
4 | 4 | import decimal |
| 5 | from collections import defaultdict |
5 | 6 | |
6 | 7 | from django.db import models |
7 | 8 | from django.db.models.constants import LOOKUP_SEP |
… |
… |
def flatten_fieldsets(fieldsets):
|
95 | 96 | return field_names |
96 | 97 | |
97 | 98 | |
| 99 | def map_nested_list(func, ls): |
| 100 | if isinstance(ls, (tuple, list)): |
| 101 | return [map_nested_list(func, x) for x in ls] |
| 102 | else: |
| 103 | return func(ls) |
| 104 | |
| 105 | |
98 | 106 | def get_deleted_objects(objs, opts, user, admin_site, using): |
99 | 107 | """ |
100 | 108 | Find all objects related to ``objs`` that should also be deleted. ``objs`` |
101 | 109 | must be a homogenous iterable of objects (e.g. a QuerySet). |
102 | 110 | |
103 | | Returns a nested list of strings suitable for display in the |
104 | | template with the ``unordered_list`` filter. |
| 111 | Returns a 4-tuple ((deleted_objects, model_count), perms_needed, protected) where |
105 | 112 | |
| 113 | * deleted_objects is a nested list of strings suitable for display in the |
| 114 | template with the ``unordered_list`` filter. |
| 115 | * model_count is a map from "model name" to the "number of objects of that |
| 116 | type involved in the deletion", e.g. {'User': 3, 'Group': 5} |
| 117 | * perms_needed is a list of model names that hinder the deletion because of |
| 118 | lacking permissions |
| 119 | * protected is a list of objects that hinder the deletion because their |
| 120 | deletion strategy is set to PROTECTED in the model definition. |
106 | 121 | """ |
107 | | collector = NestedObjects(using=using) |
| 122 | collector = NestedObjects(user, using=using) |
108 | 123 | collector.collect(objs) |
109 | | perms_needed = set() |
| 124 | |
| 125 | if collector.perms_needed: |
| 126 | return [], {}, collector.perms_needed, [] |
110 | 127 | |
111 | 128 | def format_callback(obj): |
112 | 129 | has_admin = obj.__class__ in admin_site._registry |
113 | 130 | opts = obj._meta |
114 | 131 | |
115 | 132 | if has_admin: |
| 133 | # Display a link to the admin page. |
116 | 134 | admin_url = reverse('%s:%s_%s_change' |
117 | 135 | % (admin_site.name, |
118 | 136 | opts.app_label, |
119 | 137 | opts.model_name), |
120 | 138 | None, (quote(obj._get_pk_val()),)) |
121 | | p = '%s.%s' % (opts.app_label, |
122 | | opts.get_delete_permission()) |
123 | | if not user.has_perm(p): |
124 | | perms_needed.add(opts.verbose_name) |
125 | | # Display a link to the admin page. |
126 | 139 | return format_html('{0}: <a href="{1}">{2}</a>', |
127 | 140 | capfirst(opts.verbose_name), |
128 | 141 | admin_url, |
… |
… |
def get_deleted_objects(objs, opts, user, admin_site, using):
|
133 | 146 | return '%s: %s' % (capfirst(opts.verbose_name), |
134 | 147 | force_text(obj)) |
135 | 148 | |
136 | | to_delete = collector.nested(format_callback) |
137 | | |
138 | | protected = [format_callback(obj) for obj in collector.protected] |
| 149 | if collector.protected: |
| 150 | return [], {}, [], map(format_callback, collector.protected) |
139 | 151 | |
140 | | return to_delete, perms_needed, protected |
| 152 | to_delete = map_nested_list(format_callback, collector.as_nested_list()) |
| 153 | return to_delete, collector.model_count, [], [] |
141 | 154 | |
142 | 155 | |
143 | 156 | class NestedObjects(Collector): |
144 | | def __init__(self, *args, **kwargs): |
| 157 | def __init__(self, user, *args, **kwargs): |
145 | 158 | super(NestedObjects, self).__init__(*args, **kwargs) |
| 159 | self.user = user |
146 | 160 | self.edges = {} # {from_instance: [to_instances]} |
147 | 161 | self.protected = set() |
| 162 | self.perms_needed = set() |
| 163 | self.model_count = defaultdict(int) |
148 | 164 | |
149 | 165 | def add_edge(self, source, target): |
150 | 166 | self.edges.setdefault(source, []).append(target) |
151 | 167 | |
152 | 168 | def collect(self, objs, source_attr=None, **kwargs): |
153 | 169 | for obj in objs: |
| 170 | opts = obj._meta |
| 171 | permission_name = '%s.%s' % (opts.app_label, |
| 172 | opts.get_delete_permission()) |
| 173 | if not self.user.has_perm(permission_name): |
| 174 | self.perms_needed.add(opts.verbose_name) |
| 175 | |
154 | 176 | if source_attr: |
155 | 177 | self.add_edge(getattr(obj, source_attr), obj) |
156 | 178 | else: |
157 | 179 | if obj._meta.proxy: |
158 | 180 | # Take concrete model's instance to avoid mismatch in edges |
159 | | obj = obj._meta.concrete_model(pk=obj.pk) |
| 181 | obj = opts.concrete_model(pk=obj.pk) |
160 | 182 | self.add_edge(None, obj) |
| 183 | |
| 184 | self.model_count[opts.verbose_name] += 1 |
| 185 | |
161 | 186 | try: |
162 | 187 | return super(NestedObjects, self).collect(objs, source_attr=source_attr, **kwargs) |
163 | 188 | except models.ProtectedError as e: |
… |
… |
class NestedObjects(Collector):
|
167 | 192 | qs = super(NestedObjects, self).related_objects(related, objs) |
168 | 193 | return qs.select_related(related.field.name) |
169 | 194 | |
170 | | def _nested(self, obj, seen, format_callback): |
| 195 | def _as_nested_list(self, obj, seen): |
171 | 196 | if obj in seen: |
172 | 197 | return [] |
173 | 198 | seen.add(obj) |
174 | 199 | children = [] |
175 | 200 | for child in self.edges.get(obj, ()): |
176 | | children.extend(self._nested(child, seen, format_callback)) |
177 | | if format_callback: |
178 | | ret = [format_callback(obj)] |
179 | | else: |
180 | | ret = [obj] |
| 201 | children.extend(self._as_nested_list(child, seen)) |
181 | 202 | if children: |
182 | | ret.append(children) |
183 | | return ret |
| 203 | return [obj, children] |
| 204 | else: |
| 205 | return [obj] |
184 | 206 | |
185 | | def nested(self, format_callback=None): |
| 207 | def as_nested_list(self): |
186 | 208 | """ |
187 | 209 | Return the graph as a nested list. |
188 | | |
189 | 210 | """ |
190 | 211 | seen = set() |
191 | 212 | roots = [] |
192 | 213 | for root in self.edges.get(None, ()): |
193 | | roots.extend(self._nested(root, seen, format_callback)) |
| 214 | roots.extend(self._as_nested_list(root, seen)) |
194 | 215 | return roots |
195 | 216 | |
196 | 217 | def can_fast_delete(self, *args, **kwargs): |
diff --git a/tests/admin_util/tests.py b/tests/admin_util/tests.py
index 7898f20..7e7037a 100644
a
|
b
|
from datetime import datetime
|
4 | 4 | |
5 | 5 | from django.conf import settings |
6 | 6 | from django.contrib import admin |
| 7 | from django.contrib.auth.models import User |
7 | 8 | from django.contrib.admin import helpers |
8 | 9 | from django.contrib.admin.util import (display_for_field, flatten_fieldsets, |
9 | | label_for_field, lookup_field, NestedObjects) |
| 10 | label_for_field, lookup_field, NestedObjects, map_nested_list) |
10 | 11 | from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE |
11 | 12 | from django.contrib.sites.models import Site |
12 | 13 | from django.db import models, DEFAULT_DB_ALIAS |
… |
… |
class NestedObjectsTests(TestCase):
|
26 | 27 | |
27 | 28 | """ |
28 | 29 | def setUp(self): |
29 | | self.n = NestedObjects(using=DEFAULT_DB_ALIAS) |
| 30 | self.n = NestedObjects(User.objects.create(), using=DEFAULT_DB_ALIAS) |
30 | 31 | self.objs = [Count.objects.create(num=i) for i in range(5)] |
31 | 32 | |
32 | 33 | def _check(self, target): |
33 | | self.assertEqual(self.n.nested(lambda obj: obj.num), target) |
| 34 | self.assertEqual( |
| 35 | map_nested_list(lambda obj: obj.num, self.n.as_nested_list()), |
| 36 | target |
| 37 | ) |
34 | 38 | |
35 | 39 | def _connect(self, i, j): |
36 | 40 | self.objs[i].parent = self.objs[j] |
… |
… |
class NestedObjectsTests(TestCase):
|
68 | 72 | self._connect(2, 0) |
69 | 73 | # 1 query to fetch all children of 0 (1 and 2) |
70 | 74 | # 1 query to fetch all children of 1 and 2 (none) |
| 75 | # 2 queries to check for permissions of 2 objects |
71 | 76 | # Should not require additional queries to populate the nested graph. |
72 | | self.assertNumQueries(2, self._collect, 0) |
| 77 | self.assertNumQueries(4, self._collect, 0) |
73 | 78 | |
74 | 79 | def test_on_delete_do_nothing(self): |
75 | 80 | """ |
76 | 81 | Check that the nested collector doesn't query for DO_NOTHING objects. |
77 | 82 | """ |
78 | | n = NestedObjects(using=DEFAULT_DB_ALIAS) |
79 | 83 | objs = [Event.objects.create()] |
80 | 84 | EventGuide.objects.create(event=objs[0]) |
81 | | with self.assertNumQueries(2): |
82 | | # One for Location, one for Guest, and no query for EventGuide |
83 | | n.collect(objs) |
| 85 | with self.assertNumQueries(4): |
| 86 | # One for Location, one for Guest, two for permissions, |
| 87 | # and no query for EventGuide |
| 88 | self.n.collect(objs) |
84 | 89 | |
85 | 90 | class UtilTests(unittest.TestCase): |
86 | 91 | def test_values_from_lookup_field(self): |