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/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -13,6 +13,7 @@ from django.db import models, transaction
 from django.db.models.fields import BLANK_CHOICE_DASH
 from django.http import Http404, HttpResponse, HttpResponseRedirect
 from django.shortcuts import get_object_or_404, render_to_response
+from django.template.loader import find_template
 from django.utils.decorators import method_decorator
 from django.utils.datastructures import SortedDict
 from django.utils.functional import update_wrapper
@@ -210,6 +211,7 @@ class ModelAdmin(BaseModelAdmin):
     # Actions
     actions = []
     action_form = helpers.ActionForm
+    actions_template = None
     actions_on_top = True
     actions_on_bottom = False
     actions_selection_counter = True
@@ -1060,8 +1062,26 @@ class ModelAdmin(BaseModelAdmin):
         if actions:
             action_form = self.action_form(auto_id=None)
             action_form.fields['action'].choices = self.get_action_choices(request)
+
+            if self.actions_template:
+                actions_template = self.actions_template
+            else:
+                # Search for the appropriate template path for inclusion by {% admin_actions %}
+                for t in (
+                    'admin/%s/%s/actions.html' % (app_label, opts.object_name.lower()),
+                    'admin/%s/actions.html' % app_label,
+                    'admin/actions.html',
+                ):
+                    try:
+                        find_template(t)
+                    except template.TemplateDoesNotExist:
+                        continue
+                    else:
+                        actions_template = t
+                        break
         else:
             action_form = None
+            actions_template = None
 
         selection_note_all = ungettext('%(total_count)s selected',
             'All %(total_count)s selected', cl.result_count)
@@ -1078,6 +1098,7 @@ class ModelAdmin(BaseModelAdmin):
             'root_path': self.admin_site.root_path,
             'app_label': app_label,
             'action_form': action_form,
+            'actions_template': actions_template,
             'actions_on_top': self.actions_on_top,
             'actions_on_bottom': self.actions_on_bottom,
             '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/django/contrib/admin/templatetags/admin_list.py
+++ b/django/contrib/admin/templatetags/admin_list.py
@@ -13,7 +13,8 @@ from django.utils.safestring import mark_safe
 from django.utils.text import capfirst
 from django.utils.translation import ugettext as _
 from django.utils.encoding import smart_unicode, force_unicode
-from django.template import Library
+from django.template import Library, Node
+from django.template.loader import get_template
 
 
 register = Library()
@@ -287,11 +288,19 @@ def admin_list_filter(cl, spec):
     return {'title': spec.title(), 'choices' : list(spec.choices(cl))}
 admin_list_filter = register.inclusion_tag('admin/filter.html')(admin_list_filter)
 
-def admin_actions(context):
-    """
-    Track the number of times the action field has been rendered on the page,
-    so we know which value to use.
-    """
-    context['action_index'] = context.get('action_index', -1) + 1
-    return context
-admin_actions = register.inclusion_tag("admin/actions.html", takes_context=True)(admin_actions)
+#@register.tag
+def admin_actions(parser, token):
+    class IncludeActionsTemplateNode(Node):
+        def render(self, context):
+            # Track the number of times the action field has been rendered on
+            # the page, so we know which value to use.
+            context['action_index'] = context.get('action_index', -1) + 1
+
+            try:
+                t = get_template(context.get('actions_template', 'admin/actions.html'))
+                return t.render(context)
+            except:
+                return ''
+
+    return IncludeActionsTemplateNode()
+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/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -718,6 +718,16 @@ The `Overriding Admin Templates`_ section describes how to override or extend
 the default admin templates.  Use the following options to override the default
 templates used by the :class:`ModelAdmin` views:
 
+.. attribute:: ModelAdmin.actions_template
+
+.. versionadded:: 1.2
+
+Path to a custom template that will be used to display actions in the model
+objects "change list" view.
+
+If you don't specify this attribute, a default template shipped with Django
+that provides the standard appearance is used.
+
 .. attribute:: ModelAdmin.add_form_template
 
     .. versionadded:: 1.2
@@ -1363,6 +1373,7 @@ app or per model. The following can:
     * ``app_index.html``
     * ``change_form.html``
     * ``change_list.html``
+    * ``actions.html``
     * ``delete_confirmation.html``
     * ``object_history.html``
 
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
--- /dev/null
+++ b/tests/regressiontests/admin_actions/fixtures/admin-views-users.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<django-objects version="1.0">
+    <object pk="100" model="auth.user">
+        <field type="CharField" name="username">super</field>
+        <field type="CharField" name="first_name">Super</field>
+        <field type="CharField" name="last_name">User</field>
+        <field type="CharField" name="email">super@example.com</field>
+        <field type="CharField" name="password">sha1$995a3$6011485ea3834267d719b4c801409b8b1ddd0158</field>
+        <field type="BooleanField" name="is_staff">True</field>
+        <field type="BooleanField" name="is_active">True</field>
+        <field type="BooleanField" name="is_superuser">True</field>
+        <field type="DateTimeField" name="last_login">2007-05-30 13:20:10</field>
+        <field type="DateTimeField" name="date_joined">2007-05-30 13:20:10</field>
+        <field to="auth.group" name="groups" rel="ManyToManyRel"></field>
+        <field to="auth.permission" name="user_permissions" rel="ManyToManyRel"></field>
+    </object>
+</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
--- /dev/null
+++ b/tests/regressiontests/admin_actions/models.py
@@ -0,0 +1,19 @@
+from django.contrib import admin
+from django.db import models
+
+class Animal(models.Model):
+    name = models.CharField(max_length=128)
+
+    class Meta:
+        abstract = True
+
+class Cat(Animal):
+    """Used for model overrides."""
+    pass
+
+class Dog(Animal):
+    """Used for app overrides."""
+    pass
+
+admin.site.register(Cat)
+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
--- /dev/null
+++ b/tests/regressiontests/admin_actions/tests.py
@@ -0,0 +1,40 @@
+from django.test import TestCase
+
+from models import Cat, Dog
+
+class TestInline(TestCase):
+    fixtures = ['admin-views-users.xml']
+
+    def setUp(self):
+        self.base_url = '/test_admin/admin/admin_actions'
+
+        # this is needed for the actions form to show up (with the 'delete' action)
+        Dog(name='Jacky').save()
+        Cat(name='Felix').save()
+
+        self.app_element = '<p class="app-element"></p>'
+        self.model_element = '<p class="model-element"></p>'
+
+        result = self.client.login(username='super', password='secret')
+        self.failUnlessEqual(result, True)
+
+    def tearDown(self):
+        self.client.logout()
+
+    def test_app_can_override(self):
+        """
+        Test that the actions.html template can be overriden by an app.
+        """
+        changelist_url = '%s/dog/' % self.base_url
+        response = self.client.get(changelist_url)
+
+        self.assertContains(response, self.app_element)
+
+    def test_model_can_override(self):
+        """
+        Test that the actions.html template can be overriden by a model.
+        """
+        changelist_url = '%s/cat/' % self.base_url
+        response = self.client.get(changelist_url)
+
+        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
--- /dev/null
+++ b/tests/templates/admin/admin_actions/actions.html
@@ -0,0 +1,3 @@
+{% include "admin/actions.html" %}
+
+<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
--- /dev/null
+++ b/tests/templates/admin/admin_actions/cat/actions.html
@@ -0,0 +1,3 @@
+{% include "admin/actions.html" %}
+
+<p class="model-element"></p>
-- 
1.7.1.513.g06a69

