]> git.openstreetmap.org Git - osqa.git/blob - forum/views/readers.py
Fixes OSQA-88 (Improve "hottest" functionality on front page), and makes some improve...
[osqa.git] / forum / views / readers.py
1 # encoding:utf-8
2 import datetime
3 import logging
4 from urllib import unquote
5 from forum import settings as django_settings
6 from django.shortcuts import render_to_response, get_object_or_404
7 from django.http import HttpResponseRedirect, HttpResponse, Http404, HttpResponsePermanentRedirect
8 from django.core.paginator import Paginator, EmptyPage, InvalidPage
9 from django.template import RequestContext
10 from django import template
11 from django.utils.html import *
12 from django.utils import simplejson
13 from django.db.models import Q, Count
14 from django.utils.translation import ugettext as _
15 from django.template.defaultfilters import slugify
16 from django.core.urlresolvers import reverse
17 from django.utils.datastructures import SortedDict
18 from django.views.decorators.cache import cache_page
19 from django.utils.http import urlquote  as django_urlquote
20 from django.template.defaultfilters import slugify
21 from django.utils.safestring import mark_safe
22
23 from forum.utils.html import sanitize_html, hyperlink
24 from forum.utils.diff import textDiff as htmldiff
25 from forum.utils import pagination
26 from forum.forms import *
27 from forum.models import *
28 from forum.forms import get_next_url
29 from forum.actions import QuestionViewAction
30 from forum.http_responses import HttpResponseUnauthorized
31 from forum.feed import RssQuestionFeed, RssAnswerFeed
32 import decorators
33
34 class HottestQuestionsSort(pagination.SortBase):
35     def apply(self, questions):
36         return questions.annotate(new_child_count=Count('all_children')).filter(
37                 all_children__added_at__gt=datetime.datetime.now() - datetime.timedelta(days=1)).order_by('-new_child_count')
38
39
40 class QuestionListPaginatorContext(pagination.PaginatorContext):
41     def __init__(self, id='QUESTIONS_LIST', prefix='', default_pagesize=30):
42         super (QuestionListPaginatorContext, self).__init__(id, sort_methods=(
43             (_('active'), pagination.SimpleSort(_('active'), '-last_activity_at', _("Most <strong>recently updated</strong> questions"))),
44             (_('newest'), pagination.SimpleSort(_('newest'), '-added_at', _("most <strong>recently asked</strong> questions"))),
45             (_('hottest'), HottestQuestionsSort(_('hottest'), _("most <strong>active</strong> questions in the last 24 hours</strong>"))),
46             (_('mostvoted'), pagination.SimpleSort(_('most voted'), '-score', _("most <strong>voted</strong> questions"))),
47         ), pagesizes=(15, 30, 50), default_pagesize=default_pagesize, prefix=prefix)
48
49 class AnswerPaginatorContext(pagination.PaginatorContext):
50     def __init__(self, id='ANSWER_LIST', prefix='', default_pagesize=10):
51         super (AnswerPaginatorContext, self).__init__(id, sort_methods=(
52             (_('oldest'), pagination.SimpleSort(_('oldest answers'), ('-marked', 'added_at'), _("oldest answers will be shown first"))),
53             (_('newest'), pagination.SimpleSort(_('newest answers'), ('-marked', '-added_at'), _("newest answers will be shown first"))),
54             (_('votes'), pagination.SimpleSort(_('popular answers'), ('-marked', '-score', 'added_at'), _("most voted answers will be shown first"))),
55         ), default_sort=_('votes'), pagesizes=(5, 10, 20), default_pagesize=default_pagesize, prefix=prefix)
56
57 class TagPaginatorContext(pagination.PaginatorContext):
58     def __init__(self):
59         super (TagPaginatorContext, self).__init__('TAG_LIST', sort_methods=(
60             (_('name'), pagination.SimpleSort(_('by name'), 'name', _("sorted alphabetically"))),
61             (_('used'), pagination.SimpleSort(_('by popularity'), '-used_count', _("sorted by frequency of tag use"))),
62         ), default_sort=_('used'), pagesizes=(30, 60, 120))
63     
64
65 def feed(request):
66     return RssQuestionFeed(
67                 request,
68                 Question.objects.filter_state(deleted=False).order_by('-last_activity_at'),
69                 settings.APP_TITLE + _(' - ')+ _('latest questions'),
70                 settings.APP_DESCRIPTION)(request)
71
72
73 @decorators.render('index.html')
74 def index(request):
75     paginator_context = QuestionListPaginatorContext()
76     paginator_context.base_path = reverse('questions')
77     return question_list(request,
78                          Question.objects.all(),
79                          base_path=reverse('questions'),
80                          feed_url=reverse('latest_questions_feed'),
81                          paginator_context=paginator_context)
82
83 @decorators.render('questions.html', 'unanswered', _('unanswered'), weight=400)
84 def unanswered(request):
85     return question_list(request,
86                          Question.objects.filter(extra_ref=None),
87                          _('open questions without an accepted answer'),
88                          None,
89                          _("Unanswered Questions"))
90
91 @decorators.render('questions.html', 'questions', _('questions'), weight=0)
92 def questions(request):
93     return question_list(request, Question.objects.all(), _('questions'))
94
95 @decorators.render('questions.html')
96 def tag(request, tag):
97     return question_list(request,
98                          Question.objects.filter(tags__name=unquote(tag)),
99                          mark_safe(_('questions tagged <span class="tag">%(tag)s</span>') % {'tag': tag}),
100                          None,
101                          mark_safe(_('Questions Tagged With %(tag)s') % {'tag': tag}),
102                          False)
103
104 @decorators.render('questions.html', 'questions', tabbed=False)
105 def user_questions(request, mode, user, slug):
106     user = get_object_or_404(User, id=user)
107
108     if mode == _('asked-by'):
109         questions = Question.objects.filter(author=user)
110         description = _("Questions asked by %s")
111     elif mode == _('answered-by'):
112         questions = Question.objects.filter(children__author=user, children__node_type='answer').distinct()
113         description = _("Questions answered by %s")
114     elif mode == _('subscribed-by'):
115         if not (request.user.is_superuser or request.user == user):
116             return HttpResponseUnauthorized(request)
117         questions = user.subscriptions
118
119         if request.user == user:
120             description = _("Questions you subscribed %s")
121         else:
122             description = _("Questions subscribed by %s")
123     else:
124         raise Http404
125
126
127     return question_list(request, questions,
128                          mark_safe(description % hyperlink(user.get_profile_url(), user.username)),
129                          page_title=description % user.username)
130
131 def question_list(request, initial,
132                   list_description=_('questions'),
133                   base_path=None,
134                   page_title=_("All Questions"),
135                   allowIgnoreTags=True,
136                   feed_url=None,
137                   paginator_context=None):
138
139     questions = initial.filter_state(deleted=False)
140
141     if request.user.is_authenticated() and allowIgnoreTags:
142         questions = questions.filter(~Q(tags__id__in = request.user.marked_tags.filter(user_selections__reason = 'bad')))
143
144     if page_title is None:
145         page_title = _("Questions")
146
147     if request.GET.get('type', None) == 'rss':
148         questions = questions.order_by('-added_at')
149         return RssQuestionFeed(request, questions, page_title, list_description)(request)
150
151     keywords =  ""
152     if request.GET.get("q"):
153         keywords = request.GET.get("q").strip()
154
155     answer_count = Answer.objects.filter_state(deleted=False).filter(parent__in=questions).count()
156     answer_description = _("answers")
157
158     if not feed_url:
159         req_params = "&".join(["%s=%s" % (k, v) for k, v in request.GET.items() if not k in (_('page'), _('pagesize'), _('sort'))])
160         if req_params:
161             req_params = '&' + req_params
162
163         feed_url = mark_safe(request.path + "?type=rss" + req_params)
164
165     return pagination.paginated(request, ('questions', paginator_context or QuestionListPaginatorContext()), {
166     "questions" : questions,
167     "questions_count" : questions.count(),
168     "answer_count" : answer_count,
169     "keywords" : keywords,
170     "list_description": list_description,
171     "answer_description": answer_description,
172     "base_path" : base_path,
173     "page_title" : page_title,
174     "tab" : "questions",
175     'feed_url': feed_url,
176     })
177
178
179 def search(request):
180     if request.method == "GET" and "q" in request.GET:
181         keywords = request.GET.get("q")
182         search_type = request.GET.get("t")
183
184         if not keywords:
185             return HttpResponseRedirect(reverse(index))
186         if search_type == 'tag':
187             return HttpResponseRedirect(reverse('tags') + '?q=%s' % urlquote(keywords.strip()))
188         elif search_type == "user":
189             return HttpResponseRedirect(reverse('users') + '?q=%s' % urlquote(keywords.strip()))
190         elif search_type == "question":
191             return question_search(request, keywords)
192     else:
193         return render_to_response("search.html", context_instance=RequestContext(request))
194
195 @decorators.render('questions.html')
196 def question_search(request, keywords):
197     can_rank, initial = Question.objects.search(keywords)
198
199     if can_rank:
200         paginator_context = QuestionListPaginatorContext()
201         paginator_context.sort_methods[_('ranking')] = pagination.SimpleSort(_('relevance'), '-ranking', _("most relevant questions"))
202         paginator_context.force_sort = _('ranking')
203     else:
204         paginator_context = None
205
206     return question_list(request, initial,
207                          _("questions matching '%(keywords)s'") % {'keywords': keywords},
208                          False,
209                          "%s?t=question&q=%s" % (reverse('search'),django_urlquote(keywords)),
210                          _("questions matching '%(keywords)s'") % {'keywords': keywords},
211                          paginator_context=paginator_context)
212
213
214 @decorators.render('tags.html', 'tags', _('tags'), weight=100)
215 def tags(request):
216     stag = ""
217     tags = Tag.active.all()
218
219     if request.method == "GET":
220         stag = request.GET.get("q", "").strip()
221         if stag:
222             tags = tags.filter(name__contains=stag)
223
224     return pagination.paginated(request, ('tags', TagPaginatorContext()), {
225         "tags" : tags,
226         "stag" : stag,
227         "keywords" : stag
228     })
229
230 def update_question_view_times(request, question):
231     if not 'last_seen_in_question' in request.session:
232         request.session['last_seen_in_question'] = {}
233
234     last_seen = request.session['last_seen_in_question'].get(question.id, None)
235
236     if (not last_seen) or last_seen < question.last_activity_at:
237         QuestionViewAction(question, request.user, ip=request.META['REMOTE_ADDR']).save()
238         request.session['last_seen_in_question'][question.id] = datetime.datetime.now()
239
240     request.session['last_seen_in_question'][question.id] = datetime.datetime.now()
241
242 def match_question_slug(slug):
243     slug_words = slug.split('-')
244     qs = Question.objects.filter(title__istartswith=slug_words[0])
245
246     for q in qs:
247         if slug == urlquote(slugify(q.title)):
248             return q
249
250     return None
251
252 def answer_redirect(request, answer):
253     pc = AnswerPaginatorContext()
254
255     sort = pc.sort(request)
256
257     if sort == _('oldest'):
258         filter = Q(added_at__lt=answer.added_at)
259     elif sort == _('newest'):
260         filter = Q(added_at__gt=answer.added_at)
261     elif sort == _('votes'):
262         filter = Q(score__gt=answer.score) | Q(score=answer.score, added_at__lt=answer.added_at)
263     else:
264         raise Http404()
265         
266     count = answer.question.answers.filter(Q(marked=True) | filter).count()
267     pagesize = pc.pagesize(request)
268
269     page = count / pagesize
270     
271     if count % pagesize:
272         page += 1
273         
274     if page == 0:
275         page = 1
276
277     return HttpResponsePermanentRedirect("%s?%s=%s#%s" % (
278         answer.question.get_absolute_url(), _('page'), page, answer.id))
279
280 @decorators.render("question.html", 'questions')
281 def question(request, id, slug='', answer=None):
282     try:
283         question = Question.objects.get(id=id)
284     except:
285         if slug:
286             question = match_question_slug(slug)
287             if question is not None:
288                 return HttpResponseRedirect(question.get_absolute_url())
289
290         raise Http404()
291
292     if question.nis.deleted and not request.user.can_view_deleted_post(question):
293         raise Http404
294
295     if request.GET.get('type', None) == 'rss':
296         return RssAnswerFeed(request, question, include_comments=request.GET.get('comments', None) == 'yes')(request)
297
298     if answer:
299         answer = get_object_or_404(Answer, id=answer)
300
301         if (question.nis.deleted and not request.user.can_view_deleted_post(question)) or answer.question != question:
302             raise Http404
303
304         if answer.marked:
305             return HttpResponsePermanentRedirect(question.get_absolute_url())
306
307         return answer_redirect(request, answer)
308
309     if settings.FORCE_SINGLE_URL and (slug != slugify(question.title)):
310         return HttpResponsePermanentRedirect(question.get_absolute_url())
311
312     if request.POST:
313         answer_form = AnswerForm(question, request.POST)
314     else:
315         answer_form = AnswerForm(question)
316
317     answers = request.user.get_visible_answers(question)
318
319     update_question_view_times(request, question)
320
321     if request.user.is_authenticated():
322         try:
323             subscription = QuestionSubscription.objects.get(question=question, user=request.user)
324         except:
325             subscription = False
326     else:
327         subscription = False
328
329     return pagination.paginated(request, ('answers', AnswerPaginatorContext()), {
330     "question" : question,
331     "answer" : answer_form,
332     "answers" : answers,
333     "similar_questions" : question.get_related_questions(),
334     "subscription": subscription,
335     })
336
337
338 REVISION_TEMPLATE = template.loader.get_template('node/revision.html')
339
340 def revisions(request, id):
341     post = get_object_or_404(Node, id=id).leaf
342     revisions = list(post.revisions.order_by('revised_at'))
343     rev_ctx = []
344
345     for i, revision in enumerate(revisions):
346         rev_ctx.append(dict(inst=revision, html=template.loader.get_template('node/revision.html').render(template.Context({
347         'title': revision.title,
348         'html': revision.html,
349         'tags': revision.tagname_list(),
350         }))))
351
352         if i > 0:
353             rev_ctx[i]['diff'] = mark_safe(htmldiff(rev_ctx[i-1]['html'], rev_ctx[i]['html']))
354         else:
355             rev_ctx[i]['diff'] = mark_safe(rev_ctx[i]['html'])
356
357         if not (revision.summary):
358             rev_ctx[i]['summary'] = _('Revision n. %(rev_number)d') % {'rev_number': revision.revision}
359         else:
360             rev_ctx[i]['summary'] = revision.summary
361
362     rev_ctx.reverse()
363
364     return render_to_response('revisions.html', {
365     'post': post,
366     'revisions': rev_ctx,
367     }, context_instance=RequestContext(request))
368
369
370