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):
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 post = get_object_or_404(Node, id=id).leaf
81 if not user.is_authenticated():
82 raise AnonymousNotAllowedException(_('vote'))
84 if user == post.author:
85 raise CannotDoOnOwnException(_('vote'))
87 if not (vote_type == 'up' and user.can_vote_up() or user.can_vote_down()):
88 reputation_required = int(settings.REP_TO_VOTE_UP) if vote_type == 'up' else int(settings.REP_TO_VOTE_DOWN)
89 action_type = vote_type == 'up' and _('upvote') or _('downvote')
90 raise NotEnoughRepPointsException(action_type, user_reputation=user.reputation, reputation_required=reputation_required)
92 user_vote_count_today = user.get_vote_count_today()
93 user_can_vote_count_today = user.can_vote_count_today()
95 if user_vote_count_today >= user.can_vote_count_today():
96 raise NotEnoughLeftException(_('votes'), str(settings.MAX_VOTES_PER_DAY))
98 new_vote_cls = (vote_type == 'up') and VoteUpAction or VoteDownAction
101 old_vote = VoteAction.get_action_for(node=post, user=user)
104 if old_vote.action_date < datetime.datetime.now() - datetime.timedelta(days=int(settings.DENY_UNVOTE_DAYS)):
105 raise CommandException(
106 _("Sorry but you cannot cancel a vote after %(ndays)d %(tdays)s from the original vote") %
107 {'ndays': int(settings.DENY_UNVOTE_DAYS),
108 'tdays': ungettext('day', 'days', int(settings.DENY_UNVOTE_DAYS))}
111 old_vote.cancel(ip=request.META['REMOTE_ADDR'])
112 score_inc = (old_vote.__class__ == VoteDownAction) and 1 or -1
115 new_vote_cls(user=user, node=post, ip=request.META['REMOTE_ADDR']).save()
116 score_inc = (new_vote_cls == VoteUpAction) and 1 or -1
120 'update_post_score': [id, score_inc],
121 'update_user_post_vote': [id, vote_type]
125 votes_left = (user_can_vote_count_today - user_vote_count_today) + (vote_type == 'none' and -1 or 1)
127 if int(settings.START_WARN_VOTES_LEFT) >= votes_left:
128 response['message'] = _("You have %(nvotes)s %(tvotes)s left today.") % \
129 {'nvotes': votes_left, 'tvotes': ungettext('vote', 'votes', votes_left)}
133 @decorate.withfn(command)
134 def flag_post(request, id):
136 return render_to_response('node/report.html', {'types': settings.FLAG_TYPES})
138 post = get_object_or_404(Node, id=id)
141 if not user.is_authenticated():
142 raise AnonymousNotAllowedException(_('flag posts'))
144 if user == post.author:
145 raise CannotDoOnOwnException(_('flag'))
147 if not (user.can_flag_offensive(post)):
148 raise NotEnoughRepPointsException(_('flag posts'))
150 user_flag_count_today = user.get_flagged_items_count_today()
152 if user_flag_count_today >= int(settings.MAX_FLAGS_PER_DAY):
153 raise NotEnoughLeftException(_('flags'), str(settings.MAX_FLAGS_PER_DAY))
156 current = FlagAction.objects.get(canceled=False, user=user, node=post)
157 raise CommandException(
158 _("You already flagged this post with the following reason: %(reason)s") % {'reason': current.extra})
159 except ObjectDoesNotExist:
160 reason = request.POST.get('prompt', '').strip()
163 raise CommandException(_("Reason is empty"))
165 FlagAction(user=user, node=post, extra=reason, ip=request.META['REMOTE_ADDR']).save()
167 return {'message': _("Thank you for your report. A moderator will review your submission shortly.")}
169 @decorate.withfn(command)
170 def like_comment(request, id):
171 comment = get_object_or_404(Comment, id=id)
174 if not user.is_authenticated():
175 raise AnonymousNotAllowedException(_('like comments'))
177 if user == comment.user:
178 raise CannotDoOnOwnException(_('like'))
180 if not user.can_like_comment(comment):
181 raise NotEnoughRepPointsException( _('like comments'))
183 like = VoteAction.get_action_for(node=comment, user=user)
186 like.cancel(ip=request.META['REMOTE_ADDR'])
189 VoteUpCommentAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
194 'update_post_score': [comment.id, likes and 1 or -1],
195 'update_user_post_vote': [comment.id, likes and 'up' or 'none']
199 @decorate.withfn(command)
200 def delete_comment(request, id):
201 comment = get_object_or_404(Comment, id=id)
204 if not user.is_authenticated():
205 raise AnonymousNotAllowedException(_('delete comments'))
207 if not user.can_delete_comment(comment):
208 raise NotEnoughRepPointsException( _('delete comments'))
210 if not comment.nis.deleted:
211 DeleteAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
215 'remove_comment': [comment.id],
219 @decorate.withfn(command)
220 def mark_favorite(request, id):
221 question = get_object_or_404(Question, id=id)
223 if not request.user.is_authenticated():
224 raise AnonymousNotAllowedException(_('mark a question as favorite'))
227 favorite = FavoriteAction.objects.get(canceled=False, node=question, user=request.user)
228 favorite.cancel(ip=request.META['REMOTE_ADDR'])
230 except ObjectDoesNotExist:
231 FavoriteAction(node=question, user=request.user, ip=request.META['REMOTE_ADDR']).save()
236 'update_favorite_count': [added and 1 or -1],
237 'update_favorite_mark': [added and 'on' or 'off']
241 @decorate.withfn(command)
242 def comment(request, id):
243 post = get_object_or_404(Node, id=id)
246 if not user.is_authenticated():
247 raise AnonymousNotAllowedException(_('comment'))
249 if not request.method == 'POST':
250 raise CommandException(_("Invalid request"))
252 comment_text = request.POST.get('comment', '').strip()
254 if not len(comment_text):
255 raise CommandException(_("Comment is empty"))
257 if len(comment_text) < settings.FORM_MIN_COMMENT_BODY:
258 raise CommandException(_("At least %d characters required on comment body.") % settings.FORM_MIN_COMMENT_BODY)
260 if len(comment_text) > settings.FORM_MAX_COMMENT_BODY:
261 raise CommandException(_("No more than %d characters on comment body.") % settings.FORM_MAX_COMMENT_BODY)
263 if 'id' in request.POST:
264 comment = get_object_or_404(Comment, id=request.POST['id'])
266 if not user.can_edit_comment(comment):
267 raise NotEnoughRepPointsException( _('edit comments'))
269 comment = ReviseAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(
270 data=dict(text=comment_text)).node
272 if not user.can_comment(post):
273 raise NotEnoughRepPointsException( _('comment'))
275 comment = CommentAction(user=user, ip=request.META['REMOTE_ADDR']).save(
276 data=dict(text=comment_text, parent=post)).node
278 if comment.active_revision.revision == 1:
282 id, comment.id, comment.comment, user.decorated_name, user.get_profile_url(),
283 reverse('delete_comment', kwargs={'id': comment.id}),
284 reverse('node_markdown', kwargs={'id': comment.id}),
285 reverse('convert_comment', kwargs={'id': comment.id}),
286 user.can_convert_comment_to_answer(comment),
293 'update_comment': [comment.id, comment.comment]
297 @decorate.withfn(command)
298 def node_markdown(request, id):
301 if not user.is_authenticated():
302 raise AnonymousNotAllowedException(_('accept answers'))
304 node = get_object_or_404(Node, id=id)
305 return HttpResponse(node.active_revision.body, mimetype="text/plain")
308 @decorate.withfn(command)
309 def accept_answer(request, id):
310 if settings.DISABLE_ACCEPTING_FEATURE:
315 if not user.is_authenticated():
316 raise AnonymousNotAllowedException(_('accept answers'))
318 answer = get_object_or_404(Answer, id=id)
319 question = answer.question
321 if not user.can_accept_answer(answer):
322 raise CommandException(_("Sorry but you cannot accept the answer"))
326 if answer.nis.accepted:
327 answer.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR'])
328 commands['unmark_accepted'] = [answer.id]
330 if settings.MAXIMUM_ACCEPTED_ANSWERS and (question.accepted_count >= settings.MAXIMUM_ACCEPTED_ANSWERS):
331 raise CommandException(ungettext("This question already has an accepted answer.",
332 "Sorry but this question has reached the limit of accepted answers.", int(settings.MAXIMUM_ACCEPTED_ANSWERS)))
334 if settings.MAXIMUM_ACCEPTED_PER_USER and question.accepted_count:
335 accepted_from_author = question.accepted_answers.filter(author=answer.author).count()
337 if accepted_from_author >= settings.MAXIMUM_ACCEPTED_PER_USER:
338 raise CommandException(ungettext("The author of this answer already has an accepted answer in this question.",
339 "Sorry but the author of this answer has reached the limit of accepted answers per question.", int(settings.MAXIMUM_ACCEPTED_PER_USER)))
342 AcceptAnswerAction(node=answer, user=user, ip=request.META['REMOTE_ADDR']).save()
344 # If the request is not an AJAX redirect to the answer URL rather than to the home page
345 if not request.is_ajax():
347 Congratulations! You've accepted an answer.
350 # Notify the user with a message that an answer has been accepted
351 request.user.message_set.create(message=msg)
353 # Redirect URL should include additional get parameters that might have been attached
354 redirect_url = answer.parent.get_absolute_url() + "?accepted_answer=true&%s" % smart_unicode(urlencode(request.GET))
356 return HttpResponseRedirect(redirect_url)
358 commands['mark_accepted'] = [answer.id]
360 return {'commands': commands}
362 @decorate.withfn(command)
363 def delete_post(request, id):
364 post = get_object_or_404(Node, id=id)
367 if not user.is_authenticated():
368 raise AnonymousNotAllowedException(_('delete posts'))
370 if not (user.can_delete_post(post)):
371 raise NotEnoughRepPointsException(_('delete posts'))
373 ret = {'commands': {}}
376 post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
377 ret['commands']['unmark_deleted'] = [post.node_type, id]
379 DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
381 ret['commands']['mark_deleted'] = [post.node_type, id]
385 @decorate.withfn(command)
386 def close(request, id, close):
387 if close and not request.POST:
388 return render_to_response('node/report.html', {'types': settings.CLOSE_TYPES})
390 question = get_object_or_404(Question, id=id)
393 if not user.is_authenticated():
394 raise AnonymousNotAllowedException(_('close questions'))
396 if question.nis.closed:
397 if not user.can_reopen_question(question):
398 raise NotEnoughRepPointsException(_('reopen questions'))
400 question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
402 if not request.user.can_close_question(question):
403 raise NotEnoughRepPointsException(_('close questions'))
405 reason = request.POST.get('prompt', '').strip()
408 raise CommandException(_("Reason is empty"))
410 CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
412 return RefreshPageCommand()
414 @decorate.withfn(command)
415 def wikify(request, id):
416 node = get_object_or_404(Node, id=id)
419 if not user.is_authenticated():
420 raise AnonymousNotAllowedException(_('mark posts as community wiki'))
423 if not user.can_cancel_wiki(node):
424 raise NotEnoughRepPointsException(_('cancel a community wiki post'))
426 if node.nstate.wiki.action_type == "wikify":
427 node.nstate.wiki.cancel()
429 node.nstate.wiki = None
431 if not user.can_wikify(node):
432 raise NotEnoughRepPointsException(_('mark posts as community wiki'))
434 WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
436 return RefreshPageCommand()
438 @decorate.withfn(command)
439 def convert_to_comment(request, id):
441 answer = get_object_or_404(Answer, id=id)
442 question = answer.question
444 # Check whether the user has the required permissions
445 if not user.is_authenticated():
446 raise AnonymousNotAllowedException(_("convert answers to comments"))
448 if not user.can_convert_to_comment(answer):
449 raise NotEnoughRepPointsException(_("convert answers to comments"))
452 description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': smart_unicode(a.author.username),
453 'snippet': a.summary[:10]}
454 nodes = [(question.id, _("Question"))]
455 [nodes.append((a.id, description(a))) for a in
456 question.answers.filter_state(deleted=False).exclude(id=answer.id)]
458 return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
461 new_parent = Node.objects.get(id=request.POST.get('under', None))
463 raise CommandException(_("That is an invalid post to put the comment under"))
465 if not (new_parent == question or (new_parent.node_type == 'answer' and new_parent.parent == question)):
466 raise CommandException(_("That is an invalid post to put the comment under"))
468 AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
470 return RefreshPageCommand()
472 @decorate.withfn(command)
473 def convert_comment_to_answer(request, id):
475 comment = get_object_or_404(Comment, id=id)
476 parent = comment.parent
478 if not parent.question:
481 question = parent.question
483 if not user.is_authenticated():
484 raise AnonymousNotAllowedException(_("convert comments to answers"))
486 if not user.can_convert_comment_to_answer(comment):
487 raise NotEnoughRepPointsException(_("convert comments to answers"))
489 CommentToAnswerAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(question=question))
491 return RefreshPageCommand()
493 @decorate.withfn(command)
494 def subscribe(request, id, user=None):
497 user = User.objects.get(id=user)
498 except User.DoesNotExist:
501 if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
502 raise CommandException(_("You do not have the correct credentials to preform this action."))
506 question = get_object_or_404(Question, id=id)
509 subscription = QuestionSubscription.objects.get(question=question, user=user)
510 subscription.delete()
513 subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
519 'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
520 'set_subscription_status': ['']
524 #internally grouped views - used by the tagging system
526 def mark_tag(request, tag=None, **kwargs):#tagging system
527 action = kwargs['action']
528 ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
529 if action == 'remove':
530 logging.debug('deleting tag %s' % tag)
533 reason = kwargs['reason']
536 t = Tag.objects.get(name=tag)
537 mt = MarkedTag(user=request.user, reason=reason, tag=t)
542 ts.update(reason=reason)
543 return HttpResponse(simplejson.dumps(''), mimetype="application/json")
545 def matching_tags(request):
546 if len(request.GET['q']) == 0:
547 raise CommandException(_("Invalid request"))
549 possible_tags = Tag.active.filter(name__icontains = request.GET['q'])
551 for tag in possible_tags:
552 tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
554 return HttpResponse(tag_output, mimetype="text/plain")
556 def matching_users(request):
557 if len(request.GET['q']) == 0:
558 raise CommandException(_("Invalid request"))
560 possible_users = User.objects.filter(username__icontains = request.GET['q'])
563 for user in possible_users:
564 output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
566 return HttpResponse(output, mimetype="text/plain")
568 def related_questions(request):
569 if request.POST and request.POST.get('title', None):
570 can_rank, questions = Question.objects.search(request.POST['title'])
572 if can_rank and isinstance(can_rank, basestring):
573 questions = questions.order_by(can_rank)
575 return HttpResponse(simplejson.dumps(
576 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
577 for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")
581 @decorate.withfn(command)
582 def answer_permanent_link(request, id):
583 # Getting the current answer object
584 answer = get_object_or_404(Answer, id=id)
586 # Getting the current object URL -- the Application URL + the object relative URL
587 url = '%s%s' % (settings.APP_BASE_URL, answer.get_absolute_url())
590 # Display the template
591 return render_to_response('node/permanent_link.html', { 'url' : url, })
595 'copy_url' : [request.POST['permanent_link_url'],],
597 'message' : _("The permanent URL to the answer has been copied to your clipboard."),
600 @decorate.withfn(command)
601 def award_points(request, user_id, answer_id):
603 awarded_user = get_object_or_404(User, id=user_id)
604 answer = get_object_or_404(Answer, id=answer_id)
606 # Users shouldn't be able to award themselves
607 if awarded_user.id == user.id:
608 raise CannotDoOnOwnException(_("award"))
610 # Anonymous users cannot award points, they just don't have such
611 if not user.is_authenticated():
612 raise AnonymousNotAllowedException(_('award'))
615 return render_to_response("node/award_points.html", { 'user' : user, 'awarded_user' : awarded_user, })
617 points = int(request.POST['points'])
619 # We should check if the user has enough reputation points, otherwise we raise an exception.
621 raise CommandException(_("The number of points to award needs to be a positive value."))
623 if user.reputation < points:
624 raise NotEnoughRepPointsException(_("award"))
626 extra = dict(message=request.POST.get('message', ''), awarding_user=request.user.id, value=points)
628 # We take points from the awarding user
629 AwardPointsAction(user=request.user, node=answer, extra=extra).save(data=dict(value=points, affected=awarded_user))
631 return { 'message' : _("You have awarded %(awarded_user)s with %(points)d points") % {'awarded_user' : awarded_user, 'points' : points} }