4 from urllib import unquote
5 from django.conf import settings
6 from django.shortcuts import render_to_response, get_object_or_404
7 from django.http import HttpResponseRedirect, HttpResponse, HttpResponseForbidden, Http404
8 from django.core.paginator import Paginator, EmptyPage, InvalidPage
9 from django.template import RequestContext
10 from django.utils.html import *
11 from django.utils import simplejson
12 from django.db.models import Q
13 from django.utils.translation import ugettext as _
14 from django.template.defaultfilters import slugify
15 from django.core.urlresolvers import reverse
16 from django.utils.datastructures import SortedDict
18 from forum.utils.html import sanitize_html
19 from markdown2 import Markdown
20 #from lxml.html.diff import htmldiff
21 from forum.utils.diff import textDiff as htmldiff
22 from forum.forms import *
23 from forum.models import *
24 from forum.auth import *
25 from forum.const import *
26 from forum import auth
27 from forum.utils.forms import get_next_url
30 #refactor - move these numbers somewhere?
35 DEFAULT_PAGE_SIZE = 60
37 QUESTIONS_PAGE_SIZE = 30
39 ANSWERS_PAGE_SIZE = 10
41 markdowner = Markdown(html4tags=True)
43 #system to display main content
44 def _get_tags_cache_json():#service routine used by views requiring tag list in the javascript space
45 """returns list of all tags in json format
46 no caching yet, actually
48 tags = Tag.objects.filter(deleted=False).all()
51 dic = {'n': tag.name, 'c': tag.used_count}
53 tags = simplejson.dumps(tags_list)
56 def _get_and_remember_questions_sort_method(request, view_dic, default):#service routine used by q listing views and question view
57 """manages persistence of post sort order
58 it is assumed that when user wants newest question -
59 then he/she wants newest answers as well, etc.
60 how far should this assumption actually go - may be a good question
62 if default not in view_dic:
63 raise Exception('default value must be in view_dic')
65 q_sort_method = request.REQUEST.get('sort', None)
66 if q_sort_method == None:
67 q_sort_method = request.session.get('questions_sort_method', default)
69 if q_sort_method not in view_dic:
70 q_sort_method = default
71 request.session['questions_sort_method'] = q_sort_method
72 return q_sort_method, view_dic[q_sort_method]
74 #refactor? - we have these
75 #views that generate a listing of questions in one way or another:
76 #index, unanswered, questions, search, tag
77 #should we dry them up?
78 #related topics - information drill-down, search refinement
80 def index(request):#generates front page - shows listing of questions sorted in various ways
81 """index view mapped to the root url of the Q&A site
84 "latest":"-last_activity_at",
85 "hottest":"-answer_count",
88 view_id, orderby = _get_and_remember_questions_sort_method(request, view_dic, 'latest')
90 pagesize = request.session.get("pagesize",QUESTIONS_PAGE_SIZE)
92 page = int(request.GET.get('page', '1'))
96 qs = Question.objects.exclude(deleted=True).order_by(orderby)
98 objects_list = Paginator(qs, pagesize)
99 questions = objects_list.page(page)
101 # RISK - inner join queries
102 #questions = questions.select_related()
103 tags = Tag.objects.get_valid_tags(INDEX_TAGS_SIZE)
105 awards = Award.objects.get_recent_awards()
107 (interesting_tag_names, ignored_tag_names) = (None, None)
108 if request.user.is_authenticated():
109 pt = MarkedTag.objects.filter(user=request.user)
110 interesting_tag_names = pt.filter(reason='good').values_list('tag__name', flat=True)
111 ignored_tag_names = pt.filter(reason='bad').values_list('tag__name', flat=True)
113 tags_autocomplete = _get_tags_cache_json()
115 return render_to_response('index.html', {
116 'interesting_tag_names': interesting_tag_names,
117 'tags_autocomplete': tags_autocomplete,
118 'ignored_tag_names': ignored_tag_names,
119 "questions" : questions,
122 "awards" : awards[:INDEX_AWARD_SIZE],
124 'is_paginated' : True,
125 'pages': objects_list.num_pages,
127 'has_previous': questions.has_previous(),
128 'has_next': questions.has_next(),
129 'previous': questions.previous_page_number(),
130 'next': questions.next_page_number(),
131 'base_url' : request.path + '?sort=%s&' % view_id,
132 'pagesize' : pagesize
133 }}, context_instance=RequestContext(request))
135 def unanswered(request):#generates listing of unanswered questions
136 return questions(request, unanswered=True)
138 def questions(request, tagname=None, unanswered=False):#a view generating listing of questions, used by 'unanswered' too
140 List of Questions, Tagged questions, and Unanswered questions.
143 # "questions.html" or maybe index.html in the future
144 template_file = "questions.html"
145 # Set flag to False by default. If it is equal to True, then need to be saved.
146 pagesize_changed = False
147 # get pagesize from session, if failed then get default value
148 pagesize = request.session.get("pagesize",QUESTIONS_PAGE_SIZE)
150 page = int(request.GET.get('page', '1'))
154 view_dic = {"latest":"-added_at", "active":"-last_activity_at", "hottest":"-answer_count", "mostvoted":"-score" }
155 view_id, orderby = _get_and_remember_questions_sort_method(request,view_dic,'latest')
157 # check if request is from tagged questions
158 qs = Question.objects.exclude(deleted=True)
160 if tagname is not None:
161 qs = qs.filter(tags__name = unquote(tagname))
164 qs = qs.exclude(answer_accepted=True)
167 #user contributed questions & answers
168 if 'user' in request.GET:
170 author_name = request.GET['user']
171 u = User.objects.get(username=author_name)
172 qs = qs.filter(Q(author=u) | Q(answers__author=u))
173 except User.DoesNotExist:
176 if request.user.is_authenticated():
177 uid_str = str(request.user.id)
179 select = SortedDict([
182 'SELECT COUNT(1) FROM forum_markedtag, question_tags '
183 + 'WHERE forum_markedtag.user_id = %s '
184 + 'AND forum_markedtag.tag_id = question_tags.tag_id '
185 + 'AND forum_markedtag.reason = \'good\' '
186 + 'AND question_tags.question_id = question.id'
189 select_params = (uid_str,),
191 if request.user.hide_ignored_questions:
192 ignored_tags = Tag.objects.filter(user_selections__reason='bad',
193 user_selections__user = request.user)
194 qs = qs.exclude(tags__in=ignored_tags)
197 select = SortedDict([
200 'SELECT COUNT(1) FROM forum_markedtag, question_tags '
201 + 'WHERE forum_markedtag.user_id = %s '
202 + 'AND forum_markedtag.tag_id = question_tags.tag_id '
203 + 'AND forum_markedtag.reason = \'bad\' '
204 + 'AND question_tags.question_id = question.id'
207 select_params = (uid_str, )
210 qs = qs.select_related(depth=1).order_by(orderby)
212 objects_list = Paginator(qs, pagesize)
213 questions = objects_list.page(page)
215 # Get related tags from this page objects
216 if questions.object_list.count() > 0:
217 related_tags = Tag.objects.get_tags_by_questions(questions.object_list)
220 tags_autocomplete = _get_tags_cache_json()
222 # get the list of interesting and ignored tags
223 (interesting_tag_names, ignored_tag_names) = (None, None)
224 if request.user.is_authenticated():
225 pt = MarkedTag.objects.filter(user=request.user)
226 interesting_tag_names = pt.filter(reason='good').values_list('tag__name', flat=True)
227 ignored_tag_names = pt.filter(reason='bad').values_list('tag__name', flat=True)
229 return render_to_response(template_file, {
230 "questions" : questions,
231 "author_name" : author_name,
233 "questions_count" : objects_list.count,
234 "tags" : related_tags,
235 "tags_autocomplete" : tags_autocomplete,
236 "searchtag" : tagname,
237 "is_unanswered" : unanswered,
238 "interesting_tag_names": interesting_tag_names,
239 'ignored_tag_names': ignored_tag_names,
241 'is_paginated' : True,
242 'pages': objects_list.num_pages,
244 'has_previous': questions.has_previous(),
245 'has_next': questions.has_next(),
246 'previous': questions.previous_page_number(),
247 'next': questions.next_page_number(),
248 'base_url' : request.path + '?sort=%s&' % view_id,
249 'pagesize' : pagesize
250 }}, context_instance=RequestContext(request))
252 def search(request): #generates listing of questions matching a search query - including tags and just words
253 """generates listing of questions matching a search query
254 supports full text search in mysql db using sphinx and internally in postgresql
255 falls back on simple partial string matching approach if
256 full text search function is not available
258 if request.method == "GET":
259 keywords = request.GET.get("q")
260 search_type = request.GET.get("t")
262 page = int(request.GET.get('page', '1'))
266 return HttpResponseRedirect(reverse(index))
267 if search_type == 'tag':
268 return HttpResponseRedirect(reverse('tags') + '?q=%s&page=%s' % (keywords.strip(), page))
269 elif search_type == "user":
270 return HttpResponseRedirect(reverse('users') + '?q=%s&page=%s' % (keywords.strip(), page))
271 elif search_type == "question":
273 template_file = "questions.html"
274 # Set flag to False by default. If it is equal to True, then need to be saved.
275 pagesize_changed = False
276 # get pagesize from session, if failed then get default value
277 user_page_size = request.session.get("pagesize", QUESTIONS_PAGE_SIZE)
278 # set pagesize equal to logon user specified value in database
279 if request.user.is_authenticated() and request.user.questions_per_page > 0:
280 user_page_size = request.user.questions_per_page
283 page = int(request.GET.get('page', '1'))
284 # get new pagesize from UI selection
285 pagesize = int(request.GET.get('pagesize', user_page_size))
286 if pagesize <> user_page_size:
287 pagesize_changed = True
291 pagesize = user_page_size
293 # save this pagesize to user database
295 request.session["pagesize"] = pagesize
296 if request.user.is_authenticated():
298 user.questions_per_page = pagesize
301 view_id = request.GET.get('sort', None)
302 view_dic = {"latest":"-added_at", "active":"-last_activity_at", "hottest":"-answer_count", "mostvoted":"-score" }
304 orderby = view_dic[view_id]
307 orderby = "-added_at"
309 def question_search(keywords, orderby):
310 objects = Question.objects.filter(deleted=False).extra(where=['title like %s'], params=['%' + keywords + '%']).order_by(orderby)
311 # RISK - inner join queries
312 return objects.select_related();
314 from forum.modules import get_handler
316 question_search = get_handler('question_search', question_search)
318 objects = question_search(keywords, orderby)
320 objects_list = Paginator(objects, pagesize)
321 questions = objects_list.page(page)
323 # Get related tags from this page objects
325 for question in questions.object_list:
326 tags = list(question.tags.all())
328 if tag not in related_tags:
329 related_tags.append(tag)
331 #if is_search is true in the context, prepend this string to soting tabs urls
332 search_uri = "?q=%s&page=%d&t=question" % ("+".join(keywords.split()), page)
334 return render_to_response(template_file, {
335 "questions" : questions,
337 "questions_count" : objects_list.count,
338 "tags" : related_tags,
340 "searchtitle" : keywords,
341 "keywords" : keywords,
342 "is_unanswered" : False,
344 "search_uri": search_uri,
346 'is_paginated' : True,
347 'pages': objects_list.num_pages,
349 'has_previous': questions.has_previous(),
350 'has_next': questions.has_next(),
351 'previous': questions.previous_page_number(),
352 'next': questions.next_page_number(),
353 'base_url' : request.path + '?t=question&q=%s&sort=%s&' % (keywords, view_id),
354 'pagesize' : pagesize
355 }}, context_instance=RequestContext(request))
360 def tag(request, tag):#stub generates listing of questions tagged with a single tag
361 return questions(request, tagname=tag)
363 def tags(request):#view showing a listing of available tags - plain list
366 sortby = request.GET.get('sort', 'used')
368 page = int(request.GET.get('page', '1'))
372 if request.method == "GET":
373 stag = request.GET.get("q", "").strip()
375 objects_list = Paginator(Tag.objects.filter(deleted=False).exclude(used_count=0).extra(where=['name like %s'], params=['%' + stag + '%']), DEFAULT_PAGE_SIZE)
378 objects_list = Paginator(Tag.objects.all().filter(deleted=False).exclude(used_count=0).order_by("name"), DEFAULT_PAGE_SIZE)
380 objects_list = Paginator(Tag.objects.all().filter(deleted=False).exclude(used_count=0).order_by("-used_count"), DEFAULT_PAGE_SIZE)
383 tags = objects_list.page(page)
384 except (EmptyPage, InvalidPage):
385 tags = objects_list.page(objects_list.num_pages)
387 return render_to_response('tags.html', {
393 'is_paginated' : is_paginated,
394 'pages': objects_list.num_pages,
396 'has_previous': tags.has_previous(),
397 'has_next': tags.has_next(),
398 'previous': tags.previous_page_number(),
399 'next': tags.next_page_number(),
400 'base_url' : reverse('tags') + '?sort=%s&' % sortby
402 }, context_instance=RequestContext(request))
404 def question(request, id):#refactor - long subroutine. display question body, answers and comments
405 """view that displays body of the question and
409 page = int(request.GET.get('page', '1'))
413 view_id = request.GET.get('sort', None)
414 view_dic = {"latest":"-added_at", "oldest":"added_at", "votes":"-score" }
416 orderby = view_dic[view_id]
418 qsm = request.session.get('questions_sort_method',None)
419 if qsm in ('mostvoted','latest'):
420 logging.debug('loaded from session ' + qsm)
421 if qsm == 'mostvoted':
426 orderby = '-added_at'
431 logging.debug('view_id=' + str(view_id))
433 question = get_object_or_404(Question, id=id)
435 pattern = r'/%s%s%d/([\w-]+)' % (settings.FORUM_SCRIPT_ALIAS,_('question/'), question.id)
436 path_re = re.compile(pattern)
437 logging.debug(pattern)
438 logging.debug(request.path)
439 m = path_re.match(request.path)
442 logging.debug('have slug %s' % slug)
443 assert(slug == slugify(question.title))
445 logging.debug('no match!')
447 return HttpResponseRedirect(question.get_absolute_url())
449 if question.deleted and not auth.can_view_deleted_post(request.user, question):
451 answer_form = AnswerForm(question,request.user)
452 answers = Answer.objects.get_answers_from_question(question, request.user)
453 answers = answers.select_related(depth=1)
455 favorited = question.has_favorite_by_user(request.user)
456 if request.user.is_authenticated():
457 question_vote = question.votes.select_related().filter(user=request.user)
459 question_vote = None #is this correct?
460 if question_vote is not None and question_vote.count() > 0:
461 question_vote = question_vote[0]
463 user_answer_votes = {}
464 for answer in answers:
465 vote = answer.get_user_vote(request.user)
466 if vote is not None and not user_answer_votes.has_key(answer.id):
470 user_answer_votes[answer.id] = vote_value
472 if answers is not None:
473 answers = answers.order_by("-accepted", orderby)
475 filtered_answers = []
476 for answer in answers:
477 if answer.deleted == True:
478 if answer.author_id == request.user.id:
479 filtered_answers.append(answer)
481 filtered_answers.append(answer)
483 objects_list = Paginator(filtered_answers, ANSWERS_PAGE_SIZE)
484 page_objects = objects_list.page(page)
486 #todo: merge view counts per user and per session
487 #1) view count per session
488 update_view_count = False
489 if 'question_view_times' not in request.session:
490 request.session['question_view_times'] = {}
492 last_seen = request.session['question_view_times'].get(question.id,None)
493 updated_when, updated_who = question.get_last_update_info()
495 if updated_who != request.user:
497 if last_seen < updated_when:
498 update_view_count = True
500 update_view_count = True
502 request.session['question_view_times'][question.id] = datetime.datetime.now()
504 if update_view_count:
505 question.view_count += 1
508 #2) question view count per user
509 if request.user.is_authenticated():
511 question_view = QuestionView.objects.get(who=request.user, question=question)
512 except QuestionView.DoesNotExist:
513 question_view = QuestionView(who=request.user, question=question)
514 question_view.when = datetime.datetime.now()
517 return render_to_response('question.html', {
518 "question" : question,
519 "question_vote" : question_vote,
520 "question_comment_count":question.comments.count(),
521 "answer" : answer_form,
522 "answers" : page_objects.object_list,
523 "user_answer_votes": user_answer_votes,
524 "tags" : question.tags.all(),
526 "favorited" : favorited,
527 "similar_questions" : Question.objects.get_similar_questions(question),
529 'is_paginated' : True,
530 'pages': objects_list.num_pages,
532 'has_previous': page_objects.has_previous(),
533 'has_next': page_objects.has_next(),
534 'previous': page_objects.previous_page_number(),
535 'next': page_objects.next_page_number(),
536 'base_url' : request.path + '?sort=%s&' % view_id,
537 'extend_url' : "#sort-top"
539 }, context_instance=RequestContext(request))
541 QUESTION_REVISION_TEMPLATE = ('<h1>%(title)s</h1>\n'
542 '<div class="text">%(html)s</div>\n'
543 '<div class="tags">%(tags)s</div>')
544 def question_revisions(request, id):
545 post = get_object_or_404(Question, id=id)
546 revisions = list(post.revisions.all())
548 for i, revision in enumerate(revisions):
549 revision.html = QUESTION_REVISION_TEMPLATE % {
550 'title': revision.title,
551 'html': sanitize_html(markdowner.convert(revision.text)),
552 'tags': ' '.join(['<a class="post-tag">%s</a>' % tag
553 for tag in revision.tagnames.split(' ')]),
556 revisions[i].diff = htmldiff(revisions[i-1].html, revision.html)
558 revisions[i].diff = QUESTION_REVISION_TEMPLATE % {
559 'title': revisions[0].title,
560 'html': sanitize_html(markdowner.convert(revisions[0].text)),
561 'tags': ' '.join(['<a class="post-tag">%s</a>' % tag
562 for tag in revisions[0].tagnames.split(' ')]),
564 revisions[i].summary = _('initial version')
565 return render_to_response('revisions_question.html', {
567 'revisions': revisions,
568 }, context_instance=RequestContext(request))
570 ANSWER_REVISION_TEMPLATE = ('<div class="text">%(html)s</div>')
571 def answer_revisions(request, id):
572 post = get_object_or_404(Answer, id=id)
573 revisions = list(post.revisions.all())
575 for i, revision in enumerate(revisions):
576 revision.html = ANSWER_REVISION_TEMPLATE % {
577 'html': sanitize_html(markdowner.convert(revision.text))
580 revisions[i].diff = htmldiff(revisions[i-1].html, revision.html)
582 revisions[i].diff = revisions[i].text
583 revisions[i].summary = _('initial version')
584 return render_to_response('revisions_answer.html', {
586 'revisions': revisions,
587 }, context_instance=RequestContext(request))