Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #10061 -- Added namespacing for named URLs - most importantly, …

…for the admin site, where the absence of this facility was causing problems. Thanks to the many people who contributed to and helped review this patch.

This change is backwards incompatible for anyone that is using the named URLs
introduced in [9739]. Any usage of the old admin_XXX names need to be modified
to use the new namespaced format; in many cases this will be as simple as a
search & replace for "admin_" -> "admin:". See the docs for more details on
the new URL names, and the namespace resolution strategy.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@11250 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 8d48eaa064c88533be5082e3f45638fbd48491d8 1 parent 9fd19c0
Russell Keith-Magee authored July 16, 2009
16  django/conf/urls/defaults.py
@@ -6,7 +6,16 @@
6 6
 handler404 = 'django.views.defaults.page_not_found'
7 7
 handler500 = 'django.views.defaults.server_error'
8 8
 
9  
-include = lambda urlconf_module: [urlconf_module]
  9
+def include(arg, namespace=None, app_name=None):
  10
+    if isinstance(arg, tuple):
  11
+        # callable returning a namespace hint
  12
+        if namespace:
  13
+            raise ImproperlyConfigured('Cannot override the namespace for a dynamic module that provides a namespace')
  14
+        urlconf_module, app_name, namespace = arg
  15
+    else:
  16
+        # No namespace hint - use manually provided namespace
  17
+        urlconf_module = arg
  18
+    return (urlconf_module, app_name, namespace)
10 19
 
11 20
 def patterns(prefix, *args):
12 21
     pattern_list = []
@@ -19,9 +28,10 @@ def patterns(prefix, *args):
19 28
     return pattern_list
20 29
 
21 30
 def url(regex, view, kwargs=None, name=None, prefix=''):
22  
-    if type(view) == list:
  31
+    if isinstance(view, (list,tuple)):
23 32
         # For include(...) processing.
24  
-        return RegexURLResolver(regex, view[0], kwargs)
  33
+        urlconf_module, app_name, namespace = view
  34
+        return RegexURLResolver(regex, urlconf_module, kwargs, app_name=app_name, namespace=namespace)
25 35
     else:
26 36
         if isinstance(view, basestring):
27 37
             if not view:
24  django/contrib/admin/options.py
@@ -226,24 +226,24 @@ def wrapper(*args, **kwargs):
226 226
                 return self.admin_site.admin_view(view)(*args, **kwargs)
227 227
             return update_wrapper(wrapper, view)
228 228
 
229  
-        info = self.admin_site.name, self.model._meta.app_label, self.model._meta.module_name
  229
+        info = self.model._meta.app_label, self.model._meta.module_name
230 230
 
231 231
         urlpatterns = patterns('',
232 232
             url(r'^$',
233 233
                 wrap(self.changelist_view),
234  
-                name='%sadmin_%s_%s_changelist' % info),
  234
+                name='%s_%s_changelist' % info),
235 235
             url(r'^add/$',
236 236
                 wrap(self.add_view),
237  
-                name='%sadmin_%s_%s_add' % info),
  237
+                name='%s_%s_add' % info),
238 238
             url(r'^(.+)/history/$',
239 239
                 wrap(self.history_view),
240  
-                name='%sadmin_%s_%s_history' % info),
  240
+                name='%s_%s_history' % info),
241 241
             url(r'^(.+)/delete/$',
242 242
                 wrap(self.delete_view),
243  
-                name='%sadmin_%s_%s_delete' % info),
  243
+                name='%s_%s_delete' % info),
244 244
             url(r'^(.+)/$',
245 245
                 wrap(self.change_view),
246  
-                name='%sadmin_%s_%s_change' % info),
  246
+                name='%s_%s_change' % info),
247 247
         )
248 248
         return urlpatterns
249 249
 
@@ -582,11 +582,12 @@ def render_change_form(self, request, context, add=False, change=False, form_url
582 582
             'save_on_top': self.save_on_top,
583 583
             'root_path': self.admin_site.root_path,
584 584
         })
  585
+        context_instance = template.RequestContext(request, current_app=self.admin_site.name)
585 586
         return render_to_response(self.change_form_template or [
586 587
             "admin/%s/%s/change_form.html" % (app_label, opts.object_name.lower()),
587 588
             "admin/%s/change_form.html" % app_label,
588 589
             "admin/change_form.html"
589  
-        ], context, context_instance=template.RequestContext(request))
  590
+        ], context, context_instance=context_instance)
590 591
 
591 592
     def response_add(self, request, obj, post_url_continue='../%s/'):
