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