test_base.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520
  1. import time
  2. import unittest
  3. from django.core.exceptions import ImproperlyConfigured
  4. from django.http import HttpResponse
  5. from django.test import RequestFactory, SimpleTestCase, override_settings
  6. from django.test.utils import require_jinja2
  7. from django.urls import resolve
  8. from django.views.generic import RedirectView, TemplateView, View
  9. from . import views
  10. class SimpleView(View):
  11. """
  12. A simple view with a docstring.
  13. """
  14. def get(self, request):
  15. return HttpResponse('This is a simple view')
  16. class SimplePostView(SimpleView):
  17. post = SimpleView.get
  18. class PostOnlyView(View):
  19. def post(self, request):
  20. return HttpResponse('This view only accepts POST')
  21. class CustomizableView(SimpleView):
  22. parameter = {}
  23. def decorator(view):
  24. view.is_decorated = True
  25. return view
  26. class DecoratedDispatchView(SimpleView):
  27. @decorator
  28. def dispatch(self, request, *args, **kwargs):
  29. return super().dispatch(request, *args, **kwargs)
  30. class AboutTemplateView(TemplateView):
  31. def get(self, request):
  32. return self.render_to_response({})
  33. def get_template_names(self):
  34. return ['generic_views/about.html']
  35. class AboutTemplateAttributeView(TemplateView):
  36. template_name = 'generic_views/about.html'
  37. def get(self, request):
  38. return self.render_to_response(context={})
  39. class InstanceView(View):
  40. def get(self, request):
  41. return self
  42. class ViewTest(unittest.TestCase):
  43. rf = RequestFactory()
  44. def _assert_simple(self, response):
  45. self.assertEqual(response.status_code, 200)
  46. self.assertEqual(response.content, b'This is a simple view')
  47. def test_no_init_kwargs(self):
  48. """
  49. A view can't be accidentally instantiated before deployment
  50. """
  51. with self.assertRaises(AttributeError):
  52. SimpleView(key='value').as_view()
  53. def test_no_init_args(self):
  54. """
  55. A view can't be accidentally instantiated before deployment
  56. """
  57. with self.assertRaises(TypeError):
  58. SimpleView.as_view('value')
  59. def test_pathological_http_method(self):
  60. """
  61. The edge case of a http request that spoofs an existing method name is caught.
  62. """
  63. self.assertEqual(SimpleView.as_view()(
  64. self.rf.get('/', REQUEST_METHOD='DISPATCH')
  65. ).status_code, 405)
  66. def test_get_only(self):
  67. """
  68. Test a view which only allows GET doesn't allow other methods.
  69. """
  70. self._assert_simple(SimpleView.as_view()(self.rf.get('/')))
  71. self.assertEqual(SimpleView.as_view()(self.rf.post('/')).status_code, 405)
  72. self.assertEqual(SimpleView.as_view()(
  73. self.rf.get('/', REQUEST_METHOD='FAKE')
  74. ).status_code, 405)
  75. def test_get_and_head(self):
  76. """
  77. Test a view which supplies a GET method also responds correctly to HEAD.
  78. """
  79. self._assert_simple(SimpleView.as_view()(self.rf.get('/')))
  80. response = SimpleView.as_view()(self.rf.head('/'))
  81. self.assertEqual(response.status_code, 200)
  82. def test_head_no_get(self):
  83. """
  84. Test a view which supplies no GET method responds to HEAD with HTTP 405.
  85. """
  86. response = PostOnlyView.as_view()(self.rf.head('/'))
  87. self.assertEqual(response.status_code, 405)
  88. def test_get_and_post(self):
  89. """
  90. Test a view which only allows both GET and POST.
  91. """
  92. self._assert_simple(SimplePostView.as_view()(self.rf.get('/')))
  93. self._assert_simple(SimplePostView.as_view()(self.rf.post('/')))
  94. self.assertEqual(SimplePostView.as_view()(
  95. self.rf.get('/', REQUEST_METHOD='FAKE')
  96. ).status_code, 405)
  97. def test_invalid_keyword_argument(self):
  98. """
  99. View arguments must be predefined on the class and can't
  100. be named like a HTTP method.
  101. """
  102. # Check each of the allowed method names
  103. for method in SimpleView.http_method_names:
  104. with self.assertRaises(TypeError):
  105. SimpleView.as_view(**{method: 'value'})
  106. # Check the case view argument is ok if predefined on the class...
  107. CustomizableView.as_view(parameter="value")
  108. # ...but raises errors otherwise.
  109. with self.assertRaises(TypeError):
  110. CustomizableView.as_view(foobar="value")
  111. def test_calling_more_than_once(self):
  112. """
  113. Test a view can only be called once.
  114. """
  115. request = self.rf.get('/')
  116. view = InstanceView.as_view()
  117. self.assertNotEqual(view(request), view(request))
  118. def test_class_attributes(self):
  119. """
  120. The callable returned from as_view() has proper
  121. docstring, name and module.
  122. """
  123. self.assertEqual(SimpleView.__doc__, SimpleView.as_view().__doc__)
  124. self.assertEqual(SimpleView.__name__, SimpleView.as_view().__name__)
  125. self.assertEqual(SimpleView.__module__, SimpleView.as_view().__module__)
  126. def test_dispatch_decoration(self):
  127. """
  128. Attributes set by decorators on the dispatch method
  129. are also present on the closure.
  130. """
  131. self.assertTrue(DecoratedDispatchView.as_view().is_decorated)
  132. def test_options(self):
  133. """
  134. Views respond to HTTP OPTIONS requests with an Allow header
  135. appropriate for the methods implemented by the view class.
  136. """
  137. request = self.rf.options('/')
  138. view = SimpleView.as_view()
  139. response = view(request)
  140. self.assertEqual(200, response.status_code)
  141. self.assertTrue(response['Allow'])
  142. def test_options_for_get_view(self):
  143. """
  144. A view implementing GET allows GET and HEAD.
  145. """
  146. request = self.rf.options('/')
  147. view = SimpleView.as_view()
  148. response = view(request)
  149. self._assert_allows(response, 'GET', 'HEAD')
  150. def test_options_for_get_and_post_view(self):
  151. """
  152. A view implementing GET and POST allows GET, HEAD, and POST.
  153. """
  154. request = self.rf.options('/')
  155. view = SimplePostView.as_view()
  156. response = view(request)
  157. self._assert_allows(response, 'GET', 'HEAD', 'POST')
  158. def test_options_for_post_view(self):
  159. """
  160. A view implementing POST allows POST.
  161. """
  162. request = self.rf.options('/')
  163. view = PostOnlyView.as_view()
  164. response = view(request)
  165. self._assert_allows(response, 'POST')
  166. def _assert_allows(self, response, *expected_methods):
  167. "Assert allowed HTTP methods reported in the Allow response header"
  168. response_allows = set(response['Allow'].split(', '))
  169. self.assertEqual(set(expected_methods + ('OPTIONS',)), response_allows)
  170. def test_args_kwargs_request_on_self(self):
  171. """
  172. Test a view only has args, kwargs & request once `as_view`
  173. has been called.
  174. """
  175. bare_view = InstanceView()
  176. view = InstanceView.as_view()(self.rf.get('/'))
  177. for attribute in ('args', 'kwargs', 'request'):
  178. self.assertNotIn(attribute, dir(bare_view))
  179. self.assertIn(attribute, dir(view))
  180. def test_direct_instantiation(self):
  181. """
  182. It should be possible to use the view by directly instantiating it
  183. without going through .as_view() (#21564).
  184. """
  185. view = PostOnlyView()
  186. response = view.dispatch(self.rf.head('/'))
  187. self.assertEqual(response.status_code, 405)
  188. @override_settings(ROOT_URLCONF='generic_views.urls')
  189. class TemplateViewTest(SimpleTestCase):
  190. rf = RequestFactory()
  191. def _assert_about(self, response):
  192. response.render()
  193. self.assertContains(response, '<h1>About</h1>')
  194. def test_get(self):
  195. """
  196. Test a view that simply renders a template on GET
  197. """
  198. self._assert_about(AboutTemplateView.as_view()(self.rf.get('/about/')))
  199. def test_head(self):
  200. """
  201. Test a TemplateView responds correctly to HEAD
  202. """
  203. response = AboutTemplateView.as_view()(self.rf.head('/about/'))
  204. self.assertEqual(response.status_code, 200)
  205. def test_get_template_attribute(self):
  206. """
  207. Test a view that renders a template on GET with the template name as
  208. an attribute on the class.
  209. """
  210. self._assert_about(AboutTemplateAttributeView.as_view()(self.rf.get('/about/')))
  211. def test_get_generic_template(self):
  212. """
  213. Test a completely generic view that renders a template on GET
  214. with the template name as an argument at instantiation.
  215. """
  216. self._assert_about(TemplateView.as_view(template_name='generic_views/about.html')(self.rf.get('/about/')))
  217. def test_template_name_required(self):
  218. """
  219. A template view must provide a template name.
  220. """
  221. with self.assertRaises(ImproperlyConfigured):
  222. self.client.get('/template/no_template/')
  223. @require_jinja2
  224. def test_template_engine(self):
  225. """
  226. A template view may provide a template engine.
  227. """
  228. request = self.rf.get('/using/')
  229. view = TemplateView.as_view(template_name='generic_views/using.html')
  230. self.assertEqual(view(request).render().content, b'DTL\n')
  231. view = TemplateView.as_view(template_name='generic_views/using.html', template_engine='django')
  232. self.assertEqual(view(request).render().content, b'DTL\n')
  233. view = TemplateView.as_view(template_name='generic_views/using.html', template_engine='jinja2')
  234. self.assertEqual(view(request).render().content, b'Jinja2\n')
  235. def test_template_params(self):
  236. """
  237. A generic template view passes kwargs as context.
  238. """
  239. response = self.client.get('/template/simple/bar/')
  240. self.assertEqual(response.status_code, 200)
  241. self.assertEqual(response.context['foo'], 'bar')
  242. self.assertIsInstance(response.context['view'], View)
  243. def test_extra_template_params(self):
  244. """
  245. A template view can be customized to return extra context.
  246. """
  247. response = self.client.get('/template/custom/bar/')
  248. self.assertEqual(response.status_code, 200)
  249. self.assertEqual(response.context['foo'], 'bar')
  250. self.assertEqual(response.context['key'], 'value')
  251. self.assertIsInstance(response.context['view'], View)
  252. def test_cached_views(self):
  253. """
  254. A template view can be cached
  255. """
  256. response = self.client.get('/template/cached/bar/')
  257. self.assertEqual(response.status_code, 200)
  258. time.sleep(1.0)
  259. response2 = self.client.get('/template/cached/bar/')
  260. self.assertEqual(response2.status_code, 200)
  261. self.assertEqual(response.content, response2.content)
  262. time.sleep(2.0)
  263. # Let the cache expire and test again
  264. response2 = self.client.get('/template/cached/bar/')
  265. self.assertEqual(response2.status_code, 200)
  266. self.assertNotEqual(response.content, response2.content)
  267. def test_content_type(self):
  268. response = self.client.get('/template/content_type/')
  269. self.assertEqual(response['Content-Type'], 'text/plain')
  270. def test_resolve_view(self):
  271. match = resolve('/template/content_type/')
  272. self.assertIs(match.func.view_class, TemplateView)
  273. self.assertEqual(match.func.view_initkwargs['content_type'], 'text/plain')
  274. def test_resolve_login_required_view(self):
  275. match = resolve('/template/login_required/')
  276. self.assertIs(match.func.view_class, TemplateView)
  277. @override_settings(ROOT_URLCONF='generic_views.urls')
  278. class RedirectViewTest(SimpleTestCase):
  279. rf = RequestFactory()
  280. def test_no_url(self):
  281. "Without any configuration, returns HTTP 410 GONE"
  282. response = RedirectView.as_view()(self.rf.get('/foo/'))
  283. self.assertEqual(response.status_code, 410)
  284. def test_default_redirect(self):
  285. "Default is a temporary redirect"
  286. response = RedirectView.as_view(url='/bar/')(self.rf.get('/foo/'))
  287. self.assertEqual(response.status_code, 302)
  288. self.assertEqual(response.url, '/bar/')
  289. def test_permanent_redirect(self):
  290. "Permanent redirects are an option"
  291. response = RedirectView.as_view(url='/bar/', permanent=True)(self.rf.get('/foo/'))
  292. self.assertEqual(response.status_code, 301)
  293. self.assertEqual(response.url, '/bar/')
  294. def test_temporary_redirect(self):
  295. "Temporary redirects are an option"
  296. response = RedirectView.as_view(url='/bar/', permanent=False)(self.rf.get('/foo/'))
  297. self.assertEqual(response.status_code, 302)
  298. self.assertEqual(response.url, '/bar/')
  299. def test_include_args(self):
  300. "GET arguments can be included in the redirected URL"
  301. response = RedirectView.as_view(url='/bar/')(self.rf.get('/foo/'))
  302. self.assertEqual(response.status_code, 302)
  303. self.assertEqual(response.url, '/bar/')
  304. response = RedirectView.as_view(url='/bar/', query_string=True)(self.rf.get('/foo/?pork=spam'))
  305. self.assertEqual(response.status_code, 302)
  306. self.assertEqual(response.url, '/bar/?pork=spam')
  307. def test_include_urlencoded_args(self):
  308. "GET arguments can be URL-encoded when included in the redirected URL"
  309. response = RedirectView.as_view(url='/bar/', query_string=True)(
  310. self.rf.get('/foo/?unicode=%E2%9C%93'))
  311. self.assertEqual(response.status_code, 302)
  312. self.assertEqual(response.url, '/bar/?unicode=%E2%9C%93')
  313. def test_parameter_substitution(self):
  314. "Redirection URLs can be parameterized"
  315. response = RedirectView.as_view(url='/bar/%(object_id)d/')(self.rf.get('/foo/42/'), object_id=42)
  316. self.assertEqual(response.status_code, 302)
  317. self.assertEqual(response.url, '/bar/42/')
  318. def test_named_url_pattern(self):
  319. "Named pattern parameter should reverse to the matching pattern"
  320. response = RedirectView.as_view(pattern_name='artist_detail')(self.rf.get('/foo/'), pk=1)
  321. self.assertEqual(response.status_code, 302)
  322. self.assertEqual(response['Location'], '/detail/artist/1/')
  323. def test_named_url_pattern_using_args(self):
  324. response = RedirectView.as_view(pattern_name='artist_detail')(self.rf.get('/foo/'), 1)
  325. self.assertEqual(response.status_code, 302)
  326. self.assertEqual(response['Location'], '/detail/artist/1/')
  327. def test_redirect_POST(self):
  328. "Default is a temporary redirect"
  329. response = RedirectView.as_view(url='/bar/')(self.rf.post('/foo/'))
  330. self.assertEqual(response.status_code, 302)
  331. self.assertEqual(response.url, '/bar/')
  332. def test_redirect_HEAD(self):
  333. "Default is a temporary redirect"
  334. response = RedirectView.as_view(url='/bar/')(self.rf.head('/foo/'))
  335. self.assertEqual(response.status_code, 302)
  336. self.assertEqual(response.url, '/bar/')
  337. def test_redirect_OPTIONS(self):
  338. "Default is a temporary redirect"
  339. response = RedirectView.as_view(url='/bar/')(self.rf.options('/foo/'))
  340. self.assertEqual(response.status_code, 302)
  341. self.assertEqual(response.url, '/bar/')
  342. def test_redirect_PUT(self):
  343. "Default is a temporary redirect"
  344. response = RedirectView.as_view(url='/bar/')(self.rf.put('/foo/'))
  345. self.assertEqual(response.status_code, 302)
  346. self.assertEqual(response.url, '/bar/')
  347. def test_redirect_PATCH(self):
  348. "Default is a temporary redirect"
  349. response = RedirectView.as_view(url='/bar/')(self.rf.patch('/foo/'))
  350. self.assertEqual(response.status_code, 302)
  351. self.assertEqual(response.url, '/bar/')
  352. def test_redirect_DELETE(self):
  353. "Default is a temporary redirect"
  354. response = RedirectView.as_view(url='/bar/')(self.rf.delete('/foo/'))
  355. self.assertEqual(response.status_code, 302)
  356. self.assertEqual(response.url, '/bar/')
  357. def test_redirect_when_meta_contains_no_query_string(self):
  358. "regression for #16705"
  359. # we can't use self.rf.get because it always sets QUERY_STRING
  360. response = RedirectView.as_view(url='/bar/')(self.rf.request(PATH_INFO='/foo/'))
  361. self.assertEqual(response.status_code, 302)
  362. def test_direct_instantiation(self):
  363. """
  364. It should be possible to use the view without going through .as_view()
  365. (#21564).
  366. """
  367. view = RedirectView()
  368. response = view.dispatch(self.rf.head('/foo/'))
  369. self.assertEqual(response.status_code, 410)
  370. class GetContextDataTest(unittest.TestCase):
  371. def test_get_context_data_super(self):
  372. test_view = views.CustomContextView()
  373. context = test_view.get_context_data(kwarg_test='kwarg_value')
  374. # the test_name key is inserted by the test classes parent
  375. self.assertIn('test_name', context)
  376. self.assertEqual(context['kwarg_test'], 'kwarg_value')
  377. self.assertEqual(context['custom_key'], 'custom_value')
  378. # test that kwarg overrides values assigned higher up
  379. context = test_view.get_context_data(test_name='test_value')
  380. self.assertEqual(context['test_name'], 'test_value')
  381. def test_object_at_custom_name_in_context_data(self):
  382. # Checks 'pony' key presence in dict returned by get_context_date
  383. test_view = views.CustomSingleObjectView()
  384. test_view.context_object_name = 'pony'
  385. context = test_view.get_context_data()
  386. self.assertEqual(context['pony'], test_view.object)
  387. def test_object_in_get_context_data(self):
  388. # Checks 'object' key presence in dict returned by get_context_date #20234
  389. test_view = views.CustomSingleObjectView()
  390. context = test_view.get_context_data()
  391. self.assertEqual(context['object'], test_view.object)
  392. class UseMultipleObjectMixinTest(unittest.TestCase):
  393. rf = RequestFactory()
  394. def test_use_queryset_from_view(self):
  395. test_view = views.CustomMultipleObjectMixinView()
  396. test_view.get(self.rf.get('/'))
  397. # Don't pass queryset as argument
  398. context = test_view.get_context_data()
  399. self.assertEqual(context['object_list'], test_view.queryset)
  400. def test_overwrite_queryset(self):
  401. test_view = views.CustomMultipleObjectMixinView()
  402. test_view.get(self.rf.get('/'))
  403. queryset = [{'name': 'Lennon'}, {'name': 'Ono'}]
  404. self.assertNotEqual(test_view.queryset, queryset)
  405. # Overwrite the view's queryset with queryset from kwarg
  406. context = test_view.get_context_data(object_list=queryset)
  407. self.assertEqual(context['object_list'], queryset)
  408. class SingleObjectTemplateResponseMixinTest(unittest.TestCase):
  409. def test_template_mixin_without_template(self):
  410. """
  411. We want to makes sure that if you use a template mixin, but forget the
  412. template, it still tells you it's ImproperlyConfigured instead of
  413. TemplateDoesNotExist.
  414. """
  415. view = views.TemplateResponseWithoutTemplate()
  416. with self.assertRaises(ImproperlyConfigured):
  417. view.get_template_names()