592 593
         """
@@ -977,11 +978,12 @@ def changelist_view(self, request, extra_context=None):
977 978
             'actions_on_bottom': self.actions_on_bottom,
978 979
         }
979 980
         context.update(extra_context or {})
  981
+        context_instance = template.RequestContext(request, current_app=self.admin_site.name)
980 982
         return render_to_response(self.change_list_template or [
981 983
             'admin/%s/%s/change_list.html' % (app_label, opts.object_name.lower()),
982 984
             'admin/%s/change_list.html' % app_label,
983 985
             'admin/change_list.html'
984  
-        ], context, context_instance=template.RequestContext(request))
  986
+        ], context, context_instance=context_instance)
985 987
 
986 988
     def delete_view(self, request, object_id, extra_context=None):
987 989
         "The 'delete' admin view for this model."
@@ -1032,11 +1034,12 @@ def delete_view(self, request, object_id, extra_context=None):
1032 1034
             "app_label": app_label,
1033 1035
         }
1034 1036
         context.update(extra_context or {})
  1037
+        context_instance = template.RequestContext(request, current_app=self.admin_site.name)
1035 1038
         return render_to_response(self.delete_confirmation_template or [
1036 1039
             "admin/%s/%s/delete_confirmation.html" % (app_label, opts.object_name.lower()),
1037 1040
             "admin/%s/delete_confirmation.html" % app_label,
1038 1041
             "admin/delete_confirmation.html"
1039  
-        ], context, context_instance=template.RequestContext(request))
  1042
+        ], context, context_instance=context_instance)
1040 1043
 
1041 1044
     def history_view(self, request, object_id, extra_context=None):
1042 1045
         "The 'history' admin view for this model."
@@ -1059,11 +1062,12 @@ def history_view(self, request, object_id, extra_context=None):
1059 1062
             'app_label': app_label,
1060 1063
         }
1061 1064
         context.update(extra_context or {})
  1065
+        context_instance = template.RequestContext(request, current_app=self.admin_site.name)
1062 1066
         return render_to_response(self.object_history_template or [
1063 1067
             "admin/%s/%s/object_history.html" % (app_label, opts.object_name.lower()),
1064 1068
             "admin/%s/object_history.html" % app_label,
1065 1069
             "admin/object_history.html"
1066  
-        ], context, context_instance=template.RequestContext(request))
  1070
+        ], context, context_instance=context_instance)
1067 1071
 
1068 1072
     #
1069 1073
     # DEPRECATED methods.
44  django/contrib/admin/sites.py
@@ -5,6 +5,7 @@
5 5
 from django.contrib.auth import authenticate, login
6 6
 from django.db.models.base import ModelBase
7 7
 from django.core.exceptions import ImproperlyConfigured
  8
+from django.core.urlresolvers import reverse
8 9
 from django.shortcuts import render_to_response
9 10
 from django.utils.functional import update_wrapper
10 11
 from django.utils.safestring import mark_safe
@@ -38,17 +39,14 @@ class AdminSite(object):
38 39
     login_template = None
39 40
     app_index_template = None
40 41
 
41  
-    def __init__(self, name=None):
  42
+    def __init__(self, name=None, app_name='admin'):
42 43
         self._registry = {} # model_class class -> admin_class instance
43  
-        # TODO Root path is used to calculate urls under the old root() method
44  
-        # in order to maintain backwards compatibility we are leaving that in
45  
-        # so root_path isn't needed, not sure what to do about this.
46  
-        self.root_path = 'admin/'
  44
+        self.root_path = None
47 45
         if name is None:
48  
-            name = ''
  46
+            self.name = 'admin'
49 47
         else:
50  
-            name += '_'
51  
-        self.name = name
  48
+            self.name = name
  49
+        self.app_name = app_name
52 50
         self._actions = {'delete_selected': actions.delete_selected}
53 51
         self._global_actions = self._actions.copy()
54 52
 
@@ -202,24 +200,24 @@ def wrapper(*args, **kwargs):
202 200
         urlpatterns = patterns('',
203 201
             url(r'^$',
204 202
                 wrap(self.index),
205  
-                name='%sadmin_index' % self.name),
  203
+                name='index'),
206 204
             url(r'^logout/$',
207 205
                 wrap(self.logout),
208  
-                name='%sadmin_logout'),
  206
+                name='logout'),
209 207
             url(r'^password_change/$',
210 208
                 wrap(self.password_change, cacheable=True),
211  
-                name='%sadmin_password_change' % self.name),
  209
+                name='password_change'),
212 210
             url(r'^password_change/done/$',
213 211
                 wrap(self.password_change_done, cacheable=True),
214  
-                name='%sadmin_password_change_done' % self.name),
  212
+                name='password_change_done'),
215 213
             url(r'^jsi18n/$',
216 214
                 wrap(self.i18n_javascript, cacheable=True),
217  
-                name='%sadmin_jsi18n' % self.name),
  215
+                name='jsi18n'),
218 216
             url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$',
219 217
                 'django.views.defaults.shortcut'),
220 218
             url(r'^(?P<app_label>\w+)/$',
221 219
                 wrap(self.app_index),
222  
-                name='%sadmin_app_list' % self.name),
  220
+                name='app_list')
223 221
         )
224 222
 
225 223
         # Add in each model's views.
@@ -231,7 +229,7 @@ def wrapper(*args, **kwargs):
231 229
         return urlpatterns
232 230
 
233 231
     def urls(self):
234  
-        return self.get_urls()
  232
+        return self.get_urls(), self.app_name, self.name
235 233
     urls = property(urls)
236 234
 
237 235
     def password_change(self, request):
@@ -239,8 +237,11 @@ def password_change(self, request):
239 237
         Handles the "change password" task -- both form display and validation.
240 238
         """
241 239
         from django.contrib.auth.views import password_change
