Implement admin_filter_first in Instances and Images Admin Views

Implementing ADMIN_FILTER_DATA_FIRST setting will allow operators to
require admin users provide a search criteria first before loading
data into the Admin views.
Starting with Instances and Images admin views since they already have
server side filtering options.

Implements blueprint: admin-views-filter-first

Change-Id: I846690854fd443bb98692674bf9fe6733dd04263
This commit is contained in:
Luis Daniel Castellanos 2016-05-23 15:25:49 -05:00
parent 8408be31dc
commit 488efd784f
10 changed files with 97 additions and 4 deletions

View File

@ -1542,6 +1542,17 @@ Can be used to selectively disable certain costly extensions for performance
reasons. reasons.
``ADMIN_FILTER_DATA_FIRST``
---------------------------
.. versionadded:: 10.0.0(Newton)
Default: ``False``
If True, when admin views load, an empty table will be rendered and the
user will be asked to provide a search criteria first (in case no search
criteria was provided) before loading any data.
``OPERATION_LOG_ENABLED`` ``OPERATION_LOG_ENABLED``
------------------------- -------------------------

View File

@ -1063,6 +1063,11 @@ class DataTableOptions(object):
'data_type_name', 'data_type_name',
"_table_data_type") "_table_data_type")
self.filter_first_message = \
getattr(options,
'filter_first_message',
_('Please specify a search criteria first.'))
class DataTableMetaclass(type): class DataTableMetaclass(type):
"""Metaclass to add options to DataTable class and collect columns.""" """Metaclass to add options to DataTable class and collect columns."""
@ -1177,6 +1182,8 @@ class DataTable(object):
self.breadcrumb = None self.breadcrumb = None
self.current_item_id = None self.current_item_id = None
self.permissions = self._meta.permissions self.permissions = self._meta.permissions
self.needs_filter_first = False
self._filter_first_message = self._meta.filter_first_message
# Create a new set # Create a new set
columns = [] columns = []
@ -1324,6 +1331,12 @@ class DataTable(object):
"""Returns the message to be displayed when there is no data.""" """Returns the message to be displayed when there is no data."""
return self._no_data_message return self._no_data_message
def get_filter_first_message(self):
"""Return the message to be displayed when the user needs to provide
first a search criteria before loading any data.
"""
return self._filter_first_message
def get_object_by_id(self, lookup): def get_object_by_id(self, lookup):
"""Returns the data object from the table's dataset which matches """Returns the data object from the table's dataset which matches
the ``lookup`` parameter specified. An error will be raised if the ``lookup`` parameter specified. An error will be raised if

View File

@ -14,6 +14,7 @@
from collections import defaultdict from collections import defaultdict
from django.conf import settings
from django import shortcuts from django import shortcuts
from horizon import views from horizon import views
@ -30,9 +31,11 @@ class MultiTableMixin(object):
self.table_classes = getattr(self, "table_classes", []) self.table_classes = getattr(self, "table_classes", [])
self._data = {} self._data = {}
self._tables = {} self._tables = {}
self._data_methods = defaultdict(list) self._data_methods = defaultdict(list)
self.get_data_methods(self.table_classes, self._data_methods) self.get_data_methods(self.table_classes, self._data_methods)
self.admin_filter_first = getattr(settings,
'ADMIN_FILTER_DATA_FIRST',
False)
def _get_data_dict(self): def _get_data_dict(self):
if not self._data: if not self._data:
@ -116,10 +119,15 @@ class MultiTableMixin(object):
def has_more_data(self, table): def has_more_data(self, table):
return False return False
def needs_filter_first(self, table):
return False
def handle_table(self, table): def handle_table(self, table):
name = table.name name = table.name
data = self._get_data_dict() data = self._get_data_dict()
self._tables[name].data = data[table._meta.name] self._tables[name].data = data[table._meta.name]
self._tables[name].needs_filter_first = \
self.needs_filter_first(table)
self._tables[name]._meta.has_more_data = self.has_more_data(table) self._tables[name]._meta.has_more_data = self.has_more_data(table)
self._tables[name]._meta.has_prev_data = self.has_prev_data(table) self._tables[name]._meta.has_prev_data = self.has_prev_data(table)
handled = self._tables[name].maybe_handle() handled = self._tables[name].maybe_handle()

View File

@ -46,7 +46,11 @@
{{ row.render }} {{ row.render }}
{% empty %} {% empty %}
<tr class="{% cycle 'odd' 'even' %} empty"> <tr class="{% cycle 'odd' 'even' %} empty">
{% if table.needs_filter_first %}
<td colspan="{{ columns|length }}">{{ table.get_filter_first_message }}</td>
{% else %}
<td colspan="{{ columns|length }}">{{ table.get_empty_message }}</td> <td colspan="{{ columns|length }}">{{ table.get_empty_message }}</td>
{% endif %}
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -68,6 +68,13 @@ class ImagesViewTest(test.BaseAdminViewTests):
self.assertEqual(len(res.context['images_table'].data), self.assertEqual(len(res.context['images_table'].data),
len(self.images.list())) len(self.images.list()))
@test.update_settings(ADMIN_FILTER_DATA_FIRST=True)
def test_images_list_with_admin_filter_first(self):
res = self.client.get(reverse('horizon:admin:images:index'))
self.assertTemplateUsed(res, 'admin/images/index.html')
images = res.context['table'].data
self.assertItemsEqual(images, [])
@override_settings(API_RESULT_PAGE_SIZE=2) @override_settings(API_RESULT_PAGE_SIZE=2)
@test.create_stubs({api.glance: ('image_list_detailed',), @test.create_stubs({api.glance: ('image_list_detailed',),
api.keystone: ('tenant_list',)}) api.keystone: ('tenant_list',)})

