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()
76 user_can_vote_count_today = user.can_vote_count_today()
78 if user_vote_count_today >= user.can_vote_count_today():
79 raise NotEnoughLeftException(_('votes'), str(settings.MAX_VOTES_PER_DAY))
81 new_vote_cls = (vote_type == 'up') and VoteUpAction or VoteDownAction
84 old_vote = VoteAction.get_action_for(node=post, user=user)
87 if old_vote.action_date < datetime.datetime.now() - datetime.timedelta(days=int(settings.DENY_UNVOTE_DAYS)):
88 raise CommandException(
89 _("Sorry but you cannot cancel a vote after %(ndays)d %(tdays)s from the original vote") %
90 {'ndays': int(settings.DENY_UNVOTE_DAYS),
91 'tdays': ungettext('day', 'days', int(settings.DENY_UNVOTE_DAYS))}
94 old_vote.cancel(ip=request.META['REMOTE_ADDR'])
95 score_inc = (old_vote.__class__ == VoteDownAction) and 1 or -1
98 new_vote_cls(user=user, node=post, ip=request.META['REMOTE_ADDR']).save()
99 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 = (user_can_vote_count_today - 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}),
268 reverse('convert_comment', kwargs={'id': comment.id}),
269 user.can_convert_comment_to_answer(comment),
276 'update_comment': [comment.id, comment.comment]
280 @decorate.withfn(command)
281 def node_markdown(request, id):
284 if not user.is_authenticated():
285 raise AnonymousNotAllowedException(_('accept answers'))
287 node = get_object_or_404(Node, id=id)
288 return HttpResponse(node.active_revision.body, mimetype="text/plain")
291 @decorate.withfn(command)
292 def accept_answer(request, id):
293 if settings.DISABLE_ACCEPTING_FEATURE:
298 if not user.is_authenticated():
299 raise AnonymousNotAllowedException(_('accept answers'))
301 answer = get_object_or_404(Answer, id=id)
302 question = answer.question
304 if not user.can_accept_answer(answer):
305 raise CommandException(_("Sorry but you cannot accept the answer"))
309 if answer.nis.accepted:
310 answer.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR'])
311 commands['unmark_accepted'] = [answer.id]
313 if settings.MAXIMUM_ACCEPTED_ANSWERS and (question.accepted_count >= settings.MAXIMUM_ACCEPTED_ANSWERS):
314 raise CommandException(ungettext("This question already has an accepted answer.",
315 "Sorry but this question has reached the limit of accepted answers.", int(settings.MAXIMUM_ACCEPTED_ANSWERS)))
317 if settings.MAXIMUM_ACCEPTED_PER_USER and question.accepted_count:
318 accepted_from_author = question.accepted_answers.filter(author=answer.author).count()
320 if accepted_from_author >= settings.MAXIMUM_ACCEPTED_PER_USER:
321 raise CommandException(ungettext("The author of this answer already has an accepted answer in this question.",
322 "Sorry but the author of this answer has reached the limit of accepted answers per question.", int(settings.MAXIMUM_ACCEPTED_PER_USER)))
325 AcceptAnswerAction(node=answer, user=user, ip=request.META['REMOTE_ADDR']).save()
326 commands['mark_accepted'] = [answer.id]
328 return {'commands': commands}
330 @decorate.withfn(command)
331 def delete_post(request, id):
332 post = get_object_or_404(Node, id=id)
335 if not user.is_authenticated():
336 raise AnonymousNotAllowedException(_('delete posts'))
338 if not (user.can_delete_post(post)):
339 raise NotEnoughRepPointsException(_('delete posts'))
341 ret = {'commands': {}}
344 post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
345 ret['commands']['unmark_deleted'] = [post.node_type, id]
347 DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
349 ret['commands']['mark_deleted'] = [post.node_type, id]
353 @decorate.withfn(command)
354 def close(request, id, close):
355 if close and not request.POST:
356 return render_to_response('node/report.html', {'types': settings.CLOSE_TYPES})
358 question = get_object_or_404(Question, id=id)
361 if not user.is_authenticated():
362 raise AnonymousNotAllowedException(_('close questions'))
364 if question.nis.closed:
365 if not user.can_reopen_question(question):
366 raise NotEnoughRepPointsException(_('reopen questions'))
368 question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
370 if not request.user.can_close_question(question):
371 raise NotEnoughRepPointsException(_('close questions'))
373 reason = request.POST.get('prompt', '').strip()
376 raise CommandException(_("Reason is empty"))
378 CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
380 return RefreshPageCommand()
382 @decorate.withfn(command)
383 def wikify(request, id):
384 node = get_object_or_404(Node, id=id)
387 if not user.is_authenticated():
388 raise AnonymousNotAllowedException(_('mark posts as community wiki'))
391 if not user.can_cancel_wiki(node):
392 raise NotEnoughRepPointsException(_('cancel a community wiki post'))
394 if node.nstate.wiki.action_type == "wikify":
395 node.nstate.wiki.cancel()
397 node.nstate.wiki = None
399 if not user.can_wikify(node):
400 raise NotEnoughRepPointsException(_('mark posts as community wiki'))
402 WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
404 return RefreshPageCommand()
406 @decorate.withfn(command)
407 def convert_to_comment(request, id):
409 answer = get_object_or_404(Answer, id=id)
410 question = answer.question
413 description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': a.author.username,
414 'snippet': a.summary[:10]}
415 nodes = [(question.id, _("Question"))]
416 [nodes.append((a.id, description(a))) for a in
417 question.answers.filter_state(deleted=False).exclude(id=answer.id)]
419 return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
421 if not user.is_authenticated():
422 raise AnonymousNotAllowedException(_("convert answers to comments"))
424 if not user.can_convert_to_comment(answer):
425 raise NotEnoughRepPointsException(_("convert answers to comments"))
428 new_parent = Node.objects.get(id=request.POST.get('under', None))
430 raise CommandException(_("That is an invalid post to put the comment under"))
432 if not (new_parent == question or (new_parent.node_type == 'answer' and new_parent.parent == question)):
433 raise CommandException(_("That is an invalid post to put the comment under"))
435 AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
437 return RefreshPageCommand()
439 @decorate.withfn(command)
440 def convert_comment_to_answer(request, id):
442 comment = get_object_or_404(Comment, id=id)
443 parent = comment.parent
445 if not parent.question:
448 question = parent.question
450 if not user.is_authenticated():
451 raise AnonymousNotAllowedException(_("convert comments to answers"))
453 if not user.can_convert_comment_to_answer(comment):
454 raise NotEnoughRepPointsException(_("convert comments to answers"))
456 CommentToAnswerAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(question=question))
458 return RefreshPageCommand()
460 @decorate.withfn(command)
461 def subscribe(request, id, user=None):
464 user = User.objects.get(id=user)
465 except User.DoesNotExist:
468 if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
469 raise CommandException(_("You do not have the correct credentials to preform this action."))
473 question = get_object_or_404(Question, id=id)
476 subscription = QuestionSubscription.objects.get(question=question, user=user)
477 subscription.delete()
480 subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
486 'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
487 'set_subscription_status': ['']
491 #internally grouped views - used by the tagging system
493 def mark_tag(request, tag=None, **kwargs):#tagging system
494 action = kwargs['action']
495 ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
496 if action == 'remove':
497 logging.debug('deleting tag %s' % tag)
500 reason = kwargs['reason']
503 t = Tag.objects.get(name=tag)
504 mt = MarkedTag(user=request.user, reason=reason, tag=t)
509 ts.update(reason=reason)
510 return HttpResponse(simplejson.dumps(''), mimetype="application/json")
512 def matching_tags(request):
513 if len(request.GET['q']) == 0:
514 raise CommandException(_("Invalid request"))
516 possible_tags = Tag.active.filter(name__icontains = request.GET['q'])
518 for tag in possible_tags:
519 tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
521 return HttpResponse(tag_output, mimetype="text/plain")
523 def matching_users(request):
524 if len(request.GET['q']) == 0:
525 raise CommandException(_("Invalid request"))
527 possible_users = User.objects.filter(username__icontains = request.GET['q'])
530 for user in possible_users:
531 output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
533 return HttpResponse(output, mimetype="text/plain")
535 def related_questions(request):
536 if request.POST and request.POST.get('title', None):
537 can_rank, questions = Question.objects.search(request.POST['title'])
539 if can_rank and isinstance(can_rank, basestring):
540 questions = questions.order_by(can_rank)
542 return HttpResponse(simplejson.dumps(
543 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
544 for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")
548 @decorate.withfn(command)
549 def answer_permanent_link(request, id):
550 # Getting the current answer object
551 answer = get_object_or_404(Answer, id=id)
553 # Getting the current object URL -- the Application URL + the object relative URL
554 url = '%s%s' % (settings.APP_BASE_URL, answer.get_absolute_url())
557 # Display the template
558 return render_to_response('node/permanent_link.html', { 'url' : url, })
562 'copy_url' : [request.POST['permanent_link_url'],],
564 'message' : _("The permanent URL to the answer has been copied to your clipboard."),
567 @decorate.withfn(command)
568 def award_points(request, user_id, answer_id):
570 awarded_user = get_object_or_404(User, id=user_id)
571 answer = get_object_or_404(Answer, id=answer_id)
573 # Users shouldn't be able to award themselves
574 if awarded_user.id == user.id:
575 raise CannotDoOnOwnException(_("award"))
577 # Anonymous users cannot award points, they just don't have such
578 if not user.is_authenticated():
579 raise AnonymousNotAllowedException(_('award'))
582 return render_to_response("node/award_points.html", { 'user' : user, 'awarded_user' : awarded_user, })
584 points = int(request.POST['points'])
586 # We should check if the user has enough reputation points, otherwise we raise an exception.
588 raise CommandException(_("The number of points to award needs to be a positive value."))
590 if user.reputation < points:
591 raise NotEnoughRepPointsException(_("award"))
593 extra = dict(message=request.POST.get('message', ''), awarding_user=request.user.id, value=points)
595 # We take points from the awarding user
596 AwardPointsAction(user=request.user, node=answer, extra=extra).save(data=dict(value=points, affected=awarded_user))
598 return { 'message' : _("You have awarded %(awarded_user)s with %(points)d points") % {'awarded_user' : awarded_user, 'points' : points} }