diff options
author | Gabriel Hurley <gabriel@strikeawe.com> | 2012-03-24 16:11:19 -0700 |
---|---|---|
committer | Gabriel Hurley <gabriel@strikeawe.com> | 2012-03-24 16:14:33 -0700 |
commit | e02c442c86a0e961442d2825d451ef0508acbd72 (patch) | |
tree | e2bb158ee5ec3afc9ba985568275dd7239d54cc4 /horizon/tabs | |
parent | ab71aff23f30360e8463d3a5ca2cec5719994b8c (diff) | |
download | horizon-e02c442c86a0e961442d2825d451ef0508acbd72.tar.gz |
Adds support for tabs + tables.
Creates new TableTab and TabbedTableView classes to support
the complex logic involved in processing both table and tab
actions in a single view.
Fixes bug 964214.
Change-Id: I3f70d77975593773bf783d31de06d2b724aad2d5
Diffstat (limited to 'horizon/tabs')
-rw-r--r-- | horizon/tabs/__init__.py | 4 | ||||
-rw-r--r-- | horizon/tabs/base.py | 107 | ||||
-rw-r--r-- | horizon/tabs/views.py | 107 |
3 files changed, 198 insertions, 20 deletions
diff --git a/horizon/tabs/__init__.py b/horizon/tabs/__init__.py index de62cf8ac..8f9834f42 100644 --- a/horizon/tabs/__init__.py +++ b/horizon/tabs/__init__.py @@ -14,5 +14,5 @@ # License for the specific language governing permissions and limitations # under the License. -from .base import TabGroup, Tab -from .views import TabView +from .base import TabGroup, Tab, TableTab +from .views import TabView, TabbedTableView diff --git a/horizon/tabs/base.py b/horizon/tabs/base.py index 12ade1986..520b62da7 100644 --- a/horizon/tabs/base.py +++ b/horizon/tabs/base.py @@ -95,14 +95,18 @@ class TabGroup(html.HTMLElement): self._tabs = SortedDict(tab_instances) if not self._set_active_tab(): self.tabs_not_available() - # Preload all data that will be loaded to allow errors to be displayed - for tab in self._tabs.values(): - if tab.load: - tab._context_data = tab.get_context_data(request) def __repr__(self): return "<%s: %s>" % (self.__class__.__name__, self.slug) + def load_tab_data(self): + """ + Preload all data that for the tabs that will be displayed. + """ + for tab in self._tabs.values(): + if tab.load and not tab.data_loaded: + tab._data = tab.get_context_data(self.request) + def get_id(self): """ Returns the id for this tab group. Defaults to the value of the tab @@ -171,6 +175,9 @@ class TabGroup(html.HTMLElement): return tab return None + def get_loaded_tabs(self): + return filter(lambda t: self.get_tab(t.slug), self._tabs.values()) + def get_selected_tab(self): """ Returns the tab specific by the GET request parameter. @@ -254,15 +261,20 @@ class Tab(html.HTMLElement): return load_preloaded and self._allowed and self._enabled @property - def context_data(self): - if not getattr(self, "_context_data", None): - self._context_data = self.get_context_data(self.request) - return self._context_data + def data(self): + if not getattr(self, "_data", None): + self._data = self.get_context_data(self.request) + return self._data + + @property + def data_loaded(self): + return getattr(self, "_data", None) is not None def render(self): """ - Renders the tab to HTML using the :meth:`~horizon.tabs.Tab.get_data` - method and the :meth:`~horizon.tabs.Tab.get_template_name` method. + Renders the tab to HTML using the + :meth:`~horizon.tabs.Tab.get_context_data` method and + the :meth:`~horizon.tabs.Tab.get_template_name` method. If :attr:`~horizon.tabs.Tab.preload` is ``False`` and ``force_load`` is not ``True``, or @@ -273,7 +285,7 @@ class Tab(html.HTMLElement): if not self.load: return '' try: - context = self.context_data + context = self.data except exceptions.Http302: raise except: @@ -350,3 +362,76 @@ class Tab(html.HTMLElement): The default behavior is to return ``True`` for all cases. """ return True + + +class TableTab(Tab): + """ + A :class:`~horizon.tabs.Tab` class which knows how to deal with + :class:`~horizon.tables.DataTable` classes rendered inside of it. + + This distinct class is required due to the complexity involved in handling + both dynamic tab loading, dynamic table updating and table actions all + within one view. + + .. attribute:: table_classes + + An iterable containing the :class:`~horizon.tables.DataTable` classes + which this tab will contain. Equivalent to the + :attr:`~horizon.tables.MultiTableView.table_classes` attribute on + :class:`~horizon.tables.MultiTableView`. For each table class you + need to define a corresponding ``get_{{ table_name }}_data`` method + as with :class:`~horizon.tables.MultiTableView`. + """ + table_classes = None + + def __init__(self, tab_group, request): + super(TableTab, self).__init__(tab_group, request) + if not self.table_classes: + class_name = self.__class__.__name__ + raise NotImplementedError("You must define a table_class " + "attribute on %s" % class_name) + # Instantiate our table classes but don't assign data yet + table_instances = [(table._meta.name, + table(request, **tab_group.kwargs)) + for table in self.table_classes] + self._tables = SortedDict(table_instances) + self._table_data_loaded = False + + def load_table_data(self): + """ + Calls the ``get_{{ table_name }}_data`` methods for each table class + and sets the data on the tables. + """ + # We only want the data to be loaded once, so we track if we have... + if not self._table_data_loaded: + for table_name, table in self._tables.items(): + # Fetch the data function. + func_name = "get_%s_data" % table_name + 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 " + "on %s." % (func_name, cls_name)) + # Load the data. + table.data = data_func() + # Mark our data as loaded so we don't run the loaders again. + self._table_data_loaded = True + + def get_context_data(self, request): + """ + Adds a ``{{ table_name }}_table`` item to the context for each table + in the :attr:`~horizon.tabs.TableTab.table_classes` attribute. + + If only one table class is provided, a shortcut ``table`` context + variable is also added containing the single table. + """ + context = {} + # If the data hasn't been manually loaded before now, + # make certain it's loaded before setting the context. + self.load_table_data() + for table_name, table in self._tables.items(): + # If there's only one table class, add a shortcut name as well. + if len(self.table_classes) == 1: + context["table"] = table + context["%s_table" % table_name] = table + return context diff --git a/horizon/tabs/views.py b/horizon/tabs/views.py index b13a925e2..2a4addcea 100644 --- a/horizon/tabs/views.py +++ b/horizon/tabs/views.py @@ -2,6 +2,8 @@ from django import http from django.views import generic from horizon import exceptions +from horizon import tables +from .base import TableTab class TabView(generic.TemplateView): @@ -17,30 +19,45 @@ class TabView(generic.TemplateView): inherits from :class:`horizon.tabs.TabGroup`. """ tab_group_class = None + _tab_group = None def __init__(self): if not self.tab_group_class: raise AttributeError("You must set the tab_group_class attribute " "on %s." % self.__class__.__name__) - def get_tabs(self, request, *args, **kwargs): - return self.tab_group_class(request, **kwargs) + def get_tabs(self, request, **kwargs): + """ Returns the initialized tab group for this view. """ + if self._tab_group is None: + self._tab_group = self.tab_group_class(request, **kwargs) + return self._tab_group - def get(self, request, *args, **kwargs): - context = self.get_context_data(**kwargs) + def get_context_data(self, **kwargs): + """ Adds the ``tab_group`` variable to the context data. """ + context = super(TabView, self).get_context_data(**kwargs) try: - tab_group = self.get_tabs(request, *args, **kwargs) + tab_group = self.get_tabs(self.request, **kwargs) context["tab_group"] = tab_group except: - exceptions.handle(request) + exceptions.handle(self.request) + return context - if request.is_ajax(): + def handle_tabbed_response(self, tab_group, context): + """ + Sends back an AJAX-appropriate response for the tab group if + required, otherwise renders the response as normal. + """ + if self.request.is_ajax(): if tab_group.selected: return http.HttpResponse(tab_group.selected.render()) else: return http.HttpResponse(tab_group.render()) return self.render_to_response(context) + def get(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + return self.handle_tabbed_response(context["tab_group"], context) + def render_to_response(self, *args, **kwargs): response = super(TabView, self).render_to_response(*args, **kwargs) # Because Django's TemplateView uses the TemplateResponse class @@ -50,3 +67,79 @@ class TabView(generic.TemplateView): # of the exception-handling middleware. response.render() return response + + +class TabbedTableView(tables.MultiTableMixin, TabView): + def __init__(self, *args, **kwargs): + super(TabbedTableView, self).__init__(*args, **kwargs) + self.table_classes = [] + self._table_dict = {} + + def load_tabs(self): + """ + Loads the tab group, and compiles the table instances for each + table attached to any :class:`horizon.tabs.TableTab` instances on + the tab group. This step is necessary before processing any + tab or table actions. + """ + tab_group = self.get_tabs(self.request, **self.kwargs) + tabs = tab_group.get_tabs() + for tab in [t for t in tabs if issubclass(t.__class__, TableTab)]: + self.table_classes.extend(tab.table_classes) + for table in tab._tables.values(): + self._table_dict[table._meta.name] = {'table': table, + 'tab': tab} + + def get_tables(self): + """ A no-op on this class. Tables are handled at the tab level. """ + # Override the base class implementation so that the MultiTableMixin + # doesn't freak out. We do the processing at the TableTab level. + return {} + + def handle_table(self, table_dict): + """ + For the given dict containing a ``DataTable`` and a ``TableTab`` + instance, it loads the table data for that tab and calls the + table's :meth:`~horizon.tables.DataTable.maybe_handle` method. The + return value will be the result of ``maybe_handle``. + """ + table = table_dict['table'] + tab = table_dict['tab'] + tab.load_table_data() + table_name = table._meta.name + tab._tables[table_name]._meta.has_more_data = self.has_more_data(table) + handled = tab._tables[table_name].maybe_handle() + return handled + + def get_context_data(self, **kwargs): + """ Adds the ``tab_group`` variable to the context data. """ + context = super(TabbedTableView, self).get_context_data(**kwargs) + context['tab_group'].load_tab_data() + return context + + def get(self, request, *args, **kwargs): + self.load_tabs() + # Gather our table instances. It's important that they're the + # actual instances and not the classes! + table_instances = [t['table'] for t in self._table_dict.values()] + # Early out before any tab or table data is loaded + for table in table_instances: + preempted = table.maybe_preempt() + if preempted: + return preempted + + # If we have an action, determine if it belongs to one of our tables. + # We don't iterate through all of the tables' maybes_handle + # methods; just jump to the one that's got the matching name. + table_name, action, obj_id = tables.DataTable.check_handler(request) + if table_name in self._table_dict: + handled = self.handle_table(self._table_dict[table_name]) + if handled: + return handled + + context = self.get_context_data(**kwargs) + return self.handle_tabbed_response(context["tab_group"], context) + + def post(self, request, *args, **kwargs): + # GET and POST handling are the same + return self.get(request, *args, **kwargs) |