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
This commit is contained in:
Gabriel Hurley 2012-03-24 16:11:19 -07:00
parent ab71aff23f
commit e02c442c86
12 changed files with 367 additions and 75 deletions

View File

@ -27,6 +27,11 @@ view of data.
.. autoclass:: Tab .. autoclass:: Tab
:members: :members:
.. autoclass:: TableTab
:members:
TabView TabView
======= =======
@ -35,3 +40,6 @@ the display of a :class:`~horizon.tabs.TabGroup` class.
.. autoclass:: TabView .. autoclass:: TabView
:members: :members:
.. autoclass:: TabbedTableView
:members:

View File

@ -175,8 +175,7 @@ class LogLink(tables.LinkAction):
class UpdateRow(tables.Row): class UpdateRow(tables.Row):
ajax = True ajax = True
@classmethod def get_data(self, request, instance_id):
def get_data(cls, request, instance_id):
instance = api.server_get(request, instance_id) instance = api.server_get(request, instance_id)
flavors = api.flavor_list(request) flavors = api.flavor_list(request)
keyed_flavors = [(str(flavor.id), flavor) for flavor in flavors] keyed_flavors = [(str(flavor.id), flavor) for flavor in flavors]

View File

@ -74,8 +74,7 @@ class CreateSnapshot(tables.LinkAction):
class UpdateRow(tables.Row): class UpdateRow(tables.Row):
ajax = True ajax = True
@classmethod def get_data(self, request, volume_id):
def get_data(cls, request, volume_id):
volume = api.volume_get(request, volume_id) volume = api.volume_get(request, volume_id)
return volume return volume

View File

@ -18,4 +18,4 @@
from .actions import (Action, BatchAction, DeleteAction, from .actions import (Action, BatchAction, DeleteAction,
LinkAction, FilterAction) LinkAction, FilterAction)
from .base import DataTable, Column, Row from .base import DataTable, Column, Row
from .views import DataTableView, MultiTableView from .views import DataTableView, MultiTableView, MultiTableMixin

View File

