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
 
  20 class NotEnoughRepPointsException(CommandException):
 
  21     def __init__(self, action):
 
  22         super(NotEnoughRepPointsException, self).__init__(
 
  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')}
 
  28 class CannotDoOnOwnException(CommandException):
 
  29     def __init__(self, action):
 
  30         super(CannotDoOnOwnException, self).__init__(
 
  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')}
 
  36 class AnonymousNotAllowedException(CommandException):
 
  37     def __init__(self, action):
 
  38         super(AnonymousNotAllowedException, self).__init__(
 
  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')}
 
  44 class NotEnoughLeftException(CommandException):
 
  45     def __init__(self, action, limit):
 
  46         super(NotEnoughLeftException, self).__init__(
 
  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')}
 
  52 class CannotDoubleActionException(CommandException):
 
  53     def __init__(self, action):
 
  54         super(CannotDoubleActionException, self).__init__(
 
  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')}
 
  61 @decorate.withfn(command)
 
  62 def vote_post(request, id, vote_type):
 
  63     post = get_object_or_404(Node, id=id).leaf
 
  66     if not user.is_authenticated():
 
  67         raise AnonymousNotAllowedException(_('vote'))
 
  69     if user == post.author:
 
  70         raise CannotDoOnOwnException(_('vote'))
 
  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'))
 
  75     user_vote_count_today = user.get_vote_count_today()
 
  77     if user_vote_count_today >= user.can_vote_count_today():
 
  78         raise NotEnoughLeftException(_('votes'), str(settings.MAX_VOTES_PER_DAY))
 
  80     new_vote_cls = (vote_type == 'up') and VoteUpAction or VoteDownAction
 
  83     old_vote = VoteAction.get_action_for(node=post, user=user)
 
  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))}
 
  93         old_vote.cancel(ip=request.META['REMOTE_ADDR'])
 
  94         score_inc = (old_vote.__class__ == VoteDownAction) and 1 or -1
 
  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
 
 102     'update_post_score': [id, score_inc],
 
 103     'update_user_post_vote': [id, vote_type]
 
 107     votes_left = (int(settings.MAX_VOTES_PER_DAY) - user_vote_count_today) + (vote_type == 'none' and -1 or 1)
 
 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)}
 
 115 @decorate.withfn(command)
 
 116 def flag_post(request, id):
 
 118         return render_to_response('node/report.html', {'types': settings.FLAG_TYPES})
 
 120     post = get_object_or_404(Node, id=id)
 
 123     if not user.is_authenticated():
 
 124         raise AnonymousNotAllowedException(_('flag posts'))
 
 126     if user == post.author:
 
 127         raise CannotDoOnOwnException(_('flag'))
 
 129     if not (user.can_flag_offensive(post)):
 
 130         raise NotEnoughRepPointsException(_('flag posts'))
 
 132     user_flag_count_today = user.get_flagged_items_count_today()
 
 134     if user_flag_count_today >= int(settings.MAX_FLAGS_PER_DAY):
 
 135         raise NotEnoughLeftException(_('flags'), str(settings.MAX_FLAGS_PER_DAY))
 
 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()
 
 145             raise CommandException(_("Reason is empty"))
 
 147         FlagAction(user=user, node=post, extra=reason, ip=request.META['REMOTE_ADDR']).save()
 
 149     return {'message': _("Thank you for your report. A moderator will review your submission shortly.")}
 
 151 @decorate.withfn(command)
 
 152 def like_comment(request, id):
 
 153     comment = get_object_or_404(Comment, id=id)
 
 156     if not user.is_authenticated():
 
 157         raise AnonymousNotAllowedException(_('like comments'))
 
 159     if user == comment.user:
 
 160         raise CannotDoOnOwnException(_('like'))
 
 162     if not user.can_like_comment(comment):
 
 163         raise NotEnoughRepPointsException( _('like comments'))
 
 165     like = VoteAction.get_action_for(node=comment, user=user)
 
 168         like.cancel(ip=request.META['REMOTE_ADDR'])
 
 171         VoteUpCommentAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
 
 176     'update_post_score': [comment.id, likes and 1 or -1],
 
 177     'update_user_post_vote': [comment.id, likes and 'up' or 'none']
 
 181 @decorate.withfn(command)
 
 182 def delete_comment(request, id):
 
 183     comment = get_object_or_404(Comment, id=id)
 
 186     if not user.is_authenticated():
 
 187         raise AnonymousNotAllowedException(_('delete comments'))
 
 189     if not user.can_delete_comment(comment):
 
 190         raise NotEnoughRepPointsException( _('delete comments'))
 
 192     if not comment.nis.deleted:
 
 193         DeleteAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
 
 197     'remove_comment': [comment.id],
 
 201 @decorate.withfn(command)
 
 202 def mark_favorite(request, id):
 
 203     question = get_object_or_404(Question, id=id)
 
 205     if not request.user.is_authenticated():
 
 206         raise AnonymousNotAllowedException(_('mark a question as favorite'))
 
 209         favorite = FavoriteAction.objects.get(canceled=False, node=question, user=request.user)
 
 210         favorite.cancel(ip=request.META['REMOTE_ADDR'])
 
 212     except ObjectDoesNotExist:
 
 213         FavoriteAction(node=question, user=request.user, ip=request.META['REMOTE_ADDR']).save()
 
 218     'update_favorite_count': [added and 1 or -1],
 
 219     'update_favorite_mark': [added and 'on' or 'off']
 
 223 @decorate.withfn(command)
 
 224 def comment(request, id):
 
 225     post = get_object_or_404(Node, id=id)
 
 228     if not user.is_authenticated():
 
 229         raise AnonymousNotAllowedException(_('comment'))
 
 231     if not request.method == 'POST':
 
 232         raise CommandException(_("Invalid request"))
 
 234     comment_text = request.POST.get('comment', '').strip()
 
 236     if not len(comment_text):
 
 237         raise CommandException(_("Comment is empty"))
 
 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)
 
 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)
 
 245     if 'id' in request.POST:
 
 246         comment = get_object_or_404(Comment, id=request.POST['id'])
 
 248         if not user.can_edit_comment(comment):
 
 249             raise NotEnoughRepPointsException( _('edit comments'))
 
 251         comment = ReviseAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(
 
 252                 data=dict(text=comment_text)).node
 
 254         if not user.can_comment(post):
 
 255             raise NotEnoughRepPointsException( _('comment'))
 
 257         comment = CommentAction(user=user, ip=request.META['REMOTE_ADDR']).save(
 
 258                 data=dict(text=comment_text, parent=post)).node
 
 260     if comment.active_revision.revision == 1:
 
 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}),            
 
 274         'update_comment': [comment.id, comment.comment]
 
 278 @decorate.withfn(command)
 
 279 def node_markdown(request, id):
 
 282     if not user.is_authenticated():
 
 283         raise AnonymousNotAllowedException(_('accept answers'))
 
 285     node = get_object_or_404(Node, id=id)
 
 286     return HttpResponse(node.active_revision.body, mimetype="text/plain")
 
 289 @decorate.withfn(command)
 
 290 def accept_answer(request, id):
 
 291     if settings.DISABLE_ACCEPTING_FEATURE:
 
 296     if not user.is_authenticated():
 
 297         raise AnonymousNotAllowedException(_('accept answers'))
 
 299     answer = get_object_or_404(Answer, id=id)
 
 300     question = answer.question
 
 302     if not user.can_accept_answer(answer):
 
 303         raise CommandException(_("Sorry but you cannot accept the answer"))
 
 307     if answer.nis.accepted:
 
 308         answer.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR'])
 
 309         commands['unmark_accepted'] = [answer.id]
 
 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)))
 
 315         if settings.MAXIMUM_ACCEPTED_PER_USER and question.accepted_count:
 
 316             accepted_from_author = question.accepted_answers.filter(author=answer.author).count()
 
 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)))             
 
 323         AcceptAnswerAction(node=answer, user=user, ip=request.META['REMOTE_ADDR']).save()
 
 324         commands['mark_accepted'] = [answer.id]
 
 326     return {'commands': commands}
 
 328 @decorate.withfn(command)
 
 329 def delete_post(request, id):
 
 330     post = get_object_or_404(Node, id=id)
 
 333     if not user.is_authenticated():
 
 334         raise AnonymousNotAllowedException(_('delete posts'))
 
 336     if not (user.can_delete_post(post)):
 
 337         raise NotEnoughRepPointsException(_('delete posts'))
 
 339     ret = {'commands': {}}
 
 342         post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
 
 343         ret['commands']['unmark_deleted'] = [post.node_type, id]
 
 345         DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
 
 347         ret['commands']['mark_deleted'] = [post.node_type, id]
 
 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})
 
 356     question = get_object_or_404(Question, id=id)
 
 359     if not user.is_authenticated():
 
 360         raise AnonymousNotAllowedException(_('close questions'))
 
 362     if question.nis.closed:
 
 363         if not user.can_reopen_question(question):
 
 364             raise NotEnoughRepPointsException(_('reopen questions'))
 
 366         question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
 
 368         if not request.user.can_close_question(question):
 
 369             raise NotEnoughRepPointsException(_('close questions'))
 
 371         reason = request.POST.get('prompt', '').strip()
 
 374             raise CommandException(_("Reason is empty"))
 
 376         CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
 
 378     return RefreshPageCommand()
 
 380 @decorate.withfn(command)
 
 381 def wikify(request, id):
 
 382     node = get_object_or_404(Node, id=id)
 
 385     if not user.is_authenticated():
 
 386         raise AnonymousNotAllowedException(_('mark posts as community wiki'))
 
 389         if not user.can_cancel_wiki(node):
 
 390             raise NotEnoughRepPointsException(_('cancel a community wiki post'))
 
 392         if node.nstate.wiki.action_type == "wikify":
 
 393             node.nstate.wiki.cancel()
 
 395             node.nstate.wiki = None
 
 397         if not user.can_wikify(node):
 
 398             raise NotEnoughRepPointsException(_('mark posts as community wiki'))
 
 400         WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
 
 402     return RefreshPageCommand()
 
 404 @decorate.withfn(command)
 
 405 def convert_to_comment(request, id):
 
 407     answer = get_object_or_404(Answer, id=id)
 
 408     question = answer.question
 
 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)]
 
 417         return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
 
 419     if not user.is_authenticated():
 
 420         raise AnonymousNotAllowedException(_("convert answers to comments"))
 
 422     if not user.can_convert_to_comment(answer):
 
 423         raise NotEnoughRepPointsException(_("convert answers to comments"))
 
 426         new_parent = Node.objects.get(id=request.POST.get('under', None))
 
 428         raise CommandException(_("That is an invalid post to put the comment under"))
 
 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"))
 
 433     AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
 
 435     return RefreshPageCommand()
 
 437 @decorate.withfn(command)
 
 438 def convert_comment_to_answer(request, id):
 
 440     comment = get_object_or_404(Comment, id=id)
 
 441     parent = comment.parent
 
 443     if not parent.question:
 
 446         question = parent.question
 
 448     if not user.is_authenticated():
 
 449         raise AnonymousNotAllowedException(_("convert comments to answers"))
 
 451     if not user.can_convert_comment_to_answer(comment):
 
 452         raise NotEnoughRepPointsException(_("convert comments to answers"))
 
 454     CommentToAnswerAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(question=question))
 
 456     return RefreshPageCommand()
 
 458 @decorate.withfn(command)
 
 459 def subscribe(request, id, user=None):
 
 462             user = User.objects.get(id=user)
 
 463         except User.DoesNotExist:
 
 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."))
 
 471     question = get_object_or_404(Question, id=id)
 
 474         subscription = QuestionSubscription.objects.get(question=question, user=user)
 
 475         subscription.delete()
 
 478         subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
 
 484             'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
 
 485             'set_subscription_status': ['']
 
 489 #internally grouped views - used by the tagging system
 
 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)
 
 498         reason = kwargs['reason']
 
 501                 t = Tag.objects.get(name=tag)
 
 502                 mt = MarkedTag(user=request.user, reason=reason, tag=t)
 
 507             ts.update(reason=reason)
 
 508     return HttpResponse(simplejson.dumps(''), mimetype="application/json")
 
 510 def matching_tags(request):
 
 511     if len(request.GET['q']) == 0:
 
 512         raise CommandException(_("Invalid request"))
 
 514     possible_tags = Tag.active.filter(name__icontains = request.GET['q'])
 
 516     for tag in possible_tags:
 
 517         tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
 
 519     return HttpResponse(tag_output, mimetype="text/plain")
 
 521 def matching_users(request):
 
 522     if len(request.GET['q']) == 0:
 
 523         raise CommandException(_("Invalid request"))
 
 525     possible_users = User.objects.filter(username__icontains = request.GET['q'])
 
 528     for user in possible_users:
 
 529         output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
 
 531     return HttpResponse(output, mimetype="text/plain")
 
 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")
 
 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)
 
 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())
 
 551         # Display the template
 
 552         return render_to_response('node/permanent_link.html', { 'url' : url, })
 
 556             'copy_url' : [request.POST['permanent_link_url'],],
 
 558         'message' : _("The permanent URL to the answer has been copied to your clipboard."),
 
 561 @decorate.withfn(command)
 
 562 def award_points(request, user_id, answer_id):
 
 564     awarded_user = get_object_or_404(User, id=user_id)
 
 565     answer = get_object_or_404(Answer, id=answer_id)
 
 567     # Users shouldn't be able to award themselves
 
 568     if awarded_user.id == user.id:
 
 569         raise CannotDoOnOwnException(_("award"))
 
 571     # Anonymous users cannot award  points, they just don't have such
 
 572     if not user.is_authenticated():
 
 573         raise AnonymousNotAllowedException(_('award'))
 
 576         return render_to_response("node/award_points.html", { 'user' : user, 'awarded_user' : awarded_user, })
 
 578         points = int(request.POST['points'])
 
 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"))
 
 584         extra = dict(message=request.POST.get('message', ''), awarding_user=request.user.id, value=points)
 
 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))
 
 589         return { 'message' : _("You have awarded %(awarded_user)s with %(points)d points") % {'awarded_user' : awarded_user, 'points' : points} }