242  
-        return password_change(request,
243  
-            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('admin:password_change_done', current_app=self.name)
  244
+        return password_change(request, post_change_redirect=url)
244 245
 
245 246
     def password_change_done(self, request):
246 247
         """
@@ -368,8 +369,9 @@ def index(self, request, extra_context=None):
368 369
             'root_path': self.root_path,
369 370
         }
370 371
         context.update(extra_context or {})
  372
+        context_instance = template.RequestContext(request, current_app=self.name)
371 373
         return render_to_response(self.index_template or 'admin/index.html', context,
372  
-            context_instance=template.RequestContext(request)
  374
+            context_instance=context_instance
373 375
         )
374 376
     index = never_cache(index)
375 377
 
@@ -382,8 +384,9 @@ def display_login_form(self, request, error_message='', extra_context=None):
382 384
             'root_path': self.root_path,
383 385
         }
384 386
         context.update(extra_context or {})
  387
+        context_instance = template.RequestContext(request, current_app=self.name)
385 388
         return render_to_response(self.login_template or 'admin/login.html', context,
386  
-            context_instance=template.RequestContext(request)
  389
+            context_instance=context_instance
387 390
         )
388 391
 
389 392
     def app_index(self, request, app_label, extra_context=None):
@@ -425,9 +428,10 @@ def app_index(self, request, app_label, extra_context=None):
425 428
             'root_path': self.root_path,
426 429
         }
427 430
         context.update(extra_context or {})
  431
+        context_instance = template.RequestContext(request, current_app=self.name)
428 432
         return render_to_response(self.app_index_template or ('admin/%s/app_index.html' % app_label,
429 433
             'admin/app_index.html'), context,
430  
-            context_instance=template.RequestContext(request)
  434
+            context_instance=context_instance
431 435
         )
432 436
 
433 437
     def root(self, request, url):
25  django/contrib/admin/templates/admin/base.html
@@ -23,7 +23,30 @@
23 23
         {% block branding %}{% endblock %}
24 24
         </div>
25 25
         {% if user.is_authenticated and user.is_staff %}
26  
-        <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>
  26
+        <div id="user-tools">
  27
+            {% trans 'Welcome,' %}
  28
+            <strong>{% firstof user.first_name user.username %}</strong>.
  29
+            {% block userlinks %}
  30
+                {% url django-admindocs-docroot as docsroot %}
  31
+                {% if docsroot %}
  32
+                    <a href="{{ docsroot }}">{% trans 'Documentation' %}</a> /
  33
+                {% endif %}
  34
+                {% url admin:password_change as password_change_url %}
  35
+                {% if password_change_url %}
  36
+                    <a href="{{ password_change_url }}">
  37
+                {% else %}
  38
+                    <a href="{{ root_path }}password_change/">
  39
+                {% endif %}
  40
+                {% trans 'Change password' %}</a> /
  41
+                {% url admin:logout as logout_url %}
  42
+                {% if logout_url %}
  43
+                    <a href="{{ logout_url }}">
  44
+                {% else %}
  45
+                    <a href="{{ root_path }}logout/">
  46
+                {% endif %}
  47
+                {% trans 'Log out' %}</a>
  48
+            {% endblock %}
  49
+        </div>
27 50
         {% endif %}
28 51
         {% block nav-global %}{% endblock %}
29 52
     </div>
15  django/contrib/admin/widgets.py
@@ -125,7 +125,7 @@ def render(self, name, value, attrs=None):
125 125
         if value:
126 126
             output.append(self.label_for_value(value))
127 127
         return mark_safe(u''.join(output))
128  
-    
  128
+
129 129
     def base_url_parameters(self):
130 130
         params = {}
131 131
         if self.rel.limit_choices_to:
@@ -137,14 +137,14 @@ def base_url_parameters(self):
137 137
                     v = str(v)
138 138
                 items.append((k, v))
139 139
             params.update(dict(items))
140  
-        return params    
141  
-    
  140
+        return params
  141
+
142 142
     def url_parameters(self):
143 143
         from django.contrib.admin.views.main import TO_FIELD_VAR
144 144
         params = self.base_url_parameters()
145 145
         params.update({TO_FIELD_VAR: self.rel.get_related_field().name})
146 146
         return params
147  
-            
  147
+
148 148
     def label_for_value(self, value):
149 149
         key = self.rel.get_related_field().name
150 150
         obj = self.rel.to._default_manager.get(**{key: value})
@@ -165,10 +165,10 @@ def render(self, name, value, attrs=None):
165 165
         else:
166 166
             value = ''
167 167
         return super(ManyToManyRawIdWidget, self).render(name, value, attrs)
168  
-    
  168
+
169 169
     def url_parameters(self):
170 170
         return self.base_url_parameters()
171  
-    
  171
+
172 172
     def label_for_value(self, value):
173 173
         return ''
174 174
 
@@ -222,8 +222,7 @@ def render(self, name, value, *args, **kwargs):
222 222
         rel_to = self.rel.to
223 223
         info = (rel_to._meta.app_label, rel_to._meta.object_name.lower())
224 224
         try:
225  
-            related_info = (self.admin_site.name,) + info
226  
-            related_url = reverse('%sadmin_%s_%s_add' % related_info)
  225
+            related_url = reverse('admin:%s_%s_add' % info, current_app=self.admin_site.name)
227 226
         except NoReverseMatch:
228 227
             related_url = '../../../%s/%s/add/' % info
229 228
         self.widget.choices = self.choices
2  django/contrib/admindocs/templates/admin_doc/index.html
... ...
@@ -1,6 +1,6 @@
1 1
 {% extends "admin/base_site.html" %}
2 2
 {% load i18n %}
3  
-{% block breadcrumbs %}<div class="breadcrumbs"><a href="../">Home</a> &rsaquo; Documentation</div>{% endblock %}
  3
+{% block breadcrumbs %}<div class="breadcrumbs"><a href="{{ root_path }}">Home</a> &rsaquo; Documentation</div>{% endblock %}
4 4
 {% block title %}Documentation{% endblock %}
5 5
 
6 6
 {% block content %}
11  django/contrib/admindocs/views.py
@@ -22,11 +22,14 @@ class GenericSite(object):
22 22
     name = 'my site'
23 23
 
24 24
 def get_root_path():
25  
-    from django.contrib import admin
26 25
     try:
27  
-        return urlresolvers.reverse(admin.site.root, args=[''])
  26
+        return urlresolvers.reverse('admin:index')
28 27
     except urlresolvers.NoReverseMatch:
29  
-        return getattr(settings, "ADMIN_SITE_ROOT_URL", "/admin/")
  28
+        from django.contrib import admin
  29
+        try:
  30
+            return urlresolvers.reverse(admin.site.root, args=[''])
  31
+        except urlresolvers.NoReverseMatch:
  32
+            return getattr(settings, "ADMIN_SITE_ROOT_URL", "/admin/")
30 33
 
31 34
 def doc_index(request):
32 35
     if not utils.docutils_is_available:
@@ -179,7 +182,7 @@ def model_index(request):
179 182
 def model_detail(request, app_label, model_name):
180 183
     if not utils.docutils_is_available:
181 184
         return missing_docutils_page(request)
182  
-        
  185
+
183 186
     # Get the model class.
184 187
     try:
185 188
         app_mod = models.get_app(app_label)
109  django/core/urlresolvers.py
@@ -139,7 +139,7 @@ def _get_callback(self):
139 139
     callback = property(_get_callback)
140 140
 
141 141
 class RegexURLResolver(object):
142  
-    def __init__(self, regex, urlconf_name, default_kwargs=None):
  142
+    def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
143 143
         # regex is a string representing a regular expression.
144 144
         # urlconf_name is a string representing the module containing URLconfs.
145 145
         self.regex = re.compile(regex, re.UNICODE)
@@ -148,19 +148,29 @@ def __init__(self, regex, urlconf_name, default_kwargs=None):
148 148
             self._urlconf_module = self.urlconf_name
149 149
         self.callback = None
150 150
         self.default_kwargs = default_kwargs or {}
151  
-        self._reverse_dict = MultiValueDict()
  151
+        self.namespace = namespace
  152
+        self.app_name = app_name
  153
+        self._reverse_dict = None
  154
+        self._namespace_dict = None
  155
+        self._app_dict = None
152 156
 
153 157
     def __repr__(self):
154  
-        return '<%s %s %s>' % (self.__class__.__name__, self.urlconf_name, self.regex.pattern)
155  
-
156  
-    def _get_reverse_dict(self):
157  
-        if not self._reverse_dict:
158  
-            lookups = MultiValueDict()
159  
-            for pattern in reversed(self.url_patterns):
160  
-                p_pattern = pattern.regex.pattern
161  
-                if p_pattern.startswith('^'):
162  
-                    p_pattern = p_pattern[1:]
163  
-                if isinstance(pattern, RegexURLResolver):
  158
+        return '<%s %s (%s:%s) %s>' % (self.__class__.__name__, self.urlconf_name, self.app_name, self.namespace, self.regex.pattern)
  159
+
  160
+    def _populate(self):
  161
+        lookups = MultiValueDict()
  162
+        namespaces = {}
  163
+        apps = {}
  164
+        for pattern in reversed(self.url_patterns):
  165
+            p_pattern = pattern.regex.pattern
  166
+            if p_pattern.startswith('^'):
  167
+                p_pattern = p_pattern[1:]
  168
+            if isinstance(pattern, RegexURLResolver):
  169
+                if pattern.namespace:
  170
+                    namespaces[pattern.namespace] = (p_pattern, pattern)
  171
+                    if pattern.app_name:
  172
+                        apps.setdefault(pattern.app_name, []).append(pattern.namespace)
  173
+                else:
164 174
                     parent = normalize(pattern.regex.pattern)
165 175
                     for name in pattern.reverse_dict:
166 176
                         for matches, pat in pattern.reverse_dict.getlist(name):
@@ -168,14 +178,36 @@ def _get_reverse_dict(self):
168 178
                             for piece, p_args in parent:
169 179
                                 new_matches.extend([(piece + suffix, p_args + args) for (suffix, args) in matches])
170 180
                             lookups.appendlist(name, (new_matches, p_pattern + pat))
171  
-                else:
172  
-                    bits = normalize(p_pattern)
173  
-                    lookups.appendlist(pattern.callback, (bits, p_pattern))
174  
-                    lookups.appendlist(pattern.name, (bits, p_pattern))
175  
-            self._reverse_dict = lookups
  181
+                    for namespace, (prefix, sub_pattern) in pattern.namespace_dict.items():
  182
+                        namespaces[namespace] = (p_pattern + prefix, sub_pattern)
  183
+                    for app_name, namespace_list in pattern.app_dict.items():
  184
+                        apps.setdefault(app_name, []).extend(namespace_list)
  185
+            else:
  186
+                bits = normalize(p_pattern)
  187
+                lookups.appendlist(pattern.callback, (bits, p_pattern))
  188
+                lookups.appendlist(pattern.name, (bits, p_pattern))
  189
+        self._reverse_dict = lookups
  190
+        self._namespace_dict = namespaces
  191
+        self._app_dict = apps
  192
+
  193
+    def _get_reverse_dict(self):
  194
+        if self._reverse_dict is None:
  195
+            self._populate()
176 196
         return self._reverse_dict
177 197
     reverse_dict = property(_get_reverse_dict)
178 198
 
  199
+    def _get_namespace_dict(self):
  200
+        if self._namespace_dict is None:
  201
+            self._populate()
  202
+        return self._namespace_dict
  203
+    namespace_dict = property(_get_namespace_dict)
  204
+
  205
+    def _get_app_dict(self):
  206
+        if self._app_dict is None:
  207
+            self._populate()
  208
+        return self._app_dict
  209
+    app_dict = property(_get_app_dict)
  210
+
179 211
     def resolve(self, path):
180 212
         tried = []
181 213
         match = self.regex.search(path)
@@ -261,12 +293,51 @@ def reverse(self, lookup_view, *args, **kwargs):
261 293
 def resolve(path, urlconf=None):
262 294
     return get_resolver(urlconf).resolve(path)
263 295
 
264  
-def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None):
  296
+def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, current_app=None):
  297
+    resolver = get_resolver(urlconf)
265 298
     args = args or []
266 299
     kwargs = kwargs or {}
  300
+
267 301
     if prefix is None:
268 302
         prefix = get_script_prefix()
269  
-    return iri_to_uri(u'%s%s' % (prefix, get_resolver(urlconf).reverse(viewname,
  303
+
  304
+    if not isinstance(viewname, basestring):
  305
+        view = viewname
  306
+    else:
  307
+        parts = viewname.split(':')
  308
+        parts.reverse()
  309
+        view = parts[0]
  310
+        path = parts[1:]
  311
+
  312
+        resolved_path = []
  313
+        while path:
  314
+            ns = path.pop()
  315
+
  316
+            # Lookup the name to see if it could be an app identifier
  317
+            try:
  318
+                app_list = resolver.app_dict[ns]
  319
+                # Yes! Path part matches an app in the current Resolver
  320
+                if current_app and current_app in app_list:
  321
+                    # If we are reversing for a particular app, use that namespace
  322
+                    ns = current_app
  323
+                elif ns not in app_list:
  324
+                    # The name isn't shared by one of the instances (i.e., the default)
  325
+                    # so just pick the first instance as the default.
  326
+                    ns = app_list[0]
  327
+            except KeyError:
  328
+                pass
  329
+
  330
+            try:
  331
+                extra, resolver = resolver.namespace_dict[ns]
  332
+                resolved_path.append(ns)
  333
+                prefix = prefix + extra
  334
+            except KeyError, key:
  335
+                if resolved_path:
  336
+                    raise NoReverseMatch("%s is not a registered namespace inside '%s'" % (key, ':'.join(resolved_path)))
  337
+                else:
  338
+                    raise NoReverseMatch("%s is not a registered namespace" % key)
  339
+
  340
+    return iri_to_uri(u'%s%s' % (prefix, resolver.reverse(view,
270 341
             *args, **kwargs)))
271 342
 
272 343
 def clear_url_caches():
7  django/template/context.py
@@ -9,10 +9,11 @@ class ContextPopException(Exception):
9 9
 
10 10
 class Context(object):
11 11
     "A stack container for variable context"
12  
-    def __init__(self, dict_=None, autoescape=True):
  12
+    def __init__(self, dict_=None, autoescape=True, current_app=None):
13 13
         dict_ = dict_ or {}
14 14
         self.dicts = [dict_]
15 15
         self.autoescape = autoescape
  16
+        self.current_app = current_app
16 17
 
17 18
     def __repr__(self):
18 19
         return repr(self.dicts)
@@ -96,8 +97,8 @@ class RequestContext(Context):
96 97
     Additional processors can be specified as a list of callables
97 98
     using the "processors" keyword argument.
98 99
     """
99  
-    def __init__(self, request, dict=None, processors=None):
100  
-        Context.__init__(self, dict)
  100
+    def __init__(self, request, dict=None, processors=None, current_app=None):
  101
+        Context.__init__(self, dict, current_app=current_app)
101 102
         if processors is None:
102 103
             processors = ()
103 104
         else:
6  django/template/defaulttags.py
@@ -367,17 +367,17 @@ def render(self, context):
367 367
         # {% url ... as var %} construct in which cause return nothing.
368 368
         url = ''
369 369
         try:
370  
-            url = reverse(self.view_name, args=args, kwargs=kwargs)
  370
+            url = reverse(self.view_name, args=args, kwargs=kwargs, current_app=context.current_app)
371 371
         except NoReverseMatch, e:
372 372
             if settings.SETTINGS_MODULE:
373 373
                 project_name = settings.SETTINGS_MODULE.split('.')[0]
374 374
                 try:
375 375
                     url = reverse(project_name + '.' + self.view_name,
376  
-                              args=args, kwargs=kwargs)
  376
+                              args=args, kwargs=kwargs, current_app=context.current_app)
377 377
                 except NoReverseMatch:
