From 2888dd42ebafd09fc856dfbcc084bc909466936e Mon Sep 17 00:00:00 2001
From: Tay Ray Chuan <rctay89@gmail.com>
Date: Sat, 13 Mar 2010 15:15:42 +0800
Subject: [PATCH] contrib.admin: allow overriding of actions.html template at app level
 - make changelist_view() set the context variable actions_template,
   checking the usual template path
 - make {% admin_actions %} get and render the actions template manually
   using the path set in the context variable actions_template
 - update documentation to say that actions.html can be overriden at the
   app level via the ModelAdmin attribute, actions_template
 - add regression tests in tests/regressiontests/admin_actions
---
 django/contrib/admin/options.py                    |   21 ++++++++++
 django/contrib/admin/templatetags/admin_list.py    |   27 +++++++++----
 docs/ref/contrib/admin/index.txt                   |   11 +++++
 .../admin_actions/fixtures/admin-views-users.xml   |   17 ++++++++
 tests/regressiontests/admin_actions/models.py      |   19 +++++++++
 tests/regressiontests/admin_actions/tests.py       |   40 ++++++++++++++++++++
 tests/templates/admin/admin_actions/actions.html   |    3 +
 .../templates/admin/admin_actions/cat/actions.html |    3 +
 8 files changed, 132 insertions(+), 9 deletions(-)
 create mode 100644 tests/regressiontests/admin_actions/__init__.py
 create mode 100644 tests/regressiontests/admin_actions/fixtures/admin-views-users.xml
 create mode 100644 tests/regressiontests/admin_actions/models.py
 create mode 100644 tests/regressiontests/admin_actions/tests.py
 create mode 100644 tests/templates/admin/admin_actions/actions.html
 create mode 100644 tests/templates/admin/admin_actions/cat/actions.html
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 1f8ff6d..6a17b46 100644
      
        
          
        
        
          
            | a | b | from django.db import models, transaction | 
        
        
          
            | 13 | 13 | from django.db.models.fields import BLANK_CHOICE_DASH | 
          
            | 14 | 14 | from django.http import Http404, HttpResponse, HttpResponseRedirect | 
          
            | 15 | 15 | from django.shortcuts import get_object_or_404, render_to_response | 
        
        
          
            |  | 16 | from django.template.loader import find_template | 
        
        
          
            | 16 | 17 | from django.utils.decorators import method_decorator | 
          
            | 17 | 18 | from django.utils.datastructures import SortedDict | 
          
            | 18 | 19 | from django.utils.functional import update_wrapper | 
        
        
          
            | … | … | class ModelAdmin(BaseModelAdmin): | 
        
        
          
            | 210 | 211 | # Actions | 
          
            | 211 | 212 | actions = [] | 
          
            | 212 | 213 | action_form = helpers.ActionForm | 
        
        
          
            |  | 214 | actions_template = None | 
        
        
          
            | 213 | 215 | actions_on_top = True | 
          
            | 214 | 216 | actions_on_bottom = False | 
          
            | 215 | 217 | actions_selection_counter = True | 
        
        
          
            | … | … | class ModelAdmin(BaseModelAdmin): | 
        
        
          
            | 1060 | 1062 | if actions: | 
          
            | 1061 | 1063 | action_form = self.action_form(auto_id=None) | 
          
            | 1062 | 1064 | action_form.fields['action'].choices = self.get_action_choices(request) | 
        
        
          
            |  | 1065 |  | 
          
            |  | 1066 | if self.actions_template: | 
          
            |  | 1067 | actions_template = self.actions_template | 
          
            |  | 1068 | else: | 
          
            |  | 1069 | # Search for the appropriate template path for inclusion by {% admin_actions %} | 
          
            |  | 1070 | for t in ( | 
          
            |  | 1071 | 'admin/%s/%s/actions.html' % (app_label, opts.object_name.lower()), | 
          
            |  | 1072 | 'admin/%s/actions.html' % app_label, | 
          
            |  | 1073 | 'admin/actions.html', | 
          
            |  | 1074 | ): | 
          
            |  | 1075 | try: | 
          
            |  | 1076 | find_template(t) | 
          
            |  | 1077 | except template.TemplateDoesNotExist: | 
          
            |  | 1078 | continue | 
          
            |  | 1079 | else: | 
          
            |  | 1080 | actions_template = t | 
          
            |  | 1081 | break | 
        
        
          
            | 1063 | 1082 | else: | 
          
            | 1064 | 1083 | action_form = None | 
        
        
          
            |  | 1084 | actions_template = None | 
        
        
          
            | 1065 | 1085 |  | 
          
            | 1066 | 1086 | selection_note_all = ungettext('%(total_count)s selected', | 
          
            | 1067 | 1087 | 'All %(total_count)s selected', cl.result_count) | 
        
        
          
            | … | … | class ModelAdmin(BaseModelAdmin): | 
        
        
          
            | 1078 | 1098 | 'root_path': self.admin_site.root_path, | 
          
            | 1079 | 1099 | 'app_label': app_label, | 
          
            | 1080 | 1100 | 'action_form': action_form, | 
        
        
          
            |  | 1101 | 'actions_template': actions_template, | 
        
        
          
            | 1081 | 1102 | 'actions_on_top': self.actions_on_top, | 
          
            | 1082 | 1103 | 'actions_on_bottom': self.actions_on_bottom, | 
          
            | 1083 | 1104 | 'actions_selection_counter': self.actions_selection_counter, | 
        
      
    
    
      
      diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
