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, 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()
341 commands['mark_accepted'] = [answer.id]
343 return {'commands': commands}
345 @decorate.withfn(command)
346 def delete_post(request, id):
347 post = get_object_or_404(Node, id=id)
350 if not user.is_authenticated():
351 raise AnonymousNotAllowedException(_('delete posts'))
353 if not (user.can_delete_post(post)):
354 raise NotEnoughRepPointsException(_('delete posts'))
356 ret = {'commands': {}}
359 post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
360 ret['commands']['unmark_deleted'] = [post.node_type, id]
362 DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
364 ret['commands']['mark_deleted'] = [post.node_type, id]
368 @decorate.withfn(command)
369 def close(request, id, close):
370 if close and not request.POST:
371 return render_to_response('node/report.html', {'types': settings.CLOSE_TYPES})
373 question = get_object_or_404(Question, id=id)
376 if not user.is_authenticated():
377 raise AnonymousNotAllowedException(_('close questions'))
379 if question.nis.closed:
380 if not user.can_reopen_question(question):
381 raise NotEnoughRepPointsException(_('reopen questions'))
383 question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
385 if not request.user.can_close_question(question):
386 raise NotEnoughRepPointsException(_('close questions'))
388 reason = request.POST.get('prompt', '').strip()
391 raise CommandException(_("Reason is empty"))
393 CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
395 return RefreshPageCommand()
397 @decorate.withfn(command)
398 def wikify(request, id):
399 node = get_object_or_404(Node, id=id)
402 if not user.is_authenticated():
403 raise AnonymousNotAllowedException(_('mark posts as community wiki'))
406 if not user.can_cancel_wiki(node):
407 raise NotEnoughRepPointsException(_('cancel a community wiki post'))
409 if node.nstate.wiki.action_type == "wikify":
410 node.nstate.wiki.cancel()
412 node.nstate.wiki = None
414 if not user.can_wikify(node):
415 raise NotEnoughRepPointsException(_('mark posts as community wiki'))
417 WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
419 return RefreshPageCommand()
421 @decorate.withfn(command)
422 def convert_to_comment(request, id):
424 answer = get_object_or_404(Answer, id=id)
425 question = answer.question
427 # Check whether the user has the required permissions
428 if not user.is_authenticated():
429 raise AnonymousNotAllowedException(_("convert answers to comments"))
431 if not user.can_convert_to_comment(answer):
432 raise NotEnoughRepPointsException(_("convert answers to comments"))
435 description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': smart_unicode(a.author.username),
436 'snippet': a.summary[:10]}
437 nodes = [(question.id, _("Question"))]
438 [nodes.append((a.id, description(a))) for a in
439 question.answers.filter_state(deleted=False).exclude(id=answer.id)]
441 return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
444 new_parent = Node.objects.get(id=request.POST.get('under', None))
446 raise CommandException(_("That is an invalid post to put the comment under"))
448 if not (new_parent == question or (new_parent.node_type == 'answer' and new_parent.parent == question)):
449 raise CommandException(_("That is an invalid post to put the comment under"))
451 AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
453 return RefreshPageCommand()
455 @decorate.withfn(command)
456 def convert_comment_to_answer(request, id):
458 comment = get_object_or_404(Comment, id=id)
459 parent = comment.parent
461 if not parent.question:
464 question = parent.question
466 if not user.is_authenticated():
467 raise AnonymousNotAllowedException(_("convert comments to answers"))
469 if not user.can_convert_comment_to_answer(comment):
470 raise NotEnoughRepPointsException(_("convert comments to answers"))
472 CommentToAnswerAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(question=question))
474 return RefreshPageCommand()
476 @decorate.withfn(command)
477 def subscribe(request, id, user=None):
480 user = User.objects.get(id=user)
481 except User.DoesNotExist:
484 if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
485 raise CommandException(_("You do not have the correct credentials to preform this action."))
489 question = get_object_or_404(Question, id=id)
492 subscription = QuestionSubscription.objects.get(question=question, user=user)
493 subscription.delete()
496 subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
502 'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
503 'set_subscription_status': ['']
507 #internally grouped views - used by the tagging system
509 def mark_tag(request, tag=None, **kwargs):#tagging system
510 action = kwargs['action']
511 ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
512 if action == 'remove':
513 logging.debug('deleting tag %s' % tag)
516 reason = kwargs['reason']
519 t = Tag.objects.get(name=tag)
520 mt = MarkedTag(user=request.user, reason=reason, tag=t)
525 ts.update(reason=reason)
526 return HttpResponse(simplejson.dumps(''), mimetype="application/json")
528 def matching_tags(request):
529 if len(request.GET['q']) == 0:
530 raise CommandException(_("Invalid request"))
532 possible_tags = Tag.active.filter(name__icontains = request.GET['q'])
534 for tag in possible_tags:
535 tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
537 return HttpResponse(tag_output, mimetype="text/plain")
539 def matching_users(request):
540 if len(request.GET['q']) == 0:
541 raise CommandException(_("Invalid request"))
543 possible_users = User.objects.filter(username__icontains = request.GET['q'])
546 for user in possible_users:
547 output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
549 return HttpResponse(output, mimetype="text/plain")
551 def related_questions(request):
552 if request.POST and request.POST.get('title', None):
553 can_rank, questions = Question.objects.search(request.POST['title'])
555 if can_rank and isinstance(can_rank, basestring):
556 questions = questions.order_by(can_rank)
558 return HttpResponse(simplejson.dumps(
559 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
560 for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")
564 @decorate.withfn(command)
565 def answer_permanent_link(request, id):
566 # Getting the current answer object
567 answer = get_object_or_404(Answer, id=id)
569 # Getting the current object URL -- the Application URL + the object relative URL
570 url = '%s%s' % (settings.APP_BASE_URL, answer.get_absolute_url())
573 # Display the template
574 return render_to_response('node/permanent_link.html', { 'url' : url, })
578 'copy_url' : [request.POST['permanent_link_url'],],
580 'message' : _("The permanent URL to the answer has been copied to your clipboard."),
583 @decorate.withfn(command)
584 def award_points(request, user_id, answer_id):
586 awarded_user = get_object_or_404(User, id=user_id)
587 answer = get_object_or_404(Answer, id=answer_id)
589 # Users shouldn't be able to award themselves
590 if awarded_user.id == user.id:
591 raise CannotDoOnOwnException(_("award"))
593 # Anonymous users cannot award points, they just don't have such
594 if not user.is_authenticated():
595 raise AnonymousNotAllowedException(_('award'))
598 return render_to_response("node/award_points.html", { 'user' : user, 'awarded_user' : awarded_user, })
600 points = int(request.POST['points'])
602 # We should check if the user has enough reputation points, otherwise we raise an exception.
604 raise CommandException(_("The number of points to award needs to be a positive value."))
606 if user.reputation < points:
607 raise NotEnoughRepPointsException(_("award"))
609 extra = dict(message=request.POST.get('message', ''), awarding_user=request.user.id, value=points)
611 # We take points from the awarding user
612 AwardPointsAction(user=request.user, node=answer, extra=extra).save(data=dict(value=points, affected=awarded_user))
614 return { 'message' : _("You have awarded %(awarded_user)s with %(points)d points") % {'awarded_user' : awarded_user, 'points' : points} }