|
1 """ |
|
2 Unit tests for reverse URL lookups. |
|
3 """ |
|
4 import unittest |
|
5 |
|
6 from django.conf import settings |
|
7 from django.core.exceptions import ImproperlyConfigured |
|
8 from django.core.urlresolvers import reverse, resolve, NoReverseMatch, Resolver404, RegexURLResolver |
|
9 from django.http import HttpResponseRedirect, HttpResponsePermanentRedirect |
|
10 from django.shortcuts import redirect |
|
11 from django.test import TestCase |
|
12 |
|
13 import urlconf_outer |
|
14 import urlconf_inner |
|
15 import middleware |
|
16 |
|
17 test_data = ( |
|
18 ('places', '/places/3/', [3], {}), |
|
19 ('places', '/places/3/', ['3'], {}), |
|
20 ('places', NoReverseMatch, ['a'], {}), |
|
21 ('places', NoReverseMatch, [], {}), |
|
22 ('places?', '/place/', [], {}), |
|
23 ('places+', '/places/', [], {}), |
|
24 ('places*', '/place/', [], {}), |
|
25 ('places2?', '/', [], {}), |
|
26 ('places2+', '/places/', [], {}), |
|
27 ('places2*', '/', [], {}), |
|
28 ('places3', '/places/4/', [4], {}), |
|
29 ('places3', '/places/harlem/', ['harlem'], {}), |
|
30 ('places3', NoReverseMatch, ['harlem64'], {}), |
|
31 ('places4', '/places/3/', [], {'id': 3}), |
|
32 ('people', NoReverseMatch, [], {}), |
|
33 ('people', '/people/adrian/', ['adrian'], {}), |
|
34 ('people', '/people/adrian/', [], {'name': 'adrian'}), |
|
35 ('people', NoReverseMatch, ['name with spaces'], {}), |
|
36 ('people', NoReverseMatch, [], {'name': 'name with spaces'}), |
|
37 ('people2', '/people/name/', [], {}), |
|
38 ('people2a', '/people/name/fred/', ['fred'], {}), |
|
39 ('optional', '/optional/fred/', [], {'name': 'fred'}), |
|
40 ('optional', '/optional/fred/', ['fred'], {}), |
|
41 ('hardcoded', '/hardcoded/', [], {}), |
|
42 ('hardcoded2', '/hardcoded/doc.pdf', [], {}), |
|
43 ('people3', '/people/il/adrian/', [], {'state': 'il', 'name': 'adrian'}), |
|
44 ('people3', NoReverseMatch, [], {'state': 'il'}), |
|
45 ('people3', NoReverseMatch, [], {'name': 'adrian'}), |
|
46 ('people4', NoReverseMatch, [], {'state': 'il', 'name': 'adrian'}), |
|
47 ('people6', '/people/il/test/adrian/', ['il/test', 'adrian'], {}), |
|
48 ('people6', '/people//adrian/', ['adrian'], {}), |
|
49 ('range', '/character_set/a/', [], {}), |
|
50 ('range2', '/character_set/x/', [], {}), |
|
51 ('price', '/price/$10/', ['10'], {}), |
|
52 ('price2', '/price/$10/', ['10'], {}), |
|
53 ('price3', '/price/$10/', ['10'], {}), |
|
54 ('product', '/product/chocolate+($2.00)/', [], {'price': '2.00', 'product': 'chocolate'}), |
|
55 ('headlines', '/headlines/2007.5.21/', [], dict(year=2007, month=5, day=21)), |
|
56 ('windows', r'/windows_path/C:%5CDocuments%20and%20Settings%5Cspam/', [], dict(drive_name='C', path=r'Documents and Settings\spam')), |
|
57 ('special', r'/special_chars/+%5C$*/', [r'+\$*'], {}), |
|
58 ('special', NoReverseMatch, [''], {}), |
|
59 ('mixed', '/john/0/', [], {'name': 'john'}), |
|
60 ('repeats', '/repeats/a/', [], {}), |
|
61 ('repeats2', '/repeats/aa/', [], {}), |
|
62 ('repeats3', '/repeats/aa/', [], {}), |
|
63 ('insensitive', '/CaseInsensitive/fred', ['fred'], {}), |
|
64 ('test', '/test/1', [], {}), |
|
65 ('test2', '/test/2', [], {}), |
|
66 ('inner-nothing', '/outer/42/', [], {'outer': '42'}), |
|
67 ('inner-nothing', '/outer/42/', ['42'], {}), |
|
68 ('inner-nothing', NoReverseMatch, ['foo'], {}), |
|
69 ('inner-extra', '/outer/42/extra/inner/', [], {'extra': 'inner', 'outer': '42'}), |
|
70 ('inner-extra', '/outer/42/extra/inner/', ['42', 'inner'], {}), |
|
71 ('inner-extra', NoReverseMatch, ['fred', 'inner'], {}), |
|
72 ('disjunction', NoReverseMatch, ['foo'], {}), |
|
73 ('inner-disjunction', NoReverseMatch, ['10', '11'], {}), |
|
74 ('extra-places', '/e-places/10/', ['10'], {}), |
|
75 ('extra-people', '/e-people/fred/', ['fred'], {}), |
|
76 ('extra-people', '/e-people/fred/', [], {'name': 'fred'}), |
|
77 ('part', '/part/one/', [], {'value': 'one'}), |
|
78 ('part', '/prefix/xx/part/one/', [], {'value': 'one', 'prefix': 'xx'}), |
|
79 ('part2', '/part2/one/', [], {'value': 'one'}), |
|
80 ('part2', '/part2/', [], {}), |
|
81 ('part2', '/prefix/xx/part2/one/', [], {'value': 'one', 'prefix': 'xx'}), |
|
82 ('part2', '/prefix/xx/part2/', [], {'prefix': 'xx'}), |
|
83 |
|
84 # Regression for #9038 |
|
85 # These views are resolved by method name. Each method is deployed twice - |
|
86 # once with an explicit argument, and once using the default value on |
|
87 # the method. This is potentially ambiguous, as you have to pick the |
|
88 # correct view for the arguments provided. |
|
89 ('kwargs_view', '/arg_view/', [], {}), |
|
90 ('kwargs_view', '/arg_view/10/', [], {'arg1':10}), |
|
91 ('regressiontests.urlpatterns_reverse.views.absolute_kwargs_view', '/absolute_arg_view/', [], {}), |
|
92 ('regressiontests.urlpatterns_reverse.views.absolute_kwargs_view', '/absolute_arg_view/10/', [], {'arg1':10}), |
|
93 ('non_path_include', '/includes/non_path_include/', [], {}) |
|
94 |
|
95 ) |
|
96 |
|
97 class NoURLPatternsTests(TestCase): |
|
98 urls = 'regressiontests.urlpatterns_reverse.no_urls' |
|
99 |
|
100 def assertRaisesErrorWithMessage(self, error, message, callable, |
|
101 *args, **kwargs): |
|
102 self.assertRaises(error, callable, *args, **kwargs) |
|
103 try: |
|
104 callable(*args, **kwargs) |
|
105 except error, e: |
|
106 self.assertEqual(message, str(e)) |
|
107 |
|
108 def test_no_urls_exception(self): |
|
109 """ |
|
110 RegexURLResolver should raise an exception when no urlpatterns exist. |
|
111 """ |
|
112 resolver = RegexURLResolver(r'^$', self.urls) |
|
113 |
|
114 self.assertRaisesErrorWithMessage(ImproperlyConfigured, |
|
115 "The included urlconf regressiontests.urlpatterns_reverse.no_urls "\ |
|
116 "doesn't have any patterns in it", getattr, resolver, 'url_patterns') |
|
117 |
|
118 class URLPatternReverse(TestCase): |
|
119 urls = 'regressiontests.urlpatterns_reverse.urls' |
|
120 |
|
121 def test_urlpattern_reverse(self): |
|
122 for name, expected, args, kwargs in test_data: |
|
123 try: |
|
124 got = reverse(name, args=args, kwargs=kwargs) |
|
125 except NoReverseMatch, e: |
|
126 self.assertEqual(expected, NoReverseMatch) |
|
127 else: |
|
128 self.assertEquals(got, expected) |
|
129 |
|
130 def test_reverse_none(self): |
|
131 # Reversing None should raise an error, not return the last un-named view. |
|
132 self.assertRaises(NoReverseMatch, reverse, None) |
|
133 |
|
134 class ResolverTests(unittest.TestCase): |
|
135 def test_non_regex(self): |
|
136 """ |
|
137 Verifies that we raise a Resolver404 if what we are resolving doesn't |
|
138 meet the basic requirements of a path to match - i.e., at the very |
|
139 least, it matches the root pattern '^/'. We must never return None |
|
140 from resolve, or we will get a TypeError further down the line. |
|
141 |
|
142 Regression for #10834. |
|
143 """ |
|
144 self.assertRaises(Resolver404, resolve, '') |
|
145 self.assertRaises(Resolver404, resolve, 'a') |
|
146 self.assertRaises(Resolver404, resolve, '\\') |
|
147 self.assertRaises(Resolver404, resolve, '.') |
|
148 |
|
149 class ReverseShortcutTests(TestCase): |
|
150 urls = 'regressiontests.urlpatterns_reverse.urls' |
|
151 |
|
152 def test_redirect_to_object(self): |
|
153 # We don't really need a model; just something with a get_absolute_url |
|
154 class FakeObj(object): |
|
155 def get_absolute_url(self): |
|
156 return "/hi-there/" |
|
157 |
|
158 res = redirect(FakeObj()) |
|
159 self.assert_(isinstance(res, HttpResponseRedirect)) |
|
160 self.assertEqual(res['Location'], '/hi-there/') |
|
161 |
|
162 res = redirect(FakeObj(), permanent=True) |
|
163 self.assert_(isinstance(res, HttpResponsePermanentRedirect)) |
|
164 self.assertEqual(res['Location'], '/hi-there/') |
|
165 |
|
166 def test_redirect_to_view_name(self): |
|
167 res = redirect('hardcoded2') |
|
168 self.assertEqual(res['Location'], '/hardcoded/doc.pdf') |
|
169 res = redirect('places', 1) |
|
170 self.assertEqual(res['Location'], '/places/1/') |
|
171 res = redirect('headlines', year='2008', month='02', day='17') |
|
172 self.assertEqual(res['Location'], '/headlines/2008.02.17/') |
|
173 self.assertRaises(NoReverseMatch, redirect, 'not-a-view') |
|
174 |
|
175 def test_redirect_to_url(self): |
|
176 res = redirect('/foo/') |
|
177 self.assertEqual(res['Location'], '/foo/') |
|
178 res = redirect('http://example.com/') |
|
179 self.assertEqual(res['Location'], 'http://example.com/') |
|
180 |
|
181 def test_redirect_view_object(self): |
|
182 from views import absolute_kwargs_view |
|
183 res = redirect(absolute_kwargs_view) |
|
184 self.assertEqual(res['Location'], '/absolute_arg_view/') |
|
185 self.assertRaises(NoReverseMatch, redirect, absolute_kwargs_view, wrong_argument=None) |
|
186 |
|
187 |
|
188 class NamespaceTests(TestCase): |
|
189 urls = 'regressiontests.urlpatterns_reverse.namespace_urls' |
|
190 |
|
191 def test_ambiguous_object(self): |
|
192 "Names deployed via dynamic URL objects that require namespaces can't be resolved" |
|
193 self.assertRaises(NoReverseMatch, reverse, 'urlobject-view') |
|
194 self.assertRaises(NoReverseMatch, reverse, 'urlobject-view', args=[37,42]) |
|
195 self.assertRaises(NoReverseMatch, reverse, 'urlobject-view', kwargs={'arg1':42, 'arg2':37}) |
|
196 |
|
197 def test_ambiguous_urlpattern(self): |
|
198 "Names deployed via dynamic URL objects that require namespaces can't be resolved" |
|
199 self.assertRaises(NoReverseMatch, reverse, 'inner-nothing') |
|
200 self.assertRaises(NoReverseMatch, reverse, 'inner-nothing', args=[37,42]) |
|
201 self.assertRaises(NoReverseMatch, reverse, 'inner-nothing', kwargs={'arg1':42, 'arg2':37}) |
|
202 |
|
203 def test_non_existent_namespace(self): |
|
204 "Non-existent namespaces raise errors" |
|
205 self.assertRaises(NoReverseMatch, reverse, 'blahblah:urlobject-view') |
|
206 self.assertRaises(NoReverseMatch, reverse, 'test-ns1:blahblah:urlobject-view') |
|
207 |
|
208 def test_normal_name(self): |
|
209 "Normal lookups work as expected" |
|
210 self.assertEquals('/normal/', reverse('normal-view')) |
|
211 self.assertEquals('/normal/37/42/', reverse('normal-view', args=[37,42])) |
|
212 self.assertEquals('/normal/42/37/', reverse('normal-view', kwargs={'arg1':42, 'arg2':37})) |
|
213 |
|
214 def test_simple_included_name(self): |
|
215 "Normal lookups work on names included from other patterns" |
|
216 self.assertEquals('/included/normal/', reverse('inc-normal-view')) |
|
217 self.assertEquals('/included/normal/37/42/', reverse('inc-normal-view', args=[37,42])) |
|
218 self.assertEquals('/included/normal/42/37/', reverse('inc-normal-view', kwargs={'arg1':42, 'arg2':37})) |
|
219 |
|
220 def test_namespace_object(self): |
|
221 "Dynamic URL objects can be found using a namespace" |
|
222 self.assertEquals('/test1/inner/', reverse('test-ns1:urlobject-view')) |
|
223 self.assertEquals('/test1/inner/37/42/', reverse('test-ns1:urlobject-view', args=[37,42])) |
|
224 self.assertEquals('/test1/inner/42/37/', reverse('test-ns1:urlobject-view', kwargs={'arg1':42, 'arg2':37})) |
|
225 |
|
226 def test_embedded_namespace_object(self): |
|
227 "Namespaces can be installed anywhere in the URL pattern tree" |
|
228 self.assertEquals('/included/test3/inner/', reverse('test-ns3:urlobject-view')) |
|
229 self.assertEquals('/included/test3/inner/37/42/', reverse('test-ns3:urlobject-view', args=[37,42])) |
|
230 self.assertEquals('/included/test3/inner/42/37/', reverse('test-ns3:urlobject-view', kwargs={'arg1':42, 'arg2':37})) |
|
231 |
|
232 def test_namespace_pattern(self): |
|
233 "Namespaces can be applied to include()'d urlpatterns" |
|
234 self.assertEquals('/ns-included1/normal/', reverse('inc-ns1:inc-normal-view')) |
|
235 self.assertEquals('/ns-included1/normal/37/42/', reverse('inc-ns1:inc-normal-view', args=[37,42])) |
|
236 self.assertEquals('/ns-included1/normal/42/37/', reverse('inc-ns1:inc-normal-view', kwargs={'arg1':42, 'arg2':37})) |
|
237 |
|
238 def test_multiple_namespace_pattern(self): |
|
239 "Namespaces can be embedded" |
|
240 self.assertEquals('/ns-included1/test3/inner/', reverse('inc-ns1:test-ns3:urlobject-view')) |
|
241 self.assertEquals('/ns-included1/test3/inner/37/42/', reverse('inc-ns1:test-ns3:urlobject-view', args=[37,42])) |
|
242 self.assertEquals('/ns-included1/test3/inner/42/37/', reverse('inc-ns1:test-ns3:urlobject-view', kwargs={'arg1':42, 'arg2':37})) |
|
243 |
|
244 def test_app_lookup_object(self): |
|
245 "A default application namespace can be used for lookup" |
|
246 self.assertEquals('/default/inner/', reverse('testapp:urlobject-view')) |
|
247 self.assertEquals('/default/inner/37/42/', reverse('testapp:urlobject-view', args=[37,42])) |
|
248 self.assertEquals('/default/inner/42/37/', reverse('testapp:urlobject-view', kwargs={'arg1':42, 'arg2':37})) |
|
249 |
|
250 def test_app_lookup_object_with_default(self): |
|
251 "A default application namespace is sensitive to the 'current' app can be used for lookup" |
|
252 self.assertEquals('/included/test3/inner/', reverse('testapp:urlobject-view', current_app='test-ns3')) |
|
253 self.assertEquals('/included/test3/inner/37/42/', reverse('testapp:urlobject-view', args=[37,42], current_app='test-ns3')) |
|
254 self.assertEquals('/included/test3/inner/42/37/', reverse('testapp:urlobject-view', kwargs={'arg1':42, 'arg2':37}, current_app='test-ns3')) |
|
255 |
|
256 def test_app_lookup_object_without_default(self): |
|
257 "An application namespace without a default is sensitive to the 'current' app can be used for lookup" |
|
258 self.assertEquals('/other2/inner/', reverse('nodefault:urlobject-view')) |
|
259 self.assertEquals('/other2/inner/37/42/', reverse('nodefault:urlobject-view', args=[37,42])) |
|
260 self.assertEquals('/other2/inner/42/37/', reverse('nodefault:urlobject-view', kwargs={'arg1':42, 'arg2':37})) |
|
261 |
|
262 self.assertEquals('/other1/inner/', reverse('nodefault:urlobject-view', current_app='other-ns1')) |
|
263 self.assertEquals('/other1/inner/37/42/', reverse('nodefault:urlobject-view', args=[37,42], current_app='other-ns1')) |
|
264 self.assertEquals('/other1/inner/42/37/', reverse('nodefault:urlobject-view', kwargs={'arg1':42, 'arg2':37}, current_app='other-ns1')) |
|
265 |
|
266 class RequestURLconfTests(TestCase): |
|
267 def setUp(self): |
|
268 self.root_urlconf = settings.ROOT_URLCONF |
|
269 self.middleware_classes = settings.MIDDLEWARE_CLASSES |
|
270 settings.ROOT_URLCONF = urlconf_outer.__name__ |
|
271 |
|
272 def tearDown(self): |
|
273 settings.ROOT_URLCONF = self.root_urlconf |
|
274 settings.MIDDLEWARE_CLASSES = self.middleware_classes |
|
275 |
|
276 def test_urlconf(self): |
|
277 response = self.client.get('/test/me/') |
|
278 self.assertEqual(response.status_code, 200) |
|
279 self.assertEqual(response.content, 'outer:/test/me/,' |
|
280 'inner:/inner_urlconf/second_test/') |
|
281 response = self.client.get('/inner_urlconf/second_test/') |
|
282 self.assertEqual(response.status_code, 200) |
|
283 response = self.client.get('/second_test/') |
|
284 self.assertEqual(response.status_code, 404) |
|
285 |
|
286 def test_urlconf_overridden(self): |
|
287 settings.MIDDLEWARE_CLASSES += ( |
|
288 '%s.ChangeURLconfMiddleware' % middleware.__name__, |
|
289 ) |
|
290 response = self.client.get('/test/me/') |
|
291 self.assertEqual(response.status_code, 404) |
|
292 response = self.client.get('/inner_urlconf/second_test/') |
|
293 self.assertEqual(response.status_code, 404) |
|
294 response = self.client.get('/second_test/') |
|
295 self.assertEqual(response.status_code, 200) |
|
296 self.assertEqual(response.content, 'outer:,inner:/second_test/') |
|
297 |
|
298 def test_urlconf_overridden_with_null(self): |
|
299 settings.MIDDLEWARE_CLASSES += ( |
|
300 '%s.NullChangeURLconfMiddleware' % middleware.__name__, |
|
301 ) |
|
302 self.assertRaises(ImproperlyConfigured, self.client.get, '/test/me/') |
|
303 |
|
304 class ErrorHandlerResolutionTests(TestCase): |
|
305 """Tests for handler404 and handler500""" |
|
306 |
|
307 def setUp(self): |
|
308 urlconf = 'regressiontests.urlpatterns_reverse.urls_error_handlers' |
|
309 urlconf_callables = 'regressiontests.urlpatterns_reverse.urls_error_handlers_callables' |
|
310 self.resolver = RegexURLResolver(r'^$', urlconf) |
|
311 self.callable_resolver = RegexURLResolver(r'^$', urlconf_callables) |
|
312 |
|
313 def test_named_handlers(self): |
|
314 from views import empty_view |
|
315 handler = (empty_view, {}) |
|
316 self.assertEqual(self.resolver.resolve404(), handler) |
|
317 self.assertEqual(self.resolver.resolve500(), handler) |
|
318 |
|
319 def test_callable_handers(self): |
|
320 from views import empty_view |
|
321 handler = (empty_view, {}) |
|
322 self.assertEqual(self.callable_resolver.resolve404(), handler) |
|
323 self.assertEqual(self.callable_resolver.resolve500(), handler) |
|
324 |
|
325 class NoRootUrlConfTests(TestCase): |
|
326 """Tests for handler404 and handler500 if urlconf is None""" |
|
327 urls = None |
|
328 |
|
329 def test_no_handler_exception(self): |
|
330 self.assertRaises(ImproperlyConfigured, self.client.get, '/test/me/') |