From 5cc28dc752c3ae78456bb835c3ba195489fc26d7 Mon Sep 17 00:00:00 2001
From: Raffaele Salmaso <raffaele@salmaso.org>
Date: Thu, 12 Jan 2017 17:06:00 +0100
Subject: [PATCH] Fixed #27728 -- Allowed overriding admin templatetags
 templates.

---
 .../admin/templates/admin/actions.html        |  8 +++
 .../admin/templates/admin/change_form.html    |  6 +-
 .../admin/change_form_object_tools.html       |  8 +++
 .../admin/templates/admin/change_list.html    |  9 +--
 .../admin/change_list_object_tools.html       | 12 ++++
 .../admin/templates/admin/date_hierarchy.html |  6 ++
 .../admin/templates/admin/submit_line.html    |  2 +
 .../contrib/admin/templatetags/admin_list.py  | 63 +++++++++++++++++--
 .../admin/templatetags/admin_modify.py        | 30 ++++++++-
 django/contrib/admin/templatetags/base.py     | 31 +++++++++
 docs/ref/contrib/admin/index.txt              | 16 +++++
 docs/releases/2.1.txt                         |  8 +++
 tests/admin_changelist/tests.py               | 10 +--
 .../admin/admin_views/article/actions.html    |  6 ++
 .../article/change_form_object_tools.html     |  7 +++
 .../article/change_list_object_tools.html     |  7 +++
 .../article/change_list_results.html          | 38 +++++++++++
 .../admin_views/article/date_hierarchy.html   |  9 +++
 .../admin/admin_views/article/pagination.html | 12 ++++
 .../article/prepopulated_fields_js.html       |  7 +++
 .../admin_views/article/search_form.html      | 16 +++++
 .../admin_views/article/submit_line.html      |  7 +++
 tests/admin_views/test_templatetags.py        | 45 ++++++++++++-
 23 files changed, 336 insertions(+), 27 deletions(-)
 create mode 100644 django/contrib/admin/templates/admin/change_form_object_tools.html
 create mode 100644 django/contrib/admin/templates/admin/change_list_object_tools.html
 create mode 100644 django/contrib/admin/templatetags/base.py
 create mode 100644 tests/admin_views/templates/admin/admin_views/article/actions.html
 create mode 100644 tests/admin_views/templates/admin/admin_views/article/change_form_object_tools.html
 create mode 100644 tests/admin_views/templates/admin/admin_views/article/change_list_object_tools.html
 create mode 100644 tests/admin_views/templates/admin/admin_views/article/change_list_results.html
 create mode 100644 tests/admin_views/templates/admin/admin_views/article/date_hierarchy.html
 create mode 100644 tests/admin_views/templates/admin/admin_views/article/pagination.html
 create mode 100644 tests/admin_views/templates/admin/admin_views/article/prepopulated_fields_js.html
 create mode 100644 tests/admin_views/templates/admin/admin_views/article/search_form.html
 create mode 100644 tests/admin_views/templates/admin/admin_views/article/submit_line.html

diff --git a/django/contrib/admin/templates/admin/actions.html b/django/contrib/admin/templates/admin/actions.html
index 80ffa066ed..ef2232e13f 100644
--- a/django/contrib/admin/templates/admin/actions.html
+++ b/django/contrib/admin/templates/admin/actions.html
@@ -1,7 +1,13 @@
 {% load i18n %}
 <div class="actions">
+  {% block actions %}
+    {% block actions-form %}
     {% for field in action_form %}{% if field.label %}<label>{{ field.label }} {% endif %}{{ field }}{% if field.label %}</label>{% endif %}{% endfor %}
+    {% endblock %}
+    {% block actions-submit %}
     <button type="submit" class="button" title="{% trans "Run the selected action" %}" name="index" value="{{ action_index|default:0 }}">{% trans "Go" %}</button>
+    {% endblock %}
+    {% block actions-counter %}
     {% if actions_selection_counter %}
         <span class="action-counter" data-actions-icnt="{{ cl.result_list|length }}">{{ selection_note }}</span>
         {% if cl.result_count != cl.result_list|length %}
