]> git.openstreetmap.org Git - osqa.git/blob - forum/views/admin.py
adding support for focusing answers when linking to them
[osqa.git] / forum / views / admin.py
1 from datetime import datetime, timedelta
2 import time
3
4 from django.views.decorators.csrf import csrf_exempt
5 from django.shortcuts import render_to_response, get_object_or_404
6 from django.core.urlresolvers import reverse
7 from django.http import HttpResponseRedirect, HttpResponse, Http404
8 from django.template import RequestContext
9 from django.utils.translation import ugettext as _
10 from django.utils import simplejson
11 from django.db import models
12
13 from forum.http_responses import HttpResponseUnauthorized
14 from forum.settings.base import Setting
15 from forum.forms import MaintenanceModeForm, PageForm, CreateUserForm
16 from forum.settings.forms import SettingsSetForm
17 from forum.utils import pagination, html
18 from forum.utils.mail import send_template_email
19 from forum.models import Question, Answer, User, Node, Action, Page, NodeState, Tag
20 from forum.models.node import NodeMetaClass
21 from forum.actions import NewPageAction, EditPageAction, PublishAction, DeleteAction, UserJoinsAction, CloseAction
22 from forum import settings
23
24 TOOLS = {}
25
26 def super_user_required(fn):
27     def wrapper(request, *args, **kwargs):
28         if request.user.is_authenticated() and request.user.is_superuser:
29             return fn(request, *args, **kwargs)
30         else:
31             return HttpResponseUnauthorized(request)
32
33     return wrapper
34
35 def staff_user_required(fn):
36     def wrapper(request, *args, **kwargs):
37         if request.user.is_authenticated() and (request.user.is_staff or request.user.is_superuser):
38             return fn(request, *args, **kwargs)
39         else:
40             return HttpResponseUnauthorized(request)
41
42     return wrapper
43
44 def admin_page_wrapper(fn, request, *args, **kwargs):
45     res = fn(request, *args, **kwargs)
46     if isinstance(res, HttpResponse):
47         return res
48
49     template, context = res
50     context['basetemplate'] = settings.DJSTYLE_ADMIN_INTERFACE and "osqaadmin/djstyle_base.html" or "osqaadmin/base.html"
51     context['allsets'] = Setting.sets
52     context['othersets'] = sorted(
53             [s for s in Setting.sets.values() if not s.name in
54             ('basic', 'users', 'email', 'paths', 'extkeys', 'repgain', 'minrep', 'voting', 'accept', 'badges', 'about', 'faq', 'sidebar',
55             'form', 'moderation', 'css', 'headandfoot', 'head', 'view', 'urls')]
56             , lambda s1, s2: s1.weight - s2.weight)
57
58     context['tools'] = [(name, fn.label) for name, fn in TOOLS.items()]
59
60     unsaved = request.session.get('previewing_settings', {})
61     context['unsaved'] = set([getattr(settings, s).set.name for s in unsaved.keys() if hasattr(settings, s)])
62
63     return render_to_response(template, context, context_instance=RequestContext(request))
64
65 def admin_page(fn):
66     @super_user_required
67     def wrapper(request, *args, **kwargs):
68         return admin_page_wrapper(fn, request, *args, **kwargs)
69
70     return wrapper
71
72 def moderation_page(fn):
73     @staff_user_required
74     def wrapper(request, *args, **kwargs):
75         return admin_page_wrapper(fn, request, *args, **kwargs)
76
77     return wrapper
78
79 def admin_tools_page(name, label):    
80     def decorator(fn):
81         fn = admin_page(fn)
82         fn.label = label
83         TOOLS[name] = fn
84
85         return fn
86     return decorator
87
88 class ActivityPaginatorContext(pagination.PaginatorContext):
89     def __init__(self):
90         super (ActivityPaginatorContext, self).__init__('ADMIN_RECENT_ACTIVITY', pagesizes=(20, 40, 80), default_pagesize=40)
91
92 @admin_page
93 def dashboard(request):
94     return ('osqaadmin/dashboard.html', pagination.paginated(request, ("recent_activity", ActivityPaginatorContext()), {
95     'settings_pack': unicode(settings.SETTINGS_PACK),
96     'statistics': get_statistics(),
97     'recent_activity': get_recent_activity(),
98     'flagged_posts': get_flagged_posts(),
99     }))
100
101 @super_user_required
102 def interface_switch(request):
103     if request.GET and request.GET.get('to', None) and request.GET['to'] in ('default', 'djstyle'):
104         settings.DJSTYLE_ADMIN_INTERFACE.set_value(request.GET['to'] == 'djstyle')
105
106     return HttpResponseRedirect(reverse('admin_index'))
107
108 @admin_page
109 def statistics(request):
110     today = datetime.now()
111     last_month = today - timedelta(days=30)
112
113     last_month_questions = Question.objects.filter_state(deleted=False).filter(added_at__gt=last_month
114                                                                                ).order_by('added_at').values_list(
115             'added_at', flat=True)
116
117     last_month_n_questions = Question.objects.filter_state(deleted=False).filter(added_at__lt=last_month).count()
118     qgraph_data = simplejson.dumps([
119     (time.mktime(d.timetuple()) * 1000, i + last_month_n_questions)
120     for i, d in enumerate(last_month_questions)
121     ])
122
123     last_month_users = User.objects.filter(date_joined__gt=last_month
124                                            ).order_by('date_joined').values_list('date_joined', flat=True)
125
126     last_month_n_users = User.objects.filter(date_joined__lt=last_month).count()
127
128     ugraph_data = simplejson.dumps([
129     (time.mktime(d.timetuple()) * 1000, i + last_month_n_users)
130     for i, d in enumerate(last_month_users)
131     ])
132
133     return 'osqaadmin/statistics.html', {
134     'graphs': [
135             {
136             'id': 'questions_graph',
137             'caption': _("Questions Graph"),
138             'data': qgraph_data
139             }, {
140             'id': 'userss_graph',
141             'caption': _("Users Graph"),
142             'data': ugraph_data
143             }
144             ]
145     }
146
147 @admin_page
148 def tools_page(request, name):
149     if not name in TOOLS:
150         raise Http404
151
152     return TOOLS[name](request)
153
154
155 @admin_page
156 def settings_set(request, set_name):
157     set = Setting.sets.get(set_name, {})
158     current_preview = request.session.get('previewing_settings', {})
159
160     if set is None:
161         raise Http404
162
163     if request.POST:
164         form = SettingsSetForm(set, data=request.POST, files=request.FILES)
165
166         if form.is_valid():
167             if 'preview' in request.POST:
168                 current_preview.update(form.cleaned_data)
169                 request.session['previewing_settings'] = current_preview
170
171                 return HttpResponseRedirect(reverse('index'))
172             else:
173                 for s in set:
174                     current_preview.pop(s.name, None)
175
176                 request.session['previewing_settings'] = current_preview
177
178                 if not 'reset' in request.POST:
179                     form.save()
180                     request.user.message_set.create(message=_("'%s' settings saved succesfully") % set_name)
181
182                     if set_name in ('minrep', 'badges', 'repgain'):
183                         settings.SETTINGS_PACK.set_value("custom")
184
185                 return HttpResponseRedirect(reverse('admin_set', args=[set_name]))
186     else:
187         form = SettingsSetForm(set, unsaved=current_preview)
188
189     return 'osqaadmin/set.html', {
190     'form': form,
191     'markdown': set.markdown,
192     }
193
194 @super_user_required
195 def get_default(request, set_name, var_name):
196     set = Setting.sets.get(set_name, None)
197     if set is None: raise Http404
198
199     setting = dict([(s.name, s) for s in set]).get(var_name, None)
200     if setting is None: raise Http404
201
202     setting.to_default()
203
204     if request.is_ajax():
205         return HttpResponse(setting.default)
206     else:
207         return HttpResponseRedirect(reverse('admin_set', kwargs={'set_name': set_name}))
208
209
210 def get_recent_activity():
211     return Action.objects.order_by('-action_date')
212
213 def get_flagged_posts():
214     return Action.objects.filter(canceled=False, action_type="flag").order_by('-action_date')[0:30]
215
216 def get_statistics():
217     return {
218     'total_users': User.objects.all().count(),
219     'users_last_24': User.objects.filter(date_joined__gt=(datetime.now() - timedelta(days=1))).count(),
220     'total_questions': Question.objects.filter_state(deleted=False).count(),
221     'questions_last_24': Question.objects.filter_state(deleted=False).filter(
222             added_at__gt=(datetime.now() - timedelta(days=1))).count(),
223     'total_answers': Answer.objects.filter_state(deleted=False).count(),
224     'answers_last_24': Answer.objects.filter_state(deleted=False).filter(
225             added_at__gt=(datetime.now() - timedelta(days=1))).count(),
226     }
227
228 @super_user_required
229 def go_bootstrap(request):
230 #todo: this is the quick and dirty way of implementing a bootstrap mode
231     try:
232         from forum_modules.default_badges import settings as dbsets
233         dbsets.POPULAR_QUESTION_VIEWS.set_value(100)
234         dbsets.NOTABLE_QUESTION_VIEWS.set_value(200)
235         dbsets.FAMOUS_QUESTION_VIEWS.set_value(300)
236         dbsets.NICE_ANSWER_VOTES_UP.set_value(2)
237         dbsets.NICE_QUESTION_VOTES_UP.set_value(2)
238         dbsets.GOOD_ANSWER_VOTES_UP.set_value(4)
239         dbsets.GOOD_QUESTION_VOTES_UP.set_value(4)
240         dbsets.GREAT_ANSWER_VOTES_UP.set_value(8)
241         dbsets.GREAT_QUESTION_VOTES_UP.set_value(8)
242         dbsets.FAVORITE_QUESTION_FAVS.set_value(1)
243         dbsets.STELLAR_QUESTION_FAVS.set_value(3)
244         dbsets.DISCIPLINED_MIN_SCORE.set_value(3)
245         dbsets.PEER_PRESSURE_MAX_SCORE.set_value(-3)
246         dbsets.CIVIC_DUTY_VOTES.set_value(15)
247         dbsets.PUNDIT_COMMENT_COUNT.set_value(10)
248         dbsets.SELF_LEARNER_UP_VOTES.set_value(2)
249         dbsets.STRUNK_AND_WHITE_EDITS.set_value(10)
250         dbsets.ENLIGHTENED_UP_VOTES.set_value(2)
251         dbsets.GURU_UP_VOTES.set_value(4)
252         dbsets.NECROMANCER_UP_VOTES.set_value(2)
253         dbsets.NECROMANCER_DIF_DAYS.set_value(30)
254         dbsets.TAXONOMIST_USE_COUNT.set_value(5)
255     except:
256         pass
257
258     settings.REP_TO_VOTE_UP.set_value(0)
259     settings.REP_TO_VOTE_DOWN.set_value(15)
260     settings.REP_TO_FLAG.set_value(15)
261     settings.REP_TO_COMMENT.set_value(0)
262     settings.REP_TO_LIKE_COMMENT.set_value(0)
263     settings.REP_TO_UPLOAD.set_value(0)
264     settings.REP_TO_CREATE_TAGS.set_value(0)
265     settings.REP_TO_CLOSE_OWN.set_value(60)
266     settings.REP_TO_REOPEN_OWN.set_value(120)
267     settings.REP_TO_RETAG.set_value(150)
268     settings.REP_TO_EDIT_WIKI.set_value(200)
269     settings.REP_TO_EDIT_OTHERS.set_value(400)
270     settings.REP_TO_CLOSE_OTHERS.set_value(600)
271     settings.REP_TO_DELETE_COMMENTS.set_value(400)
272     settings.REP_TO_VIEW_FLAGS.set_value(30)
273
274     settings.INITIAL_REP.set_value(1)
275     settings.MAX_REP_BY_UPVOTE_DAY.set_value(300)
276     settings.REP_GAIN_BY_UPVOTED.set_value(15)
277     settings.REP_LOST_BY_DOWNVOTED.set_value(1)
278     settings.REP_LOST_BY_DOWNVOTING.set_value(0)
279     settings.REP_GAIN_BY_ACCEPTED.set_value(25)
280     settings.REP_GAIN_BY_ACCEPTING.set_value(5)
281     settings.REP_LOST_BY_FLAGGED.set_value(2)
282     settings.REP_LOST_BY_FLAGGED_3_TIMES.set_value(30)
283     settings.REP_LOST_BY_FLAGGED_5_TIMES.set_value(100)
284
285     settings.SETTINGS_PACK.set_value("bootstrap")
286
287     request.user.message_set.create(message=_('Bootstrap mode enabled'))
288     return HttpResponseRedirect(reverse('admin_index'))
289
290 @super_user_required
291 def go_defaults(request):
292     for setting in Setting.sets['badges']:
293         setting.to_default()
294     for setting in Setting.sets['minrep']:
295         setting.to_default()
296     for setting in Setting.sets['repgain']:
297         setting.to_default()
298
299     settings.SETTINGS_PACK.set_value("default")
300
301     request.user.message_set.create(message=_('All values reverted to defaults'))
302     return HttpResponseRedirect(reverse('admin_index'))
303
304
305 @super_user_required
306 def recalculate_denormalized(request):
307     for n in Node.objects.all():
308         n = n.leaf
309         n.score = n.votes.aggregate(score=models.Sum('value'))['score']
310         if not n.score: n.score = 0
311         n.save()
312
313     for u in User.objects.all():
314         u.reputation = u.reputes.aggregate(reputation=models.Sum('value'))['reputation']
315         u.save()
316
317     request.user.message_set.create(message=_('All values recalculated'))
318     return HttpResponseRedirect(reverse('admin_index'))
319
320 @admin_page
321 def maintenance(request):
322     if request.POST:
323         if 'close' in request.POST or 'adjust' in request.POST:
324             form = MaintenanceModeForm(request.POST)
325
326             if form.is_valid():
327                 settings.MAINTAINANCE_MODE.set_value({
328                 'allow_ips': form.cleaned_data['ips'],
329                 'message': form.cleaned_data['message']})
330
331                 if 'close' in request.POST:
332                     message = _('Maintenance mode enabled')
333                 else:
334                     message = _('Settings adjusted')
335
336                 request.user.message_set.create(message=message)
337
338                 return HttpResponseRedirect(reverse('admin_maintenance'))
339         elif 'open' in request.POST:
340             settings.MAINTAINANCE_MODE.set_value(None)
341             request.user.message_set.create(message=_("Your site is now running normally"))
342             return HttpResponseRedirect(reverse('admin_maintenance'))
343     else:
344         form = MaintenanceModeForm(initial={'ips': request.META['REMOTE_ADDR'],
345                                             'message': _('Currently down for maintenance. We\'ll be back soon')})
346
347     return ('osqaadmin/maintenance.html', {'form': form, 'in_maintenance': settings.MAINTAINANCE_MODE.value is not None
348                                            })
349
350
351 @moderation_page
352 def flagged_posts(request):
353     return ('osqaadmin/flagged_posts.html', {
354     'flagged_posts': get_flagged_posts(),
355     })
356
357 @admin_page
358 def static_pages(request):
359     pages = Page.objects.all()
360
361     return ('osqaadmin/static_pages.html', {
362     'pages': pages,
363     })
364
365 @admin_page
366 def edit_page(request, id=None):
367     if id:
368         page = get_object_or_404(Page, id=id)
369     else:
370         page = None
371
372     if request.POST:
373         form = PageForm(page, request.POST)
374
375         if form.is_valid():
376             if form.has_changed():
377                 if not page:
378                     page = NewPageAction(user=request.user, ip=request.META['REMOTE_ADDR']).save(data=form.cleaned_data
379                                                                                                  ).node
380                 else:
381                     EditPageAction(user=request.user, node=page, ip=request.META['REMOTE_ADDR']).save(
382                             data=form.cleaned_data)
383
384             if ('publish' in request.POST) and (not page.published):
385                 PublishAction(user=request.user, node=page, ip=request.META['REMOTE_ADDR']).save()
386             elif ('unpublish' in request.POST) and page.published:
387                 page.nstate.published.cancel(ip=request.META['REMOTE_ADDR'])
388
389             return HttpResponseRedirect(reverse('admin_edit_page', kwargs={'id': page.id}))
390
391     else:
392         form = PageForm(page)
393
394     if page:
395         published = page.published
396     else:
397         published = False
398
399     return ('osqaadmin/edit_page.html', {
400     'page': page,
401     'form': form,
402     'published': published
403     })
404
405 @admin_page
406 def delete_page(request, id=None):
407     page = get_object_or_404(Page, id=id)
408     page.delete()
409     return HttpResponseRedirect(reverse('admin_static_pages'))
410
411 @admin_tools_page(_('createuser'), _("Create new user"))
412 def create_user(request):
413     if request.POST:
414         form = CreateUserForm(request.POST)
415
416         if form.is_valid():
417             user_ = User(username=form.cleaned_data['username'], email=form.cleaned_data['email'])
418             user_.set_password(form.cleaned_data['password1'])
419
420             if not form.cleaned_data.get('validate_email', False):
421                 user_.email_isvalid = True
422
423             user_.save()
424             UserJoinsAction(user=user_).save()
425
426             request.user.message_set.create(message=_("New user created sucessfully. %s.") % html.hyperlink(
427                     user_.get_profile_url(), _("See %s profile") % user_.username, target="_blank"))
428
429             return HttpResponseRedirect(reverse("admin_tools", kwargs={'name': 'createuser'}))
430     else:
431         form = CreateUserForm()
432
433     return ('osqaadmin/createuser.html', {
434         'form': form,
435     })
436
437 class NodeManagementPaginatorContext(pagination.PaginatorContext):
438     def __init__(self, id='QUESTIONS_LIST', prefix='', default_pagesize=100):
439         super (NodeManagementPaginatorContext, self).__init__(id, sort_methods=(
440             (_('added_at'), pagination.SimpleSort(_('added_at'), '-added_at', "")),
441             (_('added_at_asc'), pagination.SimpleSort(_('added_at_asc'), 'added_at', "")),
442             (_('author'), pagination.SimpleSort(_('author'), '-author__username', "")),
443             (_('author_asc'), pagination.SimpleSort(_('author_asc'), 'author__username', "")),
444             (_('score'), pagination.SimpleSort(_('score'), '-score', "")),
445             (_('score_asc'), pagination.SimpleSort(_('score_asc'), 'score', "")),
446             (_('act_at'), pagination.SimpleSort(_('act_at'), '-last_activity_at', "")),
447             (_('act_at_asc'), pagination.SimpleSort(_('act_at_asc'), 'last_activity_at', "")),
448             (_('act_by'), pagination.SimpleSort(_('act_by'), '-last_activity_by__username', "")),
449             (_('act_by_asc'), pagination.SimpleSort(_('act_by_asc'), 'last_activity_by__username', "")),
450         ), pagesizes=(default_pagesize,), force_sort='added_at', default_pagesize=default_pagesize, prefix=prefix)
451
452 @admin_tools_page(_("nodeman"), _("Bulk management"))
453 def node_management(request):
454     if request.POST:
455         params = pagination.generate_uri(request.GET, ('page',))
456
457         if "save_filter" in request.POST:
458             filter_name = request.POST.get('filter_name', _('filter'))
459             params = pagination.generate_uri(request.GET, ('page',))
460             current_filters = settings.NODE_MAN_FILTERS.value
461             current_filters.append((filter_name, params))
462             settings.NODE_MAN_FILTERS.set_value(current_filters)
463
464         elif r"execute" in request.POST:
465             selected_nodes = request.POST.getlist('_selected_node')
466
467             if selected_nodes and request.POST.get('action', None):
468                 action = str(request.POST['action'])
469                 selected_nodes = Node.objects.filter(id__in=selected_nodes)
470
471                 message = _("No action performed")
472
473                 if action == 'delete_selected':
474                     for node in selected_nodes:
475                         if node.node_type in ('question', 'answer', 'comment') and (not node.nis.deleted):
476                             DeleteAction(user=request.user, node=node, ip=request.META['REMOTE_ADDR']).save()
477
478                     message = _("All selected nodes marked as deleted")
479
480                 if action == 'undelete_selected':
481                     for node in selected_nodes:
482                         if node.node_type in ('question', 'answer', 'comment') and (node.nis.deleted):
483                             node.nstate.deleted.cancel(ip=request.META['REMOTE_ADDR'])
484
485                     message = _("All selected nodes undeleted")
486
487                 if action == "close_selected":
488                     for node in selected_nodes:
489                         if node.node_type == "question" and (not node.nis.closed):
490                             CloseAction(node=node.leaf, user=request.user, extra=_("bulk close"), ip=request.META['REMOTE_ADDR']).save()
491
492                     message = _("Selected questions were closed")
493
494                 if action == "hard_delete_selected":
495                     ids = [n.id for n in selected_nodes]
496
497                     for id in ids:
498                         try:
499                             node = Node.objects.get(id=id)
500                             node.delete()
501                         except:
502                             pass
503
504                     message = _("All selected nodes deleted")
505
506                 request.user.message_set.create(message=message)
507
508                 params = pagination.generate_uri(request.GET, ('page',))
509                 
510             return HttpResponseRedirect(reverse("admin_tools", kwargs={'name': 'nodeman'}) + "?" + params)
511
512
513     nodes = Node.objects.all()
514
515     text = request.GET.get('text', '')
516     text_in = request.GET.get('text_in', 'body')
517
518     authors = request.GET.getlist('authors')
519     tags = request.GET.getlist('tags')
520
521     type_filter = request.GET.getlist('node_type')
522     state_filter = request.GET.getlist('state_type')
523     state_filter_type = request.GET.get('state_filter_type', 'any')
524
525     if type_filter:
526         nodes = nodes.filter(node_type__in=type_filter)
527
528     state_types = NodeState.objects.filter(node__in=nodes).values_list('state_type', flat=True).distinct('state_type')
529     state_filter = [s for s in state_filter if s in state_types]
530
531     if state_filter:
532         if state_filter_type == 'all':
533             nodes = nodes.all_states(*state_filter)
534         else:
535             nodes = nodes.any_state(*state_filter)
536
537     if (authors):
538         nodes = nodes.filter(author__id__in=authors)
539         authors = User.objects.filter(id__in=authors)
540
541     if (tags):
542         nodes = nodes.filter(tags__id__in=tags)
543         tags = Tag.objects.filter(id__in=tags)
544
545     if text:
546         text_in = request.GET.get('text_in', 'body')
547         filter = None
548
549         if text_in == 'title' or text_in == 'both':
550             filter = models.Q(title__icontains=text)
551
552         if text_in == 'body' or text_in == 'both':
553             sec_filter = models.Q(body__icontains=text)
554             if filter:
555                 filter = filter | sec_filter
556             else:
557                 filter = sec_filter
558
559         if filter:
560             nodes = nodes.filter(filter)
561
562     node_types = [(k, n.friendly_name) for k, n in NodeMetaClass.types.items()]
563
564     return ('osqaadmin/nodeman.html', pagination.paginated(request, ("nodes", NodeManagementPaginatorContext()), {
565     'nodes': nodes,
566     'text': text,
567     'text_in': text_in,
568     'type_filter': type_filter,
569     'state_filter': state_filter,
570     'state_filter_type': state_filter_type,
571     'node_types': node_types,
572     'state_types': state_types,
573     'authors': authors,
574     'tags': tags,
575     'hide_menu': True
576     }))
577
578 @csrf_exempt
579 @super_user_required
580 def test_email_settings(request):
581     user = request.user
582
583     send_template_email([user,], 'osqaadmin/mail_test.html', { 'user' : user })
584
585     return render_to_response(
586         'osqaadmin/test_email_settings.html',
587         { 'user': user, },
588         RequestContext(request)
589     )