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 HttpResponseBadRequest)
15 from django.shortcuts import get_object_or_404, render_to_response
17 from django.contrib import messages
19 from forum.models import *
20 from forum.utils.decorators import ajax_login_required
21 from forum.actions import *
22 from forum.modules import decorate
23 from forum import settings
25 from decorators import command, CommandException, RefreshPageCommand
27 class NotEnoughRepPointsException(CommandException):
28 def __init__(self, action, user_reputation=None, reputation_required=None, node=None):
29 if reputation_required is not None and user_reputation is not None:
31 """Sorry, but you don't have enough reputation points to %(action)s.<br />
32 The minimum reputation required is %(reputation_required)d (yours is %(user_reputation)d).
33 Please check the <a href='%(faq_url)s'>FAQ</a>"""
36 'faq_url': reverse('faq'),
37 'reputation_required' : reputation_required,
38 'user_reputation' : user_reputation,
42 """Sorry, but you don't have enough reputation points to %(action)s.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
43 ) % {'action': action, 'faq_url': reverse('faq')}
44 super(NotEnoughRepPointsException, self).__init__(message)
46 class CannotDoOnOwnException(CommandException):
47 def __init__(self, action):
48 super(CannotDoOnOwnException, self).__init__(
50 """Sorry but you cannot %(action)s your own post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
51 ) % {'action': action, 'faq_url': reverse('faq')}
54 class AnonymousNotAllowedException(CommandException):
55 def __init__(self, action):
56 super(AnonymousNotAllowedException, self).__init__(
58 """Sorry but anonymous users cannot %(action)s.<br />Please login or create an account <a href='%(signin_url)s'>here</a>."""
59 ) % {'action': action, 'signin_url': reverse('auth_signin')}
62 class NotEnoughLeftException(CommandException):
63 def __init__(self, action, limit):
64 super(NotEnoughLeftException, self).__init__(
66 """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>"""
67 ) % {'action': action, 'limit': limit, 'faq_url': reverse('faq')}
70 class CannotDoubleActionException(CommandException):
71 def __init__(self, action):
72 super(CannotDoubleActionException, self).__init__(
74 """Sorry, but you cannot %(action)s twice the same post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
75 ) % {'action': action, 'faq_url': reverse('faq')}
79 @decorate.withfn(command)
80 def vote_post(request, id, vote_type):
81 if not request.method == 'POST':
82 raise CommandException(_("Invalid request"))
85 post = get_object_or_404(Node, id=id).leaf
88 if not user.is_authenticated():
89 raise AnonymousNotAllowedException(_('vote'))
91 if user == post.author:
92 raise CannotDoOnOwnException(_('vote'))
94 if not (vote_type == 'up' and user.can_vote_up() or user.can_vote_down()):
95 reputation_required = int(settings.REP_TO_VOTE_UP) if vote_type == 'up' else int(settings.REP_TO_VOTE_DOWN)
96 action_type = vote_type == 'up' and _('upvote') or _('downvote')
97 raise NotEnoughRepPointsException(action_type, user_reputation=user.reputation, reputation_required=reputation_required, node=post)
99 user_vote_count_today = user.get_vote_count_today()
100 user_can_vote_count_today = user.can_vote_count_today()
102 if user_vote_count_today >= user.can_vote_count_today():
103 raise NotEnoughLeftException(_('votes'), str(settings.MAX_VOTES_PER_DAY))
105 new_vote_cls = (vote_type == 'up') and VoteUpAction or VoteDownAction
108 old_vote = VoteAction.get_action_for(node=post, user=user)
111 if old_vote.action_date < datetime.datetime.now() - datetime.timedelta(days=int(settings.DENY_UNVOTE_DAYS)):
112 raise CommandException(
113 _("Sorry but you cannot cancel a vote after %(ndays)d %(tdays)s from the original vote") %
114 {'ndays': int(settings.DENY_UNVOTE_DAYS),
115 'tdays': ungettext('day', 'days', int(settings.DENY_UNVOTE_DAYS))}
118 old_vote.cancel(ip=request.META['REMOTE_ADDR'])
119 score_inc = (old_vote.__class__ == VoteDownAction) and 1 or -1
122 new_vote_cls(user=user, node=post, ip=request.META['REMOTE_ADDR']).save()
123 score_inc = (new_vote_cls == VoteUpAction) and 1 or -1
127 'update_post_score': [id, score_inc],
128 'update_user_post_vote': [id, vote_type]
132 votes_left = (user_can_vote_count_today - user_vote_count_today) + (vote_type == 'none' and -1 or 1)
134 if int(settings.START_WARN_VOTES_LEFT) >= votes_left:
135 response['message'] = _("You have %(nvotes)s %(tvotes)s left today.") % \
136 {'nvotes': votes_left, 'tvotes': ungettext('vote', 'votes', votes_left)}
140 @decorate.withfn(command)
141 def flag_post(request, id):
143 return render_to_response('node/report.html', {'types': settings.FLAG_TYPES})
145 post = get_object_or_404(Node, id=id)
148 if not user.is_authenticated():
149 raise AnonymousNotAllowedException(_('flag posts'))
151 if user == post.author:
152 raise CannotDoOnOwnException(_('flag'))
154 if not (user.can_flag_offensive(post)):
155 raise NotEnoughRepPointsException(_('flag posts'))
157 user_flag_count_today = user.get_flagged_items_count_today()
159 if user_flag_count_today >= int(settings.MAX_FLAGS_PER_DAY):
160 raise NotEnoughLeftException(_('flags'), str(settings.MAX_FLAGS_PER_DAY))
163 current = FlagAction.objects.get(canceled=False, user=user, node=post)
164 raise CommandException(
165 _("You already flagged this post with the following reason: %(reason)s") % {'reason': current.extra})
166 except ObjectDoesNotExist:
167 reason = request.POST.get('prompt', '').strip()
170 raise CommandException(_("Reason is empty"))
172 FlagAction(user=user, node=post, extra=reason, ip=request.META['REMOTE_ADDR']).save()
174 return {'message': _("Thank you for your report. A moderator will review your submission shortly.")}
176 @decorate.withfn(command)
177 def like_comment(request, id):
178 comment = get_object_or_404(Comment, id=id)
181 if not user.is_authenticated():
182 raise AnonymousNotAllowedException(_('like comments'))
184 if user == comment.user:
185 raise CannotDoOnOwnException(_('like'))
187 if not user.can_like_comment(comment):
188 raise NotEnoughRepPointsException( _('like comments'), node=comment)
190 like = VoteAction.get_action_for(node=comment, user=user)
193 like.cancel(ip=request.META['REMOTE_ADDR'])
196 VoteUpCommentAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
201 'update_post_score': [comment.id, likes and 1 or -1],
202 'update_user_post_vote': [comment.id, likes and 'up' or 'none']
206 @decorate.withfn(command)
207 def delete_comment(request, id):
208 comment = get_object_or_404(Comment, id=id)
211 if not user.is_authenticated():
212 raise AnonymousNotAllowedException(_('delete comments'))
214 if not user.can_delete_comment(comment):
215 raise NotEnoughRepPointsException( _('delete comments'))
217 if not comment.nis.deleted:
218 DeleteAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
222 'remove_comment': [comment.id],
226 @decorate.withfn(command)
227 def mark_favorite(request, id):
228 node = get_object_or_404(Node, id=id)
230 if not request.user.is_authenticated():
231 raise AnonymousNotAllowedException(_('mark a question as favorite'))
234 favorite = FavoriteAction.objects.get(canceled=False, node=node, user=request.user)
235 favorite.cancel(ip=request.META['REMOTE_ADDR'])
237 except ObjectDoesNotExist:
238 FavoriteAction(node=node, user=request.user, ip=request.META['REMOTE_ADDR']).save()
243 'update_favorite_count': [added and 1 or -1],
244 'update_favorite_mark': [added and 'on' or 'off']
248 @decorate.withfn(command)
249 def comment(request, id):
250 post = get_object_or_404(Node, id=id)
253 if not user.is_authenticated():
254 raise AnonymousNotAllowedException(_('comment'))
256 if not request.method == 'POST':
257 raise CommandException(_("Invalid request"))
259 comment_text = request.POST.get('comment', '').strip()
261 if not len(comment_text):
262 raise CommandException(_("Comment is empty"))
264 if len(comment_text) < settings.FORM_MIN_COMMENT_BODY:
265 raise CommandException(_("At least %d characters required on comment body.") % settings.FORM_MIN_COMMENT_BODY)
267 if len(comment_text) > settings.FORM_MAX_COMMENT_BODY:
268 raise CommandException(_("No more than %d characters on comment body.") % settings.FORM_MAX_COMMENT_BODY)
270 if 'id' in request.POST:
271 comment = get_object_or_404(Comment, id=request.POST['id'])
273 if not user.can_edit_comment(comment):
274 raise NotEnoughRepPointsException( _('edit comments'))
276 comment = ReviseAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(
277 data=dict(text=comment_text)).node
279 if not user.can_comment(post):
280 raise NotEnoughRepPointsException( _('comment'))
282 comment = CommentAction(user=user, ip=request.META['REMOTE_ADDR']).save(
283 data=dict(text=comment_text, parent=post)).node
285 if comment.active_revision.revision == 1:
289 id, comment.id, comment.comment, user.decorated_name, user.get_profile_url(),
290 reverse('delete_comment', kwargs={'id': comment.id}),
291 reverse('node_markdown', kwargs={'id': comment.id}),
292 reverse('convert_comment', kwargs={'id': comment.id}),
293 user.can_convert_comment_to_answer(comment),
294 bool(settings.SHOW_LATEST_COMMENTS_FIRST)
301 'update_comment': [comment.id, comment.comment]
305 @decorate.withfn(command)
306 def node_markdown(request, id):
309 if not user.is_authenticated():
310 raise AnonymousNotAllowedException(_('accept answers'))
312 node = get_object_or_404(Node, id=id)
313 return HttpResponse(node.active_revision.body, content_type="text/plain")
316 @decorate.withfn(command)
317 def accept_answer(request, id):
318 if settings.DISABLE_ACCEPTING_FEATURE:
323 if not user.is_authenticated():
324 raise AnonymousNotAllowedException(_('accept answers'))
326 answer = get_object_or_404(Answer, id=id)
327 question = answer.question
329 if not user.can_accept_answer(answer):
330 raise CommandException(_("Sorry but you cannot accept the answer"))
334 if answer.nis.accepted:
335 answer.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR'])
336 commands['unmark_accepted'] = [answer.id]
338 if settings.MAXIMUM_ACCEPTED_ANSWERS and (question.accepted_count >= settings.MAXIMUM_ACCEPTED_ANSWERS):
339 raise CommandException(ungettext("This question already has an accepted answer.",
340 "Sorry but this question has reached the limit of accepted answers.", int(settings.MAXIMUM_ACCEPTED_ANSWERS)))
342 if settings.MAXIMUM_ACCEPTED_PER_USER and question.accepted_count:
343 accepted_from_author = question.accepted_answers.filter(author=answer.author).count()
345 if accepted_from_author >= settings.MAXIMUM_ACCEPTED_PER_USER:
346 raise CommandException(ungettext("The author of this answer already has an accepted answer in this question.",
347 "Sorry but the author of this answer has reached the limit of accepted answers per question.", int(settings.MAXIMUM_ACCEPTED_PER_USER)))
350 AcceptAnswerAction(node=answer, user=user, ip=request.META['REMOTE_ADDR']).save()
352 # If the request is not an AJAX redirect to the answer URL rather than to the home page
353 if not request.is_ajax():
355 Congratulations! You've accepted an answer.
358 # Notify the user with a message that an answer has been accepted
359 messages.info(request, msg)
361 # Redirect URL should include additional get parameters that might have been attached
362 redirect_url = answer.parent.get_absolute_url() + "?accepted_answer=true&%s" % smart_unicode(urlencode(request.GET))
364 return HttpResponseRedirect(redirect_url)
366 commands['mark_accepted'] = [answer.id]
368 return {'commands': commands}
370 @decorate.withfn(command)
371 def delete_post(request, id):
372 post = get_object_or_404(Node, id=id)
375 if not user.is_authenticated():
376 raise AnonymousNotAllowedException(_('delete posts'))
378 if not (user.can_delete_post(post)):
379 raise NotEnoughRepPointsException(_('delete posts'))
381 ret = {'commands': {}}
384 post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
385 ret['commands']['unmark_deleted'] = [post.node_type, id]
387 DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
389 ret['commands']['mark_deleted'] = [post.node_type, id]
393 @decorate.withfn(command)
394 def close(request, id, close):
395 if close and not request.POST:
396 return render_to_response('node/report.html', {'types': settings.CLOSE_TYPES})
398 question = get_object_or_404(Question, id=id)
401 if not user.is_authenticated():
402 raise AnonymousNotAllowedException(_('close questions'))
404 if question.nis.closed:
405 if not user.can_reopen_question(question):
406 raise NotEnoughRepPointsException(_('reopen questions'))
408 question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
410 if not request.user.can_close_question(question):
411 raise NotEnoughRepPointsException(_('close questions'))
413 reason = request.POST.get('prompt', '').strip()
416 raise CommandException(_("Reason is empty"))
418 CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
420 return RefreshPageCommand()
422 @decorate.withfn(command)
423 def wikify(request, id):
424 node = get_object_or_404(Node, id=id)
427 if not user.is_authenticated():
428 raise AnonymousNotAllowedException(_('mark posts as community wiki'))
431 if not user.can_cancel_wiki(node):
432 raise NotEnoughRepPointsException(_('cancel a community wiki post'))
434 if node.nstate.wiki.action_type == "wikify":
435 node.nstate.wiki.cancel()
437 node.nstate.wiki = None
439 if not user.can_wikify(node):
440 raise NotEnoughRepPointsException(_('mark posts as community wiki'))
442 WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
444 return RefreshPageCommand()
446 @decorate.withfn(command)
447 def convert_to_comment(request, id):
449 answer = get_object_or_404(Answer, id=id)
450 question = answer.question
452 # Check whether the user has the required permissions
453 if not user.is_authenticated():
454 raise AnonymousNotAllowedException(_("convert answers to comments"))
456 if not user.can_convert_to_comment(answer):
457 raise NotEnoughRepPointsException(_("convert answers to comments"))
460 description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': smart_unicode(a.author.username),
461 'snippet': a.summary[:10]}
462 nodes = [(question.id, _("Question"))]
463 [nodes.append((a.id, description(a))) for a in
464 question.answers.filter_state(deleted=False).exclude(id=answer.id)]
466 return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
469 new_parent = Node.objects.get(id=request.POST.get('under', None))
471 raise CommandException(_("That is an invalid post to put the comment under"))
473 if not (new_parent == question or (new_parent.node_type == 'answer' and new_parent.parent == question)):
474 raise CommandException(_("That is an invalid post to put the comment under"))
476 AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
478 return RefreshPageCommand()
480 @decorate.withfn(command)
481 def convert_comment_to_answer(request, id):
483 comment = get_object_or_404(Comment, id=id)
484 parent = comment.parent
486 if not parent.question:
489 question = parent.question
491 if not user.is_authenticated():
492 raise AnonymousNotAllowedException(_("convert comments to answers"))
494 if not user.can_convert_comment_to_answer(comment):
495 raise NotEnoughRepPointsException(_("convert comments to answers"))
497 CommentToAnswerAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(question=question))
499 return RefreshPageCommand()
501 @decorate.withfn(command)
502 def subscribe(request, id, user=None):
505 user = User.objects.get(id=user)
506 except User.DoesNotExist:
509 if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
510 raise CommandException(_("You do not have the correct credentials to preform this action."))
514 question = get_object_or_404(Question, id=id)
517 subscription = QuestionSubscription.objects.get(question=question, user=user)
518 subscription.delete()
521 subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
527 'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
528 'set_subscription_status': ['']
532 #internally grouped views - used by the tagging system
534 def mark_tag(request, tag=None, **kwargs):#tagging system
535 action = kwargs['action']
536 ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
537 if action == 'remove':
538 logging.debug('deleting tag %s' % tag)
541 reason = kwargs['reason']
544 t = Tag.objects.get(name=tag)
545 mt = MarkedTag(user=request.user, reason=reason, tag=t)
550 ts.update(reason=reason)
551 return HttpResponse(json.dumps(''), content_type="application/json")
553 def matching_tags(request):
554 q = request.GET.get('q')
556 return HttpResponseBadRequest(_("Invalid request"))
558 possible_tags = Tag.active.filter(name__icontains=q)
560 for tag in possible_tags:
561 tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
563 return HttpResponse(tag_output, content_type="text/plain")
565 def matching_users(request):
566 if len(request.GET['q']) == 0:
567 raise CommandException(_("Invalid request"))
569 possible_users = User.objects.filter(username__icontains = request.GET['q'])
572 for user in possible_users:
573 output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
575 return HttpResponse(output, content_type="text/plain")
577 def related_questions(request):
578 if request.POST and request.POST.get('title', None):
579 can_rank, questions = Question.objects.search(request.POST['title'])
581 if can_rank and isinstance(can_rank, basestring):
582 questions = questions.order_by(can_rank)
584 return HttpResponse(json.dumps(
585 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
586 for q in questions.filter_state(deleted=False)[0:10]]), content_type="application/json")
590 @decorate.withfn(command)
591 def answer_permanent_link(request, id):
592 # Getting the current answer object
593 answer = get_object_or_404(Answer, id=id)
595 # Getting the current object URL -- the Application URL + the object relative URL
596 url = '%s%s' % (settings.APP_BASE_URL, answer.get_absolute_url())
599 # Display the template
600 return render_to_response('node/permanent_link.html', { 'url' : url, })
604 'copy_url' : [request.POST['permanent_link_url'],],
606 'message' : _("The permanent URL to the answer has been copied to your clipboard."),
609 @decorate.withfn(command)
610 def award_points(request, user_id, answer_id):
612 awarded_user = get_object_or_404(User, id=user_id)
613 answer = get_object_or_404(Answer, id=answer_id)
615 # Users shouldn't be able to award themselves
616 if awarded_user.id == user.id:
617 raise CannotDoOnOwnException(_("award"))
619 # Anonymous users cannot award points, they just don't have such
620 if not user.is_authenticated():
621 raise AnonymousNotAllowedException(_('award'))
624 return render_to_response("node/award_points.html", {
626 'awarded_user' : awarded_user,
627 'reputation_to_comment' : str(settings.REP_TO_COMMENT)
630 points = int(request.POST['points'])
632 # We should check if the user has enough reputation points, otherwise we raise an exception.
634 raise CommandException(_("The number of points to award needs to be a positive value."))
636 if user.reputation < points:
637 raise NotEnoughRepPointsException(_("award"))
639 extra = dict(message=request.POST.get('message', ''), awarding_user=request.user.id, value=points)
641 # We take points from the awarding user
642 AwardPointsAction(user=request.user, node=answer, extra=extra).save(data=dict(value=points, affected=awarded_user))
644 return { 'message' : _("You have awarded %(awarded_user)s with %(points)d points") % {'awarded_user' : awarded_user, 'points' : points} }