Commit 1f84630c authored by Jacob Kaplan-Moss's avatar Jacob Kaplan-Moss
Browse files

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
parent 6c4e5f0f
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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)),
)
+145 −96
Original line number Diff line number Diff line
@@ -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
@@ -183,18 +184,38 @@ class ModelAdmin(BaseModelAdmin):
            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
@@ -545,7 +566,7 @@ class ModelAdmin(BaseModelAdmin):
        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
@@ -659,7 +680,7 @@ class ModelAdmin(BaseModelAdmin):
        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
@@ -674,7 +695,7 @@ class ModelAdmin(BaseModelAdmin):
        
        # Populate deleted_objects, a data structure of all related objects that
        # 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()
        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"
        ], 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.
+188 −97
Original line number Diff line number Diff line
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'
@@ -34,8 +33,17 @@ class AdminSite(object):
    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):
        """
@@ -115,66 +123,74 @@ class AdminSite(object):
        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):
        """
        Handles main URL routing for the admin app.

        `url` is the remainder of the URL -- e.g. 'comments/comment/'.
    def admin_view(self, view):
        """
        if request.method == 'GET' and not request.path.endswith('/'):
            return http.HttpResponseRedirect(request.path + '/')

        if settings.DEBUG:
            self.check_dependencies()
        Decorator to create an "admin view attached to this ``AdminSite``. This
        wraps the view and provides permission checking by calling
        ``self.has_permission``.
        
        # Figure out the admin base URL path and stash it for later use
        self.root_path = re.sub(re.escape(url) + '$', '', request.path)
        You'll want to use this from within ``AdminSite.get_urls()``:
            
        url = url.rstrip('/') # Trim trailing slash, if it exists.
            class MyAdminSite(AdminSite):
                
        # The 'logout' view doesn't require that the person is logged in.
        if url == 'logout':
            return self.logout(request)
                def get_urls(self):
                    from django.conf.urls.defaults import patterns, url
                    
        # Check permission to continue or display login form.
                    urls = super(MyAdminSite, self).get_urls()
                    urls += patterns('',
                        url(r'^my_view/$', self.protected_view(some_view))
                    )
                    return urls
        """
        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<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),
        )
        
        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.')
        # 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 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 urls(self):
        return self.get_urls()
    urls = property(urls)
        
    def password_change(self, request):
        """
@@ -378,6 +394,81 @@ class AdminSite(object):
            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.
site = AdminSite()
+0 −1
Original line number Diff line number Diff line
@@ -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
+6 −0
Original line number Diff line number Diff line
@@ -41,6 +41,12 @@ class UserAdmin(admin.ModelAdmin):
            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
        # permission for users. If we allowed such users to add users, they
Loading