From: hernani Date: Sat, 11 Sep 2010 00:59:10 +0000 (+0000) Subject: Bulk management changes: X-Git-Tag: live~545 X-Git-Url: https://git.openstreetmap.org./osqa.git/commitdiff_plain/51b7f4ab41479e5a0cbfc171a274a82fd4100140 Bulk management changes: Highlights nodes with deleted or accepted state. Adds date and by to each state in the states column. Shows the diamonds next to the usernames (su or staff) Allows resizing the summary column via javascript (not perfect though). Allows the colapsing of the filter panel. Allows filtering by multiple nodes or state types. git-svn-id: http://svn.osqa.net/svnroot/osqa/trunk@589 0cfe37f9-358a-4d5e-be75-b63607b5c754 --- diff --git a/forum/forms/admin.py b/forum/forms/admin.py index 32d710b..a1d7b72 100644 --- a/forum/forms/admin.py +++ b/forum/forms/admin.py @@ -92,19 +92,6 @@ 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(required=False, widget=forms.RadioSelect, choices=TEXT_IN_CHOICES, initial='title') - - from forum.forms.auth import SimpleRegistrationForm from forum.forms.general import SetPasswordForm diff --git a/forum/models/node.py b/forum/models/node.py index ea83e7f..f62a8e0 100644 --- a/forum/models/node.py +++ b/forum/models/node.py @@ -108,6 +108,30 @@ class NodeQuerySet(CachedQuerySet): return node + def any_state(self, *args): + filter = None + + for s in args: + s_filter = models.Q(state_string__contains="(%s)" % s) + filter = filter and (filter | s_filter) or s_filter + + if filter: + return self.filter(filter) + else: + return self + + def all_states(self, *args): + filter = None + + for s in args: + s_filter = models.Q(state_string__contains="(%s)" % s) + filter = filter and (filter & s_filter) or s_filter + + if filter: + return self.filter(filter) + else: + return self + def filter_state(self, **kwargs): apply_bool = lambda q, b: b and q or ~q return self.filter(*[apply_bool(models.Q(state_string__contains="(%s)" % s), b) for s, b in kwargs.items()]) diff --git a/forum/skins/default/templates/osqaadmin/nodeman.html b/forum/skins/default/templates/osqaadmin/nodeman.html index af8d1ce..500e05b 100644 --- a/forum/skins/default/templates/osqaadmin/nodeman.html +++ b/forum/skins/default/templates/osqaadmin/nodeman.html @@ -7,14 +7,37 @@ $(function() { var $form = $('#changelist-search'); + $('#all-node-type-link').click(function() { + $('#type-filter-container').find('input').remove(); + $form.submit(); + }); + $('.node-type-link').click(function() { - $form.find('#id_state_type').val('any'); - $form.find('#id_node_type').val($(this).attr('href').substring(1)); + var link_type = $(this).attr('href').substring(1); + + if ($('#type-filter-container').find('input[value=' + link_type + ']').length == 0) { + $('#type-filter-container').append($("")); + } else { + $('#type-filter-container').find('input[value=' + link_type + ']').remove(); + } + + $form.submit(); + }); + + $('#all-state-link').click(function() { + $('#state-filter-container').find('input').remove(); $form.submit(); }); $('.state-type-link').click(function() { - $form.find('#id_state_type').val($(this).attr('href').substring(1)); + var state_type = $(this).attr('href').substring(1); + + if ($('#state-filter-container').find('input[value=' + state_type + ']').length == 0) { + $('#state-filter-container').append($("")); + } else { + $('#state-filter-container').find('input[value=' + state_type + ']').remove(); + } + $form.submit(); }); @@ -114,6 +137,46 @@ } }); + var resize_data = null; + + $('.col-resizer').mousedown(function(e) { + var $to_resize = $(this).prev(); + + resize_data = { + resizer: $(this), + to_resize: $to_resize, + start_width: $to_resize.innerWidth(), + x_start: e.pageX, + } + }); + + $('body').mousemove(function(e) { + if (resize_data != null) { + var new_size = (resize_data.start_width - (resize_data.x_start - e.pageX)) + 'px'; + resize_data.to_resize.css({'max-width': new_size, 'min-width': new_size}) + resize_data.resizer.css('max-width', '3px'); + } + }); + + $('body').mouseup(function() { + if (resize_data != null) + resize_data = null; + }); + + $('#filter-panel-header').click(function() { + $('#filter-panel').slideToggle(); + }); + + $('#state-filter-type').change(function() { + $('#state-filter-type-hidden').val($(this).val()); + $form.submit(); + }); + + $('#reset-text-filter').click(function() { + $('#text-filter-input').val(''); + $form.submit(); + return false; + }); }); @@ -185,15 +276,34 @@
-

