4 from forum import settings
5 from django.core.exceptions import ObjectDoesNotExist
6 from django.core.urlresolvers import reverse
7 from django.utils import simplejson
8 from django.utils.encoding import smart_unicode
9 from django.utils.translation import ungettext, ugettext as _
10 from django.http import HttpResponse, HttpResponseRedirect, Http404
11 from django.shortcuts import get_object_or_404, render_to_response
12 from django.template import RequestContext
14 from django.template.loader import render_to_string
15 from forum.models import *
16 from forum.models.node import NodeMetaClass
17 from forum.utils.decorators import ajax_method, ajax_login_required
18 from forum.actions import *
19 from forum.modules import decorate
20 from forum import settings
22 from decorators import command, CommandException, RefreshPageCommand
24 class NotEnoughRepPointsException(CommandException):
25 def __init__(self, action):
26 super(NotEnoughRepPointsException, self).__init__(
28 """Sorry, but you don't have enough reputation points to %(action)s.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
29 ) % {'action': action, 'faq_url': reverse('faq')}
32 class CannotDoOnOwnException(CommandException):
33 def __init__(self, action):
34 super(CannotDoOnOwnException, self).__init__(
36 """Sorry but you cannot %(action)s your own post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
37 ) % {'action': action, 'faq_url': reverse('faq')}
40 class AnonymousNotAllowedException(CommandException):
41 def __init__(self, action):
42 super(AnonymousNotAllowedException, self).__init__(
44 """Sorry but anonymous users cannot %(action)s.<br />Please login or create an account <a href='%(signin_url)s'>here</a>."""
45 ) % {'action': action, 'signin_url': reverse('auth_signin')}
48 class NotEnoughLeftException(CommandException):
49 def __init__(self, action, limit):
50 super(NotEnoughLeftException, self).__init__(
52 """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>"""
53 ) % {'action': action, 'limit': limit, 'faq_url': reverse('faq')}
56 class CannotDoubleActionException(CommandException):
57 def __init__(self, action):
58 super(CannotDoubleActionException, self).__init__(
60 """Sorry, but you cannot %(action)s twice the same post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
61 ) % {'action': action, 'faq_url': reverse('faq')}
65 @decorate.withfn(command)
66 def vote_post(request, id, vote_type):
67 post = get_object_or_404(Node, id=id).leaf
70 if not user.is_authenticated():
71 raise AnonymousNotAllowedException(_('vote'))
73 if user == post.author:
74 raise CannotDoOnOwnException(_('vote'))
76 if not (vote_type == 'up' and user.can_vote_up() or user.can_vote_down()):
77 raise NotEnoughRepPointsException(vote_type == 'up' and _('upvote') or _('downvote'))
79 user_vote_count_today = user.get_vote_count_today()
80 user_can_vote_count_today = user.can_vote_count_today()
82 if user_vote_count_today >= user.can_vote_count_today():
83 raise NotEnoughLeftException(_('votes'), str(settings.MAX_VOTES_PER_DAY))
85 new_vote_cls = (vote_type == 'up') and VoteUpAction or VoteDownAction
88 old_vote = VoteAction.get_action_for(node=post, user=user)
91 if old_vote.action_date < datetime.datetime.now() - datetime.timedelta(days=int(settings.DENY_UNVOTE_DAYS)):
92 raise CommandException(
93 _("Sorry but you cannot cancel a vote after %(ndays)d %(tdays)s from the original vote") %
94 {'ndays': int(settings.DENY_UNVOTE_DAYS),
95 'tdays': ungettext('day', 'days', int(settings.DENY_UNVOTE_DAYS))}
98 old_vote.cancel(ip=request.META['REMOTE_ADDR'])
99 score_inc = (old_vote.__class__ == VoteDownAction) and 1 or -1
102 new_vote_cls(user=user, node=post, ip=request.META['REMOTE_ADDR']).save()
103 score_inc = (new_vote_cls == VoteUpAction) and 1 or -1
107 'update_post_score': [id, score_inc],
108 'update_user_post_vote': [id, vote_type]
112 votes_left = (user_can_vote_count_today - user_vote_count_today) + (vote_type == 'none' and -1 or 1)
114 if int(settings.START_WARN_VOTES_LEFT) >= votes_left:
115 response['message'] = _("You have %(nvotes)s %(tvotes)s left today.") % \
116 {'nvotes': votes_left, 'tvotes': ungettext('vote', 'votes', votes_left)}
120 @decorate.withfn(command)
121 def flag_post(request, id):
123 return render_to_response('node/report.html', {'types': settings.FLAG_TYPES})
125 post = get_object_or_404(Node, id=id)
128 if not user.is_authenticated():
129 raise AnonymousNotAllowedException(_('flag posts'))
131 if user == post.author:
132 raise CannotDoOnOwnException(_('flag'))
134 if not (user.can_flag_offensive(post)):
135 raise NotEnoughRepPointsException(_('flag posts'))
137 user_flag_count_today = user.get_flagged_items_count_today()
139 if user_flag_count_today >= int(settings.MAX_FLAGS_PER_DAY):
140 raise NotEnoughLeftException(_('flags'), str(settings.MAX_FLAGS_PER_DAY))
143 current = FlagAction.objects.get(canceled=False, user=user, node=post)
144 raise CommandException(
145 _("You already flagged this post with the following reason: %(reason)s") % {'reason': current.extra})
146 except ObjectDoesNotExist:
147 reason = request.POST.get('prompt', '').strip()
150 raise CommandException(_("Reason is empty"))
152 FlagAction(user=user, node=post, extra=reason, ip=request.META['REMOTE_ADDR']).save()
154 return {'message': _("Thank you for your report. A moderator will review your submission shortly.")}
156 @decorate.withfn(command)
157 def like_comment(request, id):
158 comment = get_object_or_404(Comment, id=id)
161 if not user.is_authenticated():
162 raise AnonymousNotAllowedException(_('like comments'))
164 if user == comment.user:
165 raise CannotDoOnOwnException(_('like'))
167 if not user.can_like_comment(comment):
168 raise NotEnoughRepPointsException( _('like comments'))
170 like = VoteAction.get_action_for(node=comment, user=user)
173 like.cancel(ip=request.META['REMOTE_ADDR'])
176 VoteUpCommentAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
181 'update_post_score': [comment.id, likes and 1 or -1],
182 'update_user_post_vote': [comment.id, likes and 'up' or 'none']
186 @decorate.withfn(command)
187 def delete_comment(request, id):
188 comment = get_object_or_404(Comment, id=id)
191 if not user.is_authenticated():
192 raise AnonymousNotAllowedException(_('delete comments'))
194 if not user.can_delete_comment(comment):
195 raise NotEnoughRepPointsException( _('delete comments'))
197 if not comment.nis.deleted:
198 DeleteAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
202 'remove_comment': [comment.id],
206 @decorate.withfn(command)
207 def mark_favorite(request, id):
208 question = get_object_or_404(Question, id=id)
210 if not request.user.is_authenticated():
211 raise AnonymousNotAllowedException(_('mark a question as favorite'))
214 favorite = FavoriteAction.objects.get(canceled=False, node=question, user=request.user)
215 favorite.cancel(ip=request.META['REMOTE_ADDR'])
217 except ObjectDoesNotExist:
218 FavoriteAction(node=question, user=request.user, ip=request.META['REMOTE_ADDR']).save()
223 'update_favorite_count': [added and 1 or -1],
224 'update_favorite_mark': [added and 'on' or 'off']
228 @decorate.withfn(command)
229 def comment(request, id):
230 post = get_object_or_404(Node, id=id)
233 if not user.is_authenticated():
234 raise AnonymousNotAllowedException(_('comment'))
236 if not request.method == 'POST':
237 raise CommandException(_("Invalid request"))
239 comment_text = request.POST.get('comment', '').strip()
241 if not len(comment_text):
242 raise CommandException(_("Comment is empty"))
244 if len(comment_text) < settings.FORM_MIN_COMMENT_BODY:
245 raise CommandException(_("At least %d characters required on comment body.") % settings.FORM_MIN_COMMENT_BODY)
247 if len(comment_text) > settings.FORM_MAX_COMMENT_BODY:
248 raise CommandException(_("No more than %d characters on comment body.") % settings.FORM_MAX_COMMENT_BODY)
250 if 'id' in request.POST:
251 comment = get_object_or_404(Comment, id=request.POST['id'])
253 if not user.can_edit_comment(comment):
254 raise NotEnoughRepPointsException( _('edit comments'))
256 comment = ReviseAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(
257 data=dict(text=comment_text)).node
259 if not user.can_comment(post):
260 raise NotEnoughRepPointsException( _('comment'))
262 comment = CommentAction(user=user, ip=request.META['REMOTE_ADDR']).save(
263 data=dict(text=comment_text, parent=post)).node
265 if comment.active_revision.revision == 1:
269 id, comment.id, comment.comment, user.decorated_name, user.get_profile_url(),
270 reverse('delete_comment', kwargs={'id': comment.id}),
271 reverse('node_markdown', kwargs={'id': comment.id}),
272 reverse('convert_comment', kwargs={'id': comment.id}),
273 user.can_convert_comment_to_answer(comment),
280 'update_comment': [comment.id, comment.comment]
284 @decorate.withfn(command)
285 def node_markdown(request, id):
288 if not user.is_authenticated():
289 raise AnonymousNotAllowedException(_('accept answers'))
291 node = get_object_or_404(Node, id=id)
292 return HttpResponse(node.active_revision.body, mimetype="text/plain")
295 @decorate.withfn(command)
296 def accept_answer(request, id):
297 if settings.DISABLE_ACCEPTING_FEATURE:
302 if not user.is_authenticated():
303 raise AnonymousNotAllowedException(_('accept answers'))
305 answer = get_object_or_404(Answer, id=id)
306 question = answer.question
308 if not user.can_accept_answer(answer):
309 raise CommandException(_("Sorry but you cannot accept the answer"))
313 if answer.nis.accepted:
314 answer.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR'])
315 commands['unmark_accepted'] = [answer.id]
317 if settings.MAXIMUM_ACCEPTED_ANSWERS and (question.accepted_count >= settings.MAXIMUM_ACCEPTED_ANSWERS):
318 raise CommandException(ungettext("This question already has an accepted answer.",
319 "Sorry but this question has reached the limit of accepted answers.", int(settings.MAXIMUM_ACCEPTED_ANSWERS)))
321 if settings.MAXIMUM_ACCEPTED_PER_USER and question.accepted_count:
322 accepted_from_author = question.accepted_answers.filter(author=answer.author).count()
324 if accepted_from_author >= settings.MAXIMUM_ACCEPTED_PER_USER:
325 raise CommandException(ungettext("The author of this answer already has an accepted answer in this question.",
326 "Sorry but the author of this answer has reached the limit of accepted answers per question.", int(settings.MAXIMUM_ACCEPTED_PER_USER)))
329 AcceptAnswerAction(node=answer, user=user, ip=request.META['REMOTE_ADDR']).save()
330 commands['mark_accepted'] = [answer.id]
332 return {'commands': commands}
334 @decorate.withfn(command)
335 def delete_post(request, id):
336 post = get_object_or_404(Node, id=id)
339 if not user.is_authenticated():
340 raise AnonymousNotAllowedException(_('delete posts'))
342 if not (user.can_delete_post(post)):
343 raise NotEnoughRepPointsException(_('delete posts'))
345 ret = {'commands': {}}
348 post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
349 ret['commands']['unmark_deleted'] = [post.node_type, id]
351 DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
353 ret['commands']['mark_deleted'] = [post.node_type, id]
357 @decorate.withfn(command)
358 def close(request, id, close):
359 if close and not request.POST:
360 return render_to_response('node/report.html', {'types': settings.CLOSE_TYPES})
362 question = get_object_or_404(Question, id=id)
365 if not user.is_authenticated():
366 raise AnonymousNotAllowedException(_('close questions'))
368 if question.nis.closed:
369 if not user.can_reopen_question(question):
370 raise NotEnoughRepPointsException(_('reopen questions'))
372 question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
374 if not request.user.can_close_question(question):
375 raise NotEnoughRepPointsException(_('close questions'))
377 reason = request.POST.get('prompt', '').strip()
380 raise CommandException(_("Reason is empty"))
382 CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
384 return RefreshPageCommand()
386 @decorate.withfn(command)
387 def wikify(request, id):
388 node = get_object_or_404(Node, id=id)
391 if not user.is_authenticated():
392 raise AnonymousNotAllowedException(_('mark posts as community wiki'))
395 if not user.can_cancel_wiki(node):
396 raise NotEnoughRepPointsException(_('cancel a community wiki post'))
398 if node.nstate.wiki.action_type == "wikify":
399 node.nstate.wiki.cancel()
401 node.nstate.wiki = None
403 if not user.can_wikify(node):
404 raise NotEnoughRepPointsException(_('mark posts as community wiki'))
406 WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
408 return RefreshPageCommand()
410 @decorate.withfn(command)
411 def convert_to_comment(request, id):
413 answer = get_object_or_404(Answer, id=id)
414 question = answer.question
416 # Check whether the user has the required permissions
417 if not user.is_authenticated():
418 raise AnonymousNotAllowedException(_("convert answers to comments"))
420 if not user.can_convert_to_comment(answer):
421 raise NotEnoughRepPointsException(_("convert answers to comments"))
424 description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': smart_unicode(a.author.username),
425 'snippet': a.summary[:10]}
426 nodes = [(question.id, _("Question"))]
427 [nodes.append((a.id, description(a))) for a in
428 question.answers.filter_state(deleted=False).exclude(id=answer.id)]
430 return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
433 new_parent = Node.objects.get(id=request.POST.get('under', None))
435 raise CommandException(_("That is an invalid post to put the comment under"))
437 if not (new_parent == question or (new_parent.node_type == 'answer' and new_parent.parent == question)):
438 raise CommandException(_("That is an invalid post to put the comment under"))
440 AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
442 return RefreshPageCommand()
444 @decorate.withfn(command)
445 def convert_comment_to_answer(request, id):
447 comment = get_object_or_404(Comment, id=id)
448 parent = comment.parent
450 if not parent.question:
453 question = parent.question
455 if not user.is_authenticated():
456 raise AnonymousNotAllowedException(_("convert comments to answers"))
458 if not user.can_convert_comment_to_answer(comment):
459 raise NotEnoughRepPointsException(_("convert comments to answers"))
461 CommentToAnswerAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(question=question))
463 return RefreshPageCommand()
465 @decorate.withfn(command)
466 def subscribe(request, id, user=None):
469 user = User.objects.get(id=user)
470 except User.DoesNotExist:
473 if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
474 raise CommandException(_("You do not have the correct credentials to preform this action."))
478 question = get_object_or_404(Question, id=id)
481 subscription = QuestionSubscription.objects.get(question=question, user=user)
482 subscription.delete()
485 subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
491 'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
492 'set_subscription_status': ['']
496 @decorate.withfn(command)
497 def canned_comments(request, post_id):
500 # Check whether the user has the required permissions to use the tool.
501 if not user.can_use_canned_comments:
502 raise CommandException(_("You cannot use the canned comments tool."))
506 for comment in settings.CANNED_COMMENTS:
507 canned_comments.append(smart_unicode(comment))
509 return render_to_response('node/canned_comments.html', {
510 'canned_comments' : canned_comments,
511 }, RequestContext(request))
513 comment = request.POST.get('comment', '')
517 'canned_comment' : [post_id, comment],
521 #internally grouped views - used by the tagging system
523 def mark_tag(request, tag=None, **kwargs):#tagging system
524 action = kwargs['action']
525 ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
526 if action == 'remove':
527 logging.debug('deleting tag %s' % tag)
530 reason = kwargs['reason']
533 t = Tag.objects.get(name=tag)
534 mt = MarkedTag(user=request.user, reason=reason, tag=t)
539 ts.update(reason=reason)
540 return HttpResponse(simplejson.dumps(''), mimetype="application/json")
542 def matching_tags(request):
543 if len(request.GET['q']) == 0:
544 raise CommandException(_("Invalid request"))
546 possible_tags = Tag.active.filter(name__icontains = request.GET['q'])
548 for tag in possible_tags:
549 tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
551 return HttpResponse(tag_output, mimetype="text/plain")
553 def matching_users(request):
554 if len(request.GET['q']) == 0:
555 raise CommandException(_("Invalid request"))
557 possible_users = User.objects.filter(username__icontains = request.GET['q'])
560 for user in possible_users:
561 output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
563 return HttpResponse(output, mimetype="text/plain")
565 def related_questions(request):
566 if request.POST and request.POST.get('title', None):
567 can_rank, questions = Question.objects.search(request.POST['title'])
569 if can_rank and isinstance(can_rank, basestring):
570 questions = questions.order_by(can_rank)
572 return HttpResponse(simplejson.dumps(
573 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
574 for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")
578 @decorate.withfn(command)
579 def answer_permanent_link(request, id):
580 # Getting the current answer object
581 answer = get_object_or_404(Answer, id=id)
583 # Getting the current object URL -- the Application URL + the object relative URL
584 url = '%s%s' % (settings.APP_BASE_URL, answer.get_absolute_url())
587 # Display the template
588 return render_to_response('node/permanent_link.html', { 'url' : url, })
592 'copy_url' : [request.POST['permanent_link_url'],],
594 'message' : _("The permanent URL to the answer has been copied to your clipboard."),
597 @decorate.withfn(command)
598 def award_points(request, user_id, answer_id):
600 awarded_user = get_object_or_404(User, id=user_id)
601 answer = get_object_or_404(Answer, id=answer_id)
603 # Users shouldn't be able to award themselves
604 if awarded_user.id == user.id:
605 raise CannotDoOnOwnException(_("award"))
607 # Anonymous users cannot award points, they just don't have such
608 if not user.is_authenticated():
609 raise AnonymousNotAllowedException(_('award'))
612 return render_to_response("node/award_points.html", { 'user' : user, 'awarded_user' : awarded_user, })
614 points = int(request.POST['points'])
616 # We should check if the user has enough reputation points, otherwise we raise an exception.
618 raise CommandException(_("The number of points to award needs to be a positive value."))
620 if user.reputation < points:
621 raise NotEnoughRepPointsException(_("award"))
623 extra = dict(message=request.POST.get('message', ''), awarding_user=request.user.id, value=points)
625 # We take points from the awarding user
626 AwardPointsAction(user=request.user, node=answer, extra=extra).save(data=dict(value=points, affected=awarded_user))
628 return { 'message' : _("You have awarded %(awarded_user)s with %(points)d points") % {'awarded_user' : awarded_user, 'points' : points} }