378 378
                     if self.asvar is None:
379 379
                         # Re-raise the original exception, not the one with
380  
-                        # the path relative to the project. This makes a 
  380
+                        # the path relative to the project. This makes a
381 381
                         # better error message.
382 382
                         raise e
383 383
             else:
80  docs/ref/contrib/admin/index.txt
@@ -1242,7 +1242,7 @@ or :attr:`AdminSite.login_template` properties.
1242 1242
 ``AdminSite`` objects
1243 1243
 =====================
1244 1244
 
1245  
-.. class:: AdminSite
  1245
+.. class:: AdminSite(name=None)
1246 1246
 
1247 1247
 A Django administrative site is represented by an instance of
1248 1248
 ``django.contrib.admin.sites.AdminSite``; by default, an instance of
@@ -1256,6 +1256,14 @@ or add anything you like. Then, simply create an instance of your
1256 1256
 Python class), and register your models and ``ModelAdmin`` subclasses
1257 1257
 with it instead of using the default.
1258 1258
 
  1259
+.. versionadded:: 1.1
  1260
+
  1261
+When constructing an instance of an ``AdminSite``, you are able to provide
  1262
+a unique instance name using the ``name`` argument to the constructor. This
  1263
+instance name is used to identify the instance, especially when
  1264
+:ref:`reversing admin URLs <admin-reverse-urls>`. If no instance name is
  1265
