Code

Ticket #10061: t10061-r11201.v3.diff

File t10061-r11201.v3.diff, 36.8 KB (added by russellm, 5 years ago)

Third pass - now passing full test suite.

Line 
1diff --git a/django/conf/urls/defaults.py b/django/conf/urls/defaults.py
2index 26cdd3e..572a7b0 100644
3--- a/django/conf/urls/defaults.py
4+++ b/django/conf/urls/defaults.py
5@@ -6,7 +6,16 @@ __all__ = ['handler404', 'handler500', 'include', 'patterns', 'url']
6 handler404 = 'django.views.defaults.page_not_found'
7 handler500 = 'django.views.defaults.server_error'
8 
9-include = lambda urlconf_module: [urlconf_module]
10+def include(arg, namespace=None, app_name=None):
11+    if type(arg) == tuple:
12+        # callable returning a namespace hint
13+        if namespace:
14+            raise ImproperlyConfigured('Cannot override the namespace for a dynamic module that provides a namespace')
15+        urlconf_module, app_name, namespace = arg
16+    else:
17+        # No namespace hint - use manually provided namespace
18+        urlconf_module = arg
19+    return (urlconf_module, app_name, namespace)
20 
21 def patterns(prefix, *args):
22     pattern_list = []
23@@ -19,9 +28,10 @@ def patterns(prefix, *args):
24     return pattern_list
25 
26 def url(regex, view, kwargs=None, name=None, prefix=''):
27-    if type(view) == list:
28+    if type(view) == tuple:
29         # For include(...) processing.
30-        return RegexURLResolver(regex, view[0], kwargs)
31+        urlconf_module, app_name, namespace = view
32+        return RegexURLResolver(regex, urlconf_module, kwargs, app_name=app_name, namespace=namespace)
33     else:
34         if isinstance(view, basestring):
35             if not view:
36diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
37index 8297eca..0545409 100644
38--- a/django/contrib/admin/options.py
39+++ b/django/contrib/admin/options.py
40@@ -226,24 +226,24 @@ class ModelAdmin(BaseModelAdmin):
41                 return self.admin_site.admin_view(view)(*args, **kwargs)
42             return update_wrapper(wrapper, view)
43 
44-        info = self.admin_site.name, self.model._meta.app_label, self.model._meta.module_name
45+        info = self.model._meta.app_label, self.model._meta.module_name
46 
47         urlpatterns = patterns('',
48             url(r'^$',
49                 wrap(self.changelist_view),
50-                name='%sadmin_%s_%s_changelist' % info),
51+                name='%s_%s_changelist' % info),
52             url(r'^add/$',
53                 wrap(self.add_view),
54-                name='%sadmin_%s_%s_add' % info),
55+                name='%s_%s_add' % info),
56             url(r'^(.+)/history/$',
57                 wrap(self.history_view),
58-                name='%sadmin_%s_%s_history' % info),
59+                name='%s_%s_history' % info),
60             url(r'^(.+)/delete/$',
61                 wrap(self.delete_view),
62-                name='%sadmin_%s_%s_delete' % info),
63+                name='%s_%s_delete' % info),
64             url(r'^(.+)/$',
65                 wrap(self.change_view),
66-                name='%sadmin_%s_%s_change' % info),
67+                name='%s_%s_change' % info),
68         )
69         return urlpatterns
70 
71@@ -582,11 +582,12 @@ class ModelAdmin(BaseModelAdmin):
72             'save_on_top': self.save_on_top,
73             'root_path': self.admin_site.root_path,
74         })
75+        context_instance = template.RequestContext(request, app_name=self.admin_site.name)
76         return render_to_response(self.change_form_template or [
77             "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()),
78             "admin/%s/change_form.html" % app_label,
79             "admin/change_form.html"
80-        ], context, context_instance=template.RequestContext(request))
81+        ], context, context_instance=context_instance)
82 
83     def response_add(self, request, obj, post_url_continue='../%s/'):
84         """
85@@ -977,11 +978,12 @@ class ModelAdmin(BaseModelAdmin):
86             'actions_on_bottom': self.actions_on_bottom,
87         }
88         context.update(extra_context or {})
89+        context_instance = template.RequestContext(request, app_name=self.admin_site.name)
90         return render_to_response(self.change_list_template or [
91             'admin/%s/%s/change_list.html' % (app_label, opts.object_name.lower()),
92             'admin/%s/change_list.html' % app_label,
93             'admin/change_list.html'
94-        ], context, context_instance=template.RequestContext(request))
95+        ], context, context_instance=context_instance)
96 
97     def delete_view(self, request, object_id, extra_context=None):
98         "The 'delete' admin view for this model."
99@@ -1032,11 +1034,12 @@ class ModelAdmin(BaseModelAdmin):
100             "app_label": app_label,
101         }
102         context.update(extra_context or {})
103+        context_instance = template.RequestContext(request, app_name=self.admin_site.name)
104         return render_to_response(self.delete_confirmation_template or [
105             "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()),
106             "admin/%s/delete_confirmation.html" % app_label,
107             "admin/delete_confirmation.html"
108-        ], context, context_instance=template.RequestContext(request))
109+        ], context, context_instance=context_instance)
110 
111     def history_view(self, request, object_id, extra_context=None):
112         "The 'history' admin view for this model."
113@@ -1059,11 +1062,12 @@ class ModelAdmin(BaseModelAdmin):
114             'app_label': app_label,
115         }
116         context.update(extra_context or {})
117+        context_instance = template.RequestContext(request, app_name=self.admin_site.name)
118         return render_to_response(self.object_history_template or [
119             "admin/%s/%s/object_history.html" % (app_label, opts.object_name.lower()),
120             "admin/%s/object_history.html" % app_label,
121             "admin/object_history.html"
122-        ], context, context_instance=template.RequestContext(request))
123+        ], context, context_instance=context_instance)
124 
125     #
126     # DEPRECATED methods.
127diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py
128index 6e9ef11..e161516 100644
129--- a/django/contrib/admin/sites.py
130+++ b/django/contrib/admin/sites.py
131@@ -5,6 +5,7 @@ from django.contrib.admin import actions
132 from django.contrib.auth import authenticate, login
133 from django.db.models.base import ModelBase
134 from django.core.exceptions import ImproperlyConfigured
135+from django.core.urlresolvers import reverse
136 from django.shortcuts import render_to_response
137 from django.utils.functional import update_wrapper
138 from django.utils.safestring import mark_safe
139@@ -38,17 +39,14 @@ class AdminSite(object):
140     login_template = None
141     app_index_template = None
142 
143-    def __init__(self, name=None):
144+    def __init__(self, name=None, app_name='admin'):
145         self._registry = {} # model_class class -> admin_class instance
146-        # TODO Root path is used to calculate urls under the old root() method
147-        # in order to maintain backwards compatibility we are leaving that in
148-        # so root_path isn't needed, not sure what to do about this.
149-        self.root_path = 'admin/'
150+        self.root_path = None
151         if name is None:
152-            name = ''
153+            self.name = 'admin'
154         else:
155-            name += '_'
156-        self.name = name
157+            self.name = name
158+        self.app_name = app_name
159         self._actions = {'delete_selected': actions.delete_selected}
160         self._global_actions = self._actions.copy()
161 
162@@ -114,20 +112,20 @@ class AdminSite(object):
163         name = name or action.__name__
164         self._actions[name] = action
165         self._global_actions[name] = action
166-       
167+
168     def disable_action(self, name):
169         """
170         Disable a globally-registered action. Raises KeyError for invalid names.
171         """
172         del self._actions[name]
173-       
174+
175     def get_action(self, name):
176         """
177         Explicitally get a registered global action wheather it's enabled or
178         not. Raises KeyError for invalid names.
179         """
180         return self._global_actions[name]
181-   
182+
183     def actions(self):
184         """
185         Get all the enabled actions as an iterable of (name, func).
186@@ -186,7 +184,6 @@ class AdminSite(object):
187 
188     def get_urls(self):
189         from django.conf.urls.defaults import patterns, url, include
190-
191         def wrap(view):
192             def wrapper(*args, **kwargs):
193                 return self.admin_view(view)(*args, **kwargs)
194@@ -196,24 +193,24 @@ class AdminSite(object):
195         urlpatterns = patterns('',
196             url(r'^$',
197                 wrap(self.index),
198-                name='%sadmin_index' % self.name),
199+                name='index'),
200             url(r'^logout/$',
201                 wrap(self.logout),
202-                name='%sadmin_logout'),
203+                name='logout'),
204             url(r'^password_change/$',
205                 wrap(self.password_change),
206-                name='%sadmin_password_change' % self.name),
207+                name='password_change'),
208             url(r'^password_change/done/$',
209                 wrap(self.password_change_done),
210-                name='%sadmin_password_change_done' % self.name),
211+                name='password_change_done'),
212             url(r'^jsi18n/$',
213                 wrap(self.i18n_javascript),
214-                name='%sadmin_jsi18n' % self.name),
215+                name='jsi18n'),
216             url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$',
217                 'django.views.defaults.shortcut'),
218             url(r'^(?P<app_label>\w+)/$',
219                 wrap(self.app_index),
220-                name='%sadmin_app_list' % self.name),
221+                name='app_list')
222         )
223 
224         # Add in each model's views.
225@@ -222,7 +219,7 @@ class AdminSite(object):
226                 url(r'^%s/%s/' % (model._meta.app_label, model._meta.module_name),
227                     include(model_admin.urls))
228             )
229-        return urlpatterns
230+        return urlpatterns, self.app_name, self.name
231 
232     def urls(self):
233         return self.get_urls()
234@@ -233,8 +230,11 @@ class AdminSite(object):
235         Handles the "change password" task -- both form display and validation.
236         """
237         from django.contrib.auth.views import password_change
238-        return password_change(request,
239-            post_change_redirect='%spassword_change/done/' % self.root_path)
240+        if self.root_path is not None:
241+            url = '%spassword_change/done/' % self.root_path
242+        else:
243+            url = reverse('%s:password_change_done' % self.name)
244+        return password_change(request, post_change_redirect=url)
245 
246     def password_change_done(self, request):
247         """
248@@ -362,8 +362,9 @@ class AdminSite(object):
249             'root_path': self.root_path,
250         }
251         context.update(extra_context or {})
252+        context_instance = template.RequestContext(request, app_name=self.name)
253         return render_to_response(self.index_template or 'admin/index.html', context,
254-            context_instance=template.RequestContext(request)
255+            context_instance=context_instance
256         )
257     index = never_cache(index)
258 
259@@ -376,8 +377,9 @@ class AdminSite(object):
260             'root_path': self.root_path,
261         }
262         context.update(extra_context or {})
263+        context_instance = template.RequestContext(request, app_name=self.name)
264         return render_to_response(self.login_template or 'admin/login.html', context,
265-            context_instance=template.RequestContext(request)
266+            context_instance=context_instance
267         )
268 
269     def app_index(self, request, app_label, extra_context=None):
270@@ -419,9 +421,10 @@ class AdminSite(object):
271             'root_path': self.root_path,
272         }
273         context.update(extra_context or {})
274+        context_instance = template.RequestContext(request, app_name=self.name)
275         return render_to_response(self.app_index_template or ('admin/%s/app_index.html' % app_label,
276             'admin/app_index.html'), context,
277-            context_instance=template.RequestContext(request)
278+            context_instance=context_instance
279         )
280 
281     def root(self, request, url):
282diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html
283index 8cab439..9525728 100644
284--- a/django/contrib/admin/templates/admin/base.html
285+++ b/django/contrib/admin/templates/admin/base.html
286@@ -23,7 +23,30 @@
287         {% block branding %}{% endblock %}
288         </div>
289         {% if user.is_authenticated and user.is_staff %}
290-        <div id="user-tools">{% trans 'Welcome,' %} <strong>{% firstof user.first_name user.username %}</strong>. {% block userlinks %}{% url django-admindocs-docroot as docsroot %}{% if docsroot %}<a href="{{ docsroot }}">{% trans 'Documentation' %}</a> / {% endif %}<a href="{{ root_path }}password_change/">{% trans 'Change password' %}</a> / <a href="{{ root_path }}logout/">{% trans 'Log out' %}</a>{% endblock %}</div>
291+        <div id="user-tools">
292+            {% trans 'Welcome,' %}
293+            <strong>{% firstof user.first_name user.username %}</strong>.
294+            {% block userlinks %}
295+                {% url django-admindocs-docroot as docsroot %}
296+                {% if docsroot %}
297+                    <a href="{{ docsroot }}">{% trans 'Documentation' %}</a> /
298+                {% endif %}
299+                {% url admin:password_change as password_change_url %}
300+                {% if password_change_url %}
301+                    <a href="{{ password_change_url }}">
302+                {% else %}
303+                    <a href="{{ root_path }}password_change/">
304+                {% endif %}
305+                {% trans 'Change password' %}</a> /
306+                {% url admin:logout as logout_url %}
307+                {% if logout_url %}
308+                    <a href="{{ logout_url }}">
309+                {% else %}
310+                    <a href="{{ root_path }}logout/">
311+                {% endif %}
312+                {% trans 'Log out' %}</a>
313+            {% endblock %}
314+        </div>
315         {% endif %}
316         {% block nav-global %}{% endblock %}
317     </div>
318diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py
319index 7ae5e64..778cf4a 100644
320--- a/django/contrib/admin/widgets.py
321+++ b/django/contrib/admin/widgets.py
322@@ -125,7 +125,7 @@ class ForeignKeyRawIdWidget(forms.TextInput):
323         if value:
324             output.append(self.label_for_value(value))
325         return mark_safe(u''.join(output))
326-   
327+
328     def base_url_parameters(self):
329         params = {}
330         if self.rel.limit_choices_to:
331@@ -137,14 +137,14 @@ class ForeignKeyRawIdWidget(forms.TextInput):
332                     v = str(v)
333                 items.append((k, v))
334             params.update(dict(items))
335-        return params   
336-   
337+        return params
338+
339     def url_parameters(self):
340         from django.contrib.admin.views.main import TO_FIELD_VAR
341         params = self.base_url_parameters()
342         params.update({TO_FIELD_VAR: self.rel.get_related_field().name})
343         return params
344-           
345+
346     def label_for_value(self, value):
347         key = self.rel.get_related_field().name
348         obj = self.rel.to._default_manager.get(**{key: value})
349@@ -165,10 +165,10 @@ class ManyToManyRawIdWidget(ForeignKeyRawIdWidget):
350         else:
351             value = ''
352         return super(ManyToManyRawIdWidget, self).render(name, value, attrs)
353-   
354+
355     def url_parameters(self):
356         return self.base_url_parameters()
357-   
358+
359     def label_for_value(self, value):
360         return ''
361 
362@@ -222,8 +222,7 @@ class RelatedFieldWidgetWrapper(forms.Widget):
363         rel_to = self.rel.to
364         info = (rel_to._meta.app_label, rel_to._meta.object_name.lower())
365         try:
366-            related_info = (self.admin_site.name,) + info
367-            related_url = reverse('%sadmin_%s_%s_add' % related_info)
368+            related_url = reverse('admin:%s_%s_add' % info, app_name=self.admin_site.name)
369         except NoReverseMatch:
370             related_url = '../../../%s/%s/add/' % info
371         self.widget.choices = self.choices
372diff --git a/django/contrib/admindocs/templates/admin_doc/index.html b/django/contrib/admindocs/templates/admin_doc/index.html
373index 242fc73..a8b21c3 100644
374--- a/django/contrib/admindocs/templates/admin_doc/index.html
375+++ b/django/contrib/admindocs/templates/admin_doc/index.html
376@@ -1,6 +1,6 @@
377 {% extends "admin/base_site.html" %}
378 {% load i18n %}
379-{% block breadcrumbs %}<div class="breadcrumbs"><a href="../">Home</a> &rsaquo; Documentation</div>{% endblock %}
380+{% block breadcrumbs %}<div class="breadcrumbs"><a href="{{ root_path }}">Home</a> &rsaquo; Documentation</div>{% endblock %}
381 {% block title %}Documentation{% endblock %}
382 
383 {% block content %}
384diff --git a/django/contrib/admindocs/views.py b/django/contrib/admindocs/views.py
385index 4f22fe0..063aac9 100644
386--- a/django/contrib/admindocs/views.py
387+++ b/django/contrib/admindocs/views.py
388@@ -22,11 +22,7 @@ class GenericSite(object):
389     name = 'my site'
390 
391 def get_root_path():
392-    from django.contrib import admin
393-    try:
394-        return urlresolvers.reverse(admin.site.root, args=[''])
395-    except urlresolvers.NoReverseMatch:
396-        return getattr(settings, "ADMIN_SITE_ROOT_URL", "/admin/")
397+    return urlresolvers.reverse('admin:index')
398 
399 def doc_index(request):
400     if not utils.docutils_is_available:
401@@ -179,7 +175,7 @@ model_index = staff_member_required(model_index)
402 def model_detail(request, app_label, model_name):
403     if not utils.docutils_is_available:
404         return missing_docutils_page(request)
405-       
406+
407     # Get the model class.
408     try:
409         app_mod = models.get_app(app_label)
410diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py
411index 10e97bb..823cd77 100644
412--- a/django/core/urlresolvers.py
413+++ b/django/core/urlresolvers.py
414@@ -139,7 +139,7 @@ class RegexURLPattern(object):
415     callback = property(_get_callback)
416 
417 class RegexURLResolver(object):
418-    def __init__(self, regex, urlconf_name, default_kwargs=None):
419+    def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
420         # regex is a string representing a regular expression.
421         # urlconf_name is a string representing the module containing URLconfs.
422         self.regex = re.compile(regex, re.UNICODE)
423@@ -148,19 +148,29 @@ class RegexURLResolver(object):
424             self._urlconf_module = self.urlconf_name
425         self.callback = None
426         self.default_kwargs = default_kwargs or {}
427-        self._reverse_dict = MultiValueDict()
428+        self.namespace = namespace
429+        self.app_name = app_name
430+        self._reverse_dict = None
431+        self._namespace_dict = None
432+        self._app_dict = None
433 
434     def __repr__(self):
435-        return '<%s %s %s>' % (self.__class__.__name__, self.urlconf_name, self.regex.pattern)
436-
437-    def _get_reverse_dict(self):
438-        if not self._reverse_dict:
439-            lookups = MultiValueDict()
440-            for pattern in reversed(self.url_patterns):
441-                p_pattern = pattern.regex.pattern
442-                if p_pattern.startswith('^'):
443-                    p_pattern = p_pattern[1:]
444-                if isinstance(pattern, RegexURLResolver):
445+        return '<%s %s (%s:%s) %s>' % (self.__class__.__name__, self.urlconf_name, self.app_name, self.namespace, self.regex.pattern)
446+
447+    def _populate(self):
448+        lookups = MultiValueDict()
449+        namespaces = {}
450+        apps = {}
451+        for pattern in reversed(self.url_patterns):
452+            p_pattern = pattern.regex.pattern
453+            if p_pattern.startswith('^'):
454+                p_pattern = p_pattern[1:]
455+            if isinstance(pattern, RegexURLResolver):
456+                if pattern.namespace:
457+                    namespaces[pattern.namespace] = (p_pattern, pattern)
458+                    if pattern.app_name:
459+                        apps.setdefault(pattern.app_name, []).append(pattern.namespace)
460+                else:
461                     parent = normalize(pattern.regex.pattern)
462                     for name in pattern.reverse_dict:
463                         for matches, pat in pattern.reverse_dict.getlist(name):
464@@ -168,14 +178,36 @@ class RegexURLResolver(object):
465                             for piece, p_args in parent:
466                                 new_matches.extend([(piece + suffix, p_args + args) for (suffix, args) in matches])
467                             lookups.appendlist(name, (new_matches, p_pattern + pat))
468-                else:
469-                    bits = normalize(p_pattern)
470-                    lookups.appendlist(pattern.callback, (bits, p_pattern))
471-                    lookups.appendlist(pattern.name, (bits, p_pattern))
472-            self._reverse_dict = lookups
473+                    for namespace, (prefix, sub_pattern) in pattern.namespace_dict.items():
474+                        namespaces[namespace] = (p_pattern + prefix, sub_pattern)
475+                    for app_name, namespace_list in pattern.app_dict.items():
476+                        apps.setdefault(app_name, []).extend(namespace_list)
477+            else:
478+                bits = normalize(p_pattern)
479+                lookups.appendlist(pattern.callback, (bits, p_pattern))
480+                lookups.appendlist(pattern.name, (bits, p_pattern))
481+        self._reverse_dict = lookups
482+        self._namespace_dict = namespaces
483+        self._app_dict = apps
484+
485+    def _get_reverse_dict(self):
486+        if self._reverse_dict is None:
487+            self._populate()
488         return self._reverse_dict
489     reverse_dict = property(_get_reverse_dict)
490 
491+    def _get_namespace_dict(self):
492+        if self._namespace_dict is None:
493+            self._populate()
494+        return self._namespace_dict
495+    namespace_dict = property(_get_namespace_dict)
496+
497+    def _get_app_dict(self):
498+        if self._app_dict is None:
499+            self._populate()
500+        return self._app_dict
501+    app_dict = property(_get_app_dict)
502+
503     def resolve(self, path):
504         tried = []
505         match = self.regex.search(path)
506@@ -261,12 +293,51 @@ class RegexURLResolver(object):
507 def resolve(path, urlconf=None):
508     return get_resolver(urlconf).resolve(path)
509 
510-def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None):
511+def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, app_name=None):
512+    resolver = get_resolver(urlconf)
513     args = args or []
514     kwargs = kwargs or {}
515+
516     if prefix is None:
517         prefix = get_script_prefix()
518-    return iri_to_uri(u'%s%s' % (prefix, get_resolver(urlconf).reverse(viewname,
519+
520+    if not isinstance(viewname, basestring):
521+        view = viewname
522+    else:
523+        parts = viewname.split(':')
524+        parts.reverse()
525+        view = parts[0]
526+        path = parts[1:]
527+
528+        resolved_path = []
529+        while path:
530+            ns = path.pop()
531+
532+            # Lookup the name to see if it could be an app identifier
533+            try:
534+                app_list = resolver.app_dict[ns]
535+                # Yes! Path part matches an app in the current Resolver
536+                if app_name and app_name in app_list:
537+                    # If we are reversing for a particular app, use that namespace
538+                    ns = app_name
539+                elif ns not in app_list:
540+                    # The name isn't shared by one of the instances (i.e., the default)
541+                    # so just pick the first instance as the default.
542+                    ns = app_list[0]
543+            except KeyError:
544+                pass
545+
546+            try:
547+                extra, resolver = resolver.namespace_dict[ns]
548+                resolved_path.append(ns)
549+                prefix = prefix + extra
550+            except KeyError, key:
551+                if resolved_path:
552+                    raise NoReverseMatch("%s is not a registered namespace inside '%s'" % (key, ':'.join(resolved_path)))
553+                else:
554+                    raise NoReverseMatch("%s is not a registered namespace" % key)
555+
556+    return iri_to_uri(u'%s%s' % (prefix, resolver.reverse(view,
557             *args, **kwargs)))
558 
559 def clear_url_caches():
560diff --git a/django/template/context.py b/django/template/context.py
561index 0ccb5fa..e79af4d 100644
562--- a/django/template/context.py
563+++ b/django/template/context.py
564@@ -9,10 +9,11 @@ class ContextPopException(Exception):
565 
566 class Context(object):
567     "A stack container for variable context"
568-    def __init__(self, dict_=None, autoescape=True):
569+    def __init__(self, dict_=None, autoescape=True, app_name=None):
570         dict_ = dict_ or {}
571         self.dicts = [dict_]
572         self.autoescape = autoescape
573+        self.app_name = app_name
574 
575     def __repr__(self):
576         return repr(self.dicts)
577@@ -96,8 +97,8 @@ class RequestContext(Context):
578     Additional processors can be specified as a list of callables
579     using the "processors" keyword argument.
580     """
581-    def __init__(self, request, dict=None, processors=None):
582-        Context.__init__(self, dict)
583+    def __init__(self, request, dict=None, processors=None, app_name=None):
584+        Context.__init__(self, dict, app_name=app_name)
585         if processors is None:
586             processors = ()
587         else:
588diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
589index 7d91cd6..a61e965 100644
590--- a/django/template/defaulttags.py
591+++ b/django/template/defaulttags.py
592@@ -367,17 +367,17 @@ class URLNode(Node):
593         # {% url ... as var %} construct in which cause return nothing.
594         url = ''
595         try:
596-            url = reverse(self.view_name, args=args, kwargs=kwargs)
597+            url = reverse(self.view_name, args=args, kwargs=kwargs, app_name=context.app_name)
598         except NoReverseMatch, e:
599             if settings.SETTINGS_MODULE:
600                 project_name = settings.SETTINGS_MODULE.split('.')[0]
601                 try:
602                     url = reverse(project_name + '.' + self.view_name,
603-                              args=args, kwargs=kwargs)
604+                              args=args, kwargs=kwargs, app_name=context.app_name)
605                 except NoReverseMatch:
606                     if self.asvar is None:
607                         # Re-raise the original exception, not the one with
608-                        # the path relative to the project. This makes a
609+                        # the path relative to the project. This makes a
610                         # better error message.
611                         raise e
612             else:
613diff --git a/tests/regressiontests/admin_views/customadmin.py b/tests/regressiontests/admin_views/customadmin.py
614index 70e87eb..4aa781e 100644
615--- a/tests/regressiontests/admin_views/customadmin.py
616+++ b/tests/regressiontests/admin_views/customadmin.py
617@@ -10,19 +10,20 @@ import models
618 class Admin2(admin.AdminSite):
619     login_template = 'custom_admin/login.html'
620     index_template = 'custom_admin/index.html'
621-   
622+
623     # A custom index view.
624     def index(self, request, extra_context=None):
625         return super(Admin2, self).index(request, {'foo': '*bar*'})
626-   
627+
628     def get_urls(self):
629+        base_patterns, app_name, name = super(Admin2, self).get_urls()
630         return patterns('',
631             (r'^my_view/$', self.admin_view(self.my_view)),
632-        ) + super(Admin2, self).get_urls()
633-   
634+        ) + base_patterns, app_name, name
635+
636     def my_view(self, request):
637         return HttpResponse("Django is a magical pony!")
638-   
639+
640 site = Admin2(name="admin2")
641 
642 site.register(models.Article, models.ArticleAdmin)
643diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py
644index 99168fd..3fc145b 100644
645--- a/tests/regressiontests/admin_views/tests.py
646+++ b/tests/regressiontests/admin_views/tests.py
647@@ -204,6 +204,11 @@ class AdminViewBasicTest(TestCase):
648         response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'color__id__exact': 'StringNotInteger!'})
649         self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit)
650 
651+    def testLogoutAndPasswordChangeURLs(self):
652+        response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit)
653+        self.failIf('<a href="/test_admin/%s/logout/">' % self.urlbit not in response.content)
654+        self.failIf('<a href="/test_admin/%s/password_change/">' % self.urlbit not in response.content)
655+
656     def testNamedGroupFieldChoicesChangeList(self):
657         """
658         Ensures the admin changelist shows correct values in the relevant column
659diff --git a/tests/regressiontests/admin_widgets/widgetadmin.py b/tests/regressiontests/admin_widgets/widgetadmin.py
660index bd68954..9257c30 100644
661--- a/tests/regressiontests/admin_widgets/widgetadmin.py
662+++ b/tests/regressiontests/admin_widgets/widgetadmin.py
663@@ -19,7 +19,7 @@ class CarTireAdmin(admin.ModelAdmin):
664             return db_field.formfield(**kwargs)
665         return super(CarTireAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
666 
667-site = WidgetAdmin()
668+site = WidgetAdmin(name='widget-admin')
669 
670 site.register(models.User)
671 site.register(models.Car, CarAdmin)
672diff --git a/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py b/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py
673new file mode 100644
674index 0000000..0731906
675--- /dev/null
676+++ b/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py
677@@ -0,0 +1,13 @@
678+from django.conf.urls.defaults import *
679+from namespace_urls import URLObject
680+
681+testobj3 = URLObject('testapp', 'test-ns3')
682+
683+urlpatterns = patterns('regressiontests.urlpatterns_reverse.views',
684+    url(r'^normal/$', 'empty_view', name='inc-normal-view'),
685+    url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='inc-normal-view'),
686+
687+    (r'^test3/', include(testobj3.urls)),
688+    (r'^ns-included3/', include('regressiontests.urlpatterns_reverse.included_urls', namespace='inc-ns3')),
689+)
690+
691diff --git a/tests/regressiontests/urlpatterns_reverse/namespace_urls.py b/tests/regressiontests/urlpatterns_reverse/namespace_urls.py
692new file mode 100644
693index 0000000..27cc7f7
694--- /dev/null
695+++ b/tests/regressiontests/urlpatterns_reverse/namespace_urls.py
696@@ -0,0 +1,38 @@
697+from django.conf.urls.defaults import *
698+
699+class URLObject(object):
700+    def __init__(self, app_name, namespace):
701+        self.app_name = app_name
702+        self.namespace = namespace
703+
704+    def urls(self):
705+        return patterns('',
706+            url(r'^inner/$', 'empty_view', name='urlobject-view'),
707+            url(r'^inner/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='urlobject-view'),
708+        ), self.app_name, self.namespace
709+    urls = property(urls)
710+
711+testobj1 = URLObject('testapp', 'test-ns1')
712+testobj2 = URLObject('testapp', 'test-ns2')
713+default_testobj = URLObject('testapp', 'testapp')
714+
715+otherobj1 = URLObject('nodefault', 'other-ns1')
716+otherobj2 = URLObject('nodefault', 'other-ns2')
717+
718+urlpatterns = patterns('regressiontests.urlpatterns_reverse.views',
719+    url(r'^normal/$', 'empty_view', name='normal-view'),
720+    url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='normal-view'),
721+
722+    (r'^test1/', include(testobj1.urls)),
723+    (r'^test2/', include(testobj2.urls)),
724+    (r'^default/', include(default_testobj.urls)),
725+
726+    (r'^other1/', include(otherobj1.urls)),
727+    (r'^other2/', include(otherobj2.urls)),
728+
729+    (r'^ns-included1/', include('regressiontests.urlpatterns_reverse.included_namespace_urls', namespace='inc-ns1')),
730+    (r'^ns-included2/', include('regressiontests.urlpatterns_reverse.included_namespace_urls', namespace='inc-ns2')),
731+
732+    (r'^included/', include('regressiontests.urlpatterns_reverse.included_namespace_urls')),
733+
734+)
735diff --git a/tests/regressiontests/urlpatterns_reverse/tests.py b/tests/regressiontests/urlpatterns_reverse/tests.py
736index 9def6b2..68e34eb 100644
737--- a/tests/regressiontests/urlpatterns_reverse/tests.py
738+++ b/tests/regressiontests/urlpatterns_reverse/tests.py
739@@ -158,4 +158,84 @@ class ReverseShortcutTests(TestCase):
740         res = redirect('/foo/')
741         self.assertEqual(res['Location'], '/foo/')
742         res = redirect('http://example.com/')
743-        self.assertEqual(res['Location'], 'http://example.com/')
744\ No newline at end of file
745+        self.assertEqual(res['Location'], 'http://example.com/')
746+
747+
748+class NamespaceTests(TestCase):
749+    urls = 'regressiontests.urlpatterns_reverse.namespace_urls'
750+
751+    def test_ambiguous_object(self):
752+        "Names deployed via dynamic URL objects that require namespaces can't be resolved"
753+        self.assertRaises(NoReverseMatch, reverse, 'urlobject-view')
754+        self.assertRaises(NoReverseMatch, reverse, 'urlobject-view', args=[37,42])
755+        self.assertRaises(NoReverseMatch, reverse, 'urlobject-view', kwargs={'arg1':42, 'arg2':37})
756+
757+    def test_ambiguous_urlpattern(self):
758+        "Names deployed via dynamic URL objects that require namespaces can't be resolved"
759+        self.assertRaises(NoReverseMatch, reverse, 'inner-nothing')
760+        self.assertRaises(NoReverseMatch, reverse, 'inner-nothing', args=[37,42])
761+        self.assertRaises(NoReverseMatch, reverse, 'inner-nothing', kwargs={'arg1':42, 'arg2':37})
762+
763+    def test_non_existent_namespace(self):
764+        "Non-existent namespaces raise errors"
765+        self.assertRaises(NoReverseMatch, reverse, 'blahblah:urlobject-view')
766+        self.assertRaises(NoReverseMatch, reverse, 'test-ns1:blahblah:urlobject-view')
767+
768+    def test_normal_name(self):
769+        "Normal lookups work as expected"
770+        self.assertEquals('/normal/', reverse('normal-view'))
771+        self.assertEquals('/normal/37/42/', reverse('normal-view', args=[37,42]))
772+        self.assertEquals('/normal/42/37/', reverse('normal-view', kwargs={'arg1':42, 'arg2':37}))
773+
774+    def test_simple_included_name(self):
775+        "Normal lookups work on names included from other patterns"
776+        self.assertEquals('/included/normal/', reverse('inc-normal-view'))
777+        self.assertEquals('/included/normal/37/42/', reverse('inc-normal-view', args=[37,42]))
778+        self.assertEquals('/included/normal/42/37/', reverse('inc-normal-view', kwargs={'arg1':42, 'arg2':37}))
779+
780+    def test_namespace_object(self):
781+        "Dynamic URL objects can be found using a namespace"
782+        self.assertEquals('/test1/inner/', reverse('test-ns1:urlobject-view'))
783+        self.assertEquals('/test1/inner/37/42/', reverse('test-ns1:urlobject-view', args=[37,42]))
784+        self.assertEquals('/test1/inner/42/37/', reverse('test-ns1:urlobject-view', kwargs={'arg1':42, 'arg2':37}))
785+
786+    def test_embedded_namespace_object(self):
787+        "Namespaces can be installed anywhere in the URL pattern tree"
788+        self.assertEquals('/included/test3/inner/', reverse('test-ns3:urlobject-view'))
789+        self.assertEquals('/included/test3/inner/37/42/', reverse('test-ns3:urlobject-view', args=[37,42]))
790+        self.assertEquals('/included/test3/inner/42/37/', reverse('test-ns3:urlobject-view', kwargs={'arg1':42, 'arg2':37}))
791+
792+    def test_namespace_pattern(self):
793+        "Namespaces can be applied to include()'d urlpatterns"
794+        self.assertEquals('/ns-included1/normal/', reverse('inc-ns1:inc-normal-view'))
795+        self.assertEquals('/ns-included1/normal/37/42/', reverse('inc-ns1:inc-normal-view', args=[37,42]))
796+        self.assertEquals('/ns-included1/normal/42/37/', reverse('inc-ns1:inc-normal-view', kwargs={'arg1':42, 'arg2':37}))
797+
798+    def test_multiple_namespace_pattern(self):
799+        "Namespaces can be embedded"
800+        self.assertEquals('/ns-included1/test3/inner/', reverse('inc-ns1:test-ns3:urlobject-view'))
801+        self.assertEquals('/ns-included1/test3/inner/37/42/', reverse('inc-ns1:test-ns3:urlobject-view', args=[37,42]))
802+        self.assertEquals('/ns-included1/test3/inner/42/37/', reverse('inc-ns1:test-ns3:urlobject-view', kwargs={'arg1':42, 'arg2':37}))
803+
804+    def test_app_lookup_object(self):
805+        "A default application namespace can be used for lookup"
806+        self.assertEquals('/default/inner/', reverse('testapp:urlobject-view'))
807+        self.assertEquals('/default/inner/37/42/', reverse('testapp:urlobject-view', args=[37,42]))
808+        self.assertEquals('/default/inner/42/37/', reverse('testapp:urlobject-view', kwargs={'arg1':42, 'arg2':37}))
809+
810+    def test_app_lookup_object_with_default(self):
811+        "A default application namespace is sensitive to the 'current' app can be used for lookup"
812+        self.assertEquals('/included/test3/inner/', reverse('testapp:urlobject-view', app_name='test-ns3'))
813+        self.assertEquals('/included/test3/inner/37/42/', reverse('testapp:urlobject-view', args=[37,42], app_name='test-ns3'))
814+        self.assertEquals('/included/test3/inner/42/37/', reverse('testapp:urlobject-view', kwargs={'arg1':42, 'arg2':37}, app_name='test-ns3'))
815+
816+    def test_app_lookup_object_without_default(self):
817+        "An application namespace without a default is sensitive to the 'current' app can be used for lookup"
818+        self.assertEquals('/other2/inner/', reverse('nodefault:urlobject-view'))
819+        self.assertEquals('/other2/inner/37/42/', reverse('nodefault:urlobject-view', args=[37,42]))
820+        self.assertEquals('/other2/inner/42/37/', reverse('nodefault:urlobject-view', kwargs={'arg1':42, 'arg2':37}))
821+
822+        self.assertEquals('/other1/inner/', reverse('nodefault:urlobject-view', app_name='other-ns1'))
823+        self.assertEquals('/other1/inner/37/42/', reverse('nodefault:urlobject-view', args=[37,42], app_name='other-ns1'))
824+        self.assertEquals('/other1/inner/42/37/', reverse('nodefault:urlobject-view', kwargs={'arg1':42, 'arg2':37}, app_name='other-ns1'))
825+