1 # -*- coding: utf-8 -*-
6 from urllib import urlencode
8 from django.core.exceptions import ObjectDoesNotExist
9 from django.core.urlresolvers import reverse
10 from django.utils import simplejson
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 forum.models import *
17 from forum.utils.decorators import 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, user_reputation=None, reputation_required=None, node=None):
26 if reputation_required is not None and user_reputation is not None:
28 """Sorry, but you don't have enough reputation points to %(action)s.<br />
29 The minimum reputation required is %(reputation_required)d (yours is %(user_reputation)d).
30 Please check the <a href='%(faq_url)s'>FAQ</a>"""
33 'faq_url': reverse('faq'),
34 'reputation_required' : reputation_required,
35 'user_reputation' : user_reputation,
39 """Sorry, but you don't have enough reputation points to %(action)s.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
40 ) % {'action': action, 'faq_url': reverse('faq')}
41 super(NotEnoughRepPointsException, self).__init__(message)
43 class CannotDoOnOwnException(CommandException):
44 def __init__(self, action):
45 super(CannotDoOnOwnException, self).__init__(
47 """Sorry but you cannot %(action)s your own post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
48 ) % {'action': action, 'faq_url': reverse('faq')}
51 class AnonymousNotAllowedException(CommandException):
52 def __init__(self, action):
53 super(AnonymousNotAllowedException, self).__init__(
55 """Sorry but anonymous users cannot %(action)s.<br />Please login or create an account <a href='%(signin_url)s'>here</a>."""
56 ) % {'action': action, 'signin_url': reverse('auth_signin')}
59 class NotEnoughLeftException(CommandException):
60 def __init__(self, action, limit):
61 super(NotEnoughLeftException, self).__init__(
63 """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>"""
64 ) % {'action': action, 'limit': limit, 'faq_url': reverse('faq')}
67 class CannotDoubleActionException(CommandException):
68 def __init__(self, action):
69 super(CannotDoubleActionException, self).__init__(
71 """Sorry, but you cannot %(action)s twice the same post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
72 ) % {'action': action, 'faq_url': reverse('faq')}
76 @decorate.withfn(command)
77 def vote_post(request, id, vote_type):
78 if not request.method == 'POST':
79 raise CommandException(_("Invalid request"))
82 post = get_object_or_404(Node, id=id).leaf
85 if not user.is_authenticated():
86 raise AnonymousNotAllowedException(_('vote'))
88 if user == post.author:
89 raise CannotDoOnOwnException(_('vote'))
91 if not (vote_type == 'up' and user.can_vote_up() or user.can_vote_down()):
92 reputation_required = int(settings.REP_TO_VOTE_UP) if vote_type == 'up' else int(settings.REP_TO_VOTE_DOWN)
93 action_type = vote_type == 'up' and _('upvote') or _('downvote')
94 raise NotEnoughRepPointsException(action_type, user_reputation=user.reputation, reputation_required=reputation_required, node=post)
96 user_vote_count_today = user.get_vote_count_today()
97 user_can_vote_count_today = user.can_vote_count_today()
99 if user_vote_count_today >= user.can_vote_count_today():
100 raise NotEnoughLeftException(_('votes'), str(settings.MAX_VOTES_PER_DAY))
102 new_vote_cls = (vote_type == 'up') and VoteUpAction or VoteDownAction
105 old_vote = VoteAction.get_action_for(node=post, user=user)
108 if old_vote.action_date < datetime.datetime.now() - datetime.timedelta(days=int(settings.DENY_UNVOTE_DAYS)):
109 raise CommandException(
110 _("Sorry but you cannot cancel a vote after %(ndays)d %(tdays)s from the original vote") %
111 {'ndays': int(settings.DENY_UNVOTE_DAYS),
112 'tdays': ungettext('day', 'days', int(settings.DENY_UNVOTE_DAYS))}
115 old_vote.cancel(ip=request.META['REMOTE_ADDR'])
116 score_inc = (old_vote.__class__ == VoteDownAction) and 1 or -1
119 new_vote_cls(user=user, node=post, ip=request.META['REMOTE_ADDR']).save()
120 score_inc = (new_vote_cls == VoteUpAction) and 1 or -1
124 'update_post_score': [id, score_inc],
125 'update_user_post_vote': [id, vote_type]
129 votes_left = (user_can_vote_count_today - user_vote_count_today) + (vote_type == 'none' and -1 or 1)
131 if int(settings.START_WARN_VOTES_LEFT) >= votes_left:
132 response['message'] = _("You have %(nvotes)s %(tvotes)s left today.") % \
133 {'nvotes': votes_left, 'tvotes': ungettext('vote', 'votes', votes_left)}
137 @decorate.withfn(command)
138 def flag_post(request, id):
140 return render_to_response('node/report.html', {'types': settings.FLAG_TYPES})
142 post = get_object_or_404(Node, id=id)
145 if not user.is_authenticated():
146 raise AnonymousNotAllowedException(_('flag posts'))
148 if user == post.author:
149 raise CannotDoOnOwnException(_('flag'))
151 if not (user.can_flag_offensive(post)):
152 raise NotEnoughRepPointsException(_('flag posts'))
154 user_flag_count_today = user.get_flagged_items_count_today()
156 if user_flag_count_today >= int(settings.MAX_FLAGS_PER_DAY):
157 raise NotEnoughLeftException(_('flags'), str(settings.MAX_FLAGS_PER_DAY))
160 current = FlagAction.objects.get(canceled=False, user=user, node=post)
161 raise CommandException(
162 _("You already flagged this post with the following reason: %(reason)s") % {'reason': current.extra})
163 except ObjectDoesNotExist:
164 reason = request.POST.get('prompt', '').strip()
167 raise CommandException(_("Reason is empty"))
169 FlagAction(user=user, node=post, extra=reason, ip=request.META['REMOTE_ADDR']).save()
171 return {'message': _("Thank you for your report. A moderator will review your submission shortly.")}
173 @decorate.withfn(command)
174 def like_comment(request, id):
175 comment = get_object_or_404(Comment, id=id)
178 if not user.is_authenticated():
179 raise AnonymousNotAllowedException(_('like comments'))
181 if user == comment.user:
182 raise CannotDoOnOwnException(_('like'))
184 if not user.can_like_comment(comment):
185 raise NotEnoughRepPointsException( _('like comments'), node=comment)
187 like = VoteAction.get_action_for(node=comment, user=user)
190 like.cancel(ip=request.META['REMOTE_ADDR'])
193 VoteUpCommentAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
198 'update_post_score': [comment.id, likes and 1 or -1],
199 'update_user_post_vote': [comment.id, likes and 'up' or 'none']
203 @decorate.withfn(command)
204 def delete_comment(request, id):
205 comment = get_object_or_404(Comment, id=id)
208 if not user.is_authenticated():
209 raise AnonymousNotAllowedException(_('delete comments'))
211 if not user.can_delete_comment(comment):
212 raise NotEnoughRepPointsException( _('delete comments'))
214 if not comment.nis.deleted:
215 DeleteAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
219 'remove_comment': [comment.id],
223 @decorate.withfn(command)
224 def mark_favorite(request, id):
225 node = get_object_or_404(Node, id=id)
227 if not request.user.is_authenticated():
228 raise AnonymousNotAllowedException(_('mark a question as favorite'))
231 favorite = FavoriteAction.objects.get(canceled=False, node=node, user=request.user)
232 favorite.cancel(ip=request.META['REMOTE_ADDR'])
234 except ObjectDoesNotExist:
235 FavoriteAction(node=node, user=request.user, ip=request.META['REMOTE_ADDR']).save()
240 'update_favorite_count': [added and 1 or -1],
241 'update_favorite_mark': [added and 'on' or 'off']
245 @decorate.withfn(command)
246 def comment(request, id):
247 post = get_object_or_404(Node, id=id)
250 if not user.is_authenticated():
251 raise AnonymousNotAllowedException(_('comment'))
253 if not request.method == 'POST':
254 raise CommandException(_("Invalid request"))
256 comment_text = request.POST.get('comment', '').strip()
258 if not len(comment_text):
259 raise CommandException(_("Comment is empty"))
261 if len(comment_text) < settings.FORM_MIN_COMMENT_BODY:
262 raise CommandException(_("At least %d characters required on comment body.") % settings.FORM_MIN_COMMENT_BODY)
264 if len(comment_text) > settings.FORM_MAX_COMMENT_BODY:
265 raise CommandException(_("No more than %d characters on comment body.") % settings.FORM_MAX_COMMENT_BODY)
267 if 'id' in request.POST:
268 comment = get_object_or_404(Comment, id=request.POST['id'])
270 if not user.can_edit_comment(comment):
271 raise NotEnoughRepPointsException( _('edit comments'))
273 comment = ReviseAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(
274 data=dict(text=comment_text)).node
276 if not user.can_comment(post):
277 raise NotEnoughRepPointsException( _('comment'))
279 comment = CommentAction(user=user, ip=request.META['REMOTE_ADDR']).save(
280 data=dict(text=comment_text, parent=post)).node
282 if comment.active_revision.revision == 1:
286 id, comment.id, comment.comment, user.decorated_name, user.get_profile_url(),
287 reverse('delete_comment', kwargs={'id': comment.id}),
288 reverse('node_markdown', kwargs={'id': comment.id}),
289 reverse('convert_comment', kwargs={'id': comment.id}),
290 user.can_convert_comment_to_answer(comment),
291 bool(settings.SHOW_LATEST_COMMENTS_FIRST)
298 'update_comment': [comment.id, comment.comment]
302 @decorate.withfn(command)
303 def node_markdown(request, id):
306 if not user.is_authenticated():
307 raise AnonymousNotAllowedException(_('accept answers'))
309 node = get_object_or_404(Node, id=id)
310 return HttpResponse(node.active_revision.body, mimetype="text/plain")
313 @decorate.withfn(command)
314 def accept_answer(request, id):
315 if settings.DISABLE_ACCEPTING_FEATURE:
320 if not user.is_authenticated():
321 raise AnonymousNotAllowedException(_('accept answers'))
323 answer = get_object_or_404(Answer, id=id)
324 question = answer.question
326 if not user.can_accept_answer(answer):
327 raise CommandException(_("Sorry but you cannot accept the answer"))
331 if answer.nis.accepted:
332 answer.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR'])
333 commands['unmark_accepted'] = [answer.id]
335 if settings.MAXIMUM_ACCEPTED_ANSWERS and (question.accepted_count >= settings.MAXIMUM_ACCEPTED_ANSWERS):
336 raise CommandException(ungettext("This question already has an accepted answer.",
337 "Sorry but this question has reached the limit of accepted answers.", int(settings.MAXIMUM_ACCEPTED_ANSWERS)))
339 if settings.MAXIMUM_ACCEPTED_PER_USER and question.accepted_count:
340 accepted_from_author = question.accepted_answers.filter(author=answer.author).count()
342 if accepted_from_author >= settings.MAXIMUM_ACCEPTED_PER_USER:
343 raise CommandException(ungettext("The author of this answer already has an accepted answer in this question.",
344 "Sorry but the author of this answer has reached the limit of accepted answers per question.", int(settings.MAXIMUM_ACCEPTED_PER_USER)))
347 AcceptAnswerAction(node=answer, user=user, ip=request.META['REMOTE_ADDR']).save()
349 # If the request is not an AJAX redirect to the answer URL rather than to the home page
350 if not request.is_ajax():
352 Congratulations! You've accepted an answer.
355 # Notify the user with a message that an answer has been accepted
356 request.user.message_set.create(message=msg)
358 # Redirect URL should include additional get parameters that might have been attached
359 redirect_url = answer.parent.get_absolute_url() + "?accepted_answer=true&%s" % smart_unicode(urlencode(request.GET))
361 return HttpResponseRedirect(redirect_url)
363 commands['mark_accepted'] = [answer.id]
365 return {'commands': commands}
367 @decorate.withfn(command)
368 def delete_post(request, id):
369 post = get_object_or_404(Node, id=id)
372 if not user.is_authenticated():
373 raise AnonymousNotAllowedException(_('delete posts'))
375 if not (user.can_delete_post(post)):
376 raise NotEnoughRepPointsException(_('delete posts'))
378 ret = {'commands': {}}
381 post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
382 ret['commands']['unmark_deleted'] = [post.node_type, id]
384 DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
386 ret['commands']['mark_deleted'] = [post.node_type, id]
390 @decorate.withfn(command)
391 def close(request, id, close):
392 if close and not request.POST:
393 return render_to_response('node/report.html', {'types': settings.CLOSE_TYPES})
395 question = get_object_or_404(Question, id=id)
398 if not user.is_authenticated():
399 raise AnonymousNotAllowedException(_('close questions'))
401 if question.nis.closed:
402 if not user.can_reopen_question(question):
403 raise NotEnoughRepPointsException(_('reopen questions'))
405 question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
407 if not request.user.can_close_question(question):
408 raise NotEnoughRepPointsException(_('close questions'))
410 reason = request.POST.get('prompt', '').strip()
413 raise CommandException(_("Reason is empty"))
415 CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
417 return RefreshPageCommand()
419 @decorate.withfn(command)
420 def wikify(request, id):
421 node = get_object_or_404(Node, id=id)
424 if not user.is_authenticated():
425 raise AnonymousNotAllowedException(_('mark posts as community wiki'))
428 if not user.can_cancel_wiki(node):
429 raise NotEnoughRepPointsException(_('cancel a community wiki post'))
431 if node.nstate.wiki.action_type == "wikify":
432 node.nstate.wiki.cancel()
434 node.nstate.wiki = None
436 if not user.can_wikify(node):
437 raise NotEnoughRepPointsException(_('mark posts as community wiki'))
439 WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
441 return RefreshPageCommand()
443 @decorate.withfn(command)
444 def convert_to_comment(request, id):
446 answer = get_object_or_404(Answer, id=id)
447 question = answer.question
449 # Check whether the user has the required permissions
450 if not user.is_authenticated():
451 raise AnonymousNotAllowedException(_("convert answers to comments"))
453 if not user.can_convert_to_comment(answer):
454 raise NotEnoughRepPointsException(_("convert answers to comments"))
457 description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': smart_unicode(a.author.username),
458 'snippet': a.summary[:10]}
459 nodes = [(question.id, _("Question"))]
460 [nodes.append((a.id, description(a))) for a in
461 question.answers.filter_state(deleted=False).exclude(id=answer.id)]
463 return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
466 new_parent = Node.objects.get(id=request.POST.get('under', None))
468 raise CommandException(_("That is an invalid post to put the comment under"))
470 if not (new_parent == question or (new_parent.node_type == 'answer' and new_parent.parent == question)):
471 raise CommandException(_("That is an invalid post to put the comment under"))
473 AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
475 return RefreshPageCommand()
477 @decorate.withfn(command)
478 def convert_comment_to_answer(request, id):
480 comment = get_object_or_404(Comment, id=id)
481 parent = comment.parent
483 if not parent.question:
486 question = parent.question
488 if not user.is_authenticated():
489 raise AnonymousNotAllowedException(_("convert comments to answers"))
491 if not user.can_convert_comment_to_answer(comment):
492 raise NotEnoughRepPointsException(_("convert comments to answers"))
494 CommentToAnswerAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(question=question))
496 return RefreshPageCommand()
498 @decorate.withfn(command)
499 def subscribe(request, id, user=None):
502 user = User.objects.get(id=user)
503 except User.DoesNotExist:
506 if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
507 raise CommandException(_("You do not have the correct credentials to preform this action."))
511 question = get_object_or_404(Question, id=id)
514 subscription = QuestionSubscription.objects.get(question=question, user=user)
515 subscription.delete()
518 subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
524 'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
525 'set_subscription_status': ['']
529 #internally grouped views - used by the tagging system
531 def mark_tag(request, tag=None, **kwargs):#tagging system
532 action = kwargs['action']
533 ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
534 if action == 'remove':
535 logging.debug('deleting tag %s' % tag)
538 reason = kwargs['reason']
541 t = Tag.objects.get(name=tag)
542 mt = MarkedTag(user=request.user, reason=reason, tag=t)
547 ts.update(reason=reason)
548 return HttpResponse(simplejson.dumps(''), mimetype="application/json")
550 def matching_tags(request):
551 if len(request.GET['q']) == 0:
552 raise CommandException(_("Invalid request"))
554 possible_tags = Tag.active.filter(name__icontains = request.GET['q'])
556 for tag in possible_tags:
557 tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
559 return HttpResponse(tag_output, mimetype="text/plain")
561 def matching_users(request):
562 if len(request.GET['q']) == 0:
563 raise CommandException(_("Invalid request"))
565 possible_users = User.objects.filter(username__icontains = request.GET['q'])
568 for user in possible_users:
569 output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
571 return HttpResponse(output, mimetype="text/plain")
573 def related_questions(request):
574 if request.POST and request.POST.get('title', None):
575 can_rank, questions = Question.objects.search(request.POST['title'])
577 if can_rank and isinstance(can_rank, basestring):
578 questions = questions.order_by(can_rank)
580 return HttpResponse(simplejson.dumps(
581 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
582 for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")
586 @decorate.withfn(command)
587 def answer_permanent_link(request, id):
588 # Getting the current answer object
589 answer = get_object_or_404(Answer, id=id)
591 # Getting the current object URL -- the Application URL + the object relative URL
592 url = '%s%s' % (settings.APP_BASE_URL, answer.get_absolute_url())
595 # Display the template
596 return render_to_response('node/permanent_link.html', { 'url' : url, })
600 'copy_url' : [request.POST['permanent_link_url'],],
602 'message' : _("The permanent URL to the answer has been copied to your clipboard."),
605 @decorate.withfn(command)
606 def award_points(request, user_id, answer_id):
608 awarded_user = get_object_or_404(User, id=user_id)
609 answer = get_object_or_404(Answer, id=answer_id)
611 # Users shouldn't be able to award themselves
612 if awarded_user.id == user.id:
613 raise CannotDoOnOwnException(_("award"))
615 # Anonymous users cannot award points, they just don't have such
616 if not user.is_authenticated():
617 raise AnonymousNotAllowedException(_('award'))
620 return render_to_response("node/award_points.html", {
622 'awarded_user' : awarded_user,
623 'reputation_to_comment' : str(settings.REP_TO_COMMENT)
626 points = int(request.POST['points'])
628 # We should check if the user has enough reputation points, otherwise we raise an exception.
630 raise CommandException(_("The number of points to award needs to be a positive value."))
632 if user.reputation < points:
633 raise NotEnoughRepPointsException(_("award"))
635 extra = dict(message=request.POST.get('message', ''), awarding_user=request.user.id, value=points)
637 # We take points from the awarding user
638 AwardPointsAction(user=request.user, node=answer, extra=extra).save(data=dict(value=points, affected=awarded_user))
640 return { 'message' : _("You have awarded %(awarded_user)s with %(points)d points") % {'awarded_user' : awarded_user, 'points' : points} }