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