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 forum.models import *
10 from forum.models.node import NodeMetaClass
11 from forum.actions import *
12 from django.core.urlresolvers import reverse
13 from django.contrib.auth.decorators import login_required
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 >= int(settings.MAX_VOTES_PER_DAY):
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
96 if old_vote.__class__ != new_vote_cls:
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
104 'update_post_score': [id, score_inc],
105 'update_user_post_vote': [id, vote_type]
109 votes_left = (int(settings.MAX_VOTES_PER_DAY) - user_vote_count_today) + (vote_type == 'none' and -1 or 1)
111 if int(settings.START_WARN_VOTES_LEFT) >= votes_left:
112 response['message'] = _("You have %(nvotes)s %(tvotes)s left today.") % \
113 {'nvotes': votes_left, 'tvotes': ungettext('vote', 'votes', votes_left)}
117 @decorate.withfn(command)
118 def flag_post(request, id):
120 return render_to_response('node/report.html', {'types': settings.FLAG_TYPES})
122 post = get_object_or_404(Node, id=id)
125 if not user.is_authenticated():
126 raise AnonymousNotAllowedException(_('flag posts'))
128 if user == post.author:
129 raise CannotDoOnOwnException(_('flag'))
131 if not (user.can_flag_offensive(post)):
132 raise NotEnoughRepPointsException(_('flag posts'))
134 user_flag_count_today = user.get_flagged_items_count_today()
136 if user_flag_count_today >= int(settings.MAX_FLAGS_PER_DAY):
137 raise NotEnoughLeftException(_('flags'), str(settings.MAX_FLAGS_PER_DAY))
140 current = FlagAction.objects.get(canceled=False, user=user, node=post)
141 raise CommandException(
142 _("You already flagged this post with the following reason: %(reason)s") % {'reason': current.extra})
143 except ObjectDoesNotExist:
144 reason = request.POST.get('prompt', '').strip()
147 raise CommandException(_("Reason is empty"))
149 FlagAction(user=user, node=post, extra=reason, ip=request.META['REMOTE_ADDR']).save()
151 return {'message': _("Thank you for your report. A moderator will review your submission shortly.")}
153 @decorate.withfn(command)
154 def like_comment(request, id):
155 comment = get_object_or_404(Comment, id=id)
158 if not user.is_authenticated():
159 raise AnonymousNotAllowedException(_('like comments'))
161 if user == comment.user:
162 raise CannotDoOnOwnException(_('like'))
164 if not user.can_like_comment(comment):
165 raise NotEnoughRepPointsException( _('like comments'))
167 like = VoteAction.get_action_for(node=comment, user=user)
170 like.cancel(ip=request.META['REMOTE_ADDR'])
173 VoteUpCommentAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
178 'update_post_score': [comment.id, likes and 1 or -1],
179 'update_user_post_vote': [comment.id, likes and 'up' or 'none']
183 @decorate.withfn(command)
184 def delete_comment(request, id):
185 comment = get_object_or_404(Comment, id=id)
188 if not user.is_authenticated():
189 raise AnonymousNotAllowedException(_('delete comments'))
191 if not user.can_delete_comment(comment):
192 raise NotEnoughRepPointsException( _('delete comments'))
194 if not comment.nis.deleted:
195 DeleteAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
199 'remove_comment': [comment.id],
203 @decorate.withfn(command)
204 def mark_favorite(request, id):
205 question = get_object_or_404(Question, id=id)
207 if not request.user.is_authenticated():
208 raise AnonymousNotAllowedException(_('mark a question as favorite'))
211 favorite = FavoriteAction.objects.get(canceled=False, node=question, user=request.user)
212 favorite.cancel(ip=request.META['REMOTE_ADDR'])
214 except ObjectDoesNotExist:
215 FavoriteAction(node=question, user=request.user, ip=request.META['REMOTE_ADDR']).save()
220 'update_favorite_count': [added and 1 or -1],
221 'update_favorite_mark': [added and 'on' or 'off']
225 @decorate.withfn(command)
226 def comment(request, id):
227 post = get_object_or_404(Node, id=id)
230 if not user.is_authenticated():
231 raise AnonymousNotAllowedException(_('comment'))
233 if not request.method == 'POST':
234 raise CommandException(_("Invalid request"))
236 comment_text = request.POST.get('comment', '').strip()
238 if not len(comment_text):
239 raise CommandException(_("Comment is empty"))
241 if len(comment_text) < settings.FORM_MIN_COMMENT_BODY:
242 raise CommandException(_("At least %d characters required on comment body.") % settings.FORM_MIN_COMMENT_BODY)
244 if len(comment_text) > settings.FORM_MAX_COMMENT_BODY:
245 raise CommandException(_("No more than %d characters on comment body.") % settings.FORM_MAX_COMMENT_BODY)
247 if 'id' in request.POST:
248 comment = get_object_or_404(Comment, id=request.POST['id'])
250 if not user.can_edit_comment(comment):
251 raise NotEnoughRepPointsException( _('edit comments'))
253 comment = ReviseAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(
254 data=dict(text=comment_text)).node
256 if not user.can_comment(post):
257 raise NotEnoughRepPointsException( _('comment'))
259 comment = CommentAction(user=user, ip=request.META['REMOTE_ADDR']).save(
260 data=dict(text=comment_text, parent=post)).node
262 if comment.active_revision.revision == 1:
266 id, comment.id, comment.comment, user.decorated_name, user.get_profile_url(),
267 reverse('delete_comment', kwargs={'id': comment.id}),
268 reverse('node_markdown', kwargs={'id': comment.id})
275 'update_comment': [comment.id, comment.comment]
279 @decorate.withfn(command)
280 def node_markdown(request, id):
283 if not user.is_authenticated():
284 raise AnonymousNotAllowedException(_('accept answers'))
286 node = get_object_or_404(Node, id=id)
287 return HttpResponse(node.body, mimetype="text/plain")
290 @decorate.withfn(command)
291 def accept_answer(request, id):
292 if settings.DISABLE_ACCEPTING_FEATURE:
297 if not user.is_authenticated():
298 raise AnonymousNotAllowedException(_('accept answers'))
300 answer = get_object_or_404(Answer, id=id)
301 question = answer.question
303 if not user.can_accept_answer(answer):
304 raise CommandException(_("Sorry but you cannot accept the answer"))
308 if answer.nis.accepted:
309 answer.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR'])
310 commands['unmark_accepted'] = [answer.id]
312 if settings.MAXIMUM_ACCEPTED_ANSWERS and (question.accepted_count >= settings.MAXIMUM_ACCEPTED_ANSWERS):
313 raise CommandException(ungettext("This question already has an accepted answer.",
314 "Sorry but this question has reached the limit of accepted answers.", int(settings.MAXIMUM_ACCEPTED_ANSWERS)))
316 if settings.MAXIMUM_ACCEPTED_PER_USER and question.accepted_count:
317 accepted_from_author = question.accepted_answers.filter(author=answer.author).count()
319 if accepted_from_author >= settings.MAXIMUM_ACCEPTED_PER_USER:
320 raise CommandException(ungettext("The author of this answer already has an accepted answer in this question.",
321 "Sorry but the author of this answer has reached the limit of accepted answers per question.", int(settings.MAXIMUM_ACCEPTED_PER_USER)))
324 AcceptAnswerAction(node=answer, user=user, ip=request.META['REMOTE_ADDR']).save()
325 commands['mark_accepted'] = [answer.id]
327 return {'commands': commands}
329 @decorate.withfn(command)
330 def delete_post(request, id):
331 post = get_object_or_404(Node, id=id)
334 if not user.is_authenticated():
335 raise AnonymousNotAllowedException(_('delete posts'))
337 if not (user.can_delete_post(post)):
338 raise NotEnoughRepPointsException(_('delete posts'))
340 ret = {'commands': {}}
343 post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
344 ret['commands']['unmark_deleted'] = [post.node_type, id]
346 DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
348 ret['commands']['mark_deleted'] = [post.node_type, id]
352 @decorate.withfn(command)
353 def close(request, id, close):
354 if close and not request.POST:
355 return render_to_response('node/report.html', {'types': settings.CLOSE_TYPES})
357 question = get_object_or_404(Question, id=id)
360 if not user.is_authenticated():
361 raise AnonymousNotAllowedException(_('close questions'))
363 if question.nis.closed:
364 if not user.can_reopen_question(question):
365 raise NotEnoughRepPointsException(_('reopen questions'))
367 question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
369 if not request.user.can_close_question(question):
370 raise NotEnoughRepPointsException(_('close questions'))
372 reason = request.POST.get('prompt', '').strip()
375 raise CommandException(_("Reason is empty"))
377 CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
379 return RefreshPageCommand()
381 @decorate.withfn(command)
382 def wikify(request, id):
383 node = get_object_or_404(Node, id=id)
386 if not user.is_authenticated():
387 raise AnonymousNotAllowedException(_('mark posts as community wiki'))
390 if not user.can_cancel_wiki(node):
391 raise NotEnoughRepPointsException(_('cancel a community wiki post'))
393 if node.nstate.wiki.action_type == "wikify":
394 node.nstate.wiki.cancel()
396 node.nstate.wiki = None
398 if not user.can_wikify(node):
399 raise NotEnoughRepPointsException(_('mark posts as community wiki'))
401 WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
403 return RefreshPageCommand()
405 @decorate.withfn(command)
406 def convert_to_comment(request, id):
408 answer = get_object_or_404(Answer, id=id)
409 question = answer.question
412 description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': a.author.username,
413 'snippet': a.summary[:10]}
414 nodes = [(question.id, _("Question"))]
415 [nodes.append((a.id, description(a))) for a in
416 question.answers.filter_state(deleted=False).exclude(id=answer.id)]
418 return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
420 if not user.is_authenticated():
421 raise AnonymousNotAllowedException(_("convert answers to comments"))
423 if not user.can_convert_to_comment(answer):
424 raise NotEnoughRepPointsException(_("convert answers to comments"))
427 new_parent = Node.objects.get(id=request.POST.get('under', None))
429 raise CommandException(_("That is an invalid post to put the comment under"))
431 if not (new_parent == question or (new_parent.node_type == 'answer' and new_parent.parent == question)):
432 raise CommandException(_("That is an invalid post to put the comment under"))
434 AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
436 return RefreshPageCommand()
438 @decorate.withfn(command)
439 def subscribe(request, id, user=0):
441 user = User.objects.filter(id=user)[0]
442 if user.is_a_super_user_or_staff():
443 raise CommandException(_("You do not have the correct credentials to preform this action."))
448 question = get_object_or_404(Question, id=id)
451 subscription = QuestionSubscription.objects.get(question=question, user=user)
452 subscription.delete()
455 subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
461 'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
462 'set_subscription_status': ['']
466 #internally grouped views - used by the tagging system
468 def mark_tag(request, tag=None, **kwargs):#tagging system
469 action = kwargs['action']
470 ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
471 if action == 'remove':
472 logging.debug('deleting tag %s' % tag)
475 reason = kwargs['reason']
478 t = Tag.objects.get(name=tag)
479 mt = MarkedTag(user=request.user, reason=reason, tag=t)
484 ts.update(reason=reason)
485 return HttpResponse(simplejson.dumps(''), mimetype="application/json")
487 def matching_tags(request):
488 if len(request.GET['q']) == 0:
489 raise CommandException(_("Invalid request"))
491 possible_tags = Tag.active.filter(name__istartswith = request.GET['q'])
493 for tag in possible_tags:
494 tag_output += (tag.name + "|" + tag.name + "." + tag.used_count.__str__() + "\n")
496 return HttpResponse(tag_output, mimetype="text/plain")
498 def related_questions(request):
499 if request.POST and request.POST.get('title', None):
500 can_rank, questions = Question.objects.search(request.POST['title'])
501 return HttpResponse(simplejson.dumps(
502 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
503 for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")