+provided, a default instance name of ``admin`` will be used.
  1266
+
1259 1267
 ``AdminSite`` attributes
1260 1268
 ------------------------
1261 1269
 
@@ -1353,10 +1361,10 @@ a pattern for your new view.
1353 1361
 
1354 1362
 .. note::
1355 1363
     Any view you render that uses the admin templates, or extends the base
1356  
-    admin template, should include in it's context a variable named
1357  
-    ``admin_site`` that contains the name of the :class:`AdminSite` instance. For
1358  
-    :class:`AdminSite` instances, this means ``self.name``; for :class:`ModelAdmin`
1359  
-    instances, this means ``self.admin_site.name``.
  1364
+    admin template, should provide the ``current_app`` argument to
  1365
+    ``RequestContext`` or ``Context`` when rendering the template.  It should
  1366
+    be set to either ``self.name`` if your view is on an ``AdminSite`` or
  1367
+    ``self.admin_site.name`` if your view is on a ``ModelAdmin``.
1360 1368
 
1361 1369
 .. _admin-reverse-urls:
1362 1370
 
@@ -1370,37 +1378,31 @@ accessible using Django's :ref:`URL reversing system <naming-url-patterns>`.
1370 1378
 
1371 1379
 The :class:`AdminSite` provides the following named URL patterns:
1372 1380
 
1373  
-    ======================  =============================== =============
1374  
-    Page                    URL name                        Parameters
1375  
-    ======================  =============================== =============
1376  
-    Index                   ``admin_index``
1377  
-    Logout                  ``admin_logout``
1378  
-    Password change         ``admin_password_change``
1379  
-    Password change done    ``admin_password_change_done``
1380  
-    i18n javascript         ``admin_jsi18n``
1381  
-    Application index page  ``admin_app_list``              ``app_label``
1382  
-    ======================  =============================== =============
1383  
-
1384  
-These names will be prefixed with the name of the :class:`AdminSite` instance,
1385  
-plus an underscore. For example, if your :class:`AdminSite` was named
1386  
-``custom``, then the Logout view would be served using a URL with the name
1387  
-``custom_admin_logout``. The default :class:`AdminSite` doesn't use a prefix
1388  
-in it's URL names.
  1381
+    ======================  ========================  =============
  1382
+    Page                    URL name                  Parameters
  1383
+    ======================  ========================  =============
  1384
+    Index                   ``index``
  1385
+    Logout                  ``logout``
  1386
+    Password change         ``password_change``
  1387
+    Password change done    ``password_change_done``
  1388
+    i18n javascript         ``jsi18n``
  1389
+    Application index page  ``app_list``              ``app_label``
  1390
+    ======================  ========================  =============
1389 1391
 
1390 1392
 Each :class:`ModelAdmin` instance provides an additional set of named URLs:
1391 1393
 
1392  
-    ======================  =====================================================   =============
1393  
-    Page                    URL name                                                Parameters
1394  
-    ======================  =====================================================   =============
1395  
-    Changelist              ``admin_{{ app_label }}_{{ model_name }}_changelist``
1396  
-    Add                     ``admin_{{ app_label }}_{{ model_name }}_add``
1397  
-    History                 ``admin_{{ app_label }}_{{ model_name }}_history``      ``object_id``
1398  
-    Delete                  ``admin_{{ app_label }}_{{ model_name }}_delete``       ``object_id``
1399  
-    Change                  ``admin_{{ app_label }}_{{ model_name }}_change``       ``object_id``
1400  
-    ======================  =====================================================   =============
  1394
+    ======================  ===============================================   =============
  1395
+    Page                    URL name                                          Parameters
  1396
+    ======================  ===============================================   =============
  1397
+    Changelist              ``{{ app_label }}_{{ model_name }}_changelist``
  1398
+    Add                     ``{{ app_label }}_{{ model_name }}_add``
  1399
+    History                 ``{{ app_label }}_{{ model_name }}_history``      ``object_id``
  1400
+    Delete                  ``{{ app_label }}_{{ model_name }}_delete``       ``object_id``
  1401
+    Change                  ``{{ app_label }}_{{ model_name }}_change``       ``object_id``
  1402
+    ======================  ===============================================   =============
1401 1403
 
1402  
-Again, these names will be prefixed by the name of the :class:`AdminSite` in
1403  
-which they are deployed.
  1404
+These named URLs are registered with the application namespace ``admin``, and
  1405
+with an instance namespace corresponding to the name of the Site instance.
1404 1406
 
1405 1407
 So - if you wanted to get a reference to the Change view for a particular
1406 1408
 ``Choice`` object (from the polls application) in the default admin, you would
