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 from django.shortcuts import get_object_or_404, render_to_response
16 from django.contrib import messages
18 from forum.models import *
19 from forum.utils.decorators import ajax_login_required
20 from forum.actions import *
21 from forum.modules import decorate
22 from forum import settings
24 from decorators import command, CommandException, RefreshPageCommand
26 class NotEnoughRepPointsException(CommandException):
27 def __init__(self, action, user_reputation=None, reputation_required=None, node=None):
28 if reputation_required is not None and user_reputation is not None:
30 """Sorry, but you don't have enough reputation points to %(action)s.<br />
31 The minimum reputation required is %(reputation_required)d (yours is %(user_reputation)d).
32 Please check the <a href='%(faq_url)s'>FAQ</a>"""
35 'faq_url': reverse('faq'),
36 'reputation_required' : reputation_required,
37 'user_reputation' : user_reputation,
41 """Sorry, but you don't have enough reputation points to %(action)s.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
42 ) % {'action': action, 'faq_url': reverse('faq')}
43 super(NotEnoughRepPointsException, self).__init__(message)
45 class CannotDoOnOwnException(CommandException):
46 def __init__(self, action):
47 super(CannotDoOnOwnException, self).__init__(
49 """Sorry but you cannot %(action)s your own post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
50 ) % {'action': action, 'faq_url': reverse('faq')}
53 class AnonymousNotAllowedException(CommandException):
54 def __init__(self, action):
55 super(AnonymousNotAllowedException, self).__init__(
57 """Sorry but anonymous users cannot %(action)s.<br />Please login or create an account <a href='%(signin_url)s'>here</a>."""
58 ) % {'action': action, 'signin_url': reverse('auth_signin')}
61 class NotEnoughLeftException(CommandException):
62 def __init__(self, action, limit):
63 super(NotEnoughLeftException, self).__init__(
65 """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>"""
66 ) % {'action': action, 'limit': limit, 'faq_url': reverse('faq')}
69 class CannotDoubleActionException(CommandException):
70 def __init__(self, action):
71 super(CannotDoubleActionException, self).__init__(
73 """Sorry, but you cannot %(action)s twice the same post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
74 ) % {'action': action, 'faq_url': reverse('faq')}
78 @decorate.withfn(command)
79 def vote_post(request, id, vote_type):
80 if not request.method == 'POST':
81 raise CommandException(_("Invalid request"))
84 post = get_object_or_404(Node, id=id).leaf
87 if not user.is_authenticated():
88 raise AnonymousNotAllowedException(_('vote'))
90 if user == post.author:
91 raise CannotDoOnOwnException(_('vote'))
93 if not (vote_type == 'up' and user.can_vote_up() or user.can_vote_down()):
94 reputation_required = int(settings.REP_TO_VOTE_UP) if vote_type == 'up' else int(settings.REP_TO_VOTE_DOWN)
95 action_type = vote_type == 'up' and _('upvote') or _('downvote')
96 raise NotEnoughRepPointsException(action_type, user_reputation=user.reputation, reputation_required=reputation_required, node=post)
98 user_vote_count_today = user.get_vote_count_today()
99 user_can_vote_count_today = user.can_vote_count_today()
101 if user_vote_count_today >= user.can_vote_count_today():
102 raise NotEnoughLeftException(_('votes'), str(settings.MAX_VOTES_PER_DAY))
104 new_vote_cls = (vote_type == 'up') and VoteUpAction or VoteDownAction
107 old_vote = VoteAction.get_action_for(node=post, user=user)
110 if old_vote.action_date < datetime.datetime.now() - datetime.timedelta(days=int(settings.DENY_UNVOTE_DAYS)):
111 raise CommandException(
112 _("Sorry but you cannot cancel a vote after %(ndays)d %(tdays)s from the original vote") %
113 {'ndays': int(settings.DENY_UNVOTE_DAYS),
114 'tdays': ungettext('day', 'days', int(settings.DENY_UNVOTE_DAYS))}
117 old_vote.cancel(ip=request.META['REMOTE_ADDR'])
118 score_inc = (old_vote.__class__ == VoteDownAction) and 1 or -1
121 new_vote_cls(user=user, node=post, ip=request.META['REMOTE_ADDR']).save()
122 score_inc = (new_vote_cls == VoteUpAction) and 1 or -1
126 'update_post_score': [id, score_inc],
127 'update_user_post_vote': [id, vote_type]
131 votes_left = (user_can_vote_count_today - user_vote_count_today) + (vote_type == 'none' and -1 or 1)
133 if int(settings.START_WARN_VOTES_LEFT) >= votes_left:
134 response['message'] = _("You have %(nvotes)s %(tvotes)s left today.") % \
135 {'nvotes': votes_left, 'tvotes': ungettext('vote', 'votes', votes_left)}
139 @decorate.withfn(command)
140 def flag_post(request, id):
142 return render_to_response('node/report.html', {'types': settings.FLAG_TYPES})
144 post = get_object_or_404(Node, id=id)
147 if not user.is_authenticated():
148 raise AnonymousNotAllowedException(_('flag posts'))
150 if user == post.author:
151 raise CannotDoOnOwnException(_('flag'))
153 if not (user.can_flag_offensive(post)):
154 raise NotEnoughRepPointsException(_('flag posts'))
156 user_flag_count_today = user.get_flagged_items_count_today()
158 if user_flag_count_today >= int(settings.MAX_FLAGS_PER_DAY):
159 raise NotEnoughLeftException(_('flags'), str(settings.MAX_FLAGS_PER_DAY))
162 current = FlagAction.objects.get(canceled=False, user=user, node=post)
163 raise CommandException(
164 _("You already flagged this post with the following reason: %(reason)s") % {'reason': current.extra})
165 except ObjectDoesNotExist:
166 reason = request.POST.get('prompt', '').strip()
169 raise CommandException(_("Reason is empty"))
171 FlagAction(user=user, node=post, extra=reason, ip=request.META['REMOTE_ADDR']).save()
173 return {'message': _("Thank you for your report. A moderator will review your submission shortly.")}
175 @decorate.withfn(command)
176 def like_comment(request, id):
177 comment = get_object_or_404(Comment, id=id)
180 if not user.is_authenticated():
181 raise AnonymousNotAllowedException(_('like comments'))
183 if user == comment.user:
184 raise CannotDoOnOwnException(_('like'))
186 if not user.can_like_comment(comment):
187 raise NotEnoughRepPointsException( _('like comments'), node=comment)
189 like = VoteAction.get_action_for(node=comment, user=user)
192 like.cancel(ip=request.META['REMOTE_ADDR'])
195 VoteUpCommentAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
200 'update_post_score': [comment.id, likes and 1 or -1],
201 'update_user_post_vote': [comment.id, likes and 'up' or 'none']
205 @decorate.withfn(command)
206 def delete_comment(request, id):
207 comment = get_object_or_404(Comment, id=id)
210 if not user.is_authenticated():
211 raise AnonymousNotAllowedException(_('delete comments'))
213 if not user.can_delete_comment(comment):
214 raise NotEnoughRepPointsException( _('delete comments'))
216 if not comment.nis.deleted:
217 DeleteAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
221 'remove_comment': [comment.id],
225 @decorate.withfn(command)
226 def mark_favorite(request, id):
227 node = get_object_or_404(Node, id=id)
229 if not request.user.is_authenticated():
230 raise AnonymousNotAllowedException(_('mark a question as favorite'))
233 favorite = FavoriteAction.objects.get(canceled=False, node=node, user=request.user)
234 favorite.cancel(ip=request.META['REMOTE_ADDR'])
236 except ObjectDoesNotExist:
237 FavoriteAction(node=node, user=request.user, ip=request.META['REMOTE_ADDR']).save()
242 'update_favorite_count': [added and 1 or -1],
243 'update_favorite_mark': [added and 'on' or 'off']
247 @decorate.withfn(command)
248 def comment(request, id):
249 post = get_object_or_404(Node, id=id)
252 if not user.is_authenticated():
253 raise AnonymousNotAllowedException(_('comment'))
255 if not request.method == 'POST':
256 raise CommandException(_("Invalid request"))
258 comment_text = request.POST.get('comment', '').strip()
260 if not len(comment_text):
261 raise CommandException(_("Comment is empty"))
263 if len(comment_text) < settings.FORM_MIN_COMMENT_BODY:
264 raise CommandException(_("At least %d characters required on comment body.") % settings.FORM_MIN_COMMENT_BODY)
266 if len(comment_text) > settings.FORM_MAX_COMMENT_BODY:
267 raise CommandException(_("No more than %d characters on comment body.") % settings.FORM_MAX_COMMENT_BODY)
269 if 'id' in request.POST:
270 comment = get_object_or_404(Comment, id=request.POST['id'])
272 if not user.can_edit_comment(comment):
273 raise NotEnoughRepPointsException( _('edit comments'))
275 comment = ReviseAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(
276 data=dict(text=comment_text)).node
278 if not user.can_comment(post):
279 raise NotEnoughRepPointsException( _('comment'))
281 comment = CommentAction(user=user, ip=request.META['REMOTE_ADDR']).save(
282 data=dict(text=comment_text, parent=post)).node
284 if comment.active_revision.revision == 1:
288 id, comment.id, comment.comment, user.decorated_name, user.get_profile_url(),
289 reverse('delete_comment', kwargs={'id': comment.id}),
290 reverse('node_markdown', kwargs={'id': comment.id}),
291 reverse('convert_comment', kwargs={'id': comment.id}),
292 user.can_convert_comment_to_answer(comment),
293 bool(settings.SHOW_LATEST_COMMENTS_FIRST)
300 'update_comment': [comment.id, comment.comment]
304 @decorate.withfn(command)
305 def node_markdown(request, id):
308 if not user.is_authenticated():
309 raise AnonymousNotAllowedException(_('accept answers'))
311 node = get_object_or_404(Node, id=id)
312 return HttpResponse(node.active_revision.body, mimetype="text/plain")
315 @decorate.withfn(command)
316 def accept_answer(request, id):
317 if settings.DISABLE_ACCEPTING_FEATURE:
322 if not user.is_authenticated():
323 raise AnonymousNotAllowedException(_('accept answers'))
325 answer = get_object_or_404(Answer, id=id)
326 question = answer.question
328 if not user.can_accept_answer(answer):
329 raise CommandException(_("Sorry but you cannot accept the answer"))
333 if answer.nis.accepted:
334 answer.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR'])
335 commands['unmark_accepted'] = [answer.id]
337 if settings.MAXIMUM_ACCEPTED_ANSWERS and (question.accepted_count >= settings.MAXIMUM_ACCEPTED_ANSWERS):
338 raise CommandException(ungettext("This question already has an accepted answer.",
339 "Sorry but this question has reached the limit of accepted answers.", int(settings.MAXIMUM_ACCEPTED_ANSWERS)))
341 if settings.MAXIMUM_ACCEPTED_PER_USER and question.accepted_count:
342 accepted_from_author = question.accepted_answers.filter(author=answer.author).count()
344 if accepted_from_author >= settings.MAXIMUM_ACCEPTED_PER_USER:
345 raise CommandException(ungettext("The author of this answer already has an accepted answer in this question.",
346 "Sorry but the author of this answer has reached the limit of accepted answers per question.", int(settings.MAXIMUM_ACCEPTED_PER_USER)))
349 AcceptAnswerAction(node=answer, user=user, ip=request.META['REMOTE_ADDR']).save()
351 # If the request is not an AJAX redirect to the answer URL rather than to the home page
352 if not request.is_ajax():
354 Congratulations! You've accepted an answer.
357 # Notify the user with a message that an answer has been accepted
358 messages.info(request, msg)
360 # Redirect URL should include additional get parameters that might have been attached
361 redirect_url = answer.parent.get_absolute_url() + "?accepted_answer=true&%s" % smart_unicode(urlencode(request.GET))
363 return HttpResponseRedirect(redirect_url)
365 commands['mark_accepted'] = [answer.id]
367 return {'commands': commands}
369 @decorate.withfn(command)
370 def delete_post(request, id):
371 post = get_object_or_404(Node, id=id)
374 if not user.is_authenticated():
375 raise AnonymousNotAllowedException(_('delete posts'))
377 if not (user.can_delete_post(post)):
378 raise NotEnoughRepPointsException(_('delete posts'))
380 ret = {'commands': {}}
383 post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
384 ret['commands']['unmark_deleted'] = [post.node_type, id]
386 DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
388 ret['commands']['mark_deleted'] = [post.node_type, id]
392 @decorate.withfn(command)
393 def close(request, id, close):
394 if close and not request.POST:
395 return render_to_response('node/report.html', {'types': settings.CLOSE_TYPES})
397 question = get_object_or_404(Question, id=id)
400 if not user.is_authenticated():
401 raise AnonymousNotAllowedException(_('close questions'))
403 if question.nis.closed:
404 if not user.can_reopen_question(question):
405 raise NotEnoughRepPointsException(_('reopen questions'))
407 question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
409 if not request.user.can_close_question(question):
410 raise NotEnoughRepPointsException(_('close questions'))
412 reason = request.POST.get('prompt', '').strip()
415 raise CommandException(_("Reason is empty"))
417 CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
419 return RefreshPageCommand()
421 @decorate.withfn(command)
422 def wikify(request, id):
423 node = get_object_or_404(Node, id=id)
426 if not user.is_authenticated():
427 raise AnonymousNotAllowedException(_('mark posts as community wiki'))
430 if not user.can_cancel_wiki(node):
431 raise NotEnoughRepPointsException(_('cancel a community wiki post'))
433 if node.nstate.wiki.action_type == "wikify":
434 node.nstate.wiki.cancel()
436 node.nstate.wiki = None
438 if not user.can_wikify(node):
439 raise NotEnoughRepPointsException(_('mark posts as community wiki'))
441 WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
443 return RefreshPageCommand()
445 @decorate.withfn(command)
446 def convert_to_comment(request, id):
448 answer = get_object_or_404(Answer, id=id)
449 question = answer.question
451 # Check whether the user has the required permissions
452 if not user.is_authenticated():
453 raise AnonymousNotAllowedException(_("convert answers to comments"))
455 if not user.can_convert_to_comment(answer):
456 raise NotEnoughRepPointsException(_("convert answers to comments"))
459 description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': smart_unicode(a.author.username),
460 'snippet': a.summary[:10]}
461 nodes = [(question.id, _("Question"))]
462 [nodes.append((a.id, description(a))) for a in
463 question.answers.filter_state(deleted=False).exclude(id=answer.id)]
465 return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
468 new_parent = Node.objects.get(id=request.POST.get('under', None))
470 raise CommandException(_("That is an invalid post to put the comment under"))
472 if not (new_parent == question or (new_parent.node_type == 'answer' and new_parent.parent == question)):
473 raise CommandException(_("That is an invalid post to put the comment under"))
475 AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
477 return RefreshPageCommand()
479 @decorate.withfn(command)
480 def convert_comment_to_answer(request, id):
482 comment = get_object_or_404(Comment, id=id)
483 parent = comment.parent
485 if not parent.question:
488 question = parent.question
490 if not user.is_authenticated():
491 raise AnonymousNotAllowedException(_("convert comments to answers"))
493 if not user.can_convert_comment_to_answer(comment):
494 raise NotEnoughRepPointsException(_("convert comments to answers"))
496 CommentToAnswerAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(question=question))
498 return RefreshPageCommand()
500 @decorate.withfn(command)
501 def subscribe(request, id, user=None):
504 user = User.objects.get(id=user)
505 except User.DoesNotExist:
508 if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
509 raise CommandException(_("You do not have the correct credentials to preform this action."))
513 question = get_object_or_404(Question, id=id)
516 subscription = QuestionSubscription.objects.get(question=question, user=user)
517 subscription.delete()
520 subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
526 'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
527 'set_subscription_status': ['']
531 #internally grouped views - used by the tagging system
533 def mark_tag(request, tag=None, **kwargs):#tagging system
534 action = kwargs['action']
535 ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
536 if action == 'remove':
537 logging.debug('deleting tag %s' % tag)
540 reason = kwargs['reason']
543 t = Tag.objects.get(name=tag)
544 mt = MarkedTag(user=request.user, reason=reason, tag=t)
549 ts.update(reason=reason)
550 return HttpResponse(json.dumps(''), mimetype="application/json")
552 def matching_tags(request):
553 if len(request.GET['q']) == 0:
554 raise CommandException(_("Invalid request"))
556 possible_tags = Tag.active.filter(name__icontains = request.GET['q'])
558 for tag in possible_tags:
559 tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
561 return HttpResponse(tag_output, mimetype="text/plain")
563 def matching_users(request):
564 if len(request.GET['q']) == 0:
565 raise CommandException(_("Invalid request"))
567 possible_users = User.objects.filter(username__icontains = request.GET['q'])
570 for user in possible_users:
571 output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
573 return HttpResponse(output, mimetype="text/plain")
575 def related_questions(request):
576 if request.POST and request.POST.get('title', None):
577 can_rank, questions = Question.objects.search(request.POST['title'])
579 if can_rank and isinstance(can_rank, basestring):
580 questions = questions.order_by(can_rank)
582 return HttpResponse(json.dumps(
583 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
584 for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")
588 @decorate.withfn(command)
589 def answer_permanent_link(request, id):
590 # Getting the current answer object
591 answer = get_object_or_404(Answer, id=id)
593 # Getting the current object URL -- the Application URL + the object relative URL
594 url = '%s%s' % (settings.APP_BASE_URL, answer.get_absolute_url())
597 # Display the template
598 return render_to_response('node/permanent_link.html', { 'url' : url, })
602 'copy_url' : [request.POST['permanent_link_url'],],
604 'message' : _("The permanent URL to the answer has been copied to your clipboard."),
607 @decorate.withfn(command)
608 def award_points(request, user_id, answer_id):
610 awarded_user = get_object_or_404(User, id=user_id)
611 answer = get_object_or_404(Answer, id=answer_id)
613 # Users shouldn't be able to award themselves
614 if awarded_user.id == user.id:
615 raise CannotDoOnOwnException(_("award"))
617 # Anonymous users cannot award points, they just don't have such
618 if not user.is_authenticated():
619 raise AnonymousNotAllowedException(_('award'))
622 return render_to_response("node/award_points.html", {
624 'awarded_user' : awarded_user,
625 'reputation_to_comment' : str(settings.REP_TO_COMMENT)
628 points = int(request.POST['points'])
630 # We should check if the user has enough reputation points, otherwise we raise an exception.
632 raise CommandException(_("The number of points to award needs to be a positive value."))
634 if user.reputation < points:
635 raise NotEnoughRepPointsException(_("award"))
637 extra = dict(message=request.POST.get('message', ''), awarding_user=request.user.id, value=points)
639 # We take points from the awarding user
640 AwardPointsAction(user=request.user, node=answer, extra=extra).save(data=dict(value=points, affected=awarded_user))
642 return { 'message' : _("You have awarded %(awarded_user)s with %(points)d points") % {'awarded_user' : awarded_user, 'points' : points} }