PageRenderTime 304ms CodeModel.GetById 141ms app.highlight 118ms RepoModel.GetById 13ms app.codeStats 2ms

/tests/regressiontests/admin_views/tests.py

https://code.google.com/p/mango-py/
Python | 2965 lines | 2815 code | 74 blank | 76 comment | 37 complexity | 5e7b4ee6ace0b61c905cd2fad1686963 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1# coding: utf-8
   2
   3import re
   4import datetime
   5import urlparse
   6
   7from django.conf import settings
   8from django.core import mail
   9from django.core.exceptions import SuspiciousOperation
  10from django.core.files import temp as tempfile
  11from django.core.urlresolvers import reverse
  12# Register auth models with the admin.
  13from django.contrib.auth import REDIRECT_FIELD_NAME, admin
  14from django.contrib.auth.models import User, Permission, UNUSABLE_PASSWORD
  15from django.contrib.contenttypes.models import ContentType
  16from django.contrib.admin.models import LogEntry, DELETION
  17from django.contrib.admin.sites import LOGIN_FORM_KEY
  18from django.contrib.admin.util import quote
  19from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
  20from django.contrib.admin.views.main import IS_POPUP_VAR
  21from django.forms.util import ErrorList
  22import django.template.context
  23from django.test import TestCase
  24from django.utils import formats
  25from django.utils.cache import get_max_age
  26from django.utils.encoding import iri_to_uri
  27from django.utils.html import escape
  28from django.utils.http import urlencode
  29from django.utils.translation import activate, deactivate
  30from django.utils import unittest
  31
  32# local test models
  33from models import (Article, BarAccount, CustomArticle, EmptyModel,
  34    FooAccount, Gallery, ModelWithStringPrimaryKey,
  35    Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast,
  36    Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit,
  37    Category, Post, Plot, FunkyTag, Chapter, Book, Promo, WorkHour, Employee,
  38    Question, Answer, Inquisition, Actor, FoodDelivery,
  39    RowLevelChangePermissionModel, Paper, CoverLetter, Story, OtherStory)
  40
  41
  42class AdminViewBasicTest(TestCase):
  43    fixtures = ['admin-views-users.xml', 'admin-views-colors.xml',
  44                'admin-views-fabrics.xml', 'admin-views-books.xml']
  45
  46    # Store the bit of the URL where the admin is registered as a class
  47    # variable. That way we can test a second AdminSite just by subclassing
  48    # this test case and changing urlbit.
  49    urlbit = 'admin'
  50
  51    def setUp(self):
  52        self.old_USE_I18N = settings.USE_I18N
  53        self.old_USE_L10N = settings.USE_L10N
  54        self.old_LANGUAGE_CODE = settings.LANGUAGE_CODE
  55        self.client.login(username='super', password='secret')
  56        settings.USE_I18N = True
  57
  58    def tearDown(self):
  59        settings.USE_I18N = self.old_USE_I18N
  60        settings.USE_L10N = self.old_USE_L10N
  61        settings.LANGUAGE_CODE = self.old_LANGUAGE_CODE
  62        self.client.logout()
  63        formats.reset_format_cache()
  64
  65    def testTrailingSlashRequired(self):
  66        """
  67        If you leave off the trailing slash, app should redirect and add it.
  68        """
  69        request = self.client.get('/test_admin/%s/admin_views/article/add' % self.urlbit)
  70        self.assertRedirects(request,
  71            '/test_admin/%s/admin_views/article/add/' % self.urlbit, status_code=301
  72        )
  73
  74    def testBasicAddGet(self):
  75        """
  76        A smoke test to ensure GET on the add_view works.
  77        """
  78        response = self.client.get('/test_admin/%s/admin_views/section/add/' % self.urlbit)
  79        self.assertEqual(response.status_code, 200)
  80
  81    def testAddWithGETArgs(self):
  82        response = self.client.get('/test_admin/%s/admin_views/section/add/' % self.urlbit, {'name': 'My Section'})
  83        self.assertEqual(response.status_code, 200)
  84        self.assertTrue(
  85            'value="My Section"' in response.content,
  86            "Couldn't find an input with the right value in the response."
  87        )
  88
  89    def testBasicEditGet(self):
  90        """
  91        A smoke test to ensure GET on the change_view works.
  92        """
  93        response = self.client.get('/test_admin/%s/admin_views/section/1/' % self.urlbit)
  94        self.assertEqual(response.status_code, 200)
  95
  96    def testBasicEditGetStringPK(self):
  97        """
  98        A smoke test to ensure GET on the change_view works (returns an HTTP
  99        404 error, see #11191) when passing a string as the PK argument for a
 100        model with an integer PK field.
 101        """
 102        response = self.client.get('/test_admin/%s/admin_views/section/abc/' % self.urlbit)
 103        self.assertEqual(response.status_code, 404)
 104
 105    def testBasicAddPost(self):
 106        """
 107        A smoke test to ensure POST on add_view works.
 108        """
 109        post_data = {
 110            "name": u"Another Section",
 111            # inline data
 112            "article_set-TOTAL_FORMS": u"3",
 113            "article_set-INITIAL_FORMS": u"0",
 114            "article_set-MAX_NUM_FORMS": u"0",
 115        }
 116        response = self.client.post('/test_admin/%s/admin_views/section/add/' % self.urlbit, post_data)
 117        self.assertEqual(response.status_code, 302) # redirect somewhere
 118
 119    def testPopupAddPost(self):
 120        """
 121        Ensure http response from a popup is properly escaped.
 122        """
 123        post_data = {
 124            '_popup': u'1',
 125            'title': u'title with a new\nline',
 126            'content': u'some content',
 127            'date_0': u'2010-09-10',
 128            'date_1': u'14:55:39',
 129        }
 130        response = self.client.post('/test_admin/%s/admin_views/article/add/' % self.urlbit, post_data)
 131        self.failUnlessEqual(response.status_code, 200)
 132        self.assertContains(response, 'dismissAddAnotherPopup')
 133        self.assertContains(response, 'title with a new\u000Aline')
 134
 135    # Post data for edit inline
 136    inline_post_data = {
 137        "name": u"Test section",
 138        # inline data
 139        "article_set-TOTAL_FORMS": u"6",
 140        "article_set-INITIAL_FORMS": u"3",
 141        "article_set-MAX_NUM_FORMS": u"0",
 142        "article_set-0-id": u"1",
 143        # there is no title in database, give one here or formset will fail.
 144        "article_set-0-title": u"Norske bostaver ćřĺ skaper problemer",
 145        "article_set-0-content": u"<p>Middle content</p>",
 146        "article_set-0-date_0": u"2008-03-18",
 147        "article_set-0-date_1": u"11:54:58",
 148        "article_set-0-section": u"1",
 149        "article_set-1-id": u"2",
 150        "article_set-1-title": u"Need a title.",
 151        "article_set-1-content": u"<p>Oldest content</p>",
 152        "article_set-1-date_0": u"2000-03-18",
 153        "article_set-1-date_1": u"11:54:58",
 154        "article_set-2-id": u"3",
 155        "article_set-2-title": u"Need a title.",
 156        "article_set-2-content": u"<p>Newest content</p>",
 157        "article_set-2-date_0": u"2009-03-18",
 158        "article_set-2-date_1": u"11:54:58",
 159        "article_set-3-id": u"",
 160        "article_set-3-title": u"",
 161        "article_set-3-content": u"",
 162        "article_set-3-date_0": u"",
 163        "article_set-3-date_1": u"",
 164        "article_set-4-id": u"",
 165        "article_set-4-title": u"",
 166        "article_set-4-content": u"",
 167        "article_set-4-date_0": u"",
 168        "article_set-4-date_1": u"",
 169        "article_set-5-id": u"",
 170        "article_set-5-title": u"",
 171        "article_set-5-content": u"",
 172        "article_set-5-date_0": u"",
 173        "article_set-5-date_1": u"",
 174    }
 175
 176    def testBasicEditPost(self):
 177        """
 178        A smoke test to ensure POST on edit_view works.
 179        """
 180        response = self.client.post('/test_admin/%s/admin_views/section/1/' % self.urlbit, self.inline_post_data)
 181        self.assertEqual(response.status_code, 302) # redirect somewhere
 182
 183    def testEditSaveAs(self):
 184        """
 185        Test "save as".
 186        """
 187        post_data = self.inline_post_data.copy()
 188        post_data.update({
 189            '_saveasnew': u'Save+as+new',
 190            "article_set-1-section": u"1",
 191            "article_set-2-section": u"1",
 192            "article_set-3-section": u"1",
 193            "article_set-4-section": u"1",
 194            "article_set-5-section": u"1",
 195        })
 196        response = self.client.post('/test_admin/%s/admin_views/section/1/' % self.urlbit, post_data)
 197        self.assertEqual(response.status_code, 302) # redirect somewhere
 198
 199    def testChangeListSortingCallable(self):
 200        """
 201        Ensure we can sort on a list_display field that is a callable
 202        (column 2 is callable_year in ArticleAdmin)
 203        """
 204        response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 2})
 205        self.assertEqual(response.status_code, 200)
 206        self.assertTrue(
 207            response.content.index('Oldest content') < response.content.index('Middle content') and
 208            response.content.index('Middle content') < response.content.index('Newest content'),
 209            "Results of sorting on callable are out of order."
 210        )
 211
 212    def testChangeListSortingModel(self):
 213        """
 214        Ensure we can sort on a list_display field that is a Model method
 215        (colunn 3 is 'model_year' in ArticleAdmin)
 216        """
 217        response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'dsc', 'o': 3})
 218        self.assertEqual(response.status_code, 200)
 219        self.assertTrue(
 220            response.content.index('Newest content') < response.content.index('Middle content') and
 221            response.content.index('Middle content') < response.content.index('Oldest content'),
 222            "Results of sorting on Model method are out of order."
 223        )
 224
 225    def testChangeListSortingModelAdmin(self):
 226        """
 227        Ensure we can sort on a list_display field that is a ModelAdmin method
 228        (colunn 4 is 'modeladmin_year' in ArticleAdmin)
 229        """
 230        response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 4})
 231        self.assertEqual(response.status_code, 200)
 232        self.assertTrue(
 233            response.content.index('Oldest content') < response.content.index('Middle content') and
 234            response.content.index('Middle content') < response.content.index('Newest content'),
 235            "Results of sorting on ModelAdmin method are out of order."
 236        )
 237
 238    def testLimitedFilter(self):
 239        """Ensure admin changelist filters do not contain objects excluded via limit_choices_to.
 240        This also tests relation-spanning filters (e.g. 'color__value').
 241        """
 242        response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit)
 243        self.assertEqual(response.status_code, 200)
 244        self.assertTrue(
 245            '<div id="changelist-filter">' in response.content,
 246            "Expected filter not found in changelist view."
 247        )
 248        self.assertFalse(
 249            '<a href="?color__id__exact=3">Blue</a>' in response.content,
 250            "Changelist filter not correctly limited by limit_choices_to."
 251        )
 252
 253    def testRelationSpanningFilters(self):
 254        response = self.client.get('/test_admin/%s/admin_views/chapterxtra1/' %
 255                                   self.urlbit)
 256        self.assertEqual(response.status_code, 200)
 257        self.assertContains(response, '<div id="changelist-filter">')
 258        filters = {
 259            'chap__id__exact': dict(
 260                values=[c.id for c in Chapter.objects.all()],
 261                test=lambda obj, value: obj.chap.id == value),
 262            'chap__title': dict(
 263                values=[c.title for c in Chapter.objects.all()],
 264                test=lambda obj, value: obj.chap.title == value),
 265            'chap__book__id__exact': dict(
 266                values=[b.id for b in Book.objects.all()],
 267                test=lambda obj, value: obj.chap.book.id == value),
 268            'chap__book__name': dict(
 269                values=[b.name for b in Book.objects.all()],
 270                test=lambda obj, value: obj.chap.book.name == value),
 271            'chap__book__promo__id__exact': dict(
 272                values=[p.id for p in Promo.objects.all()],
 273                test=lambda obj, value:
 274                    obj.chap.book.promo_set.filter(id=value).exists()),
 275            'chap__book__promo__name': dict(
 276                values=[p.name for p in Promo.objects.all()],
 277                test=lambda obj, value:
 278                    obj.chap.book.promo_set.filter(name=value).exists()),
 279            }
 280        for filter_path, params in filters.items():
 281            for value in params['values']:
 282                query_string = urlencode({filter_path: value})
 283                # ensure filter link exists
 284                self.assertContains(response, '<a href="?%s">' % query_string)
 285                # ensure link works
 286                filtered_response = self.client.get(
 287                    '/test_admin/%s/admin_views/chapterxtra1/?%s' % (
 288                        self.urlbit, query_string))
 289                self.assertEqual(filtered_response.status_code, 200)
 290                # ensure changelist contains only valid objects
 291                for obj in filtered_response.context['cl'].query_set.all():
 292                    self.assertTrue(params['test'](obj, value))
 293
 294    def testIncorrectLookupParameters(self):
 295        """Ensure incorrect lookup parameters are handled gracefully."""
 296        response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'notarealfield': '5'})
 297        self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit)
 298        response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'color__id__exact': 'StringNotInteger!'})
 299        self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit)
 300
 301    def testIsNullLookups(self):
 302        """Ensure is_null is handled correctly."""
 303        Article.objects.create(title="I Could Go Anywhere", content="Versatile", date=datetime.datetime.now())
 304        response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit)
 305        self.assertTrue('4 articles' in response.content, '"4 articles" missing from response')
 306        response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'section__isnull': 'false'})
 307        self.assertTrue('3 articles' in response.content, '"3 articles" missing from response')
 308        response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'section__isnull': 'true'})
 309        self.assertTrue('1 article' in response.content, '"1 article" missing from response')
 310
 311    def testLogoutAndPasswordChangeURLs(self):
 312        response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit)
 313        self.assertFalse('<a href="/test_admin/%s/logout/">' % self.urlbit not in response.content)
 314        self.assertFalse('<a href="/test_admin/%s/password_change/">' % self.urlbit not in response.content)
 315
 316    def testNamedGroupFieldChoicesChangeList(self):
 317        """
 318        Ensures the admin changelist shows correct values in the relevant column
 319        for rows corresponding to instances of a model in which a named group
 320        has been used in the choices option of a field.
 321        """
 322        response = self.client.get('/test_admin/%s/admin_views/fabric/' % self.urlbit)
 323        self.assertEqual(response.status_code, 200)
 324        self.assertTrue(
 325            '<a href="1/">Horizontal</a>' in response.content and
 326            '<a href="2/">Vertical</a>' in response.content,
 327            "Changelist table isn't showing the right human-readable values set by a model field 'choices' option named group."
 328        )
 329
 330    def testNamedGroupFieldChoicesFilter(self):
 331        """
 332        Ensures the filter UI shows correctly when at least one named group has
 333        been used in the choices option of a model field.
 334        """
 335        response = self.client.get('/test_admin/%s/admin_views/fabric/' % self.urlbit)
 336        self.assertEqual(response.status_code, 200)
 337        self.assertTrue(
 338            '<div id="changelist-filter">' in response.content,
 339            "Expected filter not found in changelist view."
 340        )
 341        self.assertTrue(
 342            '<a href="?surface__exact=x">Horizontal</a>' in response.content and
 343            '<a href="?surface__exact=y">Vertical</a>' in response.content,
 344            "Changelist filter isn't showing options contained inside a model field 'choices' option named group."
 345        )
 346
 347    def testChangeListNullBooleanDisplay(self):
 348        Post.objects.create(public=None)
 349        # This hard-codes the URl because it'll fail if it runs
 350        # against the 'admin2' custom admin (which doesn't have the
 351        # Post model).
 352        response = self.client.get("/test_admin/admin/admin_views/post/")
 353        self.assertTrue('icon-unknown.gif' in response.content)
 354
 355    def testI18NLanguageNonEnglishDefault(self):
 356        """
 357        Check if the Javascript i18n view returns an empty language catalog
 358        if the default language is non-English but the selected language
 359        is English. See #13388 and #3594 for more details.
 360        """
 361        try:
 362            settings.LANGUAGE_CODE = 'fr'
 363            activate('en-us')
 364            response = self.client.get('/test_admin/admin/jsi18n/')
 365            self.assertNotContains(response, 'Choisir une heure')
 366        finally:
 367            deactivate()
 368
 369    def testI18NLanguageNonEnglishFallback(self):
 370        """
 371        Makes sure that the fallback language is still working properly
 372        in cases where the selected language cannot be found.
 373        """
 374        try:
 375            settings.LANGUAGE_CODE = 'fr'
 376            activate('none')
 377            response = self.client.get('/test_admin/admin/jsi18n/')
 378            self.assertContains(response, 'Choisir une heure')
 379        finally:
 380            deactivate()
 381
 382    def testL10NDeactivated(self):
 383        """
 384        Check if L10N is deactivated, the Javascript i18n view doesn't
 385        return localized date/time formats. Refs #14824.
 386        """
 387        try:
 388            settings.LANGUAGE_CODE = 'ru'
 389            settings.USE_L10N = False
 390            activate('ru')
 391            response = self.client.get('/test_admin/admin/jsi18n/')
 392            self.assertNotContains(response, '%d.%m.%Y %H:%M:%S')
 393            self.assertContains(response, '%Y-%m-%d %H:%M:%S')
 394        finally:
 395            deactivate()
 396
 397
 398    def test_disallowed_filtering(self):
 399        self.assertRaises(SuspiciousOperation,
 400            self.client.get, "/test_admin/admin/admin_views/album/?owner__email__startswith=fuzzy"
 401        )
 402
 403        try:
 404            self.client.get("/test_admin/admin/admin_views/thing/?color__value__startswith=red")
 405            self.client.get("/test_admin/admin/admin_views/thing/?color__value=red")
 406        except SuspiciousOperation:
 407            self.fail("Filters are allowed if explicitly included in list_filter")
 408
 409        try:
 410            self.client.get("/test_admin/admin/admin_views/person/?age__gt=30")
 411        except SuspiciousOperation:
 412            self.fail("Filters should be allowed if they involve a local field without the need to whitelist them in list_filter or date_hierarchy.")
 413
 414        e1 = Employee.objects.create(name='Anonymous', gender=1, age=22, alive=True, code='123')
 415        e2 = Employee.objects.create(name='Visitor', gender=2, age=19, alive=True, code='124')
 416        WorkHour.objects.create(datum=datetime.datetime.now(), employee=e1)
 417        WorkHour.objects.create(datum=datetime.datetime.now(), employee=e2)
 418        response = self.client.get("/test_admin/admin/admin_views/workhour/")
 419        self.assertEqual(response.status_code, 200)
 420        self.assertContains(response, 'employee__person_ptr__exact')
 421        response = self.client.get("/test_admin/admin/admin_views/workhour/?employee__person_ptr__exact=%d" % e1.pk)
 422        self.assertEqual(response.status_code, 200)
 423
 424    def test_allowed_filtering_15103(self):
 425        """
 426        Regressions test for ticket 15103 - filtering on fields defined in a
 427        ForeignKey 'limit_choices_to' should be allowed, otherwise raw_id_fields
 428        can break.
 429        """
 430        try:
 431            self.client.get("/test_admin/admin/admin_views/inquisition/?leader__name=Palin&leader__age=27")
 432        except SuspiciousOperation:
 433            self.fail("Filters should be allowed if they are defined on a ForeignKey pointing to this model")
 434
 435class AdminJavaScriptTest(AdminViewBasicTest):
 436    def testSingleWidgetFirsFieldFocus(self):
 437        """
 438        JavaScript-assisted auto-focus on first field.
 439        """
 440        response = self.client.get('/test_admin/%s/admin_views/picture/add/' % self.urlbit)
 441        self.assertContains(
 442            response,
 443            '<script type="text/javascript">document.getElementById("id_name").focus();</script>'
 444        )
 445
 446    def testMultiWidgetFirsFieldFocus(self):
 447        """
 448        JavaScript-assisted auto-focus should work if a model/ModelAdmin setup
 449        is such that the first form field has a MultiWidget.
 450        """
 451        response = self.client.get('/test_admin/%s/admin_views/reservation/add/' % self.urlbit)
 452        self.assertContains(
 453            response,
 454            '<script type="text/javascript">document.getElementById("id_start_date_0").focus();</script>'
 455        )
 456
 457
 458class SaveAsTests(TestCase):
 459    fixtures = ['admin-views-users.xml','admin-views-person.xml']
 460
 461    def setUp(self):
 462        self.client.login(username='super', password='secret')
 463
 464    def tearDown(self):
 465        self.client.logout()
 466
 467    def test_save_as_duplication(self):
 468        """Ensure save as actually creates a new person"""
 469        post_data = {'_saveasnew':'', 'name':'John M', 'gender':1, 'age': 42}
 470        response = self.client.post('/test_admin/admin/admin_views/person/1/', post_data)
 471        self.assertEqual(len(Person.objects.filter(name='John M')), 1)
 472        self.assertEqual(len(Person.objects.filter(id=1)), 1)
 473
 474    def test_save_as_display(self):
 475        """
 476        Ensure that 'save as' is displayed when activated and after submitting
 477        invalid data aside save_as_new will not show us a form to overwrite the
 478        initial model.
 479        """
 480        response = self.client.get('/test_admin/admin/admin_views/person/1/')
 481        self.assertTrue(response.context['save_as'])
 482        post_data = {'_saveasnew':'', 'name':'John M', 'gender':3, 'alive':'checked'}
 483        response = self.client.post('/test_admin/admin/admin_views/person/1/', post_data)
 484        self.assertEqual(response.context['form_url'], '../add/')
 485
 486class CustomModelAdminTest(AdminViewBasicTest):
 487    urlbit = "admin2"
 488
 489    def testCustomAdminSiteLoginForm(self):
 490        self.client.logout()
 491        request = self.client.get('/test_admin/admin2/')
 492        self.assertEqual(request.status_code, 200)
 493        login = self.client.post('/test_admin/admin2/', {
 494            REDIRECT_FIELD_NAME: '/test_admin/admin2/',
 495            LOGIN_FORM_KEY: 1,
 496            'username': 'customform',
 497            'password': 'secret',
 498        })
 499        self.assertEqual(login.status_code, 200)
 500        self.assertContains(login, 'custom form error')
 501
 502    def testCustomAdminSiteLoginTemplate(self):
 503        self.client.logout()
 504        request = self.client.get('/test_admin/admin2/')
 505        self.assertTemplateUsed(request, 'custom_admin/login.html')
 506        self.assertTrue('Hello from a custom login template' in request.content)
 507
 508    def testCustomAdminSiteLogoutTemplate(self):
 509        request = self.client.get('/test_admin/admin2/logout/')
 510        self.assertTemplateUsed(request, 'custom_admin/logout.html')
 511        self.assertTrue('Hello from a custom logout template' in request.content)
 512
 513    def testCustomAdminSiteIndexViewAndTemplate(self):
 514        request = self.client.get('/test_admin/admin2/')
 515        self.assertTemplateUsed(request, 'custom_admin/index.html')
 516        self.assertTrue('Hello from a custom index template *bar*' in request.content)
 517
 518    def testCustomAdminSitePasswordChangeTemplate(self):
 519        request = self.client.get('/test_admin/admin2/password_change/')
 520        self.assertTemplateUsed(request, 'custom_admin/password_change_form.html')
 521        self.assertTrue('Hello from a custom password change form template' in request.content)
 522
 523    def testCustomAdminSitePasswordChangeDoneTemplate(self):
 524        request = self.client.get('/test_admin/admin2/password_change/done/')
 525        self.assertTemplateUsed(request, 'custom_admin/password_change_done.html')
 526        self.assertTrue('Hello from a custom password change done template' in request.content)
 527
 528    def testCustomAdminSiteView(self):
 529        self.client.login(username='super', password='secret')
 530        response = self.client.get('/test_admin/%s/my_view/' % self.urlbit)
 531        self.assertTrue(response.content == "Django is a magical pony!", response.content)
 532
 533def get_perm(Model, perm):
 534    """Return the permission object, for the Model"""
 535    ct = ContentType.objects.get_for_model(Model)
 536    return Permission.objects.get(content_type=ct, codename=perm)
 537
 538class AdminViewPermissionsTest(TestCase):
 539    """Tests for Admin Views Permissions."""
 540
 541    fixtures = ['admin-views-users.xml']
 542
 543    def setUp(self):
 544        """Test setup."""
 545        # Setup permissions, for our users who can add, change, and delete.
 546        # We can't put this into the fixture, because the content type id
 547        # and the permission id could be different on each run of the test.
 548
 549        opts = Article._meta
 550
 551        # User who can add Articles
 552        add_user = User.objects.get(username='adduser')
 553        add_user.user_permissions.add(get_perm(Article,
 554            opts.get_add_permission()))
 555
 556        # User who can change Articles
 557        change_user = User.objects.get(username='changeuser')
 558        change_user.user_permissions.add(get_perm(Article,
 559            opts.get_change_permission()))
 560
 561        # User who can delete Articles
 562        delete_user = User.objects.get(username='deleteuser')
 563        delete_user.user_permissions.add(get_perm(Article,
 564            opts.get_delete_permission()))
 565
 566        delete_user.user_permissions.add(get_perm(Section,
 567            Section._meta.get_delete_permission()))
 568
 569        # login POST dicts
 570        self.super_login = {
 571            REDIRECT_FIELD_NAME: '/test_admin/admin/',
 572            LOGIN_FORM_KEY: 1,
 573            'username': 'super',
 574            'password': 'secret',
 575        }
 576        self.super_email_login = {
 577            REDIRECT_FIELD_NAME: '/test_admin/admin/',
 578            LOGIN_FORM_KEY: 1,
 579            'username': 'super@example.com',
 580            'password': 'secret',
 581        }
 582        self.super_email_bad_login = {
 583            REDIRECT_FIELD_NAME: '/test_admin/admin/',
 584            LOGIN_FORM_KEY: 1,
 585            'username': 'super@example.com',
 586            'password': 'notsecret',
 587        }
 588        self.adduser_login = {
 589            REDIRECT_FIELD_NAME: '/test_admin/admin/',
 590            LOGIN_FORM_KEY: 1,
 591            'username': 'adduser',
 592            'password': 'secret',
 593        }
 594        self.changeuser_login = {
 595            REDIRECT_FIELD_NAME: '/test_admin/admin/',
 596            LOGIN_FORM_KEY: 1,
 597            'username': 'changeuser',
 598            'password': 'secret',
 599        }
 600        self.deleteuser_login = {
 601            REDIRECT_FIELD_NAME: '/test_admin/admin/',
 602            LOGIN_FORM_KEY: 1,
 603            'username': 'deleteuser',
 604            'password': 'secret',
 605        }
 606        self.joepublic_login = {
 607            REDIRECT_FIELD_NAME: '/test_admin/admin/',
 608            LOGIN_FORM_KEY: 1,
 609            'username': 'joepublic',
 610            'password': 'secret',
 611        }
 612        self.no_username_login = {
 613            REDIRECT_FIELD_NAME: '/test_admin/admin/',
 614            LOGIN_FORM_KEY: 1,
 615            'password': 'secret',
 616        }
 617
 618    def testLogin(self):
 619        """
 620        Make sure only staff members can log in.
 621
 622        Successful posts to the login page will redirect to the orignal url.
 623        Unsuccessfull attempts will continue to render the login page with
 624        a 200 status code.
 625        """
 626        # Super User
 627        request = self.client.get('/test_admin/admin/')
 628        self.assertEqual(request.status_code, 200)
 629        login = self.client.post('/test_admin/admin/', self.super_login)
 630        self.assertRedirects(login, '/test_admin/admin/')
 631        self.assertFalse(login.context)
 632        self.client.get('/test_admin/admin/logout/')
 633
 634        # Test if user enters e-mail address
 635        request = self.client.get('/test_admin/admin/')
 636        self.assertEqual(request.status_code, 200)
 637        login = self.client.post('/test_admin/admin/', self.super_email_login)
 638        self.assertContains(login, "Your e-mail address is not your username")
 639        # only correct passwords get a username hint
 640        login = self.client.post('/test_admin/admin/', self.super_email_bad_login)
 641        self.assertContains(login, "Please enter a correct username and password.")
 642        new_user = User(username='jondoe', password='secret', email='super@example.com')
 643        new_user.save()
 644        # check to ensure if there are multiple e-mail addresses a user doesn't get a 500
 645        login = self.client.post('/test_admin/admin/', self.super_email_login)
 646        self.assertContains(login, "Please enter a correct username and password.")
 647
 648        # Add User
 649        request = self.client.get('/test_admin/admin/')
 650        self.assertEqual(request.status_code, 200)
 651        login = self.client.post('/test_admin/admin/', self.adduser_login)
 652        self.assertRedirects(login, '/test_admin/admin/')
 653        self.assertFalse(login.context)
 654        self.client.get('/test_admin/admin/logout/')
 655
 656        # Change User
 657        request = self.client.get('/test_admin/admin/')
 658        self.assertEqual(request.status_code, 200)
 659        login = self.client.post('/test_admin/admin/', self.changeuser_login)
 660        self.assertRedirects(login, '/test_admin/admin/')
 661        self.assertFalse(login.context)
 662        self.client.get('/test_admin/admin/logout/')
 663
 664        # Delete User
 665        request = self.client.get('/test_admin/admin/')
 666        self.assertEqual(request.status_code, 200)
 667        login = self.client.post('/test_admin/admin/', self.deleteuser_login)
 668        self.assertRedirects(login, '/test_admin/admin/')
 669        self.assertFalse(login.context)
 670        self.client.get('/test_admin/admin/logout/')
 671
 672        # Regular User should not be able to login.
 673        request = self.client.get('/test_admin/admin/')
 674        self.assertEqual(request.status_code, 200)
 675        login = self.client.post('/test_admin/admin/', self.joepublic_login)
 676        self.assertEqual(login.status_code, 200)
 677        self.assertContains(login, "Please enter a correct username and password.")
 678
 679        # Requests without username should not return 500 errors.
 680        request = self.client.get('/test_admin/admin/')
 681        self.assertEqual(request.status_code, 200)
 682        login = self.client.post('/test_admin/admin/', self.no_username_login)
 683        self.assertEqual(login.status_code, 200)
 684        form = login.context[0].get('form')
 685        self.assertEqual(form.errors['username'][0], 'This field is required.')
 686
 687    def testLoginSuccessfullyRedirectsToOriginalUrl(self):
 688        request = self.client.get('/test_admin/admin/')
 689        self.assertEqual(request.status_code, 200)
 690        query_string = 'the-answer=42'
 691        redirect_url = '/test_admin/admin/?%s' % query_string
 692        new_next = {REDIRECT_FIELD_NAME: redirect_url}
 693        login = self.client.post('/test_admin/admin/', dict(self.super_login, **new_next), QUERY_STRING=query_string)
 694        self.assertRedirects(login, redirect_url)
 695
 696    def testAddView(self):
 697        """Test add view restricts access and actually adds items."""
 698
 699        add_dict = {'title' : 'Dřm ikke',
 700                    'content': '<p>great article</p>',
 701                    'date_0': '2008-03-18', 'date_1': '10:54:39',
 702                    'section': 1}
 703
 704        # Change User should not have access to add articles
 705        self.client.get('/test_admin/admin/')
 706        self.client.post('/test_admin/admin/', self.changeuser_login)
 707        # make sure the view removes test cookie
 708        self.assertEqual(self.client.session.test_cookie_worked(), False)
 709        request = self.client.get('/test_admin/admin/admin_views/article/add/')
 710        self.assertEqual(request.status_code, 403)
 711        # Try POST just to make sure
 712        post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict)
 713        self.assertEqual(post.status_code, 403)
 714        self.assertEqual(Article.objects.all().count(), 3)
 715        self.client.get('/test_admin/admin/logout/')
 716
 717        # Add user may login and POST to add view, then redirect to admin root
 718        self.client.get('/test_admin/admin/')
 719        self.client.post('/test_admin/admin/', self.adduser_login)
 720        addpage = self.client.get('/test_admin/admin/admin_views/article/add/')
 721        self.assertEqual(addpage.status_code, 200)
 722        change_list_link = '<a href="../">Articles</a> &rsaquo;'
 723        self.assertFalse(change_list_link in addpage.content,
 724                    'User restricted to add permission is given link to change list view in breadcrumbs.')
 725        post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict)
 726        self.assertRedirects(post, '/test_admin/admin/')
 727        self.assertEqual(Article.objects.all().count(), 4)
 728        self.assertEqual(len(mail.outbox), 1)
 729        self.assertEqual(mail.outbox[0].subject, 'Greetings from a created object')
 730        self.client.get('/test_admin/admin/logout/')
 731
 732        # Super can add too, but is redirected to the change list view
 733        self.client.get('/test_admin/admin/')
 734        self.client.post('/test_admin/admin/', self.super_login)
 735        addpage = self.client.get('/test_admin/admin/admin_views/article/add/')
 736        self.assertEqual(addpage.status_code, 200)
 737        self.assertFalse(change_list_link not in addpage.content,
 738                    'Unrestricted user is not given link to change list view in breadcrumbs.')
 739        post = self.client.post('/test_admin/admin/admin_views/article/add/', add_dict)
 740        self.assertRedirects(post, '/test_admin/admin/admin_views/article/')
 741        self.assertEqual(Article.objects.all().count(), 5)
 742        self.client.get('/test_admin/admin/logout/')
 743
 744        # 8509 - if a normal user is already logged in, it is possible
 745        # to change user into the superuser without error
 746        login = self.client.login(username='joepublic', password='secret')
 747        # Check and make sure that if user expires, data still persists
 748        self.client.get('/test_admin/admin/')
 749        self.client.post('/test_admin/admin/', self.super_login)
 750        # make sure the view removes test cookie
 751        self.assertEqual(self.client.session.test_cookie_worked(), False)
 752
 753    def testChangeView(self):
 754        """Change view should restrict access and allow users to edit items."""
 755
 756        change_dict = {'title' : 'Ikke fordřmt',
 757                       'content': '<p>edited article</p>',
 758                       'date_0': '2008-03-18', 'date_1': '10:54:39',
 759                       'section': 1}
 760
 761        # add user shoud not be able to view the list of article or change any of them
 762        self.client.get('/test_admin/admin/')
 763        self.client.post('/test_admin/admin/', self.adduser_login)
 764        request = self.client.get('/test_admin/admin/admin_views/article/')
 765        self.assertEqual(request.status_code, 403)
 766        request = self.client.get('/test_admin/admin/admin_views/article/1/')
 767        self.assertEqual(request.status_code, 403)
 768        post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
 769        self.assertEqual(post.status_code, 403)
 770        self.client.get('/test_admin/admin/logout/')
 771
 772        # change user can view all items and edit them
 773        self.client.get('/test_admin/admin/')
 774        self.client.post('/test_admin/admin/', self.changeuser_login)
 775        request = self.client.get('/test_admin/admin/admin_views/article/')
 776        self.assertEqual(request.status_code, 200)
 777        request = self.client.get('/test_admin/admin/admin_views/article/1/')
 778        self.assertEqual(request.status_code, 200)
 779        post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
 780        self.assertRedirects(post, '/test_admin/admin/admin_views/article/')
 781        self.assertEqual(Article.objects.get(pk=1).content, '<p>edited article</p>')
 782
 783        # one error in form should produce singular error message, multiple errors plural
 784        change_dict['title'] = ''
 785        post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
 786        self.assertEqual(request.status_code, 200)
 787        self.assertTrue('Please correct the error below.' in post.content,
 788                        'Singular error message not found in response to post with one error.')
 789        change_dict['content'] = ''
 790        post = self.client.post('/test_admin/admin/admin_views/article/1/', change_dict)
 791        self.assertEqual(request.status_code, 200)
 792        self.assertTrue('Please correct the errors below.' in post.content,
 793                        'Plural error message not found in response to post with multiple errors.')
 794        self.client.get('/test_admin/admin/logout/')
 795
 796        # Test redirection when using row-level change permissions. Refs #11513.
 797        RowLevelChangePermissionModel.objects.create(id=1, name="odd id")
 798        RowLevelChangePermissionModel.objects.create(id=2, name="even id")
 799        for login_dict in [self.super_login, self.changeuser_login, self.adduser_login, self.deleteuser_login]:
 800            self.client.post('/test_admin/admin/', login_dict)
 801            request = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/')
 802            self.assertEqual(request.status_code, 403)
 803            request = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/', {'name': 'changed'})
 804            self.assertEqual(RowLevelChangePermissionModel.objects.get(id=1).name, 'odd id')
 805            self.assertEqual(request.status_code, 403)
 806            request = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/')
 807            self.assertEqual(request.status_code, 200)
 808            request = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/', {'name': 'changed'})
 809            self.assertEqual(RowLevelChangePermissionModel.objects.get(id=2).name, 'changed')
 810            self.assertRedirects(request, '/test_admin/admin/')
 811            self.client.get('/test_admin/admin/logout/')
 812        for login_dict in [self.joepublic_login, self.no_username_login]:
 813            self.client.post('/test_admin/admin/', login_dict)
 814            request = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/')
 815            self.assertEqual(request.status_code, 200)
 816            self.assertContains(request, 'login-form')
 817            request = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/1/', {'name': 'changed'})
 818            self.assertEqual(RowLevelChangePermissionModel.objects.get(id=1).name, 'odd id')
 819            self.assertEqual(request.status_code, 200)
 820            self.assertContains(request, 'login-form')
 821            request = self.client.get('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/')
 822            self.assertEqual(request.status_code, 200)
 823            self.assertContains(request, 'login-form')
 824            request = self.client.post('/test_admin/admin/admin_views/rowlevelchangepermissionmodel/2/', {'name': 'changed again'})
 825            self.assertEqual(RowLevelChangePermissionModel.objects.get(id=2).name, 'changed')
 826            self.assertEqual(request.status_code, 200)
 827            self.assertContains(request, 'login-form')
 828            self.client.get('/test_admin/admin/logout/')
 829
 830    def testConditionallyShowAddSectionLink(self):
 831        """
 832        The foreign key widget should only show the "add related" button if the
 833        user has permission to add that related item.
 834        """
 835        # Set up and log in user.
 836        url = '/test_admin/admin/admin_views/article/add/'
 837        add_link_text = ' class="add-another"'
 838        self.client.get('/test_admin/admin/')
 839        self.client.post('/test_admin/admin/', self.adduser_login)
 840        # The add user can't add sections yet, so they shouldn't see the "add
 841        # section" link.
 842        response = self.client.get(url)
 843        self.assertNotContains(response, add_link_text)
 844        # Allow the add user to add sections too. Now they can see the "add
 845        # section" link.
 846        add_user = User.objects.get(username='adduser')
 847        perm = get_perm(Section, Section._meta.get_add_permission())
 848        add_user.user_permissions.add(perm)
 849        response = self.client.get(url)
 850        self.assertContains(response, add_link_text)
 851
 852    def testCustomModelAdminTemplates(self):
 853        self.client.get('/test_admin/admin/')
 854        self.client.post('/test_admin/admin/', self.super_login)
 855
 856        # Test custom change list template with custom extra context
 857        request = self.client.get('/test_admin/admin/admin_views/customarticle/')
 858        self.assertEqual(request.status_code, 200)
 859        self.assertTrue("var hello = 'Hello!';" in request.content)
 860        self.assertTemplateUsed(request, 'custom_admin/change_list.html')
 861
 862        # Test custom add form template
 863        request = self.client.get('/test_admin/admin/admin_views/customarticle/add/')
 864        self.assertTemplateUsed(request, 'custom_admin/add_form.html')
 865
 866        # Add an article so we can test delete, change, and history views
 867        post = self.client.post('/test_admin/admin/admin_views/customarticle/add/', {
 868            'content': '<p>great article</p>',
 869            'date_0': '2008-03-18',
 870            'date_1': '10:54:39'
 871        })
 872        self.assertRedirects(post, '/test_admin/admin/admin_views/customarticle/')
 873        self.assertEqual(CustomArticle.objects.all().count(), 1)
 874        article_pk = CustomArticle.objects.all()[0].pk
 875
 876        # Test custom delete, change, and object history templates
 877        # Test custom change form template
 878        request = self.client.get('/test_admin/admin/admin_views/customarticle/%d/' % article_pk)
 879        self.assertTemplateUsed(request, 'custom_admin/change_form.html')
 880        request = self.client.get('/test_admin/admin/admin_views/customarticle/%d/delete/' % article_pk)
 881        self.assertTemplateUsed(request, 'custom_admin/delete_confirmation.html')
 882        request = self.client.post('/test_admin/admin/admin_views/customarticle/', data={
 883                'index': 0,
 884                'action': ['delete_selected'],
 885                '_selected_action': ['1'],
 886            })
 887        self.assertTemplateUsed(request, 'custom_admin/delete_selected_confirmation.html')
 888        request = self.client.get('/test_admin/admin/admin_views/customarticle/%d/history/' % article_pk)
 889        self.assertTemplateUsed(request, 'custom_admin/object_history.html')
 890
 891        self.client.get('/test_admin/admin/logout/')
 892
 893    def testDeleteView(self):
 894        """Delete view should restrict access and actually delete items."""
 895
 896        delete_dict = {'post': 'yes'}
 897
 898        # add user shoud not be able to delete articles
 899        self.client.get('/test_admin/admin/')
 900        self.client.post('/test_admin/admin/', self.adduser_login)
 901        request = self.client.get('/test_admin/admin/admin_views/article/1/delete/')
 902        self.assertEqual(request.status_code, 403)
 903        post = self.client.post('/test_admin/admin/admin_views/article/1/delete/', delete_dict)
 904        self.assertEqual(post.status_code, 403)
 905        self.assertEqual(Article.objects.all().count(), 3)
 906        self.client.get('/test_admin/admin/logout/')
 907
 908        # Delete user can delete
 909        self.client.get('/test_admin/admin/')
 910        self.client.post('/test_admin/admin/', self.deleteuser_login)
 911        response = self.client.get('/test_admin/admin/admin_views/section/1/delete/')
 912         # test response contains link to related Article
 913        self.assertContains(response, "admin_views/article/1/")
 914
 915        response = self.client.get('/test_admin/admin/admin_views/article/1/delete/')
 916        self.assertEqual(response.status_code, 200)
 917        post = self.client.post('/test_admin/admin/admin_views/article/1/delete/', delete_dict)
 918        self.assertRedirects(post, '/test_admin/admin/')
 919        self.assertEqual(Article.objects.all().count(), 2)
 920        self.assertEqual(len(mail.outbox), 1)
 921        self.assertEqual(mail.outbox[0].subject, 'Greetings from a deleted object')
 922        article_ct = ContentType.objects.get_for_model(Article)
 923        logged = LogEntry.objects.get(content_type=article_ct, action_flag=DELETION)
 924        self.assertEqual(logged.object_id, u'1')
 925        self.client.get('/test_admin/admin/logout/')
 926
 927    def testDisabledPermissionsWhenLoggedIn(self):
 928        self.client.login(username='super', password='secret')
 929        superuser = User.objects.get(username='super')
 930        superuser.is_active = False
 931        superuser.save()
 932
 933        response = self.client.get('/test_admin/admin/')
 934        self.assertContains(response, 'id="login-form"')
 935        self.assertNotContains(response, 'Log out')
 936
 937        response = self.client.get('/test_admin/admin/secure-view/')
 938        self.assertContains(response, 'id="login-form"')
 939
 940
 941class AdminViewDeletedObjectsTest(TestCase):
 942    fixtures = ['admin-views-users.xml', 'deleted-objects.xml']
 943
 944    def setUp(self):
 945        self.client.login(username='super', password='secret')
 946
 947    def tearDown(self):
 948        self.client.logout()
 949
 950    def test_nesting(self):
 951        """
 952        Objects should be nested to display the relationships that
 953        cause them to be scheduled for deletion.
 954        """
 955        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>""")
 956        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1))
 957        self.assertTrue(pattern.search(response.content))
 958
 959    def test_cyclic(self):
 960        """
 961        Cyclic relationships should still cause each object to only be
 962        listed once.
 963
 964        """
 965        one = """<li>Cyclic one: <a href="/test_admin/admin/admin_views/cyclicone/1/">I am recursive</a>"""
 966        two = """<li>Cyclic two: <a href="/test_admin/admin/admin_views/cyclictwo/1/">I am recursive too</a>"""
 967        response = self.client.get('/test_admin/admin/admin_views/cyclicone/%s/delete/' % quote(1))
 968
 969        self.assertContains(response, one, 1)
 970        self.assertContains(response, two, 1)
 971
 972    def test_perms_needed(self):
 973        self.client.logout()
 974        delete_user = User.objects.get(username='deleteuser')
 975        delete_user.user_permissions.add(get_perm(Plot,
 976            Plot._meta.get_delete_permission()))
 977
 978        self.assertTrue(self.client.login(username='deleteuser',
 979                                          password='secret'))
 980
 981        response = self.client.get('/test_admin/admin/admin_views/plot/%s/delete/' % quote(1))
 982        self.assertContains(response, "your account doesn't have permission to delete the following types of objects")
 983        self.assertContains(response, "<li>plot details</li>")
 984
 985    def test_protected(self):
 986        q = Question.objects.create(question="Why?")
 987        a1 = Answer.objects.create(question=q, answer="Because.")
 988        a2 = Answer.objects.create(question=q, answer="Yes.")
 989
 990        response = self.client.get("/test_admin/admin/admin_views/question/%s/delete/" % quote(q.pk))
 991        self.assertContains(response, "would require deleting the following protected related objects")
 992        self.assertContains(response, '<li>Answer: <a href="/test_admin/admin/admin_views/answer/%s/">Because.</a></li>' % a1.pk)
 993        self.assertContains(response, '<li>Answer: <a href="/test_admin/admin/admin_views/answer/%s/">Yes.</a></li>' % a2.pk)
 994
 995    def test_not_registered(self):
 996        should_contain = """<li>Secret hideout: underground bunker"""
 997        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1))
 998        self.assertContains(response, should_contain, 1)
 999
1000    def test_multiple_fkeys_to_same_model(self):
1001        """
1002        If a deleted object has two relationships from another model,
1003        both of those should be followed in looking for related
1004        objects to delete.
1005
1006        """
1007        should_contain = """<li>Plot: <a href="/test_admin/admin/admin_views/plot/1/">World Domination</a>"""
1008        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(1))
1009        self.assertContains(response, should_contain)
1010        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(2))
1011        self.assertContains(response, should_contain)
1012
1013    def test_multiple_fkeys_to_same_instance(self):
1014        """
1015        If a deleted object has two relationships pointing to it from
1016        another object, the other object should still only be listed
1017        once.
1018
1019        """
1020        should_contain = """<li>Plot: <a href="/test_admin/admin/admin_views/plot/2/">World Peace</a></li>"""
1021        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(2))
1022        self.assertContains(response, should_contain, 1)
1023
1024    def test_inheritance(self):
1025        """
1026        In the case of an inherited model, if either the child or
1027        parent-model instance is deleted, both instances are listed
1028        for deletion, as well as any relationships they have.
1029
1030        """
1031        should_contain = [
1032            """<li>Villain: <a href="/test_admin/admin/admin_views/villain/3/">Bob</a>""",
1033            """<li>Super villain: <a href="/test_admin/admin/admin_views/supervillain/3/">Bob</a>""",
1034            """<li>Secret hideout: floating castle""",
1035            """<li>Super secret hideout: super floating castle!"""
1036            ]
1037        response = self.client.get('/test_admin/admin/admin_views/villain/%s/delete/' % quote(3))
1038        for should in should_contain:
1039            self.a

Large files files are truncated, but you can click here to view the full file