@@ -1408,8 +1410,16 @@ call::
1408 1410
 
1409 1411
     >>> from django.core import urlresolvers
1410 1412
     >>> c = Choice.objects.get(...)
1411  
-    >>> change_url = urlresolvers.reverse('admin_polls_choice_change', args=(c.id,))
  1413
+    >>> change_url = urlresolvers.reverse('admin:polls_choice_change', args=(c.id,))
  1414
+
  1415
+This will find the first registered instance of the admin application (whatever the instance
  1416
+name), and resolve to the view for changing ``poll.Choice`` instances in that instance.
  1417
+
  1418
+If you want to find a URL in a specific admin instance, provide the name of that instance
  1419
+as a ``current_app`` hint to the reverse call. For example, if you specifically wanted
  1420
+the admin view from the admin instance named ``custom``, you would need to call::
1412 1421
 
1413  
-However, if the admin instance was named ``custom``, you would need to call::
  1422
+    >>> change_url = urlresolvers.reverse('custom:polls_choice_change', args=(c.id,))
1414 1423
 
1415  
-    >>> change_url = urlresolvers.reverse('custom_admin_polls_choice_change', args=(c.id,))
  1424
+For more details, see the documentation on :ref:`reversing namespaced URLs
  1425
+<topics-http-reversing-url-namespaces>`.
17  docs/ref/templates/api.txt
@@ -86,9 +86,16 @@ Rendering a context
86 86
 
87 87
 Once you have a compiled ``Template`` object, you can render a context -- or
88 88
 multiple contexts -- with it. The ``Context`` class lives at
89  
-``django.template.Context``, and the constructor takes one (optional)
90  
-argument: a dictionary mapping variable names to variable values. Call the
91  
-``Template`` object's ``render()`` method with the context to "fill" the
  89
+``django.template.Context``, and the constructor takes two (optional)
  90
+arguments:
  91
+
  92
+    * A dictionary mapping variable names to variable values.
  93
+
  94
+    * The name of the current application. This application name is used
  95
+      to help :ref:`resolve namespaced URLs<topics-http-reversing-url-namespaces>`.
  96
+      If you're not using namespaced URLs, you can ignore this argument.
  97
+
  98
+Call the ``Template`` object's ``render()`` method with the context to "fill" the
92 99
 template::
93 100
 
94 101
     >>> from django.template import Context, Template
@@ -549,13 +556,13 @@ Here are the template loaders that come with Django:
549 556
     Note that the loader performs an optimization when it is first imported: It
550 557
     caches a list of which :setting:`INSTALLED_APPS` packages have a
551 558
     ``templates`` subdirectory.
552  
-    
  559
+
553 560
     This loader is enabled by default.
554 561
 
555 562
 ``django.template.loaders.eggs.load_template_source``
556 563
     Just like ``app_directories`` above, but it loads templates from Python
557 564
     eggs rather than from the filesystem.
558  
-    
  565
+
559 566
     This loader is disabled by default.
560 567
 
561 568
 Django uses the template loaders in order according to the
10  docs/ref/templates/builtins.txt
@@ -795,6 +795,16 @@ missing. In practice you'll use this to link to views that are optional::
795 795
       <a href="{{ the_url }}">Link to optional stuff</a>
796 796
     {% endif %}
797 797
 
  798
+.. versionadded:: 1.1
  799
+
  800
+If you'd like to retrieve a namespaced URL, specify the fully qualified name::
  801
+
  802
+    {% url myapp:view-name %}
  803
+
  804
+This will follow the normal :ref:`namespaced URL resolution strategy
  805
+<topics-http-reversing-url-namespaces>`, including using any hints provided
  806
+by the context as to the current application.
  807
+
798 808
 .. templatetag:: widthratio
799 809
 
800 810
 widthratio
151  docs/topics/http/urls.txt
@@ -400,7 +400,7 @@ further processing.
400 400
 
401 401
 .. versionadded:: 1.1
402 402
 
403  
-Another posibility is to include additional URL patterns not by specifying the
  403
+Another possibility is to include additional URL patterns not by specifying the
404 404
 URLconf Python module defining them as the `include`_ argument but by using
405 405
 directly the pattern list as returned by `patterns`_ instead. For example::
406 406
 
@@ -417,6 +417,13 @@ directly the pattern list as returned by `patterns`_ instead. For example::
417 417
         (r'^credit/', include(extra_patterns)),
418 418
     )
419 419
 
  420
+This approach can be seen in use when you deploy an instance of the Django
  421
+Admin application. The Django Admin is deployed as instances of a
  422
+:class:`AdminSite`; each :class:`AdminSite` instance has an attribute
  423
+``urls`` that returns the url patterns available to that instance. It is this
  424
+attribute that you ``included()`` into your projects ``urlpatterns`` when you
  425
+deploy the admin instance.
  426
+
420 427
 .. _`Django Web site`: http://www.djangoproject.com/
421 428
 
422 429
 Captured parameters
@@ -439,6 +446,58 @@ the following example is valid::
439 446
 In the above example, the captured ``"username"`` variable is passed to the
440 447
 included URLconf, as expected.
441 448
 
  449
+.. _topics-http-defining-url-namespaces:
  450
+
  451
+Defining URL Namespaces
  452
+-----------------------
  453
+
  454
+When you need to deploying multiple instances of a single application, it can
  455
+be helpful to be able to differentiate between instances. This is especially
  456
+important when using _`named URL patterns <naming-url-patterns>`, since
  457
+multiple instances of a single application will share named URLs. Namespaces
  458
+provide a way to tell these named URLs apart.
  459
+
  460
+A URL namespace comes in two parts, both of which are strings:
  461
+
  462
+    * An **application namespace**. This describes the name of the application
  463
+      that is being deployed. Every instance of a single application will have
  464
+      the same application namespace. For example, Django's admin application
  465
+      has the somewhat predictable application namespace of ``admin``.
  466
+
  467
+    * An **instance namespace**. This identifies a specific instance of an
  468
+      application. Instance namespaces should be unique across your entire
  469
+      project. However, and instance namespace can be the same as the
  470
+      application namespace. This is used to specify a default instance of an
  471
+      application. For example, the default Django Admin instance has an
  472
+      instance namespace of ``admin``.
  473
+
  474
+URL Namespaces can be specified in two ways.
  475
+
  476
+Firstly, you can provide the applicaiton and instance namespace as arguments
  477
+to the ``include()`` when you construct your URL patterns. For example,::
  478
+
  479
