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:
parent
8408be31dc
commit
488efd784f
@ -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``
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
@ -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>
|
||||||
|
@ -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',)})
|
||||||
|
@ -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
|
||||||
|
@ -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, [])
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
@ -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.
|
Loading…
Reference in New Issue
Block a user