]> git.openstreetmap.org Git - osqa.git/blob - forum/views/readers.py
Initial commit
[osqa.git] / forum / views / readers.py
1 # encoding:utf-8
2 import datetime
3 import logging
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
17
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
28
29 # used in index page
30 #refactor - move these numbers somewhere?
31 INDEX_PAGE_SIZE = 30
32 INDEX_AWARD_SIZE = 15
33 INDEX_TAGS_SIZE = 25
34 # used in tags list
35 DEFAULT_PAGE_SIZE = 60
36 # used in questions
37 QUESTIONS_PAGE_SIZE = 30
38 # used in answers
39 ANSWERS_PAGE_SIZE = 10
40
41 markdowner = Markdown(html4tags=True)
42
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
47     """
48     tags = Tag.objects.filter(deleted=False).all()
49     tags_list = []
50     for tag in tags:
51         dic = {'n': tag.name, 'c': tag.used_count}
52         tags_list.append(dic)
53     tags = simplejson.dumps(tags_list)
54     return tags
55
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
61     """
62     if default not in view_dic:
63         raise Exception('default value must be in view_dic')
64
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)
68
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]
73
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
79
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
82     """
83     view_dic = {
84              "latest":"-last_activity_at",
85              "hottest":"-answer_count",
86              "mostvoted":"-score",
87              }
88     view_id, orderby = _get_and_remember_questions_sort_method(request, view_dic, 'latest')
89
90     pagesize = request.session.get("pagesize",QUESTIONS_PAGE_SIZE)
91     try:
92         page = int(request.GET.get('page', '1'))
93     except ValueError:
94         page = 1
95
96     qs = Question.objects.exclude(deleted=True).order_by(orderby)
97
98     objects_list = Paginator(qs, pagesize)
99     questions = objects_list.page(page)
100
101     # RISK - inner join queries
102     #questions = questions.select_related()
103     tags = Tag.objects.get_valid_tags(INDEX_TAGS_SIZE)
104
105     awards = Award.objects.get_recent_awards()
106
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)
112
113     tags_autocomplete = _get_tags_cache_json()
114
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,
120         "tab_id" : view_id,
121         "tags" : tags,
122         "awards" : awards[:INDEX_AWARD_SIZE],
123         "context" : {
124             'is_paginated' : True,
125             'pages': objects_list.num_pages,
126             'page': page,
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))
134
135 def unanswered(request):#generates listing of unanswered questions
136     return questions(request, unanswered=True)
137
138 def questions(request, tagname=None, unanswered=False):#a view generating listing of questions, used by 'unanswered' too
139     """
140     List of Questions, Tagged questions, and Unanswered questions.
141     """
142     # template file
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)
149     try:
150         page = int(request.GET.get('page', '1'))
151     except ValueError:
152         page = 1
153
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')
156
157     # check if request is from tagged questions
158     qs = Question.objects.exclude(deleted=True)
159
160     if tagname is not None:
161         qs = qs.filter(tags__name = unquote(tagname))
162
163     if unanswered:
164         qs = qs.exclude(answer_accepted=True)
165
166     author_name = None
167     #user contributed questions & answers
168     if 'user' in request.GET:
169         try:
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:
174             author_name = None
175
176     if request.user.is_authenticated():
177         uid_str = str(request.user.id)
178         qs = qs.extra(
179                         select = SortedDict([
180                             (
181                                 'interesting_score', 
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'
187                             ),
188                                 ]),
189                         select_params = (uid_str,),
190                      )
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)
195         else:
196             qs = qs.extra(
197                         select = SortedDict([
198                             (
199                                 'ignored_score', 
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'
205                             )
206                                 ]),
207                         select_params = (uid_str, )
208                      )
209
210     qs = qs.select_related(depth=1).order_by(orderby)
211
212     objects_list = Paginator(qs, pagesize)
213     questions = objects_list.page(page)
214
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)
218     else:
219         related_tags = None
220     tags_autocomplete = _get_tags_cache_json()
221
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)
228
229     return render_to_response(template_file, {
230         "questions" : questions,
231         "author_name" : author_name,
232         "tab_id" : view_id,
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, 
240         "context" : {
241             'is_paginated' : True,
242             'pages': objects_list.num_pages,
243             'page': page,
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))
251
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
257     """
258     if request.method == "GET":
259         keywords = request.GET.get("q")
260         search_type = request.GET.get("t")
261         try:
262             page = int(request.GET.get('page', '1'))
263         except ValueError:
264             page = 1
265         if keywords is None:
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":
272             
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
281
282             try:
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
288
289             except ValueError:
290                 page = 1
291                 pagesize  = user_page_size
292
293             # save this pagesize to user database
294             if pagesize_changed:
295                 request.session["pagesize"] = pagesize
296                 if request.user.is_authenticated():
297                     user = request.user
298                     user.questions_per_page = pagesize
299                     user.save()
300
301             view_id = request.GET.get('sort', None)
302             view_dic = {"latest":"-added_at", "active":"-last_activity_at", "hottest":"-answer_count", "mostvoted":"-score" }
303             try:
304                 orderby = view_dic[view_id]
305             except KeyError:
306                 view_id = "latest"
307                 orderby = "-added_at"
308
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();
313
314             from forum.modules import get_handler
315
316             question_search = get_handler('question_search', question_search)
317             
318             objects = question_search(keywords, orderby)
319
320             objects_list = Paginator(objects, pagesize)
321             questions = objects_list.page(page)
322
323             # Get related tags from this page objects
324             related_tags = []
325             for question in questions.object_list:
326                 tags = list(question.tags.all())
327                 for tag in tags:
328                     if tag not in related_tags:
329                         related_tags.append(tag)
330
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)
333
334             return render_to_response(template_file, {
335                 "questions" : questions,
336                 "tab_id" : view_id,
337                 "questions_count" : objects_list.count,
338                 "tags" : related_tags,
339                 "searchtag" : None,
340                 "searchtitle" : keywords,
341                 "keywords" : keywords,
342                 "is_unanswered" : False,
343                 "is_search": True, 
344                 "search_uri":  search_uri, 
345                 "context" : {
346                     'is_paginated' : True,
347                     'pages': objects_list.num_pages,
348                     'page': page,
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))
356  
357     else:
358         raise Http404
359
360 def tag(request, tag):#stub generates listing of questions tagged with a single tag
361     return questions(request, tagname=tag)
362
363 def tags(request):#view showing a listing of available tags - plain list
364     stag = ""
365     is_paginated = True
366     sortby = request.GET.get('sort', 'used')
367     try:
368         page = int(request.GET.get('page', '1'))
369     except ValueError:
370         page = 1
371
372     if request.method == "GET":
373         stag = request.GET.get("q", "").strip()
374         if stag != '':
375             objects_list = Paginator(Tag.objects.filter(deleted=False).exclude(used_count=0).extra(where=['name like %s'], params=['%' + stag + '%']), DEFAULT_PAGE_SIZE)
376         else:
377             if sortby == "name":
378                 objects_list = Paginator(Tag.objects.all().filter(deleted=False).exclude(used_count=0).order_by("name"), DEFAULT_PAGE_SIZE)
379             else:
380                 objects_list = Paginator(Tag.objects.all().filter(deleted=False).exclude(used_count=0).order_by("-used_count"), DEFAULT_PAGE_SIZE)
381
382     try:
383         tags = objects_list.page(page)
384     except (EmptyPage, InvalidPage):
385         tags = objects_list.page(objects_list.num_pages)
386
387     return render_to_response('tags.html', {
388                                             "tags" : tags,
389                                             "stag" : stag,
390                                             "tab_id" : sortby,
391                                             "keywords" : stag,
392                                             "context" : {
393                                                 'is_paginated' : is_paginated,
394                                                 'pages': objects_list.num_pages,
395                                                 'page': page,
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
401                                             }
402                                 }, context_instance=RequestContext(request))
403
404 def question(request, id):#refactor - long subroutine. display question body, answers and comments
405     """view that displays body of the question and 
406     all answers to it
407     """
408     try:
409         page = int(request.GET.get('page', '1'))
410     except ValueError:
411         page = 1
412
413     view_id = request.GET.get('sort', None)
414     view_dic = {"latest":"-added_at", "oldest":"added_at", "votes":"-score" }
415     try:
416         orderby = view_dic[view_id]
417     except KeyError:
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':
422                 view_id = 'votes'
423                 orderby = '-score'
424             else:
425                 view_id = 'latest'
426                 orderby = '-added_at'
427         else:
428             view_id = "votes"
429             orderby = "-score"
430
431     logging.debug('view_id=' + str(view_id))
432
433     question = get_object_or_404(Question, id=id)
434     try:
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)
440         if m:
441             slug = m.group(1)
442             logging.debug('have slug %s' % slug)
443             assert(slug == slugify(question.title))
444         else:
445             logging.debug('no match!')
446     except:
447         return HttpResponseRedirect(question.get_absolute_url())
448
449     if question.deleted and not auth.can_view_deleted_post(request.user, question):
450         raise Http404
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)
454
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)
458     else:
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]
462
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):
467             vote_value = -1
468             if vote.is_upvote():
469                 vote_value = 1
470             user_answer_votes[answer.id] = vote_value
471
472     if answers is not None:
473         answers = answers.order_by("-accepted", orderby)
474
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)
480         else:
481             filtered_answers.append(answer)
482
483     objects_list = Paginator(filtered_answers, ANSWERS_PAGE_SIZE)
484     page_objects = objects_list.page(page)
485
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'] = {}
491
492     last_seen = request.session['question_view_times'].get(question.id,None)
493     updated_when, updated_who = question.get_last_update_info()
494
495     if updated_who != request.user:
496         if last_seen:
497             if last_seen < updated_when:
498                 update_view_count = True 
499         else:
500             update_view_count = True
501
502     request.session['question_view_times'][question.id] = datetime.datetime.now()
503
504     if update_view_count:
505         question.view_count += 1
506         question.save()
507
508     #2) question view count per user
509     if request.user.is_authenticated():
510         try:
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()
515         question_view.save()
516
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(),
525         "tab_id" : view_id,
526         "favorited" : favorited,
527         "similar_questions" : Question.objects.get_similar_questions(question),
528         "context" : {
529             'is_paginated' : True,
530             'pages': objects_list.num_pages,
531             'page': page,
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"
538         }
539         }, context_instance=RequestContext(request))
540
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())
547     revisions.reverse()
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(' ')]),
554         }
555         if i > 0:
556             revisions[i].diff = htmldiff(revisions[i-1].html, revision.html)
557         else:
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(' ')]),
563             }
564             revisions[i].summary = _('initial version') 
565     return render_to_response('revisions_question.html', {
566                               'post': post,
567                               'revisions': revisions,
568                               }, context_instance=RequestContext(request))
569
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())
574     revisions.reverse()
575     for i, revision in enumerate(revisions):
576         revision.html = ANSWER_REVISION_TEMPLATE % {
577             'html': sanitize_html(markdowner.convert(revision.text))
578         }
579         if i > 0:
580             revisions[i].diff = htmldiff(revisions[i-1].html, revision.html)
581         else:
582             revisions[i].diff = revisions[i].text
583             revisions[i].summary = _('initial version')
584     return render_to_response('revisions_answer.html', {
585                               'post': post,
586                               'revisions': revisions,
587                               }, context_instance=RequestContext(request))
588