Ticket #6191: 6191_r12557.diff

File 6191_r12557.diff, 31.8 KB (added by Carl Meyer, 14 years ago)

updated patch with (hopefully temporary) workaround fix for #12025 (generics)

  • 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.
  • 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>
    25     {% endfor %}
     23    <ul>{{ deletable_objects|unordered_list }}</ul>
    2624    <form action="" method="post">{% csrf_token %}
    2725    <div>
    2826    {% for obj in queryset %}
  • 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 get_deleted_objects(objs, opts, user, admin_site, levels_to_root=4):
     61    """
     62    Find all objects related to ``objs`` that should also be
     63    deleted. ``objs`` should be an iterable of objects.
    6564
    66 def get_change_view_url(app_label, module_name, pk, admin_site, levels_to_root):
    67     """
    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)
     65    Returns a nested list of strings suitable for display in the
     66    template with the ``unordered_list`` filter.
    7567
    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.
    79 
    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.
     68    `levels_to_root` defines the number of directories (../) to reach
     69    the admin root path. In a change_view this is 4, in a change_list
     70    view 2.
    8271
    8372    This is for backwards compatibility since the options.delete_selected
    8473    method uses this function also from a change_list view.
    8574    This will not be used if we can reverse the URL.
    8675    """
    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)
     76    collector = NestedObjects()
     77    for obj in objs:
     78        # TODO using a private model API!
     79        obj._collect_sub_objects(collector)
     80
     81    # TODO this is needed only because GenericRelations are
     82    # cascaded-deleted way down in the internals in
     83    # DeleteQuery.delete_batch_related, instead of being found by
     84    # _collect_sub_objects.
     85    from django.contrib.contenttypes import generic
     86    for f in obj._meta.many_to_many:
     87        if isinstance(f, generic.GenericRelation):
     88            rel_manager = f.value_from_object(obj)
     89            for related in rel_manager.all():
     90                # There's a wierdness here in the case that the
     91                # generic-related object also has FKs pointing to it
     92                # from elsewhere. DeleteQuery does not follow those
     93                # FKs or delete any such objects explicitly (which is
     94                # probably a bug). Some databases may cascade those
     95                # deletes themselves, and some won't. So do we report
     96                # those objects as to-be-deleted? No right answer; for
     97                # now we opt to report only on objects that Django
     98                # will explicitly delete, at risk that some further
     99                # objects will be silently deleted by a
     100                # referential-integrity-maintaining database.
     101                collector.add(related.__class__, related.pk, related,
     102                              obj.__class__, obj)
     103
     104    perms_needed = set()
     105
     106    def _format_callback(obj):
     107        has_admin = obj.__class__ in admin_site._registry
     108        opts = obj._meta
     109        try:
     110            admin_url = reverse('%s:%s_%s_change'
     111                                % (admin_site.name,
     112                                   opts.app_label,
     113                                   opts.object_name.lower()),
     114                                None, (quote(obj._get_pk_val()),))
     115        except NoReverseMatch:
     116            admin_url = '%s%s/%s/%s/' % ('../'*levels_to_root,
     117                                         opts.app_label,
     118                                         opts.object_name.lower(),
     119                                         quote(obj._get_pk_val()))
     120        if has_admin:
     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            return mark_safe(u'%s: <a href="%s">%s</a>' %
     127                             (escape(capfirst(opts.verbose_name)),
     128                              admin_url,
     129                              escape(obj)))
    125130        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
     131            # Don't display link to edit, because it either has no
     132            # admin or is edited inline.
     133            return u'%s: %s' % (capfirst(opts.verbose_name),
     134                                force_unicode(obj))
    158135
    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
     136    to_delete = collector.nested(_format_callback)
    164137
    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)
     138    return to_delete, perms_needed
     139
     140
     141class NestedObjects(object):
     142    """
     143    A directed acyclic graph collection that exposes the add() API
     144    expected by Model._collect_sub_objects and can present its data as
     145    a nested list of objects.
     146
     147    """
     148    def __init__(self):
     149        # Use object keys of the form (model, pk) because actual model
     150        # objects may not be unique
     151
     152        # maps object key to set of child keys
     153        self.children = SortedDict()
     154
     155        # maps object key to parent key
     156        self.parents = SortedDict()
     157
     158        # maps object key to actual object
     159        self.seen = SortedDict()
     160
     161    def add(self, model, pk, obj,
     162            parent_model=None, parent_obj=None, nullable=False):
     163        """
     164        Add item ``obj`` to the graph. Returns True (and does nothing)
     165        if the item has been seen already.
     166
     167        The ``parent_obj`` argument must already exist in the graph; if
     168        not, it's ignored (but ``obj`` is still added with no
     169        parent). In any case, Model._collect_sub_objects (for whom
     170        this API exists) will never pass a parent that hasn't already
     171        been added itself.
     172
     173        These restrictions in combination ensure the graph will remain
     174        acyclic (but can have multiple roots).
     175
     176        ``model``, ``pk``, and ``parent_model`` arguments are ignored
     177        in favor of the appropriate lookups on ``obj`` and
     178        ``parent_obj``; unlike CollectedObjects, we can't maintain
     179        independence from the knowledge that we're operating on model
     180        instances, and we don't want to allow for inconsistency.
     181
     182        ``nullable`` arg is ignored: it doesn't affect how the tree of
     183        collected objects should be nested for display.
     184        """
     185        model, pk = type(obj), obj._get_pk_val()
     186
     187        key = model, pk
     188
     189        if key in self.seen:
     190            return True
     191        self.seen.setdefault(key, obj)
     192
     193        if parent_obj is not None:
     194            parent_model, parent_pk = (type(parent_obj),
     195                                       parent_obj._get_pk_val())
     196            parent_key = (parent_model, parent_pk)
     197            if parent_key in self.seen:
     198                self.children.setdefault(parent_key, set()).add(key)
     199                self.parents.setdefault(key, parent_key)
     200
     201    def _nested(self, key, format_callback=None):
     202        obj = self.seen[key]
     203        if format_callback:
     204            ret = [format_callback(obj)]
     205        else:
     206            ret = [obj]
     207
     208        children = []
     209        for child in self.children.get(key, ()):
     210            children.extend(self._nested(child, format_callback))
     211        if children:
     212            ret.append(children)
     213
     214        return ret
     215
     216    def nested(self, format_callback=None):
     217        """
     218        Return the graph as a nested list.
     219
     220        """
     221        roots = []
     222        for key in self.seen.keys():
     223            if key not in self.parents:
     224                roots.extend(self._nested(key, format_callback))
     225        return roots
     226
    189227
    190228def model_format_dict(obj):
    191229    """
  • django/db/models/base.py

    diff --git a/django/db/models/base.py b/django/db/models/base.py
    a b  
    545545             (model_class, {pk_val: obj, pk_val: obj, ...}), ...]
    546546        """
    547547        pk_val = self._get_pk_val()
    548         if seen_objs.add(self.__class__, pk_val, self, parent, nullable):
     548        if seen_objs.add(self.__class__, pk_val, self,
     549                         type(parent), parent, nullable):
    549550            return
    550551
    551552        for related in self._meta.get_all_related_objects():
     
    556557                except ObjectDoesNotExist:
    557558                    pass
    558559                else:
    559                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
     560                    sub_obj._collect_sub_objects(seen_objs, self, related.field.null)
    560561            else:
    561562                # To make sure we can access all elements, we can't use the
    562563                # normal manager on the related object. So we work directly
     
    574575                        continue
    575576                delete_qs = rel_descriptor.delete_manager(self).all()
    576577                for sub_obj in delete_qs:
    577                     sub_obj._collect_sub_objects(seen_objs, self.__class__, related.field.null)
     578                    sub_obj._collect_sub_objects(seen_objs, self, related.field.null)
    578579
    579580        # Handle any ancestors (for the model-inheritance case). We do this by
    580581        # 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="1" model="admin_views.plot">
     10        <field type="CharField" name="name">World Domination</field>
     11        <field type="ForeignKey" name="team_leader">1</field>
     12        <field type="ForeignKey" name="contact">2</field>
     13    </object>
     14    <object pk="2" model="admin_views.plot">
     15        <field type="CharField" name="name">World Peace</field>
     16        <field type="ForeignKey" name="team_leader">2</field>
     17        <field type="ForeignKey" name="contact">2</field>
     18    </object>
     19    <object pk="1" model="admin_views.plotdetails">
     20        <field type="CharField" name="details">almost finished</field>
     21        <field type="ForeignKey" name="plot">1</field>
     22    </object>
     23    <object pk="1" model="admin_views.secrethideout">
     24        <field type="CharField" name="location">underground bunker</field>
     25        <field type="ForeignKey" name="villain">1</field>
     26    </object>
     27    <object pk="1" model="admin_views.cyclicone">
     28        <field type="CharField" name="name">I am recursive</field>
     29        <field type="ForeignKey" name="two">1</field>
     30    </object>
     31    <object pk="1" model="admin_views.cyclictwo">
     32        <field type="CharField" name="name">I am recursive too</field>
     33        <field type="ForeignKey" name="one">1</field>
     34    </object>
     35    <object pk="3" model="admin_views.plot">
     36        <field type="CharField" name="name">Corn Conspiracy</field>
     37        <field type="ForeignKey" name="team_leader">1</field>
     38        <field type="ForeignKey" name="contact">1</field>
     39    </object>
     40</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 FunkyTag(models.Model):
     505    "Because we all know there's only one real use case for GFKs."
     506    name = models.CharField(max_length=25)
     507    content_type = models.ForeignKey(ContentType)
     508    object_id = models.PositiveIntegerField()
     509    content_object = generic.GenericForeignKey('content_type', 'object_id')
     510
     511    def __unicode__(self):
     512        return self.name
     513
     514class Plot(models.Model):
     515    name = models.CharField(max_length=100)
     516    team_leader = models.ForeignKey(Villain, related_name='lead_plots')
     517    contact = models.ForeignKey(Villain, related_name='contact_plots')
     518    tags = generic.GenericRelation(FunkyTag)
     519
     520    def __unicode__(self):
     521        return self.name
     522
     523class PlotDetails(models.Model):
     524    details = models.CharField(max_length=100)
     525    plot = models.OneToOneField(Plot)
     526
     527    def __unicode__(self):
     528        return self.details
     529
     530class SecretHideout(models.Model):
     531    """ Secret! Not registered with the admin! """
     532    location = models.CharField(max_length=100)
     533    villain = models.ForeignKey(Villain)
     534
     535    def __unicode__(self):
     536        return self.location
     537
     538class CyclicOne(models.Model):
     539    name = models.CharField(max_length=25)
     540    two = models.ForeignKey('CyclicTwo')
     541
     542    def __unicode__(self):
     543        return self.name
     544
     545class CyclicTwo(models.Model):
     546    name = models.CharField(max_length=25)
     547    one = models.ForeignKey(CyclicOne)
     548
     549    def __unicode__(self):
     550        return self.name
     551
    497552admin.site.register(Article, ArticleAdmin)
    498553admin.site.register(CustomArticle, CustomArticleAdmin)
    499554admin.site.register(Section, save_as=True, inlines=[ArticleInline])
     
    519574admin.site.register(Category, CategoryAdmin)
    520575admin.site.register(Post, PostAdmin)
    521576admin.site.register(Gadget, GadgetAdmin)
     577admin.site.register(Villain)
     578admin.site.register(Plot)
     579admin.site.register(PlotDetails)
     580admin.site.register(CyclicOne)
     581admin.site.register(CyclicTwo)
    522582
    523583# We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
    524584# 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_generic_relations(self):
     717        """
     718        If a deleted object has GenericForeignKeys pointing to it,
     719        those objects should be listed for deletion.
     720
     721        """
     722        plot = Plot.objects.get(pk=3)
     723        tag = FunkyTag.objects.create(content_object=plot, name='hott')
     724        should_contain = """<li>Funky tag: hott"""
     725        response = self.client.get('/test_admin/admin/admin_views/plot/%s/delete/' % quote(3))
     726        self.assertContains(response, should_contain)
     727
    640728class AdminViewStringPrimaryKeyTest(TestCase):
    641729    fixtures = ['admin-views-users.xml', 'string-primary-key.xml']
    642730
     
    699787    def test_deleteconfirmation_link(self):
    700788        "The link from the delete confirmation page referring back to the changeform of the object should be quoted"
    701789        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))
     790        # this URL now comes through reverse(), thus iri_to_uri encoding
     791        should_contain = """/%s/">%s</a>""" % (iri_to_uri(quote(self.pk)), escape(self.pk))
    703792        self.assertContains(response, should_contain)
    704793
    705794    def test_url_conflicts_with_add(self):
Back to Top