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