@ -306,21 +306,36 @@ class Row(html.HTMLElement):
ajax = False ajax = False
ajax_action_name = "row_update" ajax_action_name = "row_update"
def __init__(self, table, datum): def __init__(self, table, datum=None):
super(Row, self).__init__() super(Row, self).__init__()
self.table = table self.table = table
self.datum = datum self.datum = datum
id_vals = {"table": self.table.name, if self.datum:
"sep": STRING_SEPARATOR, self.load_cells()
"id": table.get_object_id(datum)} else:
self.id = "%(table)s%(sep)srow%(sep)s%(id)s" % id_vals self.id = None
if self.ajax: self.cells = []
interval = settings.HORIZON_CONFIG.get('ajax_poll_interval', 2500)
self.attrs['data-update-interval'] = interval
self.attrs['data-update-url'] = self.get_ajax_update_url()
self.classes.append("ajax-update")
def load_cells(self, datum=None):
"""
Load the row's data (either provided at initialization or as an
argument to this function), initiailize all the cells contained
by this row, and set the appropriate row properties which require
the row's data to be determined.
This function is called automatically by
:meth:`~horizon.tables.Row.__init__` if the ``datum`` argument is
provided. However, by not providing the data during initialization
this function allows for the possibility of a two-step loading
pattern when you need a row instance but don't yet have the data
available.
"""
# Compile all the cells on instantiation. # Compile all the cells on instantiation.
table = self.table
if datum:
self.datum = datum
else:
datum = self.datum
cells = [] cells = []
for column in table.columns.values(): for column in table.columns.values():
if column.auto == "multi_select": if column.auto == "multi_select":
@ -338,8 +353,18 @@ class Row(html.HTMLElement):
cells.append((column.name or column.auto, cell)) cells.append((column.name or column.auto, cell))
self.cells = SortedDict(cells) self.cells = SortedDict(cells)
if self.ajax:
interval = settings.HORIZON_CONFIG.get('ajax_poll_interval', 2500)
self.attrs['data-update-interval'] = interval
self.attrs['data-update-url'] = self.get_ajax_update_url()
self.classes.append("ajax-update")
# Add the row's status class and id to the attributes to be rendered. # Add the row's status class and id to the attributes to be rendered.
self.classes.append(self.status_class) self.classes.append(self.status_class)
id_vals = {"table": self.table.name,
"sep": STRING_SEPARATOR,
"id": table.get_object_id(datum)}
self.id = "%(table)s%(sep)srow%(sep)s%(id)s" % id_vals
self.attrs['id'] = self.id self.attrs['id'] = self.id
def __repr__(self): def __repr__(self):
@ -379,14 +404,13 @@ class Row(html.HTMLElement):
"obj_id": self.table.get_object_id(self.datum)}) "obj_id": self.table.get_object_id(self.datum)})
return "%s?%s" % (table_url, params) return "%s?%s" % (table_url, params)
@classmethod def get_data(self, request, obj_id):
def get_data(cls, request, obj_id):
""" """
Fetches the updated data for the row based on the object id Fetches the updated data for the row based on the object id
passed in. Must be implemented by a subclass to allow AJAX updating. passed in. Must be implemented by a subclass to allow AJAX updating.
""" """
raise NotImplementedError("You must define a get_data method on %s" raise NotImplementedError("You must define a get_data method on %s"
% cls.__name__) % self.__class__.__name__)
class Cell(html.HTMLElement): class Cell(html.HTMLElement):
@ -756,7 +780,7 @@ class DataTable(object):
For convenience it defaults to the value of For convenience it defaults to the value of
``request.get_full_path()`` with any query string stripped off, ``request.get_full_path()`` with any query string stripped off,
e.g. the path at which the table was requested. e.g. the path at which the table was requested.
""" """
return self._meta.request.get_full_path().partition('?')[0] return self._meta.request.get_full_path().partition('?')[0]
@ -833,7 +857,8 @@ class DataTable(object):
context = template.RequestContext(self._meta.request, extra_context) context = template.RequestContext(self._meta.request, extra_context)
return row_actions_template.render(context) return row_actions_template.render(context)
def parse_action(self, action_string): @staticmethod
def parse_action(action_string):
""" """
Parses the ``action`` parameter (a string) sent back with the Parses the ``action`` parameter (a string) sent back with the
POST data. By default this parses a string formatted as POST data. By default this parses a string formatted as
@ -885,12 +910,11 @@ class DataTable(object):
_("Please select a row before taking that action.")) _("Please select a row before taking that action."))
return None return None
def _check_handler(self): @classmethod
def check_handler(cls, request):
""" Determine whether the request should be handled by this table. """ """ Determine whether the request should be handled by this table. """
request = self._meta.request
if request.method == "POST" and "action" in request.POST: if request.method == "POST" and "action" in request.POST:
table, action, obj_id = self.parse_action(request.POST["action"]) table, action, obj_id = cls.parse_action(request.POST["action"])
elif "table" in request.GET and "action" in request.GET: elif "table" in request.GET and "action" in request.GET:
table = request.GET["table"] table = request.GET["table"]
action = request.GET["action"] action = request.GET["action"]
@ -904,22 +928,23 @@ class DataTable(object):
Determine whether the request should be handled by a preemptive action Determine whether the request should be handled by a preemptive action
on this table or by an AJAX row update before loading any data. on this table or by an AJAX row update before loading any data.
""" """
table_name, action_name, obj_id = self._check_handler() request = self._meta.request
table_name, action_name, obj_id = self.check_handler(request)
if table_name == self.name: if table_name == self.name:
# Handle AJAX row updating. # Handle AJAX row updating.
row_class = self._meta.row_class new_row = self._meta.row_class(self)
if row_class.ajax and row_class.ajax_action_name == action_name: if new_row.ajax and new_row.ajax_action_name == action_name:
try: try:
datum = row_class.get_data(self._meta.request, obj_id) datum = new_row.get_data(request, obj_id)
new_row.load_cells(datum)
error = False error = False
except: except:
datum = None datum = None
error = exceptions.handle(self._meta.request, ignore=True) error = exceptions.handle(request, ignore=True)
if self._meta.request.is_ajax(): if request.is_ajax():
if not error: if not error:
row = row_class(self, datum) return HttpResponse(new_row.render())
return HttpResponse(row.render())
else: else:
return HttpResponse(status=error.status_code) return HttpResponse(status=error.status_code)
@ -938,7 +963,8 @@ class DataTable(object):
Determine whether the request should be handled by any action on this Determine whether the request should be handled by any action on this
table after data has been loaded. table after data has been loaded.
""" """
table_name, action_name, obj_id = self._check_handler() request = self._meta.request
table_name, action_name, obj_id = self.check_handler(request)
if table_name == self.name and action_name: if table_name == self.name and action_name:
return self.take_action(action_name, obj_id) return self.take_action(action_name, obj_id)
return None return None

View File