index 565db32..6f65044 100644
      
        
          
        
        
          
            | a | b | from django.utils.safestring import mark_safe | 
        
        
          
            | 13 | 13 | from django.utils.text import capfirst | 
          
            | 14 | 14 | from django.utils.translation import ugettext as _ | 
          
            | 15 | 15 | from django.utils.encoding import smart_unicode, force_unicode | 
        
        
          
            | 16 |  | from django.template import Library | 
          
            |  | 16 | from django.template import Library, Node | 
          
            |  | 17 | from django.template.loader import get_template | 
        
        
          
            | 17 | 18 |  | 
          
            | 18 | 19 |  | 
          
            | 19 | 20 | register = Library() | 
        
        
          
            | … | … | def admin_list_filter(cl, spec): | 
        
        
          
            | 287 | 288 | return {'title': spec.title(), 'choices' : list(spec.choices(cl))} | 
          
            | 288 | 289 | admin_list_filter = register.inclusion_tag('admin/filter.html')(admin_list_filter) | 
          
            | 289 | 290 |  | 
        
        
          
            | 290 |  | def admin_actions(context): | 
          
            | 291 |  | """ | 
          
            | 292 |  | Track the number of times the action field has been rendered on the page, | 
          
            | 293 |  | so we know which value to use. | 
          
            | 294 |  | """ | 
          
            | 295 |  | context['action_index'] = context.get('action_index', -1) + 1 | 
          
            | 296 |  | return context | 
          
            | 297 |  | admin_actions = register.inclusion_tag("admin/actions.html", takes_context=True)(admin_actions) | 
          
            |  | 291 | #@register.tag | 
          
            |  | 292 | def admin_actions(parser, token): | 
          
            |  | 293 | class IncludeActionsTemplateNode(Node): | 
          
            |  | 294 | def render(self, context): | 
          
            |  | 295 | # Track the number of times the action field has been rendered on | 
          
            |  | 296 | # the page, so we know which value to use. | 
          
            |  | 297 | context['action_index'] = context.get('action_index', -1) + 1 | 
          
            |  | 298 |  | 
          
            |  | 299 | try: | 
          
            |  | 300 | t = get_template(context.get('actions_template', 'admin/actions.html')) | 
          
            |  | 301 | return t.render(context) | 
          
            |  | 302 | except: | 
          
            |  | 303 | return '' | 
          
            |  | 304 |  | 
          
            |  | 305 | return IncludeActionsTemplateNode() | 
          
            |  | 306 | admin_actions = register.tag(admin_actions) | 
        
      
    
    
      
      diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index f7aefa4..f3c9e50 100644
      
        
          
        
        
          
            | a | b | The `Overriding Admin Templates`_ section describes how to override or extend | 
        
        
          
            | 718 | 718 | the default admin templates.  Use the following options to override the default | 
          
            | 719 | 719 | templates used by the :class:`ModelAdmin` views: | 
          
            | 720 | 720 |  | 
        
        
          
            |  | 721 | .. attribute:: ModelAdmin.actions_template | 
          
            |  | 722 |  | 
          
            |  | 723 | .. versionadded:: 1.2 | 
          
            |  | 724 |  | 
          
            |  | 725 | Path to a custom template that will be used to display actions in the model | 
          
            |  | 726 | objects "change list" view. | 
          
            |  | 727 |  | 
          
            |  | 728 | If you don't specify this attribute, a default template shipped with Django | 
          
            |  | 729 | that provides the standard appearance is used. | 
          
            |  | 730 |  | 
        
        
          
            | 721 | 731 | .. attribute:: ModelAdmin.add_form_template | 
          
            | 722 | 732 |  | 
          
            | 723 | 733 | .. versionadded:: 1.2 | 
        
        
          
            | … | … | app or per model. The following can: | 
        
        
          
            | 1363 | 1373 | * ``app_index.html`` | 
          
            | 1364 | 1374 | * ``change_form.html`` | 
          
            | 1365 | 1375 | * ``change_list.html`` | 
        
        
          
            |  | 1376 | * ``actions.html`` | 
        
        
          
            | 1366 | 1377 | * ``delete_confirmation.html`` | 
          
            | 1367 | 1378 | * ``object_history.html`` | 
          
            | 1368 | 1379 |  | 
        
      
    
    
      
      diff --git a/tests/regressiontests/admin_actions/__init__.py b/tests/regressiontests/admin_actions/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/regressiontests/admin_actions/fixtures/admin-views-users.xml b/tests/regressiontests/admin_actions/fixtures/admin-views-users.xml