View File

@ -41,6 +41,7 @@ LOG = logging.getLogger(__name__)
class IndexView(tables.DataTableView): class IndexView(tables.DataTableView):
DEFAULT_FILTERS = {'is_public': None}
table_class = project_tables.AdminImagesTable table_class = project_tables.AdminImagesTable
template_name = 'admin/images/index.html' template_name = 'admin/images/index.html'
page_title = _("Images") page_title = _("Images")
@ -51,13 +52,27 @@ class IndexView(tables.DataTableView):
def has_more_data(self, table): def has_more_data(self, table):
return self._more return self._more
def needs_filter_first(self, table):
return self._needs_filter_first
def get_data(self): def get_data(self):
images = [] images = []
if not policy.check((("image", "get_images"),), self.request): if not policy.check((("image", "get_images"),), self.request):
msg = _("Insufficient privilege level to retrieve image list.") msg = _("Insufficient privilege level to retrieve image list.")
messages.info(self.request, msg) messages.info(self.request, msg)
return images return images
filters = self.get_filters() filters = self.get_filters()
if self.admin_filter_first and \
len(filters) == len(self.DEFAULT_FILTERS):
self._prev = False
self._more = False
self._needs_filter_first = True
return images
self._needs_filter_first = False
prev_marker = self.request.GET.get( prev_marker = self.request.GET.get(
project_tables.AdminImagesTable._meta.prev_pagination_param, None) project_tables.AdminImagesTable._meta.prev_pagination_param, None)
@ -97,7 +112,7 @@ class IndexView(tables.DataTableView):
return images return images
def get_filters(self): def get_filters(self):
filters = {'is_public': None} filters = self.DEFAULT_FILTERS.copy()
filter_field = self.table.get_filter_field() filter_field = self.table.get_filter_field()
filter_string = self.table.get_filter_string() filter_string = self.table.get_filter_string()
filter_action = self.table._meta._filter_action filter_action = self.table._meta._filter_action

View File

@ -400,3 +400,10 @@ class InstanceViewTest(test.BaseAdminViewTests):
res = self.client.get(url) res = self.client.get(url)
self.assertRedirectsNoFollow(res, INDEX_URL) self.assertRedirectsNoFollow(res, INDEX_URL)
@test.update_settings(ADMIN_FILTER_DATA_FIRST=True)
def test_index_with_admin_filter_first(self):
res = self.client.get(INDEX_URL)
self.assertTemplateUsed(res, 'admin/instances/index.html')
instances = res.context['table'].data
self.assertItemsEqual(instances, [])

View File

@ -70,11 +70,27 @@ class AdminIndexView(tables.DataTableView):
def has_more_data(self, table): def has_more_data(self, table):
return self._more return self._more
def needs_filter_first(self, table):
return self._needs_filter_first
def get_data(self): def get_data(self):
instances = [] instances = []
marker = self.request.GET.get( marker = self.request.GET.get(
project_tables.AdminInstancesTable._meta.pagination_param, None) project_tables.AdminInstancesTable._meta.pagination_param, None)
search_opts = self.get_filters({'marker': marker, 'paginate': True}) default_search_opts = {'marker': marker, 'paginate': True}
search_opts = self.get_filters(default_search_opts.copy())
"""If admin_filter_first is set and if there are not other filters
selected, then search criteria must be provided and return an empty
list"""
if self.admin_filter_first and \
len(search_opts) == len(default_search_opts):
self._needs_filter_first = True
self._more = False
return instances
self._needs_filter_first = False
# Gather our tenants to correlate against IDs # Gather our tenants to correlate against IDs
try: try:
tenants, has_more = api.keystone.tenant_list(self.request) tenants, has_more = api.keystone.tenant_list(self.request)

View File

@ -771,3 +771,8 @@ REST_API_REQUIRED_SETTINGS = ['OPENSTACK_HYPERVISOR_FEATURES',
# until today (if set to None). This setting should be used to limit the amount # until today (if set to None). This setting should be used to limit the amount
# of data fetched by default when rendering the Overview panel. # of data fetched by default when rendering the Overview panel.
#OVERVIEW_DAYS_RANGE = 1 #OVERVIEW_DAYS_RANGE = 1
# To allow operators to require admin users provide a search criteria first
# before loading any data into the admin views, set the following attribute to
# True
#ADMIN_FILTER_DATA_FIRST=False

View File

@ -0,0 +1,7 @@
---
features:
- >
[`blueprint admin-views-filter-first <https://blueprints.launchpad.net/horizon/+spec/admin-views-filter-first>`_]
This blueprint provides a configurable setting to allow operators
require admin users to provide a search criteria first before loading data
into admin views.