Code

Ticket #10061: t10061-r11201.diff

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

First attempt at a complete namespace lookup fix for redeployed applications

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..964719e 100644
129--- a/django/contrib/admin/sites.py
130+++ b/django/contrib/admin/sites.py
131@@ -38,17 +38,14 @@ class AdminSite(object):
132     login_template = None
133     app_index_template = None
134 
135-    def __init__(self, name=None):
136+    def __init__(self, name=None, app_name='admin'):
137         self._registry = {} # model_class class -> admin_class instance
138-        # TODO Root path is used to calculate urls under the old root() method
139-        # in order to maintain backwards compatibility we are leaving that in
140-        # so root_path isn't needed, not sure what to do about this.
141-        self.root_path = 'admin/'
142+        self.root_path = None
143         if name is None:
144-            name = ''
145+            self.name = 'admin'
146         else:
147-            name += '_'
148-        self.name = name
149+            self.name = name
150+        self.app_name = app_name
151         self._actions = {'delete_selected': actions.delete_selected}
152         self._global_actions = self._actions.copy()
153 
154@@ -114,20 +111,20 @@ class AdminSite(object):
155         name = name or action.__name__
156         self._actions[name] = action
157         self._global_actions[name] = action
158-       
159+
160     def disable_action(self, name):
161         """
162         Disable a globally-registered action. Raises KeyError for invalid names.
163         """
164         del self._actions[name]
165-       
166+
167     def get_action(self, name):
168         """
169         Explicitally get a registered global action wheather it's enabled or
170         not. Raises KeyError for invalid names.
171         """
172         return self._global_actions[name]
173-   
174+
175     def actions(self):
176         """
177         Get all the enabled actions as an iterable of (name, func).
178@@ -186,7 +183,6 @@ class AdminSite(object):
179 
180     def get_urls(self):
181         from django.conf.urls.defaults import patterns, url, include
182-
183         def wrap(view):
184             def wrapper(*args, **kwargs):
185                 return self.admin_view(view)(*args, **kwargs)
186@@ -196,24 +192,24 @@ class AdminSite(object):
187         urlpatterns = patterns('',
188             url(r'^$',
189                 wrap(self.index),
190-                name='%sadmin_index' % self.name),
191+                name='index'),
192             url(r'^logout/$',
193                 wrap(self.logout),
194-                name='%sadmin_logout'),
195+                name='logout'),
196             url(r'^password_change/$',
197                 wrap(self.password_change),
198-                name='%sadmin_password_change' % self.name),
199+                name='password_change'),
200             url(r'^password_change/done/$',
201                 wrap(self.password_change_done),
202-                name='%sadmin_password_change_done' % self.name),
203+                name='password_change_done'),
204             url(r'^jsi18n/$',
205                 wrap(self.i18n_javascript),
206-                name='%sadmin_jsi18n' % self.name),
207+                name='jsi18n'),
208             url(r'^r/(?P<content_type_id>\d+)/(?P<object_id>.+)/$',
209                 'django.views.defaults.shortcut'),
210             url(r'^(?P<app_label>\w+)/$',
211                 wrap(self.app_index),
212-                name='%sadmin_app_list' % self.name),
213+                name='app_list')
214         )
215 
216         # Add in each model's views.
217@@ -222,7 +218,7 @@ class AdminSite(object):
218                 url(r'^%s/%s/' % (model._meta.app_label, model._meta.module_name),
219                     include(model_admin.urls))
220             )
221-        return urlpatterns
222+        return urlpatterns, self.app_name, self.name
223 
224     def urls(self):
225         return self.get_urls()
226@@ -233,8 +229,11 @@ class AdminSite(object):
227         Handles the "change password" task -- both form display and validation.
228         """
229         from django.contrib.auth.views import password_change
230-        return password_change(request,
231-            post_change_redirect='%spassword_change/done/' % self.root_path)
232+        if self.root_path is not None:
233+            url = '%spassword_change/done/' % self.root_path
234+        else:
235+            url = reverse('%s:password_change_done' % self.name)
236+        return password_change(request, post_change_redirect=url)
237 
238     def password_change_done(self, request):
239         """
240@@ -362,8 +361,9 @@ class AdminSite(object):
241             'root_path': self.root_path,
242         }
243         context.update(extra_context or {})
244+        context_instance = template.RequestContext(request, app_name=self.name)
245         return render_to_response(self.index_template or 'admin/index.html', context,
246-            context_instance=template.RequestContext(request)
247+            context_instance=context_instance
248         )
249     index = never_cache(index)
250 
251@@ -376,8 +376,9 @@ class AdminSite(object):
252             'root_path': self.root_path,
253         }
254         context.update(extra_context or {})
255+        context_instance = template.RequestContext(request, app_name=self.name)
256         return render_to_response(self.login_template or 'admin/login.html', context,
257-            context_instance=template.RequestContext(request)
258+            context_instance=context_instance
259         )
260 
261     def app_index(self, request, app_label, extra_context=None):
262@@ -419,9 +420,10 @@ class AdminSite(object):
263             'root_path': self.root_path,
264         }
265         context.update(extra_context or {})
266+        context_instance = template.RequestContext(request, app_name=self.name)
267         return render_to_response(self.app_index_template or ('admin/%s/app_index.html' % app_label,
268             'admin/app_index.html'), context,
269-            context_instance=template.RequestContext(request)
270+            context_instance=context_instance
271         )
272 
273     def root(self, request, url):
274diff --git a/django/contrib/admin/templates/admin/base.html b/django/contrib/admin/templates/admin/base.html
275index 8cab439..9525728 100644
276--- a/django/contrib/admin/templates/admin/base.html
277+++ b/django/contrib/admin/templates/admin/base.html
278@@ -23,7 +23,30 @@
279         {% block branding %}{% endblock %}
280         </div>
281         {% if user.is_authenticated and user.is_staff %}
282-        <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>
283+        <div id="user-tools">
284+            {% trans 'Welcome,' %}
285+            <strong>{% firstof user.first_name user.username %}</strong>.
286+            {% block userlinks %}
287+                {% url django-admindocs-docroot as docsroot %}
288+                {% if docsroot %}
289+                    <a href="{{ docsroot }}">{% trans 'Documentation' %}</a> /
290+                {% endif %}
291+                {% url admin:password_change as password_change_url %}
292+                {% if password_change_url %}
293+                    <a href="{{ password_change_url }}">
294+                {% else %}
295+                    <a href="{{ root_path }}password_change/">
296+                {% endif %}
297+                {% trans 'Change password' %}</a> /
298+                {% url admin:logout as logout_url %}
299+                {% if logout_url %}
300+                    <a href="{{ logout_url }}">
301+                {% else %}
302+                    <a href="{{ root_path }}logout/">
303+                {% endif %}
304+                {% trans 'Log out' %}</a>
305+            {% endblock %}
306+        </div>
307         {% endif %}
308         {% block nav-global %}{% endblock %}
309     </div>
310diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py
311index 10e97bb..6b41659 100644
312--- a/django/core/urlresolvers.py
313+++ b/django/core/urlresolvers.py
314@@ -139,7 +139,7 @@ class RegexURLPattern(object):
315     callback = property(_get_callback)
316 
317 class RegexURLResolver(object):
318-    def __init__(self, regex, urlconf_name, default_kwargs=None):
319+    def __init__(self, regex, urlconf_name, default_kwargs=None, app_name=None, namespace=None):
320         # regex is a string representing a regular expression.
321         # urlconf_name is a string representing the module containing URLconfs.
322         self.regex = re.compile(regex, re.UNICODE)
323@@ -148,19 +148,29 @@ class RegexURLResolver(object):
324             self._urlconf_module = self.urlconf_name
325         self.callback = None
326         self.default_kwargs = default_kwargs or {}
327-        self._reverse_dict = MultiValueDict()
328+        self.namespace = namespace
329+        self.app_name = app_name
330+        self._reverse_dict = None
331+        self._namespace_dict = None
332+        self._app_dict = None
333 
334     def __repr__(self):
335-        return '<%s %s %s>' % (self.__class__.__name__, self.urlconf_name, self.regex.pattern)
336-
337-    def _get_reverse_dict(self):
338-        if not self._reverse_dict:
339-            lookups = MultiValueDict()
340-            for pattern in reversed(self.url_patterns):
341-                p_pattern = pattern.regex.pattern
342-                if p_pattern.startswith('^'):
343-                    p_pattern = p_pattern[1:]
344-                if isinstance(pattern, RegexURLResolver):
345+        return '<%s %s (%s:%s) %s>' % (self.__class__.__name__, self.urlconf_name, self.app_name, self.namespace, self.regex.pattern)
346+
347+    def _populate(self):
348+        lookups = MultiValueDict()
349+        namespaces = {}
350+        apps = {}
351+        for pattern in reversed(self.url_patterns):
352+            p_pattern = pattern.regex.pattern
353+            if p_pattern.startswith('^'):
354+                p_pattern = p_pattern[1:]
355+            if isinstance(pattern, RegexURLResolver):
356+                if pattern.namespace:
357+                    namespaces[pattern.namespace] = (p_pattern, pattern)
358+                    if pattern.app_name:
359+                        apps.setdefault(pattern.app_name, []).append(pattern.namespace)
360+                else:
361                     parent = normalize(pattern.regex.pattern)
362                     for name in pattern.reverse_dict:
363                         for matches, pat in pattern.reverse_dict.getlist(name):
364@@ -168,14 +178,36 @@ class RegexURLResolver(object):
365                             for piece, p_args in parent:
366                                 new_matches.extend([(piece + suffix, p_args + args) for (suffix, args) in matches])
367                             lookups.appendlist(name, (new_matches, p_pattern + pat))
368-                else:
369-                    bits = normalize(p_pattern)
370-                    lookups.appendlist(pattern.callback, (bits, p_pattern))
371-                    lookups.appendlist(pattern.name, (bits, p_pattern))
372-            self._reverse_dict = lookups
373+                    for namespace, (prefix, sub_pattern) in pattern.namespace_dict.items():
374+                        namespaces[namespace] = (p_pattern + prefix, sub_pattern)
375+                    for app_name, namespace_list in pattern.app_dict.items():
376+                        apps.setdefault(app_name, []).extend(namespace_list)
377+            else:
378+                bits = normalize(p_pattern)
379+                lookups.appendlist(pattern.callback, (bits, p_pattern))
380+                lookups.appendlist(pattern.name, (bits, p_pattern))
381+        self._reverse_dict = lookups
382+        self._namespace_dict = namespaces
383+        self._app_dict = apps
384+
385+    def _get_reverse_dict(self):
386+        if self._reverse_dict is None:
387+            self._populate()
388         return self._reverse_dict
389     reverse_dict = property(_get_reverse_dict)
390 
391+    def _get_namespace_dict(self):
392+        if self._namespace_dict is None:
393+            self._populate()
394+        return self._namespace_dict
395+    namespace_dict = property(_get_namespace_dict)
396+
397+    def _get_app_dict(self):
398+        if self._app_dict is None:
399+            self._populate()
400+        return self._app_dict
401+    app_dict = property(_get_app_dict)
402+
403     def resolve(self, path):
404         tried = []
405         match = self.regex.search(path)
406@@ -261,12 +293,48 @@ class RegexURLResolver(object):
407 def resolve(path, urlconf=None):
408     return get_resolver(urlconf).resolve(path)
409 
410-def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None):
411+def reverse(viewname, urlconf=None, args=None, kwargs=None, prefix=None, app_name=None):
412+    print "REVERSE",viewname,app_name
413+    parts = viewname.split(':')
414+    parts.reverse()
415+    view = parts[0]
416+    path = parts[1:]
417+
418     args = args or []
419     kwargs = kwargs or {}
420     if prefix is None:
421         prefix = get_script_prefix()
422-    return iri_to_uri(u'%s%s' % (prefix, get_resolver(urlconf).reverse(viewname,
423+
424+    resolver = get_resolver(urlconf)
425+    resolver._populate()
426+    resolved_path = []
427+    while path:
428+        ns = path.pop()
429+        # Lookup the name to see if it could be an app identifier
430+        try:
431+            app_list = resolver.app_dict[ns]
432+            # Yes! Path part matches an app in the current Resolver
433+            if app_name and app_name in app_list:
434+                # If we are reversing for a particular app, use that namespace
435+                ns = app_name
436+            elif ns not in app_list:
437+                # The name isn't shared by one of the instances (i.e., the default)
438+                # so just pick the first instance as the default.
439+                ns = app_list[0]
440+        except KeyError:
441+            pass
442+
443+        try:
444+            extra, resolver = resolver.namespace_dict[ns]
445+            resolved_path.append(ns)
446+            prefix = prefix + extra
447+        except KeyError, key:
448+            if resolved_path:
449+                raise NoReverseMatch("%s is not a registered namespace inside '%s'" % (key, ':'.join(resolved_path)))
450+            else:
451+                raise NoReverseMatch("%s is not a registered namespace" % key)
452+
453+    return iri_to_uri(u'%s%s' % (prefix, resolver.reverse(view,
454             *args, **kwargs)))
455 
456 def clear_url_caches():
457diff --git a/django/template/context.py b/django/template/context.py
458index 0ccb5fa..e79af4d 100644
459--- a/django/template/context.py
460+++ b/django/template/context.py
461@@ -9,10 +9,11 @@ class ContextPopException(Exception):
462 
463 class Context(object):
464     "A stack container for variable context"
465-    def __init__(self, dict_=None, autoescape=True):
466+    def __init__(self, dict_=None, autoescape=True, app_name=None):
467         dict_ = dict_ or {}
468         self.dicts = [dict_]
469         self.autoescape = autoescape
470+        self.app_name = app_name
471 
472     def __repr__(self):
473         return repr(self.dicts)
474@@ -96,8 +97,8 @@ class RequestContext(Context):
475     Additional processors can be specified as a list of callables
476     using the "processors" keyword argument.
477     """
478-    def __init__(self, request, dict=None, processors=None):
479-        Context.__init__(self, dict)
480+    def __init__(self, request, dict=None, processors=None, app_name=None):
481+        Context.__init__(self, dict, app_name=app_name)
482         if processors is None:
483             processors = ()
484         else:
485diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
486index 7d91cd6..a61e965 100644
487--- a/django/template/defaulttags.py
488+++ b/django/template/defaulttags.py
489@@ -367,17 +367,17 @@ class URLNode(Node):
490         # {% url ... as var %} construct in which cause return nothing.
491         url = ''
492         try:
493-            url = reverse(self.view_name, args=args, kwargs=kwargs)
494+            url = reverse(self.view_name, args=args, kwargs=kwargs, app_name=context.app_name)
495         except NoReverseMatch, e:
496             if settings.SETTINGS_MODULE:
497                 project_name = settings.SETTINGS_MODULE.split('.')[0]
498                 try:
499                     url = reverse(project_name + '.' + self.view_name,
500-                              args=args, kwargs=kwargs)
501+                              args=args, kwargs=kwargs, app_name=context.app_name)
502                 except NoReverseMatch:
503                     if self.asvar is None:
504                         # Re-raise the original exception, not the one with
505-                        # the path relative to the project. This makes a
506+                        # the path relative to the project. This makes a
507                         # better error message.
508                         raise e
509             else:
510diff --git a/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py b/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py
511new file mode 100644
512index 0000000..08b8567
513--- /dev/null
514+++ b/tests/regressiontests/urlpatterns_reverse/included_namespace_urls.py
515@@ -0,0 +1,13 @@
516+from django.conf.urls.defaults import *
517+from namespace_urls import URLObject
518+
519+testobj3 = URLObject('test-ns3')
520+
521+urlpatterns = patterns('regressiontests.urlpatterns_reverse.views',
522+    url(r'^normal/$', 'empty_view', name='inc-normal-view'),
523+    url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='inc-normal-view'),
524+
525+    (r'^test3/', include(testobj3.urls)),
526+    (r'^ns-included3/', include('regressiontests.urlpatterns_reverse.included_urls', namespace='inc-ns3')),
527+)
528+
529diff --git a/tests/regressiontests/urlpatterns_reverse/namespace_urls.py b/tests/regressiontests/urlpatterns_reverse/namespace_urls.py
530new file mode 100644
531index 0000000..db00e49
532--- /dev/null
533+++ b/tests/regressiontests/urlpatterns_reverse/namespace_urls.py
534@@ -0,0 +1,31 @@
535+from django.conf.urls.defaults import *
536+
537+class URLObject(object):
538+    def __init__(self, namespace):
539+        self.namespace = namespace
540+
541+    def urls(self):
542+        return patterns('',
543+            url(r'^inner/$', 'empty_view', name='urlobject-view'),
544+            url(r'^inner/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='urlobject-view'),
545+        ), 'testapp', self.namespace
546+    urls = property(urls)
547+
548+testobj1 = URLObject('test-ns1')
549+testobj2 = URLObject('test-ns2')
550+default_testobj = URLObject('testapp')
551+
552+urlpatterns = patterns('regressiontests.urlpatterns_reverse.views',
553+    url(r'^normal/$', 'empty_view', name='normal-view'),
554+    url(r'^normal/(?P<arg1>\d+)/(?P<arg2>\d+)/$', 'empty_view', name='normal-view'),
555+
556+    (r'^test1/', include(testobj1.urls)),
557+    (r'^test2/', include(testobj2.urls)),
558+    (r'^default/', include(default_testobj.urls)),
559+
560+    (r'^ns-included1/', include('regressiontests.urlpatterns_reverse.included_namespace_urls', namespace='inc-ns1')),
561+    (r'^ns-included2/', include('regressiontests.urlpatterns_reverse.included_namespace_urls', namespace='inc-ns2')),
562+
563+    (r'^included/', include('regressiontests.urlpatterns_reverse.included_namespace_urls')),
564+
565+)
566diff --git a/tests/regressiontests/urlpatterns_reverse/tests.py b/tests/regressiontests/urlpatterns_reverse/tests.py
567index 9def6b2..cb6e22a 100644
568--- a/tests/regressiontests/urlpatterns_reverse/tests.py
569+++ b/tests/regressiontests/urlpatterns_reverse/tests.py
570@@ -158,4 +158,74 @@ class ReverseShortcutTests(TestCase):
571         res = redirect('/foo/')
572         self.assertEqual(res['Location'], '/foo/')
573         res = redirect('http://example.com/')
574-        self.assertEqual(res['Location'], 'http://example.com/')
575\ No newline at end of file
576+        self.assertEqual(res['Location'], 'http://example.com/')
577+
578+
579+class NamespaceTests(TestCase):
580+    urls = 'regressiontests.urlpatterns_reverse.namespace_urls'
581+
582+    def test_ambiguous_object(self):
583+        "Names deployed via dynamic URL objects that require namespaces can't be resolved"
584+        self.assertRaises(NoReverseMatch, reverse, 'urlobject-view')
585+        self.assertRaises(NoReverseMatch, reverse, 'urlobject-view', args=[37,42])
586+        self.assertRaises(NoReverseMatch, reverse, 'urlobject-view', kwargs={'arg1':42, 'arg2':37})
587+
588+    def test_ambiguous_urlpattern(self):
589+        "Names deployed via dynamic URL objects that require namespaces can't be resolved"
590+        self.assertRaises(NoReverseMatch, reverse, 'inner-nothing')
591+        self.assertRaises(NoReverseMatch, reverse, 'inner-nothing', args=[37,42])
592+        self.assertRaises(NoReverseMatch, reverse, 'inner-nothing', kwargs={'arg1':42, 'arg2':37})
593+
594+    def test_non_existent_namespace(self):
595+        "Non-existent namespaces raise errors"
596+        self.assertRaises(NoReverseMatch, reverse, 'blahblah:urlobject-view')
597+        self.assertRaises(NoReverseMatch, reverse, 'test-ns1:blahblah:urlobject-view')
598+
599+    def test_normal_name(self):
600+        "Normal lookups work as expected"
601+        self.assertEquals('/normal/', reverse('normal-view'))
602+        self.assertEquals('/normal/37/42/', reverse('normal-view', args=[37,42]))
603+        self.assertEquals('/normal/42/37/', reverse('normal-view', kwargs={'arg1':42, 'arg2':37}))
604+
605+    def test_simple_included_name(self):
606+        "Normal lookups work on names included from other patterns"
607+        self.assertEquals('/included/normal/', reverse('inc-normal-view'))
608+        self.assertEquals('/included/normal/37/42/', reverse('inc-normal-view', args=[37,42]))
609+        self.assertEquals('/included/normal/42/37/', reverse('inc-normal-view', kwargs={'arg1':42, 'arg2':37}))
610+
611+    def test_namespace_object(self):
612+        "Dynamic URL objects can be found using a namespace"
613+        self.assertEquals('/test1/inner/', reverse('test-ns1:urlobject-view'))
614+        self.assertEquals('/test1/inner/37/42/', reverse('test-ns1:urlobject-view', args=[37,42]))
615+        self.assertEquals('/test1/inner/42/37/', reverse('test-ns1:urlobject-view', kwargs={'arg1':42, 'arg2':37}))
616+
617+    def test_embedded_namespace_object(self):
618+        "Namespaces can be installed anywhere in the URL pattern tree"
619+        self.assertEquals('/included/test3/inner/', reverse('test-ns3:urlobject-view'))
620+        self.assertEquals('/included/test3/inner/37/42/', reverse('test-ns3:urlobject-view', args=[37,42]))
621+        self.assertEquals('/included/test3/inner/42/37/', reverse('test-ns3:urlobject-view', kwargs={'arg1':42, 'arg2':37}))
622+
623+    def test_namespace_pattern(self):
624+        "Namespaces can be applied to include()'d urlpatterns"
625+        self.assertEquals('/ns-included1/normal/', reverse('inc-ns1:inc-normal-view'))
626+        self.assertEquals('/ns-included1/normal/37/42/', reverse('inc-ns1:inc-normal-view', args=[37,42]))
627+        self.assertEquals('/ns-included1/normal/42/37/', reverse('inc-ns1:inc-normal-view', kwargs={'arg1':42, 'arg2':37}))
628+
629+    def test_multiple_namespace_pattern(self):
630+        "Namespaces can be embedded"
631+        self.assertEquals('/ns-included1/test3/inner/', reverse('inc-ns1:test-ns3:urlobject-view'))
632+        self.assertEquals('/ns-included1/test3/inner/37/42/', reverse('inc-ns1:test-ns3:urlobject-view', args=[37,42]))
633+        self.assertEquals('/ns-included1/test3/inner/42/37/', reverse('inc-ns1:test-ns3:urlobject-view', kwargs={'arg1':42, 'arg2':37}))
634+
635+    def test_app_lookup_object(self):
636+        "A default application namespace can be used for lookup"
637+        self.assertEquals('/default/inner/', reverse('testapp:urlobject-view'))
638+        self.assertEquals('/default/inner/37/42/', reverse('testapp:urlobject-view', args=[37,42]))
639+        self.assertEquals('/default/inner/42/37/', reverse('testapp:urlobject-view', kwargs={'arg1':42, 'arg2':37}))
640+
641+    def test_app_lookup_object_with_default(self):
642+        "A default application namespace is sensitive to the 'current' app can be used for lookup"
643+        self.assertEquals('/included/test3/inner/', reverse('testapp:urlobject-view', app_name='test-ns3'))
644+        self.assertEquals('/included/test3/inner/37/42/', reverse('testapp:urlobject-view', args=[37,42], app_name='test-ns3'))
645+        self.assertEquals('/included/test3/inner/42/37/', reverse('testapp:urlobject-view', kwargs={'arg1':42, 'arg2':37}, app_name='test-ns3'))
646+