]> git.openstreetmap.org Git - osqa.git/blob - forum/views/commands.py
Makes related questions search work with weighted results.
[osqa.git] / forum / views / commands.py
1 import datetime
2 from forum import settings
3 from django.core.exceptions import ObjectDoesNotExist
4 from django.utils import simplejson
5 from django.http import HttpResponse, HttpResponseRedirect, Http404
6 from django.shortcuts import get_object_or_404, render_to_response
7 from django.utils.translation import ungettext, ugettext as _
8 from django.template import RequestContext
9 from django.template.loader import render_to_string
10 from forum.models import *
11 from forum.models.node import NodeMetaClass
12 from forum.actions import *
13 from django.core.urlresolvers import reverse
14 from forum.utils.decorators import ajax_method, ajax_login_required
15 from decorators import command, CommandException, RefreshPageCommand
16 from forum.modules import decorate
17 from forum import settings
18 import logging
19
20 class NotEnoughRepPointsException(CommandException):
21     def __init__(self, action):
22         super(NotEnoughRepPointsException, self).__init__(
23                 _(
24                         """Sorry, but you don't have enough reputation points to %(action)s.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
25                         ) % {'action': action, 'faq_url': reverse('faq')}
26                 )
27
28 class CannotDoOnOwnException(CommandException):
29     def __init__(self, action):
30         super(CannotDoOnOwnException, self).__init__(
31                 _(
32                         """Sorry but you cannot %(action)s your own post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
33                         ) % {'action': action, 'faq_url': reverse('faq')}
34                 )
35
36 class AnonymousNotAllowedException(CommandException):
37     def __init__(self, action):
38         super(AnonymousNotAllowedException, self).__init__(
39                 _(
40                         """Sorry but anonymous users cannot %(action)s.<br />Please login or create an account <a href='%(signin_url)s'>here</a>."""
41                         ) % {'action': action, 'signin_url': reverse('auth_signin')}
42                 )
43
44 class NotEnoughLeftException(CommandException):
45     def __init__(self, action, limit):
46         super(NotEnoughLeftException, self).__init__(
47                 _(
48                         """Sorry, but you don't have enough %(action)s left for today..<br />The limit is %(limit)s per day..<br />Please check the <a href='%(faq_url)s'>faq</a>"""
49                         ) % {'action': action, 'limit': limit, 'faq_url': reverse('faq')}
50                 )
51
52 class CannotDoubleActionException(CommandException):
53     def __init__(self, action):
54         super(CannotDoubleActionException, self).__init__(
55                 _(
56                         """Sorry, but you cannot %(action)s twice the same post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
57                         ) % {'action': action, 'faq_url': reverse('faq')}
58                 )
59
60
61 @decorate.withfn(command)
62 def vote_post(request, id, vote_type):
63     post = get_object_or_404(Node, id=id).leaf
64     user = request.user
65
66     if not user.is_authenticated():
67         raise AnonymousNotAllowedException(_('vote'))
68
69     if user == post.author:
70         raise CannotDoOnOwnException(_('vote'))
71
72     if not (vote_type == 'up' and user.can_vote_up() or user.can_vote_down()):
73         raise NotEnoughRepPointsException(vote_type == 'up' and _('upvote') or _('downvote'))
74
75     user_vote_count_today = user.get_vote_count_today()
76
77     if user_vote_count_today >= user.can_vote_count_today():
78         raise NotEnoughLeftException(_('votes'), str(settings.MAX_VOTES_PER_DAY))
79
80     new_vote_cls = (vote_type == 'up') and VoteUpAction or VoteDownAction
81     score_inc = 0
82
83     old_vote = VoteAction.get_action_for(node=post, user=user)
84
85     if old_vote:
86         if old_vote.action_date < datetime.datetime.now() - datetime.timedelta(days=int(settings.DENY_UNVOTE_DAYS)):
87             raise CommandException(
88                     _("Sorry but you cannot cancel a vote after %(ndays)d %(tdays)s from the original vote") %
89                     {'ndays': int(settings.DENY_UNVOTE_DAYS),
90                      'tdays': ungettext('day', 'days', int(settings.DENY_UNVOTE_DAYS))}
91                     )
92
93         old_vote.cancel(ip=request.META['REMOTE_ADDR'])
94         score_inc = (old_vote.__class__ == VoteDownAction) and 1 or -1
95         vote_type = "none"
96     else:
97         new_vote_cls(user=user, node=post, ip=request.META['REMOTE_ADDR']).save()
98         score_inc = (new_vote_cls == VoteUpAction) and 1 or -1
99
100     response = {
101     'commands': {
102     'update_post_score': [id, score_inc],
103     'update_user_post_vote': [id, vote_type]
104     }
105     }
106
107     votes_left = (int(settings.MAX_VOTES_PER_DAY) - user_vote_count_today) + (vote_type == 'none' and -1 or 1)
108
109     if int(settings.START_WARN_VOTES_LEFT) >= votes_left:
110         response['message'] = _("You have %(nvotes)s %(tvotes)s left today.") % \
111                     {'nvotes': votes_left, 'tvotes': ungettext('vote', 'votes', votes_left)}
112
113     return response
114
115 @decorate.withfn(command)
116 def flag_post(request, id):
117     if not request.POST:
118         return render_to_response('node/report.html', {'types': settings.FLAG_TYPES})
119
120     post = get_object_or_404(Node, id=id)
121     user = request.user
122
123     if not user.is_authenticated():
124         raise AnonymousNotAllowedException(_('flag posts'))
125
126     if user == post.author:
127         raise CannotDoOnOwnException(_('flag'))
128
129     if not (user.can_flag_offensive(post)):
130         raise NotEnoughRepPointsException(_('flag posts'))
131
132     user_flag_count_today = user.get_flagged_items_count_today()
133
134     if user_flag_count_today >= int(settings.MAX_FLAGS_PER_DAY):
135         raise NotEnoughLeftException(_('flags'), str(settings.MAX_FLAGS_PER_DAY))
136
137     try:
138         current = FlagAction.objects.get(canceled=False, user=user, node=post)
139         raise CommandException(
140                 _("You already flagged this post with the following reason: %(reason)s") % {'reason': current.extra})
141     except ObjectDoesNotExist:
142         reason = request.POST.get('prompt', '').strip()
143
144         if not len(reason):
145             raise CommandException(_("Reason is empty"))
146
147         FlagAction(user=user, node=post, extra=reason, ip=request.META['REMOTE_ADDR']).save()
148
149     return {'message': _("Thank you for your report. A moderator will review your submission shortly.")}
150
151 @decorate.withfn(command)
152 def like_comment(request, id):
153     comment = get_object_or_404(Comment, id=id)
154     user = request.user
155
156     if not user.is_authenticated():
157         raise AnonymousNotAllowedException(_('like comments'))
158
159     if user == comment.user:
160         raise CannotDoOnOwnException(_('like'))
161
162     if not user.can_like_comment(comment):
163         raise NotEnoughRepPointsException( _('like comments'))
164
165     like = VoteAction.get_action_for(node=comment, user=user)
166
167     if like:
168         like.cancel(ip=request.META['REMOTE_ADDR'])
169         likes = False
170     else:
171         VoteUpCommentAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
172         likes = True
173
174     return {
175     'commands': {
176     'update_post_score': [comment.id, likes and 1 or -1],
177     'update_user_post_vote': [comment.id, likes and 'up' or 'none']
178     }
179     }
180
181 @decorate.withfn(command)
182 def delete_comment(request, id):
183     comment = get_object_or_404(Comment, id=id)
184     user = request.user
185
186     if not user.is_authenticated():
187         raise AnonymousNotAllowedException(_('delete comments'))
188
189     if not user.can_delete_comment(comment):
190         raise NotEnoughRepPointsException( _('delete comments'))
191
192     if not comment.nis.deleted:
193         DeleteAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
194
195     return {
196     'commands': {
197     'remove_comment': [comment.id],
198     }
199     }
200
201 @decorate.withfn(command)
202 def mark_favorite(request, id):
203     question = get_object_or_404(Question, id=id)
204
205     if not request.user.is_authenticated():
206         raise AnonymousNotAllowedException(_('mark a question as favorite'))
207
208     try:
209         favorite = FavoriteAction.objects.get(canceled=False, node=question, user=request.user)
210         favorite.cancel(ip=request.META['REMOTE_ADDR'])
211         added = False
212     except ObjectDoesNotExist:
213         FavoriteAction(node=question, user=request.user, ip=request.META['REMOTE_ADDR']).save()
214         added = True
215
216     return {
217     'commands': {
218     'update_favorite_count': [added and 1 or -1],
219     'update_favorite_mark': [added and 'on' or 'off']
220     }
221     }
222
223 @decorate.withfn(command)
224 def comment(request, id):
225     post = get_object_or_404(Node, id=id)
226     user = request.user
227
228     if not user.is_authenticated():
229         raise AnonymousNotAllowedException(_('comment'))
230
231     if not request.method == 'POST':
232         raise CommandException(_("Invalid request"))
233
234     comment_text = request.POST.get('comment', '').strip()
235
236     if not len(comment_text):
237         raise CommandException(_("Comment is empty"))
238
239     if len(comment_text) < settings.FORM_MIN_COMMENT_BODY:
240         raise CommandException(_("At least %d characters required on comment body.") % settings.FORM_MIN_COMMENT_BODY)
241
242     if len(comment_text) > settings.FORM_MAX_COMMENT_BODY:
243         raise CommandException(_("No more than %d characters on comment body.") % settings.FORM_MAX_COMMENT_BODY)
244
245     if 'id' in request.POST:
246         comment = get_object_or_404(Comment, id=request.POST['id'])
247
248         if not user.can_edit_comment(comment):
249             raise NotEnoughRepPointsException( _('edit comments'))
250
251         comment = ReviseAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(
252                 data=dict(text=comment_text)).node
253     else:
254         if not user.can_comment(post):
255             raise NotEnoughRepPointsException( _('comment'))
256
257         comment = CommentAction(user=user, ip=request.META['REMOTE_ADDR']).save(
258                 data=dict(text=comment_text, parent=post)).node
259
260     if comment.active_revision.revision == 1:
261         return {
262         'commands': {
263         'insert_comment': [
264                 id, comment.id, comment.comment, user.decorated_name, user.get_profile_url(),
265                 reverse('delete_comment', kwargs={'id': comment.id}),
266                 reverse('node_markdown', kwargs={'id': comment.id}),
267                 reverse('convert_comment', kwargs={'id': comment.id}),
268                 user.can_convert_comment_to_answer(comment),
269                 ]
270         }
271         }
272     else:
273         return {
274         'commands': {
275         'update_comment': [comment.id, comment.comment]
276         }
277         }
278
279 @decorate.withfn(command)
280 def node_markdown(request, id):
281     user = request.user
282
283     if not user.is_authenticated():
284         raise AnonymousNotAllowedException(_('accept answers'))
285
286     node = get_object_or_404(Node, id=id)
287     return HttpResponse(node.active_revision.body, mimetype="text/plain")
288
289
290 @decorate.withfn(command)
291 def accept_answer(request, id):
292     if settings.DISABLE_ACCEPTING_FEATURE:
293         raise Http404()
294
295     user = request.user
296
297     if not user.is_authenticated():
298         raise AnonymousNotAllowedException(_('accept answers'))
299
300     answer = get_object_or_404(Answer, id=id)
301     question = answer.question
302
303     if not user.can_accept_answer(answer):
304         raise CommandException(_("Sorry but you cannot accept the answer"))
305
306     commands = {}
307
308     if answer.nis.accepted:
309         answer.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR'])
310         commands['unmark_accepted'] = [answer.id]
311     else:
312         if settings.MAXIMUM_ACCEPTED_ANSWERS and (question.accepted_count >= settings.MAXIMUM_ACCEPTED_ANSWERS):
313             raise CommandException(ungettext("This question already has an accepted answer.",
314                 "Sorry but this question has reached the limit of accepted answers.", int(settings.MAXIMUM_ACCEPTED_ANSWERS)))
315
316         if settings.MAXIMUM_ACCEPTED_PER_USER and question.accepted_count:
317             accepted_from_author = question.accepted_answers.filter(author=answer.author).count()
318
319             if accepted_from_author >= settings.MAXIMUM_ACCEPTED_PER_USER:
320                 raise CommandException(ungettext("The author of this answer already has an accepted answer in this question.",
321                 "Sorry but the author of this answer has reached the limit of accepted answers per question.", int(settings.MAXIMUM_ACCEPTED_PER_USER)))             
322
323
324         AcceptAnswerAction(node=answer, user=user, ip=request.META['REMOTE_ADDR']).save()
325         commands['mark_accepted'] = [answer.id]
326
327     return {'commands': commands}
328
329 @decorate.withfn(command)
330 def delete_post(request, id):
331     post = get_object_or_404(Node, id=id)
332     user = request.user
333
334     if not user.is_authenticated():
335         raise AnonymousNotAllowedException(_('delete posts'))
336
337     if not (user.can_delete_post(post)):
338         raise NotEnoughRepPointsException(_('delete posts'))
339
340     ret = {'commands': {}}
341
342     if post.nis.deleted:
343         post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
344         ret['commands']['unmark_deleted'] = [post.node_type, id]
345     else:
346         DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
347
348         ret['commands']['mark_deleted'] = [post.node_type, id]
349
350     return ret
351
352 @decorate.withfn(command)
353 def close(request, id, close):
354     if close and not request.POST:
355         return render_to_response('node/report.html', {'types': settings.CLOSE_TYPES})
356
357     question = get_object_or_404(Question, id=id)
358     user = request.user
359
360     if not user.is_authenticated():
361         raise AnonymousNotAllowedException(_('close questions'))
362
363     if question.nis.closed:
364         if not user.can_reopen_question(question):
365             raise NotEnoughRepPointsException(_('reopen questions'))
366
367         question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
368     else:
369         if not request.user.can_close_question(question):
370             raise NotEnoughRepPointsException(_('close questions'))
371
372         reason = request.POST.get('prompt', '').strip()
373
374         if not len(reason):
375             raise CommandException(_("Reason is empty"))
376
377         CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
378
379     return RefreshPageCommand()
380
381 @decorate.withfn(command)
382 def wikify(request, id):
383     node = get_object_or_404(Node, id=id)
384     user = request.user
385
386     if not user.is_authenticated():
387         raise AnonymousNotAllowedException(_('mark posts as community wiki'))
388
389     if node.nis.wiki:
390         if not user.can_cancel_wiki(node):
391             raise NotEnoughRepPointsException(_('cancel a community wiki post'))
392
393         if node.nstate.wiki.action_type == "wikify":
394             node.nstate.wiki.cancel()
395         else:
396             node.nstate.wiki = None
397     else:
398         if not user.can_wikify(node):
399             raise NotEnoughRepPointsException(_('mark posts as community wiki'))
400
401         WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
402
403     return RefreshPageCommand()
404
405 @decorate.withfn(command)
406 def convert_to_comment(request, id):
407     user = request.user
408     answer = get_object_or_404(Answer, id=id)
409     question = answer.question
410
411     if not request.POST:
412         description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': a.author.username,
413                                                                             'snippet': a.summary[:10]}
414         nodes = [(question.id, _("Question"))]
415         [nodes.append((a.id, description(a))) for a in
416          question.answers.filter_state(deleted=False).exclude(id=answer.id)]
417
418         return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
419
420     if not user.is_authenticated():
421         raise AnonymousNotAllowedException(_("convert answers to comments"))
422
423     if not user.can_convert_to_comment(answer):
424         raise NotEnoughRepPointsException(_("convert answers to comments"))
425
426     try:
427         new_parent = Node.objects.get(id=request.POST.get('under', None))
428     except:
429         raise CommandException(_("That is an invalid post to put the comment under"))
430
431     if not (new_parent == question or (new_parent.node_type == 'answer' and new_parent.parent == question)):
432         raise CommandException(_("That is an invalid post to put the comment under"))
433
434     AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
435
436     return RefreshPageCommand()
437
438 @decorate.withfn(command)
439 def convert_comment_to_answer(request, id):
440     user = request.user
441     comment = get_object_or_404(Comment, id=id)
442     parent = comment.parent
443
444     if not parent.question:
445         question = parent
446     else:
447         question = parent.question
448     
449     if not user.is_authenticated():
450         raise AnonymousNotAllowedException(_("convert comments to answers"))
451
452     if not user.can_convert_comment_to_answer(comment):
453         raise NotEnoughRepPointsException(_("convert comments to answers"))
454     
455     CommentToAnswerAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(question=question))
456
457     return RefreshPageCommand()
458
459 @decorate.withfn(command)
460 def subscribe(request, id, user=None):
461     if user:
462         try:
463             user = User.objects.get(id=user)
464         except User.DoesNotExist:
465             raise Http404()
466
467         if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
468             raise CommandException(_("You do not have the correct credentials to preform this action."))
469     else:
470         user = request.user
471
472     question = get_object_or_404(Question, id=id)
473
474     try:
475         subscription = QuestionSubscription.objects.get(question=question, user=user)
476         subscription.delete()
477         subscribed = False
478     except:
479         subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
480         subscription.save()
481         subscribed = True
482
483     return {
484         'commands': {
485             'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
486             'set_subscription_status': ['']
487         }
488     }
489
490 #internally grouped views - used by the tagging system
491 @ajax_login_required
492 def mark_tag(request, tag=None, **kwargs):#tagging system
493     action = kwargs['action']
494     ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
495     if action == 'remove':
496         logging.debug('deleting tag %s' % tag)
497         ts.delete()
498     else:
499         reason = kwargs['reason']
500         if len(ts) == 0:
501             try:
502                 t = Tag.objects.get(name=tag)
503                 mt = MarkedTag(user=request.user, reason=reason, tag=t)
504                 mt.save()
505             except:
506                 pass
507         else:
508             ts.update(reason=reason)
509     return HttpResponse(simplejson.dumps(''), mimetype="application/json")
510
511 def matching_tags(request):
512     if len(request.GET['q']) == 0:
513         raise CommandException(_("Invalid request"))
514
515     possible_tags = Tag.active.filter(name__icontains = request.GET['q'])
516     tag_output = ''
517     for tag in possible_tags:
518         tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
519
520     return HttpResponse(tag_output, mimetype="text/plain")
521
522 def matching_users(request):
523     if len(request.GET['q']) == 0:
524         raise CommandException(_("Invalid request"))
525
526     possible_users = User.objects.filter(username__icontains = request.GET['q'])
527     output = ''
528
529     for user in possible_users:
530         output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
531
532     return HttpResponse(output, mimetype="text/plain")
533
534 def related_questions(request):
535     if request.POST and request.POST.get('title', None):
536         can_rank, questions = Question.objects.search(request.POST['title'])
537
538         if can_rank and isinstance(can_rank, basestring):
539             questions = questions.order_by(can_rank)
540
541         return HttpResponse(simplejson.dumps(
542                 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
543                  for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")
544     else:
545         raise Http404()
546
547 @decorate.withfn(command)
548 def answer_permanent_link(request, id):
549     # Getting the current answer object
550     answer = get_object_or_404(Answer, id=id)
551
552     # Getting the current object URL -- the Application URL + the object relative URL
553     url = '%s%s' % (settings.APP_BASE_URL, answer.get_absolute_url())
554
555     if not request.POST:
556         # Display the template
557         return render_to_response('node/permanent_link.html', { 'url' : url, })
558
559     return {
560         'commands' : {
561             'copy_url' : [request.POST['permanent_link_url'],],
562         },
563         'message' : _("The permanent URL to the answer has been copied to your clipboard."),
564     }
565
566 @decorate.withfn(command)
567 def award_points(request, user_id, answer_id):
568     user = request.user
569     awarded_user = get_object_or_404(User, id=user_id)
570     answer = get_object_or_404(Answer, id=answer_id)
571
572     # Users shouldn't be able to award themselves
573     if awarded_user.id == user.id:
574         raise CannotDoOnOwnException(_("award"))
575
576     # Anonymous users cannot award  points, they just don't have such
577     if not user.is_authenticated():
578         raise AnonymousNotAllowedException(_('award'))
579
580     if not request.POST:
581         return render_to_response("node/award_points.html", { 'user' : user, 'awarded_user' : awarded_user, })
582     else:
583         points = int(request.POST['points'])
584
585         # We should check if the user has enough reputation points, otherwise we raise an exception.
586         if user.reputation < points:
587             raise NotEnoughRepPointsException(_("award"))
588
589         extra = dict(message=request.POST.get('message', ''), awarding_user=request.user.id, value=points)
590
591         # We take points from the awarding user
592         AwardPointsAction(user=request.user, node=answer, extra=extra).save(data=dict(value=points, affected=awarded_user))
593
594         return { 'message' : _("You have awarded %(awarded_user)s with %(points)d points") % {'awarded_user' : awarded_user, 'points' : points} }