PageRenderTime 59ms CodeModel.GetById 19ms 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
  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 lorem ipsum <select></select>',
  1026. content='test question content lorem ipsum <select></select>',
  1027. save=True)
  1028. response = self.client.get(reverse('questions.list', args=['all']))
  1029. assert 'test question lorem ipsum' in response.content
  1030. assert 'test question content lorem ipsum' in response.content
  1031. doc = pq(response.content)
  1032. eq_(0, len(doc('article.questions select')))
  1033. def test_truncated_text_is_stripped(self):
  1034. """Verify we strip html from truncated text."""
  1035. long_str = ''.join(random.choice(letters) for x in xrange(170))
  1036. question(
  1037. content='<p>%s</p>' % long_str,
  1038. save=True)
  1039. response = self.client.get(reverse('questions.list', args=['all']))
  1040. # Verify that the <p> was stripped
  1041. assert '<p class="short-text"><p>' not in response.content
  1042. assert '<p class="short-text">%s' % long_str[:5] in response.content
  1043. def test_views(self):
  1044. """Verify the view count is displayed correctly."""
  1045. q = question(save=True)
  1046. q.questionvisits_set.create(visits=1007)
  1047. response = self.client.get(reverse('questions.list', args=['all']))
  1048. doc = pq(response.content)
  1049. eq_('1007 views', doc('div.views').text())
  1050. def test_no_unarchive_on_old_questions(self):
  1051. ques = question(save=True,
  1052. created=(datetime.now() - timedelta(days=200)),
  1053. is_archived=True)
  1054. response = get(self.client, 'questions.details', args=[ques.id])
  1055. assert 'Archive this post' not in response.content
  1056. class QuestionsTemplateTestCaseNoFixtures(TestCase):
  1057. client_class = LocalizingClient
  1058. def test_locked_questions_dont_appear(self):
  1059. """Locked questions are not listed on the no-replies list."""
  1060. question(save=True)
  1061. question(save=True)
  1062. question(is_locked=True, save=True)
  1063. url = reverse('questions.list', args=['all'])
  1064. url = urlparams(url, filter='no-replies')
  1065. response = self.client.get(url)
  1066. doc = pq(response.content)
  1067. eq_(2, len(doc('article.questions > section')))
  1068. class QuestionEditingTests(TestCaseBase):
  1069. """Tests for the question-editing view and templates"""
  1070. def setUp(self):
  1071. super(QuestionEditingTests, self).setUp()
  1072. self.user = user(save=True)
  1073. add_permission(self.user, Question, 'change_question')
  1074. self.client.login(username=self.user.username, password='testpass')
  1075. def test_extra_fields(self):
  1076. """The edit-question form should show appropriate metadata fields."""
  1077. question_id = question(save=True).id
  1078. response = get(self.client, 'questions.edit_question',
  1079. kwargs={'question_id': question_id})
  1080. eq_(response.status_code, 200)
  1081. # Make sure each extra metadata field is in the form:
  1082. doc = pq(response.content)
  1083. q = Question.objects.get(pk=question_id)
  1084. extra_fields = (q.product.get('extra_fields', []) +
  1085. q.category.get('extra_fields', []))
  1086. for field in extra_fields:
  1087. assert (doc('input[name=%s]' % field) or
  1088. doc('textarea[name=%s]' % field)), (
  1089. "The %s field is missing from the edit page." % field)
  1090. def test_no_extra_fields(self):
  1091. """The edit-question form shouldn't show inappropriate metadata."""
  1092. question_id = question(save=True).id
  1093. response = get(self.client, 'questions.edit_question',
  1094. kwargs={'question_id': question_id})
  1095. eq_(response.status_code, 200)
  1096. # Take the "os" field as representative. Make sure it doesn't show up:
  1097. doc = pq(response.content)
  1098. assert not doc('input[name=os]')
  1099. def test_post(self):
  1100. """Posting a valid edit form should save the question."""
  1101. p = product(slug='desktop', save=True)
  1102. q = question(save=True)
  1103. q.products.add(p)
  1104. response = post(self.client, 'questions.edit_question',
  1105. {'title': 'New title',
  1106. 'content': 'New content',
  1107. 'ff_version': 'New version'},
  1108. kwargs={'question_id': q.id})
  1109. # Make sure the form redirects and thus appears to succeed:
  1110. url = 'http://testserver%s' % reverse('questions.details',
  1111. kwargs={'question_id': q.id},
  1112. force_locale=True)
  1113. self.assertRedirects(response, url)
  1114. # Make sure the static fields, the metadata, and the updated_by field
  1115. # changed:
  1116. q = Question.objects.get(pk=q.id)
  1117. eq_(q.title, 'New title')
  1118. eq_(q.content, 'New content')
  1119. eq_(q.updated_by, self.user)
  1120. class AAQTemplateTestCase(TestCaseBase):
  1121. """Test the AAQ template."""
  1122. data = {'title': 'A test question',
  1123. 'content': 'I have this question that I hope...',
  1124. 'sites_affected': 'http://example.com',
  1125. 'ff_version': '3.6.6',
  1126. 'os': 'Intel Mac OS X 10.6',
  1127. 'plugins': '* Shockwave Flash 10.1 r53',
  1128. 'useragent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8.3) '
  1129. 'Gecko/20120221 Firefox/18.0',
  1130. 'troubleshooting': '''{
  1131. "accessibility": {
  1132. "isActive": true
  1133. },
  1134. "application": {
  1135. "name": "Firefox",
  1136. "supportURL": "Some random url.",
  1137. "userAgent": "A user agent.",
  1138. "version": "18.0.2"
  1139. },
  1140. "extensions": [],
  1141. "graphics": {},
  1142. "javaScript": {},
  1143. "modifiedPreferences": {
  1144. "print.macosx.pagesetup": "QWERTY",
  1145. "print.macosx.pagesetup-2": "POIUYT"
  1146. },
  1147. "userJS": {
  1148. "exists": false
  1149. }
  1150. }'''}
  1151. def setUp(self):
  1152. super(AAQTemplateTestCase, self).setUp()
  1153. self.user = user(save=True)
  1154. self.client.login(username=self.user.username, password='testpass')
  1155. def _post_new_question(self, locale=None):
  1156. """Post a new question and return the response."""
  1157. p = product(title='Firefox', slug='firefox', save=True)
  1158. topic(slug='fix-problems', product=p, save=True)
  1159. extra = {}
  1160. if locale is not None:
  1161. extra['locale'] = locale
  1162. url = urlparams(
  1163. reverse('questions.aaq_step5', args=['desktop', 'fix-problems'],
  1164. **extra),
  1165. search='A test question')
  1166. # Set 'in-aaq' for the session. It isn't already set because this
  1167. # test doesn't do a GET of the form first.
  1168. s = self.client.session
  1169. s['in-aaq'] = True
  1170. s.save()
  1171. return self.client.post(url, self.data, follow=True)
  1172. def test_full_workflow(self):
  1173. response = self._post_new_question()
  1174. eq_(200, response.status_code)
  1175. assert 'Done!' in pq(response.content)('ul.user-messages li').text()
  1176. # Verify question is in db now
  1177. question = Question.objects.filter(title='A test question')[0]
  1178. # Make sure question is in questions list
  1179. response = self.client.get(reverse('questions.list', args=['all']))
  1180. doc = pq(response.content)
  1181. eq_(1, len(doc('#question-%s' % question.id)))
  1182. # And no email was sent
  1183. eq_(0, len(mail.outbox))
  1184. # Verify product and topic assigned to question.
  1185. topics = question.topics.all()
  1186. eq_(1, len(topics))
  1187. eq_('fix-problems', topics[0].slug)
  1188. products = question.products.all()
  1189. eq_(1, len(products))
  1190. eq_('firefox', products[0].slug)
  1191. # Verify troubleshooting information
  1192. troubleshooting = question.metadata['troubleshooting']
  1193. assert 'modifiedPreferences' in troubleshooting
  1194. assert 'print.macosx' not in troubleshooting
  1195. # Verify firefox version
  1196. version = question.metadata['ff_version']
  1197. eq_('18.0.2', version)
  1198. def test_localized_creation(self):
  1199. response = self._post_new_question(locale='pt-BR')
  1200. eq_(200, response.status_code)
  1201. assert 'Done!' in pq(response.content)('ul.user-messages li').text()
  1202. # Verify question is in db now
  1203. question = Question.objects.filter(title='A test question')[0]
  1204. eq_(question.locale, 'pt-BR')
  1205. def test_full_workflow_inactive(self):
  1206. u = self.user
  1207. u.is_active = False
  1208. u.save()
  1209. RegistrationProfile.objects.create_profile(u)
  1210. response = self._post_new_question()
  1211. eq_(200, response.status_code)
  1212. # Verify question is in db now
  1213. question = Question.objects.filter(title='A test question')[0]
  1214. # Make sure question is not in questions list
  1215. response = self.client.get(reverse('questions.list', args=['all']))
  1216. doc = pq(response.content)
  1217. eq_(0, len(doc('li#question-%s' % question.id)))
  1218. # And no confirmation email was sent (already sent on registration)
  1219. eq_(0, len(mail.outbox))
  1220. def test_invalid_type(self):
  1221. """Providing an invalid type returns 400."""
  1222. p = product(slug='firefox', save=True)
  1223. topic(slug='fix-problems', product=p, save=True)
  1224. self.client.logout()
  1225. url = urlparams(
  1226. reverse('questions.aaq_step5', args=['desktop', 'fix-problems']),
  1227. search='A test question')
  1228. # Register before asking question
  1229. data = {'username': 'testaaq',
  1230. 'password': 'testpass', 'password2': 'testpass',
  1231. 'email': 'testaaq@example.com'}
  1232. data.update(**self.data)
  1233. response = self.client.post(url, data, follow=True)
  1234. eq_(400, response.status_code)
  1235. assert 'Request type not recognized' in response.content
  1236. @mock.patch.object(Site.objects, 'get_current')
  1237. def test_register_through_aaq(self, get_current):
  1238. """Registering through AAQ form sends confirmation email."""
  1239. get_current.return_value.domain = 'testserver'
  1240. p = product(slug='firefox', save=True)
  1241. topic(slug='fix-problems', product=p, save=True)
  1242. self.client.logout()
  1243. title = 'A test question'
  1244. url = urlparams(
  1245. reverse('questions.aaq_step5', args=['desktop', 'fix-problems']),
  1246. search=title)
  1247. # Register before asking question
  1248. data = {'register': 'Register', 'username': 'testaaq',
  1249. 'password': 'testpass1', 'password2': 'testpass1',
  1250. 'email': 'testaaq@example.com'}
  1251. data.update(**self.data)
  1252. self.client.post(url, data, follow=True)
  1253. # Confirmation email is sent
  1254. eq_(1, len(mail.outbox))
  1255. eq_(mail.outbox[0].subject,
  1256. 'Please confirm your Firefox Help question')
  1257. assert mail.outbox[0].body.find('?reg=aaq') > 0
  1258. # Finally post question
  1259. self.client.post(url, self.data, follow=True)
  1260. # Verify question is in db now
  1261. question = Question.objects.filter(title=title)
  1262. eq_(1, question.count())
  1263. eq_('testaaq', question[0].creator.username)
  1264. # And no confirmation email was sent (already sent on registration)
  1265. # Note: there was already an email sent above
  1266. eq_(1, len(mail.outbox))
  1267. def test_invalid_product_404(self):
  1268. url = reverse('questions.aaq_step2', args=['lipsum'])
  1269. response = self.client.get(url)
  1270. eq_(404, response.status_code)
  1271. def test_invalid_category_302(self):
  1272. url = reverse('questions.aaq_step3', args=['desktop', 'lipsum'])
  1273. response = self.client.get(url)
  1274. eq_(302, response.status_code)
  1275. def test_no_aaq_link_in_header(self):
  1276. """Verify the ASK A QUESTION link isn't present in header."""
  1277. url = reverse('questions.aaq_step2', args=['desktop'])
  1278. response = self.client.get(url)
  1279. eq_(200, response.status_code)
  1280. assert '/questions/new' not in pq(response.content)('#aux-nav').html()
  1281. class ProductForumTemplateTestCase(TestCaseBase):
  1282. def test_product_forum_listing(self):
  1283. firefox = product(
  1284. title='Firefox', slug='firefox', questions_enabled=True, save=True)
  1285. android = product(title='Firefox for Android', slug='mobile',
  1286. questions_enabled=True, save=True)
  1287. fxos = product(title='Firefox OS', slug='firefox-os',
  1288. questions_enabled=True, save=True)
  1289. openbadges = product(title='Open Badges', slug='open-badges',
  1290. questions_enabled=False, save=True)
  1291. response = self.client.get(reverse('questions.home'))
  1292. eq_(200, response.status_code)
  1293. doc = pq(response.content)
  1294. eq_(4, len(doc('.product-list .product')))
  1295. product_list_html = doc('.product-list').html()
  1296. assert firefox.title in product_list_html
  1297. assert android.title in product_list_html
  1298. assert fxos.title in product_list_html
  1299. assert openbadges.title not in product_list_html