]> git.openstreetmap.org Git - osqa.git/blob - forum/views/commands.py
OSQA-497, adding a new settings that allows to specify whether the permissions to...
[osqa.git] / forum / views / commands.py
1 import datetime
2 import logging
3
4 from forum import settings
5 from django.core.exceptions import ObjectDoesNotExist
6 from django.core.urlresolvers import reverse
7 from django.utils import simplejson
8 from django.utils.encoding import smart_unicode
9 from django.utils.translation import ungettext, ugettext as _
10 from django.http import HttpResponse, HttpResponseRedirect, Http404
11 from django.shortcuts import get_object_or_404, render_to_response
12 from django.template import RequestContext
13
14 from django.template.loader import render_to_string
15 from forum.models import *
16 from forum.models.node import NodeMetaClass
17 from forum.utils.decorators import ajax_method, ajax_login_required
18 from forum.actions import *
19 from forum.modules import decorate
20 from forum import settings
21
22 from decorators import command, CommandException, RefreshPageCommand
23
24 class NotEnoughRepPointsException(CommandException):
25     def __init__(self, action):
26         super(NotEnoughRepPointsException, self).__init__(
27                 _(
28                         """Sorry, but you don't have enough reputation points to %(action)s.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
29                         ) % {'action': action, 'faq_url': reverse('faq')}
30                 )
31
32 class CannotDoOnOwnException(CommandException):
33     def __init__(self, action):
34         super(CannotDoOnOwnException, self).__init__(
35                 _(
36                         """Sorry but you cannot %(action)s your own post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
37                         ) % {'action': action, 'faq_url': reverse('faq')}
38                 )
39
40 class AnonymousNotAllowedException(CommandException):
41     def __init__(self, action):
42         super(AnonymousNotAllowedException, self).__init__(
43                 _(
44                         """Sorry but anonymous users cannot %(action)s.<br />Please login or create an account <a href='%(signin_url)s'>here</a>."""
45                         ) % {'action': action, 'signin_url': reverse('auth_signin')}
46                 )
47
48 class NotEnoughLeftException(CommandException):
49     def __init__(self, action, limit):
50         super(NotEnoughLeftException, self).__init__(
51                 _(
52                         """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>"""
53                         ) % {'action': action, 'limit': limit, 'faq_url': reverse('faq')}
54                 )
55
56 class CannotDoubleActionException(CommandException):
57     def __init__(self, action):
58         super(CannotDoubleActionException, self).__init__(
59                 _(
60                         """Sorry, but you cannot %(action)s twice the same post.<br />Please check the <a href='%(faq_url)s'>faq</a>"""
61                         ) % {'action': action, 'faq_url': reverse('faq')}
62                 )
63
64
65 @decorate.withfn(command)
66 def vote_post(request, id, vote_type):
67     post = get_object_or_404(Node, id=id).leaf
68     user = request.user
69
70     if not user.is_authenticated():
71         raise AnonymousNotAllowedException(_('vote'))
72
73     if user == post.author:
74         raise CannotDoOnOwnException(_('vote'))
75
76     if not (vote_type == 'up' and user.can_vote_up() or user.can_vote_down()):
77         raise NotEnoughRepPointsException(vote_type == 'up' and _('upvote') or _('downvote'))
78
79     user_vote_count_today = user.get_vote_count_today()
80     user_can_vote_count_today = user.can_vote_count_today()
81
82     if user_vote_count_today >= user.can_vote_count_today():
83         raise NotEnoughLeftException(_('votes'), str(settings.MAX_VOTES_PER_DAY))
84
85     new_vote_cls = (vote_type == 'up') and VoteUpAction or VoteDownAction
86     score_inc = 0
87
88     old_vote = VoteAction.get_action_for(node=post, user=user)
89
90     if old_vote:
91         if old_vote.action_date < datetime.datetime.now() - datetime.timedelta(days=int(settings.DENY_UNVOTE_DAYS)):
92             raise CommandException(
93                     _("Sorry but you cannot cancel a vote after %(ndays)d %(tdays)s from the original vote") %
94                     {'ndays': int(settings.DENY_UNVOTE_DAYS),
95                      'tdays': ungettext('day', 'days', int(settings.DENY_UNVOTE_DAYS))}
96                     )
97
98         old_vote.cancel(ip=request.META['REMOTE_ADDR'])
99         score_inc = (old_vote.__class__ == VoteDownAction) and 1 or -1
100         vote_type = "none"
101     else:
102         new_vote_cls(user=user, node=post, ip=request.META['REMOTE_ADDR']).save()
103         score_inc = (new_vote_cls == VoteUpAction) and 1 or -1
104
105     response = {
106     'commands': {
107     'update_post_score': [id, score_inc],
108     'update_user_post_vote': [id, vote_type]
109     }
110     }
111
112     votes_left = (user_can_vote_count_today - user_vote_count_today) + (vote_type == 'none' and -1 or 1)
113
114     if int(settings.START_WARN_VOTES_LEFT) >= votes_left:
115         response['message'] = _("You have %(nvotes)s %(tvotes)s left today.") % \
116                     {'nvotes': votes_left, 'tvotes': ungettext('vote', 'votes', votes_left)}
117
118     return response
119
120 @decorate.withfn(command)
121 def flag_post(request, id):
122     if not request.POST:
123         return render_to_response('node/report.html', {'types': settings.FLAG_TYPES})
124
125     post = get_object_or_404(Node, id=id)
126     user = request.user
127
128     if not user.is_authenticated():
129         raise AnonymousNotAllowedException(_('flag posts'))
130
131     if user == post.author:
132         raise CannotDoOnOwnException(_('flag'))
133
134     if not (user.can_flag_offensive(post)):
135         raise NotEnoughRepPointsException(_('flag posts'))
136
137     user_flag_count_today = user.get_flagged_items_count_today()
138
139     if user_flag_count_today >= int(settings.MAX_FLAGS_PER_DAY):
140         raise NotEnoughLeftException(_('flags'), str(settings.MAX_FLAGS_PER_DAY))
141
142     try:
143         current = FlagAction.objects.get(canceled=False, user=user, node=post)
144         raise CommandException(
145                 _("You already flagged this post with the following reason: %(reason)s") % {'reason': current.extra})
146     except ObjectDoesNotExist:
147         reason = request.POST.get('prompt', '').strip()
148
149         if not len(reason):
150             raise CommandException(_("Reason is empty"))
151
152         FlagAction(user=user, node=post, extra=reason, ip=request.META['REMOTE_ADDR']).save()
153
154     return {'message': _("Thank you for your report. A moderator will review your submission shortly.")}
155
156 @decorate.withfn(command)
157 def like_comment(request, id):
158     comment = get_object_or_404(Comment, id=id)
159     user = request.user
160
161     if not user.is_authenticated():
162         raise AnonymousNotAllowedException(_('like comments'))
163
164     if user == comment.user:
165         raise CannotDoOnOwnException(_('like'))
166
167     if not user.can_like_comment(comment):
168         raise NotEnoughRepPointsException( _('like comments'))
169
170     like = VoteAction.get_action_for(node=comment, user=user)
171
172     if like:
173         like.cancel(ip=request.META['REMOTE_ADDR'])
174         likes = False
175     else:
176         VoteUpCommentAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
177         likes = True
178
179     return {
180     'commands': {
181     'update_post_score': [comment.id, likes and 1 or -1],
182     'update_user_post_vote': [comment.id, likes and 'up' or 'none']
183     }
184     }
185
186 @decorate.withfn(command)
187 def delete_comment(request, id):
188     comment = get_object_or_404(Comment, id=id)
189     user = request.user
190
191     if not user.is_authenticated():
192         raise AnonymousNotAllowedException(_('delete comments'))
193
194     if not user.can_delete_comment(comment):
195         raise NotEnoughRepPointsException( _('delete comments'))
196
197     if not comment.nis.deleted:
198         DeleteAction(node=comment, user=user, ip=request.META['REMOTE_ADDR']).save()
199
200     return {
201     'commands': {
202     'remove_comment': [comment.id],
203     }
204     }
205
206 @decorate.withfn(command)
207 def mark_favorite(request, id):
208     question = get_object_or_404(Question, id=id)
209
210     if not request.user.is_authenticated():
211         raise AnonymousNotAllowedException(_('mark a question as favorite'))
212
213     try:
214         favorite = FavoriteAction.objects.get(canceled=False, node=question, user=request.user)
215         favorite.cancel(ip=request.META['REMOTE_ADDR'])
216         added = False
217     except ObjectDoesNotExist:
218         FavoriteAction(node=question, user=request.user, ip=request.META['REMOTE_ADDR']).save()
219         added = True
220
221     return {
222     'commands': {
223     'update_favorite_count': [added and 1 or -1],
224     'update_favorite_mark': [added and 'on' or 'off']
225     }
226     }
227
228 @decorate.withfn(command)
229 def comment(request, id):
230     post = get_object_or_404(Node, id=id)
231     user = request.user
232
233     if not user.is_authenticated():
234         raise AnonymousNotAllowedException(_('comment'))
235
236     if not request.method == 'POST':
237         raise CommandException(_("Invalid request"))
238
239     comment_text = request.POST.get('comment', '').strip()
240
241     if not len(comment_text):
242         raise CommandException(_("Comment is empty"))
243
244     if len(comment_text) < settings.FORM_MIN_COMMENT_BODY:
245         raise CommandException(_("At least %d characters required on comment body.") % settings.FORM_MIN_COMMENT_BODY)
246
247     if len(comment_text) > settings.FORM_MAX_COMMENT_BODY:
248         raise CommandException(_("No more than %d characters on comment body.") % settings.FORM_MAX_COMMENT_BODY)
249
250     if 'id' in request.POST:
251         comment = get_object_or_404(Comment, id=request.POST['id'])
252
253         if not user.can_edit_comment(comment):
254             raise NotEnoughRepPointsException( _('edit comments'))
255
256         comment = ReviseAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(
257                 data=dict(text=comment_text)).node
258     else:
259         if not user.can_comment(post):
260             raise NotEnoughRepPointsException( _('comment'))
261
262         comment = CommentAction(user=user, ip=request.META['REMOTE_ADDR']).save(
263                 data=dict(text=comment_text, parent=post)).node
264
265     if comment.active_revision.revision == 1:
266         return {
267         'commands': {
268         'insert_comment': [
269                 id, comment.id, comment.comment, user.decorated_name, user.get_profile_url(),
270                 reverse('delete_comment', kwargs={'id': comment.id}),
271                 reverse('node_markdown', kwargs={'id': comment.id}),
272                 reverse('convert_comment', kwargs={'id': comment.id}),
273                 user.can_convert_comment_to_answer(comment),
274                 ]
275         }
276         }
277     else:
278         return {
279         'commands': {
280         'update_comment': [comment.id, comment.comment]
281         }
282         }
283
284 @decorate.withfn(command)
285 def node_markdown(request, id):
286     user = request.user
287
288     if not user.is_authenticated():
289         raise AnonymousNotAllowedException(_('accept answers'))
290
291     node = get_object_or_404(Node, id=id)
292     return HttpResponse(node.active_revision.body, mimetype="text/plain")
293
294
295 @decorate.withfn(command)
296 def accept_answer(request, id):
297     if settings.DISABLE_ACCEPTING_FEATURE:
298         raise Http404()
299
300     user = request.user
301
302     if not user.is_authenticated():
303         raise AnonymousNotAllowedException(_('accept answers'))
304
305     answer = get_object_or_404(Answer, id=id)
306     question = answer.question
307
308     if not user.can_accept_answer(answer):
309         raise CommandException(_("Sorry but you cannot accept the answer"))
310
311     commands = {}
312
313     if answer.nis.accepted:
314         answer.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR'])
315         commands['unmark_accepted'] = [answer.id]
316     else:
317         if settings.MAXIMUM_ACCEPTED_ANSWERS and (question.accepted_count >= settings.MAXIMUM_ACCEPTED_ANSWERS):
318             raise CommandException(ungettext("This question already has an accepted answer.",
319                 "Sorry but this question has reached the limit of accepted answers.", int(settings.MAXIMUM_ACCEPTED_ANSWERS)))
320
321         if settings.MAXIMUM_ACCEPTED_PER_USER and question.accepted_count:
322             accepted_from_author = question.accepted_answers.filter(author=answer.author).count()
323
324             if accepted_from_author >= settings.MAXIMUM_ACCEPTED_PER_USER:
325                 raise CommandException(ungettext("The author of this answer already has an accepted answer in this question.",
326                 "Sorry but the author of this answer has reached the limit of accepted answers per question.", int(settings.MAXIMUM_ACCEPTED_PER_USER)))             
327
328
329         AcceptAnswerAction(node=answer, user=user, ip=request.META['REMOTE_ADDR']).save()
330         commands['mark_accepted'] = [answer.id]
331
332     return {'commands': commands}
333
334 @decorate.withfn(command)
335 def delete_post(request, id):
336     post = get_object_or_404(Node, id=id)
337     user = request.user
338
339     if not user.is_authenticated():
340         raise AnonymousNotAllowedException(_('delete posts'))
341
342     if not (user.can_delete_post(post)):
343         raise NotEnoughRepPointsException(_('delete posts'))
344
345     ret = {'commands': {}}
346
347     if post.nis.deleted:
348         post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
349         ret['commands']['unmark_deleted'] = [post.node_type, id]
350     else:
351         DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
352
353         ret['commands']['mark_deleted'] = [post.node_type, id]
354
355     return ret
356
357 @decorate.withfn(command)
358 def close(request, id, close):
359     if close and not request.POST:
360         return render_to_response('node/report.html', {'types': settings.CLOSE_TYPES})
361
362     question = get_object_or_404(Question, id=id)
363     user = request.user
364
365     if not user.is_authenticated():
366         raise AnonymousNotAllowedException(_('close questions'))
367
368     if question.nis.closed:
369         if not user.can_reopen_question(question):
370             raise NotEnoughRepPointsException(_('reopen questions'))
371
372         question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
373     else:
374         if not request.user.can_close_question(question):
375             raise NotEnoughRepPointsException(_('close questions'))
376
377         reason = request.POST.get('prompt', '').strip()
378
379         if not len(reason):
380             raise CommandException(_("Reason is empty"))
381
382         CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
383
384     return RefreshPageCommand()
385
386 @decorate.withfn(command)
387 def wikify(request, id):
388     node = get_object_or_404(Node, id=id)
389     user = request.user
390
391     if not user.is_authenticated():
392         raise AnonymousNotAllowedException(_('mark posts as community wiki'))
393
394     if node.nis.wiki:
395         if not user.can_cancel_wiki(node):
396             raise NotEnoughRepPointsException(_('cancel a community wiki post'))
397
398         if node.nstate.wiki.action_type == "wikify":
399             node.nstate.wiki.cancel()
400         else:
401             node.nstate.wiki = None
402     else:
403         if not user.can_wikify(node):
404             raise NotEnoughRepPointsException(_('mark posts as community wiki'))
405
406         WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
407
408     return RefreshPageCommand()
409
410 @decorate.withfn(command)
411 def convert_to_comment(request, id):
412     user = request.user
413     answer = get_object_or_404(Answer, id=id)
414     question = answer.question
415
416     # Check whether the user has the required permissions
417     if not user.is_authenticated():
418         raise AnonymousNotAllowedException(_("convert answers to comments"))
419
420     if not user.can_convert_to_comment(answer):
421         raise NotEnoughRepPointsException(_("convert answers to comments"))
422
423     if not request.POST:
424         description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': smart_unicode(a.author.username),
425                                                                             'snippet': a.summary[:10]}
426         nodes = [(question.id, _("Question"))]
427         [nodes.append((a.id, description(a))) for a in
428          question.answers.filter_state(deleted=False).exclude(id=answer.id)]
429
430         return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
431
432     try:
433         new_parent = Node.objects.get(id=request.POST.get('under', None))
434     except:
435         raise CommandException(_("That is an invalid post to put the comment under"))
436
437     if not (new_parent == question or (new_parent.node_type == 'answer' and new_parent.parent == question)):
438         raise CommandException(_("That is an invalid post to put the comment under"))
439
440     AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
441
442     return RefreshPageCommand()
443
444 @decorate.withfn(command)
445 def convert_comment_to_answer(request, id):
446     user = request.user
447     comment = get_object_or_404(Comment, id=id)
448     parent = comment.parent
449
450     if not parent.question:
451         question = parent
452     else:
453         question = parent.question
454     
455     if not user.is_authenticated():
456         raise AnonymousNotAllowedException(_("convert comments to answers"))
457
458     if not user.can_convert_comment_to_answer(comment):
459         raise NotEnoughRepPointsException(_("convert comments to answers"))
460     
461     CommentToAnswerAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(question=question))
462
463     return RefreshPageCommand()
464
465 @decorate.withfn(command)
466 def subscribe(request, id, user=None):
467     if user:
468         try:
469             user = User.objects.get(id=user)
470         except User.DoesNotExist:
471             raise Http404()
472
473         if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
474             raise CommandException(_("You do not have the correct credentials to preform this action."))
475     else:
476         user = request.user
477
478     question = get_object_or_404(Question, id=id)
479
480     try:
481         subscription = QuestionSubscription.objects.get(question=question, user=user)
482         subscription.delete()
483         subscribed = False
484     except:
485         subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
486         subscription.save()
487         subscribed = True
488
489     return {
490         'commands': {
491             'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
492             'set_subscription_status': ['']
493         }
494     }
495
496 @decorate.withfn(command)
497 def canned_comments(request, post_id):
498     user = request.user
499
500     # Check whether the user has the required permissions to use the tool.
501     if not user.can_use_canned_comments:
502         raise CommandException(_("You cannot use the canned comments tool."))
503
504     if not request.POST:
505         canned_comments = []
506         for comment in settings.CANNED_COMMENTS:
507             canned_comments.append(smart_unicode(comment))
508
509         return render_to_response('node/canned_comments.html', {
510             'canned_comments' : canned_comments,
511         }, RequestContext(request))
512
513     comment = request.POST.get('comment', '')
514
515     return {
516         'commands' : {
517             'canned_comment' : [post_id, comment],
518         }
519     }
520
521 #internally grouped views - used by the tagging system
522 @ajax_login_required
523 def mark_tag(request, tag=None, **kwargs):#tagging system
524     action = kwargs['action']
525     ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
526     if action == 'remove':
527         logging.debug('deleting tag %s' % tag)
528         ts.delete()
529     else:
530         reason = kwargs['reason']
531         if len(ts) == 0:
532             try:
533                 t = Tag.objects.get(name=tag)
534                 mt = MarkedTag(user=request.user, reason=reason, tag=t)
535                 mt.save()
536             except:
537                 pass
538         else:
539             ts.update(reason=reason)
540     return HttpResponse(simplejson.dumps(''), mimetype="application/json")
541
542 def matching_tags(request):
543     if len(request.GET['q']) == 0:
544         raise CommandException(_("Invalid request"))
545
546     possible_tags = Tag.active.filter(name__icontains = request.GET['q'])
547     tag_output = ''
548     for tag in possible_tags:
549         tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
550
551     return HttpResponse(tag_output, mimetype="text/plain")
552
553 def matching_users(request):
554     if len(request.GET['q']) == 0:
555         raise CommandException(_("Invalid request"))
556
557     possible_users = User.objects.filter(username__icontains = request.GET['q'])
558     output = ''
559
560     for user in possible_users:
561         output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
562
563     return HttpResponse(output, mimetype="text/plain")
564
565 def related_questions(request):
566     if request.POST and request.POST.get('title', None):
567         can_rank, questions = Question.objects.search(request.POST['title'])
568
569         if can_rank and isinstance(can_rank, basestring):
570             questions = questions.order_by(can_rank)
571
572         return HttpResponse(simplejson.dumps(
573                 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
574                  for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")
575     else:
576         raise Http404()
577
578 @decorate.withfn(command)
579 def answer_permanent_link(request, id):
580     # Getting the current answer object
581     answer = get_object_or_404(Answer, id=id)
582
583     # Getting the current object URL -- the Application URL + the object relative URL
584     url = '%s%s' % (settings.APP_BASE_URL, answer.get_absolute_url())
585
586     if not request.POST:
587         # Display the template
588         return render_to_response('node/permanent_link.html', { 'url' : url, })
589
590     return {
591         'commands' : {
592             'copy_url' : [request.POST['permanent_link_url'],],
593         },
594         'message' : _("The permanent URL to the answer has been copied to your clipboard."),
595     }
596
597 @decorate.withfn(command)
598 def award_points(request, user_id, answer_id):
599     user = request.user
600     awarded_user = get_object_or_404(User, id=user_id)
601     answer = get_object_or_404(Answer, id=answer_id)
602
603     # Users shouldn't be able to award themselves
604     if awarded_user.id == user.id:
605         raise CannotDoOnOwnException(_("award"))
606
607     # Anonymous users cannot award  points, they just don't have such
608     if not user.is_authenticated():
609         raise AnonymousNotAllowedException(_('award'))
610
611     if not request.POST:
612         return render_to_response("node/award_points.html", { 'user' : user, 'awarded_user' : awarded_user, })
613     else:
614         points = int(request.POST['points'])
615
616         # We should check if the user has enough reputation points, otherwise we raise an exception.
617         if points < 0:
618             raise CommandException(_("The number of points to award needs to be a positive value."))
619
620         if user.reputation < points:
621             raise NotEnoughRepPointsException(_("award"))
622
623         extra = dict(message=request.POST.get('message', ''), awarding_user=request.user.id, value=points)
624
625         # We take points from the awarding user
626         AwardPointsAction(user=request.user, node=answer, extra=extra).save(data=dict(value=points, affected=awarded_user))
627
628         return { 'message' : _("You have awarded %(awarded_user)s with %(points)d points") % {'awarded_user' : awarded_user, 'points' : points} }