]> git.openstreetmap.org Git - osqa.git/blob - forum/views/commands.py
Applies patches by Justing Grant. Makes several improvements in the db exporter.
[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 subscribe(request, id, user=None):
439     if user:
440         try:
441             user = User.objects.get(id=user)
442         except User.DoesNotExist:
443             raise Http404()
444
445         if not (request.user.is_a_super_user_or_staff() or user.is_authenticated()):
446             raise CommandException(_("You do not have the correct credentials to preform this action."))
447     else:
448         user = request.user
449
450     question = get_object_or_404(Question, id=id)
451
452     try:
453         subscription = QuestionSubscription.objects.get(question=question, user=user)
454         subscription.delete()
455         subscribed = False
456     except:
457         subscription = QuestionSubscription(question=question, user=user, auto_subscription=False)
458         subscription.save()
459         subscribed = True
460
461     return {
462         'commands': {
463             'set_subscription_button': [subscribed and _('unsubscribe me') or _('subscribe me')],
464             'set_subscription_status': ['']
465         }
466     }
467
468 #internally grouped views - used by the tagging system
469 @ajax_login_required
470 def mark_tag(request, tag=None, **kwargs):#tagging system
471     action = kwargs['action']
472     ts = MarkedTag.objects.filter(user=request.user, tag__name=tag)
473     if action == 'remove':
474         logging.debug('deleting tag %s' % tag)
475         ts.delete()
476     else:
477         reason = kwargs['reason']
478         if len(ts) == 0:
479             try:
480                 t = Tag.objects.get(name=tag)
481                 mt = MarkedTag(user=request.user, reason=reason, tag=t)
482                 mt.save()
483             except:
484                 pass
485         else:
486             ts.update(reason=reason)
487     return HttpResponse(simplejson.dumps(''), mimetype="application/json")
488
489 def matching_tags(request):
490     if len(request.GET['q']) == 0:
491         raise CommandException(_("Invalid request"))
492
493     possible_tags = Tag.active.filter(name__icontains = request.GET['q'])
494     tag_output = ''
495     for tag in possible_tags:
496         tag_output += "%s|%s|%s\n" % (tag.id, tag.name, tag.used_count)
497
498     return HttpResponse(tag_output, mimetype="text/plain")
499
500 def matching_users(request):
501     if len(request.GET['q']) == 0:
502         raise CommandException(_("Invalid request"))
503
504     possible_users = User.objects.filter(username__icontains = request.GET['q'])
505     output = ''
506
507     for user in possible_users:
508         output += ("%s|%s|%s\n" % (user.id, user.decorated_name, user.reputation))
509
510     return HttpResponse(output, mimetype="text/plain")
511
512 def related_questions(request):
513     if request.POST and request.POST.get('title', None):
514         can_rank, questions = Question.objects.search(request.POST['title'])
515         return HttpResponse(simplejson.dumps(
516                 [dict(title=q.title, url=q.get_absolute_url(), score=q.score, summary=q.summary)
517                  for q in questions.filter_state(deleted=False)[0:10]]), mimetype="application/json")
518     else:
519         raise Http404()
520
521
522
523
524
525
526