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