{% trans "Filter" %}

+

{% trans "Filter" %} ({% trans "Click to show/hide" %})

+

{% trans "By type" %}

{% trans "By state" %}

{% trans "By author(s)" %}

{% if not authors.count %} @@ -276,6 +408,7 @@
{{ show_form.show }}
{% endcomment %} +
@@ -284,6 +417,7 @@ @@ -295,31 +429,38 @@ {% declare %} current_sort = nodes.paginator.current_sort - added_at = current_sort == "added_at" and "descending" or (current_sort == "added_at_asc" and "ascending" or "") - score = current_sort == "score" and "descending" or (current_sort == "score_asc" and "ascending" or "") - act_at = current_sort == "act_at" and "descending" or (current_sort == "act_at_asc" and "ascending" or "") + added_at = current_sort == "added_at" and "ascending" or (current_sort == "added_at_asc" and "descending" or "") + author = current_sort == "author" and "ascending" or (current_sort == "author_asc" and "descending" or "") + score = current_sort == "score" and "ascending" or (current_sort == "score_asc" and "descending" or "") + act_at = current_sort == "act_at" and "ascending" or (current_sort == "act_at_asc" and "descending" or "") + act_by = current_sort == "act_by" and "ascending" or (current_sort == "act_by_asc" and "descending" or "") added_at_link = current_sort == "added_at" and nodes.paginator.added_at_asc_sort_link or nodes.paginator.added_at_sort_link + author_link = current_sort == "author_asc" and nodes.paginator.author_sort_link or nodes.paginator.author_asc_sort_link score_link = current_sort == "score" and nodes.paginator.score_asc_sort_link or nodes.paginator.score_sort_link act_at_link = current_sort == "act_at" and nodes.paginator.act_at_asc_sort_link or nodes.paginator.act_at_sort_link + act_by_link = current_sort == "act_by_asc" and nodes.paginator.act_by_sort_link or nodes.paginator.act_by_asc_sort_link {% enddeclare %} {% spaceless %} - - {% ifequal filter_form.node_type.data "all" %} - {% trans "Type" %} - {% endifequal %} + + {% trans "Type" %} {% trans "Summary" %} + {% trans "State" %} - {% trans "Author" %} + + {% trans "Author" %} + {% trans "Added at" %} - + + + {% trans "Last activity by" %} - {% trans "Last acivity by" %} {% trans "Last activity at" %} @@ -328,21 +469,38 @@ + {% with filter_form.state_type.data as state_type %} {% for node in nodes.paginator.page %} - {% ifequal filter_form.node_type.data "all" %} - {{ node.friendly_name }} - {% endifequal %} - - {{ node.headline }}
+ {{ node.friendly_name }} + {% declare %} + is_root = node.abs_parent == None + title = is_root and node.title or node.abs_parent.title + + anchor = "%s" % html.hyperlink(node.get_absolute_url(), title) + anchor = ((not is_root) and node.abs_parent.nis.deleted) and "%s" % anchor or anchor + anchor = is_root and anchor or "(%s)" % anchor + anchor = html.mark_safe(anchor) + + td_class = "" + td_class = node.nis.accepted and "accepted" or td_class + td_class = node.nis.deleted and "deleted" or td_class + {% enddeclare %} + + {{ anchor }}
{{ node.summary }} - {{ node.state_list|join:", " }} - {{ node.author.username }} + + {% for state in node.states.all %} + {{ state.state_type }} {% diff_date state.action.at %} {% trans "by" %} + {{ state.action.by.decorated_name }}
+ {% endfor %} + + {{ node.author.decorated_name }} {% diff_date node.added_at %} - {{ node.score }} - {{ node.last_activity_by.username }} + + {{ node.last_activity_by.decorated_name }} {% diff_date node.last_activity_at %} {% for t in node.tags.all %} @@ -352,6 +510,7 @@ {% endfor %} + {% endwith %} {{ nodes.paginator.page_numbers }} diff --git a/forum/views/admin.py b/forum/views/admin.py index 0fa5333..40a098a 100644 --- a/forum/views/admin.py +++ b/forum/views/admin.py @@ -10,7 +10,7 @@ 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, NodeManFilterForm, CreateUserForm +from forum.forms import MaintenanceModeForm, PageForm, CreateUserForm from forum.settings.forms import SettingsSetForm from forum.utils import pagination, html @@ -412,113 +412,137 @@ class NodeManagementPaginatorContext(pagination.PaginatorContext): super (NodeManagementPaginatorContext, self).__init__(id, sort_methods=( (_('added_at'), pagination.SimpleSort(_('added_at'), '-added_at', "")), (_('added_at_asc'), pagination.SimpleSort(_('added_at_asc'), 'added_at', "")), + (_('author'), pagination.SimpleSort(_('author'), '-author__username', "")), + (_('author_asc'), pagination.SimpleSort(_('author_asc'), 'author__username', "")), (_('score'), pagination.SimpleSort(_('score'), '-score', "")), (_('score_asc'), pagination.SimpleSort(_('score_asc'), 'score', "")), (_('act_at'), pagination.SimpleSort(_('act_at'), '-last_activity_at', "")), (_('act_at_asc'), pagination.SimpleSort(_('act_at_asc'), 'last_activity_at', "")), + (_('act_by'), pagination.SimpleSort(_('act_by'), '-last_activity_by__username', "")), + (_('act_by_asc'), pagination.SimpleSort(_('act_by_asc'), 'last_activity_by__username', "")), ), pagesizes=(default_pagesize,), force_sort='added_at', default_pagesize=default_pagesize, prefix=prefix) @admin_tools_page(_("nodeman"), _("Bulk management")) def node_management(request): - if request.POST and "save_filter" in request.POST: - filter_name = request.POST.get('filter_name', _('filter')) + if request.POST: params = pagination.generate_uri(request.GET, ('page',)) - current_filters = settings.NODE_MAN_FILTERS.value - current_filters.append((filter_name, params)) - settings.NODE_MAN_FILTERS.set_value(current_filters) - if request.POST and "execute" in request.POST: - selected_nodes = request.POST.getlist('_selected_node') + if "save_filter" in request.POST: + filter_name = request.POST.get('filter_name', _('filter')) + params = pagination.generate_uri(request.GET, ('page',)) + current_filters = settings.NODE_MAN_FILTERS.value + current_filters.append((filter_name, params)) + settings.NODE_MAN_FILTERS.set_value(current_filters) + + elif r"execute" in request.POST: + selected_nodes = request.POST.getlist('_selected_node') - if selected_nodes and request.POST.get('action', None): - action = request.POST['action'] - selected_nodes = Node.objects.filter(id__in=selected_nodes) + if selected_nodes and request.POST.get('action', None): + action = request.POST['action'] + selected_nodes = Node.objects.filter(id__in=selected_nodes) - message = _("No action performed") + message = _("No action performed") - if action == 'delete_selected': - for node in selected_nodes: - if node.node_type in ('question', 'answer', 'comment') and (not node.nis.deleted): - DeleteAction(user=request.user, node=node, ip=request.META['REMOTE_ADDR']).save() - - message = _("All selected nodes marked as deleted") + if action == 'delete_selected': + for node in selected_nodes: + if node.node_type in ('question', 'answer', 'comment') and (not node.nis.deleted): + DeleteAction(user=request.user, node=node, ip=request.META['REMOTE_ADDR']).save() - if action == "close_selected": - for node in selected_nodes: - if node.node_type == "question" and (not node.nis.closed): - CloseAction(node=node.leaf, user=request.user, extra=_("bulk close"), ip=request.META['REMOTE_ADDR']).save() + message = _("All selected nodes marked as deleted") - message = _("Selected questions were closed") + if action == 'undelete_selected': + for node in selected_nodes: + if node.node_type in ('question', 'answer', 'comment') and (node.nis.deleted): + node.nstate.deleted.cancel(ip=request.META['REMOTE_ADDR']) - if action == "hard_delete_selected": - ids = [n.id for n in selected_nodes] + message = _("All selected nodes undeleted") - for id in ids: - try: - node = Node.objects.get(id=id) - node.delete() - except: - pass + if action == "close_selected": + for node in selected_nodes: + if node.node_type == "question" and (not node.nis.closed): + CloseAction(node=node.leaf, user=request.user, extra=_("bulk close"), ip=request.META['REMOTE_ADDR']).save() - message = _("All selected nodes deleted") + message = _("Selected questions were closed") - request.user.message_set.create(message=message) + if action == "hard_delete_selected": + ids = [n.id for n in selected_nodes] - params = pagination.generate_uri(request.GET, ('page',)) + for id in ids: + try: + node = Node.objects.get(id=id) + node.delete() + except: + pass + + message = _("All selected nodes deleted") + + request.user.message_set.create(message=message) + + params = pagination.generate_uri(request.GET, ('page',)) + return HttpResponseRedirect(reverse("admin_tools", kwargs={'name': 'nodeman'}) + "?" + params) nodes = Node.objects.all() - if (request.GET): - filter_form = NodeManFilterForm(request.GET) - else: - filter_form = NodeManFilterForm({'node_type': 'all', 'state_type': 'any'}) + text = request.GET.get('text', '') + text_in = request.GET.get('text_in', 'body') authors = request.GET.getlist('authors') tags = request.GET.getlist('tags') - if filter_form.is_valid(): - data = filter_form.cleaned_data + type_filter = request.GET.getlist('node_type') + state_filter = request.GET.getlist('state_type') + state_filter_type = request.GET.get('state_filter_type', 'any') - if data['node_type'] != 'all': - nodes = nodes.filter(node_type=data['node_type']) + if type_filter: + nodes = nodes.filter(node_type__in=type_filter) - if (data['state_type'] != 'any'): - nodes = nodes.filter_state(**{str(data['state_type']): True}) + state_types = NodeState.objects.filter(node__in=nodes).values_list('state_type', flat=True).distinct('state_type') + state_filter = [s for s in state_filter if s in state_types] - if (authors): - nodes = nodes.filter(author__id__in=authors) - authors = User.objects.filter(id__in=authors) + if state_filter: + if state_filter_type == 'all': + nodes = nodes.all_states(*state_filter) + else: + nodes = nodes.any_state(*state_filter) - if (tags): - nodes = nodes.filter(tags__id__in=tags) - tags = Tag.objects.filter(id__in=tags) + if (authors): + nodes = nodes.filter(author__id__in=authors) + authors = User.objects.filter(id__in=authors) - if data['text']: - filter = None + if (tags): + nodes = nodes.filter(tags__id__in=tags) + tags = Tag.objects.filter(id__in=tags) - if data['text_in'] == 'title' or data['text_in'] == 'both': - filter = models.Q(title__icontains=data['text']) + if text: + text_in = request.GET.get('text_in', 'body') + filter = None - 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 + if text_in == 'title' or text_in == 'both': + filter = models.Q(title__icontains=text) + if text_in == 'body' or text_in == 'both': + sec_filter = models.Q(body__icontains=text) if filter: - nodes = nodes.filter(filter) + filter = filter | sec_filter + else: + filter = sec_filter - 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') + if filter: + nodes = nodes.filter(filter) + + node_types = [(k, n.friendly_name) for k, n in NodeMetaClass.types.items()] return ('osqaadmin/nodeman.html', pagination.paginated(request, ("nodes", NodeManagementPaginatorContext()), { 'nodes': nodes, + 'text': text, + 'text_in': text_in, + 'type_filter': type_filter, + 'state_filter': state_filter, + 'state_filter_type': state_filter_type, 'node_types': node_types, 'state_types': state_types, - 'filter_form': filter_form, 'authors': authors, 'tags': tags, 'hide_menu': True