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):
24 super(NotEnoughRepPointsException, self).__init__(
26 """Sorry, but you don't have enough reputation points to %(action)s.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
27 ) % {'action': action, 'faq_url': reverse('faq')}
30 class CannotDoOnOwnException(CommandException):
31 def __init__(self, action):
32 super(CannotDoOnOwnException, self).__init__(
34 """Sorry but you cannot %(action)s your own post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
35 ) % {'action': action, 'faq_url': reverse('faq')}
38 class AnonymousNotAllowedException(CommandException):
39 def __init__(self, action):
40 super(AnonymousNotAllowedException, self).__init__(
42 """Sorry but anonymous users cannot %(action)s.<br />Please login or create an account <a href='%(signin_url)s'>here</a>."""
43 ) % {'action': action, 'signin_url': reverse('auth_signin')}
46 class NotEnoughLeftException(CommandException):
47 def __init__(self, action, limit):
48 super(NotEnoughLeftException, self).__init__(
50 """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>"""
51 ) % {'action': action, 'limit': limit, 'faq_url': reverse('faq')}
54 class CannotDoubleActionException(CommandException):
55 def __init__(self, action):
56 super(CannotDoubleActionException, self).__init__(
58 """Sorry, but you cannot %(action)s twice the same post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
59 ) % {'action': action, 'faq_url': reverse('faq')}
63 @decorate.withfn(command)
64 def vote_post(request, id, vote_type):
65 post = get_object_or_404(Node, id=id).leaf
68 if not user.is_authenticated():
69 raise AnonymousNotAllowedException(_('vote'))
71 if user == post.author:
72 raise CannotDoOnOwnException(_('vote'))
74 if not (vote_type == 'up' and user.can_vote_up() or user.can_vote_down()):
75 raise NotEnoughRepPointsException(vote_type == 'up' and _('upvote') or _('downvote'))
77 user_vote_count_today = user.get_vote_count_today()
78 user_can_vote_count_today = user.can_vote_count_today()
80 if user_vote_count_today >= user.can_vote_count_today():
81 raise NotEnoughLeftException(_('votes'), str(settings.MAX_VOTES_PER_DAY))
83 new_vote_cls = (vote_type == 'up') and VoteUpAction or VoteDownAction
86 old_vote = VoteAction.get_action_for(node=post, user=user)
89 if old_vote.action_date < datetime.datetime.now() - datetime.timedelta(days=int(settings.DENY_UNVOTE_DAYS)):
90 raise CommandException(
91 _("Sorry but you cannot cancel a vote after %(ndays)d %(tdays)s from the original vote") %
92 {'ndays': int(settings.DENY_UNVOTE_DAYS),
93 'tdays': ungettext('day', 'days', int(settings.DENY_UNVOTE_DAYS))}
96 old_vote.cancel(ip=request.META['REMOTE_ADDR'])
97 score_inc = (old_vote.__class__ == VoteDownAction) and 1 or -1
100 new_vote_cls(user=user, node=post, ip=request.META['REMOTE_ADDR']).save()
101 score_inc = (new_vote_cls == VoteUpAction) and 1 or -1
105 'update_post_score': [id, score_inc],
106 'update_user_post_vote': [id, vote_type]
110 votes_left = (user_can_vote_count_today - user_vote_count_today) + (vote_type == 'none' and -1 or 1)
112 if int(settings.START_WARN_VOTES_LEFT) >= votes_left:
113 response['message'] = _("You have %(nvotes)s %(tvotes)s left today.") % \
114 {'nvotes': votes_left, 'tvotes': ungettext('vote', 'votes', votes_left)}
118 @decorate.withfn(command)
119 def flag_post(request, id):
121 return render_to_response('node/report.html', {'types': settings.FLAG_TYPES})
123 post = get_object_or_404(Node, id=id)
126 if not user.is_authenticated():
127 raise AnonymousNotAllowedException(_('flag posts'))
129 if user == post.author:
130 raise CannotDoOnOwnException(_('flag'))
132 if not (user.can_flag_offensive(post)):
133 raise NotEnoughRepPointsException(_('flag posts'))
135 user_flag_count_today = user.get_flagged_items_count_today()
137 if user_flag_count_today >= int(settings.MAX_FLAGS_PER_DAY):
138 raise NotEnoughLeftException(_('flags'), str(settings.MAX_FLAGS_PER_DAY))
141 current = FlagAction.objects.get(canceled=False, user=user, node=post)
142 raise CommandException(
143 _("You already flagged this post with the following reason: %(reason)s") % {'reason': current.extra})
144 except ObjectDoesNotExist:
145 reason = request.POST.get('prompt', '').strip()
148 raise CommandException(_("Reason is empty"))
150 FlagAction(user=user, node=post, extra=reason, ip=request.META['REMOTE_ADDR']).save()
152 return {'message': _("Thank you for your report. A moderator will review your submission shortly.")}
154 @decorate.withfn(command)
155 def like_comment(request, id):
156 comment = get_object_or_404(Comment, id=id)
159 if not user.is_authenticated():
160 raise AnonymousNotAllowedException(_('like comments'))
162 if user == comment.user:
163 raise CannotDoOnOwnException(_('like'))
165 if not user.can_like_comment(comment):
166 raise NotEnoughRepPointsException( _('like comments'))
168 like = VoteAction.get_action_for(node=comment, user=user)
171 like.cancel(ip=request.META['REMOTE_ADDR'])
174 VoteUpCommentAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
179 'update_post_score': [comment.id, likes and 1 or -1],
180 'update_user_post_vote': [comment.id, likes and 'up' or 'none']
184 @decorate.withfn(command)
185 def delete_comment(request, id):
186 comment = get_object_or_404(Comment, id=id)
189 if not user.is_authenticated():
190 raise AnonymousNotAllowedException(_('delete comments'))
192 if not user.can_delete_comment(comment):
193 raise NotEnoughRepPointsException( _('delete comments'))
195 if not comment.nis.deleted:
196 DeleteAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
200 'remove_comment': [comment.id],
204 @decorate.withfn(command)
205 def mark_favorite(request, id):
206 question = get_object_or_404(Question, id=id)
208 if not request.user.is_authenticated():
209 raise AnonymousNotAllowedException(_('mark a question as favorite'))
212 favorite = FavoriteAction.objects.get(canceled=False, node=question, user=request.user)
213 favorite.cancel(ip=request.META['REMOTE_ADDR'])
215 except ObjectDoesNotExist:
216 FavoriteAction(node=question, user=request.user, ip=request.META['REMOTE_ADDR']).save()
221 'update_favorite_count': [added and 1 or -1],
222 'update_favorite_mark': [added and 'on' or 'off']
226 @decorate.withfn(command)
227 def comment(request, id):
228 post = get_object_or_404(Node, id=id)
231 if not user.is_authenticated():
232 raise AnonymousNotAllowedException(_('comment'))
234 if not request.method == 'POST':
235 raise CommandException(_("Invalid request"))
237 comment_text = request.POST.get('comment', '').strip()
239 if not len(comment_text):
240 raise CommandException(_("Comment is empty"))
242 if len(comment_text) < settings.FORM_MIN_COMMENT_BODY:
243 raise CommandException(_("At least %d characters required on comment body.") % settings.FORM_MIN_COMMENT_BODY)
245 if len(comment_text) > settings.FORM_MAX_COMMENT_BODY:
246 raise CommandException(_("No more than %d characters on comment body.") % settings.FORM_MAX_COMMENT_BODY)
248 if 'id' in request.POST:
249 comment = get_object_or_404(Comment, id=request.POST['id'])
251 if not user.can_edit_comment(comment):
252 raise NotEnoughRepPointsException( _('edit comments'))
254 comment = ReviseAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(
255 data=dict(text=comment_text)).node
257 if not user.can_comment(post):
258 raise NotEnoughRepPointsException( _('comment'))
260 comment = CommentAction(user=user, ip=request.META['REMOTE_ADDR']).save(
261 data=dict(text=comment_text, parent=post)).node
263 if comment.active_revision.revision == 1:
267 id, comment.id, comment.comment, user.decorated_name, user.get_profile_url(),
268 reverse('delete_comment', kwargs={'id': comment.id}),
269 reverse('node_markdown', kwargs={'id': comment.id}),
270 reverse('convert_comment', kwargs={'id': comment.id}),
271 user.can_convert_comment_to_answer(comment),
278 'update_comment': [comment.id, comment.comment]
282 @decorate.withfn(command)
283 def node_markdown(request, id):
286 if not user.is_authenticated():
287 raise AnonymousNotAllowedException(_('accept answers'))
289 node = get_object_or_404(Node, id=id)
290 return HttpResponse(node.active_revision.body, mimetype="text/plain")
293 @decorate.withfn(command)
294 def accept_answer(request, id):
295 if settings.DISABLE_ACCEPTING_FEATURE:
300 if not user.is_authenticated():
301 raise AnonymousNotAllowedException(_('accept answers'))
303 answer = get_object_or_404(Answer, id=id)
304 question = answer.question
306 if not user.can_accept_answer(answer):
307 raise CommandException(_("Sorry but you cannot accept the answer"))
311 if answer.nis.accepted:
312 answer.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR'])
313 commands['unmark_accepted'] = [answer.id]
315 if settings.MAXIMUM_ACCEPTED_ANSWERS and (question.accepted_count >= settings.MAXIMUM_ACCEPTED_ANSWERS):
316 raise CommandException(ungettext("This question already has an accepted answer.",
317 "Sorry but this question has reached the limit of accepted answers.", int(settings.MAXIMUM_ACCEPTED_ANSWERS)))
319 if settings.MAXIMUM_ACCEPTED_PER_USER and question.accepted_count:
320 accepted_from_author = question.accepted_answers.filter(author=answer.author).count()
322 if accepted_from_author >= settings.MAXIMUM_ACCEPTED_PER_USER:
323 raise CommandException(ungettext("The author of this answer already has an accepted answer in this question.",
324 "Sorry but the author of this answer has reached the limit of accepted answers per question.", int(settings.MAXIMUM_ACCEPTED_PER_USER)))
327 AcceptAnswerAction(node=answer, user=user, ip=request.META['REMOTE_ADDR']).save()
328 commands['mark_accepted'] = [answer.id]
330 return {'commands': commands}
332 @decorate.withfn(command)
333 def delete_post(request, id):
334 post = get_object_or_404(Node, id=id)
337 if not user.is_authenticated():
338 raise AnonymousNotAllowedException(_('delete posts'))
340 if not (user.can_delete_post(post)):
341 raise NotEnoughRepPointsException(_('delete posts'))
343 ret = {'commands': {}}
346 post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
347 ret['commands']['unmark_deleted'] = [post.node_type, id]
349 DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
351 ret['commands']['mark_deleted'] = [post.node_type, id]
355 @decorate.withfn(command)
356 def close(request, id, close):
357 if close and not request.POST:
358 return render_to_response('node/report.html', {'types': settings.CLOSE_TYPES})
360 question = get_object_or_404(Question, id=id)
363 if not user.is_authenticated():
364 raise AnonymousNotAllowedException(_('close questions'))
366 if question.nis.closed:
367 if not user.can_reopen_question(question):
368 raise NotEnoughRepPointsException(_('reopen questions'))
370 question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
372 if not request.user.can_close_question(question):
373 raise NotEnoughRepPointsException(_('close questions'))
375 reason = request.POST.get('prompt', '').strip()
378 raise CommandException(_("Reason is empty"))
380 CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
382 return RefreshPageCommand()
384 @decorate.withfn(command)
385 def wikify(request, id):
386 node = get_object_or_404(Node, id=id)
389 if not user.is_authenticated():
390 raise AnonymousNotAllowedException(_('mark posts as community wiki'))
393 if not user.can_cancel_wiki(node):
394 raise NotEnoughRepPointsException(_('cancel a community wiki post'))
396 if node.nstate.wiki.action_type == "wikify":
397 node.nstate.wiki.cancel()
399 node.nstate.wiki = None
401 if not user.can_wikify(node):
402 raise NotEnoughRepPointsException(_('mark posts as community wiki'))
404 WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
406 return RefreshPageCommand()
408 @decorate.withfn(command)
409 def convert_to_comment(request, id):
411 answer = get_object_or_404(Answer, id=id)
412 question = answer.question
414 # Check whether the user has the required permissions
415 if not user.is_authenticated():
416 raise AnonymousNotAllowedException(_("convert answers to comments"))
418 if not user.can_convert_to_comment(answer):
419 raise NotEnoughRepPointsException(_("convert answers to comments"))
422 description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': smart_unicode(a.author.username),
423 'snippet': a.summary[:10]}
424 nodes = [(question.id, _("Question"))]
425 [nodes.append((a.id, description(a))) for a in
426 question.answers.filter_state(deleted=False).exclude(id=answer.id)]
428 return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
431 new_parent = Node.objects.get(id=request.POST.get('under', None))
433 raise CommandException(_("That is an invalid post to put the comment under"))
435 if not (new_parent == question or (new_parent.node_type == 'answer' and new_parent.parent == question)):
436 raise CommandException(_("That is an invalid post to put the comment under"))
438 AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
440 return RefreshPageCommand()
442 @decorate.withfn(command)
443 def convert_comment_to_answer(request, id):
445 comment = get_object_or_404(Comment, id=id)
446 parent = comment.parent
448 if not parent.question:
451 question = parent.question
453 if not user.is_authenticated():
454 raise AnonymousNotAllowedException(_("convert comments to answers"))
456 if not user.can_convert_comment_to_answer(comment):
457 raise NotEnoughRepPointsException(_("convert comments to answers"))
459 CommentToAnswerAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(question=question))
461 return RefreshPageCommand()
463 @decorate.withfn(command)
464 def subscribe(request, id, user=None):
467 user = User.objects.get(id=user)
468 except User.DoesNotExist:
471 if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
472 raise CommandException(_("You do not have the correct credentials to preform this action."))
476 question = get_object_or_404(Question, id=id)
479 subscription = QuestionSubscription.objects.get(question=question, user=user)
480 subscription.delete()
483 subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
489 'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
490 'set_subscription_status': ['']
494 #internally grouped views - used by the tagging system
496 def mark_tag(request, tag=None, **kwargs):#tagging system
497 action = kwargs['action']
498 ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
499 if action == 'remove':
500 logging.debug('deleting tag %s' % tag)
503 reason = kwargs['reason']
506 t = Tag.objects.get(name=tag)
507 mt = MarkedTag(user=request.user, reason=reason, tag=t)
512 ts.update(reason=reason)
513 return HttpResponse(simplejson.dumps(''), mimetype="application/json")
515 def matching_tags(request):
516 if len(request.GET['q']) == 0:
517 raise CommandException(_("Invalid request"))
519 possible_tags = Tag.active.filter(name__icontains = request.GET['q'])
521 for tag in possible_tags:
522 tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
524 return HttpResponse(tag_output, mimetype="text/plain")
526 def matching_users(request):
527 if len(request.GET['q']) == 0:
528 raise CommandException(_("Invalid request"))
530 possible_users = User.objects.filter(username__icontains = request.GET['q'])
533 for user in possible_users:
534 output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
536 return HttpResponse(output, mimetype="text/plain")
538 def related_questions(request):
539 if request.POST and request.POST.get('title', None):
540 can_rank, questions = Question.objects.search(request.POST['title'])
542 if can_rank and isinstance(can_rank, basestring):
543 questions = questions.order_by(can_rank)
545 return HttpResponse(simplejson.dumps(
546 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
547 for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")
551 @decorate.withfn(command)
552 def answer_permanent_link(request, id):
553 # Getting the current answer object
554 answer = get_object_or_404(Answer, id=id)
556 # Getting the current object URL -- the Application URL + the object relative URL
557 url = '%s%s' % (settings.APP_BASE_URL, answer.get_absolute_url())
560 # Display the template
561 return render_to_response('node/permanent_link.html', { 'url' : url, })
565 'copy_url' : [request.POST['permanent_link_url'],],
567 'message' : _("The permanent URL to the answer has been copied to your clipboard."),
570 @decorate.withfn(command)
571 def award_points(request, user_id, answer_id):
573 awarded_user = get_object_or_404(User, id=user_id)
574 answer = get_object_or_404(Answer, id=answer_id)
576 # Users shouldn't be able to award themselves
577 if awarded_user.id == user.id:
578 raise CannotDoOnOwnException(_("award"))
580 # Anonymous users cannot award points, they just don't have such
581 if not user.is_authenticated():
582 raise AnonymousNotAllowedException(_('award'))
585 return render_to_response("node/award_points.html", { 'user' : user, 'awarded_user' : awarded_user, })
587 points = int(request.POST['points'])
589 # We should check if the user has enough reputation points, otherwise we raise an exception.
591 raise CommandException(_("The number of points to award needs to be a positive value."))
593 if user.reputation < points:
594 raise NotEnoughRepPointsException(_("award"))
596 extra = dict(message=request.POST.get('message', ''), awarding_user=request.user.id, value=points)
598 # We take points from the awarding user
599 AwardPointsAction(user=request.user, node=answer, extra=extra).save(data=dict(value=points, affected=awarded_user))
601 return { 'message' : _("You have awarded %(awarded_user)s with %(points)d points") % {'awarded_user' : awarded_user, 'points' : points} }