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