@@ -12,4 +18,6 @@
         <span class="clear"><a href="#">{% trans "Clear selection" %}</a></span>
         {% endif %}
     {% endif %}
+    {% endblock %}
+  {% endblock %}
 </div>
diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html
index f77e50a130..604747e6d9 100644
--- a/django/contrib/admin/templates/admin/change_form.html
+++ b/django/contrib/admin/templates/admin/change_form.html
@@ -28,11 +28,7 @@
 {% if change %}{% if not is_popup %}
   <ul class="object-tools">
     {% block object-tools-items %}
-    <li>
-        {% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
-        <a href="{% add_preserved_filters history_url %}" class="historylink">{% trans "History" %}</a>
-    </li>
-    {% if has_absolute_url %}<li><a href="{{ absolute_url }}" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif %}
+      {% change_form_object_tools %}
     {% endblock %}
   </ul>
 {% endif %}{% endif %}
diff --git a/django/contrib/admin/templates/admin/change_form_object_tools.html b/django/contrib/admin/templates/admin/change_form_object_tools.html
new file mode 100644
index 0000000000..32487493a2
--- /dev/null
+++ b/django/contrib/admin/templates/admin/change_form_object_tools.html
@@ -0,0 +1,8 @@
+{% load i18n admin_urls %}
+{% block object-tools-items %}
+<li>
+    {% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
+    <a href="{% add_preserved_filters history_url %}" class="historylink">{% trans "History" %}</a>
+</li>
+{% if has_absolute_url %}<li><a href="{{ absolute_url }}" class="viewsitelink">{% trans "View on site" %}</a></li>{% endif %}
+{% endblock %}
diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html
index 34a4031799..0ffca672c0 100644
--- a/django/contrib/admin/templates/admin/change_list.html
+++ b/django/contrib/admin/templates/admin/change_list.html
@@ -42,14 +42,7 @@
     {% block object-tools %}
         <ul class="object-tools">
           {% block object-tools-items %}
-            {% if has_add_permission %}
-            <li>
-              {% url cl.opts|admin_urlname:'add' as add_url %}
-              <a href="{% add_preserved_filters add_url is_popup to_field %}" class="addlink">
-                {% blocktrans with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktrans %}
-              </a>
-            </li>
-            {% endif %}
+            {% change_list_object_tools %}
           {% endblock %}
         </ul>
     {% endblock %}
diff --git a/django/contrib/admin/templates/admin/change_list_object_tools.html b/django/contrib/admin/templates/admin/change_list_object_tools.html
new file mode 100644
index 0000000000..5d6d458276
--- /dev/null
+++ b/django/contrib/admin/templates/admin/change_list_object_tools.html
@@ -0,0 +1,12 @@
+{% load i18n admin_urls %}
+
+{% block object-tools-items %}
+  {% if has_add_permission %}
+  <li>
+    {% url cl.opts|admin_urlname:'add' as add_url %}
+    <a href="{% add_preserved_filters add_url is_popup to_field %}" class="addlink">
+      {% blocktrans with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktrans %}
+    </a>
+  </li>
+  {% endif %}
+{% endblock %}
diff --git a/django/contrib/admin/templates/admin/date_hierarchy.html b/django/contrib/admin/templates/admin/date_hierarchy.html
index ecbd2a1a00..65ae800134 100644
--- a/django/contrib/admin/templates/admin/date_hierarchy.html
+++ b/django/contrib/admin/templates/admin/date_hierarchy.html
@@ -1,10 +1,16 @@
 {% if show %}
 <div class="xfull">
 <ul class="toplinks">
+{% block date-hierarchy-toplinks %}
+{% block date-hierarchy-back %}
 {% if back %}<li class="date-back"><a href="{{ back.link }}">&lsaquo; {{ back.title }}</a></li>{% endif %}
+{% endblock %}
+{% block date-hierarchy-choices %}
 {% for choice in choices %}
 <li> {% if choice.link %}<a href="{{ choice.link }}">{% endif %}{{ choice.title }}{% if choice.link %}</a>{% endif %}</li>
 {% endfor %}
+{% endblock %}
+{% endblock %}
 </ul><br class="clear">
 </div>
 {% endif %}
diff --git a/django/contrib/admin/templates/admin/submit_line.html b/django/contrib/admin/templates/admin/submit_line.html
index 2e6cf057f0..26f3920ffa 100644
--- a/django/contrib/admin/templates/admin/submit_line.html
+++ b/django/contrib/admin/templates/admin/submit_line.html
@@ -1,5 +1,6 @@
 {% load i18n admin_urls %}
 <div class="submit-row">
+{% block submit-row %}
 {% if show_save %}<input type="submit" value="{% trans 'Save' %}" class="default" name="_save">{% endif %}
 {% if show_delete_link %}
     {% url opts|admin_urlname:'delete' original.pk|admin_urlquote as delete_url %}
@@ -8,4 +9,5 @@
 {% if show_save_as_new %}<input type="submit" value="{% trans 'Save as new' %}" name="_saveasnew">{% endif %}
 {% if show_save_and_add_another %}<input type="submit" value="{% trans 'Save and add another' %}" name="_addanother">{% endif %}
 {% if show_save_and_continue %}<input type="submit" value="{% trans 'Save and continue editing' %}" name="_continue">{% endif %}
+{% endblock %}
 </div>
diff --git a/django/contrib/admin/templatetags/admin_list.py b/django/contrib/admin/templatetags/admin_list.py
index eeb56c0aa4..ff04faed94 100644
--- a/django/contrib/admin/templatetags/admin_list.py
+++ b/django/contrib/admin/templatetags/admin_list.py
@@ -19,6 +19,8 @@ from django.utils.safestring import mark_safe
 from django.utils.text import capfirst
 from django.utils.translation import gettext as _
 
+from .base import InclusionAdminNode
+
 register = Library()
 
 DOT = '.'
@@ -40,7 +42,6 @@ def paginator_number(cl, i):
                            i + 1)
 
 
