Permalink
Browse files

[1.1.X] Fixed #6191, #11296 -- Modified the admin deletion confirmati…

…on page to use the same object collection scheme as the actual deletion. This ensures that all objects that may be deleted are actually deleted, and that cyclic display problems are avoided. Thanks to carljm for the patch.

Backport of r12598 from trunk.

git-svn-id: http://code.djangoproject.com/svn/django/branches/releases/1.1.X@12600 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
1 parent a52f1dd commit 3bda1f5bd3841b28a54e0e6f07b738ae3a856359 @freakboy3742 freakboy3742 committed Feb 26, 2010
@@ -36,15 +36,7 @@ def delete_selected(modeladmin, request, queryset):
# Populate deletable_objects, a data structure of all related objects that
# will also be deleted.
-
- # deletable_objects must be a list if we want to use '|unordered_list' in the template
- deletable_objects = []
- perms_needed = set()
- i = 0
- for obj in queryset:
- deletable_objects.append([mark_safe(u'%s: <a href="%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), obj.pk, escape(obj))), []])
- get_deleted_objects(deletable_objects[i], perms_needed, request.user, obj, opts, 1, modeladmin.admin_site, levels_to_root=2)
- i=i+1
+ deletable_objects, perms_needed = get_deleted_objects(queryset, opts, request.user, modeladmin.admin_site, levels_to_root=2)
# The user has already confirmed the deletion.
# Do the deletion and return a None to display the change list view again.
@@ -66,7 +58,7 @@ def delete_selected(modeladmin, request, queryset):
context = {
"title": _("Are you sure?"),
"object_name": force_unicode(opts.verbose_name),
- "deletable_objects": deletable_objects,
+ "deletable_objects": [deletable_objects],
'queryset': queryset,
"perms_lacking": perms_needed,
"opts": opts,
@@ -1014,9 +1014,7 @@ def delete_view(self, request, object_id, extra_context=None):
# Populate deleted_objects, a data structure of all related objects that
# will also be deleted.
- deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), object_id, escape(obj))), []]
- perms_needed = set()
- get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site)
+ (deleted_objects, perms_needed) = get_deleted_objects((obj,), opts, request.user, self.admin_site)
if request.POST: # The user has already confirmed the deletion.
if perms_needed:
@@ -20,8 +20,8 @@
</ul>
{% else %}
<p>{% blocktrans %}Are you sure you want to delete the selected {{ object_name }} objects? All of the following objects and their related items will be deleted:{% endblocktrans %}</p>
- {% for deleteable_object in deletable_objects %}
- <ul>{{ deleteable_object|unordered_list }}</ul>
+ {% for deletable_object in deletable_objects %}
+ <ul>{{ deletable_object|unordered_list }}</ul>
{% endfor %}
<form action="" method="post">
<div>
@@ -7,6 +7,8 @@
from django.utils.translation import ungettext, ugettext as _
from django.core.urlresolvers import reverse, NoReverseMatch
+from django.utils.datastructures import SortedDict
+
def quote(s):
"""
Ensure that primary key values do not confuse the admin URLs by escaping
@@ -55,135 +57,179 @@ def flatten_fieldsets(fieldsets):
field_names.append(field)
return field_names
-def _nest_help(obj, depth, val):
- current = obj
- for i in range(depth):
- current = current[-1]
- current.append(val)
-
-def get_change_view_url(app_label, module_name, pk, admin_site, levels_to_root):
- """
- Returns the url to the admin change view for the given app_label,
- module_name and primary key.
- """
+def _format_callback(obj, user, admin_site, levels_to_root, perms_needed):
+ has_admin = obj.__class__ in admin_site._registry
+ opts = obj._meta
try:
- return reverse('%sadmin_%s_%s_change' % (admin_site.name, app_label, module_name), None, (pk,))
+ admin_url = reverse('%s:%s_%s_change'
+ % (admin_site.name,
+ opts.app_label,
+ opts.object_name.lower()),
+ None, (quote(obj._get_pk_val()),))
except NoReverseMatch:
- return '%s%s/%s/%s/' % ('../'*levels_to_root, app_label, module_name, pk)
+ admin_url = '%s%s/%s/%s/' % ('../'*levels_to_root,
+ opts.app_label,
+ opts.object_name.lower(),
+ quote(obj._get_pk_val()))
+ if has_admin:
+ p = '%s.%s' % (opts.app_label,
+ opts.get_delete_permission())
+ if not user.has_perm(p):
+ perms_needed.add(opts.verbose_name)
+ # Display a link to the admin page.
+ return mark_safe(u'%s: <a href="%s">%s</a>' %
+ (escape(capfirst(opts.verbose_name)),
+ admin_url,
+ escape(obj)))
+ else:
+ # Don't display link to edit, because it either has no
+ # admin or is edited inline.
+ return u'%s: %s' % (capfirst(opts.verbose_name),
+ force_unicode(obj))
-def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_depth, admin_site, levels_to_root=4):
+def get_deleted_objects(objs, opts, user, admin_site, levels_to_root=4):
"""
- Helper function that recursively populates deleted_objects.
+ Find all objects related to ``objs`` that should also be
+ deleted. ``objs`` should be an iterable of objects.
- `levels_to_root` defines the number of directories (../) to reach the
- admin root path. In a change_view this is 4, in a change_list view 2.
+ Returns a nested list of strings suitable for display in the
+ template with the ``unordered_list`` filter.
+
+ `levels_to_root` defines the number of directories (../) to reach
+ the admin root path. In a change_view this is 4, in a change_list
+ view 2.
This is for backwards compatibility since the options.delete_selected
method uses this function also from a change_list view.
This will not be used if we can reverse the URL.
"""
- nh = _nest_help # Bind to local variable for performance
- if current_depth > 16:
- return # Avoid recursing too deep.
- opts_seen = []
- for related in opts.get_all_related_objects():
- has_admin = related.model in admin_site._registry
- if related.opts in opts_seen:
- continue
- opts_seen.append(related.opts)
- rel_opts_name = related.get_accessor_name()
- if isinstance(related.field.rel, models.OneToOneRel):
- try:
- sub_obj = getattr(obj, rel_opts_name)
- except ObjectDoesNotExist:
- pass
- else:
- if has_admin:
- p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission())
- if not user.has_perm(p):
- perms_needed.add(related.opts.verbose_name)
- # We don't care about populating deleted_objects now.
- continue
- if not has_admin:
- # Don't display link to edit, because it either has no
- # admin or is edited inline.
- nh(deleted_objects, current_depth,
- [u'%s: %s' % (capfirst(related.opts.verbose_name), force_unicode(sub_obj)), []])
- else:
- # Display a link to the admin page.
- nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="%s">%s</a>' %
- (escape(capfirst(related.opts.verbose_name)),
- get_change_view_url(related.opts.app_label,
- related.opts.object_name.lower(),
- sub_obj._get_pk_val(),
- admin_site,
- levels_to_root),
- escape(sub_obj))), []])
- get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site)
+ collector = NestedObjects()
+ for obj in objs:
+ # TODO using a private model API!
+ obj._collect_sub_objects(collector)
+
+ # TODO This next bit is needed only because GenericRelations are
+ # cascade-deleted way down in the internals in
+ # DeleteQuery.delete_batch_related, instead of being found by
+ # _collect_sub_objects. Refs #12593.
+ from django.contrib.contenttypes import generic
+ for f in obj._meta.many_to_many:
+ if isinstance(f, generic.GenericRelation):
+ rel_manager = f.value_from_object(obj)
+ for related in rel_manager.all():
+ # There's a wierdness here in the case that the
+ # generic-related object also has FKs pointing to it
+ # from elsewhere. DeleteQuery does not follow those
+ # FKs or delete any such objects explicitly (which is
+ # probably a bug). Some databases may cascade those
+ # deletes themselves, and some won't. So do we report
+ # those objects as to-be-deleted? No right answer; for
+ # now we opt to report only on objects that Django
+ # will explicitly delete, at risk that some further
+ # objects will be silently deleted by a
+ # referential-integrity-maintaining database.
+ collector.add(related.__class__, related.pk, related,
+ obj.__class__, obj)
+
+ perms_needed = set()
+
+ to_delete = collector.nested(_format_callback,
+ user=user,
+ admin_site=admin_site,
+ levels_to_root=levels_to_root,
+ perms_needed=perms_needed)
+
+ return to_delete, perms_needed
+
+
+class NestedObjects(object):
+ """
+ A directed acyclic graph collection that exposes the add() API
+ expected by Model._collect_sub_objects and can present its data as
+ a nested list of objects.
+
+ """
+ def __init__(self):
+ # Use object keys of the form (model, pk) because actual model
+ # objects may not be unique
+
+ # maps object key to list of child keys
+ self.children = SortedDict()
+
+ # maps object key to parent key
+ self.parents = SortedDict()
+
+ # maps object key to actual object
+ self.seen = SortedDict()
+
+ def add(self, model, pk, obj,
+ parent_model=None, parent_obj=None, nullable=False):
+ """
+ Add item ``obj`` to the graph. Returns True (and does nothing)
+ if the item has been seen already.
+
+ The ``parent_obj`` argument must already exist in the graph; if
+ not, it's ignored (but ``obj`` is still added with no
+ parent). In any case, Model._collect_sub_objects (for whom
+ this API exists) will never pass a parent that hasn't already
+ been added itself.
+
+ These restrictions in combination ensure the graph will remain
+ acyclic (but can have multiple roots).
+
+ ``model``, ``pk``, and ``parent_model`` arguments are ignored
+ in favor of the appropriate lookups on ``obj`` and
+ ``parent_obj``; unlike CollectedObjects, we can't maintain
+ independence from the knowledge that we're operating on model
+ instances, and we don't want to allow for inconsistency.
+
+ ``nullable`` arg is ignored: it doesn't affect how the tree of
+ collected objects should be nested for display.
+ """
+ model, pk = type(obj), obj._get_pk_val()
+
+ key = model, pk
+
+ if key in self.seen:
+ return True
+ self.seen.setdefault(key, obj)
+
+ if parent_obj is not None:
+ parent_model, parent_pk = (type(parent_obj),
+ parent_obj._get_pk_val())
+ parent_key = (parent_model, parent_pk)
+ if parent_key in self.seen:
+ self.children.setdefault(parent_key, list()).append(key)
+ self.parents.setdefault(key, parent_key)
+
+ def _nested(self, key, format_callback=None, **kwargs):
+ obj = self.seen[key]
+ if format_callback:
+ ret = [format_callback(obj, **kwargs)]
else:
- has_related_objs = False
- for sub_obj in getattr(obj, rel_opts_name).all():
- has_related_objs = True
- if not has_admin:
- # Don't display link to edit, because it either has no
- # admin or is edited inline.
- nh(deleted_objects, current_depth,
- [u'%s: %s' % (capfirst(related.opts.verbose_name), force_unicode(sub_obj)), []])
- else:
- # Display a link to the admin page.
- nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="%s">%s</a>' %
- (escape(capfirst(related.opts.verbose_name)),
- get_change_view_url(related.opts.app_label,
- related.opts.object_name.lower(),
- sub_obj._get_pk_val(),
- admin_site,
- levels_to_root),
- escape(sub_obj))), []])
- get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site)
- # If there were related objects, and the user doesn't have
- # permission to delete them, add the missing perm to perms_needed.
- if has_admin and has_related_objs:
- p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission())
- if not user.has_perm(p):
- perms_needed.add(related.opts.verbose_name)
- for related in opts.get_all_related_many_to_many_objects():
- has_admin = related.model in admin_site._registry
- if related.opts in opts_seen:
- continue
- opts_seen.append(related.opts)
- rel_opts_name = related.get_accessor_name()
- has_related_objs = False
-
- # related.get_accessor_name() could return None for symmetrical relationships
- if rel_opts_name:
- rel_objs = getattr(obj, rel_opts_name, None)
- if rel_objs:
- has_related_objs = True
-
- if has_related_objs:
- for sub_obj in rel_objs.all():
- if not has_admin:
- # Don't display link to edit, because it either has no
- # admin or is edited inline.
- nh(deleted_objects, current_depth, [_('One or more %(fieldname)s in %(name)s: %(obj)s') % \
- {'fieldname': force_unicode(related.field.verbose_name), 'name': force_unicode(related.opts.verbose_name), 'obj': escape(sub_obj)}, []])
- else:
- # Display a link to the admin page.
- nh(deleted_objects, current_depth, [
- mark_safe((_('One or more %(fieldname)s in %(name)s:') % {'fieldname': escape(force_unicode(related.field.verbose_name)), 'name': escape(force_unicode(related.opts.verbose_name))}) + \
- (u' <a href="%s">%s</a>' % \
- (get_change_view_url(related.opts.app_label,
- related.opts.object_name.lower(),
- sub_obj._get_pk_val(),
- admin_site,
- levels_to_root),
- escape(sub_obj)))), []])
- # If there were related objects, and the user doesn't have
- # permission to change them, add the missing perm to perms_needed.
- if has_admin and has_related_objs:
- p = u'%s.%s' % (related.opts.app_label, related.opts.get_change_permission())
- if not user.has_perm(p):
- perms_needed.add(related.opts.verbose_name)
+ ret = [obj]
+
+ children = []
+ for child in self.children.get(key, ()):
+ children.extend(self._nested(child, format_callback, **kwargs))
+ if children:
+ ret.append(children)
+
+ return ret
+
+ def nested(self, format_callback=None, **kwargs):
+ """
+ Return the graph as a nested list.
+
+ Passes **kwargs back to the format_callback as kwargs.
+
+ """
+ roots = []
+ for key in self.seen.keys():
+ if key not in self.parents:
+ roots.extend(self._nested(key, format_callback, **kwargs))
+ return roots
+
def model_format_dict(obj):
"""
@@ -537,7 +537,8 @@ def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):
(model_class, {pk_val: obj, pk_val: obj, ...}), ...]
"""
pk_val = self._get_pk_val()
- if seen_objs.add(self.__class__, pk_val, self, parent, nullable):
+ if seen_objs.add(self.__class__, pk_val, self,
+ type(parent), parent, nullable):
return
for related in self._meta.get_all_related_objects():
@@ -548,7 +549,7 @@ def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):
except ObjectDoesNotExist:
pass
else:
- sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
+ sub_obj._collect_sub_objects(seen_objs, self, related.field.null)
else:
# To make sure we can access all elements, we can't use the
# normal manager on the related object. So we work directly
@@ -561,7 +562,7 @@ def _collect_sub_objects(self, seen_objs, parent=None, nullable=False):
raise AssertionError("Should never get here.")
delete_qs = rel_descriptor.delete_manager(self).all()
for sub_obj in delete_qs:
- sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
+ sub_obj._collect_sub_objects(seen_objs, self, related.field.null)
# Handle any ancestors (for the model-inheritance case). We do this by
# traversing to the most remote parent classes -- those with no parents
Oops, something went wrong.

0 comments on commit 3bda1f5

Please sign in to comment.