]> git.openstreetmap.org Git - osqa.git/commitdiff
Adds the bulk management interface with node bulk management with full filtering...
authorhernani <hernani@0cfe37f9-358a-4d5e-be75-b63607b5c754>
Thu, 26 Aug 2010 13:21:21 +0000 (13:21 +0000)
committerhernani <hernani@0cfe37f9-358a-4d5e-be75-b63607b5c754>
Thu, 26 Aug 2010 13:21:21 +0000 (13:21 +0000)
Adds pagination to the action list in the admin dashboard.
Makes some changes in the way admin links are registered.

git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@571 0cfe37f9-358a-4d5e-be75-b63607b5c754

forum/forms/admin.py
forum/skins/default/media/style/style.css
forum/skins/default/templates/osqaadmin/dashboard.html
forum/skins/default/templates/osqaadmin/djstyle_base.html
forum/skins/default/templates/osqaadmin/nodeman.html [new file with mode: 0644]
forum/skins/default/templates/paginator/page_numbers.html
forum/urls.py
forum/views/admin.py

index 791f5ec7e50de12859588c9bdaa0dd8e522411cc..00dc3369106e524b3f3d211e3aa06990a67cc0bf 100644 (file)
@@ -3,6 +3,7 @@ from django import forms
 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):
@@ -89,4 +90,16 @@ class PageForm(forms.Form):
 
     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
index 61b8281466ee94f759488242592a7ce98fc6094b..88815c4eeb15268e68dda1d3a0116f331d7edc73 100644 (file)
@@ -487,7 +487,7 @@ blockquote {
     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;
@@ -503,7 +503,7 @@ blockquote {
     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;
index 3cd1da9a2ca911efaa0ae30d261b889995b9c359..ec56e7808ac503d66682edee0961e5ad4f84cdb3 100644 (file)
         <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
index f0258e7636c910c99f6903c87de0411f03ce095b..255bc36ab97c4d1ec2351b4d5325fdaf99f1a3d3 100644 (file)
@@ -22,7 +22,7 @@
     <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">
@@ -43,7 +43,7 @@
             {% 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 %}
@@ -52,6 +52,7 @@
             {% 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>
diff --git a/forum/skins/default/templates/osqaadmin/nodeman.html b/forum/skins/default/templates/osqaadmin/nodeman.html
new file mode 100644 (file)
index 0000000..9e6c194
--- /dev/null
@@ -0,0 +1,104 @@
+{% 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
index 50e52521ec66e39e4afe948b2846745a875718bb..abe021dd3a22928c3778e7152c8ef9321a7f9643 100644 (file)
@@ -1,7 +1,7 @@
 {% spaceless %}
 {% load i18n %}
 
-<div class="paginator">
+<p class="paginator">
     {% if has_previous %}
         <span class="prev"><a href="{{ previous_url }}" title="{% trans "previous" %}">&laquo; {% trans "previous" %}</a></span>
     {% endif %}
@@ -9,9 +9,9 @@
         {% 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 %}
@@ -21,5 +21,5 @@
     {% if has_next %}
         <span class="next"><a href="{{ next_url }}" title="{% trans "next page" %}">{% trans "next page" %} &raquo;</a></span>    
     {% endif %}
-</div>
+</p>
 {% endspaceless %}
\ No newline at end of file
index c5a8a1610c7f06992a863f04aa4f88ab7242a556..f39533a10680a78409ec87bce65bd47896919e93 100644 (file)
@@ -49,6 +49,7 @@ urlpatterns += patterns('',
                             {'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,
@@ -190,13 +191,15 @@ urlpatterns += patterns('',
                             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"),
 
index 7b10f7cec43c22835fa84a96622dc7fa7e32edfc..a6a30c08677b4f910fbc353bdb654c342d097381 100644 (file)
@@ -1,5 +1,5 @@
 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
@@ -10,13 +10,17 @@ from django.utils.translation import ugettext as _
 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:
@@ -40,6 +44,8 @@ def admin_page(fn):
                     '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)])
 
@@ -49,14 +55,26 @@ def admin_page(fn):
 
     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):
@@ -104,6 +122,13 @@ def statistics(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):
@@ -161,7 +186,7 @@ def get_default(request, set_name, var_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]
@@ -355,78 +380,57 @@ def edit_page(request, id=None):
     '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', {})