new file mode 100644
index 0000000..aba8f4a
      
        
          
        
        
          
            | - | + |  | 
        
        
          
            |  | 1 | <?xml version="1.0" encoding="utf-8"?> | 
          
            |  | 2 | <django-objects version="1.0"> | 
          
            |  | 3 | <object pk="100" model="auth.user"> | 
          
            |  | 4 | <field type="CharField" name="username">super</field> | 
          
            |  | 5 | <field type="CharField" name="first_name">Super</field> | 
          
            |  | 6 | <field type="CharField" name="last_name">User</field> | 
          
            |  | 7 | <field type="CharField" name="email">super@example.com</field> | 
          
            |  | 8 | <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field> | 
          
            |  | 9 | <field type="BooleanField" name="is_staff">True</field> | 
          
            |  | 10 | <field type="BooleanField" name="is_active">True</field> | 
          
            |  | 11 | <field type="BooleanField" name="is_superuser">True</field> | 
          
            |  | 12 | <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field> | 
          
            |  | 13 | <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field> | 
          
            |  | 14 | <field to="auth.group" name="groups" rel="ManyToManyRel"></field> | 
          
            |  | 15 | <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field> | 
          
            |  | 16 | </object> | 
          
            |  | 17 | </django-objects> | 
        
      
    
    
      
      diff --git a/tests/regressiontests/admin_actions/models.py b/tests/regressiontests/admin_actions/models.py
