]> git.openstreetmap.org Git - osqa.git/blob - forum/views/commands.py
Improved "convert to question" routine, using the question edit form, and the same...
[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 >= user.can_vote_count_today():
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                 reverse('convert_comment', kwargs={'id': comment.id}),            
269                 ]
270         }
271         }
272     else:
273         return {
274         'commands': {
275         'update_comment': [comment.id, comment.comment]
276         }
277         }
278
279 @decorate.withfn(command)
280 def node_markdown(request, id):
281     user = request.user
282
283     if not user.is_authenticated():
284         raise AnonymousNotAllowedException(_('accept answers'))
285
286     node = get_object_or_404(Node, id=id)
287     return HttpResponse(node.active_revision.body, mimetype="text/plain")
288
289
290 @decorate.withfn(command)
291 def accept_answer(request, id):
292     if settings.DISABLE_ACCEPTING_FEATURE:
293         raise Http404()
294
295     user = request.user
296
297     if not user.is_authenticated():
298         raise AnonymousNotAllowedException(_('accept answers'))
299
300     answer = get_object_or_404(Answer, id=id)
301     question = answer.question
302
303     if not user.can_accept_answer(answer):
304         raise CommandException(_("Sorry but you cannot accept the answer"))
305
306     commands = {}
307
308     if answer.nis.accepted:
309         answer.nstate.accepted.cancel(user, ip=request.META['REMOTE_ADDR'])
310         commands['unmark_accepted'] = [answer.id]
311     else:
312         if settings.MAXIMUM_ACCEPTED_ANSWERS and (question.accepted_count >= settings.MAXIMUM_ACCEPTED_ANSWERS):
313             raise CommandException(ungettext("This question already has an accepted answer.",
314                 "Sorry but this question has reached the limit of accepted answers.", int(settings.MAXIMUM_ACCEPTED_ANSWERS)))
315
316         if settings.MAXIMUM_ACCEPTED_PER_USER and question.accepted_count:
317             accepted_from_author = question.accepted_answers.filter(author=answer.author).count()
318
319             if accepted_from_author >= settings.MAXIMUM_ACCEPTED_PER_USER:
320                 raise CommandException(ungettext("The author of this answer already has an accepted answer in this question.",
321                 "Sorry but the author of this answer has reached the limit of accepted answers per question.", int(settings.MAXIMUM_ACCEPTED_PER_USER)))             
322
323
324         AcceptAnswerAction(node=answer, user=user, ip=request.META['REMOTE_ADDR']).save()
325         commands['mark_accepted'] = [answer.id]
326
327     return {'commands': commands}
328
329 @decorate.withfn(command)
330 def delete_post(request, id):
331     post = get_object_or_404(Node, id=id)
332     user = request.user
333
334     if not user.is_authenticated():
335         raise AnonymousNotAllowedException(_('delete posts'))
336
337     if not (user.can_delete_post(post)):
338         raise NotEnoughRepPointsException(_('delete posts'))
339
340     ret = {'commands': {}}
341
342     if post.nis.deleted:
343         post.nstate.deleted.cancel(user, ip=request.META['REMOTE_ADDR'])
344         ret['commands']['unmark_deleted'] = [post.node_type, id]
345     else:
346         DeleteAction(node=post, user=user, ip=request.META['REMOTE_ADDR']).save()
347
348         ret['commands']['mark_deleted'] = [post.node_type, id]
349
350     return ret
351
352 @decorate.withfn(command)
353 def close(request, id, close):
354     if close and not request.POST:
355         return render_to_response('node/report.html', {'types': settings.CLOSE_TYPES})
356
357     question = get_object_or_404(Question, id=id)
358     user = request.user
359
360     if not user.is_authenticated():
361         raise AnonymousNotAllowedException(_('close questions'))
362
363     if question.nis.closed:
364         if not user.can_reopen_question(question):
365             raise NotEnoughRepPointsException(_('reopen questions'))
366
367         question.nstate.closed.cancel(user, ip=request.META['REMOTE_ADDR'])
368     else:
369         if not request.user.can_close_question(question):
370             raise NotEnoughRepPointsException(_('close questions'))
371
372         reason = request.POST.get('prompt', '').strip()
373
374         if not len(reason):
375             raise CommandException(_("Reason is empty"))
376
377         CloseAction(node=question, user=user, extra=reason, ip=request.META['REMOTE_ADDR']).save()
378
379     return RefreshPageCommand()
380
381 @decorate.withfn(command)
382 def wikify(request, id):
383     node = get_object_or_404(Node, id=id)
384     user = request.user
385
386     if not user.is_authenticated():
387         raise AnonymousNotAllowedException(_('mark posts as community wiki'))
388
389     if node.nis.wiki:
390         if not user.can_cancel_wiki(node):
391             raise NotEnoughRepPointsException(_('cancel a community wiki post'))
392
393         if node.nstate.wiki.action_type == "wikify":
394             node.nstate.wiki.cancel()
395         else:
396             node.nstate.wiki = None
397     else:
398         if not user.can_wikify(node):
399             raise NotEnoughRepPointsException(_('mark posts as community wiki'))
400
401         WikifyAction(node=node, user=user, ip=request.META['REMOTE_ADDR']).save()
402
403     return RefreshPageCommand()
404
405 @decorate.withfn(command)
406 def convert_to_comment(request, id):
407     user = request.user
408     answer = get_object_or_404(Answer, id=id)
409     question = answer.question
410
411     if not request.POST:
412         description = lambda a: _("Answer by %(uname)s: %(snippet)s...") % {'uname': a.author.username,
413                                                                             'snippet': a.summary[:10]}
414         nodes = [(question.id, _("Question"))]
415         [nodes.append((a.id, description(a))) for a in
416          question.answers.filter_state(deleted=False).exclude(id=answer.id)]
417
418         return render_to_response('node/convert_to_comment.html', {'answer': answer, 'nodes': nodes})
419
420     if not user.is_authenticated():
421         raise AnonymousNotAllowedException(_("convert answers to comments"))
422
423     if not user.can_convert_to_comment(answer):
424         raise NotEnoughRepPointsException(_("convert answers to comments"))
425
426     try:
427         new_parent = Node.objects.get(id=request.POST.get('under', None))
428     except:
429         raise CommandException(_("That is an invalid post to put the comment under"))
430
431     if not (new_parent == question or (new_parent.node_type == 'answer' and new_parent.parent == question)):
432         raise CommandException(_("That is an invalid post to put the comment under"))
433
434     AnswerToCommentAction(user=user, node=answer, ip=request.META['REMOTE_ADDR']).save(data=dict(new_parent=new_parent))
435
436     return RefreshPageCommand()
437
438 @decorate.withfn(command)
439 def convert_comment_to_answer(request, id):
440     user = request.user
441     comment = get_object_or_404(Comment, id=id)
442     parent = comment.parent
443
444     if not parent.question:
445         question = parent
446     else:
447         question = parent.question
448     
449     if not user.is_authenticated():
450         raise AnonymousNotAllowedException(_("convert comments to answers"))
451
452     if not user.can_convert_comment_to_answer(comment):
453         raise NotEnoughRepPointsException(_("convert comments to answers"))
454     
455     CommentToAnswerAction(user=user, node=comment, ip=request.META['REMOTE_ADDR']).save(data=dict(question=question))
456
457     return RefreshPageCommand()
458
459 @decorate.withfn(command)
460 def subscribe(request, id, user=None):
461     if user:
462         try:
463             user = User.objects.get(id=user)
464         except User.DoesNotExist:
465             raise Http404()
466
467         if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
468             raise CommandException(_("You do not have the correct credentials to preform this action."))
469     else:
470         user = request.user
471
472     question = get_object_or_404(Question, id=id)
473
474     try:
475         subscription = QuestionSubscription.objects.get(question=question, user=user)
476         subscription.delete()
477         subscribed = False
478     except:
479         subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
480         subscription.save()
481         subscribed = True
482
483     return {
484         'commands': {
485             'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
486             'set_subscription_status': ['']
487         }
488     }
489
490 #internally grouped views - used by the tagging system
491 @ajax_login_required
492 def mark_tag(request, tag=None, **kwargs):#tagging system
493     action = kwargs['action']
494     ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
495     if action == 'remove':
496         logging.debug('deleting tag %s' % tag)
497         ts.delete()
498     else:
499         reason = kwargs['reason']
500         if len(ts) == 0:
501             try:
502                 t = Tag.objects.get(name=tag)
503                 mt = MarkedTag(user=request.user, reason=reason, tag=t)
504                 mt.save()
505             except:
506                 pass
507         else:
508             ts.update(reason=reason)
509     return HttpResponse(simplejson.dumps(''), mimetype="application/json")
510
511 def matching_tags(request):
512     if len(request.GET['q']) == 0:
513         raise CommandException(_("Invalid request"))
514
515     possible_tags = Tag.active.filter(name__icontains = request.GET['q'])
516     tag_output = ''
517     for tag in possible_tags:
518         tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
519
520     return HttpResponse(tag_output, mimetype="text/plain")
521
522 def matching_users(request):
523     if len(request.GET['q']) == 0:
524         raise CommandException(_("Invalid request"))
525
526     possible_users = User.objects.filter(username__icontains = request.GET['q'])
527     output = ''
528
529     for user in possible_users:
530         output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
531
532     return HttpResponse(output, mimetype="text/plain")
533
534 def related_questions(request):
535     if request.POST and request.POST.get('title', None):
536         can_rank, questions = Question.objects.search(request.POST['title'])
537         return HttpResponse(simplejson.dumps(
538                 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
539                  for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")
540     else:
541         raise Http404()
542
543 @decorate.withfn(command)
544 def answer_permanent_link(request, id):
545     # Getting the current answer object
546     answer = get_object_or_404(Answer, id=id)
547
548     # Getting the current object URL -- the Application URL + the object relative URL
549     url = '%s%s' % (settings.APP_BASE_URL, answer.get_absolute_url())
550
551     # Display the template
552     return render_to_response('node/permanent_link.html', { 'url' : url, })
553
554 @decorate.withfn(command)
555 def award_points(request, user_id, answer_id):
556     user = request.user
557     awarded_user = get_object_or_404(User, id=user_id)
558
559     # Users shouldn't be able to award themselves
560     if awarded_user.id == user.id:
561         raise CannotDoOnOwnException(_("award"))
562
563     # Anonymous users cannot award  points, they just don't have such
564     if not user.is_authenticated():
565         raise AnonymousNotAllowedException(_('award'))
566
567     return render_to_response("node/award_points.html", { 'user' : user, 'awarded_user' : awarded_user, })
568     # Display the template
569     return render_to_response('node/permanent_link.html', { 'url' : url, })