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