From 725293d51ad6f5dbefb59fa9adfd76d09e1526c6 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Fri, 13 Jun 2008 15:42:43 +0000 Subject: [PATCH] newforms-admin: Made it easier to specify a custom template to be used in the admin section. You can now specify index_template and login_template properties on an AdminSite subclass, and change_form_template, change_list_template, object_history_template and delete_confirmation_template properties on a ModelAdmin subclass. git-svn-id: http://code.djangoproject.com/svn/django/branches/newforms-admin@7630 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/contrib/admin/options.py | 28 ++++++--- django/contrib/admin/sites.py | 56 ++++++++++-------- tests/regressiontests/admin_views/models.py | 9 ++- tests/regressiontests/admin_views/tests.py | 57 ++++++++++++++++++- tests/templates/custom_admin/change_form.html | 1 + .../change_list.html | 0 .../custom_admin/delete_confirmation.html | 1 + tests/templates/custom_admin/index.html | 6 ++ tests/templates/custom_admin/login.html | 6 ++ .../custom_admin/object_history.html | 1 + 10 files changed, 131 insertions(+), 34 deletions(-) create mode 100644 tests/templates/custom_admin/change_form.html rename tests/templates/{admin/admin_views/customarticle => custom_admin}/change_list.html (100%) create mode 100644 tests/templates/custom_admin/delete_confirmation.html create mode 100644 tests/templates/custom_admin/index.html create mode 100644 tests/templates/custom_admin/login.html create mode 100644 tests/templates/custom_admin/object_history.html diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index a7a44d3146..b52a82e61a 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -59,7 +59,10 @@ def flatten_fieldsets(fieldsets): class AdminForm(object): def __init__(self, form, fieldsets, prepopulated_fields): self.form, self.fieldsets = form, fieldsets - self.prepopulated_fields = [{'field': form[field_name], 'dependencies': [form[f] for f in dependencies]} for field_name, dependencies in prepopulated_fields.items()] + self.prepopulated_fields = [{ + 'field': form[field_name], + 'dependencies': [form[f] for f in dependencies] + } for field_name, dependencies in prepopulated_fields.items()] def __iter__(self): for name, options in self.fieldsets: @@ -233,6 +236,12 @@ class ModelAdmin(BaseModelAdmin): save_on_top = False ordering = None inlines = [] + + # Custom templates (designed to be over-ridden in subclasses) + change_form_template = None + change_list_template = None + delete_confirmation_template = None + object_history_template = None def __init__(self, model, admin_site): self.model = model @@ -372,6 +381,7 @@ class ModelAdmin(BaseModelAdmin): if request.POST.has_key("_popup"): post_url_continue += "?_popup=1" return HttpResponseRedirect(post_url_continue % pk_value) + if request.POST.has_key("_popup"): return HttpResponse('' % \ # escape() calls force_unicode. @@ -470,7 +480,7 @@ class ModelAdmin(BaseModelAdmin): 'save_as': self.save_as, 'save_on_top': self.save_on_top, }) - return render_to_response([ + return render_to_response(self.change_form_template or [ "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()), "admin/%s/change_form.html" % app_label, "admin/change_form.html" @@ -626,6 +636,7 @@ class ModelAdmin(BaseModelAdmin): if ERROR_FLAG in request.GET.keys(): return render_to_response('admin/invalid_setup.html', {'title': _('Database error')}) return HttpResponseRedirect(request.path + '?' + ERROR_FLAG + '=1') + context = { 'title': cl.title, 'is_popup': cl.is_popup, @@ -633,7 +644,7 @@ class ModelAdmin(BaseModelAdmin): } context.update({'has_add_permission': self.has_add_permission(request)}), context.update(extra_context or {}) - return render_to_response([ + return render_to_response(self.change_list_template or [ 'admin/%s/%s/change_list.html' % (app_label, opts.object_name.lower()), 'admin/%s/change_list.html' % app_label, 'admin/change_list.html' @@ -676,6 +687,7 @@ class ModelAdmin(BaseModelAdmin): if not self.has_change_permission(request, None): return HttpResponseRedirect("../../../../") return HttpResponseRedirect("../../") + context = { "title": _("Are you sure?"), "object_name": opts.verbose_name, @@ -685,7 +697,7 @@ class ModelAdmin(BaseModelAdmin): "opts": opts, } context.update(extra_context or {}) - return render_to_response([ + return render_to_response(self.delete_confirmation_template or [ "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()), "admin/%s/delete_confirmation.html" % app_label, "admin/delete_confirmation.html" @@ -697,8 +709,10 @@ class ModelAdmin(BaseModelAdmin): from django.contrib.admin.models import LogEntry model = self.model opts = model._meta - action_list = LogEntry.objects.filter(object_id=object_id, - content_type__id__exact=ContentType.objects.get_for_model(model).id).select_related().order_by('action_time') + action_list = LogEntry.objects.filter( + object_id = object_id, + content_type__id__exact = ContentType.objects.get_for_model(model).id + ).select_related().order_by('action_time') # If no history was found, see whether this object even exists. obj = get_object_or_404(model, pk=object_id) context = { @@ -708,7 +722,7 @@ class ModelAdmin(BaseModelAdmin): 'object': obj, } context.update(extra_context or {}) - return render_to_response([ + return render_to_response(self.object_history_template or [ "admin/%s/%s/object_history.html" % (opts.app_label, opts.object_name.lower()), "admin/%s/object_history.html" % opts.app_label, "admin/object_history.html" diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index b4f160dbbe..ffc7e28bdd 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -23,23 +23,6 @@ class AlreadyRegistered(Exception): class NotRegistered(Exception): pass -def _display_login_form(request, error_message=''): - request.session.set_test_cookie() - if request.POST and request.POST.has_key('post_data'): - # User has failed login BUT has previously saved post data. - post_data = request.POST['post_data'] - elif request.POST: - # User's session must have expired; save their post data. - post_data = _encode_post_data(request.POST) - else: - post_data = _encode_post_data({}) - return render_to_response('admin/login.html', { - 'title': _('Log in'), - 'app_path': request.path, - 'post_data': post_data, - 'error_message': error_message - }, context_instance=template.RequestContext(request)) - def _encode_post_data(post_data): from django.conf import settings pickled = pickle.dumps(post_data) @@ -56,6 +39,16 @@ def _decode_post_data(encoded_data): return pickle.loads(pickled) class AdminSite(object): + """ + An AdminSite object encapsulates an instance of the Django admin application, ready + to be hooked in to your URLConf. Models are registered with the AdminSite using the + register() method, and the root() method can then be used as a Django view function + that presents a full admin interface for the collection of registered models. + """ + + index_template = None + login_template = None + def __init__(self): self._registry = {} # model_class class -> admin_class instance @@ -120,7 +113,6 @@ class AdminSite(object): # expired sessions and continue through (#5999) return response - if url == '': return self.index(request) elif url == 'password_change': @@ -214,12 +206,12 @@ class AdminSite(object): message = _("Please log in again, because your session has expired. Don't worry: Your submission has been saved.") else: message = "" - return _display_login_form(request, message) + return self.display_login_form(request, message) # Check that the user accepts cookies. if not request.session.test_cookie_worked(): message = _("Looks like your browser isn't configured to accept cookies. Please enable cookies, reload this page, and try again.") - return _display_login_form(request, message) + return self.display_login_form(request, message) # Check the password. username = request.POST.get('username', None) @@ -235,7 +227,7 @@ class AdminSite(object): message = _("Usernames cannot contain the '@' character.") else: message = _("Your e-mail address is not your username. Try '%s' instead.") % user.username - return _display_login_form(request, message) + return self.display_login_form(request, message) # The user data is correct; log in the user in and continue. else: @@ -255,7 +247,7 @@ class AdminSite(object): request.session.delete_test_cookie() return http.HttpResponseRedirect(request.path) else: - return _display_login_form(request, ERROR_MESSAGE) + return self.display_login_form(request, ERROR_MESSAGE) def index(self, request): """ @@ -300,11 +292,29 @@ class AdminSite(object): for app in app_list: app['models'].sort(lambda x, y: cmp(x['name'], y['name'])) - return render_to_response('admin/index.html', { + return render_to_response(self.index_template or 'admin/index.html', { 'title': _('Site administration'), 'app_list': app_list, }, context_instance=template.RequestContext(request)) + def display_login_form(self, request, error_message=''): + request.session.set_test_cookie() + if request.POST and request.POST.has_key('post_data'): + # User has failed login BUT has previously saved post data. + post_data = request.POST['post_data'] + elif request.POST: + # User's session must have expired; save their post data. + post_data = _encode_post_data(request.POST) + else: + post_data = _encode_post_data({}) + return render_to_response(self.login_template or 'admin/login.html', { + 'title': _('Log in'), + 'app_path': request.path, + 'post_data': post_data, + 'error_message': error_message + }, context_instance=template.RequestContext(request)) + + # This global object represents the default admin site, for the common case. # You can instantiate AdminSite in your own code to create a custom admin site. site = AdminSite() diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py index b6d247a65c..2c0ba1d909 100644 --- a/tests/regressiontests/admin_views/models.py +++ b/tests/regressiontests/admin_views/models.py @@ -1,7 +1,6 @@ from django.db import models from django.contrib import admin - class Article(models.Model): """ A simple article to test admin views. Test backwards compabilty. @@ -24,6 +23,14 @@ class CustomArticle(models.Model): date = models.DateTimeField() class CustomArticleAdmin(admin.ModelAdmin): + """ + Tests various hooks for using custom templates and contexts. + """ + change_list_template = 'custom_admin/change_list.html' + change_form_template = 'custom_admin/change_form.html' + object_history_template = 'custom_admin/object_history.html' + delete_confirmation_template = 'custom_admin/delete_confirmation.html' + def changelist_view(self, request): "Test that extra_context works" return super(CustomArticleAdmin, self).changelist_view(request, extra_context={ diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 30161299cc..1e3b334221 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -5,7 +5,7 @@ from django.contrib.contenttypes.models import ContentType from django.contrib.admin.sites import LOGIN_FORM_KEY, _encode_post_data # local test models -from models import Article +from models import Article, CustomArticle def get_perm(Model, perm): """Return the permission object, for the Model""" @@ -183,13 +183,64 @@ class AdminViewPermissionsTest(TestCase): self.failUnlessEqual(Article.objects.get(pk=1).content, '

