Index: django/core/urlresolvers.py
===================================================================
--- django/core/urlresolvers.py	(revision 8111)
+++ django/core/urlresolvers.py	(working copy)
@@ -211,6 +211,8 @@
         # urlconf_name is a string representing the module containing urlconfs.
         self.regex = re.compile(regex, re.UNICODE)
         self.urlconf_name = urlconf_name
+        if not isinstance(self.urlconf_name, basestring):
+            self._urlconf_module = self.urlconf_name
         self.callback = None
         self.default_kwargs = default_kwargs or {}
         self._reverse_dict = {}
Index: django/contrib/admin/options.py
===================================================================
--- django/contrib/admin/options.py	(revision 8111)
+++ django/contrib/admin/options.py	(working copy)
@@ -4,7 +4,7 @@
 from django.forms.models import BaseInlineFormset
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.admin import widgets
-from django.contrib.admin.util import quote, unquote, get_deleted_objects
+from django.contrib.admin.util import quote, unquote, get_deleted_objects, admin_perm_test
 from django.core.exceptions import ImproperlyConfigured, PermissionDenied
 from django.db import models, transaction
 from django.http import Http404, HttpResponse, HttpResponseRedirect
@@ -15,6 +15,7 @@
 from django.utils.translation import ugettext as _
 from django.utils.encoding import force_unicode
 import sets
+import types
 
 HORIZONTAL, VERTICAL = 1, 2
 # returns the <ul> class for a given radio_admin field
@@ -237,19 +238,22 @@
                 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 _get_urls(self):
+        from django.conf.urls.defaults import patterns, url
+        urls_module = types.ModuleType('%s.urls' % self.__class__.__name__)
+        info = self.model._meta.app_label, self.model._meta.module_name
+        urlpatterns = patterns('',
+            url(r'^$', self.changelist_view, name='admin_%s_%s_changelist' % info),
+            url(r'^add/$', self.add_view, name='admin_%s_%s_add' % info),
+            url(r'^(.+)/history/$', self.history_view, name='admin_%s_%s_history' % info),
+            url(r'^(.+)/delete/$', self.delete_view, name='admin_%s_%s_delete' % info),
+            url(r'^(.+)/$', self.change_view, name='admin_%s_%s_change' % info),
+        )
+        urls_module.urlpatterns = urlpatterns
+        return urls_module
+    urls = property(_get_urls)
 
-        # Delegate to the appropriate method, based on the URL.
-        if url is None:
-            return self.changelist_view(request)
-        elif url.endswith('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 _media(self):
         from django.conf import settings
 
@@ -450,13 +454,13 @@
             'content_type_id': ContentType.objects.get_for_model(self.model).id,
             'save_as': self.save_as,
             'save_on_top': self.save_on_top,
-            'root_path': self.admin_site.root_path,
         })
         return render_to_response(self.change_form_template or [
             "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()),
             "admin/%s/change_form.html" % app_label,
             "admin/change_form.html"
         ], context, context_instance=template.RequestContext(request))
+    
 
     def add_view(self, request, form_url='', extra_context=None):
         "The 'add' admin view for this model."
@@ -510,11 +514,11 @@
             'media': mark_safe(media),
             'inline_admin_formsets': inline_admin_formsets,
             'errors': AdminErrorList(form, inline_formsets),
-            'root_path': self.admin_site.root_path,
         }
         context.update(extra_context or {})
         return self.render_change_form(request, context, add=True)
-
+    add_view = admin_perm_test(add_view)
+    
     def change_view(self, request, object_id, extra_context=None):
         "The 'change' admin view for this model."
         model = self.model
@@ -522,7 +526,7 @@
         app_label = opts.app_label
 
         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
@@ -573,10 +577,10 @@
             'media': mark_safe(media),
             'inline_admin_formsets': inline_admin_formsets,
             'errors': AdminErrorList(form, inline_formsets),
-            'root_path': self.admin_site.root_path,
         }
         context.update(extra_context or {})
         return self.render_change_form(request, context, change=True, obj=obj)
+    change_view = admin_perm_test(change_view)
 
     def changelist_view(self, request, extra_context=None):
         "The 'change list' admin view for this model."
@@ -603,7 +607,6 @@
             'is_popup': cl.is_popup,
             'cl': cl,
             'has_add_permission': self.has_add_permission(request),
-            'root_path': self.admin_site.root_path,
         }
         context.update(extra_context or {})
         return render_to_response(self.change_list_template or [
@@ -611,6 +614,7 @@
             'admin/%s/change_list.html' % app_label,
             'admin/change_list.html'
         ], context, context_instance=template.RequestContext(request))
+    changelist_view = admin_perm_test(changelist_view)
 
     def delete_view(self, request, object_id, extra_context=None):
         "The 'delete' admin view for this model."
