Merge "Add API filtering to paged tables"
This commit is contained in:
commit
b02422b521
|
@ -421,7 +421,20 @@ class FilterAction(BaseAction):
|
|||
|
||||
.. attribute: filter_type
|
||||
|
||||
A string representing the type of this filter. Default: ``"query"``.
|
||||
A string representing the type of this filter. If this is set to
|
||||
``"server"`` then ``filter_choices`` must also be provided.
|
||||
Default: ``"query"``.
|
||||
|
||||
.. attribute: filter_choices
|
||||
|
||||
Required for server type filters. A tuple of tuples representing the
|
||||
filter options. Tuple composition should evaluate to (string, string,
|
||||
boolean), representing the filter parameter, display value, and whether
|
||||
or not it should be applied to the API request as an API query
|
||||
attribute. API type filters do not need to be accounted for in the
|
||||
filter method since the API will do the filtering. However, server
|
||||
type filters in general will need to be performed in the filter method.
|
||||
By default this attribute is not provided.
|
||||
|
||||
.. attribute: needs_preloading
|
||||
|
||||
|
@ -443,10 +456,16 @@ class FilterAction(BaseAction):
|
|||
self.name = kwargs.get('name', self.name)
|
||||
self.verbose_name = kwargs.get('verbose_name', _("Filter"))
|
||||
self.filter_type = kwargs.get('filter_type', "query")
|
||||
self.filter_choices = kwargs.get('filter_choices')
|
||||
self.needs_preloading = kwargs.get('needs_preloading', False)
|
||||
self.param_name = kwargs.get('param_name', 'q')
|
||||
self.icon = "search"
|
||||
|
||||
if self.filter_type == 'server' and self.filter_choices is None:
|
||||
raise NotImplementedError('A FilterAction object with the '
|
||||
'filter_type attribute set to "server" must also have a '
|
||||
'filter_choices attribute.')
|
||||
|
||||
def get_param_name(self):
|
||||
"""Returns the full query parameter name for this action.
|
||||
|
||||
|
@ -486,6 +505,17 @@ class FilterAction(BaseAction):
|
|||
raise NotImplementedError("The filter method has not been "
|
||||
"implemented by %s." % self.__class__)
|
||||
|
||||
def is_api_filter(self, filter_field):
|
||||
"""Determine if the given filter field should be used as an
|
||||
API filter.
|
||||
"""
|
||||
if self.filter_type == 'server':
|
||||
for choice in self.filter_choices:
|
||||
if (choice[0] == filter_field and len(choice) > 2 and
|
||||
choice[2] is True):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class FixedFilterAction(FilterAction):
|
||||
"""A filter action with fixed buttons."""
|
||||
|
|
|
@ -1137,22 +1137,40 @@ class DataTable(object):
|
|||
and action.needs_preloading)
|
||||
valid_method = (request_method == action.method)
|
||||
if valid_method or needs_preloading:
|
||||
filter_field = self.get_filter_field()
|
||||
if self._meta.mixed_data_type:
|
||||
self._filtered_data = action.data_type_filter(self,
|
||||
self.data,
|
||||
filter_string)
|
||||
else:
|
||||
elif not action.is_api_filter(filter_field):
|
||||
self._filtered_data = action.filter(self,
|
||||
self.data,
|
||||
filter_string)
|
||||
return self._filtered_data
|
||||
|
||||
def get_filter_string(self):
|
||||
"""Get the filter string value. For 'server' type filters this is
|
||||
saved in the session so that it gets persisted across table loads.
|
||||
For other filter types this is obtained from the POST dict.
|
||||
"""
|
||||
filter_action = self._meta._filter_action
|
||||
param_name = filter_action.get_param_name()
|
||||
filter_string = self.request.POST.get(param_name, '')
|
||||
filter_string = ''
|
||||
if filter_action.filter_type == 'server':
|
||||
filter_string = self.request.session.get(param_name, '')
|
||||
else:
|
||||
filter_string = self.request.POST.get(param_name, '')
|
||||
return filter_string
|
||||
|
||||
def get_filter_field(self):
|
||||
"""Get the filter field value used for 'server' type filters. This
|
||||
is the value from the filter action's list of filter choices.
|
||||
"""
|
||||
filter_action = self._meta._filter_action
|
||||
param_name = '%s_field' % filter_action.get_param_name()
|
||||
filter_field = self.request.session.get(param_name, '')
|
||||
return filter_field
|
||||
|
||||
def _populate_data_cache(self):
|
||||
self._data_cache = {}
|
||||
# Set up hash tables to store data points for each column
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
|
||||
from collections import defaultdict
|
||||
|
||||
from django import shortcuts
|
||||
from django.views import generic
|
||||
|
||||
from horizon.templatetags.horizon import has_permissions # noqa
|
||||
|
@ -180,6 +181,7 @@ class DataTableView(MultiTableView):
|
|||
|
||||
def _get_data_dict(self):
|
||||
if not self._data:
|
||||
self.update_server_filter_action()
|
||||
self._data = {self.table_class._meta.name: self.get_data()}
|
||||
return self._data
|
||||
|
||||
|
@ -212,6 +214,63 @@ class DataTableView(MultiTableView):
|
|||
context[self.context_object_name] = self.table
|
||||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
# If the server side table filter changed then go back to the first
|
||||
# page of data. Otherwise GET and POST handling are the same.
|
||||
if self.handle_server_filter(request):
|
||||
return shortcuts.redirect(self.get_table().get_absolute_url())
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def get_server_filter_info(self, request):
|
||||
filter_action = self.get_table()._meta._filter_action
|
||||
if filter_action is None or filter_action.filter_type != 'server':
|
||||
return None
|
||||
param_name = filter_action.get_param_name()
|
||||
filter_string = request.POST.get(param_name)
|
||||
filter_string_session = request.session.get(param_name)
|
||||
changed = (filter_string is not None and
|
||||
filter_string != filter_string_session)
|
||||
if filter_string is None and filter_string_session is not None:
|
||||
filter_string = filter_string_session
|
||||
filter_field_param = param_name + '_field'
|
||||
filter_field = request.POST.get(filter_field_param)
|
||||
filter_field_session = request.session.get(filter_field_param)
|
||||
if filter_field is None and filter_field_session is not None:
|
||||
filter_field = filter_field_session
|
||||
filter_info = {
|
||||
'action': filter_action,
|
||||
'value_param': param_name,
|
||||
'value': filter_string,
|
||||
'field_param': filter_field_param,
|
||||
'field': filter_field,
|
||||
'changed': changed
|
||||
}
|
||||
return filter_info
|
||||
|
||||
def handle_server_filter(self, request):
|
||||
"""Update the table server filter information in the session and
|
||||
determine if the filter has been changed.
|
||||
"""
|
||||
filter_info = self.get_server_filter_info(request)
|
||||
if filter_info is None:
|
||||
return False
|
||||
request.session[filter_info['value_param']] = filter_info['value']
|
||||
if filter_info['field_param']:
|
||||
request.session[filter_info['field_param']] = filter_info['field']
|
||||
return filter_info['changed']
|
||||
|
||||
def update_server_filter_action(self):
|
||||
"""Update the table server side filter action based on the current
|
||||
filter. The filter info may be stored in the session and this will
|
||||
restore it.
|
||||
"""
|
||||
filter_info = self.get_server_filter_info(self.request)
|
||||
if filter_info is not None:
|
||||
action = filter_info['action']
|
||||
setattr(action, 'filter_string', filter_info['value'])
|
||||
if filter_info['field_param']:
|
||||
setattr(action, 'filter_field', filter_info['field'])
|
||||
|
||||
|
||||
class MixedDataTableView(DataTableView):
|
||||
"""A class-based generic view to handle DataTable with mixed data
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# Copyright 2012 Nebula, Inc.
|
||||
# Copyright 2014 IBM Corp.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
|
@ -168,6 +169,20 @@ class MyFilterAction(tables.FilterAction):
|
|||
return filter(comp, objs)
|
||||
|
||||
|
||||
class MyServerFilterAction(tables.FilterAction):
|
||||
filter_type = 'server'
|
||||
filter_choices = (('name', 'Name', False),
|
||||
('status', 'Status', True))
|
||||
needs_preloading = True
|
||||
|
||||
def filter(self, table, items, filter_string):
|
||||
filter_field = table.get_filter_field()
|
||||
if filter_field == 'name' and filter_string:
|
||||
return [item for item in items
|
||||
if filter_string in item.name]
|
||||
return items
|
||||
|
||||
|
||||
class MyUpdateAction(tables.UpdateAction):
|
||||
def allowed(self, *args):
|
||||
return True
|
||||
|
@ -221,6 +236,18 @@ class MyTable(tables.DataTable):
|
|||
row_actions = (MyAction, MyLinkAction, MyBatchAction, MyToggleAction)
|
||||
|
||||
|
||||
class MyServerFilterTable(MyTable):
|
||||
class Meta:
|
||||
name = "my_table"
|
||||
verbose_name = "My Table"
|
||||
status_columns = ["status"]
|
||||
columns = ('id', 'name', 'value', 'optional', 'status')
|
||||
row_class = MyRow
|
||||
column_class = MyColumn
|
||||
table_actions = (MyServerFilterAction, MyAction, MyBatchAction)
|
||||
row_actions = (MyAction, MyLinkAction, MyBatchAction, MyToggleAction)
|
||||
|
||||
|
||||
class MyTableSelectable(MyTable):
|
||||
class Meta:
|
||||
name = "my_table"
|
||||
|
@ -904,6 +931,32 @@ class DataTableTests(test.TestCase):
|
|||
self.assertEqual(unicode(row_actions[0].verbose_name), "Delete Me")
|
||||
self.assertEqual(unicode(row_actions[1].verbose_name), "Log In")
|
||||
|
||||
def test_server_filtering(self):
|
||||
filter_value_param = "my_table__filter__q"
|
||||
filter_field_param = '%s_field' % filter_value_param
|
||||
|
||||
# Server Filtering
|
||||
req = self.factory.post('/my_url/')
|
||||
req.session[filter_value_param] = '2'
|
||||
req.session[filter_field_param] = 'name'
|
||||
self.table = MyServerFilterTable(req, TEST_DATA)
|
||||
handled = self.table.maybe_handle()
|
||||
self.assertIsNone(handled)
|
||||
self.assertQuerysetEqual(self.table.filtered_data,
|
||||
['<FakeObject: object_2>'])
|
||||
|
||||
# Ensure API filtering does not filter on server, e.g. no filter here
|
||||
req = self.factory.post('/my_url/')
|
||||
req.session[filter_value_param] = 'up'
|
||||
req.session[filter_field_param] = 'status'
|
||||
self.table = MyServerFilterTable(req, TEST_DATA)
|
||||
handled = self.table.maybe_handle()
|
||||
self.assertIsNone(handled)
|
||||
self.assertQuerysetEqual(self.table.filtered_data,
|
||||
['<FakeObject: object_1>',
|
||||
'<FakeObject: object_2>',
|
||||
'<FakeObject: object_3>'])
|
||||
|
||||
def test_inline_edit_update_action_get_non_ajax(self):
|
||||
# Non ajax inline edit request should return None.
|
||||
url = ('/my_url/?action=cell_update'
|
||||
|
@ -1183,6 +1236,10 @@ class SingleTableView(table_views.DataTableView):
|
|||
return TEST_DATA
|
||||
|
||||
|
||||
class APIFilterTableView(SingleTableView):
|
||||
table_class = MyServerFilterTable
|
||||
|
||||
|
||||
class TableWithPermissions(tables.DataTable):
|
||||
id = tables.Column('id')
|
||||
|
||||
|
@ -1248,6 +1305,26 @@ class DataTableViewTests(test.TestCase):
|
|||
self.assertEqual(context['table_with_permissions_table'].__class__,
|
||||
TableWithPermissions)
|
||||
|
||||
def test_api_filter_table_view(self):
|
||||
filter_value_param = "my_table__filter__q"
|
||||
filter_field_param = '%s_field' % filter_value_param
|
||||
req = self.factory.post('/my_url/', {filter_value_param: 'up',
|
||||
filter_field_param: 'status'})
|
||||
req.user = self.user
|
||||
view = APIFilterTableView()
|
||||
view.request = req
|
||||
view.kwargs = {}
|
||||
view.handle_server_filter(req)
|
||||
context = view.get_context_data()
|
||||
self.assertEqual(context['table'].__class__, MyServerFilterTable)
|
||||
data = view.get_data()
|
||||
self.assertQuerysetEqual(data,
|
||||
['<FakeObject: object_1>',
|
||||
'<FakeObject: object_2>',
|
||||
'<FakeObject: object_3>'])
|
||||
self.assertEqual(req.session.get(filter_value_param), 'up')
|
||||
self.assertEqual(req.session.get(filter_field_param), 'status')
|
||||
|
||||
|
||||
class FormsetTableTests(test.TestCase):
|
||||
|
||||
|
|
|
@ -55,6 +55,15 @@ class UpdateRow(tables.Row):
|
|||
return image
|
||||
|
||||
|
||||
class AdminImageFilterAction(tables.FilterAction):
|
||||
filter_type = "server"
|
||||
filter_choices = (('name', _("Image Name ="), True),
|
||||
('status', _('Status ='), True),
|
||||
('disk_format', _('Format ='), True),
|
||||
('size_min', _('Min. Size (MB)'), True),
|
||||
('size_max', _('Max. Size (MB)'), True))
|
||||
|
||||
|
||||
class AdminImagesTable(project_tables.ImagesTable):
|
||||
name = tables.Column("name",
|
||||
link="horizon:admin:images:detail",
|
||||
|
@ -65,5 +74,6 @@ class AdminImagesTable(project_tables.ImagesTable):
|
|||
row_class = UpdateRow
|
||||
status_columns = ["status"]
|
||||
verbose_name = _("Images")
|
||||
table_actions = (AdminCreateImage, AdminDeleteImage)
|
||||
table_actions = (AdminCreateImage, AdminDeleteImage,
|
||||
AdminImageFilterAction)
|
||||
row_actions = (AdminEditImage, ViewCustomProperties, AdminDeleteImage)
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
@ -29,6 +31,8 @@ from openstack_dashboard.dashboards.admin.images import forms
|
|||
from openstack_dashboard.dashboards.admin.images \
|
||||
import tables as project_tables
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IndexView(tables.DataTableView):
|
||||
table_class = project_tables.AdminImagesTable
|
||||
|
@ -42,8 +46,7 @@ class IndexView(tables.DataTableView):
|
|||
|
||||
def get_data(self):
|
||||
images = []
|
||||
filters = {'is_public': None}
|
||||
|
||||
filters = self.get_filters()
|
||||
prev_marker = self.request.GET.get(
|
||||
project_tables.AdminImagesTable._meta.prev_pagination_param, None)
|
||||
|
||||
|
@ -73,6 +76,28 @@ class IndexView(tables.DataTableView):
|
|||
exceptions.handle(self.request, msg)
|
||||
return images
|
||||
|
||||
def get_filters(self):
|
||||
filters = {'is_public': None}
|
||||
filter_field = self.table.get_filter_field()
|
||||
filter_string = self.table.get_filter_string()
|
||||
filter_action = self.table._meta._filter_action
|
||||
if filter_field and filter_string and (
|
||||
filter_action.is_api_filter(filter_field)):
|
||||
if filter_field in ['size_min', 'size_max']:
|
||||
invalid_msg = ('API query is not valid and is ignored: %s=%s'
|
||||
% (filter_field, filter_string))
|
||||
try:
|
||||
filter_string = long(float(filter_string) * (1024 ** 2))
|
||||
if filter_string >= 0:
|
||||
filters[filter_field] = filter_string
|
||||
else:
|
||||
LOG.warning(invalid_msg)
|
||||
except ValueError:
|
||||
LOG.warning(invalid_msg)
|
||||
else:
|
||||
filters[filter_field] = filter_string
|
||||
return filters
|
||||
|
||||
|
||||
class CreateView(views.CreateView):
|
||||
template_name = 'admin/images/create.html'
|
||||
|
|
|
@ -82,26 +82,28 @@ class AdminUpdateRow(project_tables.UpdateRow):
|
|||
|
||||
|
||||
class AdminInstanceFilterAction(tables.FilterAction):
|
||||
# Change default name of 'filter' to distinguish this one from the
|
||||
# project instances table filter, since this is used as part of the
|
||||
# session property used for persisting the filter.
|
||||
name = "filter_admin_instances"
|
||||
filter_type = "server"
|
||||
filter_choices = (('project', _("Project")),
|
||||
('name', _("Name"))
|
||||
)
|
||||
needs_preloading = True
|
||||
filter_choices = (('project', _("Project"), False),
|
||||
('host', _("Host ="), True),
|
||||
('name', _("Name"), True),
|
||||
('ip', _("IPv4 Address ="), True),
|
||||
('ip6', _("IPv6 Address ="), True),
|
||||
('status', _("Status ="), True),
|
||||
('image', _("Image ID ="), True),
|
||||
('flavor', _("Flavor ID ="), True))
|
||||
|
||||
def filter(self, table, instances, filter_string):
|
||||
"""Server side search.
|
||||
When filtering is supported in the api, then we will handle in view
|
||||
"""
|
||||
filter_field = table.request.POST.get('instances__filter__q_field')
|
||||
self.filter_field = filter_field
|
||||
self.filter_string = filter_string
|
||||
filter_field = table.get_filter_field()
|
||||
if filter_field == 'project' and filter_string:
|
||||
return [inst for inst in instances
|
||||
if inst.tenant_name == filter_string]
|
||||
if filter_field == 'name' and filter_string:
|
||||
q = filter_string.lower()
|
||||
return [instance for instance in instances
|
||||
if q in instance.name.lower()]
|
||||
return instances
|
||||
|
||||
|
||||
|
|
|
@ -72,11 +72,11 @@ class AdminIndexView(tables.DataTableView):
|
|||
instances = []
|
||||
marker = self.request.GET.get(
|
||||
project_tables.AdminInstancesTable._meta.pagination_param, None)
|
||||
search_opts = self.get_filters({'marker': marker, 'paginate': True})
|
||||
try:
|
||||
instances, self._more = api.nova.server_list(
|
||||
self.request,
|
||||
search_opts={'marker': marker,
|
||||
'paginate': True},
|
||||
search_opts=search_opts,
|
||||
all_tenants=True)
|
||||
except Exception:
|
||||
self._more = False
|
||||
|
@ -126,6 +126,15 @@ class AdminIndexView(tables.DataTableView):
|
|||
inst.tenant_name = getattr(tenant, "name", None)
|
||||
return instances
|
||||
|
||||
def get_filters(self, filters):
|
||||
filter_field = self.table.get_filter_field()
|
||||
filter_action = self.table._meta._filter_action
|
||||
if filter_action.is_api_filter(filter_field):
|
||||
filter_string = self.table.get_filter_string()
|
||||
if filter_field and filter_string:
|
||||
filters[filter_field] = filter_string
|
||||
return filters
|
||||
|
||||
|
||||
class LiveMigrateView(forms.ModalFormView):
|
||||
form_class = project_forms.LiveMigrateForm
|
||||
|
|
|
@ -781,12 +781,11 @@ POWER_DISPLAY_CHOICES = (
|
|||
|
||||
|
||||
class InstancesFilterAction(tables.FilterAction):
|
||||
|
||||
def filter(self, table, instances, filter_string):
|
||||
"""Naive case-insensitive search."""
|
||||
q = filter_string.lower()
|
||||
return [instance for instance in instances
|
||||
if q in instance.name.lower()]
|
||||
filter_type = "server"
|
||||
filter_choices = (('name', _("Instance Name"), True),
|
||||
('status', _("Status ="), True),
|
||||
('image', _("Image ID ="), True),
|
||||
('flavor', _("Flavor ID ="), True))
|
||||
|
||||
|
||||
class InstancesTable(tables.DataTable):
|
||||
|
|
|
@ -57,12 +57,12 @@ class IndexView(tables.DataTableView):
|
|||
def get_data(self):
|
||||
marker = self.request.GET.get(
|
||||
project_tables.InstancesTable._meta.pagination_param, None)
|
||||
search_opts = self.get_filters({'marker': marker, 'paginate': True})
|
||||
# Gather our instances
|
||||
try:
|
||||
instances, self._more = api.nova.server_list(
|
||||
self.request,
|
||||
search_opts={'marker': marker,
|
||||
'paginate': True})
|
||||
search_opts=search_opts)
|
||||
except Exception:
|
||||
self._more = False
|
||||
instances = []
|
||||
|
@ -120,6 +120,15 @@ class IndexView(tables.DataTableView):
|
|||
exceptions.handle(self.request, msg)
|
||||
return instances
|
||||
|
||||
def get_filters(self, filters):
|
||||
filter_field = self.table.get_filter_field()
|
||||
filter_action = self.table._meta._filter_action
|
||||
if filter_action.is_api_filter(filter_field):
|
||||
filter_string = self.table.get_filter_string()
|
||||
if filter_field and filter_string:
|
||||
filters[filter_field] = filter_string
|
||||
return filters
|
||||
|
||||
|
||||
class LaunchInstanceView(workflows.WorkflowView):
|
||||
workflow_class = project_workflows.LaunchInstance
|
||||
|
|
|
@ -641,6 +641,9 @@ table form {
|
|||
input[type="text"] {
|
||||
padding-right: 26px;
|
||||
}
|
||||
select {
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
td.no-transition {
|
||||
|
|
Loading…
Reference in New Issue