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 forum.utils.decorators import ajax_method, ajax_login_required
14 from decorators import command, CommandException, RefreshPageCommand
15 from forum.modules import decorate
16 from forum import settings
19 class NotEnoughRepPointsException(CommandException):
20 def __init__(self, action):
21 super(NotEnoughRepPointsException, self).__init__(
23 """Sorry, but you don't have enough reputation points to %(action)s.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
24 ) % {'action': action, 'faq_url': reverse('faq')}
27 class CannotDoOnOwnException(CommandException):
28 def __init__(self, action):
29 super(CannotDoOnOwnException, self).__init__(
31 """Sorry but you cannot %(action)s your own post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
32 ) % {'action': action, 'faq_url': reverse('faq')}
35 class AnonymousNotAllowedException(CommandException):
36 def __init__(self, action):
37 super(AnonymousNotAllowedException, self).__init__(
39 """Sorry but anonymous users cannot %(action)s.<br />Please login or create an account <a href='%(signin_url)s'>here</a>."""
40 ) % {'action': action, 'signin_url': reverse('auth_signin')}
43 class NotEnoughLeftException(CommandException):
44 def __init__(self, action, limit):
45 super(NotEnoughLeftException, self).__init__(
47 """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>"""
48 ) % {'action': action, 'limit': limit, 'faq_url': reverse('faq')}
51 class CannotDoubleActionException(CommandException):
52 def __init__(self, action):
53 super(CannotDoubleActionException, self).__init__(
55 """Sorry, but you cannot %(action)s twice the same post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
56 ) % {'action': action, 'faq_url': reverse('faq')}
60 @decorate.withfn(command)
61 def vote_post(request, id, vote_type):
62 post = get_object_or_404(Node, id=id).leaf
65 if not user.is_authenticated():
66 raise AnonymousNotAllowedException(_('vote'))
68 if user == post.author:
69 raise CannotDoOnOwnException(_('vote'))
71 if not (vote_type == 'up' and user.can_vote_up() or user.can_vote_down()):
72 raise NotEnoughRepPointsException(vote_type == 'up' and _('upvote') or _('downvote'))
74 user_vote_count_today = user.get_vote_count_today()
76 if user_vote_count_today >= int(settings.MAX_VOTES_PER_DAY):
77 raise NotEnoughLeftException(_('votes'), str(settings.MAX_VOTES_PER_DAY))
79 new_vote_cls = (vote_type == 'up') and VoteUpAction or VoteDownAction
82 old_vote = VoteAction.get_action_for(node=post, user=user)
85 if old_vote.action_date < datetime.datetime.now() - datetime.timedelta(days=int(settings.DENY_UNVOTE_DAYS)):
86 raise CommandException(
87 _("Sorry but you cannot cancel a vote after %(ndays)d %(tdays)s from the original vote") %
88 {'ndays': int(settings.DENY_UNVOTE_DAYS),
89 'tdays': ungettext('day', 'days', int(settings.DENY_UNVOTE_DAYS))}
92 old_vote.cancel(ip=request.META['REMOTE_ADDR'])
93 score_inc += (old_vote.__class__ == VoteDownAction) and 1 or -1
95 if old_vote.__class__ != new_vote_cls:
96 new_vote_cls(user=user, node=post, ip=request.META['REMOTE_ADDR']).save()
97 score_inc += (new_vote_cls == VoteUpAction) and 1 or -1
103 'update_post_score': [id, score_inc],
104 'update_user_post_vote': [id, vote_type]
108 votes_left = (int(settings.MAX_VOTES_PER_DAY) - user_vote_count_today) + (vote_type == 'none' and -1 or 1)
110 if int(settings.START_WARN_VOTES_LEFT) >= votes_left:
111 response['message'] = _("You have %(nvotes)s %(tvotes)s left today.") % \
112 {'nvotes': votes_left, 'tvotes': ungettext('vote', 'votes', votes_left)}
116 @decorate.withfn(command)
117 def flag_post(request, id):
119 return render_to_response('node/report.html', {'types': settings.FLAG_TYPES})
121 post = get_object_or_404(Node, id=id)
124 if not user.is_authenticated():
125 raise AnonymousNotAllowedException(_('flag posts'))
127 if user == post.author:
128 raise CannotDoOnOwnException(_('flag'))
130 if not (user.can_flag_offensive(post)):
131 raise NotEnoughRepPointsException(_('flag posts'))
133 user_flag_count_today = user.get_flagged_items_count_today()
135 if user_flag_count_today >= int(settings.MAX_FLAGS_PER_DAY):
136 raise NotEnoughLeftException(_('flags'), str(settings.MAX_FLAGS_PER_DAY))
139 current = FlagAction.objects.get(canceled=False, user=user, node=post)
140 raise CommandException(
141 _("You already flagged this post with the following reason: %(reason)s") % {'reason': current.extra})
142 except ObjectDoesNotExist:
143 reason = request.POST.get('prompt', '').strip()
146 raise CommandException(_("Reason is empty"))
148 FlagAction(user=user, node=post, extra=reason, ip=request.META['REMOTE_ADDR']).save()
150 return {'message': _("Thank you for your report. A moderator will review your submission shortly.")}
152 @decorate.withfn(command)
153 def like_comment(request, id):
154 comment = get_object_or_404(Comment, id=id)
157 if not user.is_authenticated():
158 raise AnonymousNotAllowedException(_('like comments'))
160 if user == comment.user:
161 raise CannotDoOnOwnException(_('like'))
163 if not user.can_like_comment(comment):
164 raise NotEnoughRepPointsException( _('like comments'))
166 like = VoteAction.get_action_for(node=comment, user=user)
169 like.cancel(ip=request.META['REMOTE_ADDR'])
172 VoteUpCommentAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
177 'update_post_score': [comment.id, likes and 1 or -1],
178 'update_user_post_vote': [comment.id, likes and 'up' or 'none']
182 @decorate.withfn(command)
183 def delete_comment(request, id):
184 comment = get_object_or_404(Comment, id=id)
187 if not user.is_authenticated():
188 raise AnonymousNotAllowedException(_('delete comments'))
190 if not user.can_delete_comment(comment):
191 raise NotEnoughRepPointsException( _('delete comments'))
193 if not comment.nis.deleted:
194 DeleteAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
198 'remove_comment': [comment.id],
202 @decorate.withfn(command)
203 def mark_favorite(request, id):
204 question = get_object_or_404(Question, id=id)
206 if not request.user.is_authenticated():
207 raise AnonymousNotAllowedException(_('mark a question as favorite'))
210 favorite = FavoriteAction.objects.get(canceled=False, node=question, user=request.user)
211 favorite.cancel(ip=request.META['REMOTE_ADDR'])
213 except ObjectDoesNotExist:
214 FavoriteAction(node=question, user=request.user, ip=request.META['REMOTE_ADDR']).save()
219 'update_favorite_count': [added and 1 or -1],
220 'update_favorite_mark': [added and 'on' or 'off']
224 @decorate.withfn(command)
225 def comment(request, id):
226 post = get_object_or_404(Node, id=id)
229 if not user.is_authenticated():
230 raise AnonymousNotAllowedException(_('comment'))
232 if not request.method == 'POST':
233 raise CommandException(_("Invalid request"))
235 comment_text = request.POST.get('comment', '').strip()
237 if not len(comment_text):
238 raise CommandException(_("Comment is empty"))
240 if len(comment_text) < settings.FORM_MIN_COMMENT_BODY:
241 raise CommandException(_("At least %d characters required on comment body.") % settings.FORM_MIN_COMMENT_BODY)
243 if len(comment_text) > settings.FORM_MAX_COMMENT_BODY:
244 raise CommandException(_("No more than %d characters on comment body.") % settings.FORM_MAX_COMMENT_BODY)
246 if 'id' in request.POST:
247 comment = get_object_or_404(Comment, id=request.POST['id'])
249 if not user.can_edit_comment(comment):
250 raise NotEnoughRepPointsException( _('edit comments'))
252 comment = ReviseAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(
253 data=dict(text=comment_text)).node
255 if not user.can_comment(post):
256 raise NotEnoughRepPointsException( _('comment'))
258 comment = CommentAction(user=user, ip=request.META['REMOTE_ADDR']).save(
259 data=dict(text=comment_text, parent=post)).node
261 if comment.active_revision.revision == 1:
265 id, comment.id, comment.comment, user.decorated_name, user.get_profile_url(),
266 reverse('delete_comment', kwargs={'id': comment.id}),
267 reverse('node_markdown', 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.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 subscribe(request, id, user=None):
441 user = User.objects.get(id=user)
442 except User.DoesNotExist:
445 if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
446 raise CommandException(_("You do not have the correct credentials to preform this action."))
450 question = get_object_or_404(Question, id=id)
453 subscription = QuestionSubscription.objects.get(question=question, user=user)
454 subscription.delete()
457 subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
463 'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
464 'set_subscription_status': ['']
468 #internally grouped views - used by the tagging system
470 def mark_tag(request, tag=None, **kwargs):#tagging system
471 action = kwargs['action']
472 ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
473 if action == 'remove':
474 logging.debug('deleting tag %s' % tag)
477 reason = kwargs['reason']
480 t = Tag.objects.get(name=tag)
481 mt = MarkedTag(user=request.user, reason=reason, tag=t)
486 ts.update(reason=reason)
487 return HttpResponse(simplejson.dumps(''), mimetype="application/json")
489 def matching_tags(request):
490 if len(request.GET['q']) == 0:
491 raise CommandException(_("Invalid request"))
493 possible_tags = Tag.active.filter(name__istartswith = request.GET['q'])
495 for tag in possible_tags:
496 tag_output += (tag.name + "|" + tag.name + "." + tag.used_count.__str__() + "\n")
498 return HttpResponse(tag_output, mimetype="text/plain")
500 def related_questions(request):
501 if request.POST and request.POST.get('title', None):
502 can_rank, questions = Question.objects.search(request.POST['title'])
503 return HttpResponse(simplejson.dumps(
504 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
505 for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")