@ -17,20 +17,10 @@
from django.views import generic from django.views import generic
class MultiTableView(generic.TemplateView): class MultiTableMixin(object):
""" """ A generic mixin which provides methods for handling DataTables. """
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.
"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(MultiTableView, self).__init__(*args, **kwargs) super(MultiTableMixin, self).__init__(*args, **kwargs)
self.table_classes = getattr(self, "table_classes", []) self.table_classes = getattr(self, "table_classes", [])
self._data = {} self._data = {}
self._tables = {} self._tables = {}
@ -64,18 +54,36 @@ class MultiTableView(generic.TemplateView):
return self._tables return self._tables
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super(MultiTableView, self).get_context_data(**kwargs) context = super(MultiTableMixin, self).get_context_data(**kwargs)
tables = self.get_tables() tables = self.get_tables()
for name, table in tables.items(): for name, table in tables.items():
if table.data is None:
raise AttributeError('%s has no data associated with it.'
% table.__class__.__name__)
context["%s_table" % name] = table context["%s_table" % name] = table
return context return context
def has_more_data(self, table): def has_more_data(self, table):
return False 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]._meta.has_more_data = self.has_more_data(table)
handled = self._tables[name].maybe_handle()
return handled
class MultiTableView(MultiTableMixin, generic.TemplateView):
"""
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.
"""
def construct_tables(self): def construct_tables(self):
tables = self.get_tables().values() tables = self.get_tables().values()
# Early out before data is loaded # Early out before data is loaded
@ -84,14 +92,11 @@ class MultiTableView(generic.TemplateView):
if preempted: if preempted:
return preempted return preempted
# Load data into each table and check for action handlers # Load data into each table and check for action handlers
data = self._get_data_dict()
for table in tables: for table in tables:
name = table.name handled = self.handle_table(table)
self._tables[name].data = data[table._meta.name]
self._tables[name]._meta.has_more_data = self.has_more_data(table)
handled = self._tables[name].maybe_handle()
if handled: if handled:
return handled return handled
# If we didn't already return a response, returning None continues # If we didn't already return a response, returning None continues
# with the view as normal. # with the view as normal.
return None return None

View File

@ -14,5 +14,5 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from .base import TabGroup, Tab from .base import TabGroup, Tab, TableTab
from .views import TabView from .views import TabView, TabbedTableView

View File

@ -95,14 +95,18 @@ class TabGroup(html.HTMLElement):
self._tabs = SortedDict(tab_instances) self._tabs = SortedDict(tab_instances)
if not self._set_active_tab(): if not self._set_active_tab():
self.tabs_not_available() 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): def __repr__(self):
return "<%s: %s>" % (self.__class__.__name__, self.slug) 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): def get_id(self):
""" """
Returns the id for this tab group. Defaults to the value of the tab 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 tab
return None return None
def get_loaded_tabs(self):
return filter(lambda t: self.get_tab(t.slug), self._tabs.values())
def get_selected_tab(self): def get_selected_tab(self):
""" Returns the tab specific by the GET request parameter. """ 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 return load_preloaded and self._allowed and self._enabled
@property @property
def context_data(self): def data(self):
if not getattr(self, "_context_data", None): if not getattr(self, "_data", None):
self._context_data = self.get_context_data(self.request) self._data = self.get_context_data(self.request)
return self._context_data return self._data
@property
def data_loaded(self):
return getattr(self, "_data", None) is not None
def render(self): def render(self):
""" """
Renders the tab to HTML using the :meth:`~horizon.tabs.Tab.get_data` Renders the tab to HTML using the
method and the :meth:`~horizon.tabs.Tab.get_template_name` method. :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`` If :attr:`~horizon.tabs.Tab.preload` is ``False`` and ``force_load``
is not ``True``, or is not ``True``, or
@ -273,7 +285,7 @@ class Tab(html.HTMLElement):
if not self.load: if not self.load:
return '' return ''
try: try:
context = self.context_data context = self.data
except exceptions.Http302: except exceptions.Http302:
raise raise
except: except:
@ -350,3 +362,76 @@ class Tab(html.HTMLElement):
The default behavior is to return ``True`` for all cases. The default behavior is to return ``True`` for all cases.
""" """
return True 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

View File

@ -2,6 +2,8 @@ from django import http
from django.views import generic from django.views import generic
from horizon import exceptions from horizon import exceptions
from horizon import tables
from .base import TableTab
class TabView(generic.TemplateView): class TabView(generic.TemplateView):
@ -17,30 +19,45 @@ class TabView(generic.TemplateView):
inherits from :class:`horizon.tabs.TabGroup`. inherits from :class:`horizon.tabs.TabGroup`.
""" """
tab_group_class = None tab_group_class = None
_tab_group = None
def __init__(self): def __init__(self):
if not self.tab_group_class: if not self.tab_group_class:
raise AttributeError("You must set the tab_group_class attribute " raise AttributeError("You must set the tab_group_class attribute "
"on %s." % self.__class__.__name__) "on %s." % self.__class__.__name__)
def get_tabs(self, request, *args, **kwargs): def get_tabs(self, request, **kwargs):
return self.tab_group_class(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): def get_context_data(self, **kwargs):
context = self.get_context_data(**kwargs) """ Adds the ``tab_group`` variable to the context data. """
context = super(TabView, self).get_context_data(**kwargs)
try: try:
tab_group = self.get_tabs(request, *args, **kwargs) tab_group = self.get_tabs(self.request, **kwargs)
context["tab_group"] = tab_group context["tab_group"] = tab_group
except: 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: if tab_group.selected:
return http.HttpResponse(tab_group.selected.render()) return http.HttpResponse(tab_group.selected.render())
else: else:
return http.HttpResponse(tab_group.render()) return http.HttpResponse(tab_group.render())
return self.render_to_response(context) 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): def render_to_response(self, *args, **kwargs):
response = super(TabView, self).render_to_response(*args, **kwargs) response = super(TabView, self).render_to_response(*args, **kwargs)
# Because Django's TemplateView uses the TemplateResponse class # Because Django's TemplateView uses the TemplateResponse class
@ -50,3 +67,79 @@ class TabView(generic.TemplateView):
# of the exception-handling middleware. # of the exception-handling middleware.
response.render() response.render()
return response 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)

View File

@ -0,0 +1 @@
{{ table.render }}

View File

@ -20,6 +20,8 @@ from django.utils.translation import ugettext_lazy as _
from horizon import tabs as horizon_tabs from horizon import tabs as horizon_tabs
from horizon import test from horizon import test
from .table_tests import MyTable, TEST_DATA
class BaseTestTab(horizon_tabs.Tab): class BaseTestTab(horizon_tabs.Tab):
def get_context_data(self, request): def get_context_data(self, request):
@ -65,6 +67,26 @@ class Group(horizon_tabs.TabGroup):
self._assert_tabs_not_available = True self._assert_tabs_not_available = True
class TabWithTable(horizon_tabs.TableTab):
table_classes = (MyTable,)
name = _("Tab With My Table")
slug = "tab_with_table"
template_name = "horizon/common/_detail_table.html"
def get_my_table_data(self):
return TEST_DATA
class TableTabGroup(horizon_tabs.TabGroup):
slug = "tab_group"
tabs = (TabWithTable,)
class TabWithTableView(horizon_tabs.TabbedTableView):
tab_group_class = TableTabGroup
template_name = "tab_group.html"
class TabTests(test.TestCase): class TabTests(test.TestCase):
def setUp(self): def setUp(self):
super(TabTests, self).setUp() super(TabTests, self).setUp()
@ -190,3 +212,56 @@ class TabTests(test.TestCase):
tab_delayed = tg.get_tab("tab_delayed") tab_delayed = tg.get_tab("tab_delayed")
output = tab_delayed.render() output = tab_delayed.render()
self.assertEqual(output.strip(), tab_delayed.name) self.assertEqual(output.strip(), tab_delayed.name)
def test_table_tabs(self):
tab_group = TableTabGroup(self.request)
tabs = tab_group.get_tabs()
# Only one tab, as expected.
self.assertEqual(len(tabs), 1)
tab = tabs[0]
# Make sure it's the tab we think it is.
self.assertTrue(isinstance(tab, horizon_tabs.TableTab))
# Data should not be loaded yet.
self.assertFalse(tab._table_data_loaded)
table = tab._tables[MyTable.Meta.name]
self.assertTrue(isinstance(table, MyTable))
# Let's make sure the data *really* isn't loaded yet.
self.assertEqual(table.data, None)
# Okay, load the data.
tab.load_table_data()
self.assertTrue(tab._table_data_loaded)
self.assertQuerysetEqual(table.data, ['<FakeObject: object_1>',
'<FakeObject: object_2>',
'<FakeObject: object_3>'])
context = tab.get_context_data(self.request)
# Make sure our table is loaded into the context correctly
self.assertEqual(context['my_table_table'], table)
# Since we only had one table we should get the shortcut name too.
self.assertEqual(context['table'], table)
def test_tabbed_table_view(self):
view = TabWithTableView.as_view()
# Be sure we get back a rendered table containing data for a GET
req = self.factory.get("/")
res = view(req)
self.assertContains(res, "<table", 1)
self.assertContains(res, "Displaying 3 items", 1)
# AJAX response to GET for row update
params = {"table": "my_table", "action": "row_update", "obj_id": "1"}
req = self.factory.get('/', params,
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
res = view(req)
self.assertEqual(res.status_code, 200)
# Make sure we got back a row but not a table or body
self.assertContains(res, "<tr", 1)
self.assertContains(res, "<table", 0)
self.assertContains(res, "<body", 0)
# Response to POST for table action
action_string = "my_table__toggle__2"
req = self.factory.post('/', {'action': action_string})
res = view(req)
self.assertEqual(res.status_code, 302)
self.assertEqual(res["location"], "/")

View File

@ -0,0 +1 @@
{{ tab_group.render }}