]> git.openstreetmap.org Git - osqa.git/blob - forum/views/commands.py
#OSQA-581, resetting the answer count cache after the comment has been converted...
[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                 ]
269         }
270         }
271     else:
272         return {
273         'commands': {
274         'update_comment': [comment.id, comment.comment]
275         }
276         }
277
278 @decorate.withfn(command)
279 def node_markdown(request, id):
280     user = request.user
281
282     if not user.is_authenticated():
283         raise AnonymousNotAllowedException(_('accept answers'))
284
285     node = get_object_or_404(Node, id=id)
286     return HttpResponse(node.active_revision.body, mimetype="text/plain")
287
288
289 @decorate.withfn(command)
290 def accept_answer(request, id):
291     if settings.DISABLE_ACCEPTING_FEATURE:
292         raise Http404()
293
294     user = request.user
295
296     if not user.is_authenticated():
297         raise AnonymousNotAllowedException(_('accept answers'))
298
299     answer = get_object_or_404(Answer, id=id)
300     question = answer.question
301
302     if not user.can_accept_answer(answer):
303         raise CommandException(_("Sorry but you cannot accept the answer"))
304
305     commands = {}
306
307     if answer.nis.accepted:
308         answer.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR'])
309         commands['unmark_accepted'] = [answer.id]
310     else:
311         if settings.MAXIMUM_ACCEPTED_ANSWERS and (question.accepted_count >= settings.MAXIMUM_ACCEPTED_ANSWERS):
312             raise CommandException(ungettext("This question already has an accepted answer.",
313                 "Sorry but this question has reached the limit of accepted answers.", int(settings.MAXIMUM_ACCEPTED_ANSWERS)))
314
315         if settings.MAXIMUM_ACCEPTED_PER_USER and question.accepted_count:
316             accepted_from_author = question.accepted_answers.filter(author=answer.author).count()
317
318             if accepted_from_author >= settings.MAXIMUM_ACCEPTED_PER_USER:
319                 raise CommandException(ungettext("The author of this answer already has an accepted answer in this question.",
320                 "Sorry but the author of this answer has reached the limit of accepted answers per question.", int(settings.MAXIMUM_ACCEPTED_PER_USER)))             
321
322
323         AcceptAnswerAction(node=answer, user=user, ip=request.META['REMOTE_ADDR']).save()
324         commands['mark_accepted'] = [answer.id]
325
326     return {'commands': commands}
327
328 @decorate.withfn(command)
329 def delete_post(request, id):
330     post = get_object_or_404(Node, id=id)
331     user = request.user
332
333     if not user.is_authenticated():
334         raise AnonymousNotAllowedException(_('delete posts'))
335
336     if not (user.can_delete_post(post)):
337         raise NotEnoughRepPointsException(_('delete posts'))
338
339     ret = {'commands': {}}
340
341     if post.nis.deleted:
342         post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
343         ret['commands']['unmark_deleted'] = [post.node_type, id]
344     else:
345         DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
346
347         ret['commands']['mark_deleted'] = [post.node_type, id]
348
349     return ret
350
351 @decorate.withfn(command)
352 def close(request, id, close):
353     if close and not request.POST:
354         return render_to_response('node/report.html', {'types': settings.CLOSE_TYPES})
355
356     question = get_object_or_404(Question, id=id)
357     user = request.user
358
359     if not user.is_authenticated():
360         raise AnonymousNotAllowedException(_('close questions'))
361
362     if question.nis.closed:
363         if not user.can_reopen_question(question):
364             raise NotEnoughRepPointsException(_('reopen questions'))
365
366         question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
367     else:
368         if not request.user.can_close_question(question):
369             raise NotEnoughRepPointsException(_('close questions'))
370
371         reason = request.POST.get('prompt', '').strip()
372
373         if not len(reason):
374             raise CommandException(_("Reason is empty"))
375
376         CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
377
378     return RefreshPageCommand()
379
380 @decorate.withfn(command)
381 def wikify(request, id):
382     node = get_object_or_404(Node, id=id)
383     user = request.user
384
385     if not user.is_authenticated():
386         raise AnonymousNotAllowedException(_('mark posts as community wiki'))
387
388     if node.nis.wiki:
389         if not user.can_cancel_wiki(node):
390             raise NotEnoughRepPointsException(_('cancel a community wiki post'))
391
392         if node.nstate.wiki.action_type == "wikify":
393             node.nstate.wiki.cancel()
394         else:
395             node.nstate.wiki = None
396     else:
397         if not user.can_wikify(node):
398             raise NotEnoughRepPointsException(_('mark posts as community wiki'))
399
400         WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
401
402     return RefreshPageCommand()
403
404 @decorate.withfn(command)
405 def convert_to_comment(request, id):
406     user = request.user
407     answer = get_object_or_404(Answer, id=id)
408     question = answer.question
409
410     if not request.POST:
411         description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': a.author.username,
412                                                                             'snippet': a.summary[:10]}
413         nodes = [(question.id, _("Question"))]
414         [nodes.append((a.id, description(a))) for a in
415          question.answers.filter_state(deleted=False).exclude(id=answer.id)]
416
417         return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
418
419     if not user.is_authenticated():
420         raise AnonymousNotAllowedException(_("convert answers to comments"))
421
422     if not user.can_convert_to_comment(answer):
423         raise NotEnoughRepPointsException(_("convert answers to comments"))
424
425     try:
426         new_parent = Node.objects.get(id=request.POST.get('under', None))
427     except:
428         raise CommandException(_("That is an invalid post to put the comment under"))
429
430     if not (new_parent == question or (new_parent.node_type == 'answer' and new_parent.parent == question)):
431         raise CommandException(_("That is an invalid post to put the comment under"))
432
433     AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
434
435     return RefreshPageCommand()
436
437 @decorate.withfn(command)
438 def convert_comment_to_answer(request, id):
439     user = request.user
440     comment = get_object_or_404(Comment, id=id)
441     parent = comment.parent
442
443     if not parent.question:
444         question = parent
445     else:
446         question = parent.question
447     
448     if not user.is_authenticated():
449         raise AnonymousNotAllowedException(_("convert comments to answers"))
450
451     if not user.can_convert_comment_to_answer(comment):
452         raise NotEnoughRepPointsException(_("convert comments to answers"))
453     
454     CommentToAnswerAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(question=question))
455
456     return RefreshPageCommand()
457
458 @decorate.withfn(command)
459 def subscribe(request, id, user=None):
460     if user:
461         try:
462             user = User.objects.get(id=user)
463         except User.DoesNotExist:
464             raise Http404()
465
466         if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
467             raise CommandException(_("You do not have the correct credentials to preform this action."))
468     else:
469         user = request.user
470
471     question = get_object_or_404(Question, id=id)
472
473     try:
474         subscription = QuestionSubscription.objects.get(question=question, user=user)
475         subscription.delete()
476         subscribed = False
477     except:
478         subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
479         subscription.save()
480         subscribed = True
481
482     return {
483         'commands': {
484             'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
485             'set_subscription_status': ['']
486         }
487     }
488
489 #internally grouped views - used by the tagging system
490 @ajax_login_required
491 def mark_tag(request, tag=None, **kwargs):#tagging system
492     action = kwargs['action']
493     ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
494     if action == 'remove':
495         logging.debug('deleting tag %s' % tag)
496         ts.delete()
497     else:
498         reason = kwargs['reason']
499         if len(ts) == 0:
500             try:
501                 t = Tag.objects.get(name=tag)
502                 mt = MarkedTag(user=request.user, reason=reason, tag=t)
503                 mt.save()
504             except:
505                 pass
506         else:
507             ts.update(reason=reason)
508     return HttpResponse(simplejson.dumps(''), mimetype="application/json")
509
510 def matching_tags(request):
511     if len(request.GET['q']) == 0:
512         raise CommandException(_("Invalid request"))
513
514     possible_tags = Tag.active.filter(name__icontains = request.GET['q'])
515     tag_output = ''
516     for tag in possible_tags:
517         tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
518
519     return HttpResponse(tag_output, mimetype="text/plain")
520
521 def matching_users(request):
522     if len(request.GET['q']) == 0:
523         raise CommandException(_("Invalid request"))
524
525     possible_users = User.objects.filter(username__icontains = request.GET['q'])
526     output = ''
527
528     for user in possible_users:
529         output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
530
531     return HttpResponse(output, mimetype="text/plain")
532
533 def related_questions(request):
534     if request.POST and request.POST.get('title', None):
535         can_rank, questions = Question.objects.search(request.POST['title'])
536         return HttpResponse(simplejson.dumps(
537                 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
538                  for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")
539     else:
540         raise Http404()
541
542 @decorate.withfn(command)
543 def answer_permanent_link(request, id):
544     # Getting the current answer object
545     answer = get_object_or_404(Answer, id=id)
546
547     # Getting the current object URL -- the Application URL + the object relative URL
548     url = '%s%s' % (settings.APP_BASE_URL, answer.get_absolute_url())
549
550     if not request.POST:
551         # Display the template
552         return render_to_response('node/permanent_link.html', { 'url' : url, })
553
554     return {
555         'commands' : {
556             'copy_url' : [request.POST['permanent_link_url'],],
557         },
558         'message' : _("The permanent URL to the answer has been copied to your clipboard."),
559     }
560
561 @decorate.withfn(command)
562 def award_points(request, user_id, answer_id):
563     user = request.user
564     awarded_user = get_object_or_404(User, id=user_id)
565     answer = get_object_or_404(Answer, id=answer_id)
566
567     # Users shouldn't be able to award themselves
568     if awarded_user.id == user.id:
569         raise CannotDoOnOwnException(_("award"))
570
571     # Anonymous users cannot award  points, they just don't have such
572     if not user.is_authenticated():
573         raise AnonymousNotAllowedException(_('award'))
574
575     if not request.POST:
576         return render_to_response("node/award_points.html", { 'user' : user, 'awarded_user' : awarded_user, })
577     else:
578         points = int(request.POST['points'])
579
580         # We should check if the user has enough reputation points, otherwise we raise an exception.
581         if user.reputation < points:
582             raise NotEnoughRepPointsException(_("award"))
583
584         extra = dict(message=request.POST.get('message', ''), awarding_user=request.user.id, value=points)
585
586         # We take points from the awarding user
587         AwardPointsAction(user=request.user, node=answer, extra=extra).save(data=dict(value=points, affected=awarded_user))
588
589         return { 'message' : _("You have awarded %(awarded_user)s with %(points)d points") % {'awarded_user' : awarded_user, 'points' : points} }