Source code for horizon.tables.views

# 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  # noqa


[docs]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
[docs] 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)
[docs] 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
[docs] 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
[docs] def assign_type_string(self, data, type_name, data_type): for datum in data: setattr(datum, type_name, data_type)
[docs] 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
[docs] 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
[docs] def has_prev_data(self, table): return False
[docs] def has_more_data(self, table): return False
[docs] def needs_filter_first(self, table): return False
[docs] 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
[docs] 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
[docs] def handle_server_filter(self, request, table=None): """Update the table server filter information in the session and determine 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']
[docs] def update_server_filter_action(self, request, table=None): """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. """ 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'])
[docs]class MultiTableView(MultiTableMixin, views.HorizonTemplateView): """A class-based generic view to handle the display and processing of multiple :class:`~horizon.tables.DataTable` classes in a single view. 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. """
[docs] 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
[docs] 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)
[docs] def post(self, request, *args, **kwargs): # GET and POST handling are the same return self.get(request, *args, **kwargs)
[docs]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' 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
[docs] def get_data(self): return []
[docs] 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
[docs] 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
[docs] 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
[docs] 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)
[docs] 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
[docs]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
[docs] def assign_type_string(self, data, type_string): for datum in data: setattr(datum, self.table_class.data_type_name, type_string)
[docs] 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

Project Source