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