From 1f84630c87f8032b0167e6db41acaf50ab710879 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Wed, 14 Jan 2009 20:22:25 +0000 Subject: [PATCH] Fixed #6470: made the admin use a URL resolver. This *is* backwards compatible, but `admin.site.root()` has been deprecated. The new style is `('^admin/', include(admin.site.urls))`; users will need to update their code to take advantage of the new customizable admin URLs. Thanks to Alex Gaynor. git-svn-id: http://code.djangoproject.com/svn/django/trunk@9739 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/project_template/urls.py | 2 +- django/contrib/admin/options.py | 241 +++++++++------ django/contrib/admin/sites.py | 285 ++++++++++++------ django/contrib/admin/util.py | 1 - django/contrib/auth/admin.py | 6 + docs/intro/tutorial02.txt | 2 +- docs/ref/contrib/admin.txt | 76 ++++- .../admin_views/customadmin.py | 30 ++ tests/regressiontests/admin_views/tests.py | 94 +++--- tests/regressiontests/admin_views/urls.py | 4 +- 10 files changed, 484 insertions(+), 257 deletions(-) create mode 100644 tests/regressiontests/admin_views/customadmin.py diff --git a/django/conf/project_template/urls.py b/django/conf/project_template/urls.py index af1d1db8dd..dfb49d3bdc 100644 --- a/django/conf/project_template/urls.py +++ b/django/conf/project_template/urls.py @@ -13,5 +13,5 @@ urlpatterns = patterns('', # (r'^admin/doc/', include('django.contrib.admindocs.urls')), # Uncomment the next line to enable the admin: - # (r'^admin/(.*)', admin.site.root), + # (r'^admin/', include(admin.site.urls)), ) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 3d60b9ddf4..bb51875fba 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -5,11 +5,12 @@ from django.forms.models import BaseInlineFormSet from django.contrib.contenttypes.models import ContentType from django.contrib.admin import widgets from django.contrib.admin import helpers -from django.contrib.admin.util import quote, unquote, flatten_fieldsets, get_deleted_objects +from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects from django.core.exceptions import PermissionDenied from django.db import models, transaction from django.http import Http404, HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, render_to_response +from django.utils.functional import update_wrapper from django.utils.html import escape from django.utils.safestring import mark_safe from django.utils.text import capfirst, get_text_list @@ -38,12 +39,12 @@ class BaseModelAdmin(object): filter_horizontal = () radio_fields = {} prepopulated_fields = {} - + def formfield_for_dbfield(self, db_field, **kwargs): """ Hook for specifying the form Field instance for a given database Field instance. - + If kwargs are given, they're passed to the form Field's constructor. """ @@ -63,18 +64,18 @@ class BaseModelAdmin(object): else: # Otherwise, use the default select widget. return db_field.formfield(**kwargs) - + # For DateTimeFields, use a special field and widget. if isinstance(db_field, models.DateTimeField): kwargs['form_class'] = forms.SplitDateTimeField kwargs['widget'] = widgets.AdminSplitDateTime() return db_field.formfield(**kwargs) - + # For DateFields, add a custom CSS class. if isinstance(db_field, models.DateField): kwargs['widget'] = widgets.AdminDateWidget return db_field.formfield(**kwargs) - + # For TimeFields, add a custom CSS class. if isinstance(db_field, models.TimeField): kwargs['widget'] = widgets.AdminTimeWidget @@ -94,22 +95,22 @@ class BaseModelAdmin(object): if isinstance(db_field, models.IntegerField): kwargs['widget'] = widgets.AdminIntegerFieldWidget return db_field.formfield(**kwargs) - + # For CommaSeparatedIntegerFields, add a custom CSS class. if isinstance(db_field, models.CommaSeparatedIntegerField): kwargs['widget'] = widgets.AdminCommaSeparatedIntegerFieldWidget return db_field.formfield(**kwargs) - + # For TextInputs, add a custom CSS class. if isinstance(db_field, models.CharField): kwargs['widget'] = widgets.AdminTextInputWidget return db_field.formfield(**kwargs) - + # For FileFields and ImageFields add a link to the current file. if isinstance(db_field, models.ImageField) or isinstance(db_field, models.FileField): kwargs['widget'] = widgets.AdminFileWidget return db_field.formfield(**kwargs) - + # For ForeignKey or ManyToManyFields, use a special widget. if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)): if isinstance(db_field, models.ForeignKey) and db_field.name in self.raw_id_fields: @@ -139,10 +140,10 @@ class BaseModelAdmin(object): if formfield is not None: formfield.widget = widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field.rel, self.admin_site) return formfield - + # For any other type of field, just call its formfield() method. return db_field.formfield(**kwargs) - + def _declared_fieldsets(self): if self.fieldsets: return self.fieldsets @@ -154,7 +155,7 @@ class BaseModelAdmin(object): class ModelAdmin(BaseModelAdmin): "Encapsulates all admin options and functionality for a given model." __metaclass__ = forms.MediaDefiningClass - + list_display = ('__str__',) list_display_links = () list_filter = () @@ -166,13 +167,13 @@ 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 self.opts = model._meta @@ -182,59 +183,79 @@ class ModelAdmin(BaseModelAdmin): inline_instance = inline_class(self.model, self.admin_site) self.inline_instances.append(inline_instance) super(ModelAdmin, self).__init__() - - def __call__(self, request, url): - # Delegate to the appropriate method, based on the URL. - if url is None: - return self.changelist_view(request) - elif url == "add": - return self.add_view(request) - elif url.endswith('/history'): - return self.history_view(request, unquote(url[:-8])) - elif url.endswith('/delete'): - return self.delete_view(request, unquote(url[:-7])) - else: - return self.change_view(request, unquote(url)) - + + def get_urls(self): + from django.conf.urls.defaults import patterns, url + + def wrap(view): + def wrapper(*args, **kwargs): + return self.admin_site.admin_view(view)(*args, **kwargs) + return update_wrapper(wrapper, view) + + info = self.admin_site.name, self.model._meta.app_label, self.model._meta.module_name + + urlpatterns = patterns('', + url(r'^$', + wrap(self.changelist_view), + name='%sadmin_%s_%s_changelist' % info), + url(r'^add/$', + wrap(self.add_view), + name='%sadmin_%s_%s_add' % info), + url(r'^(.+)/history/$', + wrap(self.history_view), + name='%sadmin_%s_%s_history' % info), + url(r'^(.+)/delete/$', + wrap(self.delete_view), + name='%sadmin_%s_%s_delete' % info), + url(r'^(.+)/$', + wrap(self.change_view), + name='%sadmin_%s_%s_change' % info), + ) + return urlpatterns + + def urls(self): + return self.get_urls() + urls = property(urls) + def _media(self): from django.conf import settings - + js = ['js/core.js', 'js/admin/RelatedObjectLookups.js'] if self.prepopulated_fields: js.append('js/urlify.js') if self.opts.get_ordered_objects(): js.extend(['js/getElementsBySelector.js', 'js/dom-drag.js' , 'js/admin/ordering.js']) - + return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js]) media = property(_media) - + def has_add_permission(self, request): "Returns True if the given request has permission to add an object." opts = self.opts return request.user.has_perm(opts.app_label + '.' + opts.get_add_permission()) - + def has_change_permission(self, request, obj=None): """ Returns True if the given request has permission to change the given Django model instance. - + If `obj` is None, this should return True if the given request has permission to change *any* object of the given type. """ opts = self.opts return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission()) - + def has_delete_permission(self, request, obj=None): """ Returns True if the given request has permission to change the given Django model instance. - + If `obj` is None, this should return True if the given request has permission to delete *any* object of the given type. """ opts = self.opts return request.user.has_perm(opts.app_label + '.' + opts.get_delete_permission()) - + def queryset(self, request): """ Returns a QuerySet of all model instances that can be edited by the @@ -246,14 +267,14 @@ class ModelAdmin(BaseModelAdmin): if ordering: qs = qs.order_by(*ordering) return qs - + def get_fieldsets(self, request, obj=None): "Hook for specifying fieldsets for the add form." if self.declared_fieldsets: return self.declared_fieldsets form = self.get_form(request, obj) return [(None, {'fields': form.base_fields.keys()})] - + def get_form(self, request, obj=None, **kwargs): """ Returns a Form class for use in the admin add view. This is used by @@ -275,42 +296,42 @@ class ModelAdmin(BaseModelAdmin): } defaults.update(kwargs) return modelform_factory(self.model, **defaults) - + def get_formsets(self, request, obj=None): for inline in self.inline_instances: yield inline.get_formset(request, obj) - + def log_addition(self, request, object): """ - Log that an object has been successfully added. + Log that an object has been successfully added. The default implementation creates an admin LogEntry object. """ from django.contrib.admin.models import LogEntry, ADDITION LogEntry.objects.log_action( - user_id = request.user.pk, + user_id = request.user.pk, content_type_id = ContentType.objects.get_for_model(object).pk, object_id = object.pk, - object_repr = force_unicode(object), + object_repr = force_unicode(object), action_flag = ADDITION ) - + def log_change(self, request, object, message): """ - Log that an object has been successfully changed. + Log that an object has been successfully changed. The default implementation creates an admin LogEntry object. """ from django.contrib.admin.models import LogEntry, CHANGE LogEntry.objects.log_action( - user_id = request.user.pk, - content_type_id = ContentType.objects.get_for_model(object).pk, - object_id = object.pk, - object_repr = force_unicode(object), - action_flag = CHANGE, + user_id = request.user.pk, + content_type_id = ContentType.objects.get_for_model(object).pk, + object_id = object.pk, + object_repr = force_unicode(object), + action_flag = CHANGE, change_message = message ) - + def log_deletion(self, request, object, object_repr): """ Log that an object has been successfully deleted. Note that since the @@ -321,13 +342,13 @@ class ModelAdmin(BaseModelAdmin): """ from django.contrib.admin.models import LogEntry, DELETION LogEntry.objects.log_action( - user_id = request.user.id, - content_type_id = ContentType.objects.get_for_model(self.model).pk, - object_id = object.pk, + user_id = request.user.id, + content_type_id = ContentType.objects.get_for_model(self.model).pk, + object_id = object.pk, object_repr = object_repr, action_flag = DELETION ) - + def construct_change_message(self, request, form, formsets): """ @@ -336,7 +357,7 @@ class ModelAdmin(BaseModelAdmin): change_message = [] if form.changed_data: change_message.append(_('Changed %s.') % get_text_list(form.changed_data, _('and'))) - + if formsets: for formset in formsets: for added_object in formset.new_objects: @@ -357,11 +378,11 @@ class ModelAdmin(BaseModelAdmin): def message_user(self, request, message): """ - Send a message to the user. The default implementation + Send a message to the user. The default implementation posts a message using the auth Message object. """ request.user.message_set.create(message=message) - + def save_form(self, request, form, change): """ Given a ModelForm return an unsaved instance. ``change`` is True if @@ -374,13 +395,13 @@ class ModelAdmin(BaseModelAdmin): Given a model instance save it to the database. """ obj.save() - + def save_formset(self, request, form, formset, change): """ Given an inline formset save it to the database. """ formset.save() - + def render_change_form(self, request, context, add=False, change=False, form_url='', obj=None): opts = self.model._meta app_label = opts.app_label @@ -432,7 +453,7 @@ class ModelAdmin(BaseModelAdmin): return HttpResponseRedirect(request.path) else: self.message_user(request, msg) - + # Figure out where to redirect. If the user has change permission, # redirect to the change-list page for this object. Otherwise, # redirect to the admin index. @@ -466,15 +487,15 @@ class ModelAdmin(BaseModelAdmin): else: self.message_user(request, msg) return HttpResponseRedirect("../") - + def add_view(self, request, form_url='', extra_context=None): "The 'add' admin view for this model." model = self.model opts = model._meta - + if not self.has_add_permission(request): raise PermissionDenied - + ModelForm = self.get_form(request) formsets = [] if request.method == 'POST': @@ -513,17 +534,17 @@ class ModelAdmin(BaseModelAdmin): for FormSet in self.get_formsets(request): formset = FormSet(instance=self.model()) formsets.append(formset) - + adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields) media = self.media + adminForm.media - + inline_admin_formsets = [] for inline, formset in zip(self.inline_instances, formsets): fieldsets = list(inline.get_fieldsets(request)) inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets) inline_admin_formsets.append(inline_admin_formset) media = media + inline_admin_formset.media - + context = { 'title': _('Add %s') % force_unicode(opts.verbose_name), 'adminform': adminForm, @@ -538,29 +559,29 @@ class ModelAdmin(BaseModelAdmin): context.update(extra_context or {}) return self.render_change_form(request, context, add=True) add_view = transaction.commit_on_success(add_view) - + def change_view(self, request, object_id, extra_context=None): "The 'change' admin view for this model." model = self.model opts = model._meta - + try: - obj = model._default_manager.get(pk=object_id) + obj = model._default_manager.get(pk=unquote(object_id)) except model.DoesNotExist: # Don't raise Http404 just yet, because we haven't checked # permissions yet. We don't want an unauthenticated user to be able # to determine whether a given object exists. obj = None - + if not self.has_change_permission(request, obj): raise PermissionDenied - + if obj is None: raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)}) - + if request.method == 'POST' and request.POST.has_key("_saveasnew"): return self.add_view(request, form_url='../../add/') - + ModelForm = self.get_form(request, obj) formsets = [] if request.method == 'POST': @@ -575,7 +596,7 @@ class ModelAdmin(BaseModelAdmin): formset = FormSet(request.POST, request.FILES, instance=new_object) formsets.append(formset) - + if all_valid(formsets) and form_validated: self.save_model(request, new_object, form, change=True) form.save_m2m() @@ -585,16 +606,16 @@ class ModelAdmin(BaseModelAdmin): change_message = self.construct_change_message(request, form, formsets) self.log_change(request, new_object, change_message) return self.response_change(request, new_object) - + else: form = ModelForm(instance=obj) for FormSet in self.get_formsets(request, obj): formset = FormSet(instance=obj) formsets.append(formset) - + adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields) media = self.media + adminForm.media - + inline_admin_formsets = [] for inline, formset in zip(self.inline_instances, formsets): fieldsets = list(inline.get_fieldsets(request, obj)) @@ -617,7 +638,7 @@ class ModelAdmin(BaseModelAdmin): context.update(extra_context or {}) return self.render_change_form(request, context, change=True, obj=obj) change_view = transaction.commit_on_success(change_view) - + def changelist_view(self, request, extra_context=None): "The 'change list' admin view for this model." from django.contrib.admin.views.main import ChangeList, ERROR_FLAG @@ -637,7 +658,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, @@ -652,32 +673,32 @@ class ModelAdmin(BaseModelAdmin): 'admin/%s/change_list.html' % app_label, 'admin/change_list.html' ], context, context_instance=template.RequestContext(request)) - + def delete_view(self, request, object_id, extra_context=None): "The 'delete' admin view for this model." opts = self.model._meta app_label = opts.app_label - + try: - obj = self.model._default_manager.get(pk=object_id) + obj = self.model._default_manager.get(pk=unquote(object_id)) except self.model.DoesNotExist: # Don't raise Http404 just yet, because we haven't checked # permissions yet. We don't want an unauthenticated user to be able # to determine whether a given object exists. obj = None - + if not self.has_delete_permission(request, obj): raise PermissionDenied - + if obj is None: raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {'name': force_unicode(opts.verbose_name), 'key': escape(object_id)}) - + # Populate deleted_objects, a data structure of all related objects that # will also be deleted. - deleted_objects = [mark_safe(u'%s: %s' % (escape(force_unicode(capfirst(opts.verbose_name))), quote(object_id), escape(obj))), []] + deleted_objects = [mark_safe(u'%s: %s' % (escape(force_unicode(capfirst(opts.verbose_name))), object_id, escape(obj))), []] perms_needed = set() get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site) - + if request.POST: # The user has already confirmed the deletion. if perms_needed: raise PermissionDenied @@ -690,7 +711,7 @@ class ModelAdmin(BaseModelAdmin): if not self.has_change_permission(request, None): return HttpResponseRedirect("../../../../") return HttpResponseRedirect("../../") - + context = { "title": _("Are you sure?"), "object_name": force_unicode(opts.verbose_name), @@ -707,7 +728,7 @@ class ModelAdmin(BaseModelAdmin): "admin/%s/delete_confirmation.html" % app_label, "admin/delete_confirmation.html" ], context, context_instance=template.RequestContext(request)) - + def history_view(self, request, object_id, extra_context=None): "The 'history' admin view for this model." from django.contrib.admin.models import LogEntry @@ -735,10 +756,38 @@ class ModelAdmin(BaseModelAdmin): "admin/object_history.html" ], context, context_instance=template.RequestContext(request)) + # + # DEPRECATED methods. + # + def __call__(self, request, url): + """ + DEPRECATED: this is the old way of URL resolution, replaced by + ``get_urls()``. This only called by AdminSite.root(), which is also + deprecated. + + Again, remember that the following code only exists for + backwards-compatibility. Any new URLs, changes to existing URLs, or + whatever need to be done up in get_urls(), above! + + This function still exists for backwards-compatibility; it will be + removed in Django 1.3. + """ + # Delegate to the appropriate method, based on the URL. + if url is None: + return self.changelist_view(request) + elif url == "add": + return self.add_view(request) + elif url.endswith('/history'): + return self.history_view(request, unquote(url[:-8])) + elif url.endswith('/delete'): + return self.delete_view(request, unquote(url[:-7])) + else: + return self.change_view(request, unquote(url)) + class InlineModelAdmin(BaseModelAdmin): """ Options for inline editing of ``model`` instances. - + Provide ``name`` to specify the attribute name of the ``ForeignKey`` from ``model`` to its parent. This is required if ``model`` has more than one ``ForeignKey`` to its parent. @@ -751,7 +800,7 @@ class InlineModelAdmin(BaseModelAdmin): template = None verbose_name = None verbose_name_plural = None - + def __init__(self, parent_model, admin_site): self.admin_site = admin_site self.parent_model = parent_model @@ -771,7 +820,7 @@ class InlineModelAdmin(BaseModelAdmin): js.extend(['js/SelectBox.js' , 'js/SelectFilter2.js']) return forms.Media(js=['%s%s' % (settings.ADMIN_MEDIA_PREFIX, url) for url in js]) media = property(_media) - + def get_formset(self, request, obj=None, **kwargs): """Returns a BaseInlineFormSet class for use in admin add/change views.""" if self.declared_fieldsets: @@ -794,13 +843,13 @@ class InlineModelAdmin(BaseModelAdmin): } defaults.update(kwargs) return inlineformset_factory(self.parent_model, self.model, **defaults) - + def get_fieldsets(self, request, obj=None): if self.declared_fieldsets: return self.declared_fieldsets form = self.get_formset(request).form return [(None, {'fields': form.base_fields.keys()})] - + class StackedInline(InlineModelAdmin): template = 'admin/edit_inline/stacked.html' diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index c16ab6a110..42ce296c4a 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -1,4 +1,3 @@ -import base64 import re from django import http, template from django.contrib.admin import ModelAdmin @@ -6,12 +5,12 @@ from django.contrib.auth import authenticate, login from django.db.models.base import ModelBase from django.core.exceptions import ImproperlyConfigured from django.shortcuts import render_to_response +from django.utils.functional import update_wrapper from django.utils.safestring import mark_safe from django.utils.text import capfirst from django.utils.translation import ugettext_lazy, ugettext as _ from django.views.decorators.cache import never_cache from django.conf import settings -from django.utils.hashcompat import md5_constructor ERROR_MESSAGE = ugettext_lazy("Please enter a correct username and password. Note that both fields are case-sensitive.") LOGIN_FORM_KEY = 'this_is_the_login_form' @@ -29,24 +28,33 @@ class AdminSite(object): 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 app_index_template = None - - def __init__(self): + + def __init__(self, name=None): self._registry = {} # model_class class -> admin_class instance - + # TODO Root path is used to calculate urls under the old root() method + # in order to maintain backwards compatibility we are leaving that in + # so root_path isn't needed, not sure what to do about this. + self.root_path = 'admin/' + if name is None: + name = '' + else: + name += '_' + self.name = name + def register(self, model_or_iterable, admin_class=None, **options): """ Registers the given model(s) with the given admin class. - + The model(s) should be Model classes, not instances. - + If an admin class isn't given, it will use ModelAdmin (the default admin options). If keyword arguments are given -- e.g., list_display -- they'll be applied as options to the admin class. - + If a model is already registered, this will raise AlreadyRegistered. """ # Don't import the humongous validation code unless required @@ -54,7 +62,7 @@ class AdminSite(object): from django.contrib.admin.validation import validate else: validate = lambda model, adminclass: None - + if not admin_class: admin_class = ModelAdmin if isinstance(model_or_iterable, ModelBase): @@ -62,7 +70,7 @@ class AdminSite(object): for model in model_or_iterable: if model in self._registry: raise AlreadyRegistered('The model %s is already registered' % model.__name__) - + # If we got **options then dynamically construct a subclass of # admin_class with those **options. if options: @@ -71,17 +79,17 @@ class AdminSite(object): # which causes issues later on. options['__module__'] = __name__ admin_class = type("%sAdmin" % model.__name__, (admin_class,), options) - + # Validate (which might be a no-op) validate(admin_class, model) - + # Instantiate the admin class to save in the registry self._registry[model] = admin_class(model, self) - + def unregister(self, model_or_iterable): """ Unregisters the given model(s). - + If a model isn't already registered, this will raise NotRegistered. """ if isinstance(model_or_iterable, ModelBase): @@ -90,92 +98,100 @@ class AdminSite(object): if model not in self._registry: raise NotRegistered('The model %s is not registered' % model.__name__) del self._registry[model] - + def has_permission(self, request): """ Returns True if the given HttpRequest has permission to view *at least one* page in the admin site. """ return request.user.is_authenticated() and request.user.is_staff - + def check_dependencies(self): """ Check that all things needed to run the admin have been correctly installed. - + The default implementation checks that LogEntry, ContentType and the auth context processor are installed. """ from django.contrib.admin.models import LogEntry from django.contrib.contenttypes.models import ContentType - + if not LogEntry._meta.installed: raise ImproperlyConfigured("Put 'django.contrib.admin' in your INSTALLED_APPS setting in order to use the admin application.") if not ContentType._meta.installed: raise ImproperlyConfigured("Put 'django.contrib.contenttypes' in your INSTALLED_APPS setting in order to use the admin application.") if 'django.core.context_processors.auth' not in settings.TEMPLATE_CONTEXT_PROCESSORS: raise ImproperlyConfigured("Put 'django.core.context_processors.auth' in your TEMPLATE_CONTEXT_PROCESSORS setting in order to use the admin application.") - - def root(self, request, url): + + def admin_view(self, view): """ - Handles main URL routing for the admin app. - - `url` is the remainder of the URL -- e.g. 'comments/comment/'. + Decorator to create an "admin view attached to this ``AdminSite``. This + wraps the view and provides permission checking by calling + ``self.has_permission``. + + You'll want to use this from within ``AdminSite.get_urls()``: + + class MyAdminSite(AdminSite): + + def get_urls(self): + from django.conf.urls.defaults import patterns, url + + urls = super(MyAdminSite, self).get_urls() + urls += patterns('', + url(r'^my_view/$', self.protected_view(some_view)) + ) + return urls """ - if request.method == 'GET' and not request.path.endswith('/'): - return http.HttpResponseRedirect(request.path + '/') - - if settings.DEBUG: - self.check_dependencies() - - # Figure out the admin base URL path and stash it for later use - self.root_path = re.sub(re.escape(url) + '$', '', request.path) - - url = url.rstrip('/') # Trim trailing slash, if it exists. - - # The 'logout' view doesn't require that the person is logged in. - if url == 'logout': - return self.logout(request) - - # Check permission to continue or display login form. - if not self.has_permission(request): - return self.login(request) - - if url == '': - return self.index(request) - elif url == 'password_change': - return self.password_change(request) - elif url == 'password_change/done': - return self.password_change_done(request) - elif url == 'jsi18n': - return self.i18n_javascript(request) - # URLs starting with 'r/' are for the "View on site" links. - elif url.startswith('r/'): - from django.contrib.contenttypes.views import shortcut - return shortcut(request, *url.split('/')[1:]) - else: - if '/' in url: - return self.model_page(request, *url.split('/', 2)) - else: - return self.app_index(request, url) - - raise http.Http404('The requested admin page does not exist.') - - def model_page(self, request, app_label, model_name, rest_of_url=None): - """ - Handles the model-specific functionality of the admin site, delegating - to the appropriate ModelAdmin class. - """ - from django.db import models - model = models.get_model(app_label, model_name) - if model is None: - raise http.Http404("App %r, model %r, not found." % (app_label, model_name)) - try: - admin_obj = self._registry[model] - except KeyError: - raise http.Http404("This model exists but has not been registered with the admin site.") - return admin_obj(request, rest_of_url) - model_page = never_cache(model_page) - + def inner(request, *args, **kwargs): + if not self.has_permission(request): + return self.login(request) + return view(request, *args, **kwargs) + return update_wrapper(inner, view) + + def get_urls(self): + from django.conf.urls.defaults import patterns, url, include + + def wrap(view): + def wrapper(*args, **kwargs): + return self.admin_view(view)(*args, **kwargs) + return update_wrapper(wrapper, view) + + # Admin-site-wide views. + urlpatterns = patterns('', + url(r'^$', + wrap(self.index), + name='%sadmin_index' % self.name), + url(r'^logout/$', + wrap(self.logout), + name='%sadmin_logout'), + url(r'^password_change/$', + wrap(self.password_change), + name='%sadmin_password_change' % self.name), + url(r'^password_change/done/$', + wrap(self.password_change_done), + name='%sadmin_password_change_done' % self.name), + url(r'^jsi18n/$', + wrap(self.i18n_javascript), + name='%sadmin_jsi18n' % self.name), + url(r'^r/(?P\d+)/(?P.+)/$', + 'django.views.defaults.shortcut'), + url(r'^(?P\w+)/$', + wrap(self.app_index), + name='%sadmin_app_list' % self.name), + ) + + # Add in each model's views. + for model, model_admin in self._registry.iteritems(): + urlpatterns += patterns('', + url(r'^%s/%s/' % (model._meta.app_label, model._meta.module_name), + include(model_admin.urls)) + ) + return urlpatterns + + def urls(self): + return self.get_urls() + urls = property(urls) + def password_change(self, request): """ Handles the "change password" task -- both form display and validation. @@ -183,18 +199,18 @@ class AdminSite(object): from django.contrib.auth.views import password_change return password_change(request, post_change_redirect='%spassword_change/done/' % self.root_path) - + def password_change_done(self, request): """ Displays the "success" page after a password change. """ from django.contrib.auth.views import password_change_done return password_change_done(request) - + def i18n_javascript(self, request): """ Displays the i18n JavaScript that the Django admin requires. - + This takes into account the USE_I18N setting. If it's set to False, the generated JavaScript will be leaner and faster. """ @@ -203,23 +219,23 @@ class AdminSite(object): else: from django.views.i18n import null_javascript_catalog as javascript_catalog return javascript_catalog(request, packages='django.conf') - + def logout(self, request): """ Logs out the user for the given HttpRequest. - + This should *not* assume the user is already logged in. """ from django.contrib.auth.views import logout return logout(request) logout = never_cache(logout) - + def login(self, request): """ Displays the login form for the given HttpRequest. """ from django.contrib.auth.models import User - + # If this isn't already the login page, display it. if not request.POST.has_key(LOGIN_FORM_KEY): if request.POST: @@ -227,14 +243,14 @@ class AdminSite(object): else: 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 self.display_login_form(request, message) else: request.session.delete_test_cookie() - + # Check the password. username = request.POST.get('username', None) password = request.POST.get('password', None) @@ -254,7 +270,7 @@ class AdminSite(object): else: message = _("Usernames cannot contain the '@' character.") return self.display_login_form(request, message) - + # The user data is correct; log in the user in and continue. else: if user.is_active and user.is_staff: @@ -263,7 +279,7 @@ class AdminSite(object): else: return self.display_login_form(request, ERROR_MESSAGE) login = never_cache(login) - + def index(self, request, extra_context=None): """ Displays the main admin index page, which lists all of the installed @@ -274,14 +290,14 @@ class AdminSite(object): for model, model_admin in self._registry.items(): app_label = model._meta.app_label has_module_perms = user.has_module_perms(app_label) - + if has_module_perms: perms = { 'add': model_admin.has_add_permission(request), 'change': model_admin.has_change_permission(request), 'delete': model_admin.has_delete_permission(request), } - + # Check whether user has any perm for this module. # If so, add the module to the model_list. if True in perms.values(): @@ -299,15 +315,15 @@ class AdminSite(object): 'has_module_perms': has_module_perms, 'models': [model_dict], } - + # Sort the apps alphabetically. app_list = app_dict.values() app_list.sort(lambda x, y: cmp(x['name'], y['name'])) - + # Sort the models alphabetically within each app. for app in app_list: app['models'].sort(lambda x, y: cmp(x['name'], y['name'])) - + context = { 'title': _('Site administration'), 'app_list': app_list, @@ -318,7 +334,7 @@ class AdminSite(object): context_instance=template.RequestContext(request) ) index = never_cache(index) - + def display_login_form(self, request, error_message='', extra_context=None): request.session.set_test_cookie() context = { @@ -331,7 +347,7 @@ class AdminSite(object): return render_to_response(self.login_template or 'admin/login.html', context, context_instance=template.RequestContext(request) ) - + def app_index(self, request, app_label, extra_context=None): user = request.user has_module_perms = user.has_module_perms(app_label) @@ -377,6 +393,81 @@ class AdminSite(object): return render_to_response(self.app_index_template or 'admin/app_index.html', context, context_instance=template.RequestContext(request) ) + + def root(self, request, url): + """ + DEPRECATED. This function is the old way of handling URL resolution, and + is deprecated in favor of real URL resolution -- see ``get_urls()``. + + This function still exists for backwards-compatibility; it will be + removed in Django 1.3. + """ + import warnings + warnings.warn( + "AdminSite.root() is deprecated; use include(admin.site.urls) instead.", + PendingDeprecationWarning + ) + + # + # Again, remember that the following only exists for + # backwards-compatibility. Any new URLs, changes to existing URLs, or + # whatever need to be done up in get_urls(), above! + # + + if request.method == 'GET' and not request.path.endswith('/'): + return http.HttpResponseRedirect(request.path + '/') + + if settings.DEBUG: + self.check_dependencies() + + # Figure out the admin base URL path and stash it for later use + self.root_path = re.sub(re.escape(url) + '$', '', request.path) + + url = url.rstrip('/') # Trim trailing slash, if it exists. + + # The 'logout' view doesn't require that the person is logged in. + if url == 'logout': + return self.logout(request) + + # Check permission to continue or display login form. + if not self.has_permission(request): + return self.login(request) + + if url == '': + return self.index(request) + elif url == 'password_change': + return self.password_change(request) + elif url == 'password_change/done': + return self.password_change_done(request) + elif url == 'jsi18n': + return self.i18n_javascript(request) + # URLs starting with 'r/' are for the "View on site" links. + elif url.startswith('r/'): + from django.contrib.contenttypes.views import shortcut + return shortcut(request, *url.split('/')[1:]) + else: + if '/' in url: + return self.model_page(request, *url.split('/', 2)) + else: + return self.app_index(request, url) + + raise http.Http404('The requested admin page does not exist.') + + def model_page(self, request, app_label, model_name, rest_of_url=None): + """ + DEPRECATED. This is the old way of handling a model view on the admin + site; the new views should use get_urls(), above. + """ + from django.db import models + model = models.get_model(app_label, model_name) + if model is None: + raise http.Http404("App %r, model %r, not found." % (app_label, model_name)) + try: + admin_obj = self._registry[model] + except KeyError: + raise http.Http404("This model exists but has not been registered with the admin site.") + return admin_obj(request, rest_of_url) + model_page = never_cache(model_page) # 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. diff --git a/django/contrib/admin/util.py b/django/contrib/admin/util.py index 0900b4e3d9..4164c8ad9c 100644 --- a/django/contrib/admin/util.py +++ b/django/contrib/admin/util.py @@ -6,7 +6,6 @@ from django.utils.text import capfirst from django.utils.encoding import force_unicode from django.utils.translation import ugettext as _ - def quote(s): """ Ensure that primary key values do not confuse the admin URLs by escaping diff --git a/django/contrib/auth/admin.py b/django/contrib/auth/admin.py index 805ca328e1..c5326b7fae 100644 --- a/django/contrib/auth/admin.py +++ b/django/contrib/auth/admin.py @@ -40,6 +40,12 @@ class UserAdmin(admin.ModelAdmin): if url.endswith('password'): return self.user_change_password(request, url.split('/')[0]) return super(UserAdmin, self).__call__(request, url) + + def get_urls(self): + from django.conf.urls.defaults import patterns + return patterns('', + (r'^(\d+)/password/$', self.admin_site.admin_view(self.user_change_password)) + ) + super(UserAdmin, self).get_urls() def add_view(self, request): # It's an error for a user to have add permission but NOT change diff --git a/docs/intro/tutorial02.txt b/docs/intro/tutorial02.txt index 1144167276..fb6794b46c 100644 --- a/docs/intro/tutorial02.txt +++ b/docs/intro/tutorial02.txt @@ -57,7 +57,7 @@ activate the admin site for your installation, do these three things: # (r'^admin/doc/', include('django.contrib.admindocs.urls')), # Uncomment the next line to enable the admin: - **(r'^admin/(.*)', admin.site.root),** + **(r'^admin/', include(admin.site.urls)),** ) (The bold lines are the ones that needed to be uncommented.) diff --git a/docs/ref/contrib/admin.txt b/docs/ref/contrib/admin.txt index f24dc46bf5..a50aa13da9 100644 --- a/docs/ref/contrib/admin.txt +++ b/docs/ref/contrib/admin.txt @@ -632,6 +632,49 @@ model instance:: instance.save() formset.save_m2m() +``get_urls(self)`` +~~~~~~~~~~~~~~~~~~~ + +The ``get_urls`` method on a ``ModelAdmin`` returns the URLs to be used for +that ModelAdmin in the same way as a URLconf. Therefore you can extend them as +documented in :ref:`topics-http-urls`:: + + class MyModelAdmin(admin.ModelAdmin): + def get_urls(self): + urls = super(MyModelAdmin, self).get_urls() + my_urls = patterns('', + (r'^my_view/$', self.my_view) + ) + return my_urls + urls + +.. note:: + + Notice that the custom patterns are included *before* the regular admin + URLs: the admin URL patterns are very permissive and will match nearly + anything, so you'll usually want to prepend your custom URLs to the built-in + ones. + +Note, however, that the ``self.my_view`` function registered above will *not* +have any permission check done; it'll be accessible to the general public. Since +this is usually not what you want, Django provides a convience wrapper to check +permissions. This wrapper is :meth:`AdminSite.admin_view` (i.e. +``self.admin_site.admin_view`` inside a ``ModelAdmin`` instance); use it like +so:: + + class MyModelAdmin(admin.ModelAdmin): + def get_urls(self): + urls = super(MyModelAdmin, self).get_urls() + my_urls = patterns('', + (r'^my_view/$', self.admin_site.admin_view(self.my_view)) + ) + return my_urls + urls + +Notice the wrapped view in the fifth line above:: + + (r'^my_view/$', self.admin_site.admin_view(self.my_view)) + +This wrapping will protect ``self.my_view`` from unauthorized access. + ``ModelAdmin`` media definitions -------------------------------- @@ -1027,7 +1070,7 @@ In this example, we register the default ``AdminSite`` instance admin.autodiscover() urlpatterns = patterns('', - ('^admin/(.*)', admin.site.root), + ('^admin/', include(admin.site.urls)), ) Above we used ``admin.autodiscover()`` to automatically load the @@ -1041,15 +1084,13 @@ In this example, we register the ``AdminSite`` instance from myproject.admin import admin_site urlpatterns = patterns('', - ('^myadmin/(.*)', admin_site.root), + ('^myadmin/', include(admin_site.urls)), ) There is really no need to use autodiscover when using your own ``AdminSite`` instance since you will likely be importing all the per-app admin.py modules in your ``myproject.admin`` module. -Note that the regular expression in the URLpattern *must* group everything in -the URL that comes after the URL root -- hence the ``(.*)`` in these examples. Multiple admin sites in the same URLconf ---------------------------------------- @@ -1068,6 +1109,29 @@ respectively:: from myproject.admin import basic_site, advanced_site urlpatterns = patterns('', - ('^basic-admin/(.*)', basic_site.root), - ('^advanced-admin/(.*)', advanced_site.root), + ('^basic-admin/', include(basic_site.urls)), + ('^advanced-admin/', include(advanced_site.urls)), ) + +Adding views to admin sites +--------------------------- + +It possible to add additional views to the admin site in the same way one can +add them to ``ModelAdmins``. This by using the ``get_urls()`` method on an +AdminSite in the same way as `described above`__ + +__ `get_urls(self)`_ + +Protecting Custom ``AdminSite`` and ``ModelAdmin`` +-------------------------------------------------- + +By default all the views in the Django admin are protected so that only staff +members can access them. If you add your own views to either a ``ModelAdmin`` +or ``AdminSite`` you should ensure that where necessary they are protected in +the same manner. To do this use the ``admin_perm_test`` decorator provided in +``django.contrib.admin.utils.admin_perm_test``. It can be used in the same way +as the ``login_requied`` decorator. + +.. note:: + The ``admin_perm_test`` decorator can only be used on methods which are on + ``ModelAdmins`` or ``AdminSites``, you cannot use it on arbitrary functions. diff --git a/tests/regressiontests/admin_views/customadmin.py b/tests/regressiontests/admin_views/customadmin.py new file mode 100644 index 0000000000..c812eab98b --- /dev/null +++ b/tests/regressiontests/admin_views/customadmin.py @@ -0,0 +1,30 @@ +""" +A second, custom AdminSite -- see tests.CustomAdminSiteTests. +""" +from django.conf.urls.defaults import patterns +from django.contrib import admin +from django.http import HttpResponse + +import models + +class Admin2(admin.AdminSite): + login_template = 'custom_admin/login.html' + index_template = 'custom_admin/index.html' + + # A custom index view. + def index(self, request, extra_context=None): + return super(Admin2, self).index(request, {'foo': '*bar*'}) + + def get_urls(self): + return patterns('', + (r'^my_view/$', self.admin_view(self.my_view)), + ) + super(Admin2, self).get_urls() + + def my_view(self, request): + return HttpResponse("Django is a magical pony!") + +site = Admin2(name="admin2") + +site.register(models.Article, models.ArticleAdmin) +site.register(models.Section, inlines=[models.ArticleInline]) +site.register(models.Thing, models.ThingAdmin) diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py index 391d1ffa3e..39daf116ab 100644 --- a/tests/regressiontests/admin_views/tests.py +++ b/tests/regressiontests/admin_views/tests.py @@ -14,6 +14,11 @@ from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey class AdminViewBasicTest(TestCase): fixtures = ['admin-views-users.xml', 'admin-views-colors.xml'] + # Store the bit of the URL where the admin is registered as a class + # variable. That way we can test a second AdminSite just by subclassing + # this test case and changing urlbit. + urlbit = 'admin' + def setUp(self): self.client.login(username='super', password='secret') @@ -24,20 +29,20 @@ class AdminViewBasicTest(TestCase): """ If you leave off the trailing slash, app should redirect and add it. """ - request = self.client.get('/test_admin/admin/admin_views/article/add') + request = self.client.get('/test_admin/%s/admin_views/article/add' % self.urlbit) self.assertRedirects(request, - '/test_admin/admin/admin_views/article/add/' + '/test_admin/%s/admin_views/article/add/' % self.urlbit, status_code=301 ) def testBasicAddGet(self): """ A smoke test to ensure GET on the add_view works. """ - response = self.client.get('/test_admin/admin/admin_views/section/add/') + response = self.client.get('/test_admin/%s/admin_views/section/add/' % self.urlbit) self.failUnlessEqual(response.status_code, 200) def testAddWithGETArgs(self): - response = self.client.get('/test_admin/admin/admin_views/section/add/', {'name': 'My Section'}) + response = self.client.get('/test_admin/%s/admin_views/section/add/' % self.urlbit, {'name': 'My Section'}) self.failUnlessEqual(response.status_code, 200) self.failUnless( 'value="My Section"' in response.content, @@ -48,7 +53,7 @@ class AdminViewBasicTest(TestCase): """ A smoke test to ensureGET on the change_view works. """ - response = self.client.get('/test_admin/admin/admin_views/section/1/') + response = self.client.get('/test_admin/%s/admin_views/section/1/' % self.urlbit) self.failUnlessEqual(response.status_code, 200) def testBasicAddPost(self): @@ -61,7 +66,7 @@ class AdminViewBasicTest(TestCase): "article_set-TOTAL_FORMS": u"3", "article_set-INITIAL_FORMS": u"0", } - response = self.client.post('/test_admin/admin/admin_views/section/add/', post_data) + response = self.client.post('/test_admin/%s/admin_views/section/add/' % self.urlbit, post_data) self.failUnlessEqual(response.status_code, 302) # redirect somewhere def testBasicEditPost(self): @@ -106,7 +111,7 @@ class AdminViewBasicTest(TestCase): "article_set-5-date_0": u"", "article_set-5-date_1": u"", } - response = self.client.post('/test_admin/admin/admin_views/section/1/', post_data) + response = self.client.post('/test_admin/%s/admin_views/section/1/' % self.urlbit, post_data) self.failUnlessEqual(response.status_code, 302) # redirect somewhere def testChangeListSortingCallable(self): @@ -114,7 +119,7 @@ class AdminViewBasicTest(TestCase): Ensure we can sort on a list_display field that is a callable (column 2 is callable_year in ArticleAdmin) """ - response = self.client.get('/test_admin/admin/admin_views/article/', {'ot': 'asc', 'o': 2}) + response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 2}) self.failUnlessEqual(response.status_code, 200) self.failUnless( response.content.index('Oldest content') < response.content.index('Middle content') and @@ -127,7 +132,7 @@ class AdminViewBasicTest(TestCase): Ensure we can sort on a list_display field that is a Model method (colunn 3 is 'model_year' in ArticleAdmin) """ - response = self.client.get('/test_admin/admin/admin_views/article/', {'ot': 'dsc', 'o': 3}) + response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'dsc', 'o': 3}) self.failUnlessEqual(response.status_code, 200) self.failUnless( response.content.index('Newest content') < response.content.index('Middle content') and @@ -140,7 +145,7 @@ class AdminViewBasicTest(TestCase): Ensure we can sort on a list_display field that is a ModelAdmin method (colunn 4 is 'modeladmin_year' in ArticleAdmin) """ - response = self.client.get('/test_admin/admin/admin_views/article/', {'ot': 'asc', 'o': 4}) + response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit, {'ot': 'asc', 'o': 4}) self.failUnlessEqual(response.status_code, 200) self.failUnless( response.content.index('Oldest content') < response.content.index('Middle content') and @@ -150,7 +155,7 @@ class AdminViewBasicTest(TestCase): def testLimitedFilter(self): """Ensure admin changelist filters do not contain objects excluded via limit_choices_to.""" - response = self.client.get('/test_admin/admin/admin_views/thing/') + response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit) self.failUnlessEqual(response.status_code, 200) self.failUnless( '
' in response.content, @@ -163,11 +168,30 @@ class AdminViewBasicTest(TestCase): def testIncorrectLookupParameters(self): """Ensure incorrect lookup parameters are handled gracefully.""" - response = self.client.get('/test_admin/admin/admin_views/thing/', {'notarealfield': '5'}) - self.assertRedirects(response, '/test_admin/admin/admin_views/thing/?e=1') - response = self.client.get('/test_admin/admin/admin_views/thing/', {'color__id__exact': 'StringNotInteger!'}) - self.assertRedirects(response, '/test_admin/admin/admin_views/thing/?e=1') - + response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'notarealfield': '5'}) + self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit) + response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'color__id__exact': 'StringNotInteger!'}) + self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit) + +class CustomModelAdminTest(AdminViewBasicTest): + urlbit = "admin2" + + def testCustomAdminSiteLoginTemplate(self): + self.client.logout() + request = self.client.get('/test_admin/admin2/') + self.assertTemplateUsed(request, 'custom_admin/login.html') + self.assert_('Hello from a custom login template' in request.content) + + def testCustomAdminSiteIndexViewAndTemplate(self): + request = self.client.get('/test_admin/admin2/') + self.assertTemplateUsed(request, 'custom_admin/index.html') + self.assert_('Hello from a custom index template *bar*' in request.content) + + def testCustomAdminSiteView(self): + self.client.login(username='super', password='secret') + response = self.client.get('/test_admin/%s/my_view/' % self.urlbit) + self.assert_(response.content == "Django is a magical pony!", response.content) + def get_perm(Model, perm): """Return the permission object, for the Model""" ct = ContentType.objects.get_for_model(Model) @@ -432,44 +456,6 @@ class AdminViewPermissionsTest(TestCase): 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) - - # Finally, using monkey patching check we can inject custom_context arguments in to index - original_index = admin.site.index - def index(*args, **kwargs): - kwargs['extra_context'] = {'foo': '*bar*'} - return original_index(*args, **kwargs) - admin.site.index = index - request = self.client.get('/test_admin/admin/') - self.assertTemplateUsed(request, 'custom_admin/index.html') - self.assert_('Hello from a custom index template *bar*' in request.content) - - self.client.get('/test_admin/admin/logout/') - del admin.site.index # Resets to using the original - 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/regressiontests/admin_views/urls.py b/tests/regressiontests/admin_views/urls.py index 4e5da48e13..f3f1fbd43a 100644 --- a/tests/regressiontests/admin_views/urls.py +++ b/tests/regressiontests/admin_views/urls.py @@ -1,9 +1,11 @@ from django.conf.urls.defaults import * from django.contrib import admin import views +import customadmin urlpatterns = patterns('', (r'^admin/doc/', include('django.contrib.admindocs.urls')), (r'^admin/secure-view/$', views.secure_view), - (r'^admin/(.*)', admin.site.root), + (r'^admin/', include(admin.site.urls)), + (r'^admin2/', include(customadmin.site.urls)), )