Ticket #6191: 6191_r12578_incorporated_minor_review_items.diff

File 6191_r12578_incorporated_minor_review_items.diff, 34.3 KB (added by Carl Meyer, 14 years ago)

incorporates all of Russ' suggestions except for merging/API changes for NestedObjects? and CollectedObjects? - also removes unneeded parens to make Alex Gaynor happy

  • django/contrib/admin/actions.py

    diff --git a/django/contrib/admin/actions.py b/django/contrib/admin/actions.py
    a b  
    3636
    3737    # Populate deletable_objects, a data structure of all related objects that
    3838    # will also be deleted.
    39 
    40     # deletable_objects must be a list if we want to use '|unordered_list' in the template
    41     deletable_objects = []
    42     perms_needed = set()
    43     i = 0
    44     for obj in queryset:
    45         deletable_objects.append([mark_safe(u'%s: <a href="%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), obj.pk, escape(obj))), []])
    46         get_deleted_objects(deletable_objects[i], perms_needed, request.user, obj, opts, 1, modeladmin.admin_site, levels_to_root=2)
    47         i=i+1
     39    deletable_objects, perms_needed = get_deleted_objects(queryset, opts, request.user, modeladmin.admin_site, levels_to_root=2)
    4840
    4941    # The user has already confirmed the deletion.
    5042    # Do the deletion and return a None to display the change list view again.
     
    6658    context = {
    6759        "title": _("Are you sure?"),
    6860        "object_name": force_unicode(opts.verbose_name),
    69         "deletable_objects": deletable_objects,
     61        "deletable_objects": [deletable_objects],
    7062        'queryset': queryset,
    7163        "perms_lacking": perms_needed,
    7264        "opts": opts,
  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    a b  
    10801080
    10811081        # Populate deleted_objects, a data structure of all related objects that
    10821082        # will also be deleted.
    1083         deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), object_id, escape(obj))), []]
    1084         perms_needed = set()
    1085         get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site)
     1083        (deleted_objects, perms_needed) = get_deleted_objects((obj,), opts, request.user, self.admin_site)
    10861084
    10871085        if request.POST: # The user has already confirmed the deletion.
    10881086            if perms_needed:
  • 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
    a b  
    2020    </ul>
    2121{% else %}
    2222    <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>
    23     {% for deleteable_object in deletable_objects %}
    24         <ul>{{ deleteable_object|unordered_list }}</ul>
     23    {% for deletable_object in deletable_objects %}
     24        <ul>{{ deletable_object|unordered_list }}</ul>
    2525    {% endfor %}
    2626    <form action="" method="post">{% csrf_token %}
    2727    <div>
  • django/contrib/admin/util.py

    diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py
    a b  
    77from django.utils.encoding import force_unicode, smart_unicode, smart_str
    88from django.utils.translation import ungettext, ugettext as _
    99from django.core.urlresolvers import reverse, NoReverseMatch
    10 
     10from django.utils.datastructures import SortedDict
    1111
    1212def quote(s):
    1313    """
     
    5757                field_names.append(field)
    5858    return field_names
    5959
    60 def _nest_help(obj, depth, val):
    61     current = obj
    62     for i in range(depth):
    63         current = current[-1]
    64     current.append(val)
     60def _format_callback(obj, user, admin_site, levels_to_root, perms_needed):
     61    has_admin = obj.__class__ in admin_site._registry
     62    opts = obj._meta
     63    try:
     64        admin_url = reverse('%s:%s_%s_change'
     65                            % (admin_site.name,
     66                               opts.app_label,
     67                               opts.object_name.lower()),
     68                            None, (quote(obj._get_pk_val()),))
     69    except NoReverseMatch:
     70        admin_url = '%s%s/%s/%s/' % ('../'*levels_to_root,
     71                                     opts.app_label,
     72                                     opts.object_name.lower(),
     73                                     quote(obj._get_pk_val()))
     74    if has_admin:
     75        p = '%s.%s' % (opts.app_label,
     76                       opts.get_delete_permission())
     77        if not user.has_perm(p):
     78            perms_needed.add(opts.verbose_name)
     79        # Display a link to the admin page.
     80        return mark_safe(u'%s: <a href="%s">%s</a>' %
     81                         (escape(capfirst(opts.verbose_name)),
     82                          admin_url,
     83                          escape(obj)))
     84    else:
     85        # Don't display link to edit, because it either has no
     86        # admin or is edited inline.
     87        return u'%s: %s' % (capfirst(opts.verbose_name),
     88                            force_unicode(obj))
    6589
    66 def get_change_view_url(app_label, module_name, pk, admin_site, levels_to_root):
     90def get_deleted_objects(objs, opts, user, admin_site, levels_to_root=4):
    6791    """
    68     Returns the url to the admin change view for the given app_label,
    69     module_name and primary key.
    70     """
    71     try:
    72         return reverse('%sadmin_%s_%s_change' % (admin_site.name, app_label, module_name), None, (pk,))
    73     except NoReverseMatch:
    74         return '%s%s/%s/%s/' % ('../'*levels_to_root, app_label, module_name, pk)
     92    Find all objects related to ``objs`` that should also be
     93    deleted. ``objs`` should be an iterable of objects.
    7594
    76 def get_deleted_objects(deleted_objects, perms_needed, user, obj, opts, current_depth, admin_site, levels_to_root=4):
    77     """
    78     Helper function that recursively populates deleted_objects.
     95    Returns a nested list of strings suitable for display in the
     96    template with the ``unordered_list`` filter.
    7997
    80     `levels_to_root` defines the number of directories (../) to reach the
    81     admin root path. In a change_view this is 4, in a change_list view 2.
     98    `levels_to_root` defines the number of directories (../) to reach
     99    the admin root path. In a change_view this is 4, in a change_list
     100    view 2.
    82101
    83102    This is for backwards compatibility since the options.delete_selected
    84103    method uses this function also from a change_list view.
    85104    This will not be used if we can reverse the URL.
    86105    """
    87     nh = _nest_help # Bind to local variable for performance
    88     if current_depth > 16:
    89         return # Avoid recursing too deep.
    90     opts_seen = []
    91     for related in opts.get_all_related_objects():
    92         has_admin = related.model in admin_site._registry
    93         if related.opts in opts_seen:
    94             continue
    95         opts_seen.append(related.opts)
    96         rel_opts_name = related.get_accessor_name()
    97         if isinstance(related.field.rel, models.OneToOneRel):
    98             try:
    99                 sub_obj = getattr(obj, rel_opts_name)
    100             except ObjectDoesNotExist:
    101                 pass
    102             else:
    103                 if has_admin:
    104                     p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission())
    105                     if not user.has_perm(p):
    106                         perms_needed.add(related.opts.verbose_name)
    107                         # We don't care about populating deleted_objects now.
    108                         continue
    109                 if not has_admin:
    110                     # Don't display link to edit, because it either has no
    111                     # admin or is edited inline.
    112                     nh(deleted_objects, current_depth,
    113                         [u'%s: %s' % (capfirst(related.opts.verbose_name), force_unicode(sub_obj)), []])
    114                 else:
    115                     # Display a link to the admin page.
    116                     nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="%s">%s</a>' %
    117                         (escape(capfirst(related.opts.verbose_name)),
    118                         get_change_view_url(related.opts.app_label,
    119                                             related.opts.object_name.lower(),
    120                                             sub_obj._get_pk_val(),
    121                                             admin_site,
    122                                             levels_to_root),
    123                         escape(sub_obj))), []])
    124                 get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site)
     106    collector = NestedObjects()
     107    for obj in objs:
     108        # TODO using a private model API!
     109        obj._collect_sub_objects(collector)
     110
     111    # TODO this is needed only because GenericRelations are
     112    # cascaded-deleted way down in the internals in
     113    # DeleteQuery.delete_batch_related, instead of being found by
     114    # _collect_sub_objects.
     115    from django.contrib.contenttypes import generic
     116    for f in obj._meta.many_to_many:
     117        if isinstance(f, generic.GenericRelation):
     118            rel_manager = f.value_from_object(obj)
     119            for related in rel_manager.all():
     120                # There's a wierdness here in the case that the
     121                # generic-related object also has FKs pointing to it
     122                # from elsewhere. DeleteQuery does not follow those
     123                # FKs or delete any such objects explicitly (which is
     124                # probably a bug). Some databases may cascade those
     125                # deletes themselves, and some won't. So do we report
     126                # those objects as to-be-deleted? No right answer; for
     127                # now we opt to report only on objects that Django
     128                # will explicitly delete, at risk that some further
     129                # objects will be silently deleted by a
     130                # referential-integrity-maintaining database.
     131                collector.add(related.__class__, related.pk, related,
     132                              obj.__class__, obj)
     133
     134    perms_needed = set()
     135
     136    to_delete = collector.nested(_format_callback,
     137                                 user=user,
     138                                 admin_site=admin_site,
     139                                 levels_to_root=levels_to_root,
     140                                 perms_needed=perms_needed)
     141
     142    return to_delete, perms_needed
     143
     144
     145class NestedObjects(object):
     146    """
     147    A directed acyclic graph collection that exposes the add() API
     148    expected by Model._collect_sub_objects and can present its data as
     149    a nested list of objects.
     150
     151    """
     152    def __init__(self):
     153        # Use object keys of the form (model, pk) because actual model
     154        # objects may not be unique
     155
     156        # maps object key to list of child keys
     157        self.children = SortedDict()
     158
     159        # maps object key to parent key
     160        self.parents = SortedDict()
     161
     162        # maps object key to actual object
     163        self.seen = SortedDict()
     164
     165    def add(self, model, pk, obj,
     166            parent_model=None, parent_obj=None, nullable=False):
     167        """
     168        Add item ``obj`` to the graph. Returns True (and does nothing)
     169        if the item has been seen already.
     170
     171        The ``parent_obj`` argument must already exist in the graph; if
     172        not, it's ignored (but ``obj`` is still added with no
     173        parent). In any case, Model._collect_sub_objects (for whom
     174        this API exists) will never pass a parent that hasn't already
     175        been added itself.
     176
     177        These restrictions in combination ensure the graph will remain
     178        acyclic (but can have multiple roots).
     179
     180        ``model``, ``pk``, and ``parent_model`` arguments are ignored
     181        in favor of the appropriate lookups on ``obj`` and
     182        ``parent_obj``; unlike CollectedObjects, we can't maintain
     183        independence from the knowledge that we're operating on model
     184        instances, and we don't want to allow for inconsistency.
     185
     186        ``nullable`` arg is ignored: it doesn't affect how the tree of
     187        collected objects should be nested for display.
     188        """
     189        model, pk = type(obj), obj._get_pk_val()
     190
     191        key = model, pk
     192
     193        if key in self.seen:
     194            return True
     195        self.seen.setdefault(key, obj)
     196
     197        if parent_obj is not None:
     198            parent_model, parent_pk = (type(parent_obj),
     199                                       parent_obj._get_pk_val())
     200            parent_key = (parent_model, parent_pk)
     201            if parent_key in self.seen:
     202                self.children.setdefault(parent_key, list()).append(key)
     203                self.parents.setdefault(key, parent_key)
     204
     205    def _nested(self, key, format_callback=None, **kwargs):
     206        obj = self.seen[key]
     207        if format_callback:
     208            ret = [format_callback(obj, **kwargs)]
    125209        else:
    126             has_related_objs = False
    127             for sub_obj in getattr(obj, rel_opts_name).all():
    128                 has_related_objs = True
    129                 if not has_admin:
    130                     # Don't display link to edit, because it either has no
    131                     # admin or is edited inline.
    132                     nh(deleted_objects, current_depth,
    133                         [u'%s: %s' % (capfirst(related.opts.verbose_name), force_unicode(sub_obj)), []])
    134                 else:
    135                     # Display a link to the admin page.
    136                     nh(deleted_objects, current_depth, [mark_safe(u'%s: <a href="%s">%s</a>' %
    137                         (escape(capfirst(related.opts.verbose_name)),
    138                         get_change_view_url(related.opts.app_label,
    139                                             related.opts.object_name.lower(),
    140                                             sub_obj._get_pk_val(),
    141                                             admin_site,
    142                                             levels_to_root),
    143                         escape(sub_obj))), []])
    144                 get_deleted_objects(deleted_objects, perms_needed, user, sub_obj, related.opts, current_depth+2, admin_site)
    145             # If there were related objects, and the user doesn't have
    146             # permission to delete them, add the missing perm to perms_needed.
    147             if has_admin and has_related_objs:
    148                 p = '%s.%s' % (related.opts.app_label, related.opts.get_delete_permission())
    149                 if not user.has_perm(p):
    150                     perms_needed.add(related.opts.verbose_name)
    151     for related in opts.get_all_related_many_to_many_objects():
    152         has_admin = related.model in admin_site._registry
    153         if related.opts in opts_seen:
    154             continue
    155         opts_seen.append(related.opts)
    156         rel_opts_name = related.get_accessor_name()
    157         has_related_objs = False
     210            ret = [obj]
    158211
    159         # related.get_accessor_name() could return None for symmetrical relationships
    160         if rel_opts_name:
    161             rel_objs = getattr(obj, rel_opts_name, None)
    162             if rel_objs:
    163                 has_related_objs = True
     212        children = []
     213        for child in self.children.get(key, ()):
     214            children.extend(self._nested(child, format_callback, **kwargs))
     215        if children:
     216            ret.append(children)
    164217
    165         if has_related_objs:
    166             for sub_obj in rel_objs.all():
    167                 if not has_admin:
    168                     # Don't display link to edit, because it either has no
    169                     # admin or is edited inline.
    170                     nh(deleted_objects, current_depth, [_('One or more %(fieldname)s in %(name)s: %(obj)s') % \
    171                         {'fieldname': force_unicode(related.field.verbose_name), 'name': force_unicode(related.opts.verbose_name), 'obj': escape(sub_obj)}, []])
    172                 else:
    173                     # Display a link to the admin page.
    174                     nh(deleted_objects, current_depth, [
    175                         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))}) + \
    176                         (u' <a href="%s">%s</a>' % \
    177                             (get_change_view_url(related.opts.app_label,
    178                                                  related.opts.object_name.lower(),
    179                                                  sub_obj._get_pk_val(),
    180                                                  admin_site,
    181                                                  levels_to_root),
    182                             escape(sub_obj)))), []])
    183         # If there were related objects, and the user doesn't have
    184         # permission to change them, add the missing perm to perms_needed.
    185         if has_admin and has_related_objs:
    186             p = u'%s.%s' % (related.opts.app_label, related.opts.get_change_permission())
    187             if not user.has_perm(p):
    188                 perms_needed.add(related.opts.verbose_name)
     218        return ret
     219
     220    def nested(self, format_callback=None, **kwargs):
     221        """
     222        Return the graph as a nested list.
     223
     224        Passes **kwargs back to the format_callback as kwargs.
     225
     226        """
     227        roots = []
     228        for key in self.seen.keys():
     229            if key not in self.parents:
     230                roots.extend(self._nested(key, format_callback, **kwargs))
     231        return roots
     232
    189233
    190234def model_format_dict(obj):
    191235    """
  • django/db/models/base.py

    diff --git a/django/db/models/base.py b/django/db/models/base.py
    a b  
    549549             (model_class, {pk_val: obj, pk_val: obj, ...}), ...]
    550550        """
    551551        pk_val = self._get_pk_val()
    552         if seen_objs.add(self.__class__, pk_val, self, parent, nullable):
     552        if seen_objs.add(self.__class__, pk_val, self,
     553                         type(parent), parent, nullable):
    553554            return
    554555
    555556        for related in self._meta.get_all_related_objects():
     
    560561                except ObjectDoesNotExist:
    561562                    pass
    562563                else:
    563                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
     564                    sub_obj._collect_sub_objects(seen_objs, self, related.field.null)
    564565            else:
    565566                # To make sure we can access all elements, we can't use the
    566567                # normal manager on the related object. So we work directly
     
    578579                        continue
    579580                delete_qs = rel_descriptor.delete_manager(self).all()
    580581                for sub_obj in delete_qs:
    581                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
     582                    sub_obj._collect_sub_objects(seen_objs, self, related.field.null)
    582583
    583584        # Handle any ancestors (for the model-inheritance case). We do this by
    584585        # traversing to the most remote parent classes -- those with no parents
  • django/db/models/query_utils.py

    diff --git a/django/db/models/query_utils.py b/django/db/models/query_utils.py
    a b  
    5050        else:
    5151            self.blocked = {}
    5252
    53     def add(self, model, pk, obj, parent_model, nullable=False):
     53    def add(self, model, pk, obj, parent_model, parent_obj=None, nullable=False):
    5454        """
    5555        Adds an item to the container.
    5656
     
    6060        * obj - the object itself.
    6161        * parent_model - the model of the parent object that this object was
    6262          reached through.
     63        * parent_obj - the parent object this object was reached
     64          through (not used here, but needed in the API for use elsewhere)
    6365        * nullable - should be True if this relation is nullable.
    6466
    6567        Returns True if the item already existed in the structure and
  • tests/regressiontests/admin_util/models.py

    diff --git a/tests/regressiontests/admin_util/models.py b/tests/regressiontests/admin_util/models.py
    a b  
    1717    def test_from_model_with_override(self):
    1818        return "nothing"
    1919    test_from_model_with_override.short_description = "not what you expect"
     20
     21class Count(models.Model):
     22    num = models.PositiveSmallIntegerField()
  • tests/regressiontests/admin_util/tests.py

    diff --git a/tests/regressiontests/admin_util/tests.py b/tests/regressiontests/admin_util/tests.py
    a b  
    22import unittest
    33
    44from django.db import models
     5from django.utils.formats import localize
     6from django.test import TestCase
    57
    68from django.contrib import admin
    79from django.contrib.admin.util import display_for_field, label_for_field, lookup_field
    810from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
    911from django.contrib.sites.models import Site
    10 from django.utils.formats import localize
     12from django.contrib.admin.util import NestedObjects
    1113
    12 from models import Article
     14from models import Article, Count
    1315
    1416
     17class NestedObjectsTests(TestCase):
     18    """
     19    Tests for ``NestedObject`` utility collection.
     20
     21    """
     22    def setUp(self):
     23        self.n = NestedObjects()
     24        self.objs = [Count.objects.create(num=i) for i in range(5)]
     25
     26    def _check(self, target):
     27        self.assertEquals(self.n.nested(lambda obj: obj.num), target)
     28
     29    def _add(self, obj, parent=None):
     30        # don't bother providing the extra args that NestedObjects ignores
     31        self.n.add(None, None, obj, None, parent)
     32
     33    def test_unrelated_roots(self):
     34        self._add(self.objs[0])
     35        self._add(self.objs[1])
     36        self._add(self.objs[2], self.objs[1])
     37
     38        self._check([0, 1, [2]])
     39
     40    def test_siblings(self):
     41        self._add(self.objs[0])
     42        self._add(self.objs[1], self.objs[0])
     43        self._add(self.objs[2], self.objs[0])
     44
     45        self._check([0, [1, 2]])
     46
     47    def test_duplicate_instances(self):
     48        self._add(self.objs[0])
     49        self._add(self.objs[1])
     50        dupe = Count.objects.get(num=1)
     51        self._add(dupe, self.objs[0])
     52
     53        self._check([0, 1])
     54
     55    def test_non_added_parent(self):
     56        self._add(self.objs[0], self.objs[1])
     57
     58        self._check([0])
     59
     60    def test_cyclic(self):
     61        self._add(self.objs[0], self.objs[2])
     62        self._add(self.objs[1], self.objs[0])
     63        self._add(self.objs[2], self.objs[1])
     64        self._add(self.objs[0], self.objs[2])
     65
     66        self._check([0, [1, [2]]])
     67
    1568
    1669class UtilTests(unittest.TestCase):
    1770    def test_values_from_lookup_field(self):
  • new file tests/regressiontests/admin_views/fixtures/deleted-objects.xml

    diff --git a/tests/regressiontests/admin_views/fixtures/deleted-objects.xml b/tests/regressiontests/admin_views/fixtures/deleted-objects.xml
    new file mode 100644
    - +  
     1<?xml version="1.0" encoding="utf-8"?>
     2<django-objects version="1.0">
     3    <object pk="1" model="admin_views.villain">
     4        <field type="CharField" name="name">Adam</field>
     5    </object>
     6    <object pk="2" model="admin_views.villain">
     7        <field type="CharField" name="name">Sue</field>
     8    </object>
     9    <object pk="3" model="admin_views.villain">
     10        <field type="CharField" name="name">Bob</field>
     11    </object>
     12    <object pk="3" model="admin_views.supervillain">
     13    </object>
     14    <object pk="1" model="admin_views.plot">
     15        <field type="CharField" name="name">World Domination</field>
     16        <field type="ForeignKey" name="team_leader">1</field>
     17        <field type="ForeignKey" name="contact">2</field>
     18    </object>
     19    <object pk="2" model="admin_views.plot">
     20        <field type="CharField" name="name">World Peace</field>
     21        <field type="ForeignKey" name="team_leader">2</field>
     22        <field type="ForeignKey" name="contact">2</field>
     23    </object>
     24    <object pk="1" model="admin_views.plotdetails">
     25        <field type="CharField" name="details">almost finished</field>
     26        <field type="ForeignKey" name="plot">1</field>
     27    </object>
     28    <object pk="1" model="admin_views.secrethideout">
     29        <field type="CharField" name="location">underground bunker</field>
     30        <field type="ForeignKey" name="villain">1</field>
     31    </object>
     32    <object pk="2" model="admin_views.secrethideout">
     33        <field type="CharField" name="location">floating castle</field>
     34        <field type="ForeignKey" name="villain">3</field>
     35    </object>
     36    <object pk="1" model="admin_views.supersecrethideout">
     37        <field type="CharField" name="location">super floating castle!</field>
     38        <field type="ForeignKey" name="supervillain">3</field>
     39    </object>
     40    <object pk="1" model="admin_views.cyclicone">
     41        <field type="CharField" name="name">I am recursive</field>
     42        <field type="ForeignKey" name="two">1</field>
     43    </object>
     44    <object pk="1" model="admin_views.cyclictwo">
     45        <field type="CharField" name="name">I am recursive too</field>
     46        <field type="ForeignKey" name="one">1</field>
     47    </object>
     48    <object pk="3" model="admin_views.plot">
     49        <field type="CharField" name="name">Corn Conspiracy</field>
     50        <field type="ForeignKey" name="team_leader">1</field>
     51        <field type="ForeignKey" name="contact">1</field>
     52    </object>
     53</django-objects>
  • tests/regressiontests/admin_views/models.py

    diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py
    a b  
    1010from django.db import models
    1111from django import forms
    1212from django.forms.models import BaseModelFormSet
    13 
     13from django.contrib.contenttypes import generic
     14from django.contrib.contenttypes.models import ContentType
    1415
    1516class Section(models.Model):
    1617    """
     
    494495    def get_changelist(self, request, **kwargs):
    495496        return CustomChangeList
    496497
     498class Villain(models.Model):
     499    name = models.CharField(max_length=100)
     500
     501    def __unicode__(self):
     502        return self.name
     503
     504class SuperVillain(Villain):
     505    pass
     506   
     507class FunkyTag(models.Model):
     508    "Because we all know there's only one real use case for GFKs."
     509    name = models.CharField(max_length=25)
     510    content_type = models.ForeignKey(ContentType)
     511    object_id = models.PositiveIntegerField()
     512    content_object = generic.GenericForeignKey('content_type', 'object_id')
     513
     514    def __unicode__(self):
     515        return self.name
     516
     517class Plot(models.Model):
     518    name = models.CharField(max_length=100)
     519    team_leader = models.ForeignKey(Villain, related_name='lead_plots')
     520    contact = models.ForeignKey(Villain, related_name='contact_plots')
     521    tags = generic.GenericRelation(FunkyTag)
     522
     523    def __unicode__(self):
     524        return self.name
     525
     526class PlotDetails(models.Model):
     527    details = models.CharField(max_length=100)
     528    plot = models.OneToOneField(Plot)
     529
     530    def __unicode__(self):
     531        return self.details
     532
     533class SecretHideout(models.Model):
     534    """ Secret! Not registered with the admin! """
     535    location = models.CharField(max_length=100)
     536    villain = models.ForeignKey(Villain)
     537
     538    def __unicode__(self):
     539        return self.location
     540
     541class SuperSecretHideout(models.Model):
     542    """ Secret! Not registered with the admin! """
     543    location = models.CharField(max_length=100)
     544    supervillain = models.ForeignKey(SuperVillain)
     545
     546    def __unicode__(self):
     547        return self.location
     548
     549class CyclicOne(models.Model):
     550    name = models.CharField(max_length=25)
     551    two = models.ForeignKey('CyclicTwo')
     552
     553    def __unicode__(self):
     554        return self.name
     555
     556class CyclicTwo(models.Model):
     557    name = models.CharField(max_length=25)
     558    one = models.ForeignKey(CyclicOne)
     559
     560    def __unicode__(self):
     561        return self.name
     562
    497563admin.site.register(Article, ArticleAdmin)
    498564admin.site.register(CustomArticle, CustomArticleAdmin)
    499565admin.site.register(Section, save_as=True, inlines=[ArticleInline])
     
    519585admin.site.register(Category, CategoryAdmin)
    520586admin.site.register(Post, PostAdmin)
    521587admin.site.register(Gadget, GadgetAdmin)
     588admin.site.register(Villain)
     589admin.site.register(SuperVillain)
     590admin.site.register(Plot)
     591admin.site.register(PlotDetails)
     592admin.site.register(CyclicOne)
     593admin.site.register(CyclicTwo)
    522594
    523595# We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
    524596# That way we cover all four cases:
  • tests/regressiontests/admin_views/tests.py

    diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py
    a b  
    1616from django.utils.cache import get_max_age
    1717from django.utils.html import escape
    1818from django.utils.translation import get_date_formats
     19from django.utils.encoding import iri_to_uri
    1920
    2021# local test models
    2122from models import Article, BarAccount, CustomArticle, EmptyModel, \
    2223    ExternalSubscriber, FooAccount, Gallery, ModelWithStringPrimaryKey, \
    2324    Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast, \
    2425    Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit, \
    25     Category, Post
     26    Category, Post, Plot, FunkyTag
    2627
    2728
    2829class AdminViewBasicTest(TestCase):
     
    637638        response = self.client.get('/test_admin/admin/secure-view/')
    638639        self.assertContains(response, 'id="login-form"')
    639640
     641
     642class AdminViewDeletedObjectsTest(TestCase):
     643    fixtures = ['admin-views-users.xml', 'deleted-objects.xml']
     644
     645    def setUp(self):
     646        self.client.login(username='super', password='secret')
     647
     648    def tearDown(self):
     649        self.client.logout()
     650
     651    def test_nesting(self):
     652        """
     653        Objects should be nested to display the relationships that
     654        cause them to be scheduled for deletion.
     655        """
     656        pattern = re.compile(r"""<li>Plot: <a href=".+/admin_views/plot/1/">World Domination</a>\s*<ul>\s*<li>Plot details: <a href=".+/admin_views/plotdetails/1/">almost finished</a>""")
     657        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1))
     658        self.failUnless(pattern.search(response.content))
     659
     660    def test_cyclic(self):
     661        """
     662        Cyclic relationships should still cause each object to only be
     663        listed once.
     664
     665        """
     666        one = """<li>Cyclic one: <a href="/test_admin/admin/admin_views/cyclicone/1/">I am recursive</a>"""
     667        two = """<li>Cyclic two: <a href="/test_admin/admin/admin_views/cyclictwo/1/">I am recursive too</a>"""
     668        response = self.client.get('/test_admin/admin/admin_views/cyclicone/%s/delete/' % quote(1))
     669
     670        self.assertContains(response, one, 1)
     671        self.assertContains(response, two, 1)
     672
     673    def test_perms_needed(self):
     674        self.client.logout()
     675        delete_user = User.objects.get(username='deleteuser')
     676        delete_user.user_permissions.add(get_perm(Plot,
     677            Plot._meta.get_delete_permission()))
     678
     679        self.failUnless(self.client.login(username='deleteuser',
     680                                          password='secret'))
     681
     682        response = self.client.get('/test_admin/admin/admin_views/plot/%s/delete/' % quote(1))
     683        self.assertContains(response, "your account doesn't have permission to delete the following types of objects")
     684        self.assertContains(response, "<li>plot details</li>")
     685
     686
     687    def test_not_registered(self):
     688        should_contain = """<li>Secret hideout: underground bunker"""
     689        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1))
     690        self.assertContains(response, should_contain, 1)
     691
     692    def test_multiple_fkeys_to_same_model(self):
     693        """
     694        If a deleted object has two relationships from another model,
     695        both of those should be followed in looking for related
     696        objects to delete.
     697
     698        """
     699        should_contain = """<li>Plot: <a href="/test_admin/admin/admin_views/plot/1/">World Domination</a>"""
     700        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1))
     701        self.assertContains(response, should_contain)
     702        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(2))
     703        self.assertContains(response, should_contain)
     704
     705    def test_multiple_fkeys_to_same_instance(self):
     706        """
     707        If a deleted object has two relationships pointing to it from
     708        another object, the other object should still only be listed
     709        once.
     710
     711        """
     712        should_contain = """<li>Plot: <a href="/test_admin/admin/admin_views/plot/2/">World Peace</a></li>"""
     713        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(2))
     714        self.assertContains(response, should_contain, 1)
     715
     716    def test_inheritance(self):
     717        """
     718        In the case of an inherited model, if either the child or
     719        parent-model instance is deleted, both instances are listed
     720        for deletion, as well as any relationships they have.
     721       
     722        """
     723        should_contain = [
     724            """<li>Villain: <a href="/test_admin/admin/admin_views/villain/3/">Bob</a>""",
     725            """<li>Super villain: <a href="/test_admin/admin/admin_views/supervillain/3/">Bob</a>""",
     726            """<li>Secret hideout: floating castle""",
     727            """<li>Super secret hideout: super floating castle!"""
     728            ]
     729        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(3))
     730        for should in should_contain:
     731            self.assertContains(response, should, 1)
     732        response = self.client.get('/test_admin/admin/admin_views/supervillain/%s/delete/' % quote(3))
     733        for should in should_contain:
     734            self.assertContains(response, should, 1)
     735       
     736    def test_generic_relations(self):
     737        """
     738        If a deleted object has GenericForeignKeys pointing to it,
     739        those objects should be listed for deletion.
     740
     741        """
     742        plot = Plot.objects.get(pk=3)
     743        tag = FunkyTag.objects.create(content_object=plot, name='hott')
     744        should_contain = """<li>Funky tag: hott"""
     745        response = self.client.get('/test_admin/admin/admin_views/plot/%s/delete/' % quote(3))
     746        self.assertContains(response, should_contain)
     747
    640748class AdminViewStringPrimaryKeyTest(TestCase):
    641749    fixtures = ['admin-views-users.xml', 'string-primary-key.xml']
    642750
     
    699807    def test_deleteconfirmation_link(self):
    700808        "The link from the delete confirmation page referring back to the changeform of the object should be quoted"
    701809        response = self.client.get('/test_admin/admin/admin_views/modelwithstringprimarykey/%s/delete/' % quote(self.pk))
    702         should_contain = """<a href="../../%s/">%s</a>""" % (quote(self.pk), escape(self.pk))
     810        # this URL now comes through reverse(), thus iri_to_uri encoding
     811        should_contain = """/%s/">%s</a>""" % (iri_to_uri(quote(self.pk)), escape(self.pk))
    703812        self.assertContains(response, should_contain)
    704813
    705814    def test_url_conflicts_with_add(self):
Back to Top