@@ -619,7 +623,7 @@
         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
@@ -634,7 +638,7 @@
 
         # 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 = sets.Set()
         get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1, self.admin_site)
 
@@ -656,7 +660,6 @@
             "deleted_objects": deleted_objects,
             "perms_lacking": perms_needed,
             "opts": opts,
-            "root_path": self.admin_site.root_path,
         }
         context.update(extra_context or {})
         return render_to_response(self.delete_confirmation_template or [
@@ -664,6 +667,7 @@
             "admin/%s/delete_confirmation.html" % app_label,
             "admin/delete_confirmation.html"
         ], context, context_instance=template.RequestContext(request))
+    delete_view = admin_perm_test(delete_view)
 
     def history_view(self, request, object_id, extra_context=None):
         "The 'history' admin view for this model."
@@ -681,7 +685,6 @@
             'action_list': action_list,
             'module_name': capfirst(opts.verbose_name_plural),
             'object': obj,
-            'root_path': self.admin_site.root_path,
         }
         context.update(extra_context or {})
         return render_to_response(self.object_history_template or [
@@ -689,6 +692,7 @@
             "admin/%s/object_history.html" % opts.app_label,
             "admin/object_history.html"
         ], context, context_instance=template.RequestContext(request))
+    history_view = admin_perm_test(history_view)
 
 class InlineModelAdmin(BaseModelAdmin):
     """
Index: django/contrib/admin/util.py
===================================================================
--- django/contrib/admin/util.py	(revision 8111)
+++ django/contrib/admin/util.py	(working copy)
@@ -6,7 +6,6 @@
 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
@@ -137,3 +136,15 @@
             p = u'%s.%s' % (related.opts.app_label, related.opts.get_change_permission())
             if not user.has_perm(p):
                 perms_needed.add(related.opts.verbose_name)
+
+def admin_perm_test(func):
+    def inner(admin_site_or_modeladmin, request, *args, **kwargs):
+        if hasattr(admin_site_or_modeladmin, 'has_permission'):
+            admin_site = admin_site_or_modeladmin
+        else:
+            admin_site = admin_site_or_modeladmin.admin_site
+        if not admin_site.has_permission(request):
+            return admin_site.login(request)
+        # User has right permisssions show the view
+        return func(admin_site_or_modeladmin, request, *args, **kwargs)
+    return inner
Index: django/contrib/admin/sites.py
===================================================================
--- django/contrib/admin/sites.py	(revision 8111)
+++ django/contrib/admin/sites.py	(working copy)
@@ -1,5 +1,6 @@
 from django import http, template
 from django.contrib.admin import ModelAdmin
+from django.contrib.admin.util import admin_perm_test
 from django.contrib.auth import authenticate, login
 from django.core.exceptions import ImproperlyConfigured
 from django.db.models.base import ModelBase
@@ -13,13 +14,15 @@
 import cPickle as pickle
 import datetime
 import md5
+import types
 import re
 
 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'
 
 USER_CHANGE_PASSWORD_URL_RE = re.compile('auth/user/(\d+)/password')
 
+LOGIN_FORM_KEY = 'this_is_the_login_form'
+
 class AlreadyRegistered(Exception):
     pass
 
@@ -116,72 +119,34 @@
         """
         return request.user.is_authenticated() and request.user.is_staff
 
-    def root(self, request, url):
-        """ 
-        Handles main URL routing for the admin app.
+    def _get_urls(self):
+        from django.conf.urls.defaults import patterns, url, include
+        from django.core.urlresolvers import RegexURLResolver
+        urls_module = types.ModuleType('%s.urls' % self.__class__.__name__)
+        urlpatterns = patterns('',
+            url(r'^$', self.index, name='admin_index'),
+            url(r'^logout/$', self.logout, name='admin_logout'),
+            url(r'^password_change/$', self.password_change, name='admin_password_change'),
+            url(r'^password_change_done/$', self.password_change_done, name='admin_password_change_done'),
+            url(r'^jsi18n/$', self.i18n_javascript, name='admin_jsi18n'),
+            url(r'^auth/user/(\d+)/password/$', self.user_change_password, name='admin_change_password'),
+            url('^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$', 'django.views.defaults.shortcut')
+        )
+        for model, model_admin in self._registry.iteritems():
+            urlpatterns += patterns('',
+                url('^%s/%s/' % (model._meta.app_label, model._meta.module_name), include(model_admin.urls))
+            )
+        urls_module.urlpatterns = urlpatterns
+        return urls_module
+    urls = property(_get_urls)
 
-        `url` is the remainder of the URL -- e.g. 'comments/comment/'.
-        """
-        if request.method == 'GET' and not request.path.endswith('/'):
-            return http.HttpResponseRedirect(request.path + '/')
-        
-        # 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 "show in web" links
-        elif url.startswith('r/'):
-            from django.views.defaults import shortcut
-            return shortcut(request, *url.split('/')[1:])
-        else:
-            match = USER_CHANGE_PASSWORD_URL_RE.match(url)
-            if match:
-                return self.user_change_password(request, match.group(1))
-                
-            if '/' in url:
-                return self.model_page(request, *url.split('/', 2))
-
-        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 password_change(self, request):
         """
         Handles the "change password" task -- both form display and validation.
         """
         from django.contrib.auth.views import password_change
         return password_change(request)