new file mode 100644
index 0000000..47afed4
      
        
          
        
        
          
            | - | + |  | 
        
        
          
            |  | 1 | from django.contrib import admin | 
          
            |  | 2 | from django.db import models | 
          
            |  | 3 |  | 
          
            |  | 4 | class Animal(models.Model): | 
          
            |  | 5 | name = models.CharField(max_length=128) | 
          
            |  | 6 |  | 
          
            |  | 7 | class Meta: | 
          
            |  | 8 | abstract = True | 
          
            |  | 9 |  | 
          
            |  | 10 | class Cat(Animal): | 
          
            |  | 11 | """Used for model overrides.""" | 
          
            |  | 12 | pass | 
          
            |  | 13 |  | 
          
            |  | 14 | class Dog(Animal): | 
          
            |  | 15 | """Used for app overrides.""" | 
          
            |  | 16 | pass | 
          
            |  | 17 |  | 
          
            |  | 18 | admin.site.register(Cat) | 
          
            |  | 19 | admin.site.register(Dog) | 
        
      
    
    
      
      diff --git a/tests/regressiontests/admin_actions/tests.py b/tests/regressiontests/admin_actions/tests.py
new file mode 100644
index 0000000..6731e74
      
        
          
        
        
          
            | - | + |  | 
        
        
          
            |  | 1 | from django.test import TestCase | 
          
            |  | 2 |  | 
          
            |  | 3 | from models import Cat, Dog | 
          
            |  | 4 |  | 
          
            |  | 5 | class TestInline(TestCase): | 
          
            |  | 6 | fixtures = ['admin-views-users.xml'] | 
          
            |  | 7 |  | 
          
            |  | 8 | def setUp(self): | 
          
            |  | 9 | self.base_url = '/test_admin/admin/admin_actions' | 
          
            |  | 10 |  | 
          
            |  | 11 | # this is needed for the actions form to show up (with the 'delete' action) | 
          
            |  | 12 | Dog(name='Jacky').save() | 
          
            |  | 13 | Cat(name='Felix').save() | 
          
            |  | 14 |  | 
          
            |  | 15 | self.app_element = '<p class="app-element"></p>' | 
          
            |  | 16 | self.model_element = '<p class="model-element"></p>' | 
          
            |  | 17 |  | 
          
            |  | 18 | result = self.client.login(username='super', password='secret') | 
          
            |  | 19 | self.failUnlessEqual(result, True) | 
          
            |  | 20 |  | 
          
            |  | 21 | def tearDown(self): | 
          
            |  | 22 | self.client.logout() | 
          
            |  | 23 |  | 
          
            |  | 24 | def test_app_can_override(self): | 
          
            |  | 25 | """ | 
          
            |  | 26 | Test that the actions.html template can be overriden by an app. | 
          
            |  | 27 | """ | 
          
            |  | 28 | changelist_url = '%s/dog/' % self.base_url | 
          
            |  | 29 | response = self.client.get(changelist_url) | 
          
            |  | 30 |  | 
          
            |  | 31 | self.assertContains(response, self.app_element) | 
          
            |  | 32 |  | 
          
            |  | 33 | def test_model_can_override(self): | 
          
            |  | 34 | """ | 
          
            |  | 35 | Test that the actions.html template can be overriden by a model. | 
          
            |  | 36 | """ | 
          
            |  | 37 | changelist_url = '%s/cat/' % self.base_url | 
          
            |  | 38 | response = self.client.get(changelist_url) | 
          
            |  | 39 |  | 
          
            |  | 40 | self.assertContains(response, self.model_element) | 
        
      
    
    
      
      diff --git a/tests/templates/admin/admin_actions/actions.html b/tests/templates/admin/admin_actions/actions.html
new file mode 100644
index 0000000..9c5d2e4
      
        
          
        
        
          
            | - | + |  | 
        
        
          
            |  | 1 | {% include "admin/actions.html" %} | 
          
            |  | 2 |  | 
          
            |  | 3 | <p class="app-element"></p> | 
        
      
    
    
      
      diff --git a/tests/templates/admin/admin_actions/cat/actions.html b/tests/templates/admin/admin_actions/cat/actions.html
new file mode 100644
index 0000000..3563c8a
      
        
          
        
        
          
            | - | + |  | 
        
        
          
            |  | 1 | {% include "admin/actions.html" %} | 
          
            |  | 2 |  | 
          
            |  | 3 | <p class="model-element"></p> |