+    (r'^help/', include('apps.help.urls', namespace='foo', app_name='bar')),
  480
+
  481
+This will include the URLs defined in ``apps.help.urls`` into the application
  482
+namespace ``bar``, with the instance namespace ``foo``.
  483
+
  484
+Secondly, you can include an object that contains embedded namespace data. If
  485
+you ``include()`` a ``patterns`` object, that object will be added to the
  486
+global namespace. However, you can also ``include()`` an object that contains
  487
+a 3-tuple containing::
  488
+
  489
+    (<patterns object>, <application namespace>, <instance namespace>)
  490
+
  491
+This will include the nominated URL patterns into the given application and
  492
+instance namespace. For example, the ``urls`` attribute of Django's
  493
+:class:`AdminSite` object returns a 3-tuple that contains all the patterns in
  494
+an admin site, plus the name of the admin instance, and the application
  495
+namespace ``admin``.
  496
+
  497
+Once you have defined namespace URLs, you can reverse them. For details on
  498
+reversing namespaced urls, see the documentation on :ref:`reversing namespaced
  499
+URLs <topics-http-reversing-url-namespaces>`.
  500
+
442 501
 Passing extra options to view functions
443 502
 =======================================
444 503
 
@@ -613,6 +672,86 @@ not restricted to valid Python names.
613 672
     name, will decrease the chances of collision. We recommend something like
614 673
     ``myapp-comment`` instead of ``comment``.
615 674
 
  675
+.. _topics-http-reversing-url-namespaces:
  676
+
  677
+URL namespaces
  678
+--------------
  679
+
  680
+.. versionadded:: 1.1
  681
+
  682
+Namespaced URLs are specified using the `:` operator. For example, the main index
  683
+page of the admin application is referenced using ``admin:index``. This indicates
  684
+a namespace of ``admin``, and a named URL of ``index``.
  685
+
  686
+Namespaces can also be nested. The named URL ``foo:bar:whiz`` would look for
  687
+a pattern named ``whiz`` in the namespace ``bar`` that is itself defined within
  688
+the top-level namespace ``foo``.
  689
+
  690
+When given a namespaced URL (e.g.,, `myapp:index`) to resolve, Django splits
  691
+the fully qualified name into parts, and then tries the following lookup:
  692
+
  693
+    1. Django then looks for a matching application namespace (in this
  694
+       example, ``myapp``). This will yield a list of instances of that
  695
+       application.
  696
+
  697
+    2. If there is a ``current`` application defined, Django finds and returns
  698
+       the URL resolver for that instance. The ``current`` can be specified
  699
+       as an attribute on the template context - applications that expect to
  700
+       have multiple deployments should set the ``current_app`` attribute on
  701
+       any ``Context`` or ``RequestContext`` that is used to render a
  702
+       template.
  703
+
  704
+       The current application can also be specified manually as an argument
  705
+       to the :method:``reverse()`` function.
  706
+
  707
+    3. If there is no current application. Django looks for a default
  708
+       application instance. The default application instance is the instance
  709
+       that has an instance namespace matching the application namespace (in
  710
+       this example, an instance of the ``myapp`` called ``myapp``)
  711
+
  712
+    4. If there is no default application instance, Django will pick the first
  713
+       deployed instance of the application, whatever it's instance name may be.
  714
+
  715
+    5. If the provided namespace doesn't match an application namespace in
  716
+       step 2, Django will attempt a direct lookup of the namespace as an
  717
+       instance namespace.
  718
+
  719
+If there are nested namespaces, these steps are repeated for each part of the
  720
+namespace until only the view name is unresolved. The view name will then be
  721
+resolved into a URL in the namespace that has been found.
  722
+
  723
+To show this resolution strategy in action, consider an example of two instances
  724
+of ``myapp``: one called ``foo``, and one called ``bar``. ``myapp`` has a main
  725
+index page with a URL named `index`. Using this setup, the following lookups are
  726
+possible:
  727
+
  728
+    * If one of the instances is current - say, if we were rendering a utility page
  729
+      in the instance ``bar`` - ``myapp:index`` will resolve to the index page of
  730
+      the instance ``bar``.
  731
+
  732
+    * If there is no current instance - say, if we were rendering a page
  733
+      somewhere else on the site - ``myapp:index`` will resolve to the first
  734
+      registered instance of ``myapp``. Since there is no default instance,
  735
+      the first instance of ``myapp`` that is registered will be used. This could
  736
+      be ``foo`` or ``bar``, depending on the order they are introduced into the
  737
+      urlpatterns of the project.
  738
+
  739
+    * ``foo:index`` will always resolve to the index page of the instance ``foo``.
  740
+
  741
+If there was also a default instance - i.e., an instance named `myapp` - the
  742
+following would happen:
  743
+
  744
+    * If one of the instances is current - say, if we were rendering a utility page
  745
+      in the instance ``bar`` - ``myapp:index`` will resolve to the index page of
  746
+      the instance ``bar``.
  747
+
  748
+    * If there is no current instance - say, if we were rendering a page somewhere
  749
+      else on the site - ``myapp:index`` will resolve to the index page of the
  750
+      default instance.
  751
+
  752
+    * ``foo:index`` will again resolve to the index page of the instance ``foo``.
  753
+
  754
+
616 755
 Utility methods
617 756
 ===============
618 757
 
@@ -624,7 +763,7 @@ your code, Django provides the following method (in the
624 763
 ``django.core.urlresolvers`` module):
625 764
 
626 765
 .. currentmodule:: django.core.urlresolvers
627  
-.. function:: reverse(viewname, urlconf=None, args=None, kwargs=None)
  766
+.. function:: reverse(viewname, urlconf=None, args=None, kwargs=None, current_app=None)
628 767
 
629 768
 ``viewname`` is either the function name (either a function reference, or the
630 769
 string version of the name, if you used that form in ``urlpatterns``) or the
@@ -646,6 +785,14 @@ vertical bar (``"|"``) character. You can quite happily use such patterns for
646 785
 matching against incoming URLs and sending them off to views, but you cannot
647 786
 reverse such patterns.
648 787
 
  788
+.. versionadded:: 1.1
  789
+
  790
+The ``current_app`` argument allows you to provide a hint to the resolver
  791
+indicating the application to which the currently executing view belongs.
  792
+This ``current_app`` argument is used as a hint to resolve application
  793
+namespaces into URLs on specific application instances, according to the
  794
+:ref:`namespaced URL resolution strategy <topics-http-reversing-url-namespaces>`.
  795
