test_base.py 22 KB

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