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