Skip to content

Commit

Permalink
Merge "Adds support for tabs + tables."
Browse files Browse the repository at this point in the history
  • Loading branch information
Jenkins authored and openstack-gerrit committed Mar 25, 2012
2 parents 14e2dfc + e02c442 commit 477c13a
Show file tree
Hide file tree
Showing 12 changed files with 367 additions and 75 deletions.
8 changes: 8 additions & 0 deletions docs/source/ref/tabs.rst
Expand Up @@ -27,6 +27,11 @@ view of data.
.. autoclass:: Tab
:members:

.. autoclass:: TableTab
:members:



TabView
=======

Expand All @@ -35,3 +40,6 @@ the display of a :class:`~horizon.tabs.TabGroup` class.

.. autoclass:: TabView
:members:

.. autoclass:: TabbedTableView
:members:
Expand Up @@ -179,8 +179,7 @@ def allowed(self, request, instance=None):
class UpdateRow(tables.Row):
ajax = True

@classmethod
def get_data(cls, request, instance_id):
def get_data(self, request, instance_id):
instance = api.server_get(request, instance_id)
flavors = api.flavor_list(request)
keyed_flavors = [(str(flavor.id), flavor) for flavor in flavors]
Expand Down
Expand Up @@ -74,8 +74,7 @@ def allowed(self, request, volume=None):
class UpdateRow(tables.Row):
ajax = True

@classmethod
def get_data(cls, request, volume_id):
def get_data(self, request, volume_id):
volume = api.volume_get(request, volume_id)
return volume

Expand Down
2 changes: 1 addition & 1 deletion horizon/tables/__init__.py
Expand Up @@ -18,4 +18,4 @@
from .actions import (Action, BatchAction, DeleteAction,
LinkAction, FilterAction)
from .base import DataTable, Column, Row
from .views import DataTableView, MultiTableView
from .views import DataTableView, MultiTableView, MultiTableMixin
82 changes: 54 additions & 28 deletions horizon/tables/base.py
Expand Up @@ -306,21 +306,36 @@ class Row(html.HTMLElement):
ajax = False
ajax_action_name = "row_update"

def __init__(self, table, datum):
def __init__(self, table, datum=None):
super(Row, self).__init__()
self.table = table
self.datum = datum
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
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")
if self.datum:
self.load_cells()
else:
self.id = None
self.cells = []

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.
table = self.table
if datum:
self.datum = datum
else:
datum = self.datum
cells = []
for column in table.columns.values():
if column.auto == "multi_select":
Expand All @@ -338,8 +353,18 @@ def __init__(self, table, datum):
cells.append((column.name or column.auto, cell))
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.
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

def __repr__(self):
Expand Down Expand Up @@ -379,14 +404,13 @@ def get_ajax_update_url(self):
"obj_id": self.table.get_object_id(self.datum)})
return "%s?%s" % (table_url, params)

@classmethod
def get_data(cls, request, obj_id):
def get_data(self, request, obj_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.
"""
raise NotImplementedError("You must define a get_data method on %s"
% cls.__name__)
% self.__class__.__name__)


class Cell(html.HTMLElement):
Expand Down Expand Up @@ -756,7 +780,7 @@ def get_absolute_url(self):
For convenience it defaults to the value of
``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]

Expand Down Expand Up @@ -833,7 +857,8 @@ def render_row_actions(self, datum):
context = template.RequestContext(self._meta.request, extra_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
POST data. By default this parses a string formatted as
Expand Down Expand Up @@ -885,12 +910,11 @@ def take_action(self, action_name, obj_id=None, obj_ids=None):
_("Please select a row before taking that action."))
return None

def _check_handler(self):
@classmethod
def check_handler(cls, request):
""" Determine whether the request should be handled by this table. """
request = self._meta.request

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:
table = request.GET["table"]
action = request.GET["action"]
Expand All @@ -904,22 +928,23 @@ def maybe_preempt(self):
Determine whether the request should be handled by a preemptive action
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:
# Handle AJAX row updating.
row_class = self._meta.row_class
if row_class.ajax and row_class.ajax_action_name == action_name:
new_row = self._meta.row_class(self)
if new_row.ajax and new_row.ajax_action_name == action_name:
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
except:
datum = None
error = exceptions.handle(self._meta.request, ignore=True)
if self._meta.request.is_ajax():
error = exceptions.handle(request, ignore=True)
if request.is_ajax():
if not error:
row = row_class(self, datum)
return HttpResponse(row.render())
return HttpResponse(new_row.render())
else:
return HttpResponse(status=error.status_code)

Expand All @@ -938,7 +963,8 @@ def maybe_handle(self):
Determine whether the request should be handled by any action on this
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:
return self.take_action(action_name, obj_id)
return None
Expand Down
49 changes: 27 additions & 22 deletions horizon/tables/views.py
Expand Up @@ -17,20 +17,10 @@
from django.views import generic


class MultiTableView(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.
"""
class MultiTableMixin(object):
""" A generic mixin which provides methods for handling DataTables. """
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._data = {}
self._tables = {}
Expand Down Expand Up @@ -64,18 +54,36 @@ def get_tables(self):
return self._tables

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()
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
return context

def has_more_data(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]._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):
tables = self.get_tables().values()
# Early out before data is loaded
Expand All @@ -84,14 +92,11 @@ def construct_tables(self):
if preempted:
return preempted
# Load data into each table and check for action handlers
data = self._get_data_dict()
for table in tables:
name = table.name
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()
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
Expand Down
4 changes: 2 additions & 2 deletions horizon/tabs/__init__.py
Expand Up @@ -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

0 comments on commit 477c13a

Please sign in to comment.