1 # -*- coding: utf-8 -*-
 
   7 from urllib import urlencode
 
   9 from django.core.exceptions import ObjectDoesNotExist
 
  10 from django.core.urlresolvers import reverse
 
  11 from django.utils.encoding import smart_unicode
 
  12 from django.utils.translation import ungettext, ugettext as _
 
  13 from django.http import (HttpResponse, HttpResponseRedirect, Http404,
 
  14                          HttpResponseBadRequest)
 
  15 from django.shortcuts import get_object_or_404, render_to_response
 
  17 from django.contrib import messages
 
  19 from forum.models import *
 
  20 from forum.utils.decorators import ajax_login_required
 
  21 from forum.actions import *
 
  22 from forum.modules import decorate
 
  23 from forum import settings
 
  25 from decorators import command, CommandException, RefreshPageCommand
 
  27 class NotEnoughRepPointsException(CommandException):
 
  28     def __init__(self, action, user_reputation=None, reputation_required=None, node=None):
 
  29         if reputation_required is not None and user_reputation is not None:
 
  31                 """Sorry, but you don't have enough reputation points to %(action)s.<br />
 
  32                 The minimum reputation required is %(reputation_required)d (yours is %(user_reputation)d).
 
  33                 Please check the <a href='%(faq_url)s'>FAQ</a>"""
 
  36                 'faq_url': reverse('faq'),
 
  37                 'reputation_required' : reputation_required,
 
  38                 'user_reputation' : user_reputation,
 
  42                 """Sorry, but you don't have enough reputation points to %(action)s.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
 
  43             ) % {'action': action, 'faq_url': reverse('faq')}
 
  44         super(NotEnoughRepPointsException, self).__init__(message)
 
  46 class CannotDoOnOwnException(CommandException):
 
  47     def __init__(self, action):
 
  48         super(CannotDoOnOwnException, self).__init__(
 
  50                         """Sorry but you cannot %(action)s your own post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
 
  51                         ) % {'action': action, 'faq_url': reverse('faq')}
 
  54 class AnonymousNotAllowedException(CommandException):
 
  55     def __init__(self, action):
 
  56         super(AnonymousNotAllowedException, self).__init__(
 
  58                         """Sorry but anonymous users cannot %(action)s.<br />Please login or create an account <a href='%(signin_url)s'>here</a>."""
 
  59                         ) % {'action': action, 'signin_url': reverse('auth_signin')}
 
  62 class NotEnoughLeftException(CommandException):
 
  63     def __init__(self, action, limit):
 
  64         super(NotEnoughLeftException, self).__init__(
 
  66                         """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>"""
 
  67                         ) % {'action': action, 'limit': limit, 'faq_url': reverse('faq')}
 
  70 class CannotDoubleActionException(CommandException):
 
  71     def __init__(self, action):
 
  72         super(CannotDoubleActionException, self).__init__(
 
  74                         """Sorry, but you cannot %(action)s twice the same post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
 
  75                         ) % {'action': action, 'faq_url': reverse('faq')}
 
  79 @decorate.withfn(command)
 
  80 def vote_post(request, id, vote_type):
 
  81     if not request.method == 'POST':
 
  82         raise CommandException(_("Invalid request"))
 
  85     post = get_object_or_404(Node, id=id).leaf
 
  88     if not user.is_authenticated():
 
  89         raise AnonymousNotAllowedException(_('vote'))
 
  91     if user == post.author:
 
  92         raise CannotDoOnOwnException(_('vote'))
 
  94     if not (vote_type == 'up' and user.can_vote_up() or user.can_vote_down()):
 
  95         reputation_required = int(settings.REP_TO_VOTE_UP) if vote_type == 'up' else int(settings.REP_TO_VOTE_DOWN)
 
  96         action_type = vote_type == 'up' and _('upvote') or _('downvote')
 
  97         raise NotEnoughRepPointsException(action_type, user_reputation=user.reputation, reputation_required=reputation_required, node=post)
 
  99     user_vote_count_today = user.get_vote_count_today()
 
 100     user_can_vote_count_today = user.can_vote_count_today()
 
 102     if user_vote_count_today >= user.can_vote_count_today():
 
 103         raise NotEnoughLeftException(_('votes'), str(settings.MAX_VOTES_PER_DAY))
 
 105     new_vote_cls = (vote_type == 'up') and VoteUpAction or VoteDownAction
 
 108     old_vote = VoteAction.get_action_for(node=post, user=user)
 
 111         if old_vote.action_date < datetime.datetime.now() - datetime.timedelta(days=int(settings.DENY_UNVOTE_DAYS)):
 
 112             raise CommandException(
 
 113                     _("Sorry but you cannot cancel a vote after %(ndays)d %(tdays)s from the original vote") %
 
 114                     {'ndays': int(settings.DENY_UNVOTE_DAYS),
 
 115                      'tdays': ungettext('day', 'days', int(settings.DENY_UNVOTE_DAYS))}
 
 118         old_vote.cancel(ip=request.META['REMOTE_ADDR'])
 
 119         score_inc = (old_vote.__class__ == VoteDownAction) and 1 or -1
 
 122         new_vote_cls(user=user, node=post, ip=request.META['REMOTE_ADDR']).save()
 
 123         score_inc = (new_vote_cls == VoteUpAction) and 1 or -1
 
 127     'update_post_score': [id, score_inc],
 
 128     'update_user_post_vote': [id, vote_type]
 
 132     votes_left = (user_can_vote_count_today - user_vote_count_today) + (vote_type == 'none' and -1 or 1)
 
 134     if int(settings.START_WARN_VOTES_LEFT) >= votes_left:
 
 135         response['message'] = _("You have %(nvotes)s %(tvotes)s left today.") % \
 
 136                     {'nvotes': votes_left, 'tvotes': ungettext('vote', 'votes', votes_left)}
 
 140 @decorate.withfn(command)
 
 141 def flag_post(request, id):
 
 143         return render_to_response('node/report.html', {'types': settings.FLAG_TYPES})
 
 145     post = get_object_or_404(Node, id=id)
 
 148     if not user.is_authenticated():
 
 149         raise AnonymousNotAllowedException(_('flag posts'))
 
 151     if user == post.author:
 
 152         raise CannotDoOnOwnException(_('flag'))
 
 154     if not (user.can_flag_offensive(post)):
 
 155         raise NotEnoughRepPointsException(_('flag posts'))
 
 157     user_flag_count_today = user.get_flagged_items_count_today()
 
 159     if user_flag_count_today >= int(settings.MAX_FLAGS_PER_DAY):
 
 160         raise NotEnoughLeftException(_('flags'), str(settings.MAX_FLAGS_PER_DAY))
 
 163         current = FlagAction.objects.get(canceled=False, user=user, node=post)
 
 164         raise CommandException(
 
 165                 _("You already flagged this post with the following reason: %(reason)s") % {'reason': current.extra})
 
 166     except ObjectDoesNotExist:
 
 167         reason = request.POST.get('prompt', '').strip()
 
 170             raise CommandException(_("Reason is empty"))
 
 172         FlagAction(user=user, node=post, extra=reason, ip=request.META['REMOTE_ADDR']).save()
 
 174     return {'message': _("Thank you for your report. A moderator will review your submission shortly.")}
 
 176 @decorate.withfn(command)
 
 177 def like_comment(request, id):
 
 178     comment = get_object_or_404(Comment, id=id)
 
 181     if not user.is_authenticated():
 
 182         raise AnonymousNotAllowedException(_('like comments'))
 
 184     if user == comment.user:
 
 185         raise CannotDoOnOwnException(_('like'))
 
 187     if not user.can_like_comment(comment):
 
 188         raise NotEnoughRepPointsException( _('like comments'), node=comment)
 
 190     like = VoteAction.get_action_for(node=comment, user=user)
 
 193         like.cancel(ip=request.META['REMOTE_ADDR'])
 
 196         VoteUpCommentAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
 
 201     'update_post_score': [comment.id, likes and 1 or -1],
 
 202     'update_user_post_vote': [comment.id, likes and 'up' or 'none']
 
 206 @decorate.withfn(command)
 
 207 def delete_comment(request, id):
 
 208     comment = get_object_or_404(Comment, id=id)
 
 211     if not user.is_authenticated():
 
 212         raise AnonymousNotAllowedException(_('delete comments'))
 
 214     if not user.can_delete_comment(comment):
 
 215         raise NotEnoughRepPointsException( _('delete comments'))
 
 217     if not comment.nis.deleted:
 
 218         DeleteAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
 
 222     'remove_comment': [comment.id],
 
 226 @decorate.withfn(command)
 
 227 def mark_favorite(request, id):
 
 228     node = get_object_or_404(Node, id=id)
 
 230     if not request.user.is_authenticated():
 
 231         raise AnonymousNotAllowedException(_('mark a question as favorite'))
 
 234         favorite = FavoriteAction.objects.get(canceled=False, node=node, user=request.user)
 
 235         favorite.cancel(ip=request.META['REMOTE_ADDR'])
 
 237     except ObjectDoesNotExist:
 
 238         FavoriteAction(node=node, user=request.user, ip=request.META['REMOTE_ADDR']).save()
 
 243     'update_favorite_count': [added and 1 or -1],
 
 244     'update_favorite_mark': [added and 'on' or 'off']
 
 248 @decorate.withfn(command)
 
 249 def comment(request, id):
 
 250     post = get_object_or_404(Node, id=id)
 
 253     if not user.is_authenticated():
 
 254         raise AnonymousNotAllowedException(_('comment'))
 
 256     if not request.method == 'POST':
 
 257         raise CommandException(_("Invalid request"))
 
 259     comment_text = request.POST.get('comment', '').strip()
 
 261     if not len(comment_text):
 
 262         raise CommandException(_("Comment is empty"))
 
 264     if len(comment_text) < settings.FORM_MIN_COMMENT_BODY:
 
 265         raise CommandException(_("At least %d characters required on comment body.") % settings.FORM_MIN_COMMENT_BODY)
 
 267     if len(comment_text) > settings.FORM_MAX_COMMENT_BODY:
 
 268         raise CommandException(_("No more than %d characters on comment body.") % settings.FORM_MAX_COMMENT_BODY)
 
 270     if 'id' in request.POST:
 
 271         comment = get_object_or_404(Comment, id=request.POST['id'])
 
 273         if not user.can_edit_comment(comment):
 
 274             raise NotEnoughRepPointsException( _('edit comments'))
 
 276         comment = ReviseAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(
 
 277                 data=dict(text=comment_text)).node
 
 279         if not user.can_comment(post):
 
 280             raise NotEnoughRepPointsException( _('comment'))
 
 282         comment = CommentAction(user=user, ip=request.META['REMOTE_ADDR']).save(
 
 283                 data=dict(text=comment_text, parent=post)).node
 
 285     if comment.active_revision.revision == 1:
 
 289                 id, comment.id, comment.comment, user.decorated_name, user.get_profile_url(),
 
 290                 reverse('delete_comment', kwargs={'id': comment.id}),
 
 291                 reverse('node_markdown', kwargs={'id': comment.id}),
 
 292                 reverse('convert_comment', kwargs={'id': comment.id}),
 
 293                 user.can_convert_comment_to_answer(comment),
 
 294                 bool(settings.SHOW_LATEST_COMMENTS_FIRST)
 
 301         'update_comment': [comment.id, comment.comment]
 
 305 @decorate.withfn(command)
 
 306 def node_markdown(request, id):
 
 309     if not user.is_authenticated():
 
 310         raise AnonymousNotAllowedException(_('accept answers'))
 
 312     node = get_object_or_404(Node, id=id)
 
 313     return HttpResponse(node.active_revision.body, content_type="text/plain")
 
 316 @decorate.withfn(command)
 
 317 def accept_answer(request, id):
 
 318     if settings.DISABLE_ACCEPTING_FEATURE:
 
 323     if not user.is_authenticated():
 
 324         raise AnonymousNotAllowedException(_('accept answers'))
 
 326     answer = get_object_or_404(Answer, id=id)
 
 327     question = answer.question
 
 329     if not user.can_accept_answer(answer):
 
 330         raise CommandException(_("Sorry but you cannot accept the answer"))
 
 334     if answer.nis.accepted:
 
 335         answer.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR'])
 
 336         commands['unmark_accepted'] = [answer.id]
 
 338         if settings.MAXIMUM_ACCEPTED_ANSWERS and (question.accepted_count >= settings.MAXIMUM_ACCEPTED_ANSWERS):
 
 339             raise CommandException(ungettext("This question already has an accepted answer.",
 
 340                 "Sorry but this question has reached the limit of accepted answers.", int(settings.MAXIMUM_ACCEPTED_ANSWERS)))
 
 342         if settings.MAXIMUM_ACCEPTED_PER_USER and question.accepted_count:
 
 343             accepted_from_author = question.accepted_answers.filter(author=answer.author).count()
 
 345             if accepted_from_author >= settings.MAXIMUM_ACCEPTED_PER_USER:
 
 346                 raise CommandException(ungettext("The author of this answer already has an accepted answer in this question.",
 
 347                 "Sorry but the author of this answer has reached the limit of accepted answers per question.", int(settings.MAXIMUM_ACCEPTED_PER_USER)))             
 
 350         AcceptAnswerAction(node=answer, user=user, ip=request.META['REMOTE_ADDR']).save()
 
 352         # If the request is not an AJAX redirect to the answer URL rather than to the home page
 
 353         if not request.is_ajax():
 
 355               Congratulations! You've accepted an answer.
 
 358             # Notify the user with a message that an answer has been accepted
 
 359             messages.info(request, msg)
 
 361             # Redirect URL should include additional get parameters that might have been attached
 
 362             redirect_url = answer.parent.get_absolute_url() + "?accepted_answer=true&%s" % smart_unicode(urlencode(request.GET))
 
 364             return HttpResponseRedirect(redirect_url)
 
 366         commands['mark_accepted'] = [answer.id]
 
 368     return {'commands': commands}
 
 370 @decorate.withfn(command)
 
 371 def delete_post(request, id):
 
 372     post = get_object_or_404(Node, id=id)
 
 375     if not user.is_authenticated():
 
 376         raise AnonymousNotAllowedException(_('delete posts'))
 
 378     if not (user.can_delete_post(post)):
 
 379         raise NotEnoughRepPointsException(_('delete posts'))
 
 381     ret = {'commands': {}}
 
 384         post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
 
 385         ret['commands']['unmark_deleted'] = [post.node_type, id]
 
 387         DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
 
 389         ret['commands']['mark_deleted'] = [post.node_type, id]
 
 393 @decorate.withfn(command)
 
 394 def close(request, id, close):
 
 395     if close and not request.POST:
 
 396         return render_to_response('node/report.html', {'types': settings.CLOSE_TYPES})
 
 398     question = get_object_or_404(Question, id=id)
 
 401     if not user.is_authenticated():
 
 402         raise AnonymousNotAllowedException(_('close questions'))
 
 404     if question.nis.closed:
 
 405         if not user.can_reopen_question(question):
 
 406             raise NotEnoughRepPointsException(_('reopen questions'))
 
 408         question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
 
 410         if not request.user.can_close_question(question):
 
 411             raise NotEnoughRepPointsException(_('close questions'))
 
 413         reason = request.POST.get('prompt', '').strip()
 
 416             raise CommandException(_("Reason is empty"))
 
 418         CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
 
 420     return RefreshPageCommand()
 
 422 @decorate.withfn(command)
 
 423 def wikify(request, id):
 
 424     node = get_object_or_404(Node, id=id)
 
 427     if not user.is_authenticated():
 
 428         raise AnonymousNotAllowedException(_('mark posts as community wiki'))
 
 431         if not user.can_cancel_wiki(node):
 
 432             raise NotEnoughRepPointsException(_('cancel a community wiki post'))
 
 434         if node.nstate.wiki.action_type == "wikify":
 
 435             node.nstate.wiki.cancel()
 
 437             node.nstate.wiki = None
 
 439         if not user.can_wikify(node):
 
 440             raise NotEnoughRepPointsException(_('mark posts as community wiki'))
 
 442         WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
 
 444     return RefreshPageCommand()
 
 446 @decorate.withfn(command)
 
 447 def convert_to_comment(request, id):
 
 449     answer = get_object_or_404(Answer, id=id)
 
 450     question = answer.question
 
 452     # Check whether the user has the required permissions
 
 453     if not user.is_authenticated():
 
 454         raise AnonymousNotAllowedException(_("convert answers to comments"))
 
 456     if not user.can_convert_to_comment(answer):
 
 457         raise NotEnoughRepPointsException(_("convert answers to comments"))
 
 460         description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': smart_unicode(a.author.username),
 
 461                                                                             'snippet': a.summary[:10]}
 
 462         nodes = [(question.id, _("Question"))]
 
 463         [nodes.append((a.id, description(a))) for a in
 
 464          question.answers.filter_state(deleted=False).exclude(id=answer.id)]
 
 466         return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
 
 469         new_parent = Node.objects.get(id=request.POST.get('under', None))
 
 471         raise CommandException(_("That is an invalid post to put the comment under"))
 
 473     if not (new_parent == question or (new_parent.node_type == 'answer' and new_parent.parent == question)):
 
 474         raise CommandException(_("That is an invalid post to put the comment under"))
 
 476     AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
 
 478     return RefreshPageCommand()
 
 480 @decorate.withfn(command)
 
 481 def convert_comment_to_answer(request, id):
 
 483     comment = get_object_or_404(Comment, id=id)
 
 484     parent = comment.parent
 
 486     if not parent.question:
 
 489         question = parent.question
 
 491     if not user.is_authenticated():
 
 492         raise AnonymousNotAllowedException(_("convert comments to answers"))
 
 494     if not user.can_convert_comment_to_answer(comment):
 
 495         raise NotEnoughRepPointsException(_("convert comments to answers"))
 
 497     CommentToAnswerAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(question=question))
 
 499     return RefreshPageCommand()
 
 501 @decorate.withfn(command)
 
 502 def subscribe(request, id, user=None):
 
 505             user = User.objects.get(id=user)
 
 506         except User.DoesNotExist:
 
 509         if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
 
 510             raise CommandException(_("You do not have the correct credentials to preform this action."))
 
 514     question = get_object_or_404(Question, id=id)
 
 517         subscription = QuestionSubscription.objects.get(question=question, user=user)
 
 518         subscription.delete()
 
 521         subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
 
 527             'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
 
 528             'set_subscription_status': ['']
 
 532 #internally grouped views - used by the tagging system
 
 534 def mark_tag(request, tag=None, **kwargs):#tagging system
 
 535     action = kwargs['action']
 
 536     ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
 
 537     if action == 'remove':
 
 538         logging.debug('deleting tag %s' % tag)
 
 541         reason = kwargs['reason']
 
 544                 t = Tag.objects.get(name=tag)
 
 545                 mt = MarkedTag(user=request.user, reason=reason, tag=t)
 
 550             ts.update(reason=reason)
 
 551     return HttpResponse(json.dumps(''), content_type="application/json")
 
 553 def matching_tags(request):
 
 554     q = request.GET.get('q')
 
 556         return HttpResponseBadRequest(_("Invalid request"))
 
 558     possible_tags = Tag.active.filter(name__icontains=q)
 
 560     for tag in possible_tags:
 
 561         tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
 
 563     return HttpResponse(tag_output, content_type="text/plain")
 
 565 def matching_users(request):
 
 566     if len(request.GET['q']) == 0:
 
 567         raise CommandException(_("Invalid request"))
 
 569     possible_users = User.objects.filter(username__icontains = request.GET['q'])
 
 572     for user in possible_users:
 
 573         output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
 
 575     return HttpResponse(output, content_type="text/plain")
 
 577 def related_questions(request):
 
 578     if request.POST and request.POST.get('title', None):
 
 579         can_rank, questions = Question.objects.search(request.POST['title'])
 
 581         if can_rank and isinstance(can_rank, basestring):
 
 582             questions = questions.order_by(can_rank)
 
 584         return HttpResponse(json.dumps(
 
 585                 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
 
 586                  for q in questions.filter_state(deleted=False)[0:10]]), content_type="application/json")
 
 590 @decorate.withfn(command)
 
 591 def answer_permanent_link(request, id):
 
 592     # Getting the current answer object
 
 593     answer = get_object_or_404(Answer, id=id)
 
 595     # Getting the current object URL -- the Application URL + the object relative URL
 
 596     url = '%s%s' % (settings.APP_BASE_URL, answer.get_absolute_url())
 
 599         # Display the template
 
 600         return render_to_response('node/permanent_link.html', { 'url' : url, })
 
 604             'copy_url' : [request.POST['permanent_link_url'],],
 
 606         'message' : _("The permanent URL to the answer has been copied to your clipboard."),
 
 609 @decorate.withfn(command)
 
 610 def award_points(request, user_id, answer_id):
 
 612     awarded_user = get_object_or_404(User, id=user_id)
 
 613     answer = get_object_or_404(Answer, id=answer_id)
 
 615     # Users shouldn't be able to award themselves
 
 616     if awarded_user.id == user.id:
 
 617         raise CannotDoOnOwnException(_("award"))
 
 619     # Anonymous users cannot award  points, they just don't have such
 
 620     if not user.is_authenticated():
 
 621         raise AnonymousNotAllowedException(_('award'))
 
 624         return render_to_response("node/award_points.html", {
 
 626             'awarded_user' : awarded_user,
 
 627             'reputation_to_comment' : str(settings.REP_TO_COMMENT)
 
 630         points = int(request.POST['points'])
 
 632         # We should check if the user has enough reputation points, otherwise we raise an exception.
 
 634             raise CommandException(_("The number of points to award needs to be a positive value."))
 
 636         if user.reputation < points:
 
 637             raise NotEnoughRepPointsException(_("award"))
 
 639         extra = dict(message=request.POST.get('message', ''), awarding_user=request.user.id, value=points)
 
 641         # We take points from the awarding user
 
 642         AwardPointsAction(user=request.user, node=answer, extra=extra).save(data=dict(value=points, affected=awarded_user))
 
 644         return { 'message' : _("You have awarded %(awarded_user)s with %(points)d points") % {'awarded_user' : awarded_user, 'points' : points} }