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