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
This commit is contained in:
parent
6c4e5f0f0e
commit
1f84630c87
|
@ -13,5 +13,5 @@ urlpatterns = patterns('',
|
||||||
# (r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
# (r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
||||||
|
|
||||||
# Uncomment the next line to enable the admin:
|
# Uncomment the next line to enable the admin:
|
||||||
# (r'^admin/(.*)', admin.site.root),
|
# (r'^admin/', include(admin.site.urls)),
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,11 +5,12 @@ from django.forms.models import BaseInlineFormSet
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.admin import widgets
|
from django.contrib.admin import widgets
|
||||||
from django.contrib.admin import helpers
|
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.core.exceptions import PermissionDenied
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
||||||
from django.shortcuts import get_object_or_404, render_to_response
|
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.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.text import capfirst, get_text_list
|
from django.utils.text import capfirst, get_text_list
|
||||||
|
@ -183,18 +184,38 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
self.inline_instances.append(inline_instance)
|
self.inline_instances.append(inline_instance)
|
||||||
super(ModelAdmin, self).__init__()
|
super(ModelAdmin, self).__init__()
|
||||||
|
|
||||||
def __call__(self, request, url):
|
def get_urls(self):
|
||||||
# Delegate to the appropriate method, based on the URL.
|
from django.conf.urls.defaults import patterns, url
|
||||||
if url is None:
|
|
||||||
return self.changelist_view(request)
|
def wrap(view):
|
||||||
elif url == "add":
|
def wrapper(*args, **kwargs):
|
||||||
return self.add_view(request)
|
return self.admin_site.admin_view(view)(*args, **kwargs)
|
||||||
elif url.endswith('/history'):
|
return update_wrapper(wrapper, view)
|
||||||
return self.history_view(request, unquote(url[:-8]))
|
|
||||||
elif url.endswith('/delete'):
|
info = self.admin_site.name, self.model._meta.app_label, self.model._meta.module_name
|
||||||
return self.delete_view(request, unquote(url[:-7]))
|
|
||||||
else:
|
urlpatterns = patterns('',
|
||||||
return self.change_view(request, unquote(url))
|
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):
|
def _media(self):
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -545,7 +566,7 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
opts = model._meta
|
opts = model._meta
|
||||||
|
|
||||||
try:
|
try:
|
||||||
obj = model._default_manager.get(pk=object_id)
|
obj = model._default_manager.get(pk=unquote(object_id))
|
||||||
except model.DoesNotExist:
|
except model.DoesNotExist:
|
||||||
# Don't raise Http404 just yet, because we haven't checked
|
# Don't raise Http404 just yet, because we haven't checked
|
||||||
# permissions yet. We don't want an unauthenticated user to be able
|
# permissions yet. We don't want an unauthenticated user to be able
|
||||||
|
@ -659,7 +680,7 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
app_label = opts.app_label
|
app_label = opts.app_label
|
||||||
|
|
||||||
try:
|
try:
|
||||||
obj = self.model._default_manager.get(pk=object_id)
|
obj = self.model._default_manager.get(pk=unquote(object_id))
|
||||||
except self.model.DoesNotExist:
|
except self.model.DoesNotExist:
|
||||||
# Don't raise Http404 just yet, because we haven't checked
|
# Don't raise Http404 just yet, because we haven't checked
|
||||||
# permissions yet. We don't want an unauthenticated user to be able
|
# permissions yet. We don't want an unauthenticated user to be able
|
||||||
|
@ -674,7 +695,7 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
|
|
||||||
# Populate deleted_objects, a data structure of all related objects that
|
# Populate deleted_objects, a data structure of all related objects that
|
||||||
# will also be deleted.
|
# will also be deleted.
|
||||||
deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), quote(object_id), escape(obj))), []]
|
deleted_objects = [mark_safe(u'%s: <a href="../../%s/">%s</a>' % (escape(force_unicode(capfirst(opts.verbose_name))), object_id, escape(obj))), []]
|
||||||
perms_needed = set()
|
perms_needed = set()
|
||||||
get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site)
|
get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site)
|
||||||
|
|
||||||
|
@ -735,6 +756,34 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
"admin/object_history.html"
|
"admin/object_history.html"
|
||||||
], context, context_instance=template.RequestContext(request))
|
], 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):
|
class InlineModelAdmin(BaseModelAdmin):
|
||||||
"""
|
"""
|
||||||
Options for inline editing of ``model`` instances.
|
Options for inline editing of ``model`` instances.
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import base64
|
|
||||||
import re
|
import re
|
||||||
from django import http, template
|
from django import http, template
|
||||||
from django.contrib.admin import ModelAdmin
|
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.db.models.base import ModelBase
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.shortcuts import render_to_response
|
from django.shortcuts import render_to_response
|
||||||
|
from django.utils.functional import update_wrapper
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.text import capfirst
|
from django.utils.text import capfirst
|
||||||
from django.utils.translation import ugettext_lazy, ugettext as _
|
from django.utils.translation import ugettext_lazy, ugettext as _
|
||||||
from django.views.decorators.cache import never_cache
|
from django.views.decorators.cache import never_cache
|
||||||
from django.conf import settings
|
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.")
|
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'
|
LOGIN_FORM_KEY = 'this_is_the_login_form'
|
||||||
|
@ -34,8 +33,17 @@ class AdminSite(object):
|
||||||
login_template = None
|
login_template = None
|
||||||
app_index_template = None
|
app_index_template = None
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, name=None):
|
||||||
self._registry = {} # model_class class -> admin_class instance
|
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):
|
def register(self, model_or_iterable, admin_class=None, **options):
|
||||||
"""
|
"""
|
||||||
|
@ -115,66 +123,74 @@ class AdminSite(object):
|
||||||
if 'django.core.context_processors.auth' not in settings.TEMPLATE_CONTEXT_PROCESSORS:
|
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.")
|
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.
|
Decorator to create an "admin view attached to this ``AdminSite``. This
|
||||||
|
wraps the view and provides permission checking by calling
|
||||||
|
``self.has_permission``.
|
||||||
|
|
||||||
`url` is the remainder of the URL -- e.g. 'comments/comment/'.
|
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('/'):
|
def inner(request, *args, **kwargs):
|
||||||
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):
|
if not self.has_permission(request):
|
||||||
return self.login(request)
|
return self.login(request)
|
||||||
|
return view(request, *args, **kwargs)
|
||||||
|
return update_wrapper(inner, view)
|
||||||
|
|
||||||
if url == '':
|
def get_urls(self):
|
||||||
return self.index(request)
|
from django.conf.urls.defaults import patterns, url, include
|
||||||
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 wrap(view):
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
return self.admin_view(view)(*args, **kwargs)
|
||||||
|
return update_wrapper(wrapper, view)
|
||||||
|
|
||||||
def model_page(self, request, app_label, model_name, rest_of_url=None):
|
# Admin-site-wide views.
|
||||||
"""
|
urlpatterns = patterns('',
|
||||||
Handles the model-specific functionality of the admin site, delegating
|
url(r'^$',
|
||||||
to the appropriate ModelAdmin class.
|
wrap(self.index),
|
||||||
"""
|
name='%sadmin_index' % self.name),
|
||||||
from django.db import models
|
url(r'^logout/$',
|
||||||
model = models.get_model(app_label, model_name)
|
wrap(self.logout),
|
||||||
if model is None:
|
name='%sadmin_logout'),
|
||||||
raise http.Http404("App %r, model %r, not found." % (app_label, model_name))
|
url(r'^password_change/$',
|
||||||
try:
|
wrap(self.password_change),
|
||||||
admin_obj = self._registry[model]
|
name='%sadmin_password_change' % self.name),
|
||||||
except KeyError:
|
url(r'^password_change/done/$',
|
||||||
raise http.Http404("This model exists but has not been registered with the admin site.")
|
wrap(self.password_change_done),
|
||||||
return admin_obj(request, rest_of_url)
|
name='%sadmin_password_change_done' % self.name),
|
||||||
model_page = never_cache(model_page)
|
url(r'^jsi18n/$',
|
||||||
|
wrap(self.i18n_javascript),
|
||||||
|
name='%sadmin_jsi18n' % self.name),
|
||||||
|
url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$',
|
||||||
|
'django.views.defaults.shortcut'),
|
||||||
|
url(r'^(?P<app_label>\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):
|
def password_change(self, request):
|
||||||
"""
|
"""
|
||||||
|
@ -378,6 +394,81 @@ class AdminSite(object):
|
||||||
context_instance=template.RequestContext(request)
|
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.
|
# 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.
|
# You can instantiate AdminSite in your own code to create a custom admin site.
|
||||||
site = AdminSite()
|
site = AdminSite()
|
||||||
|
|
|
@ -6,7 +6,6 @@ from django.utils.text import capfirst
|
||||||
from django.utils.encoding import force_unicode
|
from django.utils.encoding import force_unicode
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
|
||||||
def quote(s):
|
def quote(s):
|
||||||
"""
|
"""
|
||||||
Ensure that primary key values do not confuse the admin URLs by escaping
|
Ensure that primary key values do not confuse the admin URLs by escaping
|
||||||
|
|
|
@ -41,6 +41,12 @@ class UserAdmin(admin.ModelAdmin):
|
||||||
return self.user_change_password(request, url.split('/')[0])
|
return self.user_change_password(request, url.split('/')[0])
|
||||||
return super(UserAdmin, self).__call__(request, url)
|
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):
|
def add_view(self, request):
|
||||||
# It's an error for a user to have add permission but NOT change
|
# It's an error for a user to have add permission but NOT change
|
||||||
# permission for users. If we allowed such users to add users, they
|
# permission for users. If we allowed such users to add users, they
|
||||||
|
|
|
@ -57,7 +57,7 @@ activate the admin site for your installation, do these three things:
|
||||||
# (r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
# (r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
||||||
|
|
||||||
# Uncomment the next line to enable the admin:
|
# 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.)
|
(The bold lines are the ones that needed to be uncommented.)
|
||||||
|
|
|
@ -632,6 +632,49 @@ model instance::
|
||||||
instance.save()
|
instance.save()
|
||||||
formset.save_m2m()
|
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
|
``ModelAdmin`` media definitions
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
||||||
|
@ -1027,7 +1070,7 @@ In this example, we register the default ``AdminSite`` instance
|
||||||
admin.autodiscover()
|
admin.autodiscover()
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
('^admin/(.*)', admin.site.root),
|
('^admin/', include(admin.site.urls)),
|
||||||
)
|
)
|
||||||
|
|
||||||
Above we used ``admin.autodiscover()`` to automatically load the
|
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
|
from myproject.admin import admin_site
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
('^myadmin/(.*)', admin_site.root),
|
('^myadmin/', include(admin_site.urls)),
|
||||||
)
|
)
|
||||||
|
|
||||||
There is really no need to use autodiscover when using your own ``AdminSite``
|
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
|
instance since you will likely be importing all the per-app admin.py modules
|
||||||
in your ``myproject.admin`` module.
|
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
|
Multiple admin sites in the same URLconf
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
@ -1068,6 +1109,29 @@ respectively::
|
||||||
from myproject.admin import basic_site, advanced_site
|
from myproject.admin import basic_site, advanced_site
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
('^basic-admin/(.*)', basic_site.root),
|
('^basic-admin/', include(basic_site.urls)),
|
||||||
('^advanced-admin/(.*)', advanced_site.root),
|
('^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.
|
||||||
|
|
|
@ -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)
|
|
@ -14,6 +14,11 @@ from models import Article, CustomArticle, Section, ModelWithStringPrimaryKey
|
||||||
class AdminViewBasicTest(TestCase):
|
class AdminViewBasicTest(TestCase):
|
||||||
fixtures = ['admin-views-users.xml', 'admin-views-colors.xml']
|
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):
|
def setUp(self):
|
||||||
self.client.login(username='super', password='secret')
|
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.
|
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,
|
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):
|
def testBasicAddGet(self):
|
||||||
"""
|
"""
|
||||||
A smoke test to ensure GET on the add_view works.
|
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)
|
self.failUnlessEqual(response.status_code, 200)
|
||||||
|
|
||||||
def testAddWithGETArgs(self):
|
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.failUnlessEqual(response.status_code, 200)
|
||||||
self.failUnless(
|
self.failUnless(
|
||||||
'value="My Section"' in response.content,
|
'value="My Section"' in response.content,
|
||||||
|
@ -48,7 +53,7 @@ class AdminViewBasicTest(TestCase):
|
||||||
"""
|
"""
|
||||||
A smoke test to ensureGET on the change_view works.
|
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)
|
self.failUnlessEqual(response.status_code, 200)
|
||||||
|
|
||||||
def testBasicAddPost(self):
|
def testBasicAddPost(self):
|
||||||
|
@ -61,7 +66,7 @@ class AdminViewBasicTest(TestCase):
|
||||||
"article_set-TOTAL_FORMS": u"3",
|
"article_set-TOTAL_FORMS": u"3",
|
||||||
"article_set-INITIAL_FORMS": u"0",
|
"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
|
self.failUnlessEqual(response.status_code, 302) # redirect somewhere
|
||||||
|
|
||||||
def testBasicEditPost(self):
|
def testBasicEditPost(self):
|
||||||
|
@ -106,7 +111,7 @@ class AdminViewBasicTest(TestCase):
|
||||||
"article_set-5-date_0": u"",
|
"article_set-5-date_0": u"",
|
||||||
"article_set-5-date_1": 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
|
self.failUnlessEqual(response.status_code, 302) # redirect somewhere
|
||||||
|
|
||||||
def testChangeListSortingCallable(self):
|
def testChangeListSortingCallable(self):
|
||||||
|
@ -114,7 +119,7 @@ class AdminViewBasicTest(TestCase):
|
||||||
Ensure we can sort on a list_display field that is a callable
|
Ensure we can sort on a list_display field that is a callable
|
||||||
(column 2 is callable_year in ArticleAdmin)
|
(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.failUnlessEqual(response.status_code, 200)
|
||||||
self.failUnless(
|
self.failUnless(
|
||||||
response.content.index('Oldest content') < response.content.index('Middle content') and
|
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
|
Ensure we can sort on a list_display field that is a Model method
|
||||||
(colunn 3 is 'model_year' in ArticleAdmin)
|
(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.failUnlessEqual(response.status_code, 200)
|
||||||
self.failUnless(
|
self.failUnless(
|
||||||
response.content.index('Newest content') < response.content.index('Middle content') and
|
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
|
Ensure we can sort on a list_display field that is a ModelAdmin method
|
||||||
(colunn 4 is 'modeladmin_year' in ArticleAdmin)
|
(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.failUnlessEqual(response.status_code, 200)
|
||||||
self.failUnless(
|
self.failUnless(
|
||||||
response.content.index('Oldest content') < response.content.index('Middle content') and
|
response.content.index('Oldest content') < response.content.index('Middle content') and
|
||||||
|
@ -150,7 +155,7 @@ class AdminViewBasicTest(TestCase):
|
||||||
|
|
||||||
def testLimitedFilter(self):
|
def testLimitedFilter(self):
|
||||||
"""Ensure admin changelist filters do not contain objects excluded via limit_choices_to."""
|
"""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.failUnlessEqual(response.status_code, 200)
|
||||||
self.failUnless(
|
self.failUnless(
|
||||||
'<div id="changelist-filter">' in response.content,
|
'<div id="changelist-filter">' in response.content,
|
||||||
|
@ -163,10 +168,29 @@ class AdminViewBasicTest(TestCase):
|
||||||
|
|
||||||
def testIncorrectLookupParameters(self):
|
def testIncorrectLookupParameters(self):
|
||||||
"""Ensure incorrect lookup parameters are handled gracefully."""
|
"""Ensure incorrect lookup parameters are handled gracefully."""
|
||||||
response = self.client.get('/test_admin/admin/admin_views/thing/', {'notarealfield': '5'})
|
response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'notarealfield': '5'})
|
||||||
self.assertRedirects(response, '/test_admin/admin/admin_views/thing/?e=1')
|
self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit)
|
||||||
response = self.client.get('/test_admin/admin/admin_views/thing/', {'color__id__exact': 'StringNotInteger!'})
|
response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'color__id__exact': 'StringNotInteger!'})
|
||||||
self.assertRedirects(response, '/test_admin/admin/admin_views/thing/?e=1')
|
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):
|
def get_perm(Model, perm):
|
||||||
"""Return the permission object, for the Model"""
|
"""Return the permission object, for the Model"""
|
||||||
|
@ -432,44 +456,6 @@ class AdminViewPermissionsTest(TestCase):
|
||||||
|
|
||||||
self.client.get('/test_admin/admin/logout/')
|
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):
|
def testDeleteView(self):
|
||||||
"""Delete view should restrict access and actually delete items."""
|
"""Delete view should restrict access and actually delete items."""
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
from django.conf.urls.defaults import *
|
from django.conf.urls.defaults import *
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
import views
|
import views
|
||||||
|
import customadmin
|
||||||
|
|
||||||
urlpatterns = patterns('',
|
urlpatterns = patterns('',
|
||||||
(r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
(r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
||||||
(r'^admin/secure-view/$', views.secure_view),
|
(r'^admin/secure-view/$', views.secure_view),
|
||||||
(r'^admin/(.*)', admin.site.root),
|
(r'^admin/', include(admin.site.urls)),
|
||||||
|
(r'^admin2/', include(customadmin.site.urls)),
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue