95629a337e
H405: multi line docstring summary not separated with an empty line Partial-Bug: #1696996 Change-Id: I58f71206def1a25f3eda04b9297f1aa7d3caa646
390 lines
15 KiB
Python
390 lines
15 KiB
Python
# Copyright 2012 Nebula, Inc.
|
|
#
|
|
# 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
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
from collections import defaultdict
|
|
|
|
from django import shortcuts
|
|
|
|
from horizon import views
|
|
|
|
from horizon.templatetags.horizon import has_permissions
|
|
|
|
|
|
class MultiTableMixin(object):
|
|
"""A generic mixin which provides methods for handling DataTables."""
|
|
data_method_pattern = "get_%s_data"
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(MultiTableMixin, self).__init__(*args, **kwargs)
|
|
self.table_classes = getattr(self, "table_classes", [])
|
|
self._data = {}
|
|
self._tables = {}
|
|
self._data_methods = defaultdict(list)
|
|
self.get_data_methods(self.table_classes, self._data_methods)
|
|
|
|
def _get_data_dict(self):
|
|
if not self._data:
|
|
for table in self.table_classes:
|
|
data = []
|
|
name = table._meta.name
|
|
func_list = self._data_methods.get(name, [])
|
|
for func in func_list:
|
|
data.extend(func())
|
|
self._data[name] = data
|
|
return self._data
|
|
|
|
def get_data_methods(self, table_classes, methods):
|
|
for table in table_classes:
|
|
name = table._meta.name
|
|
if table._meta.mixed_data_type:
|
|
for data_type in table._meta.data_types:
|
|
func = self.check_method_exist(self.data_method_pattern,
|
|
data_type)
|
|
if func:
|
|
type_name = table._meta.data_type_name
|
|
methods[name].append(self.wrap_func(func,
|
|
type_name,
|
|
data_type))
|
|
else:
|
|
func = self.check_method_exist(self.data_method_pattern,
|
|
name)
|
|
if func:
|
|
methods[name].append(func)
|
|
|
|
def wrap_func(self, data_func, type_name, data_type):
|
|
def final_data():
|
|
data = data_func()
|
|
self.assign_type_string(data, type_name, data_type)
|
|
return data
|
|
return final_data
|
|
|
|
def check_method_exist(self, func_pattern="%s", *names):
|
|
func_name = func_pattern % names
|
|
func = getattr(self, func_name, None)
|
|
if not func or not callable(func):
|
|
cls_name = self.__class__.__name__
|
|
raise NotImplementedError("You must define a %s method "
|
|
"in %s." % (func_name, cls_name))
|
|
else:
|
|
return func
|
|
|
|
def assign_type_string(self, data, type_name, data_type):
|
|
for datum in data:
|
|
setattr(datum, type_name, data_type)
|
|
|
|
def get_tables(self):
|
|
if not self.table_classes:
|
|
raise AttributeError('You must specify one or more DataTable '
|
|
'classes for the "table_classes" attribute '
|
|
'on %s.' % self.__class__.__name__)
|
|
if not self._tables:
|
|
for table in self.table_classes:
|
|
if not has_permissions(self.request.user,
|
|
table._meta):
|
|
continue
|
|
func_name = "get_%s_table" % table._meta.name
|
|
table_func = getattr(self, func_name, None)
|
|
if table_func is None:
|
|
tbl = table(self.request, **self.kwargs)
|
|
else:
|
|
tbl = table_func(self, self.request, **self.kwargs)
|
|
self._tables[table._meta.name] = tbl
|
|
return self._tables
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(MultiTableMixin, self).get_context_data(**kwargs)
|
|
tables = self.get_tables()
|
|
for name, table in tables.items():
|
|
context["%s_table" % name] = table
|
|
return context
|
|
|
|
def has_prev_data(self, table):
|
|
return False
|
|
|
|
def has_more_data(self, table):
|
|
return False
|
|
|
|
def needs_filter_first(self, table):
|
|
return False
|
|
|
|
def handle_table(self, table):
|
|
name = table.name
|
|
data = self._get_data_dict()
|
|
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_prev_data = self.has_prev_data(table)
|
|
handled = self._tables[name].maybe_handle()
|
|
return handled
|
|
|
|
def get_server_filter_info(self, request, table=None):
|
|
if not table:
|
|
table = self.get_table()
|
|
filter_action = 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:
|
|
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, table=None):
|
|
"""Update the table server filter information in the session.
|
|
|
|
Returns True if the filter has been changed.
|
|
"""
|
|
if not table:
|
|
table = self.get_table()
|
|
filter_info = self.get_server_filter_info(request, table)
|
|
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, request, table=None):
|
|
"""Update the table server side filter action.
|
|
|
|
It is done based on the current filter. The filter info may be stored
|
|
in the session and this will restore it.
|
|
"""
|
|
if not table:
|
|
table = self.get_table()
|
|
filter_info = self.get_server_filter_info(request, table)
|
|
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 MultiTableView(MultiTableMixin, views.HorizonTemplateView):
|
|
"""Generic view to handle multiple DataTable classes in a single view.
|
|
|
|
Each DataTable class must be a :class:`~horizon.tables.DataTable` class
|
|
or its subclass.
|
|
|
|
Three steps are required to use this view: set the ``table_classes``
|
|
attribute with a tuple of the desired
|
|
:class:`~horizon.tables.DataTable` classes;
|
|
define a ``get_{{ table_name }}_data`` method for each table class
|
|
which returns a set of data for that table; and specify a template for
|
|
the ``template_name`` attribute.
|
|
"""
|
|
|
|
def construct_tables(self):
|
|
tables = self.get_tables().values()
|
|
# Early out before data is loaded
|
|
for table in tables:
|
|
preempted = table.maybe_preempt()
|
|
if preempted:
|
|
return preempted
|
|
# Load data into each table and check for action handlers
|
|
for table in tables:
|
|
handled = self.handle_table(table)
|
|
if handled:
|
|
return handled
|
|
|
|
# If we didn't already return a response, returning None continues
|
|
# with the view as normal.
|
|
return None
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
handled = self.construct_tables()
|
|
if handled:
|
|
return handled
|
|
context = self.get_context_data(**kwargs)
|
|
return self.render_to_response(context)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
# GET and POST handling are the same
|
|
return self.get(request, *args, **kwargs)
|
|
|
|
|
|
class DataTableView(MultiTableView):
|
|
"""A class-based generic view to handle basic DataTable processing.
|
|
|
|
Three steps are required to use this view: set the ``table_class``
|
|
attribute with the desired :class:`~horizon.tables.DataTable` class;
|
|
define a ``get_data`` method which returns a set of data for the
|
|
table; and specify a template for the ``template_name`` attribute.
|
|
|
|
Optionally, you can override the ``has_more_data`` method to trigger
|
|
pagination handling for APIs that support it.
|
|
"""
|
|
table_class = None
|
|
context_object_name = 'table'
|
|
template_name = 'horizon/common/_data_table_view.html'
|
|
|
|
def _get_data_dict(self):
|
|
if not self._data:
|
|
self.update_server_filter_action(self.request)
|
|
self._data = {self.table_class._meta.name: self.get_data()}
|
|
return self._data
|
|
|
|
def get_data(self):
|
|
return []
|
|
|
|
def get_tables(self):
|
|
if not self._tables:
|
|
self._tables = {}
|
|
if has_permissions(self.request.user,
|
|
self.table_class._meta):
|
|
self._tables[self.table_class._meta.name] = self.get_table()
|
|
return self._tables
|
|
|
|
def get_table(self):
|
|
# Note: this method cannot be easily memoized, because get_context_data
|
|
# uses its cached value directly.
|
|
if not self.table_class:
|
|
raise AttributeError('You must specify a DataTable class for the '
|
|
'"table_class" attribute on %s.'
|
|
% self.__class__.__name__)
|
|
if not hasattr(self, "table"):
|
|
self.table = self.table_class(self.request, **self.kwargs)
|
|
return self.table
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super(DataTableView, self).get_context_data(**kwargs)
|
|
if hasattr(self, "table"):
|
|
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_filters(self, filters=None, filters_map=None):
|
|
"""Converts a string given by the user into a valid api filter value.
|
|
|
|
:filters: Default filter values.
|
|
{'filter1': filter_value, 'filter2': filter_value}
|
|
:filters_map: mapping between user input and valid api filter values.
|
|
{'filter_name':{_("true_value"):True, _("false_value"):False}
|
|
"""
|
|
filters = filters or {}
|
|
filters_map = filters_map or {}
|
|
filter_action = self.table._meta._filter_action
|
|
if filter_action:
|
|
filter_field = self.table.get_filter_field()
|
|
if filter_action.is_api_filter(filter_field):
|
|
filter_string = self.table.get_filter_string().strip()
|
|
if filter_field and filter_string:
|
|
filter_map = filters_map.get(filter_field, {})
|
|
# We use the filter_string given by the user and
|
|
# look for valid values in the filter_map that's why
|
|
# we apply lower()
|
|
filters[filter_field] = filter_map.get(
|
|
filter_string.lower(), filter_string)
|
|
return filters
|
|
|
|
|
|
class MixedDataTableView(DataTableView):
|
|
"""A class-based generic view to handle DataTable with mixed data types.
|
|
|
|
Basic usage is the same as DataTableView.
|
|
|
|
Three steps are required to use this view:
|
|
#. Set the ``table_class`` attribute with desired
|
|
:class:`~horizon.tables.DataTable` class. In the class the
|
|
``data_types`` list should have at least two elements.
|
|
|
|
#. Define a ``get_{{ data_type }}_data`` method for each data type
|
|
which returns a set of data for the table.
|
|
|
|
#. Specify a template for the ``template_name`` attribute.
|
|
"""
|
|
table_class = None
|
|
context_object_name = 'table'
|
|
|
|
def _get_data_dict(self):
|
|
if not self._data:
|
|
table = self.table_class
|
|
self._data = {table._meta.name: []}
|
|
for data_type in table.data_types:
|
|
func_name = "get_%s_data" % data_type
|
|
data_func = getattr(self, func_name, None)
|
|
if data_func is None:
|
|
cls_name = self.__class__.__name__
|
|
raise NotImplementedError("You must define a %s method "
|
|
"for %s data type in %s." %
|
|
(func_name, data_type, cls_name))
|
|
data = data_func()
|
|
self.assign_type_string(data, data_type)
|
|
self._data[table._meta.name].extend(data)
|
|
return self._data
|
|
|
|
def assign_type_string(self, data, type_string):
|
|
for datum in data:
|
|
setattr(datum, self.table_class.data_type_name,
|
|
type_string)
|
|
|
|
def get_table(self):
|
|
self.table = super(MixedDataTableView, self).get_table()
|
|
if not self.table._meta.mixed_data_type:
|
|
raise AttributeError('You must have at least two elements in '
|
|
'the data_types attribute '
|
|
'in table %s to use MixedDataTableView.'
|
|
% self.table._meta.name)
|
|
return self.table
|
|
|
|
|
|
class PagedTableMixin(object):
|
|
def __init__(self, *args, **kwargs):
|
|
super(PagedTableMixin, self).__init__(*args, **kwargs)
|
|
self._has_prev_data = False
|
|
self._has_more_data = False
|
|
|
|
def has_prev_data(self, table):
|
|
return self._has_prev_data
|
|
|
|
def has_more_data(self, table):
|
|
return self._has_more_data
|
|
|
|
def _get_marker(self):
|
|
try:
|
|
meta = self.table_class._meta
|
|
except AttributeError:
|
|
meta = self.table_classes[0]._meta
|
|
prev_marker = self.request.GET.get(meta.prev_pagination_param, None)
|
|
if prev_marker:
|
|
return prev_marker, "asc"
|
|
else:
|
|
marker = self.request.GET.get(meta.pagination_param, None)
|
|
if marker:
|
|
return marker, "desc"
|
|
return None, "desc"
|