Ticket #20133: patch1.patch

File patch1.patch, 15.0 KB (added by Jonas H., 12 years ago)
  • django/contrib/admin/actions.py

    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):  
    3131
    3232    # Populate deletable_objects, a data structure of all related objects that
    3333    # will also be deleted.
    34     deletable_objects, perms_needed, protected = get_deleted_objects(
     34    deletable_objects, model_count, perms_needed, protected = get_deleted_objects(
    3535        queryset, opts, request.user, modeladmin.admin_site, using)
    3636
    3737    # The user has already confirmed the deletion.
    def delete_selected(modeladmin, request, queryset):  
    6565        "title": title,
    6666        "objects_name": objects_name,
    6767        "deletable_objects": [deletable_objects],
     68        "model_count": dict(model_count),
    6869        'queryset': queryset,
    6970        "perms_lacking": perms_needed,
    7071        "protected": protected,
  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index f7bfca4..b4132aa 100644
    a b class ModelAdmin(BaseModelAdmin):  
    13361336
    13371337        # Populate deleted_objects, a data structure of all related objects that
    13381338        # will also be deleted.
    1339         (deleted_objects, perms_needed, protected) = get_deleted_objects(
     1339        (deleted_objects, model_count, perms_needed, protected) = get_deleted_objects(
    13401340            [obj], opts, request.user, self.admin_site, using)
    13411341
    13421342        if request.POST: # The user has already confirmed the deletion.
    class ModelAdmin(BaseModelAdmin):  
    13671367            "object_name": object_name,
    13681368            "object": obj,
    13691369            "deleted_objects": deleted_objects,
     1370            "model_count": dict(model_count),
    13701371            "perms_lacking": perms_needed,
    13711372            "protected": protected,
    13721373            "opts": opts,
  • django/contrib/admin/templates/admin/delete_confirmation.html

    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  
    1313{% endblock %}
    1414
    1515{% 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>
    3330{% else %}
    3431    <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>
    3539    <ul>{{ deleted_objects|unordered_list }}</ul>
    3640    <form action="" method="post">{% csrf_token %}
    3741    <div>
  • django/contrib/admin/templates/admin/delete_selected_confirmation.html

    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  
    1212{% endblock %}
    1313
    1414{% 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>
    3230{% else %}
    3331    <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>
    3439    {% for deletable_object in deletable_objects %}
    3540        <ul>{{ deletable_object|unordered_list }}</ul>
    3641    {% endfor %}
  • django/contrib/admin/util.py

    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  
    22
    33import datetime
    44import decimal
     5from collections import defaultdict
    56
    67from django.db import models
    78from django.db.models.constants import LOOKUP_SEP
    def flatten_fieldsets(fieldsets):  
    9596    return field_names
    9697
    9798
     99def 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
    98106def get_deleted_objects(objs, opts, user, admin_site, using):
    99107    """
    100108    Find all objects related to ``objs`` that should also be deleted. ``objs``
    101109    must be a homogenous iterable of objects (e.g. a QuerySet).
    102110
    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
    105112
     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.
    106121    """
    107     collector = NestedObjects(using=using)
     122    collector = NestedObjects(user, using=using)
    108123    collector.collect(objs)
    109     perms_needed = set()
     124
     125    if collector.perms_needed:
     126        return [], {}, collector.perms_needed, []
    110127
    111128    def format_callback(obj):
    112129        has_admin = obj.__class__ in admin_site._registry
    113130        opts = obj._meta
    114131
    115132        if has_admin:
     133            # Display a link to the admin page.
    116134            admin_url = reverse('%s:%s_%s_change'
    117135                                % (admin_site.name,
    118136                                   opts.app_label,
    119137                                   opts.model_name),
    120138                                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.
    126139            return format_html('{0}: <a href="{1}">{2}</a>',
    127140                               capfirst(opts.verbose_name),
    128141                               admin_url,
    def get_deleted_objects(objs, opts, user, admin_site, using):  
    133146            return '%s: %s' % (capfirst(opts.verbose_name),
    134147                                force_text(obj))
    135148
    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)
    139151
    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, [], []
    141154
    142155
    143156class NestedObjects(Collector):
    144     def __init__(self, *args, **kwargs):
     157    def __init__(self, user, *args, **kwargs):
    145158        super(NestedObjects, self).__init__(*args, **kwargs)
     159        self.user = user
    146160        self.edges = {} # {from_instance: [to_instances]}
    147161        self.protected = set()
     162        self.perms_needed = set()
     163        self.model_count = defaultdict(int)
    148164
    149165    def add_edge(self, source, target):
    150166        self.edges.setdefault(source, []).append(target)
    151167
    152168    def collect(self, objs, source_attr=None, **kwargs):
    153169        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
    154176            if source_attr:
    155177                self.add_edge(getattr(obj, source_attr), obj)
    156178            else:
    157179                if obj._meta.proxy:
    158180                    # 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)
    160182                self.add_edge(None, obj)
     183
     184            self.model_count[opts.verbose_name] += 1
     185
    161186        try:
    162187            return super(NestedObjects, self).collect(objs, source_attr=source_attr, **kwargs)
    163188        except models.ProtectedError as e:
    class NestedObjects(Collector):  
    167192        qs = super(NestedObjects, self).related_objects(related, objs)
    168193        return qs.select_related(related.field.name)
    169194
    170     def _nested(self, obj, seen, format_callback):
     195    def _as_nested_list(self, obj, seen):
    171196        if obj in seen:
    172197            return []
    173198        seen.add(obj)
    174199        children = []
    175200        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))
    181202        if children:
    182             ret.append(children)
    183         return ret
     203            return [obj, children]
     204        else:
     205            return [obj]
    184206
    185     def nested(self, format_callback=None):
     207    def as_nested_list(self):
    186208        """
    187209        Return the graph as a nested list.
    188 
    189210        """
    190211        seen = set()
    191212        roots = []
    192213        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))
    194215        return roots
    195216
    196217    def can_fast_delete(self, *args, **kwargs):
  • tests/admin_util/tests.py

    diff --git a/tests/admin_util/tests.py b/tests/admin_util/tests.py
    index 7898f20..7e7037a 100644
    a b from datetime import datetime  
    44
    55from django.conf import settings
    66from django.contrib import admin
     7from django.contrib.auth.models import User
    78from django.contrib.admin import helpers
    89from 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)
    1011from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
    1112from django.contrib.sites.models import Site
    1213from django.db import models, DEFAULT_DB_ALIAS
    class NestedObjectsTests(TestCase):  
    2627
    2728    """
    2829    def setUp(self):
    29         self.n = NestedObjects(using=DEFAULT_DB_ALIAS)
     30        self.n = NestedObjects(User.objects.create(), using=DEFAULT_DB_ALIAS)
    3031        self.objs = [Count.objects.create(num=i) for i in range(5)]
    3132
    3233    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        )
    3438
    3539    def _connect(self, i, j):
    3640        self.objs[i].parent = self.objs[j]
    class NestedObjectsTests(TestCase):  
    6872        self._connect(2, 0)
    6973        # 1 query to fetch all children of 0 (1 and 2)
    7074        # 1 query to fetch all children of 1 and 2 (none)
     75        # 2 queries to check for permissions of 2 objects
    7176        # Should not require additional queries to populate the nested graph.
    72         self.assertNumQueries(2, self._collect, 0)
     77        self.assertNumQueries(4, self._collect, 0)
    7378
    7479    def test_on_delete_do_nothing(self):
    7580        """
    7681        Check that the nested collector doesn't query for DO_NOTHING objects.
    7782        """
    78         n = NestedObjects(using=DEFAULT_DB_ALIAS)
    7983        objs = [Event.objects.create()]
    8084        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)
    8489
    8590class UtilTests(unittest.TestCase):
    8691    def test_values_from_lookup_field(self):
Back to Top