-@register.inclusion_tag('admin/pagination.html')
 def pagination(cl):
     """
     Generate the series of links to the pages in a paginated list.
@@ -89,6 +90,16 @@ def pagination(cl):
     }
 
 
+@register.tag(name='pagination')
+def pagination_tag(parser, token):
+    return InclusionAdminNode(
+        parser, token,
+        func=pagination,
+        template_name='pagination.html',
+        takes_context=False,
+    )
+
+
 def result_headers(cl):
     """
     Generate the list column headers.
@@ -314,7 +325,6 @@ def result_hidden_fields(cl):
                 yield mark_safe(form[cl.model._meta.pk.name])
 
 
-@register.inclusion_tag("admin/change_list_results.html")
 def result_list(cl):
     """
     Display the headers and data list together.
@@ -331,7 +341,16 @@ def result_list(cl):
             'results': list(results(cl))}
 
 
-@register.inclusion_tag('admin/date_hierarchy.html')
+@register.tag(name='result_list')
+def result_list_tag(parser, token):
+    return InclusionAdminNode(
+        parser, token,
+        func=result_list,
+        template_name='change_list_results.html',
+        takes_context=False,
+    )
+
+
 def date_hierarchy(cl):
     """
     Display the date hierarchy for date drill-down functionality.
@@ -406,7 +425,16 @@ def date_hierarchy(cl):
             }
 
 
-@register.inclusion_tag('admin/search_form.html')
+@register.tag(name='date_hierarchy')
+def date_hierarchy_tag(parser, token):
+    return InclusionAdminNode(
+        parser, token,
+        func=date_hierarchy,
+        template_name='date_hierarchy.html',
+        takes_context=False,
+    )
+
+
 def search_form(cl):
     """
     Display a search form for searching the list.
@@ -418,6 +446,11 @@ def search_form(cl):
     }
 
 
+@register.tag(name='search_form')
+def search_form_tag(parser, token):
+    return InclusionAdminNode(parser, token, func=search_form, template_name='search_form.html', takes_context=False)
+
+
 @register.simple_tag
 def admin_list_filter(cl, spec):
     tpl = get_template(spec.template)
@@ -428,7 +461,6 @@ def admin_list_filter(cl, spec):
     })
 
 
-@register.inclusion_tag('admin/actions.html', takes_context=True)
 def admin_actions(context):
     """
     Track the number of times the action field has been rendered on the page,
