]> git.openstreetmap.org Git - osqa.git/blob - forum/views/commands.py
4bfcf6be71df90f8b00e4f82395b569cf50f7772
[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, HttpResponseRedirect, 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
342         # If the request is not an AJAX redirect to the answer URL rather than to the home page
343         if not request.is_ajax():
344             return HttpResponseRedirect(answer.get_absolute_url())
345
346         commands['mark_accepted'] = [answer.id]
347
348     return {'commands': commands}
349
350 @decorate.withfn(command)
351 def delete_post(request, id):
352     post = get_object_or_404(Node, id=id)
353     user = request.user
354
355     if not user.is_authenticated():
356         raise AnonymousNotAllowedException(_('delete posts'))
357
358     if not (user.can_delete_post(post)):
359         raise NotEnoughRepPointsException(_('delete posts'))
360
361     ret = {'commands': {}}
362
363     if post.nis.deleted:
364         post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
365         ret['commands']['unmark_deleted'] = [post.node_type, id]
366     else:
367         DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
368
369         ret['commands']['mark_deleted'] = [post.node_type, id]
370
371     return ret
372
373 @decorate.withfn(command)
374 def close(request, id, close):
375     if close and not request.POST:
376         return render_to_response('node/report.html', {'types': settings.CLOSE_TYPES})
377
378     question = get_object_or_404(Question, id=id)
379     user = request.user
380
381     if not user.is_authenticated():
382         raise AnonymousNotAllowedException(_('close questions'))
383
384     if question.nis.closed:
385         if not user.can_reopen_question(question):
386             raise NotEnoughRepPointsException(_('reopen questions'))
387
388         question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
389     else:
390         if not request.user.can_close_question(question):
391             raise NotEnoughRepPointsException(_('close questions'))
392
393         reason = request.POST.get('prompt', '').strip()
394
395         if not len(reason):
396             raise CommandException(_("Reason is empty"))
397
398         CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
399
400     return RefreshPageCommand()
401
402 @decorate.withfn(command)
403 def wikify(request, id):
404     node = get_object_or_404(Node, id=id)
405     user = request.user
406
407     if not user.is_authenticated():
408         raise AnonymousNotAllowedException(_('mark posts as community wiki'))
409
410     if node.nis.wiki:
411         if not user.can_cancel_wiki(node):
412             raise NotEnoughRepPointsException(_('cancel a community wiki post'))
413
414         if node.nstate.wiki.action_type == "wikify":
415             node.nstate.wiki.cancel()
416         else:
417             node.nstate.wiki = None
418     else:
419         if not user.can_wikify(node):
420             raise NotEnoughRepPointsException(_('mark posts as community wiki'))
421
422         WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
423
424     return RefreshPageCommand()
425
426 @decorate.withfn(command)
427 def convert_to_comment(request, id):
428     user = request.user
429     answer = get_object_or_404(Answer, id=id)
430     question = answer.question
431
432     # Check whether the user has the required permissions
433     if not user.is_authenticated():
434         raise AnonymousNotAllowedException(_("convert answers to comments"))
435
436     if not user.can_convert_to_comment(answer):
437         raise NotEnoughRepPointsException(_("convert answers to comments"))
438
439     if not request.POST:
440         description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': smart_unicode(a.author.username),
441                                                                             'snippet': a.summary[:10]}
442         nodes = [(question.id, _("Question"))]
443         [nodes.append((a.id, description(a))) for a in
444          question.answers.filter_state(deleted=False).exclude(id=answer.id)]
445
446         return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
447
448     try:
449         new_parent = Node.objects.get(id=request.POST.get('under', None))
450     except:
451         raise CommandException(_("That is an invalid post to put the comment under"))
452
453     if not (new_parent == question or (new_parent.node_type == 'answer' and new_parent.parent == question)):
454         raise CommandException(_("That is an invalid post to put the comment under"))
455
456     AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
457
458     return RefreshPageCommand()
459
460 @decorate.withfn(command)
461 def convert_comment_to_answer(request, id):
462     user = request.user
463     comment = get_object_or_404(Comment, id=id)
464     parent = comment.parent
465
466     if not parent.question:
467         question = parent
468     else:
469         question = parent.question
470     
471     if not user.is_authenticated():
472         raise AnonymousNotAllowedException(_("convert comments to answers"))
473
474     if not user.can_convert_comment_to_answer(comment):
475         raise NotEnoughRepPointsException(_("convert comments to answers"))
476     
477     CommentToAnswerAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(question=question))
478
479     return RefreshPageCommand()
480
481 @decorate.withfn(command)
482 def subscribe(request, id, user=None):
483     if user:
484         try:
485             user = User.objects.get(id=user)
486         except User.DoesNotExist:
487             raise Http404()
488
489         if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
490             raise CommandException(_("You do not have the correct credentials to preform this action."))
491     else:
492         user = request.user
493
494     question = get_object_or_404(Question, id=id)
495
496     try:
497         subscription = QuestionSubscription.objects.get(question=question, user=user)
498         subscription.delete()
499         subscribed = False
500     except:
501         subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
502         subscription.save()
503         subscribed = True
504
505     return {
506         'commands': {
507             'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
508             'set_subscription_status': ['']
509         }
510     }
511
512 #internally grouped views - used by the tagging system
513 @ajax_login_required
514 def mark_tag(request, tag=None, **kwargs):#tagging system
515     action = kwargs['action']
516     ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
517     if action == 'remove':
518         logging.debug('deleting tag %s' % tag)
519         ts.delete()
520     else:
521         reason = kwargs['reason']
522         if len(ts) == 0:
523             try:
524                 t = Tag.objects.get(name=tag)
525                 mt = MarkedTag(user=request.user, reason=reason, tag=t)
526                 mt.save()
527             except:
528                 pass
529         else:
530             ts.update(reason=reason)
531     return HttpResponse(simplejson.dumps(''), mimetype="application/json")
532
533 def matching_tags(request):
534     if len(request.GET['q']) == 0:
535         raise CommandException(_("Invalid request"))
536
537     possible_tags = Tag.active.filter(name__icontains = request.GET['q'])
538     tag_output = ''
539     for tag in possible_tags:
540         tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
541
542     return HttpResponse(tag_output, mimetype="text/plain")
543
544 def matching_users(request):
545     if len(request.GET['q']) == 0:
546         raise CommandException(_("Invalid request"))
547
548     possible_users = User.objects.filter(username__icontains = request.GET['q'])
549     output = ''
550
551     for user in possible_users:
552         output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
553
554     return HttpResponse(output, mimetype="text/plain")
555
556 def related_questions(request):
557     if request.POST and request.POST.get('title', None):
558         can_rank, questions = Question.objects.search(request.POST['title'])
559
560         if can_rank and isinstance(can_rank, basestring):
561             questions = questions.order_by(can_rank)
562
563         return HttpResponse(simplejson.dumps(
564                 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
565                  for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")
566     else:
567         raise Http404()
568
569 @decorate.withfn(command)
570 def answer_permanent_link(request, id):
571     # Getting the current answer object
572     answer = get_object_or_404(Answer, id=id)
573
574     # Getting the current object URL -- the Application URL + the object relative URL
575     url = '%s%s' % (settings.APP_BASE_URL, answer.get_absolute_url())
576
577     if not request.POST:
578         # Display the template
579         return render_to_response('node/permanent_link.html', { 'url' : url, })
580
581     return {
582         'commands' : {
583             'copy_url' : [request.POST['permanent_link_url'],],
584         },
585         'message' : _("The permanent URL to the answer has been copied to your clipboard."),
586     }
587
588 @decorate.withfn(command)
589 def award_points(request, user_id, answer_id):
590     user = request.user
591     awarded_user = get_object_or_404(User, id=user_id)
592     answer = get_object_or_404(Answer, id=answer_id)
593
594     # Users shouldn't be able to award themselves
595     if awarded_user.id == user.id:
596         raise CannotDoOnOwnException(_("award"))
597
598     # Anonymous users cannot award  points, they just don't have such
599     if not user.is_authenticated():
600         raise AnonymousNotAllowedException(_('award'))
601
602     if not request.POST:
603         return render_to_response("node/award_points.html", { 'user' : user, 'awarded_user' : awarded_user, })
604     else:
605         points = int(request.POST['points'])
606
607         # We should check if the user has enough reputation points, otherwise we raise an exception.
608         if points < 0:
609             raise CommandException(_("The number of points to award needs to be a positive value."))
610
611         if user.reputation < points:
612             raise NotEnoughRepPointsException(_("award"))
613
614         extra = dict(message=request.POST.get('message', ''), awarding_user=request.user.id, value=points)
615
616         # We take points from the awarding user
617         AwardPointsAction(user=request.user, node=answer, extra=extra).save(data=dict(value=points, affected=awarded_user))
618
619         return { 'message' : _("You have awarded %(awarded_user)s with %(points)d points") % {'awarded_user' : awarded_user, 'points' : points} }