from django.utils.translation import ugettext as _
from qanda import TitleField, EditorField
from forum import settings
+from forum.models.node import NodeMetaClass
class IPListField(forms.CharField):
def clean(self, value):
comments = forms.BooleanField(label=_("Allow comments"), initial=False, required=False)
+TEXT_IN_CHOICES = (
+('title', _('Title')),
+('body', _('Body')),
+('both', _('Title and Body'))
+)
+
+class NodeManFilterForm(forms.Form):
+ node_type = forms.CharField(widget=forms.HiddenInput, initial='all')
+ state_type = forms.CharField(widget=forms.HiddenInput, initial='any')
+ text = forms.CharField(required=False, widget=forms.TextInput(attrs={'size': 40}))
+ text_in = forms.ChoiceField(widget=forms.RadioSelect, choices=TEXT_IN_CHOICES, initial='title')
+
\ No newline at end of file
margin-left: 0.5em;
}
-.paginator .page a, .paginator .page a:visited, .paginator .curr {
+.paginator .page, .paginator .page a:visited, .paginator .curr {
background-color: #FFFFFF;
border: 1px solid #CCCCCC;
color: #777777;
font-weight: bold;
}
-.paginator .page a:hover, .paginator .prev a:hover, .paginator .next a:hover {
+.paginator .page:hover, .paginator .prev a:hover, .paginator .next a:hover {
background-color: #777777;
border: 1px solid #777777;
color: #FFFFFF;
<caption>{% trans "Recent activity" %}</caption>
<tr>
<td colspan="2">
- <table width="100%">
- {% for activity in recent_activity %}
- <tr><td>{% activity_item activity request.user %}</td></tr>
+ <table id="result_list" width="100%">
+ {% for activity in recent_activity.paginator.page %}
+ <tr class="{% cycle 'row1' 'row2' %}"><td>{% activity_item activity request.user %}</td></tr>
{% endfor %}
</table>
</td>
</tr>
</table>
+ <p class="paginator">
+ {{ recent_activity.paginator.page_numbers }}
+ </p>
</div>
{% endblock %}
\ No newline at end of file
<title>{% trans "OSQA administration area" %} - {% block subtitle %}{% endblock %}</title>
</head>
<body>
-<div id="container">
+<div id="container" class="change-list">
<div id="header">
{% block header %}
<div id="branding">
{% block pagename %}{% endblock %} -
{% block description %}{% endblock %}
</div>
- <div id="content" class="colMS">
+ <div id="content" class="{% if hide_menu %}flex{% else %}colMS{% endif %}">
<div id="content-main">
{% autoescape off %}
{% for message in user_messages %}
{% endautoescape %}
{% block admincontent %}{% endblock %}
</div>
+ {% if not hide_menu %}
<div id="content-related">
{% if unsaved %}
<div id="changes-box" class="module">
<ul>
<li><a href="{% url admin_maintenance %}">{% trans "Maintenance mode" %}</a></li>
<li><a href="{% url admin_flagged_posts %}">{% trans "Flagged Posts" %}</a></li>
- {% comment %}<li><a href="{% url admin_moderation %}">{% trans "Moderation" %}</a></li>{% endcomment %}
+ {% for name,tool in tools.items %}
+ <li><a href="{% url admin_tools name %}">{{ tool.label }}</a></li>
+ {% endfor %}
</ul>
</div>
</div>
+ {% endif %}
</div>
<div id="footer" class="breadcumbs">
<a href="http://www.osqa.net">OSQA</a> <span class="version">{{ settings.OSQA_VERSION }} ({{ settings.SVN_REVISION }})</span>
--- /dev/null
+{% extends basetemplate %}
+
+{% load i18n %}
+{% load user_tags %}
+
+{% block adminjs %}
+ <script type="text/javascript">
+ $(function() {
+ var $form = $('#changelist-search');
+
+ $('.node-type-link').click(function() {
+ $form.find('#id_state_type').val('any');
+ $form.find('#id_node_type').val($(this).attr('href').substring(1));
+ $form.submit();
+ });
+
+ $('.state-type-link').click(function() {
+ $form.find('#id_state_type').val($(this).attr('href').substring(1));
+ $form.submit();
+ });
+ });
+ </script>
+ <style>
+ #toolbar ul li {
+ list-style-type: none;
+ display: inline;
+ margin-right: 12px;
+ }
+ </style>
+{% endblock %}
+
+{% block subtitle %}
+ {% trans "Node manager" %}
+{% endblock %}
+{% block description %}
+ {% trans "Nodes bulk management" %}
+{% endblock %}
+
+{% block admincontent %}
+ <div id="changelist" class="module filtered">
+ <div id="toolbar">
+ <form method="get" action="" id="changelist-search">
+ <div>
+ <label for="searchbar"><img alt="Search" src="{{ settings.ADMIN_MEDIA_PREFIX }}img/admin/icon_searchbox.png"></label>
+ {{ filter_form.text }}
+ {{ filter_form.node_type }}
+ {{ filter_form.state_type }}
+ <input type="submit" value="Search"><br />
+ {{ filter_form.text_in }}
+ </div>
+ </form>
+ </div>
+ <div id="changelist-filter">
+ <h2>{% trans "Filter" %}</h2>
+ <h3>{% trans "By type" %}</h3>
+ <ul>
+ {% for type, name in node_types %}
+ <li{% ifequal filter_form.node_type.data type %} class="selected"{% endifequal %}>
+ <a class="node-type-link" href="#{{ type }}">{{ name }}</a>
+ </li>
+ {% endfor %}
+ </ul>
+ <h3>{% trans "By state" %}</h3>
+ <ul>
+ <li{% ifequal filter_form.state_type.data "any" %} class="selected"{% endifequal %}><a class="state-type-link" href="#any">{% trans "any" %}</a></li>
+ {% for state_type in state_types %}
+ <li{% ifequal filter_form.state_type.data state_type %} class="selected"{% endifequal %}>
+ <a class="state-type-link" href="#{{ state_type }}">{{ state_type }}</a>
+ </li>
+ {% endfor %}
+ </ul>
+ </div>
+ <form id="changelist-form" method="POST" action="">
+ <div class="action"></div>
+ <table id="result_list" cellspacing="0">
+ <thead>
+ <tr>
+ <th class="action-checkbox-column">
+ <input type="checkbox" id="action-toggle" style="display: inline;" />
+ </th>
+ {% ifequal filter_form.node_type.data "all" %}
+ <th>{% trans "Type" %}</th>
+ {% endifequal %}
+ <th>{% trans "Title" %}</th>
+ <th>{% trans "Author" %}</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for node in nodes.paginator.page %}
+ <tr class="{% cycle 'row1' 'row2' %}">
+ <td><input type="checkbox" name="_selected_action" value="{{ node.id }}" class="action-select"></td>
+ {% ifequal filter_form.node_type.data "all" %}
+ <th>{{ node.friendly_name }}</th>
+ {% endifequal %}
+ <td><a href="{{ node.get_absolute_url }}" target="_blank">{{ node.headline }}</a></td>
+ <td><a href="{{ node.author.get_absolute_url }}">{{ node.author.username }}</a></td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ {{ nodes.paginator.page_numbers }}
+ </form>
+ </div>
+{% endblock %}
\ No newline at end of file
{% spaceless %}
{% load i18n %}
-<div class="paginator">
+<p class="paginator">
{% if has_previous %}
<span class="prev"><a href="{{ previous_url }}" title="{% trans "previous" %}">« {% trans "previous" %}</a></span>
{% endif %}
{% if range %}
{% for num, url in range %}
{% ifequal num current %}
- <span class="curr">{{ num }}</span>
+ <span class="curr this_page">{{ num }}</span>
{% else %}
- <span class="page"><a href="{{ url }}" >{{ num }}</a></span>
+ <a class="page" href="{{ url }}" >{{ num }}</a>
{% endifequal %}
{% endfor %}
{% else %}
{% if has_next %}
<span class="next"><a href="{{ next_url }}" title="{% trans "next page" %}">{% trans "next page" %} »</a></span>
{% endif %}
-</div>
+</p>
{% endspaceless %}
\ No newline at end of file
{'content': settings.ABOUT_PAGE_TEXT, 'title': _('About')}, name='about'),
url(r'^%s$' % _('markdown_help/'), app.meta.markdown_help, name='markdown_help'),
url(r'^opensearch\.xml$', app.meta.opensearch, name='opensearch'),
+ url(r'^opensearch\.xml$', app.meta.opensearch, name='opensearch'),
url(r'^%s$' % _('privacy/'), app.meta.privacy, name='privacy'),
url(r'^%s$' % _('logout/'), app.meta.logout, name='logout'),
url(r'^%s(?P<id>\d+)/%s$' % (_('answers/'), _('edit/')), app.writers.edit_answer,
name="admin_flagged_posts"),
url(r'^%s%s$' % (_('admin/'), _('static_pages/')), app.admin.static_pages,
name="admin_static_pages"),
- url(r'^%s%s$' % (_('admin/'), _('moderation/')), app.admin.moderation, name="admin_moderation"),
url(r'^%s%s%s$' % (_('admin/'), _('static_pages/'), _('new/')), app.admin.edit_page,
name="admin_new_page"),
url(r'^%s%s%s(?P<id>\d+)/$' % (_('admin/'), _('static_pages/'), _('edit/')), app.admin.edit_page
, name="admin_edit_page"),
+ url(r'^%s%s(?P<name>\w+)/$' % (_('admin/'), _('tools/')), app.admin.tools_page,
+ name="admin_tools"),
+
url(r'^%s%s(?P<set_name>\w+)/$' % (_('admin/'), _('settings/')), app.admin.settings_set,
name="admin_set"),
from datetime import datetime, timedelta
-import time
+import os, time, csv, random
from django.shortcuts import render_to_response, get_object_or_404
from django.core.urlresolvers import reverse
from django.utils import simplejson
from django.db import models
from forum.settings.base import Setting
-from forum.forms import MaintenanceModeForm, PageForm
+from forum.forms import MaintenanceModeForm, PageForm, NodeManFilterForm
from forum.settings.forms import SettingsSetForm
+from forum.utils import pagination
-from forum.models import Question, Answer, User, Node, Action, Page
+from forum.models import Question, Answer, User, Node, Action, Page, NodeState
+from forum.models.node import NodeMetaClass
from forum.actions import NewPageAction, EditPageAction, PublishAction
from forum import settings
+TOOLS = {}
+
def super_user_required(fn):
def wrapper(request, *args, **kwargs):
if request.user.is_authenticated() and request.user.is_superuser:
'form', 'moderation', 'css', 'headandfoot', 'head', 'view', 'urls')]
, lambda s1, s2: s1.weight - s2.weight)
+ context['tools'] = TOOLS
+
unsaved = request.session.get('previewing_settings', {})
context['unsaved'] = set([getattr(settings, s).set.name for s in unsaved.keys() if hasattr(settings, s)])
return wrapper
+def admin_tools_page(name, label):
+ def decorator(fn):
+ fn.label = label
+ TOOLS[name] = fn
+
+ return fn
+ return decorator
+
+class ActivityPaginatorContext(pagination.PaginatorContext):
+ def __init__(self):
+ super (ActivityPaginatorContext, self).__init__('ADMIN_RECENT_ACTIVITY', pagesizes=(20, 40, 80), default_pagesize=40)
+
@admin_page
def dashboard(request):
- return ('osqaadmin/dashboard.html', {
+ return ('osqaadmin/dashboard.html', pagination.paginated(request, ("recent_activity", ActivityPaginatorContext()), {
'settings_pack': unicode(settings.SETTINGS_PACK),
'statistics': get_statistics(),
'recent_activity': get_recent_activity(),
'flagged_posts': get_flagged_posts(),
- })
+ }))
@super_user_required
def interface_switch(request):
]
}
+@admin_page
+def tools_page(request, name):
+ if not name in TOOLS:
+ raise Http404
+
+ return TOOLS[name](request)
+
@admin_page
def settings_set(request, set_name):
def get_recent_activity():
- return Action.objects.order_by('-action_date')[0:30]
+ return Action.objects.order_by('-action_date')
def get_flagged_posts():
return Action.objects.filter(canceled=False, action_type="flag").order_by('-action_date')[0:30]
'published': published
})
-@admin_page
-def moderation(request):
- if request.POST:
- if not 'ids' in request.POST:
- verify = None
- else:
- sort = {
- 'high-rep': '-reputation',
- 'newer': '-date_joined',
- 'older': 'date_joined',
- }.get(request.POST.get('sort'), None)
-
- if sort:
- try:
- limit = int(request.POST['limit'])
- except:
- limit = 5
-
- verify = User.objects.order_by(sort)[:limit]
- else:
- verify = None
- if verify:
- possible_cheaters = []
- verify = User.objects.order_by(sort)[:5]
- cheat_score_sort = lambda c1, c2: cmp(c2.fdata['fake_score'], c1.fdata['fake_score'])
+@admin_tools_page(_("nodeman"), _("Node management"))
+def node_management(request):
+ nodes = Node.objects.all()
- for user in verify:
- possible_fakes = []
- affecters = User.objects.filter(actions__node__author=user, actions__canceled=False).annotate(
- affect_count=models.Count('actions')).order_by('-affect_count')
- user_ips = set(Action.objects.filter(user=user).values_list('ip', flat=True).distinct('ip'))
+ if (request.GET):
+ filter_form = NodeManFilterForm(request.GET)
+ else:
+ filter_form = NodeManFilterForm({'node_type': 'all', 'state_type': 'any'})
+
+ if filter_form.is_valid():
+ data = filter_form.cleaned_data
- for affecter in affecters:
- if affecter == user:
- continue
+ if data['node_type'] != 'all':
+ nodes = nodes.filter(node_type=data['node_type'])
- data = {'affect_count': affecter.affect_count}
+ if (data['state_type'] != 'any'):
+ nodes = nodes.filter_state(**{str(data['state_type']): True})
- total_actions = affecter.actions.filter(canceled=False).exclude(node=None).count()
- ratio = (float(affecter.affect_count) / float(total_actions)) * 100
+ if data['text']:
+ filter = None
- if total_actions > 10 and ratio > 50:
- data['total_actions'] = total_actions
- data['action_ratio'] = ratio
+ if data['text_in'] == 'title' or data['text_in'] == 'both':
+ filter = models.Q(title__icontains=data['text'])
+
+ if data['text_in'] == 'body' or data['text_in'] == 'both':
+ sec_filter = models.Q(body__icontains=data['text'])
+ if filter:
+ filter = filter | sec_filter
+ else:
+ filter = sec_filter
- affecter_ips = set(
- Action.objects.filter(user=affecter).values_list('ip', flat=True).distinct('ip'))
- cross_ips = len(user_ips & affecter_ips)
+ if filter:
+ nodes = nodes.filter(filter)
- data['cross_ip_count'] = cross_ips
- data['total_ip_count'] = len(affecter_ips)
- data['cross_ip_ratio'] = (float(data['cross_ip_count']) / float(data['total_ip_count'])) * 100
- if affecter.email_isvalid:
- email_score = 0
- else:
- email_score = 50.0
+ node_types = [('all', _("all"))] + [(k, n.friendly_name) for k, n in NodeMetaClass.types.items()]
+ state_types = NodeState.objects.filter(node__in=nodes).values_list('state_type', flat=True).distinct('state_type')
- data['fake_score'] = ((data['cross_ip_ratio'] + data['action_ratio'] + email_score) / 100) * 4
+ return ('osqaadmin/nodeman.html', pagination.paginated(request, ("nodes", ActivityPaginatorContext()), {
+ 'nodes': nodes,
+ 'node_types': node_types,
+ 'state_types': state_types,
+ 'filter_form': filter_form,
+ 'hide_menu': True
+ }))
- affecter.fdata = data
- possible_fakes.append(affecter)
- if len(possible_fakes) > 0:
- possible_fakes = sorted(possible_fakes, cheat_score_sort)
- possible_cheaters.append((user, possible_fakes))
- return ('osqaadmin/moderation.html', {'cheaters': possible_cheaters})
- return ('osqaadmin/moderation.html', {})