@@ -436,3 +468,24 @@ def admin_actions(context):
     """
     context['action_index'] = context.get('action_index', -1) + 1
     return context
+
+
+@register.tag(name='admin_actions')
+def admin_actions_tag(parser, token):
+    return InclusionAdminNode(parser, token, func=admin_actions, template_name='actions.html')
+
+
+def change_list_object_tools(context):
+    """
+    Displays the row of change list object tools.
+    """
+    return context
+
+
+@register.tag(name='change_list_object_tools')
+def change_list_object_tools_tag(parser, token):
+    return InclusionAdminNode(
+        parser, token,
+        func=change_list_object_tools,
+        template_name='change_list_object_tools.html',
+    )
diff --git a/django/contrib/admin/templatetags/admin_modify.py b/django/contrib/admin/templatetags/admin_modify.py
index 50ec00bb25..95f0fc977c 100644
--- a/django/contrib/admin/templatetags/admin_modify.py
+++ b/django/contrib/admin/templatetags/admin_modify.py
@@ -3,10 +3,11 @@ import json
 from django import template
 from django.template.context import Context
 
+from .base import InclusionAdminNode
+
 register = template.Library()
 
 
-@register.inclusion_tag('admin/prepopulated_fields_js.html', takes_context=True)
 def prepopulated_fields_js(context):
     """
     Create a list of prepopulated_fields that should render Javascript for
@@ -39,7 +40,11 @@ def prepopulated_fields_js(context):
     return context
 
 
