Fixed #27728 -- Allowed overriding admin templatetags templates.

This commit is contained in:
Raffaele Salmaso 2017-01-12 17:06:00 +01:00 committed by Carlton Gibson
parent 6e52e2554d
commit 5cc28dc752
23 changed files with 336 additions and 27 deletions

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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',
)

View File

@ -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."""

View File

@ -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)

View File

@ -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

View File

@ -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`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -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 = (

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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()