edited article

') self.client.get('/test_admin/admin/logout/') - def testCustomChangelistView(self): + def testCustomModelAdminTemplates(self): self.client.get('/test_admin/admin/') self.client.post('/test_admin/admin/', self.super_login) + + # Test custom change list template with custom extra context request = self.client.get('/test_admin/admin/admin_views/customarticle/') self.failUnlessEqual(request.status_code, 200) self.assert_("var hello = 'Hello!';" in request.content) - + self.assertTemplateUsed(request, 'custom_admin/change_list.html') + + # Test custom change form template + request = self.client.get('/test_admin/admin/admin_views/customarticle/add/') + self.assertTemplateUsed(request, 'custom_admin/change_form.html') + + # Add an article so we can test delete and history views + post = self.client.post('/test_admin/admin/admin_views/customarticle/add/', { + 'content': '

great article

', + 'date_0': '2008-03-18', + 'date_1': '10:54:39' + }) + self.assertRedirects(post, '/test_admin/admin/admin_views/customarticle/') + self.failUnlessEqual(CustomArticle.objects.all().count(), 1) + + # Test custom delete and object history templates + request = self.client.get('/test_admin/admin/admin_views/customarticle/1/delete/') + self.assertTemplateUsed(request, 'custom_admin/delete_confirmation.html') + request = self.client.get('/test_admin/admin/admin_views/customarticle/1/history/') + self.assertTemplateUsed(request, 'custom_admin/object_history.html') + + self.client.get('/test_admin/admin/logout/') + + def testCustomAdminSiteTemplates(self): + from django.contrib import admin + self.assertEqual(admin.site.index_template, None) + self.assertEqual(admin.site.login_template, None) + + self.client.get('/test_admin/admin/logout/') + request = self.client.get('/test_admin/admin/') + self.assertTemplateUsed(request, 'admin/login.html') + self.client.post('/test_admin/admin/', self.changeuser_login) + request = self.client.get('/test_admin/admin/') + self.assertTemplateUsed(request, 'admin/index.html') + + self.client.get('/test_admin/admin/logout/') + admin.site.login_template = 'custom_admin/login.html' + admin.site.index_template = 'custom_admin/index.html' + request = self.client.get('/test_admin/admin/') + self.assertTemplateUsed(request, 'custom_admin/login.html') + self.assert_('Hello from a custom login template' in request.content) + self.client.post('/test_admin/admin/', self.changeuser_login) + request = self.client.get('/test_admin/admin/') + self.assertTemplateUsed(request, 'custom_admin/index.html') + self.assert_('Hello from a custom index template' in request.content) + + self.client.get('/test_admin/admin/logout/') + admin.site.login_template = None + admin.site.index_template = None + def testDeleteView(self): """Delete view should restrict access and actually delete items.""" diff --git a/tests/templates/custom_admin/change_form.html b/tests/templates/custom_admin/change_form.html new file mode 100644 index 0000000000..f42ba4b649 --- /dev/null +++ b/tests/templates/custom_admin/change_form.html @@ -0,0 +1 @@ +{% extends "admin/change_form.html" %} diff --git a/tests/templates/admin/admin_views/customarticle/change_list.html b/tests/templates/custom_admin/change_list.html similarity index 100% rename from tests/templates/admin/admin_views/customarticle/change_list.html rename to tests/templates/custom_admin/change_list.html diff --git a/tests/templates/custom_admin/delete_confirmation.html b/tests/templates/custom_admin/delete_confirmation.html new file mode 100644 index 0000000000..9353c5bfc8 --- /dev/null +++ b/tests/templates/custom_admin/delete_confirmation.html @@ -0,0 +1 @@ +{% extends "admin/delete_confirmation.html" %} diff --git a/tests/templates/custom_admin/index.html b/tests/templates/custom_admin/index.html new file mode 100644 index 0000000000..d52033ee2d --- /dev/null +++ b/tests/templates/custom_admin/index.html @@ -0,0 +1,6 @@ +{% extends "admin/index.html" %} + +{% block content %} +Hello from a custom index template +{{ block.super }} +{% endblock %} diff --git a/tests/templates/custom_admin/login.html b/tests/templates/custom_admin/login.html new file mode 100644 index 0000000000..e10a26952f --- /dev/null +++ b/tests/templates/custom_admin/login.html @@ -0,0 +1,6 @@ +{% extends "admin/login.html" %} + +{% block content %} +Hello from a custom login template +{{ block.super }} +{% endblock %} diff --git a/tests/templates/custom_admin/object_history.html b/tests/templates/custom_admin/object_history.html new file mode 100644 index 0000000000..aee3b5bcba --- /dev/null +++ b/tests/templates/custom_admin/object_history.html @@ -0,0 +1 @@ +{% extends "admin/object_history.html" %}