+    passoword_change = admin_perm_test(password_change)
 
     def password_change_done(self, request):
         """
@@ -189,6 +154,7 @@
         """
         from django.contrib.auth.views import password_change_done
         return password_change_done(request)
+    password_change_done = admin_perm_test(password_change_done)
 
     def user_change_password(self, request, id):
         """
@@ -196,6 +162,7 @@
         """
         from django.contrib.auth.views import user_change_password
         return user_change_password(request, id)
+    user_change_password = admin_perm_test(user_change_password)
 
     def i18n_javascript(self, request):
         """
@@ -210,6 +177,7 @@
         else:
             from django.views.i18n import null_javascript_catalog as javascript_catalog
         return javascript_catalog(request, packages='django.conf')
+    i18n_javascript = admin_perm_test(i18n_javascript)
 
     def logout(self, request):
         """
@@ -226,7 +194,6 @@
         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:
@@ -266,13 +233,15 @@
                 login(request, user)
                 if request.POST.has_key('post_data'):
                     post_data = _decode_post_data(request.POST['post_data'])
+                    request.session.delete_test_cookie()
                     if post_data and not post_data.has_key(LOGIN_FORM_KEY):
                         # overwrite request.POST with the saved post_data, and continue
                         request.POST = post_data
                         request.user = user
-                        return self.root(request, request.path.split(self.root_path)[-1])
+                        from django.core.urlresolvers import resolve
+                        ret_view, vargs, vkwargs = resolve(request.path)
+                        return ret_view(request, *vargs, **vkwargs)
                     else:
-                        request.session.delete_test_cookie()
                         return http.HttpResponseRedirect(request.path)
             else:
                 return self.display_login_form(request, ERROR_MESSAGE)
@@ -324,13 +293,12 @@
         context = {
             'title': _('Site administration'),
             'app_list': app_list,
-            'root_path': self.root_path,
         }
         context.update(extra_context or {})
         return render_to_response(self.index_template or 'admin/index.html', context, 
             context_instance=template.RequestContext(request)
         )
-    index = never_cache(index)
+    index = never_cache(admin_perm_test(index))
 
     def display_login_form(self, request, error_message='', extra_context=None):
         request.session.set_test_cookie()
@@ -342,20 +310,18 @@
             post_data = _encode_post_data(request.POST)
         else:
             post_data = _encode_post_data({})
-        
+
         context = {
             'title': _('Log in'),
             'app_path': request.path,
             'post_data': post_data,
             'error_message': error_message,
-            'root_path': self.root_path,
         }
         context.update(extra_context or {})
         return render_to_response(self.login_template or 'admin/login.html', context,
             context_instance=template.RequestContext(request)
         )
 
-
 # This global object represents the default admin site, for the common case.
 # You can instantiate AdminSite in your own code to create a custom admin site.
 site = AdminSite()
Index: django/contrib/auth/admin.py
===================================================================
--- django/contrib/auth/admin.py	(revision 8111)
+++ django/contrib/auth/admin.py	(working copy)
@@ -58,7 +58,6 @@
             'opts': User._meta,
             'save_as': False,
             'username_help_text': User._meta.get_field('username').help_text,
-            'root_path': self.admin_site.root_path,
         }, context_instance=template.RequestContext(request))
 
 admin.site.register(Group, GroupAdmin)
Index: tests/regressiontests/admin_views/tests.py
===================================================================
--- tests/regressiontests/admin_views/tests.py	(revision 8111)
+++ tests/regressiontests/admin_views/tests.py	(working copy)
@@ -86,7 +86,7 @@
             '/test_admin/admin/admin_views/article/add'
         )
         self.assertRedirects(request,
-            '/test_admin/admin/admin_views/article/add/'
+            '/test_admin/admin/admin_views/article/add/', status_code=301
         )
 
     def testLogin(self):
Index: tests/regressiontests/admin_views/urls.py
===================================================================
--- tests/regressiontests/admin_views/urls.py	(revision 8111)
+++ tests/regressiontests/admin_views/urls.py	(working copy)
@@ -3,5 +3,5 @@
 
 urlpatterns = patterns('',
     (r'^admin/doc/', include('django.contrib.admindocs.urls')),
-    (r'^admin/(.*)', admin.site.root),
+    (r'^admin/', include(admin.site.urls)),
 )
