Commit 3bda1f5b authored by Russell Keith-Magee's avatar Russell Keith-Magee
Browse files

[1.1.X] Fixed #6191, #11296 -- Modified the admin deletion confirmation page...

[1.1.X] Fixed #6191, #11296 -- Modified the admin deletion confirmation 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
parent a52f1dd9
Loading
Loading
Loading
Loading
+2 −10
Original line number Diff line number Diff line
@@ -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,
+1 −3
Original line number Diff line number Diff line
@@ -1014,9 +1014,7 @@ class ModelAdmin(BaseModelAdmin):

        # 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:
+2 −2
Original line number Diff line number Diff line
@@ -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>
+164 −118
Original line number Diff line number Diff line
@@ -7,6 +7,8 @@ from django.utils.encoding import force_unicode
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.

    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.
    `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)
        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)}, []])
    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:
                    # 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):
    """
+4 −3
Original line number Diff line number Diff line
@@ -537,7 +537,8 @@ class Model(object):
             (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 @@ class Model(object):
                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 @@ class Model(object):
                    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
Loading