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