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 forum.models import *
 
  10 from forum.models.node import NodeMetaClass
 
  11 from forum.actions import *
 
  12 from django.core.urlresolvers import reverse
 
  13 from forum.utils.decorators import ajax_method, ajax_login_required
 
  14 from decorators import command, CommandException, RefreshPageCommand
 
  15 from forum.modules import decorate
 
  16 from forum import settings
 
  19 class NotEnoughRepPointsException(CommandException):
 
  20     def __init__(self, action):
 
  21         super(NotEnoughRepPointsException, self).__init__(
 
  23                         """Sorry, but you don't have enough reputation points to %(action)s.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
 
  24                         ) % {'action': action, 'faq_url': reverse('faq')}
 
  27 class CannotDoOnOwnException(CommandException):
 
  28     def __init__(self, action):
 
  29         super(CannotDoOnOwnException, self).__init__(
 
  31                         """Sorry but you cannot %(action)s your own post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
 
  32                         ) % {'action': action, 'faq_url': reverse('faq')}
 
  35 class AnonymousNotAllowedException(CommandException):
 
  36     def __init__(self, action):
 
  37         super(AnonymousNotAllowedException, self).__init__(
 
  39                         """Sorry but anonymous users cannot %(action)s.<br />Please login or create an account <a href='%(signin_url)s'>here</a>."""
 
  40                         ) % {'action': action, 'signin_url': reverse('auth_signin')}
 
  43 class NotEnoughLeftException(CommandException):
 
  44     def __init__(self, action, limit):
 
  45         super(NotEnoughLeftException, self).__init__(
 
  47                         """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>"""
 
  48                         ) % {'action': action, 'limit': limit, 'faq_url': reverse('faq')}
 
  51 class CannotDoubleActionException(CommandException):
 
  52     def __init__(self, action):
 
  53         super(CannotDoubleActionException, self).__init__(
 
  55                         """Sorry, but you cannot %(action)s twice the same post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
 
  56                         ) % {'action': action, 'faq_url': reverse('faq')}
 
  60 @decorate.withfn(command)
 
  61 def vote_post(request, id, vote_type):
 
  62     post = get_object_or_404(Node, id=id).leaf
 
  65     if not user.is_authenticated():
 
  66         raise AnonymousNotAllowedException(_('vote'))
 
  68     if user == post.author:
 
  69         raise CannotDoOnOwnException(_('vote'))
 
  71     if not (vote_type == 'up' and user.can_vote_up() or user.can_vote_down()):
 
  72         raise NotEnoughRepPointsException(vote_type == 'up' and _('upvote') or _('downvote'))
 
  74     user_vote_count_today = user.get_vote_count_today()
 
  76     if user_vote_count_today >= int(settings.MAX_VOTES_PER_DAY):
 
  77         raise NotEnoughLeftException(_('votes'), str(settings.MAX_VOTES_PER_DAY))
 
  79     new_vote_cls = (vote_type == 'up') and VoteUpAction or VoteDownAction
 
  82     old_vote = VoteAction.get_action_for(node=post, user=user)
 
  85         if old_vote.action_date < datetime.datetime.now() - datetime.timedelta(days=int(settings.DENY_UNVOTE_DAYS)):
 
  86             raise CommandException(
 
  87                     _("Sorry but you cannot cancel a vote after %(ndays)d %(tdays)s from the original vote") %
 
  88                     {'ndays': int(settings.DENY_UNVOTE_DAYS),
 
  89                      'tdays': ungettext('day', 'days', int(settings.DENY_UNVOTE_DAYS))}
 
  92         old_vote.cancel(ip=request.META['REMOTE_ADDR'])
 
  93         score_inc += (old_vote.__class__ == VoteDownAction) and 1 or -1
 
  95     if old_vote.__class__ != new_vote_cls:
 
  96         new_vote_cls(user=user, node=post, ip=request.META['REMOTE_ADDR']).save()
 
  97         score_inc += (new_vote_cls == VoteUpAction) and 1 or -1
 
 103     'update_post_score': [id, score_inc],
 
 104     'update_user_post_vote': [id, vote_type]
 
 108     votes_left = (int(settings.MAX_VOTES_PER_DAY) - user_vote_count_today) + (vote_type == 'none' and -1 or 1)
 
 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)}
 
 116 @decorate.withfn(command)
 
 117 def flag_post(request, id):
 
 119         return render_to_response('node/report.html', {'types': settings.FLAG_TYPES})
 
 121     post = get_object_or_404(Node, id=id)
 
 124     if not user.is_authenticated():
 
 125         raise AnonymousNotAllowedException(_('flag posts'))
 
 127     if user == post.author:
 
 128         raise CannotDoOnOwnException(_('flag'))
 
 130     if not (user.can_flag_offensive(post)):
 
 131         raise NotEnoughRepPointsException(_('flag posts'))
 
 133     user_flag_count_today = user.get_flagged_items_count_today()
 
 135     if user_flag_count_today >= int(settings.MAX_FLAGS_PER_DAY):
 
 136         raise NotEnoughLeftException(_('flags'), str(settings.MAX_FLAGS_PER_DAY))
 
 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()
 
 146             raise CommandException(_("Reason is empty"))
 
 148         FlagAction(user=user, node=post, extra=reason, ip=request.META['REMOTE_ADDR']).save()
 
 150     return {'message': _("Thank you for your report. A moderator will review your submission shortly.")}
 
 152 @decorate.withfn(command)
 
 153 def like_comment(request, id):
 
 154     comment = get_object_or_404(Comment, id=id)
 
 157     if not user.is_authenticated():
 
 158         raise AnonymousNotAllowedException(_('like comments'))
 
 160     if user == comment.user:
 
 161         raise CannotDoOnOwnException(_('like'))
 
 163     if not user.can_like_comment(comment):
 
 164         raise NotEnoughRepPointsException( _('like comments'))
 
 166     like = VoteAction.get_action_for(node=comment, user=user)
 
 169         like.cancel(ip=request.META['REMOTE_ADDR'])
 
 172         VoteUpCommentAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
 
 177     'update_post_score': [comment.id, likes and 1 or -1],
 
 178     'update_user_post_vote': [comment.id, likes and 'up' or 'none']
 
 182 @decorate.withfn(command)
 
 183 def delete_comment(request, id):
 
 184     comment = get_object_or_404(Comment, id=id)
 
 187     if not user.is_authenticated():
 
 188         raise AnonymousNotAllowedException(_('delete comments'))
 
 190     if not user.can_delete_comment(comment):
 
 191         raise NotEnoughRepPointsException( _('delete comments'))
 
 193     if not comment.nis.deleted:
 
 194         DeleteAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
 
 198     'remove_comment': [comment.id],
 
 202 @decorate.withfn(command)
 
 203 def mark_favorite(request, id):
 
 204     question = get_object_or_404(Question, id=id)
 
 206     if not request.user.is_authenticated():
 
 207         raise AnonymousNotAllowedException(_('mark a question as favorite'))
 
 210         favorite = FavoriteAction.objects.get(canceled=False, node=question, user=request.user)
 
 211         favorite.cancel(ip=request.META['REMOTE_ADDR'])
 
 213     except ObjectDoesNotExist:
 
 214         FavoriteAction(node=question, user=request.user, ip=request.META['REMOTE_ADDR']).save()
 
 219     'update_favorite_count': [added and 1 or -1],
 
 220     'update_favorite_mark': [added and 'on' or 'off']
 
 224 @decorate.withfn(command)
 
 225 def comment(request, id):
 
 226     post = get_object_or_404(Node, id=id)
 
 229     if not user.is_authenticated():
 
 230         raise AnonymousNotAllowedException(_('comment'))
 
 232     if not request.method == 'POST':
 
 233         raise CommandException(_("Invalid request"))
 
 235     comment_text = request.POST.get('comment', '').strip()
 
 237     if not len(comment_text):
 
 238         raise CommandException(_("Comment is empty"))
 
 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)
 
 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)
 
 246     if 'id' in request.POST:
 
 247         comment = get_object_or_404(Comment, id=request.POST['id'])
 
 249         if not user.can_edit_comment(comment):
 
 250             raise NotEnoughRepPointsException( _('edit comments'))
 
 252         comment = ReviseAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(
 
 253                 data=dict(text=comment_text)).node
 
 255         if not user.can_comment(post):
 
 256             raise NotEnoughRepPointsException( _('comment'))
 
 258         comment = CommentAction(user=user, ip=request.META['REMOTE_ADDR']).save(
 
 259                 data=dict(text=comment_text, parent=post)).node
 
 261     if comment.active_revision.revision == 1:
 
 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}),            
 
 275         'update_comment': [comment.id, comment.comment]
 
 279 @decorate.withfn(command)
 
 280 def node_markdown(request, id):
 
 283     if not user.is_authenticated():
 
 284         raise AnonymousNotAllowedException(_('accept answers'))
 
 286     node = get_object_or_404(Node, id=id)
 
 287     return HttpResponse(node.body, mimetype="text/plain")
 
 290 @decorate.withfn(command)
 
 291 def accept_answer(request, id):
 
 292     if settings.DISABLE_ACCEPTING_FEATURE:
 
 297     if not user.is_authenticated():
 
 298         raise AnonymousNotAllowedException(_('accept answers'))
 
 300     answer = get_object_or_404(Answer, id=id)
 
 301     question = answer.question
 
 303     if not user.can_accept_answer(answer):
 
 304         raise CommandException(_("Sorry but you cannot accept the answer"))
 
 308     if answer.nis.accepted:
 
 309         answer.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR'])
 
 310         commands['unmark_accepted'] = [answer.id]
 
 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)))
 
 316         if settings.MAXIMUM_ACCEPTED_PER_USER and question.accepted_count:
 
 317             accepted_from_author = question.accepted_answers.filter(author=answer.author).count()
 
 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)))             
 
 324         AcceptAnswerAction(node=answer, user=user, ip=request.META['REMOTE_ADDR']).save()
 
 325         commands['mark_accepted'] = [answer.id]
 
 327     return {'commands': commands}
 
 329 @decorate.withfn(command)
 
 330 def delete_post(request, id):
 
 331     post = get_object_or_404(Node, id=id)
 
 334     if not user.is_authenticated():
 
 335         raise AnonymousNotAllowedException(_('delete posts'))
 
 337     if not (user.can_delete_post(post)):
 
 338         raise NotEnoughRepPointsException(_('delete posts'))
 
 340     ret = {'commands': {}}
 
 343         post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
 
 344         ret['commands']['unmark_deleted'] = [post.node_type, id]
 
 346         DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
 
 348         ret['commands']['mark_deleted'] = [post.node_type, id]
 
 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})
 
 357     question = get_object_or_404(Question, id=id)
 
 360     if not user.is_authenticated():
 
 361         raise AnonymousNotAllowedException(_('close questions'))
 
 363     if question.nis.closed:
 
 364         if not user.can_reopen_question(question):
 
 365             raise NotEnoughRepPointsException(_('reopen questions'))
 
 367         question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
 
 369         if not request.user.can_close_question(question):
 
 370             raise NotEnoughRepPointsException(_('close questions'))
 
 372         reason = request.POST.get('prompt', '').strip()
 
 375             raise CommandException(_("Reason is empty"))
 
 377         CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
 
 379     return RefreshPageCommand()
 
 381 @decorate.withfn(command)
 
 382 def wikify(request, id):
 
 383     node = get_object_or_404(Node, id=id)
 
 386     if not user.is_authenticated():
 
 387         raise AnonymousNotAllowedException(_('mark posts as community wiki'))
 
 390         if not user.can_cancel_wiki(node):
 
 391             raise NotEnoughRepPointsException(_('cancel a community wiki post'))
 
 393         if node.nstate.wiki.action_type == "wikify":
 
 394             node.nstate.wiki.cancel()
 
 396             node.nstate.wiki = None
 
 398         if not user.can_wikify(node):
 
 399             raise NotEnoughRepPointsException(_('mark posts as community wiki'))
 
 401         WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
 
 403     return RefreshPageCommand()
 
 405 @decorate.withfn(command)
 
 406 def convert_to_comment(request, id):
 
 408     answer = get_object_or_404(Answer, id=id)
 
 409     question = answer.question
 
 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)]
 
 418         return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
 
 420     if not user.is_authenticated():
 
 421         raise AnonymousNotAllowedException(_("convert answers to comments"))
 
 423     if not user.can_convert_to_comment(answer):
 
 424         raise NotEnoughRepPointsException(_("convert answers to comments"))
 
 427         new_parent = Node.objects.get(id=request.POST.get('under', None))
 
 429         raise CommandException(_("That is an invalid post to put the comment under"))
 
 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"))
 
 434     AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
 
 436     return RefreshPageCommand()
 
 438 @decorate.withfn(command)
 
 439 def convert_comment_to_answer(request, id):
 
 441     comment = get_object_or_404(Comment, id=id)
 
 442     parent = comment.parent
 
 444     if not parent.question:
 
 447         question = parent.question
 
 449     if not user.is_authenticated():
 
 450         raise AnonymousNotAllowedException(_("convert comments to answers"))
 
 452     if not user.can_convert_comment_to_answer(comment):
 
 453         raise NotEnoughRepPointsException(_("convert comments to answers"))
 
 455     CommentToAnswerAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(question=question))
 
 457     return RefreshPageCommand()
 
 459 @decorate.withfn(command)
 
 460 def convert_to_question(request, id):
 
 462     answer = get_object_or_404(Answer, id=id)
 
 463     question = answer.question
 
 466         description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': a.author.username,
 
 467                                                                             'snippet': a.summary[:10]}
 
 468         nodes = [(question.id, _("Question"))]
 
 469         [nodes.append((a.id, description(a))) for a in
 
 470          question.answers.filter_state(deleted=False).exclude(id=answer.id)]
 
 472         return render_to_response('node/convert_to_question.html', {'answer': answer})
 
 474     if not user.is_authenticated():
 
 475         raise AnonymousNotAllowedException(_("convert answers to questions"))
 
 477     if not user.can_convert_to_question(answer):
 
 478         raise NotEnoughRepPointsException(_("convert answers to questions"))
 
 481         title = request.POST.get('title', None)
 
 483         raise CommandException(_("You haven't specified the title of the new question"))
 
 485     AnswerToQuestionAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(title=title))
 
 487     return RefreshPageCommand()
 
 489 @decorate.withfn(command)
 
 490 def subscribe(request, id, user=None):
 
 493             user = User.objects.get(id=user)
 
 494         except User.DoesNotExist:
 
 497         if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
 
 498             raise CommandException(_("You do not have the correct credentials to preform this action."))
 
 502     question = get_object_or_404(Question, id=id)
 
 505         subscription = QuestionSubscription.objects.get(question=question, user=user)
 
 506         subscription.delete()
 
 509         subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
 
 515             'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
 
 516             'set_subscription_status': ['']
 
 520 #internally grouped views - used by the tagging system
 
 522 def mark_tag(request, tag=None, **kwargs):#tagging system
 
 523     action = kwargs['action']
 
 524     ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
 
 525     if action == 'remove':
 
 526         logging.debug('deleting tag %s' % tag)
 
 529         reason = kwargs['reason']
 
 532                 t = Tag.objects.get(name=tag)
 
 533                 mt = MarkedTag(user=request.user, reason=reason, tag=t)
 
 538             ts.update(reason=reason)
 
 539     return HttpResponse(simplejson.dumps(''), mimetype="application/json")
 
 541 def matching_tags(request):
 
 542     if len(request.GET['q']) == 0:
 
 543         raise CommandException(_("Invalid request"))
 
 545     possible_tags = Tag.active.filter(name__icontains = request.GET['q'])
 
 547     for tag in possible_tags:
 
 548         tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
 
 550     return HttpResponse(tag_output, mimetype="text/plain")
 
 552 def matching_users(request):
 
 553     if len(request.GET['q']) == 0:
 
 554         raise CommandException(_("Invalid request"))
 
 556     possible_users = User.objects.filter(username__icontains = request.GET['q'])
 
 559     for user in possible_users:
 
 560         output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
 
 562     return HttpResponse(output, mimetype="text/plain")
 
 564 def related_questions(request):
 
 565     if request.POST and request.POST.get('title', None):
 
 566         can_rank, questions = Question.objects.search(request.POST['title'])
 
 567         return HttpResponse(simplejson.dumps(
 
 568                 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
 
 569                  for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")