+
649 796
 .. admonition:: Make sure your views are all correct
650 797
 
651 798
     As part of working out which URL names map to which patterns, the
8  tests/regressiontests/admin_views/customadmin.py
@@ -10,19 +10,19 @@
10 10
 class Admin2(admin.AdminSite):
11 11
     login_template = 'custom_admin/login.html'
12 12
     index_template = 'custom_admin/index.html'
13  
-    
  13
+
14 14
     # A custom index view.
15 15
     def index(self, request, extra_context=None):
16 16
         return super(Admin2, self).index(request, {'foo': '*bar*'})
17  
-    
  17
+
18 18
     def get_urls(self):
19 19
         return patterns('',
20 20
             (r'^my_view/$', self.admin_view(self.my_view)),
21 21
         ) + super(Admin2, self).get_urls()
22  
-    
  22
+
23 23
     def my_view(self, request):
24 24
         return HttpResponse("Django is a magical pony!")
25  
-    
  25
+
26 26
 site = Admin2(name="admin2")
27 27
 
28 28
 site.register(models.Article, models.ArticleAdmin)
5  tests/regressiontests/admin_views/tests.py
@@ -205,6 +205,11 @@ def testIncorrectLookupParameters(self):
205 205
         response = self.client.get('/test_admin/%s/admin_views/thing/' % self.urlbit, {'color__id__exact': 'StringNotInteger!'})
206 206
         self.assertRedirects(response, '/test_admin/%s/admin_views/thing/?e=1' % self.urlbit)
207 207
 
  208
+    def testLogoutAndPasswordChangeURLs(self):
  209
+        response = self.client.get('/test_admin/%s/admin_views/article/' % self.urlbit)
  210
+        self.failIf('<a href="/test_admin/%s/logout/">' % self.urlbit not in response.content)
  211
+        self.failIf('<a href="/test_admin/%s/password_change/">' % self.urlbit not in response.content)
  212
+
208 213
     def testNamedGroupFieldChoicesChangeList(self):
209 214
         """
210 215
         Ensures the admin changelist shows correct values in the relevant column
2  tests/regressiontests/admin_widgets/widgetadmin.py
@@ -19,7 +19,7 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs):
19 19
             return db_field.formfield(**kwargs)
20 20
         return super(CarTireAdmin, self).formfield_for_foreignkey(db_field, request, **kwargs)
21 21
 
22  
-site = WidgetAdmin()
  22
+site = WidgetAdmin(name='widget-admin')
23 23
 
24 24
 site.register(models.User)
25 25
 site.register(models.Car, CarAdmin)
13  tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py
... ...
@@ -0,0 +1,13 @@
  1
+from django.conf.urls.defaults import *
  2
+from namespace_urls import URLObject
  3
+
  4
+testobj3 = URLObject('testapp', 'test-ns3')
  5
+
  6
+urlpatterns = patterns('regressiontests.urlpatterns_reverse.views',
  7
+    url(r'^normal/$', 'empty_view', name='inc-normal-view'),
  8
+    url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='inc-normal-view'),
  9
+
  10
+    (r'^test3/', include(testobj3.urls)),
  11
+    (r'^ns-included3/', include('regressiontests.urlpatterns_reverse.included_urls', namespace='inc-ns3')),
  12
+)
  13
+
38  tests/regressiontests/urlpatterns_reverse/namespace_urls.py
... ...
@@ -0,0 +1,38 @@
  1
+from django.conf.urls.defaults import *
  2
+
  3
+class URLObject(object):
  4
+    def __init__(self, app_name, namespace):
  5
+        self.app_name = app_name
  6
+        self.namespace = namespace
  7
+
  8
+    def urls(self):
  9
+        return patterns('',
  10
+            url(r'^inner/$', 'empty_view', name='urlobject-view'),
  11
+            url(r'^inner/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='urlobject-view'),
  12
+        ), self.app_name, self.namespace
  13
+    urls = property(urls)
  14
+
  15
+testobj1 = URLObject('testapp', 'test-ns1')
  16
+testobj2 = URLObject('testapp', 'test-ns2')
  17
+default_testobj = URLObject('testapp', 'testapp')
  18
+
  19
+otherobj1 = URLObject('nodefault', 'other-ns1')
  20
+otherobj2 = URLObject('nodefault', 'other-ns2')
  21
+
  22
+urlpatterns = patterns('regressiontests.urlpatterns_reverse.views',
  23
+    url(r'^normal/$', 'empty_view', name='normal-view'),
  24
+    url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='normal-view'),
  25
+
  26
+    (r'^test1/', include(testobj1.urls)),
  27
+    (r'^test2/', include(testobj2.urls)),
  28
+    (r'^default/', include(default_testobj.urls)),
  29
+
  30
+    (r'^other1/', include(otherobj1.urls)),
  31
+    (r'^other2/', include(otherobj2.urls)),
  32
+
  33
+    (r'^ns-included1/', include('regressiontests.urlpatterns_reverse.included_namespace_urls', namespace='inc-ns1')),
  34
+    (r'^ns-included2/', include('regressiontests.urlpatterns_reverse.included_namespace_urls', namespace='inc-ns2')),
  35
+
  36
+    (r'^included/', include('regressiontests.urlpatterns_reverse.included_namespace_urls')),
  37
+
  38
+)
82  tests/regressiontests/urlpatterns_reverse/tests.py
@@ -158,4 +158,84 @@ def test_redirect_to_url(self):
158 158
         res = redirect('/foo/')
159 159
         self.assertEqual(res['Location'], '/foo/')
160 160
         res = redirect('http://example.com/')
161  
-        self.assertEqual(res['Location'], 'http://example.com/')
  161
+        self.assertEqual(res['Location'], 'http://example.com/')
  162
+
  163
+
  164
+class NamespaceTests(TestCase):
  165
+    urls = 'regressiontests.urlpatterns_reverse.namespace_urls'
  166
+
  167
+    def test_ambiguous_object(self):
  168
+        "Names deployed via dynamic URL objects that require namespaces can't be resolved"
  169
+        self.assertRaises(NoReverseMatch, reverse, 'urlobject-view')
  170
+        self.assertRaises(NoReverseMatch, reverse, 'urlobject-view', args=[37,42])
  171
+        self.assertRaises(NoReverseMatch, reverse, 'urlobject-view', kwargs={'arg1':42, 'arg2':37})
  172
+
  173
+    def test_ambiguous_urlpattern(self):
  174
+        "Names deployed via dynamic URL objects that require namespaces can't be resolved"