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}),
268 user.can_convert_comment_to_answer(comment),
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.active_revision.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 subscribe(request, id, user=None):
463 user = User.objects.get(id=user)
464 except User.DoesNotExist:
467 if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
468 raise CommandException(_("You do not have the correct credentials to preform this action."))
472 question = get_object_or_404(Question, id=id)
475 subscription = QuestionSubscription.objects.get(question=question, user=user)
476 subscription.delete()
479 subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
485 'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
486 'set_subscription_status': ['']
490 #internally grouped views - used by the tagging system
492 def mark_tag(request, tag=None, **kwargs):#tagging system
493 action = kwargs['action']
494 ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
495 if action == 'remove':
496 logging.debug('deleting tag %s' % tag)
499 reason = kwargs['reason']
502 t = Tag.objects.get(name=tag)
503 mt = MarkedTag(user=request.user, reason=reason, tag=t)
508 ts.update(reason=reason)
509 return HttpResponse(simplejson.dumps(''), mimetype="application/json")
511 def matching_tags(request):
512 if len(request.GET['q']) == 0:
513 raise CommandException(_("Invalid request"))
515 possible_tags = Tag.active.filter(name__icontains = request.GET['q'])
517 for tag in possible_tags:
518 tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
520 return HttpResponse(tag_output, mimetype="text/plain")
522 def matching_users(request):
523 if len(request.GET['q']) == 0:
524 raise CommandException(_("Invalid request"))
526 possible_users = User.objects.filter(username__icontains = request.GET['q'])
529 for user in possible_users:
530 output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
532 return HttpResponse(output, mimetype="text/plain")
534 def related_questions(request):
535 if request.POST and request.POST.get('title', None):
536 can_rank, questions = Question.objects.search(request.POST['title'])
538 if can_rank and isinstance(can_rank, basestring):
539 questions = questions.order_by(can_rank)
541 return HttpResponse(simplejson.dumps(
542 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
543 for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")
547 @decorate.withfn(command)
548 def answer_permanent_link(request, id):
549 # Getting the current answer object
550 answer = get_object_or_404(Answer, id=id)
552 # Getting the current object URL -- the Application URL + the object relative URL
553 url = '%s%s' % (settings.APP_BASE_URL, answer.get_absolute_url())
556 # Display the template
557 return render_to_response('node/permanent_link.html', { 'url' : url, })
561 'copy_url' : [request.POST['permanent_link_url'],],
563 'message' : _("The permanent URL to the answer has been copied to your clipboard."),
566 @decorate.withfn(command)
567 def award_points(request, user_id, answer_id):
569 awarded_user = get_object_or_404(User, id=user_id)
570 answer = get_object_or_404(Answer, id=answer_id)
572 # Users shouldn't be able to award themselves
573 if awarded_user.id == user.id:
574 raise CannotDoOnOwnException(_("award"))
576 # Anonymous users cannot award points, they just don't have such
577 if not user.is_authenticated():
578 raise AnonymousNotAllowedException(_('award'))
581 return render_to_response("node/award_points.html", { 'user' : user, 'awarded_user' : awarded_user, })
583 points = int(request.POST['points'])
585 # We should check if the user has enough reputation points, otherwise we raise an exception.
586 if user.reputation < points:
587 raise NotEnoughRepPointsException(_("award"))
589 extra = dict(message=request.POST.get('message', ''), awarding_user=request.user.id, value=points)
591 # We take points from the awarding user
592 AwardPointsAction(user=request.user, node=answer, extra=extra).save(data=dict(value=points, affected=awarded_user))
594 return { 'message' : _("You have awarded %(awarded_user)s with %(points)d points") % {'awarded_user' : awarded_user, 'points' : points} }