]> git.openstreetmap.org Git - osqa.git/blob - forum/views/commands.py
Work on issue #OSQA-510. Added support for ReCaptcha for users with low reputation...
[osqa.git] / forum / views / commands.py
1 import datetime
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
17 import logging
18
19 class NotEnoughRepPointsException(CommandException):
20     def __init__(self, action):
21         super(NotEnoughRepPointsException, self).__init__(
22                 _(
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')}
25                 )
26
27 class CannotDoOnOwnException(CommandException):
28     def __init__(self, action):
29         super(CannotDoOnOwnException, self).__init__(
30                 _(
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')}
33                 )
34
35 class AnonymousNotAllowedException(CommandException):
36     def __init__(self, action):
37         super(AnonymousNotAllowedException, self).__init__(
38                 _(
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')}
41                 )
42
43 class NotEnoughLeftException(CommandException):
44     def __init__(self, action, limit):
45         super(NotEnoughLeftException, self).__init__(
46                 _(
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')}
49                 )
50
51 class CannotDoubleActionException(CommandException):
52     def __init__(self, action):
53         super(CannotDoubleActionException, self).__init__(
54                 _(
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')}
57                 )
58
59
60 @decorate.withfn(command)
61 def vote_post(request, id, vote_type):
62     post = get_object_or_404(Node, id=id).leaf
63     user = request.user
64
65     if not user.is_authenticated():
66         raise AnonymousNotAllowedException(_('vote'))
67
68     if user == post.author:
69         raise CannotDoOnOwnException(_('vote'))
70
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'))
73
74     user_vote_count_today = user.get_vote_count_today()
75
76     if user_vote_count_today >= user.can_vote_count_today():
77         raise NotEnoughLeftException(_('votes'), str(settings.MAX_VOTES_PER_DAY))
78
79     new_vote_cls = (vote_type == 'up') and VoteUpAction or VoteDownAction
80     score_inc = 0
81
82     old_vote = VoteAction.get_action_for(node=post, user=user)
83
84     if old_vote:
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))}
90                     )
91
92         old_vote.cancel(ip=request.META['REMOTE_ADDR'])
93         score_inc += (old_vote.__class__ == VoteDownAction) and 1 or -1
94
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
98     else:
99         vote_type = "none"
100
101     response = {
102     'commands': {
103     'update_post_score': [id, score_inc],
104     'update_user_post_vote': [id, vote_type]
105     }
106     }
107
108     votes_left = (int(settings.MAX_VOTES_PER_DAY) - user_vote_count_today) + (vote_type == 'none' and -1 or 1)
109
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)}
113
114     return response
115
116 @decorate.withfn(command)
117 def flag_post(request, id):
118     if not request.POST:
119         return render_to_response('node/report.html', {'types': settings.FLAG_TYPES})
120
121     post = get_object_or_404(Node, id=id)
122     user = request.user
123
124     if not user.is_authenticated():
125         raise AnonymousNotAllowedException(_('flag posts'))
126
127     if user == post.author:
128         raise CannotDoOnOwnException(_('flag'))
129
130     if not (user.can_flag_offensive(post)):
131         raise NotEnoughRepPointsException(_('flag posts'))
132
133     user_flag_count_today = user.get_flagged_items_count_today()
134
135     if user_flag_count_today >= int(settings.MAX_FLAGS_PER_DAY):
136         raise NotEnoughLeftException(_('flags'), str(settings.MAX_FLAGS_PER_DAY))
137
138     try:
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()
144
145         if not len(reason):
146             raise CommandException(_("Reason is empty"))
147
148         FlagAction(user=user, node=post, extra=reason, ip=request.META['REMOTE_ADDR']).save()
149
150     return {'message': _("Thank you for your report. A moderator will review your submission shortly.")}
151
152 @decorate.withfn(command)
153 def like_comment(request, id):
154     comment = get_object_or_404(Comment, id=id)
155     user = request.user
156
157     if not user.is_authenticated():
158         raise AnonymousNotAllowedException(_('like comments'))
159
160     if user == comment.user:
161         raise CannotDoOnOwnException(_('like'))
162
163     if not user.can_like_comment(comment):
164         raise NotEnoughRepPointsException( _('like comments'))
165
166     like = VoteAction.get_action_for(node=comment, user=user)
167
168     if like:
169         like.cancel(ip=request.META['REMOTE_ADDR'])
170         likes = False
171     else:
172         VoteUpCommentAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
173         likes = True
174
175     return {
176     'commands': {
177     'update_post_score': [comment.id, likes and 1 or -1],
178     'update_user_post_vote': [comment.id, likes and 'up' or 'none']
179     }
180     }
181
182 @decorate.withfn(command)
183 def delete_comment(request, id):
184     comment = get_object_or_404(Comment, id=id)
185     user = request.user
186
187     if not user.is_authenticated():
188         raise AnonymousNotAllowedException(_('delete comments'))
189
190     if not user.can_delete_comment(comment):
191         raise NotEnoughRepPointsException( _('delete comments'))
192
193     if not comment.nis.deleted:
194         DeleteAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
195
196     return {
197     'commands': {
198     'remove_comment': [comment.id],
199     }
200     }
201
202 @decorate.withfn(command)
203 def mark_favorite(request, id):
204     question = get_object_or_404(Question, id=id)
205
206     if not request.user.is_authenticated():
207         raise AnonymousNotAllowedException(_('mark a question as favorite'))
208
209     try:
210         favorite = FavoriteAction.objects.get(canceled=False, node=question, user=request.user)
211         favorite.cancel(ip=request.META['REMOTE_ADDR'])
212         added = False
213     except ObjectDoesNotExist:
214         FavoriteAction(node=question, user=request.user, ip=request.META['REMOTE_ADDR']).save()
215         added = True
216
217     return {
218     'commands': {
219     'update_favorite_count': [added and 1 or -1],
220     'update_favorite_mark': [added and 'on' or 'off']
221     }
222     }
223
224 @decorate.withfn(command)
225 def comment(request, id):
226     post = get_object_or_404(Node, id=id)
227     user = request.user
228
229     if not user.is_authenticated():
230         raise AnonymousNotAllowedException(_('comment'))
231
232     if not request.method == 'POST':
233         raise CommandException(_("Invalid request"))
234
235     comment_text = request.POST.get('comment', '').strip()
236
237     if not len(comment_text):
238         raise CommandException(_("Comment is empty"))
239
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)
242
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)
245
246     if 'id' in request.POST:
247         comment = get_object_or_404(Comment, id=request.POST['id'])
248
249         if not user.can_edit_comment(comment):
250             raise NotEnoughRepPointsException( _('edit comments'))
251
252         comment = ReviseAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(
253                 data=dict(text=comment_text)).node
254     else:
255         if not user.can_comment(post):
256             raise NotEnoughRepPointsException( _('comment'))
257
258         comment = CommentAction(user=user, ip=request.META['REMOTE_ADDR']).save(
259                 data=dict(text=comment_text, parent=post)).node
260
261     if comment.active_revision.revision == 1:
262         return {
263         'commands': {
264         'insert_comment': [
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                 ]
270         }
271         }
272     else:
273         return {
274         'commands': {
275         'update_comment': [comment.id, comment.comment]
276         }
277         }
278
279 @decorate.withfn(command)
280 def node_markdown(request, id):
281     user = request.user
282
283     if not user.is_authenticated():
284         raise AnonymousNotAllowedException(_('accept answers'))
285
286     node = get_object_or_404(Node, id=id)
287     return HttpResponse(node.active_revision.body, mimetype="text/plain")
288
289
290 @decorate.withfn(command)
291 def accept_answer(request, id):
292     if settings.DISABLE_ACCEPTING_FEATURE:
293         raise Http404()
294
295     user = request.user
296
297     if not user.is_authenticated():
298         raise AnonymousNotAllowedException(_('accept answers'))
299
300     answer = get_object_or_404(Answer, id=id)
301     question = answer.question
302
303     if not user.can_accept_answer(answer):
304         raise CommandException(_("Sorry but you cannot accept the answer"))
305
306     commands = {}
307
308     if answer.nis.accepted:
309         answer.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR'])
310         commands['unmark_accepted'] = [answer.id]
311     else:
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)))
315
316         if settings.MAXIMUM_ACCEPTED_PER_USER and question.accepted_count:
317             accepted_from_author = question.accepted_answers.filter(author=answer.author).count()
318
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)))             
322
323
324         AcceptAnswerAction(node=answer, user=user, ip=request.META['REMOTE_ADDR']).save()
325         commands['mark_accepted'] = [answer.id]
326
327     return {'commands': commands}
328
329 @decorate.withfn(command)
330 def delete_post(request, id):
331     post = get_object_or_404(Node, id=id)
332     user = request.user
333
334     if not user.is_authenticated():
335         raise AnonymousNotAllowedException(_('delete posts'))
336
337     if not (user.can_delete_post(post)):
338         raise NotEnoughRepPointsException(_('delete posts'))
339
340     ret = {'commands': {}}
341
342     if post.nis.deleted:
343         post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
344         ret['commands']['unmark_deleted'] = [post.node_type, id]
345     else:
346         DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
347
348         ret['commands']['mark_deleted'] = [post.node_type, id]
349
350     return ret
351
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})
356
357     question = get_object_or_404(Question, id=id)
358     user = request.user
359
360     if not user.is_authenticated():
361         raise AnonymousNotAllowedException(_('close questions'))
362
363     if question.nis.closed:
364         if not user.can_reopen_question(question):
365             raise NotEnoughRepPointsException(_('reopen questions'))
366
367         question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
368     else:
369         if not request.user.can_close_question(question):
370             raise NotEnoughRepPointsException(_('close questions'))
371
372         reason = request.POST.get('prompt', '').strip()
373
374         if not len(reason):
375             raise CommandException(_("Reason is empty"))
376
377         CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
378
379     return RefreshPageCommand()
380
381 @decorate.withfn(command)
382 def wikify(request, id):
383     node = get_object_or_404(Node, id=id)
384     user = request.user
385
386     if not user.is_authenticated():
387         raise AnonymousNotAllowedException(_('mark posts as community wiki'))
388
389     if node.nis.wiki:
390         if not user.can_cancel_wiki(node):
391             raise NotEnoughRepPointsException(_('cancel a community wiki post'))
392
393         if node.nstate.wiki.action_type == "wikify":
394             node.nstate.wiki.cancel()
395         else:
396             node.nstate.wiki = None
397     else:
398         if not user.can_wikify(node):
399             raise NotEnoughRepPointsException(_('mark posts as community wiki'))
400
401         WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
402
403     return RefreshPageCommand()
404
405 @decorate.withfn(command)
406 def convert_to_comment(request, id):
407     user = request.user
408     answer = get_object_or_404(Answer, id=id)
409     question = answer.question
410
411     if not request.POST:
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)]
417
418         return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
419
420     if not user.is_authenticated():
421         raise AnonymousNotAllowedException(_("convert answers to comments"))
422
423     if not user.can_convert_to_comment(answer):
424         raise NotEnoughRepPointsException(_("convert answers to comments"))
425
426     try:
427         new_parent = Node.objects.get(id=request.POST.get('under', None))
428     except:
429         raise CommandException(_("That is an invalid post to put the comment under"))
430
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"))
433
434     AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
435
436     return RefreshPageCommand()
437
438 @decorate.withfn(command)
439 def convert_comment_to_answer(request, id):
440     user = request.user
441     comment = get_object_or_404(Comment, id=id)
442     parent = comment.parent
443
444     if not parent.question:
445         question = parent
446     else:
447         question = parent.question
448     
449     if not user.is_authenticated():
450         raise AnonymousNotAllowedException(_("convert comments to answers"))
451
452     if not user.can_convert_comment_to_answer(comment):
453         raise NotEnoughRepPointsException(_("convert comments to answers"))
454     
455     CommentToAnswerAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(question=question))
456
457     return RefreshPageCommand()
458
459 @decorate.withfn(command)
460 def convert_to_question(request, id):
461     user = request.user
462     answer = get_object_or_404(Answer, id=id)
463     question = answer.question
464
465     if not request.POST:
466         description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': a.author.username,
467                                                                             'snippet': a.summary[:10]}
468         nodes = [(question.id, _("Question"))]
469         [nodes.append((a.id, description(a))) for a in
470          question.answers.filter_state(deleted=False).exclude(id=answer.id)]
471
472         return render_to_response('node/convert_to_question.html', {'answer': answer})
473
474     if not user.is_authenticated():
475         raise AnonymousNotAllowedException(_("convert answers to questions"))
476
477     if not user.can_convert_to_question(answer):
478         raise NotEnoughRepPointsException(_("convert answers to questions"))
479
480     try:
481         title = request.POST.get('title', None)
482     except:
483         raise CommandException(_("You haven't specified the title of the new question"))
484
485     AnswerToQuestionAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(title=title))
486
487     return RefreshPageCommand()
488
489 @decorate.withfn(command)
490 def subscribe(request, id, user=None):
491     if user:
492         try:
493             user = User.objects.get(id=user)
494         except User.DoesNotExist:
495             raise Http404()
496
497         if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
498             raise CommandException(_("You do not have the correct credentials to preform this action."))
499     else:
500         user = request.user
501
502     question = get_object_or_404(Question, id=id)
503
504     try:
505         subscription = QuestionSubscription.objects.get(question=question, user=user)
506         subscription.delete()
507         subscribed = False
508     except:
509         subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
510         subscription.save()
511         subscribed = True
512
513     return {
514         'commands': {
515             'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
516             'set_subscription_status': ['']
517         }
518     }
519
520 #internally grouped views - used by the tagging system
521 @ajax_login_required
522 def mark_tag(request, tag=None, **kwargs):#tagging system
523     action = kwargs['action']
524     ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
525     if action == 'remove':
526         logging.debug('deleting tag %s' % tag)
527         ts.delete()
528     else:
529         reason = kwargs['reason']
530         if len(ts) == 0:
531             try:
532                 t = Tag.objects.get(name=tag)
533                 mt = MarkedTag(user=request.user, reason=reason, tag=t)
534                 mt.save()
535             except:
536                 pass
537         else:
538             ts.update(reason=reason)
539     return HttpResponse(simplejson.dumps(''), mimetype="application/json")
540
541 def matching_tags(request):
542     if len(request.GET['q']) == 0:
543         raise CommandException(_("Invalid request"))
544
545     possible_tags = Tag.active.filter(name__icontains = request.GET['q'])
546     tag_output = ''
547     for tag in possible_tags:
548         tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
549
550     return HttpResponse(tag_output, mimetype="text/plain")
551
552 def matching_users(request):
553     if len(request.GET['q']) == 0:
554         raise CommandException(_("Invalid request"))
555
556     possible_users = User.objects.filter(username__icontains = request.GET['q'])
557     output = ''
558
559     for user in possible_users:
560         output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
561
562     return HttpResponse(output, mimetype="text/plain")
563
564 def related_questions(request):
565     if request.POST and request.POST.get('title', None):
566         can_rank, questions = Question.objects.search(request.POST['title'])
567         return HttpResponse(simplejson.dumps(
568                 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
569                  for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")
570     else:
571         raise Http404()
572
573
574
575
576
577
578