2 from forum import settings
3 from django.core.exceptions import ObjectDoesNotExist
4 from django.utils import simplejson
5 from django.http import HttpResponse, HttpResponseRedirect, Http404
6 from django.shortcuts import get_object_or_404, render_to_response
7 from django.utils.translation import ungettext, ugettext as _
8 from django.template import RequestContext
9 from django.template.loader import render_to_string
10 from forum.models import *
11 from forum.models.node import NodeMetaClass
12 from forum.actions import *
13 from django.core.urlresolvers import reverse
14 from forum.utils.decorators import ajax_method, ajax_login_required
15 from decorators import command, CommandException, RefreshPageCommand
16 from forum.modules import decorate
17 from forum import settings
20 class NotEnoughRepPointsException(CommandException):
21 def __init__(self, action):
22 super(NotEnoughRepPointsException, self).__init__(
24 """Sorry, but you don't have enough reputation points to %(action)s.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
25 ) % {'action': action, 'faq_url': reverse('faq')}
28 class CannotDoOnOwnException(CommandException):
29 def __init__(self, action):
30 super(CannotDoOnOwnException, self).__init__(
32 """Sorry but you cannot %(action)s your own post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
33 ) % {'action': action, 'faq_url': reverse('faq')}
36 class AnonymousNotAllowedException(CommandException):
37 def __init__(self, action):
38 super(AnonymousNotAllowedException, self).__init__(
40 """Sorry but anonymous users cannot %(action)s.<br />Please login or create an account <a href='%(signin_url)s'>here</a>."""
41 ) % {'action': action, 'signin_url': reverse('auth_signin')}
44 class NotEnoughLeftException(CommandException):
45 def __init__(self, action, limit):
46 super(NotEnoughLeftException, self).__init__(
48 """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>"""
49 ) % {'action': action, 'limit': limit, 'faq_url': reverse('faq')}
52 class CannotDoubleActionException(CommandException):
53 def __init__(self, action):
54 super(CannotDoubleActionException, self).__init__(
56 """Sorry, but you cannot %(action)s twice the same post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
57 ) % {'action': action, 'faq_url': reverse('faq')}
61 @decorate.withfn(command)
62 def vote_post(request, id, vote_type):
63 post = get_object_or_404(Node, id=id).leaf
66 if not user.is_authenticated():
67 raise AnonymousNotAllowedException(_('vote'))
69 if user == post.author:
70 raise CannotDoOnOwnException(_('vote'))
72 if not (vote_type == 'up' and user.can_vote_up() or user.can_vote_down()):
73 raise NotEnoughRepPointsException(vote_type == 'up' and _('upvote') or _('downvote'))
75 user_vote_count_today = user.get_vote_count_today()
77 if user_vote_count_today >= user.can_vote_count_today():
78 raise NotEnoughLeftException(_('votes'), str(settings.MAX_VOTES_PER_DAY))
80 new_vote_cls = (vote_type == 'up') and VoteUpAction or VoteDownAction
83 old_vote = VoteAction.get_action_for(node=post, user=user)
86 if old_vote.action_date < datetime.datetime.now() - datetime.timedelta(days=int(settings.DENY_UNVOTE_DAYS)):
87 raise CommandException(
88 _("Sorry but you cannot cancel a vote after %(ndays)d %(tdays)s from the original vote") %
89 {'ndays': int(settings.DENY_UNVOTE_DAYS),
90 'tdays': ungettext('day', 'days', int(settings.DENY_UNVOTE_DAYS))}
93 old_vote.cancel(ip=request.META['REMOTE_ADDR'])
94 score_inc = (old_vote.__class__ == VoteDownAction) and 1 or -1
97 new_vote_cls(user=user, node=post, ip=request.META['REMOTE_ADDR']).save()
98 score_inc = (new_vote_cls == VoteUpAction) and 1 or -1
102 'update_post_score': [id, score_inc],
103 'update_user_post_vote': [id, vote_type]
107 votes_left = (int(settings.MAX_VOTES_PER_DAY) - user_vote_count_today) + (vote_type == 'none' and -1 or 1)
109 if int(settings.START_WARN_VOTES_LEFT) >= votes_left:
110 response['message'] = _("You have %(nvotes)s %(tvotes)s left today.") % \
111 {'nvotes': votes_left, 'tvotes': ungettext('vote', 'votes', votes_left)}
115 @decorate.withfn(command)
116 def flag_post(request, id):
118 return render_to_response('node/report.html', {'types': settings.FLAG_TYPES})
120 post = get_object_or_404(Node, id=id)
123 if not user.is_authenticated():
124 raise AnonymousNotAllowedException(_('flag posts'))
126 if user == post.author:
127 raise CannotDoOnOwnException(_('flag'))
129 if not (user.can_flag_offensive(post)):
130 raise NotEnoughRepPointsException(_('flag posts'))
132 user_flag_count_today = user.get_flagged_items_count_today()
134 if user_flag_count_today >= int(settings.MAX_FLAGS_PER_DAY):
135 raise NotEnoughLeftException(_('flags'), str(settings.MAX_FLAGS_PER_DAY))
138 current = FlagAction.objects.get(canceled=False, user=user, node=post)
139 raise CommandException(
140 _("You already flagged this post with the following reason: %(reason)s") % {'reason': current.extra})
141 except ObjectDoesNotExist:
142 reason = request.POST.get('prompt', '').strip()
145 raise CommandException(_("Reason is empty"))
147 FlagAction(user=user, node=post, extra=reason, ip=request.META['REMOTE_ADDR']).save()
149 return {'message': _("Thank you for your report. A moderator will review your submission shortly.")}
151 @decorate.withfn(command)
152 def like_comment(request, id):
153 comment = get_object_or_404(Comment, id=id)
156 if not user.is_authenticated():
157 raise AnonymousNotAllowedException(_('like comments'))
159 if user == comment.user:
160 raise CannotDoOnOwnException(_('like'))
162 if not user.can_like_comment(comment):
163 raise NotEnoughRepPointsException( _('like comments'))
165 like = VoteAction.get_action_for(node=comment, user=user)
168 like.cancel(ip=request.META['REMOTE_ADDR'])
171 VoteUpCommentAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
176 'update_post_score': [comment.id, likes and 1 or -1],
177 'update_user_post_vote': [comment.id, likes and 'up' or 'none']
181 @decorate.withfn(command)
182 def delete_comment(request, id):
183 comment = get_object_or_404(Comment, id=id)
186 if not user.is_authenticated():
187 raise AnonymousNotAllowedException(_('delete comments'))
189 if not user.can_delete_comment(comment):
190 raise NotEnoughRepPointsException( _('delete comments'))
192 if not comment.nis.deleted:
193 DeleteAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
197 'remove_comment': [comment.id],
201 @decorate.withfn(command)
202 def mark_favorite(request, id):
203 question = get_object_or_404(Question, id=id)
205 if not request.user.is_authenticated():
206 raise AnonymousNotAllowedException(_('mark a question as favorite'))
209 favorite = FavoriteAction.objects.get(canceled=False, node=question, user=request.user)
210 favorite.cancel(ip=request.META['REMOTE_ADDR'])
212 except ObjectDoesNotExist:
213 FavoriteAction(node=question, user=request.user, ip=request.META['REMOTE_ADDR']).save()
218 'update_favorite_count': [added and 1 or -1],
219 'update_favorite_mark': [added and 'on' or 'off']
223 @decorate.withfn(command)
224 def comment(request, id):
225 post = get_object_or_404(Node, id=id)
228 if not user.is_authenticated():
229 raise AnonymousNotAllowedException(_('comment'))
231 if not request.method == 'POST':
232 raise CommandException(_("Invalid request"))
234 comment_text = request.POST.get('comment', '').strip()
236 if not len(comment_text):
237 raise CommandException(_("Comment is empty"))
239 if len(comment_text) < settings.FORM_MIN_COMMENT_BODY:
240 raise CommandException(_("At least %d characters required on comment body.") % settings.FORM_MIN_COMMENT_BODY)
242 if len(comment_text) > settings.FORM_MAX_COMMENT_BODY:
243 raise CommandException(_("No more than %d characters on comment body.") % settings.FORM_MAX_COMMENT_BODY)
245 if 'id' in request.POST:
246 comment = get_object_or_404(Comment, id=request.POST['id'])
248 if not user.can_edit_comment(comment):
249 raise NotEnoughRepPointsException( _('edit comments'))
251 comment = ReviseAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(
252 data=dict(text=comment_text)).node
254 if not user.can_comment(post):
255 raise NotEnoughRepPointsException( _('comment'))
257 comment = CommentAction(user=user, ip=request.META['REMOTE_ADDR']).save(
258 data=dict(text=comment_text, parent=post)).node
260 if comment.active_revision.revision == 1:
264 id, comment.id, comment.comment, user.decorated_name, user.get_profile_url(),
265 reverse('delete_comment', kwargs={'id': comment.id}),
266 reverse('node_markdown', kwargs={'id': comment.id}),
267 reverse('convert_comment', kwargs={'id': comment.id}),
274 'update_comment': [comment.id, comment.comment]
278 @decorate.withfn(command)
279 def node_markdown(request, id):
282 if not user.is_authenticated():
283 raise AnonymousNotAllowedException(_('accept answers'))
285 node = get_object_or_404(Node, id=id)
286 return HttpResponse(node.active_revision.body, mimetype="text/plain")
289 @decorate.withfn(command)
290 def accept_answer(request, id):
291 if settings.DISABLE_ACCEPTING_FEATURE:
296 if not user.is_authenticated():
297 raise AnonymousNotAllowedException(_('accept answers'))
299 answer = get_object_or_404(Answer, id=id)
300 question = answer.question
302 if not user.can_accept_answer(answer):
303 raise CommandException(_("Sorry but you cannot accept the answer"))
307 if answer.nis.accepted:
308 answer.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR'])
309 commands['unmark_accepted'] = [answer.id]
311 if settings.MAXIMUM_ACCEPTED_ANSWERS and (question.accepted_count >= settings.MAXIMUM_ACCEPTED_ANSWERS):
312 raise CommandException(ungettext("This question already has an accepted answer.",
313 "Sorry but this question has reached the limit of accepted answers.", int(settings.MAXIMUM_ACCEPTED_ANSWERS)))
315 if settings.MAXIMUM_ACCEPTED_PER_USER and question.accepted_count:
316 accepted_from_author = question.accepted_answers.filter(author=answer.author).count()
318 if accepted_from_author >= settings.MAXIMUM_ACCEPTED_PER_USER:
319 raise CommandException(ungettext("The author of this answer already has an accepted answer in this question.",
320 "Sorry but the author of this answer has reached the limit of accepted answers per question.", int(settings.MAXIMUM_ACCEPTED_PER_USER)))
323 AcceptAnswerAction(node=answer, user=user, ip=request.META['REMOTE_ADDR']).save()
324 commands['mark_accepted'] = [answer.id]
326 return {'commands': commands}
328 @decorate.withfn(command)
329 def delete_post(request, id):
330 post = get_object_or_404(Node, id=id)
333 if not user.is_authenticated():
334 raise AnonymousNotAllowedException(_('delete posts'))
336 if not (user.can_delete_post(post)):
337 raise NotEnoughRepPointsException(_('delete posts'))
339 ret = {'commands': {}}
342 post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
343 ret['commands']['unmark_deleted'] = [post.node_type, id]
345 DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
347 ret['commands']['mark_deleted'] = [post.node_type, id]
351 @decorate.withfn(command)
352 def close(request, id, close):
353 if close and not request.POST:
354 return render_to_response('node/report.html', {'types': settings.CLOSE_TYPES})
356 question = get_object_or_404(Question, id=id)
359 if not user.is_authenticated():
360 raise AnonymousNotAllowedException(_('close questions'))
362 if question.nis.closed:
363 if not user.can_reopen_question(question):
364 raise NotEnoughRepPointsException(_('reopen questions'))
366 question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
368 if not request.user.can_close_question(question):
369 raise NotEnoughRepPointsException(_('close questions'))
371 reason = request.POST.get('prompt', '').strip()
374 raise CommandException(_("Reason is empty"))
376 CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
378 return RefreshPageCommand()
380 @decorate.withfn(command)
381 def wikify(request, id):
382 node = get_object_or_404(Node, id=id)
385 if not user.is_authenticated():
386 raise AnonymousNotAllowedException(_('mark posts as community wiki'))
389 if not user.can_cancel_wiki(node):
390 raise NotEnoughRepPointsException(_('cancel a community wiki post'))
392 if node.nstate.wiki.action_type == "wikify":
393 node.nstate.wiki.cancel()
395 node.nstate.wiki = None
397 if not user.can_wikify(node):
398 raise NotEnoughRepPointsException(_('mark posts as community wiki'))
400 WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
402 return RefreshPageCommand()
404 @decorate.withfn(command)
405 def convert_to_comment(request, id):
407 answer = get_object_or_404(Answer, id=id)
408 question = answer.question
411 description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': a.author.username,
412 'snippet': a.summary[:10]}
413 nodes = [(question.id, _("Question"))]
414 [nodes.append((a.id, description(a))) for a in
415 question.answers.filter_state(deleted=False).exclude(id=answer.id)]
417 return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
419 if not user.is_authenticated():
420 raise AnonymousNotAllowedException(_("convert answers to comments"))
422 if not user.can_convert_to_comment(answer):
423 raise NotEnoughRepPointsException(_("convert answers to comments"))
426 new_parent = Node.objects.get(id=request.POST.get('under', None))
428 raise CommandException(_("That is an invalid post to put the comment under"))
430 if not (new_parent == question or (new_parent.node_type == 'answer' and new_parent.parent == question)):
431 raise CommandException(_("That is an invalid post to put the comment under"))
433 AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
435 return RefreshPageCommand()
437 @decorate.withfn(command)
438 def convert_comment_to_answer(request, id):
440 comment = get_object_or_404(Comment, id=id)
441 parent = comment.parent
443 if not parent.question:
446 question = parent.question
448 if not user.is_authenticated():
449 raise AnonymousNotAllowedException(_("convert comments to answers"))
451 if not user.can_convert_comment_to_answer(comment):
452 raise NotEnoughRepPointsException(_("convert comments to answers"))
454 CommentToAnswerAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(question=question))
456 return RefreshPageCommand()
458 @decorate.withfn(command)
459 def subscribe(request, id, user=None):
462 user = User.objects.get(id=user)
463 except User.DoesNotExist:
466 if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
467 raise CommandException(_("You do not have the correct credentials to preform this action."))
471 question = get_object_or_404(Question, id=id)
474 subscription = QuestionSubscription.objects.get(question=question, user=user)
475 subscription.delete()
478 subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
484 'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
485 'set_subscription_status': ['']
489 #internally grouped views - used by the tagging system
491 def mark_tag(request, tag=None, **kwargs):#tagging system
492 action = kwargs['action']
493 ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
494 if action == 'remove':
495 logging.debug('deleting tag %s' % tag)
498 reason = kwargs['reason']
501 t = Tag.objects.get(name=tag)
502 mt = MarkedTag(user=request.user, reason=reason, tag=t)
507 ts.update(reason=reason)
508 return HttpResponse(simplejson.dumps(''), mimetype="application/json")
510 def matching_tags(request):
511 if len(request.GET['q']) == 0:
512 raise CommandException(_("Invalid request"))
514 possible_tags = Tag.active.filter(name__icontains = request.GET['q'])
516 for tag in possible_tags:
517 tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
519 return HttpResponse(tag_output, mimetype="text/plain")
521 def matching_users(request):
522 if len(request.GET['q']) == 0:
523 raise CommandException(_("Invalid request"))
525 possible_users = User.objects.filter(username__icontains = request.GET['q'])
528 for user in possible_users:
529 output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
531 return HttpResponse(output, mimetype="text/plain")
533 def related_questions(request):
534 if request.POST and request.POST.get('title', None):
535 can_rank, questions = Question.objects.search(request.POST['title'])
536 return HttpResponse(simplejson.dumps(
537 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
538 for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")
542 @decorate.withfn(command)
543 def answer_permanent_link(request, id):
544 # Getting the current answer object
545 answer = get_object_or_404(Answer, id=id)
547 # Getting the current object URL -- the Application URL + the object relative URL
548 url = '%s%s' % (settings.APP_BASE_URL, answer.get_absolute_url())
551 # Display the template
552 return render_to_response('node/permanent_link.html', { 'url' : url, })
556 'copy_url' : [request.POST['permanent_link_url'],],
558 'message' : _("The permanent URL to the answer has been copied to your clipboard."),
561 @decorate.withfn(command)
562 def award_points(request, user_id, answer_id):
564 awarded_user = get_object_or_404(User, id=user_id)
565 answer = get_object_or_404(Answer, id=answer_id)
567 # Users shouldn't be able to award themselves
568 if awarded_user.id == user.id:
569 raise CannotDoOnOwnException(_("award"))
571 # Anonymous users cannot award points, they just don't have such
572 if not user.is_authenticated():
573 raise AnonymousNotAllowedException(_('award'))
576 return render_to_response("node/award_points.html", { 'user' : user, 'awarded_user' : awarded_user, })
578 points = int(request.POST['points'])
580 # We should check if the user has enough reputation points, otherwise we raise an exception.
581 if user.reputation < points:
582 raise NotEnoughRepPointsException(_("award"))
584 extra = dict(message=request.POST.get('message', ''), awarding_user=request.user.id, value=points)
586 # We take points from the awarding user
587 AwardPointsAction(user=request.user, node=answer, extra=extra).save(data=dict(value=points, affected=awarded_user))
589 return { 'message' : _("You have awarded %(awarded_user)s with %(points)d points") % {'awarded_user' : awarded_user, 'points' : points} }