summaryrefslogtreecommitdiff
path: root/horizon/tabs
diff options
context:
space:
mode:
authorGabriel Hurley <gabriel@strikeawe.com>2012-03-24 16:11:19 -0700
committerGabriel Hurley <gabriel@strikeawe.com>2012-03-24 16:14:33 -0700
commite02c442c86a0e961442d2825d451ef0508acbd72 (patch)
treee2bb158ee5ec3afc9ba985568275dd7239d54cc4 /horizon/tabs
parentab71aff23f30360e8463d3a5ca2cec5719994b8c (diff)
downloadhorizon-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__.py4
-rw-r--r--horizon/tabs/base.py107
-rw-r--r--horizon/tabs/views.py107
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)