PageRenderTime 33ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/kitsune/questions/tests/test_templates.py

https://github.com/dbbhattacharya/kitsune
Python | 1542 lines | 1521 code | 14 blank | 7 comment | 1 complexity | f798251f05accc35e1a931fc61f4808d MD5 | raw file
Possible License(s): LGPL-2.1, Apache-2.0, GPL-2.0, BSD-3-Clause

Large files files are truncated, but you can click here to view the full file

  1. # -*- coding: utf-8 -*-
  2. import json
  3. import random
  4. from datetime import datetime, timedelta
  5. from string import letters
  6. from django.conf import settings
  7. from django.contrib.auth.models import User
  8. from django.contrib.sites.models import Site
  9. from django.core import mail
  10. import mock
  11. from nose.tools import eq_
  12. from pyquery import PyQuery as pq
  13. from taggit.models import Tag
  14. from tidings.models import Watch
  15. import kitsune.questions.tasks
  16. from kitsune.products.tests import product
  17. from kitsune.questions.events import QuestionReplyEvent, QuestionSolvedEvent
  18. from kitsune.questions.models import Question, Answer, VoteMetadata
  19. from kitsune.questions.tests import TestCaseBase, tags_eq, question, answer
  20. from kitsune.questions.views import UNAPPROVED_TAG, NO_TAG
  21. from kitsune.questions.cron import cache_top_contributors
  22. from kitsune.sumo.helpers import urlparams
  23. from kitsune.sumo.tests import (
  24. get, post, attrs_eq, emailmessage_raise_smtp, TestCase, LocalizingClient)
  25. from kitsune.sumo.urlresolvers import reverse
  26. from kitsune.tags.tests import tag
  27. from kitsune.products.tests import topic
  28. from kitsune.upload.models import ImageAttachment
  29. from kitsune.users.models import RegistrationProfile
  30. from kitsune.users.tests import user, add_permission
  31. class AnswersTemplateTestCase(TestCaseBase):
  32. """Test the Answers template."""
  33. def setUp(self):
  34. super(AnswersTemplateTestCase, self).setUp()
  35. self.user = user(save=True)
  36. self.client.login(username=self.user.username, password='testpass')
  37. self.question = answer(save=True).question
  38. self.answer = self.question.answers.all()[0]
  39. def test_answer(self):
  40. """Posting a valid answer inserts it."""
  41. num_answers = self.question.answers.count()
  42. content = 'lorem ipsum dolor sit amet'
  43. response = post(self.client, 'questions.reply',
  44. {'content': content},
  45. args=[self.question.id])
  46. eq_(1, len(response.redirect_chain))
  47. eq_(num_answers + 1, self.question.answers.count())
  48. new_answer = self.question.answers.order_by('-id')[0]
  49. eq_(content, new_answer.content)
  50. # Check canonical url
  51. doc = pq(response.content)
  52. eq_('/questions/%s' % self.question.id,
  53. doc('link[rel="canonical"]')[0].attrib['href'])
  54. def test_answer_upload(self):
  55. """Posting answer attaches an existing uploaded image to the answer."""
  56. f = open('kitsune/upload/tests/media/test.jpg')
  57. post(self.client, 'upload.up_image_async', {'image': f},
  58. args=['questions.Question', self.question.id])
  59. f.close()
  60. content = 'lorem ipsum dolor sit amet'
  61. response = post(self.client, 'questions.reply',
  62. {'content': content},
  63. args=[self.question.id])
  64. eq_(200, response.status_code)
  65. new_answer = self.question.answers.order_by('-id')[0]
  66. eq_(1, new_answer.images.count())
  67. image = new_answer.images.all()[0]
  68. name = '098f6b.png'
  69. message = 'File name "%s" does not contain "%s"' % (
  70. image.file.name, name)
  71. assert name in image.file.name, message
  72. eq_(self.user.username, image.creator.username)
  73. # Clean up
  74. ImageAttachment.objects.all().delete()
  75. def test_empty_answer(self):
  76. """Posting an empty answer shows error."""
  77. response = post(self.client, 'questions.reply', {'content': ''},
  78. args=[self.question.id])
  79. doc = pq(response.content)
  80. error_msg = doc('ul.errorlist li a')[0]
  81. eq_(error_msg.text, 'Please provide content.')
  82. def test_short_answer(self):
  83. """Posting a short answer shows error."""
  84. response = post(self.client, 'questions.reply', {'content': 'lor'},
  85. args=[self.question.id])
  86. doc = pq(response.content)
  87. error_msg = doc('ul.errorlist li a')[0]
  88. eq_(error_msg.text, 'Your content is too short (3 characters). ' +
  89. 'It must be at least 5 characters.')
  90. def test_long_answer(self):
  91. """Post a long answer shows error."""
  92. # Set up content length to 10,001 characters
  93. content = ''
  94. for i in range(1000):
  95. content += '1234567890'
  96. content += '1'
  97. response = post(self.client, 'questions.reply', {'content': content},
  98. args=[self.question.id])
  99. doc = pq(response.content)
  100. error_msg = doc('ul.errorlist li a')[0]
  101. eq_(error_msg.text, 'Please keep the length of your content to ' +
  102. '10,000 characters or less. It is currently ' +
  103. '10,001 characters.')
  104. def test_solve_unsolve(self):
  105. """Test accepting a solution and undoing."""
  106. response = get(self.client, 'questions.details',
  107. args=[self.question.id])
  108. doc = pq(response.content)
  109. eq_(0, len(doc('div.solution')))
  110. ans = self.question.answers.all()[0]
  111. # Sign in as asker, solve and verify
  112. self.client.login(
  113. username=self.question.creator.username, password='testpass')
  114. response = post(self.client, 'questions.solve',
  115. args=[self.question.id, ans.id])
  116. doc = pq(response.content)
  117. eq_(1, len(doc('div.solution')))
  118. div = doc('h3.is-solution')[0].getparent().getparent()
  119. eq_('answer-%s' % ans.id, div.attrib['id'])
  120. q = Question.uncached.get(pk=self.question.id)
  121. eq_(q.solution, ans)
  122. eq_(q.solver, self.question.creator)
  123. # Unsolve and verify
  124. response = post(self.client, 'questions.unsolve',
  125. args=[self.question.id, ans.id])
  126. q = Question.uncached.get(pk=self.question.id)
  127. eq_(q.solution, None)
  128. eq_(q.solver, None)
  129. def test_only_owner_or_admin_can_solve_unsolve(self):
  130. """Make sure non-owner/non-admin can't solve/unsolve."""
  131. # Try as asker
  132. self.client.login(
  133. username=self.question.creator.username, password='testpass')
  134. response = get(self.client, 'questions.details',
  135. args=[self.question.id])
  136. doc = pq(response.content)
  137. eq_(1, len(doc('input[name="solution"]')))
  138. self.client.logout()
  139. # Try as a nobody
  140. u = user(save=True)
  141. self.client.login(username=u.username, password='testpass')
  142. response = get(self.client, 'questions.details',
  143. args=[self.question.id])
  144. doc = pq(response.content)
  145. eq_(0, len(doc('input[name="solution"]')))
  146. ans = self.question.answers.all()[0]
  147. # Try to solve
  148. response = post(self.client, 'questions.solve',
  149. args=[self.question.id, ans.id])
  150. eq_(403, response.status_code)
  151. # Try to unsolve
  152. response = post(self.client, 'questions.unsolve',
  153. args=[self.question.id, ans.id])
  154. eq_(403, response.status_code)
  155. def test_solve_unsolve_with_perm(self):
  156. """Test marking solve/unsolve with 'change_solution' permission."""
  157. u = user(save=True)
  158. add_permission(u, Question, 'change_solution')
  159. self.client.login(username=u.username, password='testpass')
  160. ans = self.question.answers.all()[0]
  161. # Solve and verify
  162. post(self.client, 'questions.solve',
  163. args=[self.question.id, ans.id])
  164. q = Question.uncached.get(pk=self.question.id)
  165. eq_(q.solution, ans)
  166. eq_(q.solver, u)
  167. # Unsolve and verify
  168. post(self.client, 'questions.unsolve',
  169. args=[self.question.id, ans.id])
  170. q = Question.uncached.get(pk=self.question.id)
  171. eq_(q.solution, None)
  172. eq_(q.solver, None)
  173. def test_needs_info_checkbox(self):
  174. """Test that needs info checkbox is correctly shown"""
  175. response = get(self.client, 'questions.details',
  176. args=[self.question.id])
  177. doc = pq(response.content)
  178. eq_(1, len(doc('input[name="needsinfo"]')))
  179. self.question.set_needs_info()
  180. response = get(self.client, 'questions.details',
  181. args=[self.question.id])
  182. doc = pq(response.content)
  183. eq_(1, len(doc('input[name="clear_needsinfo"]')))
  184. def test_question_vote_GET(self):
  185. """Attempting to vote with HTTP GET returns a 405."""
  186. response = get(self.client, 'questions.vote',
  187. args=[self.question.id])
  188. eq_(405, response.status_code)
  189. def common_vote(self, me_too_count=1):
  190. """Helper method for question vote tests."""
  191. # Check that there are no votes and vote form renders
  192. response = get(self.client, 'questions.details',
  193. args=[self.question.id])
  194. doc = pq(response.content)
  195. assert '0\n' in doc('.have-problem')[0].text
  196. eq_(me_too_count, len(doc('div.me-too form')))
  197. # Vote
  198. ua = 'Mozilla/5.0 (DjangoTestClient)'
  199. self.client.post(reverse('questions.vote', args=[self.question.id]),
  200. {}, HTTP_USER_AGENT=ua)
  201. # Check that there is 1 vote and vote form doesn't render
  202. response = get(self.client, 'questions.details',
  203. args=[self.question.id])
  204. doc = pq(response.content)
  205. assert '1\n' in doc('.have-problem')[0].text
  206. eq_(0, len(doc('div.me-too form')))
  207. # Verify user agent
  208. vote_meta = VoteMetadata.objects.all()[0]
  209. eq_('ua', vote_meta.key)
  210. eq_(ua, vote_meta.value)
  211. # Voting again (same user) should not increment vote count
  212. post(self.client, 'questions.vote', args=[self.question.id])
  213. response = get(self.client, 'questions.details',
  214. args=[self.question.id])
  215. doc = pq(response.content)
  216. assert '1\n' in doc('.have-problem')[0].text
  217. def test_question_authenticated_vote(self):
  218. """Authenticated user vote."""
  219. # Common vote test
  220. self.common_vote()
  221. def test_question_anonymous_vote(self):
  222. """Anonymous user vote."""
  223. # Log out
  224. self.client.logout()
  225. # Common vote test
  226. self.common_vote(2)
  227. def common_answer_vote(self):
  228. """Helper method for answer vote tests."""
  229. # Check that there are no votes and vote form renders
  230. response = get(self.client, 'questions.details',
  231. args=[self.question.id])
  232. doc = pq(response.content)
  233. eq_(1, len(doc('form.helpful input[name="helpful"]')))
  234. # Vote
  235. ua = 'Mozilla/5.0 (DjangoTestClient)'
  236. self.client.post(reverse('questions.answer_vote',
  237. args=[self.question.id, self.answer.id]),
  238. {'helpful': 'y'}, HTTP_USER_AGENT=ua)
  239. # Check that there is 1 vote and vote form doesn't render
  240. response = get(self.client, 'questions.details',
  241. args=[self.question.id])
  242. doc = pq(response.content)
  243. eq_(1, len(doc('#answer-%s h3.is-helpful' % self.answer.id)))
  244. eq_(0, len(doc('form.helpful input[name="helpful"]')))
  245. # Verify user agent
  246. vote_meta = VoteMetadata.objects.all()[0]
  247. eq_('ua', vote_meta.key)
  248. eq_(ua, vote_meta.value)
  249. def test_answer_authenticated_vote(self):
  250. """Authenticated user answer vote."""
  251. # log in as new user (didn't ask or answer question)
  252. self.client.logout()
  253. u = user(save=True)
  254. self.client.login(username=u.username, password='testpass')
  255. # Common vote test
  256. self.common_answer_vote()
  257. def test_answer_anonymous_vote(self):
  258. """Anonymous user answer vote."""
  259. # Log out
  260. self.client.logout()
  261. # Common vote test
  262. self.common_answer_vote()
  263. def test_can_vote_on_asker_reply(self):
  264. """An answer posted by the asker can be voted on."""
  265. self.client.logout()
  266. # Post a new answer by the asker => two votable answers
  267. q = self.question
  268. Answer.objects.create(question=q, creator=q.creator, content='test')
  269. response = get(self.client, 'questions.details', args=[q.id])
  270. doc = pq(response.content)
  271. eq_(2, len(doc('form.helpful input[name="helpful"]')))
  272. def test_asker_can_vote(self):
  273. """The asker can vote Not/Helpful."""
  274. self.client.login(username=self.question.creator.username,
  275. password='testpass')
  276. self.common_answer_vote()
  277. def test_can_solve_with_answer_by_asker(self):
  278. """An answer posted by the asker can be the solution."""
  279. self.client.login(username=self.question.creator.username,
  280. password='testpass')
  281. # Post a new answer by the asker => two solvable answers
  282. q = self.question
  283. Answer.objects.create(question=q, creator=q.creator, content='test')
  284. response = get(self.client, 'questions.details', args=[q.id])
  285. doc = pq(response.content)
  286. eq_(2, len(doc('form.solution input[name="solution"]')))
  287. def test_delete_question_without_permissions(self):
  288. """Deleting a question without permissions is a 403."""
  289. u = user(save=True)
  290. self.client.login(username=u.username, password='testpass')
  291. response = get(self.client, 'questions.delete',
  292. args=[self.question.id])
  293. eq_(403, response.status_code)
  294. response = post(self.client, 'questions.delete',
  295. args=[self.question.id])
  296. eq_(403, response.status_code)
  297. def test_delete_question_logged_out(self):
  298. """Deleting a question while logged out redirects to login."""
  299. self.client.logout()
  300. response = get(self.client, 'questions.delete',
  301. args=[self.question.id])
  302. redirect = response.redirect_chain[0]
  303. eq_(302, redirect[1])
  304. eq_('http://testserver/%s%s?next=/en-US/questions/%s/delete' %
  305. (settings.LANGUAGE_CODE, settings.LOGIN_URL, self.question.id),
  306. redirect[0])
  307. response = post(self.client, 'questions.delete',
  308. args=[self.question.id])
  309. redirect = response.redirect_chain[0]
  310. eq_(302, redirect[1])
  311. eq_('http://testserver/%s%s?next=/en-US/questions/%s/delete' %
  312. (settings.LANGUAGE_CODE, settings.LOGIN_URL, self.question.id),
  313. redirect[0])
  314. def test_delete_question_with_permissions(self):
  315. """Deleting a question with permissions."""
  316. u = user(save=True)
  317. add_permission(u, Question, 'delete_question')
  318. self.client.login(username=u.username, password='testpass')
  319. response = get(self.client, 'questions.delete',
  320. args=[self.question.id])
  321. eq_(200, response.status_code)
  322. response = post(self.client, 'questions.delete',
  323. args=[self.question.id])
  324. eq_(0, Question.objects.filter(pk=self.question.id).count())
  325. def test_delete_answer_without_permissions(self):
  326. """Deleting an answer without permissions sends 403."""
  327. u = user(save=True)
  328. self.client.login(username=u.username, password='testpass')
  329. ans = self.question.last_answer
  330. response = get(self.client, 'questions.delete_answer',
  331. args=[self.question.id, ans.id])
  332. eq_(403, response.status_code)
  333. response = post(self.client, 'questions.delete_answer',
  334. args=[self.question.id, ans.id])
  335. eq_(403, response.status_code)
  336. def test_delete_answer_logged_out(self):
  337. """Deleting an answer while logged out redirects to login."""
  338. self.client.logout()
  339. q = self.question
  340. ans = q.last_answer
  341. response = get(self.client, 'questions.delete_answer',
  342. args=[self.question.id, ans.id])
  343. redirect = response.redirect_chain[0]
  344. eq_(302, redirect[1])
  345. eq_('http://testserver/%s%s?next=/en-US/questions/%s/delete/%s' %
  346. (settings.LANGUAGE_CODE, settings.LOGIN_URL, q.id, ans.id),
  347. redirect[0])
  348. response = post(self.client, 'questions.delete_answer',
  349. args=[self.question.id, ans.id])
  350. redirect = response.redirect_chain[0]
  351. eq_(302, redirect[1])
  352. eq_('http://testserver/%s%s?next=/en-US/questions/%s/delete/%s' %
  353. (settings.LANGUAGE_CODE, settings.LOGIN_URL, q.id, ans.id),
  354. redirect[0])
  355. def test_delete_answer_with_permissions(self):
  356. """Deleting an answer with permissions."""
  357. ans = self.question.last_answer
  358. u = user(save=True)
  359. add_permission(u, Answer, 'delete_answer')
  360. self.client.login(username=u.username, password='testpass')
  361. response = get(self.client, 'questions.delete_answer',
  362. args=[self.question.id, ans.id])
  363. eq_(200, response.status_code)
  364. response = post(self.client, 'questions.delete_answer',
  365. args=[self.question.id, ans.id])
  366. eq_(0, Answer.objects.filter(pk=self.question.id).count())
  367. def test_edit_answer_without_permission(self):
  368. """Editing an answer without permissions returns a 403.
  369. The edit link shouldn't show up on the Answers page."""
  370. response = get(self.client, 'questions.details',
  371. args=[self.question.id])
  372. doc = pq(response.content)
  373. eq_(0, len(doc('ol.answers li.edit')))
  374. answer = self.question.last_answer
  375. response = get(self.client, 'questions.edit_answer',
  376. args=[self.question.id, answer.id])
  377. eq_(403, response.status_code)
  378. content = 'New content for answer'
  379. response = post(self.client, 'questions.edit_answer',
  380. {'content': content},
  381. args=[self.question.id, answer.id])
  382. eq_(403, response.status_code)
  383. def test_edit_answer_with_permissions(self):
  384. """Editing an answer with permissions.
  385. The edit link should show up on the Answers page."""
  386. u = user(save=True)
  387. add_permission(u, Answer, 'change_answer')
  388. self.client.login(username=u.username, password='testpass')
  389. response = get(self.client, 'questions.details',
  390. args=[self.question.id])
  391. doc = pq(response.content)
  392. eq_(1, len(doc('li.edit')))
  393. answer = self.question.last_answer
  394. response = get(self.client, 'questions.edit_answer',
  395. args=[self.question.id, answer.id])
  396. eq_(200, response.status_code)
  397. content = 'New content for answer'
  398. response = post(self.client, 'questions.edit_answer',
  399. {'content': content},
  400. args=[self.question.id, answer.id])
  401. eq_(content, Answer.objects.get(pk=answer.id).content)
  402. def test_answer_creator_can_edit(self):
  403. """The creator of an answer can edit his/her answer."""
  404. u = user(save=True)
  405. self.client.login(username=u.username, password='testpass')
  406. # Initially there should be no edit links
  407. response = get(self.client, 'questions.details',
  408. args=[self.question.id])
  409. doc = pq(response.content)
  410. eq_(0, len(doc('ol.answers li.edit')))
  411. # Add an answer and verify the edit link shows up
  412. content = 'lorem ipsum dolor sit amet'
  413. response = post(self.client, 'questions.reply',
  414. {'content': content},
  415. args=[self.question.id])
  416. doc = pq(response.content)
  417. eq_(1, len(doc('li.edit')))
  418. new_answer = self.question.answers.order_by('-id')[0]
  419. eq_(1, len(doc('#answer-%s + div li.edit' % new_answer.id)))
  420. # Make sure it can be edited
  421. content = 'New content for answer'
  422. response = post(self.client, 'questions.edit_answer',
  423. {'content': content},
  424. args=[self.question.id, new_answer.id])
  425. eq_(200, response.status_code)
  426. # Now lock it and make sure it can't be edited
  427. self.question.is_locked = True
  428. self.question.save()
  429. response = post(self.client, 'questions.edit_answer',
  430. {'content': content},
  431. args=[self.question.id, new_answer.id])
  432. eq_(403, response.status_code)
  433. def test_lock_question_without_permissions(self):
  434. """Trying to lock a question without permission is a 403."""
  435. u = user(save=True)
  436. self.client.login(username=u.username, password='testpass')
  437. q = self.question
  438. response = post(self.client, 'questions.lock', args=[q.id])
  439. eq_(403, response.status_code)
  440. def test_lock_question_logged_out(self):
  441. """Trying to lock a question while logged out redirects to login."""
  442. self.client.logout()
  443. q = self.question
  444. response = post(self.client, 'questions.lock', args=[q.id])
  445. redirect = response.redirect_chain[0]
  446. eq_(302, redirect[1])
  447. eq_('http://testserver/%s%s?next=/en-US/questions/%s/lock' %
  448. (settings.LANGUAGE_CODE, settings.LOGIN_URL, q.id), redirect[0])
  449. def test_lock_question_with_permissions_GET(self):
  450. """Trying to lock a question via HTTP GET."""
  451. u = user(save=True)
  452. add_permission(u, Question, 'lock_question')
  453. self.client.login(username=u.username, password='testpass')
  454. response = get(self.client, 'questions.lock', args=[self.question.id])
  455. eq_(405, response.status_code)
  456. def test_lock_question_with_permissions_POST(self):
  457. """Locking questions with permissions via HTTP POST."""
  458. u = user(save=True)
  459. add_permission(u, Question, 'lock_question')
  460. self.client.login(username=u.username, password='testpass')
  461. q = self.question
  462. response = post(self.client, 'questions.lock', args=[q.id])
  463. eq_(200, response.status_code)
  464. eq_(True, Question.objects.get(pk=q.pk).is_locked)
  465. assert 'This thread was closed.' in response.content
  466. # now unlock it
  467. response = post(self.client, 'questions.lock', args=[q.id])
  468. eq_(200, response.status_code)
  469. eq_(False, Question.objects.get(pk=q.pk).is_locked)
  470. def test_reply_to_locked_question(self):
  471. """Locked questions can't be answered."""
  472. u = user(save=True)
  473. self.client.login(username=u.username, password='testpass')
  474. # Without add_answer permission, we should 403.
  475. q = self.question
  476. q.is_locked = True
  477. q.save()
  478. response = post(self.client, 'questions.reply',
  479. {'content': 'just testing'}, args=[q.id])
  480. eq_(403, response.status_code)
  481. # With add_answer permission, it should work.
  482. add_permission(u, Answer, 'add_answer')
  483. response = post(self.client, 'questions.reply',
  484. {'content': 'just testing'}, args=[q.id])
  485. eq_(200, response.status_code)
  486. def test_edit_answer_locked_question(self):
  487. """Verify edit answer of a locked question only with permissions."""
  488. self.question.is_locked = True
  489. self.question.save()
  490. # The answer creator can't edit if question is locked
  491. u = self.question.last_answer.creator
  492. self.client.login(username=u.username, password='testpass')
  493. response = get(self.client, 'questions.details',
  494. args=[self.question.id])
  495. doc = pq(response.content)
  496. eq_(0, len(doc('li.edit')))
  497. answer = self.question.last_answer
  498. response = get(self.client, 'questions.edit_answer',
  499. args=[self.question.id, answer.id])
  500. eq_(403, response.status_code)
  501. # A user with edit_answer permission can edit.
  502. u = user(save=True)
  503. self.client.login(username=u.username, password='testpass')
  504. add_permission(u, Answer, 'change_answer')
  505. response = get(self.client, 'questions.details',
  506. args=[self.question.id])
  507. doc = pq(response.content)
  508. eq_(1, len(doc('li.edit')))
  509. answer = self.question.last_answer
  510. response = get(self.client, 'questions.edit_answer',
  511. args=[self.question.id, answer.id])
  512. eq_(200, response.status_code)
  513. content = 'New content for answer'
  514. response = post(self.client, 'questions.edit_answer',
  515. {'content': content},
  516. args=[self.question.id, answer.id])
  517. eq_(content, Answer.objects.get(pk=answer.id).content)
  518. def test_vote_locked_question_403(self):
  519. """Locked questions can't be voted on."""
  520. u = user(save=True)
  521. self.client.login(username=u.username, password='testpass')
  522. q = self.question
  523. q.is_locked = True
  524. q.save()
  525. response = post(self.client, 'questions.vote', args=[q.id])
  526. eq_(403, response.status_code)
  527. def test_vote_answer_to_locked_question_403(self):
  528. """Answers to locked questions can't be voted on."""
  529. u = user(save=True)
  530. self.client.login(username=u.username, password='testpass')
  531. q = self.question
  532. q.is_locked = True
  533. q.save()
  534. response = post(self.client, 'questions.answer_vote',
  535. {'helpful': 'y'}, args=[q.id, self.answer.id])
  536. eq_(403, response.status_code)
  537. def test_watch_GET_405(self):
  538. """Watch replies with HTTP GET results in 405."""
  539. u = user(save=True)
  540. self.client.login(username=u.username, password='testpass')
  541. response = get(self.client, 'questions.watch',
  542. args=[self.question.id])
  543. eq_(405, response.status_code)
  544. def test_unwatch_GET_405(self):
  545. """Unwatch replies with HTTP GET results in 405."""
  546. u = user(save=True)
  547. self.client.login(username=u.username, password='testpass')
  548. response = get(self.client, 'questions.unwatch',
  549. args=[self.question.id])
  550. eq_(405, response.status_code)
  551. @mock.patch.object(Site.objects, 'get_current')
  552. def test_watch_replies(self, get_current):
  553. """Watch a question for replies."""
  554. get_current.return_value.domain = 'testserver'
  555. self.client.logout()
  556. # Delete existing watches
  557. Watch.objects.all().delete()
  558. post(self.client, 'questions.watch',
  559. {'email': 'some@bo.dy', 'event_type': 'reply'},
  560. args=[self.question.id])
  561. assert QuestionReplyEvent.is_notifying('some@bo.dy', self.question), (
  562. 'Watch was not created')
  563. attrs_eq(mail.outbox[1], to=['some@bo.dy'],
  564. subject='Please confirm your email address')
  565. assert 'questions/confirm/' in mail.outbox[1].body
  566. assert 'New answers' in mail.outbox[1].body
  567. # Now activate the watch.
  568. w = Watch.objects.get()
  569. get(self.client, 'questions.activate_watch', args=[w.id, w.secret])
  570. assert Watch.objects.get(id=w.id).is_active
  571. @mock.patch.object(mail.EmailMessage, 'send')
  572. def test_watch_replies_smtp_error(self, emailmessage_send):
  573. """Watch a question for replies and fail to send email."""
  574. emailmessage_send.side_effect = emailmessage_raise_smtp
  575. self.client.logout()
  576. r = post(self.client, 'questions.watch',
  577. {'email': 'some@bo.dy', 'event_type': 'reply'},
  578. args=[self.question.id])
  579. assert not QuestionReplyEvent.is_notifying(
  580. 'some@bo.dy', self.question), 'Watch was created'
  581. self.assertContains(r, 'Could not send a message to that email')
  582. @mock.patch.object(Site.objects, 'get_current')
  583. def test_watch_replies_wrong_secret(self, get_current):
  584. """Watch a question for replies."""
  585. # This also covers test_watch_solution_wrong_secret.
  586. get_current.return_value.domain = 'testserver'
  587. self.client.logout()
  588. # Delete existing watches
  589. Watch.objects.all().delete()
  590. post(self.client, 'questions.watch',
  591. {'email': 'some@bo.dy', 'event_type': 'reply'},
  592. args=[self.question.id])
  593. # Now activate the watch.
  594. w = Watch.objects.get()
  595. r = get(self.client, 'questions.activate_watch', args=[w.id, 'fail'])
  596. eq_(200, r.status_code)
  597. assert not Watch.objects.get(id=w.id).is_active
  598. def test_watch_replies_logged_in(self):
  599. """Watch a question for replies (logged in)."""
  600. u = user(save=True)
  601. self.client.login(username=u.username, password='testpass')
  602. u = User.objects.get(username=u.username)
  603. post(self.client, 'questions.watch',
  604. {'event_type': 'reply'},
  605. args=[self.question.id])
  606. assert QuestionReplyEvent.is_notifying(u, self.question), (
  607. 'Watch was not created')
  608. return u
  609. @mock.patch.object(Site.objects, 'get_current')
  610. def test_watch_solution(self, get_current):
  611. """Watch a question for solution."""
  612. self.client.logout()
  613. get_current.return_value.domain = 'testserver'
  614. # Delete existing watches
  615. Watch.objects.all().delete()
  616. post(self.client, 'questions.watch',
  617. {'email': 'some@bo.dy', 'event_type': 'solution'},
  618. args=[self.question.id])
  619. assert QuestionSolvedEvent.is_notifying('some@bo.dy', self.question), (
  620. 'Watch was not created')
  621. attrs_eq(mail.outbox[1], to=['some@bo.dy'],
  622. subject='Please confirm your email address')
  623. assert 'questions/confirm/' in mail.outbox[1].body
  624. assert 'Solution found' in mail.outbox[1].body
  625. # Now activate the watch.
  626. w = Watch.objects.get()
  627. get(self.client, 'questions.activate_watch', args=[w.id, w.secret])
  628. assert Watch.objects.get().is_active
  629. def test_unwatch(self):
  630. """Unwatch a question."""
  631. # First watch question.
  632. u = self.test_watch_replies_logged_in()
  633. # Then unwatch it.
  634. self.client.login(username=u.username, password='testpass')
  635. post(self.client, 'questions.unwatch', args=[self.question.id])
  636. assert not QuestionReplyEvent.is_notifying(u, self.question), (
  637. 'Watch was not destroyed')
  638. def test_watch_solution_and_replies(self):
  639. """User subscribes to solution and replies: page doesn't break"""
  640. u = user(save=True)
  641. self.client.login(username=u.username, password='testpass')
  642. QuestionReplyEvent.notify(u, self.question)
  643. QuestionSolvedEvent.notify(u, self.question)
  644. response = get(self.client, 'questions.details',
  645. args=[self.question.id])
  646. eq_(200, response.status_code)
  647. def test_preview_answer(self):
  648. """Preview an answer."""
  649. num_answers = self.question.answers.count()
  650. content = 'Awesome answer.'
  651. response = post(self.client, 'questions.reply',
  652. {'content': content, 'preview': 'any string'},
  653. args=[self.question.id])
  654. eq_(200, response.status_code)
  655. doc = pq(response.content)
  656. eq_(content, doc('#answer-preview div.main-content').text())
  657. eq_(num_answers, self.question.answers.count())
  658. def test_preview_answer_as_admin(self):
  659. """Preview an answer as admin and verify response is 200."""
  660. u = user(is_staff=True, is_superuser=True, save=True)
  661. self.client.login(username=u.username, password='testpass')
  662. content = 'Awesome answer.'
  663. response = post(self.client, 'questions.reply',
  664. {'content': content, 'preview': 'any string'},
  665. args=[self.question.id])
  666. eq_(200, response.status_code)
  667. def test_links_nofollow(self):
  668. """Links posted in questions and answers should have rel=nofollow."""
  669. q = self.question
  670. q.content = 'lorem http://ipsum.com'
  671. q.save()
  672. a = self.answer
  673. a.content = 'testing http://example.com'
  674. a.save()
  675. response = get(self.client, 'questions.details',
  676. args=[self.question.id])
  677. doc = pq(response.content)
  678. eq_('nofollow', doc('.question .main-content a')[0].attrib['rel'])
  679. eq_('nofollow', doc('.answer .main-content a')[0].attrib['rel'])
  680. def test_robots_noindex(self):
  681. """Verify noindex on unanswered questions over 30 days old."""
  682. q = question(save=True)
  683. # A brand new questions shouldn't be noindex'd...
  684. response = get(self.client, 'questions.details', args=[q.id])
  685. eq_(200, response.status_code)
  686. doc = pq(response.content)
  687. eq_(0, len(doc('meta[name=robots]')))
  688. # But a 31 day old question should be noindexed...
  689. q.created = datetime.now() - timedelta(days=31)
  690. q.save()
  691. response = get(self.client, 'questions.details', args=[q.id])
  692. eq_(200, response.status_code)
  693. doc = pq(response.content)
  694. eq_(1, len(doc('meta[name=robots]')))
  695. # Except if it has answers.
  696. answer(question=q, save=True)
  697. response = get(self.client, 'questions.details', args=[q.id])
  698. eq_(200, response.status_code)
  699. doc = pq(response.content)
  700. eq_(0, len(doc('meta[name=robots]')))
  701. class TaggingViewTestsAsTagger(TestCaseBase):
  702. """Tests for views that add and remove tags, logged in as someone who can
  703. add and remove but not create tags
  704. Also hits the tag-related parts of the answer template.
  705. """
  706. def setUp(self):
  707. super(TaggingViewTestsAsTagger, self).setUp()
  708. u = user(save=True)
  709. add_permission(u, Question, 'tag_question')
  710. self.client.login(username=u.username, password='testpass')
  711. self.question = question(content=u'lorém ipsuñ', save=True)
  712. # add_tag view:
  713. def test_add_tag_get_method(self):
  714. """Assert GETting the add_tag view redirects to the answers page."""
  715. response = self.client.get(_add_tag_url(self.question.id))
  716. url = 'http://testserver%s' % reverse(
  717. 'questions.details',
  718. kwargs={'question_id': self.question.id},
  719. force_locale=True)
  720. self.assertRedirects(response, url)
  721. def test_add_nonexistent_tag(self):
  722. """Assert adding a nonexistent tag sychronously shows an error."""
  723. response = self.client.post(_add_tag_url(self.question.id),
  724. data={'tag-name': 'nonexistent tag'})
  725. self.assertContains(response, UNAPPROVED_TAG)
  726. def test_add_existent_tag(self):
  727. """Test adding a tag, case insensitivity, and space stripping."""
  728. tag(name='PURplepurplepurple', slug='purplepurplepurple', save=True)
  729. response = self.client.post(_add_tag_url(self.question.id),
  730. data={'tag-name': ' PURplepurplepurple '},
  731. follow=True)
  732. self.assertContains(response, 'purplepurplepurple')
  733. def test_add_no_tag(self):
  734. """Make sure adding a blank tag shows an error message."""
  735. response = self.client.post(_add_tag_url(self.question.id),
  736. data={'tag-name': ''})
  737. self.assertContains(response, NO_TAG)
  738. # add_tag_async view:
  739. def test_add_async_nonexistent_tag(self):
  740. """Assert adding an nonexistent tag yields an AJAX error."""
  741. response = self.client.post(_add_async_tag_url(self.question.id),
  742. data={'tag-name': 'nonexistent tag'},
  743. HTTP_X_REQUESTED_WITH='XMLHttpRequest')
  744. self.assertContains(response, UNAPPROVED_TAG, status_code=400)
  745. def test_add_async_existent_tag(self):
  746. """Assert adding an unapplied tag."""
  747. tag(name='purplepurplepurple', slug='purplepurplepurple', save=True)
  748. response = self.client.post(_add_async_tag_url(self.question.id),
  749. data={'tag-name': ' PURplepurplepurple '},
  750. HTTP_X_REQUESTED_WITH='XMLHttpRequest')
  751. self.assertContains(response, 'canonicalName')
  752. tags = Question.objects.get(id=self.question.id).tags.all()
  753. # Test the backend since we don't have a newly rendered page to
  754. # rely on.
  755. eq_([t.name for t in tags], ['purplepurplepurple'])
  756. def test_add_async_no_tag(self):
  757. """Assert adding an empty tag asynchronously yields an AJAX error."""
  758. response = self.client.post(_add_async_tag_url(self.question.id),
  759. data={'tag-name': ''},
  760. HTTP_X_REQUESTED_WITH='XMLHttpRequest')
  761. self.assertContains(response, NO_TAG, status_code=400)
  762. # remove_tag view:
  763. def test_remove_applied_tag(self):
  764. """Assert removing an applied tag succeeds."""
  765. self.question.tags.add('green')
  766. self.question.tags.add('colorless')
  767. response = self.client.post(_remove_tag_url(self.question.id),
  768. data={'remove-tag-colorless': 'dummy'})
  769. self._assert_redirects_to_question(response, self.question.id)
  770. tags = Question.objects.get(pk=self.question.id).tags.all()
  771. eq_([t.name for t in tags], ['green'])
  772. def test_remove_unapplied_tag(self):
  773. """Test removing an unapplied tag fails silently."""
  774. response = self.client.post(_remove_tag_url(self.question.id),
  775. data={'remove-tag-lemon': 'dummy'})
  776. self._assert_redirects_to_question(response, self.question.id)
  777. def test_remove_no_tag(self):
  778. """Make sure removing with no params provided redirects harmlessly."""
  779. response = self.client.post(_remove_tag_url(self.question.id),
  780. data={})
  781. self._assert_redirects_to_question(response, self.question.id)
  782. def _assert_redirects_to_question(self, response, question_id):
  783. url = 'http://testserver%s' % reverse(
  784. 'questions.details', kwargs={'question_id': question_id},
  785. force_locale=True)
  786. self.assertRedirects(response, url)
  787. # remove_tag_async view:
  788. def test_remove_async_applied_tag(self):
  789. """Assert taking a tag off a question works."""
  790. self.question.tags.add('green')
  791. self.question.tags.add('colorless')
  792. response = self.client.post(_remove_async_tag_url(self.question.id),
  793. data={'name': 'colorless'},
  794. HTTP_X_REQUESTED_WITH='XMLHttpRequest')
  795. eq_(response.status_code, 200)
  796. tags = Question.objects.get(pk=self.question.id).tags.all()
  797. eq_([t.name for t in tags], ['green'])
  798. def test_remove_async_unapplied_tag(self):
  799. """Assert trying to remove a tag that isn't there succeeds."""
  800. response = self.client.post(_remove_async_tag_url(self.question.id),
  801. data={'name': 'lemon'},
  802. HTTP_X_REQUESTED_WITH='XMLHttpRequest')
  803. eq_(response.status_code, 200)
  804. def test_remove_async_no_tag(self):
  805. """Assert calling the remove handler with no param fails."""
  806. response = self.client.post(_remove_async_tag_url(self.question.id),
  807. data={},
  808. HTTP_X_REQUESTED_WITH='XMLHttpRequest')
  809. self.assertContains(response, NO_TAG, status_code=400)
  810. @mock.patch.object(kitsune.questions.tasks, 'submit_ticket')
  811. @mock.patch.object(Site.objects, 'get_current')
  812. def test_escalate_tag(self, get_current, submit_ticket):
  813. """Verify that tagging a question "escalate" submits to zendesk."""
  814. get_current.return_value.domain = 'testserver'
  815. tag(name='escalate', slug='escalate', save=True)
  816. self.client.post(
  817. _add_tag_url(self.question.id),
  818. data={'tag-name': 'escalate'},
  819. follow=True)
  820. question_url = u'https://testserver/en-US{url}'.format(
  821. url=self.question.get_absolute_url())
  822. submit_ticket.assert_called_with(
  823. email='support@mozilla.com',
  824. category='Escalated',
  825. subject=u'[Escalated] {title}'.format(title=self.question.title),
  826. body=u'{url}\n\n{content}'.format(
  827. url=question_url, content=self.question.content),
  828. tags=['escalate'])
  829. class TaggingViewTestsAsAdmin(TestCaseBase):
  830. """Tests for views that create new tags, logged in as someone who can"""
  831. def setUp(self):
  832. super(TaggingViewTestsAsAdmin, self).setUp()
  833. u = user(save=True)
  834. add_permission(u, Question, 'tag_question')
  835. add_permission(u, Tag, 'add_tag')
  836. self.client.login(username=u.username, password='testpass')
  837. self.question = question(save=True)
  838. tag(name='red', slug='red', save=True)
  839. def test_add_new_tag(self):
  840. """Assert adding a nonexistent tag sychronously creates & adds it."""
  841. self.client.post(
  842. _add_tag_url(self.question.id),
  843. data={'tag-name': 'nonexistent tag'})
  844. tags_eq(Question.objects.get(id=self.question.id),
  845. ['nonexistent tag'])
  846. def test_add_async_new_tag(self):
  847. """Assert adding an nonexistent tag creates & adds it."""
  848. response = self.client.post(_add_async_tag_url(self.question.id),
  849. data={'tag-name': 'nonexistent tag'},
  850. HTTP_X_REQUESTED_WITH='XMLHttpRequest')
  851. eq_(response.status_code, 200)
  852. tags_eq(Question.objects.get(id=self.question.id), ['nonexistent tag'])
  853. def test_add_new_case_insensitive(self):
  854. """Adding a tag differing only in case from existing ones shouldn't
  855. create a new tag."""
  856. self.client.post(_add_async_tag_url(self.question.id),
  857. data={'tag-name': 'RED'},
  858. HTTP_X_REQUESTED_WITH='XMLHttpRequest')
  859. tags_eq(Question.objects.get(id=self.question.id), ['red'])
  860. def test_add_new_canonicalizes(self):
  861. """Adding a new tag as an admin should still canonicalize case."""
  862. response = self.client.post(_add_async_tag_url(self.question.id),
  863. data={'tag-name': 'RED'},
  864. HTTP_X_REQUESTED_WITH='XMLHttpRequest')
  865. eq_(json.loads(response.content)['canonicalName'], 'red')
  866. def _add_tag_url(question_id):
  867. """Return the URL to add_tag for question 1, an untagged question."""
  868. return reverse('questions.add_tag', kwargs={'question_id': question_id})
  869. def _add_async_tag_url(question_id):
  870. """Return the URL to add_tag_async for question 1, an untagged question."""
  871. return reverse(
  872. 'questions.add_tag_async', kwargs={'question_id': question_id})
  873. def _remove_tag_url(question_id):
  874. """Return URL to remove_tag for question 2, tagged {colorless, green}."""
  875. return reverse(
  876. 'questions.remove_tag', kwargs={'question_id': question_id})
  877. def _remove_async_tag_url(question_id):
  878. """Return URL to remove_tag_async on q. 2, tagged {colorless, green}."""
  879. return reverse(
  880. 'questions.remove_tag_async', kwargs={'question_id': question_id})
  881. class QuestionsTemplateTestCase(TestCaseBase):
  882. def test_contributed_badge(self):
  883. u = user(save=True)
  884. q1 = answer(creator=u, save=True).question
  885. q2 = answer(save=True).question
  886. # u should have a contributor badge on q1 but not q2
  887. self.client.login(username=u.username, password="testpass")
  888. response = self.client.get(
  889. urlparams(reverse('questions.list', args=['all']), show='all'))
  890. doc = pq(response.content)
  891. eq_(1,
  892. len(doc('#question-%s .thread-contributed.highlighted' % q1.id)))
  893. eq_(0,
  894. len(doc('#question-%s .thread-contributed.highlighted' % q2.id)))
  895. def test_top_contributors(self):
  896. # There should be no top contributors since there are no solutions.
  897. cache_top_contributors()
  898. response = self.client.get(reverse('questions.list', args=['all']))
  899. doc = pq(response.content)
  900. eq_(0, len(doc('#top-contributors ol li')))
  901. # Solve a question and verify we now have a top conributor.
  902. a = answer(save=True)
  903. a.question.solution = a
  904. a.question.save()
  905. cache_top_contributors()
  906. response = self.client.get(reverse('questions.list', args=['all']))
  907. doc = pq(response.content)
  908. lis = doc('#top-contributors ol li')
  909. eq_(1, len(lis))
  910. eq_(a.creator.username, lis[0].text)
  911. # Make answer 8 days old. There should no be top contributors.
  912. a.created = datetime.now() - timedelta(days=8)
  913. a.save()
  914. cache_top_contributors()
  915. response = self.client.get(reverse('questions.list', args=['all']))
  916. doc = pq(response.content)
  917. eq_(0, len(doc('#top-contributors ol li')))
  918. def test_tagged(self):
  919. u = user(save=True)
  920. add_permission(u, Question, 'tag_question')
  921. tagname = 'mobile'
  922. tag(name=tagname, slug=tagname, save=True)
  923. self.client.login(username=u.username, password="testpass")
  924. tagged = urlparams(reverse(
  925. 'questions.list', args=['all']), tagged=tagname, show='all')
  926. # First there should be no questions tagged 'mobile'
  927. response = self.client.get(tagged)
  928. doc = pq(response.content)
  929. eq_(0, len(doc('article.questions > section')))
  930. # Tag a question 'mobile'
  931. q = question(save=True)
  932. response = post(self.client, 'questions.add_tag',
  933. {'tag-name': tagname},
  934. args=[q.id])
  935. eq_(200, response.status_code)
  936. # Add an answer
  937. answer(question=q, save=True)
  938. # Now there should be 1 question tagged 'mobile'
  939. response = self.client.get(tagged)
  940. doc = pq(response.content)
  941. eq_(1, len(doc('article.questions > section')))
  942. eq_('/questions/all?tagged=mobile&show=all',
  943. doc('link[rel="canonical"]')[0].attrib['href'])
  944. # Test a tag that doesn't exist. It shouldnt blow up.
  945. url = urlparams(
  946. reverse('questions.list', args=['all']),
  947. tagged='garbage-plate',
  948. show='all')
  949. response = self.client.get(url)
  950. eq_(200, response.status_code)
  951. def test_product_filter(self):
  952. p1 = product(save=True)
  953. p2 = product(save=True)
  954. p3 = product(save=True)
  955. q1 = question(save=True)
  956. q2 = question(save=True)
  957. q2.products.add(p1)
  958. q2.save()
  959. q3 = question(save=True)
  960. q3.products.add(p1, p2)
  961. q3.save()
  962. def check(product, expected):
  963. url = reverse('questions.list', args=[product])
  964. response = self.client.get(url)
  965. doc = pq(response.content)
  966. # Make sure all questions are there.
  967. # This won't work, because the test case base adds more tests than
  968. # we expect in it's setUp(). TODO: Fix that.
  969. eq_(len(expected), len(doc('.questions > section')))
  970. for q in expected:
  971. eq_(1, len(doc('.questions > section[id=question-%s]' % q.id)))
  972. # No filtering -> All questions.
  973. check('all', [q1, q2, q3])
  974. # Filter on p1 -> only q2 and q3
  975. check(p1.slug, [q2, q3])
  976. # Filter on p2 -> only q3
  977. check(p2.slug, [q3])
  978. # Filter on p3 -> No results
  979. check(p3.slug, [])
  980. # Filter on p1,p2
  981. check('%s,%s' % (p1.slug, p2.slug), [q2, q3])
  982. # Filter on p1,p3
  983. check('%s,%s' % (p1.slug, p3.slug), [q2, q3])
  984. # Filter on p2,p3
  985. check('%s,%s' % (p2.slug, p3.slug), [q3])
  986. def test_topic_filter(self):
  987. p = product(save=True)
  988. t1 = topic(product=p, save=True)
  989. t2 = topic(product=p, save=True)
  990. t3 = topic(product=p, save=True)
  991. q1 = question(save=True)
  992. q2 = question(save=True)
  993. q2.topics.add(t1)
  994. q2.save()
  995. q3 = question(save=True)
  996. q3.topics.add(t1, t2)
  997. q3.save()
  998. url = reverse('questions.list', args=['all'])
  999. def check(filter, expected):
  1000. response = self.client.get(urlparams(url, **filter))
  1001. doc = pq(response.content)
  1002. # Make sure all questions are there.
  1003. # This won't work, because the test case base adds more tests than
  1004. # we expect in it's setUp(). TODO: Fix that.
  1005. # eq_(len(expected), len(doc('.questions > section')))
  1006. for q in expected:
  1007. eq_(1, len(doc('.questions > section[id=question-%s]' % q.id)))
  1008. # No filtering -> All questions.
  1009. check({}, [q1, q2, q3])
  1010. # Filter on p1 -> only q2 and q3
  1011. check({'topic': t1.slug}, [q2, q3])
  1012. # Filter on p2 -> only q3
  1013. check({'topic': t2.slug}, [q3])
  1014. # Filter on p3 -> No results
  1015. check({'topic': t3.slug}, [])
  1016. def test_robots_noindex(self):
  1017. """Verify the page is set for noindex by robots."""
  1018. response = self.client.get(reverse('questions.list', args=['all']))
  1019. eq_(200, response.status_code)
  1020. doc = pq(response.content)
  1021. eq_(1, len(doc('meta[name=robots]')))
  1022. def test_select_in_question(self):
  1023. """Verify we properly escape <select/>."""
  1024. question(
  1025. title='test question…

Large files files are truncated, but you can click here to view the full file