-@register.inclusion_tag('admin/submit_line.html', takes_context=True)
+@register.tag(name='prepopulated_fields_js')
+def prepopulated_fields_js_tag(parser, token):
+    return InclusionAdminNode(parser, token, func=prepopulated_fields_js, template_name="prepopulated_fields_js.html")
+
+
 def submit_row(context):
     """
     Display the row of buttons for delete and save.
@@ -66,6 +71,27 @@ def submit_row(context):
     return ctx
 
 
+@register.tag(name='submit_row')
+def submit_row_tag(parser, token):
+    return InclusionAdminNode(parser, token, func=submit_row, template_name='submit_line.html')
+
+
+def change_form_object_tools(context):
+    """
+    Displays the row of change form object tools.
+    """
+    return context
+
+
+@register.tag(name='change_form_object_tools')
+def change_form_object_tools_tag(parser, token):
+    return InclusionAdminNode(
+        parser, token,
+        func=change_form_object_tools,
+        template_name='change_form_object_tools.html',
+    )
+
+
 @register.filter
 def cell_count(inline_admin_form):
     """Return the number of cells used in a tabular inline."""
diff --git a/django/contrib/admin/templatetags/base.py b/django/contrib/admin/templatetags/base.py
new file mode 100644
index 0000000000..a26a84d0de
--- /dev/null
+++ b/django/contrib/admin/templatetags/base.py
@@ -0,0 +1,31 @@
+from inspect import getfullargspec
+
+from django.template.library import InclusionNode, parse_bits
+
+
+class InclusionAdminNode(InclusionNode):
+    def __init__(self, parser, token, func, template_name, takes_context=True):
+        self.template_name = template_name
+
+        params, varargs, varkw, defaults, kwonly, kwonly_defaults, _ = getfullargspec(func)
+        if len(params) > 0 and params[0] == 'self':
+            params = params[1:]  # ignore 'self'
+        bits = token.split_contents()
+        args, kwargs = parse_bits(
+            parser, bits[1:], params, varargs, varkw, defaults, kwonly, kwonly_defaults, takes_context, bits[0]
+        )
+        super().__init__(
+            func=func, takes_context=takes_context, args=args, kwargs=kwargs, filename=None
+        )
+
+    def render(self, context):
+        opts = context['opts']
+        app_label = opts.app_label.lower()
+        object_name = opts.object_name.lower()
+        self.filename = [
+            'admin/%s/%s/%s' % (app_label, object_name, self.template_name),
+            'admin/%s/%s' % (app_label, self.template_name),
+            'admin/%s' % (self.template_name,),
+        ]
+
+        return super().render(context)
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index d1138076ff..5ea78d2278 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -2680,12 +2680,28 @@ Templates which may be overridden per app or model
 Not every template in ``contrib/admin/templates/admin`` may be overridden per
 app or per model. The following can:
 
+* ``actions.html``
 * ``app_index.html``
 * ``change_form.html``
+* ``change_form_object_tools.html``
 * ``change_list.html``
+* ``change_list_object_tools.html``
+* ``change_list_results.html``
+* ``date_hierarchy.html``
 * ``delete_confirmation.html``
 * ``object_history.html``
+* ``pagination.html``
 * ``popup_response.html``
+* ``prepopulated_fields_js.html``
+* ``search_form.html``
+* ``submit_line.html``
+
+.. versionchanged:: 2.1
+
+    The ability to override the ``actions.html``, ``change_form_object_tools.html``,
+    ``change_list_object_tools.html``, ``change_list_results.html``,
+    ``date_hierarchy.html``, ``pagination.html``, ``prepopulated_fields_js.html``,
+    ``search_form.html``, ``submit_line.html`` templates were added.
 
 For those templates that cannot be overridden in this way, you may still
 override them for your entire project. Just place the new version in your
diff --git a/docs/releases/2.1.txt b/docs/releases/2.1.txt
index 65b41b2eed..d6cb44caa1 100644
--- a/docs/releases/2.1.txt
+++ b/docs/releases/2.1.txt
@@ -52,6 +52,14 @@ Minor features
 
 * The new :meth:`.ModelAdmin.get_deleted_objects()` method allows customizing
   the deletion process of the delete view and the "delete selected" action.
+* The ``actions.html``, ``change_list_results.html``, ``date_hierarchy.html``,
+  ``pagination.html``, ``prepopulated_fields_js.html``, ``search_form.html``
+  and ``submit_line.html`` templates can be overridden even per app or
+  per model, other than globally.
+
+* The admin change list and change form object tools can now be overridden per app,
+  per model or globally with ``change_list_object_tools.html`` and
+  ``change_form_object_tools.html`` templates.
 
 :mod:`django.contrib.admindocs`
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/admin_changelist/tests.py b/tests/admin_changelist/tests.py
index 5be70ee6ad..f37e3fa039 100644
--- a/tests/admin_changelist/tests.py
+++ b/tests/admin_changelist/tests.py
@@ -119,7 +119,7 @@ class ChangeListTests(TestCase):
         cl = m.get_changelist_instance(request)
         cl.formset = None
         template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
-        context = Context({'cl': cl})
+        context = Context({'cl': cl, 'opts': Child._meta})
         table_output = template.render(context)
         link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
         row_html = build_tbody_html(new_child.id, link, '<td class="field-parent nowrap">-</td>')
@@ -137,7 +137,7 @@ class ChangeListTests(TestCase):
         cl = m.get_changelist_instance(request)
         cl.formset = None
         template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
-        context = Context({'cl': cl})
+        context = Context({'cl': cl, 'opts': Child._meta})
         table_output = template.render(context)
         link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
         row_html = build_tbody_html(new_child.id, link, '<td class="field-parent nowrap">???</td>')
@@ -153,7 +153,7 @@ class ChangeListTests(TestCase):
         cl = m.get_changelist_instance(request)
         cl.formset = None
         template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
-        context = Context({'cl': cl})
+        context = Context({'cl': cl, 'opts': Child._meta})
         table_output = template.render(context)
         link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
         row_html = build_tbody_html(
@@ -176,7 +176,7 @@ class ChangeListTests(TestCase):
         cl = m.get_changelist_instance(request)
         cl.formset = None
         template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
-        context = Context({'cl': cl})
+        context = Context({'cl': cl, 'opts': Child._meta})
         table_output = template.render(context)
         link = reverse('admin:admin_changelist_child_change', args=(new_child.id,))
         row_html = build_tbody_html(new_child.id, link, '<td class="field-parent nowrap">%s</td>' % new_parent)
@@ -204,7 +204,7 @@ class ChangeListTests(TestCase):
         FormSet = m.get_changelist_formset(request)
         cl.formset = FormSet(queryset=cl.result_list)
         template = Template('{% load admin_list %}{% spaceless %}{% result_list cl %}{% endspaceless %}')
-        context = Context({'cl': cl})
+        context = Context({'cl': cl, 'opts': Child._meta})
         table_output = template.render(context)
         # make sure that hidden fields are in the correct place
         hiddenfields_div = (
diff --git a/tests/admin_views/templates/admin/admin_views/article/actions.html b/tests/admin_views/templates/admin/admin_views/article/actions.html
new file mode 100644
index 0000000000..9aa238fd2a
--- /dev/null
+++ b/tests/admin_views/templates/admin/admin_views/article/actions.html
@@ -0,0 +1,6 @@
+{% extends "admin/actions.html" %}
+{% load i18n %}
+
+{% block actions-submit %}
+<button type="submit" class="button override-actions" title="{% trans "Run the selected action" %}" name="index" value="{{ action_index|default:0 }}">{% trans "Go" %}</button>
+{% endblock %}
diff --git a/tests/admin_views/templates/admin/admin_views/article/change_form_object_tools.html b/tests/admin_views/templates/admin/admin_views/article/change_form_object_tools.html
new file mode 100644
index 0000000000..609974bb95
--- /dev/null
+++ b/tests/admin_views/templates/admin/admin_views/article/change_form_object_tools.html
@@ -0,0 +1,7 @@
+{% extends "admin/change_form_object_tools.html" %}
+{% load i18n admin_urls %}
+
+{% block object-tools-items %}
+<li><a href="#" id="change-form-export" class="override-change_form_object_tools change-form-object-tools-item">{% trans "Export" %}</a></li>
+{{ block.super }}
+{% endblock %}
diff --git a/tests/admin_views/templates/admin/admin_views/article/change_list_object_tools.html b/tests/admin_views/templates/admin/admin_views/article/change_list_object_tools.html
new file mode 100644
index 0000000000..1a9be91952
--- /dev/null
+++ b/tests/admin_views/templates/admin/admin_views/article/change_list_object_tools.html
@@ -0,0 +1,7 @@
+{% extends "admin/change_list_object_tools.html" %}
+{% load i18n admin_urls %}
+
+{% block object-tools-items %}
+<li><a href="#" id="change-list-export" class="override-change_list_object_tools change-list-object-tools-item">{% trans "Export" %}</a></li>
+{{ block.super }}
+{% endblock %}
diff --git a/tests/admin_views/templates/admin/admin_views/article/change_list_results.html b/tests/admin_views/templates/admin/admin_views/article/change_list_results.html
new file mode 100644
index 0000000000..ceb581f8ef
--- /dev/null
+++ b/tests/admin_views/templates/admin/admin_views/article/change_list_results.html
@@ -0,0 +1,38 @@
+{% load i18n static %}
+{% if result_hidden_fields %}
+<div class="hiddenfields">{# DIV for HTML validation #}
+{% for item in result_hidden_fields %}{{ item }}{% endfor %}
+</div>
+{% endif %}
+{% if results %}
+<div class="results override-change_list_results">
+<table id="result_list">
+<thead>
+<tr>
+{% for header in result_headers %}
+<th scope="col" {{ header.class_attrib }}>
+   {% if header.sortable %}
+     {% if header.sort_priority > 0 %}
+       <div class="sortoptions">
+         <a class="sortremove" href="{{ header.url_remove }}" title="{% trans "Remove from sorting" %}"></a>
+         {% if num_sorted_fields > 1 %}<span class="sortpriority" title="{% blocktrans with priority_number=header.sort_priority %}Sorting priority: {{ priority_number }}{% endblocktrans %}">{{ header.sort_priority }}</span>{% endif %}
+         <a href="{{ header.url_toggle }}" class="toggle {% if header.ascending %}ascending{% else %}descending{% endif %}" title="{% trans "Toggle sorting" %}"></a>
+       </div>
+     {% endif %}
+   {% endif %}
+   <div class="text">{% if header.sortable %}<a href="{{ header.url_primary }}">{{ header.text|capfirst }}</a>{% else %}<span>{{ header.text|capfirst }}</span>{% endif %}</div>
+   <div class="clear"></div>
+</th>{% endfor %}
+</tr>
+</thead>
+<tbody>
+{% for result in results %}
+{% if result.form.non_field_errors %}
+    <tr><td colspan="{{ result|length }}">{{ result.form.non_field_errors }}</td></tr>
+{% endif %}
+<tr class="{% cycle 'row1' 'row2' %}">{% for item in result %}{{ item }}{% endfor %}</tr>
+{% endfor %}
+</tbody>
+</table>
+</div>
+{% endif %}
diff --git a/tests/admin_views/templates/admin/admin_views/article/date_hierarchy.html b/tests/admin_views/templates/admin/admin_views/article/date_hierarchy.html
new file mode 100644
index 0000000000..de1cb747b5
--- /dev/null
+++ b/tests/admin_views/templates/admin/admin_views/article/date_hierarchy.html
@@ -0,0 +1,9 @@
+{% extends "admin/date_hierarchy.html" %}
+{% load i18n %}
+
+{% block date-hierarchy-choices %}
+<select id="date-selector" class="override-date_hierarchy">{% for choice in choices %}
+  <option{% if choice.link %} value="{{ choice.link }}"{% endif %}>{{ choice.title }}</option>
+{% endfor %}</select>
+<button id="date-selected">{% trans "Go" %}</button>
+{% endblock %}
diff --git a/tests/admin_views/templates/admin/admin_views/article/pagination.html b/tests/admin_views/templates/admin/admin_views/article/pagination.html
new file mode 100644
index 0000000000..e072cacd3c
--- /dev/null
+++ b/tests/admin_views/templates/admin/admin_views/article/pagination.html
@@ -0,0 +1,12 @@
+{% load admin_list %}
+{% load i18n %}
+<p class="paginator override-pagination">
+{% if pagination_required %}
+{% for i in page_range %}
+    {% paginator_number cl i %}
+{% endfor %}
+{% endif %}
+{{ cl.result_count }} {% if cl.result_count == 1 %}{{ cl.opts.verbose_name }}{% else %}{{ cl.opts.verbose_name_plural }}{% endif %}
+{% if show_all_url %}&nbsp;&nbsp;<a href="{{ show_all_url }}" class="showall">{% trans 'Show all' %}</a>{% endif %}
+{% if cl.formset and cl.result_count %}<input type="submit" name="_save" class="default" value="{% trans 'Save' %}"/>{% endif %}
+</p>
diff --git a/tests/admin_views/templates/admin/admin_views/article/prepopulated_fields_js.html b/tests/admin_views/templates/admin/admin_views/article/prepopulated_fields_js.html
new file mode 100644
index 0000000000..0ee8c7a06c
--- /dev/null
+++ b/tests/admin_views/templates/admin/admin_views/article/prepopulated_fields_js.html
@@ -0,0 +1,7 @@
+{% load l10n static %}
+<script type="text/javascript"
+        id="django-admin-prepopulated-fields-constants"
+        class="override-prepopulated_fields_js"
+        src="{% static "admin/js/prepopulate_init.js" %}"
+        data-prepopulated-fields="{{ prepopulated_fields_json }}">
+</script>
diff --git a/tests/admin_views/templates/admin/admin_views/article/search_form.html b/tests/admin_views/templates/admin/admin_views/article/search_form.html
new file mode 100644
index 0000000000..5b5e6a58f6
--- /dev/null
+++ b/tests/admin_views/templates/admin/admin_views/article/search_form.html
@@ -0,0 +1,16 @@
+{% load i18n static %}
+{% if cl.search_fields %}
+<div id="toolbar" class="override-search_form"><form id="changelist-search" method="get">
+<div><!-- DIV needed for valid HTML -->
+<label for="searchbar"><img src="{% static "admin/img/search.svg" %}" alt="Search" /></label>
+<input type="text" size="40" name="{{ search_var }}" value="{{ cl.query }}" id="searchbar" autofocus />
+<input type="submit" value="{% trans 'Search' %}" />
+{% if show_result_count %}
+    <span class="small quiet">{% blocktrans count counter=cl.result_count %}{{ counter }} result{% plural %}{{ counter }} results{% endblocktrans %} (<a href="?{% if cl.is_popup %}_popup=1{% endif %}">{% if cl.show_full_result_count %}{% blocktrans with full_result_count=cl.full_result_count %}{{ full_result_count }} total{% endblocktrans %}{% else %}{% trans "Show all" %}{% endif %}</a>)</span>
+{% endif %}
+{% for pair in cl.params.items %}
+    {% if pair.0 != search_var %}<input type="hidden" name="{{ pair.0 }}" value="{{ pair.1 }}"/>{% endif %}
+{% endfor %}
+</div>
+</form></div>
+{% endif %}
diff --git a/tests/admin_views/templates/admin/admin_views/article/submit_line.html b/tests/admin_views/templates/admin/admin_views/article/submit_line.html
new file mode 100644
index 0000000000..4a2ca08890
--- /dev/null
+++ b/tests/admin_views/templates/admin/admin_views/article/submit_line.html
@@ -0,0 +1,7 @@
+{% extends "admin/submit_line.html" %}
+{% load i18n admin_urls %}
+
+{% block submit-row %}
+{% if show_publish %}<input type="submit" value="{% trans 'Publish' %}" class="default" name="_publish" />{% endif %}
+{{ block.super }}
+{% endblock %}
diff --git a/tests/admin_views/test_templatetags.py b/tests/admin_views/test_templatetags.py
index 44a08f32cd..db78636d5d 100644
--- a/tests/admin_views/test_templatetags.py
+++ b/tests/admin_views/test_templatetags.py
@@ -7,9 +7,10 @@ from django.contrib.auth.admin import UserAdmin
 from django.contrib.auth.models import User
 from django.test import RequestFactory, TestCase
 from django.urls import reverse
+from django.utils.encoding import force_text
 
-from .admin import site
-from .models import Question
+from .admin import ArticleAdmin, site
+from .models import Article, Question
 from .tests import AdminViewBasicTestCase
 
 
@@ -28,6 +29,46 @@ class AdminTemplateTagsTest(AdminViewBasicTestCase):
         self.assertIs(template_context['extra'], True)
         self.assertIs(template_context['show_save'], True)
 
+    def test_can_override_change_form_templatetags(self):
+        """
+        admin_modify templatetags can follow the 'standard' search patter admin/app_label/model/template.html
+        """
+        factory = RequestFactory()
+        article = Article.objects.all()[0]
+        request = factory.get(reverse('admin:admin_views_article_change', args=[article.pk]))
+        request.user = self.superuser
+        admin = ArticleAdmin(Article, site)
+        extra_context = {'show_publish': True, 'extra': True}
+        response = admin.change_view(request, str(article.pk), extra_context=extra_context)
+        response.render()
+        self.assertIs(response.context_data['show_publish'], True)
+        self.assertIs(response.context_data['extra'], True)
+        content = force_text(response.content)
+        self.assertIs('name="_save"' in content, True)
+        self.assertIs('name="_publish"' in content, True)
+        self.assertIs('override-change_form_object_tools' in content, True)
+        self.assertIs('override-prepopulated_fields_js' in content, True)
+
+    def test_can_override_change_list_templatetags(self):
+        """
+        admin_list templatetags can follow the 'standard' search patter admin/app_label/model/template.html
+        """
+        factory = RequestFactory()
+        request = factory.get(reverse('admin:admin_views_article_changelist'))
+        request.user = self.superuser
+        admin = ArticleAdmin(Article, site)
+        admin.date_hierarchy = 'date'
+        admin.search_fields = ('title', 'content',)
+        response = admin.changelist_view(request)
+        response.render()
+        content = force_text(response.content)
+        self.assertIs('override-actions' in content, True)
+        self.assertIs('override-change_list_object_tools' in content, True)
+        self.assertIs('override-change_list_results' in content, True)
+        self.assertIs('override-date_hierarchy' in content, True)
+        self.assertIs('override-pagination' in content, True)
+        self.assertIs('override-search_form' in content, True)
+
 
 class DateHierarchyTests(TestCase):
     factory = RequestFactory()