/soclone/views.py

http://soclone.googlecode.com/ · Python · 884 lines · 751 code · 50 blank · 83 comment · 67 complexity · 5c7ddb94409cfb68aad45412a5122fc3 MD5 · raw file

  1. """SOClone views."""
  2. import datetime
  3. import itertools
  4. from django.contrib.auth.models import User
  5. from django.contrib.auth import views as auth_views
  6. from django.contrib.contenttypes.models import ContentType
  7. from django.core.paginator import Paginator, InvalidPage
  8. from django.http import Http404, HttpResponseRedirect
  9. from django.shortcuts import get_object_or_404, render_to_response
  10. from django.template import RequestContext
  11. from django.utils.html import strip_tags
  12. from django.utils.safestring import mark_safe
  13. from lxml.html.diff import htmldiff
  14. from markdown2 import Markdown
  15. from soclone import auth
  16. from soclone import diff
  17. from soclone.forms import (AddAnswerForm, AskQuestionForm, CloseQuestionForm,
  18. CommentForm, EditAnswerForm, EditQuestionForm, RetagQuestionForm,
  19. RevisionForm)
  20. from soclone.http import JsonResponse
  21. from soclone.models import (Answer, AnswerRevision, Badge, Comment,
  22. FavouriteQuestion, Question, QuestionRevision, Tag, Vote)
  23. from soclone.questions import (all_question_views, index_question_views,
  24. unanswered_question_views)
  25. from soclone.shortcuts import get_page
  26. from soclone.utils.html import sanitize_html
  27. from soclone.utils.models import populate_foreign_key_caches
  28. markdowner = Markdown(html4tags=True)
  29. AUTO_WIKI_ANSWER_COUNT = 30
  30. def get_questions_per_page(user):
  31. if user.is_authenticated():
  32. return user.questions_per_page
  33. return 10
  34. def question_list(request, question_views, template, questions_per_page=None,
  35. page_number=None, extra_context=None):
  36. """
  37. Question list generic view.
  38. Allows the user to select from a number of ways of viewing questions,
  39. rendered with the given template.
  40. """
  41. view_id = request.GET.get('sort', None)
  42. view = dict([(q.id, q) for q in question_views]).get(view_id,
  43. question_views[0])
  44. if questions_per_page is None:
  45. questions_per_page = get_questions_per_page(request.user)
  46. paginator = Paginator(view.get_queryset(), questions_per_page)
  47. if page_number is None:
  48. page = get_page(request, paginator)
  49. else:
  50. page = paginator.page(page_number)
  51. populate_foreign_key_caches(User, ((page.object_list, (view.user,)),),
  52. fields=view.user_fields)
  53. context = {
  54. 'title': view.page_title,
  55. 'page': page,
  56. 'questions': page.object_list,
  57. 'current_view': view,
  58. 'question_views': question_views,
  59. }
  60. if extra_context is not None:
  61. context.update(extra_context)
  62. return render_to_response(template, context,
  63. context_instance=RequestContext(request))
  64. def index(request):
  65. """A condensed version of the main Question list."""
  66. extra_context = {
  67. # TODO Retrieve extra context required for index page
  68. }
  69. return question_list(request, index_question_views, 'index.html',
  70. questions_per_page=50, page_number=1,
  71. extra_context=extra_context)
  72. def about(request):
  73. """About SOClone."""
  74. raise NotImplementedError
  75. def faq(request):
  76. """Frequently Asked Questions."""
  77. raise NotImplementedError
  78. def search(request):
  79. """Search Questions and Answers."""
  80. raise NotImplementedError
  81. def login(request):
  82. """Logs in."""
  83. return auth_views.login(request, template_name='login.html')
  84. def logout(request):
  85. """Logs out."""
  86. return auth_views.logout(request, template_name='logged_out.html')
  87. def questions(request):
  88. """All Questions list."""
  89. return question_list(request, all_question_views, 'questions.html')
  90. def unanswered(request):
  91. """Unanswered Questions list."""
  92. return question_list(request, unanswered_question_views, 'unanswered.html')
  93. ANSWER_SORT = {
  94. 'votes': ('-score', '-added_at'),
  95. 'newest': ('-added_at',),
  96. 'oldest': ('added_at',),
  97. }
  98. DEFAULT_ANSWER_SORT = 'votes'
  99. def question(request, question_id):
  100. """Displays a Question."""
  101. if not request.user.is_authenticated():
  102. question = get_object_or_404(Question, id=question_id)
  103. favourite = False
  104. else:
  105. question = get_object_or_404(Question.objects.extra(
  106. select={
  107. 'user_favourite_id': (
  108. 'SELECT id FROM soclone_favouritequestion '
  109. 'WHERE question_id = soclone_question.id '
  110. 'AND user_id = %s'),
  111. },
  112. select_params=[request.user.id]
  113. ), id=question_id)
  114. favourite = (question.user_favourite_id is not None)
  115. if 'showcomments' in request.GET:
  116. return question_comments(request, question)
  117. answer_sort_type = request.GET.get('sort', DEFAULT_ANSWER_SORT)
  118. if answer_sort_type not in ANSWER_SORT:
  119. answer_sort_type = DEFAULT_ANSWER_SORT
  120. order_by = ANSWER_SORT[answer_sort_type]
  121. paginator = Paginator(Answer.objects.for_question(
  122. question, request.user).order_by(*order_by),
  123. AUTO_WIKI_ANSWER_COUNT)
  124. # Save ourselves a COUNT() query by using the denormalised count
  125. paginator._count = question.answer_count
  126. page = get_page(request, paginator)
  127. answers = page.object_list
  128. populate_foreign_key_caches(User, (
  129. ((question,), ('author', 'last_edited_by', 'closed_by')),
  130. (answers, ('author', 'last_edited_by'))
  131. ),
  132. fields=('username', 'gravatar', 'reputation', 'gold', 'silver',
  133. 'bronze'))
  134. # Look up vote status for the current user
  135. question_vote, answer_votes = Vote.objects.get_for_question_and_answers(
  136. request.user, question, page.object_list)
  137. title = question.title
  138. if question.closed:
  139. title = '%s [closed]' % title
  140. return render_to_response('question.html', {
  141. 'title': title,
  142. 'question': question,
  143. 'question_vote': question_vote,
  144. 'favourite': favourite,
  145. 'answers': page.object_list,
  146. 'answer_votes': answer_votes,
  147. 'page': page,
  148. 'answer_sort': answer_sort_type,
  149. 'answer_form': AddAnswerForm(),
  150. 'tags': question.tags.all(),
  151. }, context_instance=RequestContext(request))
  152. def question_comments(request, question, form=None):
  153. """
  154. Displays a Question and any Comments on it.
  155. This is primarily intended as a fallback for users who can't
  156. dynamically load Comments.
  157. """
  158. populate_foreign_key_caches(User, (
  159. ((question,), ('author', 'last_edited_by', 'closed_by')),
  160. ),
  161. fields=('username', 'gravatar', 'reputation', 'gold', 'silver',
  162. 'bronze'))
  163. content_type = ContentType.objects.get_for_model(Question)
  164. comments = Comment.objects.filter(content_type=content_type,
  165. object_id=question.id)
  166. if form is None:
  167. form = CommentForm()
  168. return render_to_response('question.html', {
  169. 'title': u'Comments on %s' % question.title,
  170. 'question': question,
  171. 'tags': question.tags.all(),
  172. 'comments': comments,
  173. 'comment_form': form,
  174. }, context_instance=RequestContext(request))
  175. def ask_question(request):
  176. """Adds a Question."""
  177. preview = None
  178. if request.method == 'POST':
  179. form = AskQuestionForm(request.POST)
  180. if form.is_valid():
  181. html = sanitize_html(markdowner.convert(form.cleaned_data['text']))
  182. if 'preview' in request.POST:
  183. # The user submitted the form to preview the formatted question
  184. preview = mark_safe(html)
  185. elif 'submit' in request.POST:
  186. added_at = datetime.datetime.now()
  187. # Create the Question
  188. question = Question(
  189. title = form.cleaned_data['title'],
  190. author = request.user,
  191. added_at = added_at,
  192. wiki = form.cleaned_data['wiki'],
  193. last_activity_at = added_at,
  194. last_activity_by = request.user,
  195. tagnames = form.cleaned_data['tags'],
  196. html = html,
  197. summary = strip_tags(html)[:180]
  198. )
  199. if question.wiki:
  200. question.wikified_at = added_at
  201. # When in wiki mode, we always display the last edit
  202. question.last_edited_at = added_at
  203. question.last_edited_by = request.user
  204. question.save()
  205. # Create the initial revision
  206. QuestionRevision.objects.create(
  207. question = question,
  208. revision = 1,
  209. title = question.title,
  210. author = request.user,
  211. revised_at = added_at,
  212. tagnames = question.tagnames,
  213. summary = u'asked question',
  214. text = form.cleaned_data['text']
  215. )
  216. # TODO Badges related to Tag usage
  217. # TODO Badges related to asking Questions
  218. return HttpResponseRedirect(question.get_absolute_url())
  219. else:
  220. form = AskQuestionForm()
  221. return render_to_response('ask_question.html', {
  222. 'title': u'Ask a Question',
  223. 'form': form,
  224. 'preview': preview,
  225. }, context_instance=RequestContext(request))
  226. def edit_question(request, question_id):
  227. """
  228. Entry point for editing a question.
  229. Fields which can be edited depend on the logged-in user's roles or
  230. reputation, so this view delegates to the apporopriate view based on
  231. those criteria.
  232. """
  233. question = get_object_or_404(Question, id=question_id)
  234. if auth.can_edit_post(request.user, question):
  235. return _edit_question(request, question)
  236. elif auth.can_retag_questions(request.user):
  237. return _retag_question(request, question)
  238. else:
  239. raise Http404
  240. def _edit_question(request, question):
  241. """
  242. Allows the user to edit a Question's title, text and tags.
  243. If the Question is not already in wiki mode, the user can put it in
  244. wiki mode, or it will automatically be put in wiki mode if the
  245. question has been edited five times by the person who asked it, or
  246. has been edited by four different users.
  247. """
  248. latest_revision = question.get_latest_revision()
  249. preview = None
  250. revision_form = None
  251. if request.method == 'POST':
  252. if 'select_revision' in request.POST:
  253. # The user submitted to change the revision to start editing from
  254. revision_form = RevisionForm(question, latest_revision, request.POST)
  255. if revision_form.is_valid():
  256. # Replace Question details with those from the selected revision
  257. form = EditQuestionForm(question,
  258. QuestionRevision.objects.get(question=question,
  259. revision=revision_form.cleaned_data['revision']))
  260. else:
  261. # Make sure we keep a hold of the user's other input, even
  262. # though they appear to be messing about.
  263. form = EditQuestionForm(question, latest_revision, request.POST)
  264. else:
  265. # Always check modifications against the latest revision
  266. form = EditQuestionForm(question, latest_revision, request.POST)
  267. if form.is_valid():
  268. html = sanitize_html(
  269. markdowner.convert(form.cleaned_data['text']))
  270. if 'preview' in request.POST:
  271. # The user submitted to preview the formatted question
  272. preview = mark_safe(html)
  273. elif 'submit' in request.POST:
  274. if form.has_changed():
  275. edited_at = datetime.datetime.now()
  276. tags_changed = (latest_revision.tagnames !=
  277. form.cleaned_data['tags'])
  278. tags_updated = False
  279. # Update the Question itself
  280. updated_fields = {
  281. 'title': form.cleaned_data['title'],
  282. 'last_edited_at': edited_at,
  283. 'last_edited_by': request.user,
  284. 'last_activity_at': edited_at,
  285. 'last_activity_by': request.user,
  286. 'tagnames': form.cleaned_data['tags'],
  287. 'summary': strip_tags(html)[:180],
  288. 'html': html,
  289. }
  290. if ('wiki' in form.cleaned_data and
  291. form.cleaned_data['wiki']):
  292. updated_fields['wiki'] = True
  293. updated_fields['wikified_at'] = edited_at
  294. Question.objects.filter(
  295. id=question.id).update(**updated_fields)
  296. # Update the Question's tag associations
  297. if tags_changed:
  298. tags_updated = Question.objects.update_tags(
  299. question, question.tagnames, request.user)
  300. # Create a new revision
  301. revision = QuestionRevision(
  302. question = question,
  303. title = form.cleaned_data['title'],
  304. author = request.user,
  305. revised_at = edited_at,
  306. tagnames = form.cleaned_data['tags'],
  307. text = form.cleaned_data['text']
  308. )
  309. if form.cleaned_data['summary']:
  310. revision.summary = form.cleaned_data['summary']
  311. else:
  312. revision.summary = \
  313. diff.generate_question_revision_summary(
  314. latest_revision, revision,
  315. ('wiki' in updated_fields))
  316. revision.save()
  317. # TODO 5 body edits by the author = automatic wiki mode
  318. # TODO 4 individual editors = automatic wiki mode
  319. # TODO Badges related to Tag usage
  320. # TODO Badges related to editing Questions
  321. return HttpResponseRedirect(question.get_absolute_url())
  322. else:
  323. if 'revision' in request.GET:
  324. revision_form = RevisionForm(question, latest_revision, request.GET)
  325. if revision_form.is_valid():
  326. # Replace Question details with those from the selected revision
  327. form = EditQuestionForm(question,
  328. QuestionRevision.objects.get(question=question,
  329. revision=revision_form.cleaned_data['revision']))
  330. else:
  331. revision_form = RevisionForm(question, latest_revision)
  332. form = EditQuestionForm(question, latest_revision)
  333. if revision_form is None:
  334. # We're about to redisplay after a POST where we didn't care which
  335. # revision was selected - make sure the revision the user started from
  336. # is still selected on redisplay.
  337. revision_form = RevisionForm(question, latest_revision, request.POST)
  338. return render_to_response('edit_question.html', {
  339. 'title': u'Edit Question',
  340. 'question': question,
  341. 'revision_form': revision_form,
  342. 'form': form,
  343. 'preview': preview,
  344. }, context_instance=RequestContext(request))
  345. def _retag_question(request, question):
  346. """Allows the user to edit a Question's tags."""
  347. if request.method == 'POST':
  348. form = RetagQuestionForm(question, request.POST)
  349. if form.is_valid():
  350. if form.has_changed():
  351. latest_revision = question.get_latest_revision()
  352. retagged_at = datetime.datetime.now()
  353. # Update the Question itself
  354. Question.objects.filter(id=question.id).update(
  355. tagnames = form.cleaned_data['tags'],
  356. last_edited_at = retagged_at,
  357. last_edited_by = request.user,
  358. last_activity_at = retagged_at,
  359. last_activity_by = request.user
  360. )
  361. # Update the Question's tag associations
  362. tags_updated = Question.objects.update_tags(question,
  363. form.cleaned_data['tags'], request.user)
  364. # Create a new revision
  365. QuestionRevision.objects.create(
  366. question = question,
  367. title = latest_revision.title,
  368. author = request.user,
  369. revised_at = retagged_at,
  370. tagnames = form.cleaned_data['tags'],
  371. summary = u'modified tags',
  372. text = latest_revision.text
  373. )
  374. # TODO Badges related to retagging / Tag usage
  375. # TODO Badges related to editing Questions
  376. return HttpResponseRedirect(question.get_absolute_url())
  377. else:
  378. form = RetagQuestionForm(question)
  379. return render_to_response('retag_question.html', {
  380. 'title': u'Edit Tags',
  381. 'question': question,
  382. 'form': form,
  383. }, context_instance=RequestContext(request))
  384. QUESTION_REVISION_TEMPLATE = ('<h1>%(title)s</h1>\n'
  385. '<div class="text">%(html)s</div>\n'
  386. '<div class="tags">%(tags)s</div>')
  387. def question_revisions(request, question_id):
  388. """Revision history for a Question."""
  389. question = get_object_or_404(Question, id=question_id)
  390. revisions = list(question.revisions.all())
  391. populate_foreign_key_caches(User, ((revisions, ('author',)),),
  392. fields=('username', 'gravatar', 'reputation', 'gold', 'silver',
  393. 'bronze'))
  394. for i, revision in enumerate(revisions):
  395. revision.html = QUESTION_REVISION_TEMPLATE % {
  396. 'title': revision.title,
  397. 'html': sanitize_html(markdowner.convert(revision.text)),
  398. 'tags': ' '.join(['<a class="tag">%s</a>' % tag
  399. for tag in revision.tagnames.split(' ')]),
  400. }
  401. if i > 0:
  402. revisions[i - 1].diff = htmldiff(revision.html,
  403. revisions[i - 1].html)
  404. return render_to_response('question_revisions.html', {
  405. 'title': u'Question Revisions',
  406. 'question': question,
  407. 'revisions': revisions,
  408. }, context_instance=RequestContext(request))
  409. def close_question(request, question_id):
  410. """Closes or reopens a Question based on its current closed status."""
  411. question = get_object_or_404(Question, id=question_id)
  412. if not auth.can_close_question(request.user, question):
  413. raise Http404
  414. if not question.closed:
  415. return _close_question(request, question)
  416. else:
  417. return _reopen_question(request, question)
  418. def _close_question(request, question):
  419. """Closes a Question."""
  420. if request.method == 'POST' and 'close' in request.POST:
  421. form = CloseQuestionForm(request.POST)
  422. if form.is_valid():
  423. Question.objects.filter(id=question.id).update(closed=True,
  424. closed_by=request.user, closed_at=datetime.datetime.now(),
  425. close_reason=form.cleaned_data['reason'])
  426. if request.is_ajax():
  427. return JsonResponse({'success': True})
  428. else:
  429. return HttpResponseRedirect(question.get_absolute_url())
  430. elif request.is_ajax():
  431. return JsonResponse({'success': False, 'errors': form.errors})
  432. else:
  433. if request.is_ajax():
  434. raise Http404
  435. form = CloseQuestionForm()
  436. return render_to_response('close_question.html', {
  437. 'title': u'Close Question',
  438. 'question': question,
  439. 'form': form,
  440. }, context_instance=RequestContext(request))
  441. def _reopen_question(request, question):
  442. """Reopens a Question."""
  443. if request.method == 'POST' and 'reopen' in request.POST:
  444. Question.objects.filter(id=question.id).update(closed=False,
  445. closed_by=None, closed_at=None, close_reason=None)
  446. if request.is_ajax():
  447. return JsonResponse({'success': True})
  448. else:
  449. return HttpResponseRedirect(question.get_absolute_url())
  450. if request.is_ajax():
  451. raise Http404
  452. return render_to_response('reopen_question.html', {
  453. 'title': u'Reopen Question',
  454. 'question': question,
  455. }, context_instance=RequestContext(request))
  456. def delete_question(request, question_id):
  457. """Deletes or undeletes a Question."""
  458. raise NotImplementedError
  459. def favourite_question(request, question_id):
  460. """
  461. Adds or removes a FavouriteQuestion.
  462. Favouriting will not use a confirmation page, as it's an action which
  463. is non-destructive and easily reversible.
  464. """
  465. if request.method != 'POST':
  466. raise Http404
  467. question = get_object_or_404(Question, id=question_id, deleted=False)
  468. favourite, created = FavouriteQuestion.objects.get_or_create(
  469. user=request.user, question=question)
  470. if not created:
  471. favourite.delete()
  472. if request.is_ajax():
  473. return JsonResponse({'success': True, 'favourited': created})
  474. else:
  475. return HttpResponseRedirect(question.get_absolute_url())
  476. def add_answer(request, question_id):
  477. """
  478. Adds an Answer to a Question.
  479. Once a certain number of Answers have been added, a Question and all
  480. its Answers will enter wiki mode and all subsequent Answers will be in
  481. wiki mode.
  482. """
  483. question = get_object_or_404(Question, id=question_id)
  484. preview = None
  485. if request.method == 'POST':
  486. form = AddAnswerForm(request.POST)
  487. if form.is_valid():
  488. html = sanitize_html(markdowner.convert(form.cleaned_data['text']))
  489. if 'preview' in request.POST:
  490. # The user submitted the form to preview the formatted answer
  491. preview = mark_safe(html)
  492. elif 'submit' in request.POST:
  493. added_at = datetime.datetime.now()
  494. # Create the Answer
  495. answer = Answer(
  496. question = question,
  497. author = request.user,
  498. added_at = added_at,
  499. wiki = (form.cleaned_data['wiki'] or
  500. question.answer_count >= AUTO_WIKI_ANSWER_COUNT),
  501. html = html
  502. )
  503. if answer.wiki:
  504. answer.wikified_at = added_at
  505. # When in wiki mode, we always display the last edit
  506. answer.last_edited_at = added_at
  507. answer.last_edited_by = request.user
  508. answer.save()
  509. # Create the initial revision
  510. AnswerRevision.objects.create(
  511. answer = answer,
  512. revision = 1,
  513. author = request.user,
  514. revised_at = added_at,
  515. summary = u'added answer',
  516. text = form.cleaned_data['text']
  517. )
  518. Question.objects.update_answer_count(question)
  519. # TODO Badges related to answering Questions
  520. # TODO If this is answer 30, put question and all answers into
  521. # wiki mode.
  522. # TODO Redirect needs to handle paging
  523. return HttpResponseRedirect(question.get_absolute_url())
  524. else:
  525. form = AddAnswerForm()
  526. return render_to_response('add_answer.html', {
  527. 'title': u'Post an Answer',
  528. 'question': question,
  529. 'form': form,
  530. 'preview': preview,
  531. }, context_instance=RequestContext(request))
  532. def answer_comments(request, answer_id, answer=None, form=None):
  533. """
  534. Displays a single Answer and any Comments on it.
  535. This is primarily intended as a fallback for users who can't
  536. dynamically load Comments.
  537. """
  538. if answer is None:
  539. answer = get_object_or_404(Answer, id=answer_id)
  540. populate_foreign_key_caches(User, (
  541. ((answer,), ('author', 'last_edited_by')),
  542. ),
  543. fields=('username', 'gravatar', 'reputation', 'gold', 'silver',
  544. 'bronze'))
  545. content_type = ContentType.objects.get_for_model(Answer)
  546. comments = Comment.objects.filter(content_type=content_type,
  547. object_id=answer.id)
  548. if form is None:
  549. form = CommentForm()
  550. return render_to_response('answer.html', {
  551. 'title': u'Answer Comments',
  552. 'answer': answer,
  553. 'comments': comments,
  554. 'comment_form': form,
  555. }, context_instance=RequestContext(request))
  556. def edit_answer(request, answer_id):
  557. """Edits an Answer."""
  558. answer = get_object_or_404(Answer, id=answer_id)
  559. if not auth.can_edit_post(request.user, answer):
  560. raise Http404
  561. latest_revision = answer.get_latest_revision()
  562. preview = None
  563. revision_form = None
  564. if request.method == 'POST':
  565. if 'select_revision' in request.POST:
  566. # The user submitted to change the revision to start editing from
  567. revision_form = RevisionForm(answer, latest_revision, request.POST)
  568. if revision_form.is_valid():
  569. # Replace Question details with those from the selected revision
  570. form = EditAnswerForm(answer,
  571. AnswerRevision.objects.get(answer=answer,
  572. revision=revision_form.cleaned_data['revision']))
  573. else:
  574. # Make sure we keep a hold of the user's other input, even
  575. # though they appear to be messing about.
  576. form = EditAnswerForm(answer, latest_revision, request.POST)
  577. else:
  578. # Always check modifications against the latest revision
  579. form = EditAnswerForm(answer, latest_revision, request.POST)
  580. if form.is_valid():
  581. html = sanitize_html(
  582. markdowner.convert(form.cleaned_data['text']))
  583. if 'preview' in request.POST:
  584. # The user submitted to preview the formatted question
  585. preview = mark_safe(html)
  586. elif 'submit' in request.POST:
  587. if form.has_changed():
  588. edited_at = datetime.datetime.now()
  589. # Update the Answer itself
  590. updated_fields = {
  591. 'last_edited_at': edited_at,
  592. 'last_edited_by': request.user,
  593. 'html': html,
  594. }
  595. if ('wiki' in form.cleaned_data and
  596. form.cleaned_data['wiki']):
  597. updated_fields['wiki'] = True
  598. updated_fields['wikified_at'] = edited_at
  599. Answer.objects.filter(
  600. id=answer.id).update(**updated_fields)
  601. # Create a new revision
  602. revision = AnswerRevision(
  603. answer = answer,
  604. author = request.user,
  605. revised_at = edited_at,
  606. text = form.cleaned_data['text']
  607. )
  608. if form.cleaned_data['summary']:
  609. revision.summary = form.cleaned_data['summary']
  610. else:
  611. revision.summary = \
  612. diff.generate_answer_revision_summary(
  613. latest_revision, revision,
  614. ('wiki' in updated_fields))
  615. revision.save()
  616. # TODO 5 body edits by the asker = automatic wiki mode
  617. # TODO 4 individual editors = automatic wiki mode
  618. # TODO Badges related to editing Answers
  619. return HttpResponseRedirect(answer.get_absolute_url())
  620. else:
  621. revision_form = RevisionForm(answer, latest_revision)
  622. form = EditAnswerForm(answer, latest_revision)
  623. if revision_form is None:
  624. # We're about to redisplay after a POST where we didn't care which
  625. # revision was selected - make sure the revision the user started from
  626. # is still selected on redisplay.
  627. revision_form = RevisionForm(answer, latest_revision, request.POST)
  628. return render_to_response('edit_answer.html', {
  629. 'title': u'Edit Answer',
  630. 'question': answer.question,
  631. 'answer': answer,
  632. 'revision_form': revision_form,
  633. 'form': form,
  634. 'preview': preview,
  635. }, context_instance=RequestContext(request))
  636. ANSWER_REVISION_TEMPLATE = '<div class="text">%(html)s</div>'
  637. def answer_revisions(request, answer_id):
  638. """Revision history for an Answer."""
  639. answer = get_object_or_404(Answer, id=answer_id)
  640. revisions = list(answer.revisions.all())
  641. populate_foreign_key_caches(User, ((revisions, ('author',)),),
  642. fields=('username', 'gravatar', 'reputation', 'gold', 'silver',
  643. 'bronze'))
  644. for i, revision in enumerate(revisions):
  645. revision.html = QUESTION_REVISION_TEMPLATE % {
  646. 'html': sanitize_html(markdowner.convert(revision.text)),
  647. }
  648. if i > 0:
  649. revisions[i - 1].diff = htmldiff(revision.html,
  650. revisions[i - 1].html)
  651. return render_to_response('answer_revisions.html', {
  652. 'title': u'Answer Revisions',
  653. 'answer': answer,
  654. 'revisions': revisions,
  655. }, context_instance=RequestContext(request))
  656. def accept_answer(request, answer_id):
  657. """Marks an Answer as accepted."""
  658. raise NotImplementedError
  659. def delete_answer(request, answer_id):
  660. """Deletes or undeletes an Answer."""
  661. raise NotImplementedError
  662. def vote(request, model, object_id):
  663. """
  664. Vote on a Question or Answer.
  665. """
  666. if request.method != 'POST':
  667. raise Http404
  668. vote_type = request.POST.get('type', None)
  669. if vote_type == 'up' and auth.can_vote_up(request.user):
  670. vote_type = Vote.VOTE_UP
  671. elif vote_type == 'down' and auth.can_vote_down(request.user):
  672. vote_type = Vote.VOTE_DOWN
  673. else:
  674. raise Http404
  675. # TODO Ensure users can't vote on their own posts
  676. obj = get_object_or_404(model, id=object_id, deleted=False, locked=False)
  677. content_type = ContentType.objects.get_for_model(model)
  678. try:
  679. existing_vote = Vote.objects.get(content_type=content_type,
  680. object_id=object_id,
  681. user=request.user)
  682. except Vote.DoesNotExist:
  683. existing_vote = None
  684. if existing_vote is None:
  685. Vote.objects.create(content_type=content_type,
  686. object_id=object_id,
  687. user=request.user,
  688. vote=vote_type)
  689. else:
  690. if vote_type == existing_vote.vote:
  691. existing_vote.delete()
  692. else:
  693. existing_vote.vote = vote_type
  694. existing_vote.save()
  695. # TODO Reputation management
  696. if request.is_ajax():
  697. return JsonResponse({
  698. 'success': True,
  699. 'score': model._default_manager.filter(
  700. id=object_id).values_list('score', flat=True)[0],
  701. })
  702. else:
  703. return HttpResponseRedirect(obj.get_absolute_url())
  704. def flag_item(request, model, object_id):
  705. """Flag a Question or Answer as containing offensive content."""
  706. raise NotImplementedError
  707. def add_comment(request, model, object_id):
  708. """Adds a comment to a Question or Answer."""
  709. obj = get_object_or_404(model, id=object_id)
  710. if request.method == "POST":
  711. form = CommentForm(request.POST)
  712. if form.is_valid():
  713. Comment.objects.create(
  714. content_type = ContentType.objects.get_for_model(model),
  715. object_id = object_id,
  716. author = request.user,
  717. added_at = datetime.datetime.now(),
  718. comment = form.cleaned_data['comment']
  719. )
  720. if request.is_ajax():
  721. return JsonResponse({'success': True})
  722. else:
  723. return HttpResponseRedirect(obj.get_absolute_url())
  724. elif request.is_ajax():
  725. return JsonResponse({'success': False, 'errors': form.errors})
  726. else:
  727. form = CommentForm()
  728. # Let the appropriate fallback view take care of display/redisplay
  729. if model is Question:
  730. return question_comments(request, obj, form=form)
  731. elif model is Answer:
  732. return answer_comments(request, object_id, answer=obj, form=form)
  733. def delete_comment(request, comment_id):
  734. """Deletes a Comment permenantly."""
  735. raise NotImplementedError
  736. TAG_SORT = {
  737. 'popular': ('-use_count', 'name'),
  738. 'name': ('name',),
  739. }
  740. DEFAULT_TAG_SORT = 'popular'
  741. def tags(request):
  742. """Searchable Tag list."""
  743. sort_type = request.GET.get('sort', DEFAULT_TAG_SORT)
  744. if sort_type not in TAG_SORT:
  745. sort_type = DEFAULT_TAG_SORT
  746. tags = Tag.objects.all().order_by(*TAG_SORT[sort_type])
  747. name_filter = request.GET.get('filter', '')
  748. if name_filter:
  749. tags = tags.filter(name__icontains=name_filter)
  750. paginator = Paginator(tags, 50)
  751. page = get_page(request, paginator)
  752. return render_to_response('tags.html', {
  753. 'title': u'Tags',
  754. 'tags': page.object_list,
  755. 'page': page,
  756. 'sort': sort_type,
  757. 'filter': name_filter,
  758. }, context_instance=RequestContext(request))
  759. def tag(request, tag_name):
  760. """Displayed Questions for a Tag."""
  761. raise NotImplementedError
  762. USER_SORT = {
  763. 'reputation': ('-reputation', '-date_joined'),
  764. 'newest': ('-date_joined',),
  765. 'oldest': ('date_joined',),
  766. 'name': ('username',),
  767. }
  768. DEFAULT_USER_SORT = 'reputation'
  769. def users(request):
  770. """Searchable User list."""
  771. sort_type = request.GET.get('sort', DEFAULT_USER_SORT)
  772. if sort_type not in USER_SORT:
  773. sort_type = DEFAULT_USER_SORT
  774. users = User.objects.all().order_by(*USER_SORT[sort_type])
  775. name_filter = request.GET.get('filter', '')
  776. if name_filter:
  777. users = users.filter(username__icontains=name_filter)
  778. users = users.values('id', 'username', 'gravatar', 'reputation', 'gold',
  779. 'silver', 'bronze')
  780. paginator = Paginator(users, 28)
  781. page = get_page(request, paginator)
  782. return render_to_response('users.html', {
  783. 'title': u'Users',
  784. 'users': page.object_list,
  785. 'page': page,
  786. 'sort': sort_type,
  787. 'filter': name_filter,
  788. }, context_instance=RequestContext(request))
  789. def user(request, user_id):
  790. """Displays a User and various information about them."""
  791. raise NotImplementedError
  792. def badges(request):
  793. """Badge list."""
  794. return render_to_response('badges.html', {
  795. 'title': u'Badges',
  796. 'badges': Badge.objects.all(),
  797. }, context_instance=RequestContext(request))
  798. def badge(request, badge_id):
  799. """Displays a Badge and any Users who have recently been awarded it."""
  800. badge = get_object_or_404(Badge, id=badge_id)
  801. awarded_to = badge.awarded_to.all().order_by('-award__awarded_at').values(
  802. 'id', 'username', 'reputation', 'gold', 'silver', 'bronze')[:500]
  803. return render_to_response('badge.html', {
  804. 'title': '%s Badge' % badge.name,
  805. 'badge': badge,
  806. 'awarded_to': awarded_to,
  807. }, context_instance=RequestContext(request))