1 # -*- coding: utf-8 -*-
6 from django.core.exceptions import ObjectDoesNotExist
7 from django.core.urlresolvers import reverse
8 from django.utils import simplejson
9 from django.utils.encoding import smart_unicode
10 from django.utils.translation import ungettext, ugettext as _
11 from django.http import HttpResponse, HttpResponseRedirect, Http404
12 from django.shortcuts import get_object_or_404, render_to_response
14 from forum.models import *
15 from forum.utils.decorators import ajax_login_required
16 from forum.actions import *
17 from forum.modules import decorate
18 from forum import settings
20 from decorators import command, CommandException, RefreshPageCommand
22 class NotEnoughRepPointsException(CommandException):
23 def __init__(self, action, user_reputation=None, reputation_required=None):
24 if reputation_required is not None and user_reputation is not None:
26 """Sorry, but you don't have enough reputation points to %(action)s.<br />
27 The minimum reputation required is %(reputation_required)d (yours is %(user_reputation)d).
28 Please check the <a href='%(faq_url)s'>FAQ</a>"""
31 'faq_url': reverse('faq'),
32 'reputation_required' : reputation_required,
33 'user_reputation' : user_reputation,
37 """Sorry, but you don't have enough reputation points to %(action)s.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
38 ) % {'action': action, 'faq_url': reverse('faq')}
39 super(NotEnoughRepPointsException, self).__init__(message)
41 class CannotDoOnOwnException(CommandException):
42 def __init__(self, action):
43 super(CannotDoOnOwnException, self).__init__(
45 """Sorry but you cannot %(action)s your own post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
46 ) % {'action': action, 'faq_url': reverse('faq')}
49 class AnonymousNotAllowedException(CommandException):
50 def __init__(self, action):
51 super(AnonymousNotAllowedException, self).__init__(
53 """Sorry but anonymous users cannot %(action)s.<br />Please login or create an account <a href='%(signin_url)s'>here</a>."""
54 ) % {'action': action, 'signin_url': reverse('auth_signin')}
57 class NotEnoughLeftException(CommandException):
58 def __init__(self, action, limit):
59 super(NotEnoughLeftException, self).__init__(
61 """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>"""
62 ) % {'action': action, 'limit': limit, 'faq_url': reverse('faq')}
65 class CannotDoubleActionException(CommandException):
66 def __init__(self, action):
67 super(CannotDoubleActionException, self).__init__(
69 """Sorry, but you cannot %(action)s twice the same post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
70 ) % {'action': action, 'faq_url': reverse('faq')}
74 @decorate.withfn(command)
75 def vote_post(request, id, vote_type):
76 post = get_object_or_404(Node, id=id).leaf
79 if not user.is_authenticated():
80 raise AnonymousNotAllowedException(_('vote'))
82 if user == post.author:
83 raise CannotDoOnOwnException(_('vote'))
85 if not (vote_type == 'up' and user.can_vote_up() or user.can_vote_down()):
86 reputation_required = int(settings.REP_TO_VOTE_UP) if vote_type == 'up' else int(settings.REP_TO_VOTE_DOWN)
87 action_type = vote_type == 'up' and _('upvote') or _('downvote')
88 raise NotEnoughRepPointsException(action_type, user_reputation=user.reputation, reputation_required=reputation_required)
90 user_vote_count_today = user.get_vote_count_today()
91 user_can_vote_count_today = user.can_vote_count_today()
93 if user_vote_count_today >= user.can_vote_count_today():
94 raise NotEnoughLeftException(_('votes'), str(settings.MAX_VOTES_PER_DAY))
96 new_vote_cls = (vote_type == 'up') and VoteUpAction or VoteDownAction
99 old_vote = VoteAction.get_action_for(node=post, user=user)
102 if old_vote.action_date < datetime.datetime.now() - datetime.timedelta(days=int(settings.DENY_UNVOTE_DAYS)):
103 raise CommandException(
104 _("Sorry but you cannot cancel a vote after %(ndays)d %(tdays)s from the original vote") %
105 {'ndays': int(settings.DENY_UNVOTE_DAYS),
106 'tdays': ungettext('day', 'days', int(settings.DENY_UNVOTE_DAYS))}
109 old_vote.cancel(ip=request.META['REMOTE_ADDR'])
110 score_inc = (old_vote.__class__ == VoteDownAction) and 1 or -1
113 new_vote_cls(user=user, node=post, ip=request.META['REMOTE_ADDR']).save()
114 score_inc = (new_vote_cls == VoteUpAction) and 1 or -1
118 'update_post_score': [id, score_inc],
119 'update_user_post_vote': [id, vote_type]
123 votes_left = (user_can_vote_count_today - user_vote_count_today) + (vote_type == 'none' and -1 or 1)
125 if int(settings.START_WARN_VOTES_LEFT) >= votes_left:
126 response['message'] = _("You have %(nvotes)s %(tvotes)s left today.") % \
127 {'nvotes': votes_left, 'tvotes': ungettext('vote', 'votes', votes_left)}
131 @decorate.withfn(command)
132 def flag_post(request, id):
134 return render_to_response('node/report.html', {'types': settings.FLAG_TYPES})
136 post = get_object_or_404(Node, id=id)
139 if not user.is_authenticated():
140 raise AnonymousNotAllowedException(_('flag posts'))
142 if user == post.author:
143 raise CannotDoOnOwnException(_('flag'))
145 if not (user.can_flag_offensive(post)):
146 raise NotEnoughRepPointsException(_('flag posts'))
148 user_flag_count_today = user.get_flagged_items_count_today()
150 if user_flag_count_today >= int(settings.MAX_FLAGS_PER_DAY):
151 raise NotEnoughLeftException(_('flags'), str(settings.MAX_FLAGS_PER_DAY))
154 current = FlagAction.objects.get(canceled=False, user=user, node=post)
155 raise CommandException(
156 _("You already flagged this post with the following reason: %(reason)s") % {'reason': current.extra})
157 except ObjectDoesNotExist:
158 reason = request.POST.get('prompt', '').strip()
161 raise CommandException(_("Reason is empty"))
163 FlagAction(user=user, node=post, extra=reason, ip=request.META['REMOTE_ADDR']).save()
165 return {'message': _("Thank you for your report. A moderator will review your submission shortly.")}
167 @decorate.withfn(command)
168 def like_comment(request, id):
169 comment = get_object_or_404(Comment, id=id)
172 if not user.is_authenticated():
173 raise AnonymousNotAllowedException(_('like comments'))
175 if user == comment.user:
176 raise CannotDoOnOwnException(_('like'))
178 if not user.can_like_comment(comment):
179 raise NotEnoughRepPointsException( _('like comments'))
181 like = VoteAction.get_action_for(node=comment, user=user)
184 like.cancel(ip=request.META['REMOTE_ADDR'])
187 VoteUpCommentAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
192 'update_post_score': [comment.id, likes and 1 or -1],
193 'update_user_post_vote': [comment.id, likes and 'up' or 'none']
197 @decorate.withfn(command)
198 def delete_comment(request, id):
199 comment = get_object_or_404(Comment, id=id)
202 if not user.is_authenticated():
203 raise AnonymousNotAllowedException(_('delete comments'))
205 if not user.can_delete_comment(comment):
206 raise NotEnoughRepPointsException( _('delete comments'))
208 if not comment.nis.deleted:
209 DeleteAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
213 'remove_comment': [comment.id],
217 @decorate.withfn(command)
218 def mark_favorite(request, id):
219 question = get_object_or_404(Question, id=id)
221 if not request.user.is_authenticated():
222 raise AnonymousNotAllowedException(_('mark a question as favorite'))
225 favorite = FavoriteAction.objects.get(canceled=False, node=question, user=request.user)
226 favorite.cancel(ip=request.META['REMOTE_ADDR'])
228 except ObjectDoesNotExist:
229 FavoriteAction(node=question, user=request.user, ip=request.META['REMOTE_ADDR']).save()
234 'update_favorite_count': [added and 1 or -1],
235 'update_favorite_mark': [added and 'on' or 'off']
239 @decorate.withfn(command)
240 def comment(request, id):
241 post = get_object_or_404(Node, id=id)
244 if not user.is_authenticated():
245 raise AnonymousNotAllowedException(_('comment'))
247 if not request.method == 'POST':
248 raise CommandException(_("Invalid request"))
250 comment_text = request.POST.get('comment', '').strip()
252 if not len(comment_text):
253 raise CommandException(_("Comment is empty"))
255 if len(comment_text) < settings.FORM_MIN_COMMENT_BODY:
256 raise CommandException(_("At least %d characters required on comment body.") % settings.FORM_MIN_COMMENT_BODY)
258 if len(comment_text) > settings.FORM_MAX_COMMENT_BODY:
259 raise CommandException(_("No more than %d characters on comment body.") % settings.FORM_MAX_COMMENT_BODY)
261 if 'id' in request.POST:
262 comment = get_object_or_404(Comment, id=request.POST['id'])
264 if not user.can_edit_comment(comment):
265 raise NotEnoughRepPointsException( _('edit comments'))
267 comment = ReviseAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(
268 data=dict(text=comment_text)).node
270 if not user.can_comment(post):
271 raise NotEnoughRepPointsException( _('comment'))
273 comment = CommentAction(user=user, ip=request.META['REMOTE_ADDR']).save(
274 data=dict(text=comment_text, parent=post)).node
276 if comment.active_revision.revision == 1:
280 id, comment.id, comment.comment, user.decorated_name, user.get_profile_url(),
281 reverse('delete_comment', kwargs={'id': comment.id}),
282 reverse('node_markdown', kwargs={'id': comment.id}),
283 reverse('convert_comment', kwargs={'id': comment.id}),
284 user.can_convert_comment_to_answer(comment),
291 'update_comment': [comment.id, comment.comment]
295 @decorate.withfn(command)
296 def node_markdown(request, id):
299 if not user.is_authenticated():
300 raise AnonymousNotAllowedException(_('accept answers'))
302 node = get_object_or_404(Node, id=id)
303 return HttpResponse(node.active_revision.body, mimetype="text/plain")
306 @decorate.withfn(command)
307 def accept_answer(request, id):
308 if settings.DISABLE_ACCEPTING_FEATURE:
313 if not user.is_authenticated():
314 raise AnonymousNotAllowedException(_('accept answers'))
316 answer = get_object_or_404(Answer, id=id)
317 question = answer.question
319 if not user.can_accept_answer(answer):
320 raise CommandException(_("Sorry but you cannot accept the answer"))
324 if answer.nis.accepted:
325 answer.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR'])
326 commands['unmark_accepted'] = [answer.id]
328 if settings.MAXIMUM_ACCEPTED_ANSWERS and (question.accepted_count >= settings.MAXIMUM_ACCEPTED_ANSWERS):
329 raise CommandException(ungettext("This question already has an accepted answer.",
330 "Sorry but this question has reached the limit of accepted answers.", int(settings.MAXIMUM_ACCEPTED_ANSWERS)))
332 if settings.MAXIMUM_ACCEPTED_PER_USER and question.accepted_count:
333 accepted_from_author = question.accepted_answers.filter(author=answer.author).count()
335 if accepted_from_author >= settings.MAXIMUM_ACCEPTED_PER_USER:
336 raise CommandException(ungettext("The author of this answer already has an accepted answer in this question.",
337 "Sorry but the author of this answer has reached the limit of accepted answers per question.", int(settings.MAXIMUM_ACCEPTED_PER_USER)))
340 AcceptAnswerAction(node=answer, user=user, ip=request.META['REMOTE_ADDR']).save()
342 # If the request is not an AJAX redirect to the answer URL rather than to the home page
343 if not request.is_ajax():
344 return HttpResponseRedirect(answer.get_absolute_url())
346 commands['mark_accepted'] = [answer.id]
348 return {'commands': commands}
350 @decorate.withfn(command)
351 def delete_post(request, id):
352 post = get_object_or_404(Node, id=id)
355 if not user.is_authenticated():
356 raise AnonymousNotAllowedException(_('delete posts'))
358 if not (user.can_delete_post(post)):
359 raise NotEnoughRepPointsException(_('delete posts'))
361 ret = {'commands': {}}
364 post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
365 ret['commands']['unmark_deleted'] = [post.node_type, id]
367 DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
369 ret['commands']['mark_deleted'] = [post.node_type, id]
373 @decorate.withfn(command)
374 def close(request, id, close):
375 if close and not request.POST:
376 return render_to_response('node/report.html', {'types': settings.CLOSE_TYPES})
378 question = get_object_or_404(Question, id=id)
381 if not user.is_authenticated():
382 raise AnonymousNotAllowedException(_('close questions'))
384 if question.nis.closed:
385 if not user.can_reopen_question(question):
386 raise NotEnoughRepPointsException(_('reopen questions'))
388 question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
390 if not request.user.can_close_question(question):
391 raise NotEnoughRepPointsException(_('close questions'))
393 reason = request.POST.get('prompt', '').strip()
396 raise CommandException(_("Reason is empty"))
398 CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
400 return RefreshPageCommand()
402 @decorate.withfn(command)
403 def wikify(request, id):
404 node = get_object_or_404(Node, id=id)
407 if not user.is_authenticated():
408 raise AnonymousNotAllowedException(_('mark posts as community wiki'))
411 if not user.can_cancel_wiki(node):
412 raise NotEnoughRepPointsException(_('cancel a community wiki post'))
414 if node.nstate.wiki.action_type == "wikify":
415 node.nstate.wiki.cancel()
417 node.nstate.wiki = None
419 if not user.can_wikify(node):
420 raise NotEnoughRepPointsException(_('mark posts as community wiki'))
422 WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
424 return RefreshPageCommand()
426 @decorate.withfn(command)
427 def convert_to_comment(request, id):
429 answer = get_object_or_404(Answer, id=id)
430 question = answer.question
432 # Check whether the user has the required permissions
433 if not user.is_authenticated():
434 raise AnonymousNotAllowedException(_("convert answers to comments"))
436 if not user.can_convert_to_comment(answer):
437 raise NotEnoughRepPointsException(_("convert answers to comments"))
440 description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': smart_unicode(a.author.username),
441 'snippet': a.summary[:10]}
442 nodes = [(question.id, _("Question"))]
443 [nodes.append((a.id, description(a))) for a in
444 question.answers.filter_state(deleted=False).exclude(id=answer.id)]
446 return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
449 new_parent = Node.objects.get(id=request.POST.get('under', None))
451 raise CommandException(_("That is an invalid post to put the comment under"))
453 if not (new_parent == question or (new_parent.node_type == 'answer' and new_parent.parent == question)):
454 raise CommandException(_("That is an invalid post to put the comment under"))
456 AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
458 return RefreshPageCommand()
460 @decorate.withfn(command)
461 def convert_comment_to_answer(request, id):
463 comment = get_object_or_404(Comment, id=id)
464 parent = comment.parent
466 if not parent.question:
469 question = parent.question
471 if not user.is_authenticated():
472 raise AnonymousNotAllowedException(_("convert comments to answers"))
474 if not user.can_convert_comment_to_answer(comment):
475 raise NotEnoughRepPointsException(_("convert comments to answers"))
477 CommentToAnswerAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(question=question))
479 return RefreshPageCommand()
481 @decorate.withfn(command)
482 def subscribe(request, id, user=None):
485 user = User.objects.get(id=user)
486 except User.DoesNotExist:
489 if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
490 raise CommandException(_("You do not have the correct credentials to preform this action."))
494 question = get_object_or_404(Question, id=id)
497 subscription = QuestionSubscription.objects.get(question=question, user=user)
498 subscription.delete()
501 subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
507 'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
508 'set_subscription_status': ['']
512 #internally grouped views - used by the tagging system
514 def mark_tag(request, tag=None, **kwargs):#tagging system
515 action = kwargs['action']
516 ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
517 if action == 'remove':
518 logging.debug('deleting tag %s' % tag)
521 reason = kwargs['reason']
524 t = Tag.objects.get(name=tag)
525 mt = MarkedTag(user=request.user, reason=reason, tag=t)
530 ts.update(reason=reason)
531 return HttpResponse(simplejson.dumps(''), mimetype="application/json")
533 def matching_tags(request):
534 if len(request.GET['q']) == 0:
535 raise CommandException(_("Invalid request"))
537 possible_tags = Tag.active.filter(name__icontains = request.GET['q'])
539 for tag in possible_tags:
540 tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
542 return HttpResponse(tag_output, mimetype="text/plain")
544 def matching_users(request):
545 if len(request.GET['q']) == 0:
546 raise CommandException(_("Invalid request"))
548 possible_users = User.objects.filter(username__icontains = request.GET['q'])
551 for user in possible_users:
552 output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
554 return HttpResponse(output, mimetype="text/plain")
556 def related_questions(request):
557 if request.POST and request.POST.get('title', None):
558 can_rank, questions = Question.objects.search(request.POST['title'])
560 if can_rank and isinstance(can_rank, basestring):
561 questions = questions.order_by(can_rank)
563 return HttpResponse(simplejson.dumps(
564 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
565 for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")
569 @decorate.withfn(command)
570 def answer_permanent_link(request, id):
571 # Getting the current answer object
572 answer = get_object_or_404(Answer, id=id)
574 # Getting the current object URL -- the Application URL + the object relative URL
575 url = '%s%s' % (settings.APP_BASE_URL, answer.get_absolute_url())
578 # Display the template
579 return render_to_response('node/permanent_link.html', { 'url' : url, })
583 'copy_url' : [request.POST['permanent_link_url'],],
585 'message' : _("The permanent URL to the answer has been copied to your clipboard."),
588 @decorate.withfn(command)
589 def award_points(request, user_id, answer_id):
591 awarded_user = get_object_or_404(User, id=user_id)
592 answer = get_object_or_404(Answer, id=answer_id)
594 # Users shouldn't be able to award themselves
595 if awarded_user.id == user.id:
596 raise CannotDoOnOwnException(_("award"))
598 # Anonymous users cannot award points, they just don't have such
599 if not user.is_authenticated():
600 raise AnonymousNotAllowedException(_('award'))
603 return render_to_response("node/award_points.html", { 'user' : user, 'awarded_user' : awarded_user, })
605 points = int(request.POST['points'])
607 # We should check if the user has enough reputation points, otherwise we raise an exception.
609 raise CommandException(_("The number of points to award needs to be a positive value."))
611 if user.reputation < points:
612 raise NotEnoughRepPointsException(_("award"))
614 extra = dict(message=request.POST.get('message', ''), awarding_user=request.user.id, value=points)
616 # We take points from the awarding user
617 AwardPointsAction(user=request.user, node=answer, extra=extra).save(data=dict(value=points, affected=awarded_user))
619 return { 'message' : _("You have awarded %(awarded_user)s with %(points)d points") % {'awarded_user' : awarded_user, 'points' : points} }