Index: appengine/monorail/framework/table_view_helpers.py |
diff --git a/appengine/monorail/framework/table_view_helpers.py b/appengine/monorail/framework/table_view_helpers.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..1ca8098cce4ab00ea8421d27b2a056c93f174c0a |
--- /dev/null |
+++ b/appengine/monorail/framework/table_view_helpers.py |
@@ -0,0 +1,627 @@ |
+# Copyright 2016 The Chromium Authors. All rights reserved. |
+# Use of this source code is govered by a BSD-style |
+# license that can be found in the LICENSE file or at |
+# https://developers.google.com/open-source/licenses/bsd |
+ |
+"""Classes and functions for displaying lists of project artifacts. |
+ |
+This file exports classes TableRow and TableCell that help |
+represent HTML table rows and cells. These classes make rendering |
+HTML tables that list project artifacts much easier to do with EZT. |
+""" |
+ |
+import collections |
+import logging |
+ |
+from third_party import ezt |
+ |
+from framework import framework_constants |
+from framework import template_helpers |
+from proto import tracker_pb2 |
+from tracker import tracker_bizobj |
+ |
+ |
+def ComputeUnshownColumns(results, shown_columns, config, built_in_cols): |
+ """Return a list of unshown columns that the user could add. |
+ |
+ Args: |
+ results: list of search result PBs. Each must have labels. |
+ shown_columns: list of column names to be used in results table. |
+ config: harmonized config for the issue search, including all |
+ well known labels and custom fields. |
+ built_in_cols: list of other column names that are built into the tool. |
+ E.g., star count, or creation date. |
+ |
+ Returns: |
+ List of column names to append to the "..." menu. |
+ """ |
+ unshown_set = set() # lowercases column names |
+ unshown_list = [] # original-case column names |
+ shown_set = {col.lower() for col in shown_columns} |
+ labels_already_seen = set() # whole labels, original case |
+ |
+ def _MaybeAddLabel(label_name): |
+ """Add the key part of the given label if needed.""" |
+ if label_name.lower() in labels_already_seen: |
+ return |
+ labels_already_seen.add(label_name.lower()) |
+ if '-' in label_name: |
+ col, _value = label_name.split('-', 1) |
+ _MaybeAddCol(col) |
+ |
+ def _MaybeAddCol(col): |
+ if col.lower() not in shown_set and col.lower() not in unshown_set: |
+ unshown_list.append(col) |
+ unshown_set.add(col.lower()) |
+ |
+ # The user can always add any of the default columns. |
+ for col in config.default_col_spec.split(): |
+ _MaybeAddCol(col) |
+ |
+ # The user can always add any of the built-in columns. |
+ for col in built_in_cols: |
+ _MaybeAddCol(col) |
+ |
+ # The user can add a column for any well-known labels |
+ for wkl in config.well_known_labels: |
+ _MaybeAddLabel(wkl.label) |
+ |
+ # The user can add a column for any custom field |
+ field_ids_alread_seen = set() |
+ for fd in config.field_defs: |
+ field_lower = fd.field_name.lower() |
+ field_ids_alread_seen.add(fd.field_id) |
+ if field_lower not in shown_set and field_lower not in unshown_set: |
+ unshown_list.append(fd.field_name) |
+ unshown_set.add(field_lower) |
+ |
+ # The user can add a column for any key-value label or field in the results. |
+ for r in results: |
+ for label_name in tracker_bizobj.GetLabels(r): |
+ _MaybeAddLabel(label_name) |
+ for field_value in r.field_values: |
+ if field_value.field_id not in field_ids_alread_seen: |
+ field_ids_alread_seen.add(field_value.field_id) |
+ fd = tracker_bizobj.FindFieldDefByID(field_value.field_id, config) |
+ if fd: # could be None for a foreign field, which we don't display. |
+ field_lower = fd.field_name.lower() |
+ if field_lower not in shown_set and field_lower not in unshown_set: |
+ unshown_list.append(fd.field_name) |
+ unshown_set.add(field_lower) |
+ |
+ return sorted(unshown_list) |
+ |
+ |
+def ExtractUniqueValues(columns, artifact_list, users_by_id, config): |
+ """Build a nested list of unique values so the user can auto-filter. |
+ |
+ Args: |
+ columns: a list of lowercase column name strings, which may contain |
+ combined columns like "priority/pri". |
+ artifact_list: a list of artifacts in the complete set of search results. |
+ users_by_id: dict mapping user_ids to UserViews. |
+ config: ProjectIssueConfig PB for the current project. |
+ |
+ Returns: |
+ [EZTItem(col1, colname1, [val11, val12,...]), ...] |
+ A list of EZTItems, each of which has a col_index, column_name, |
+ and a list of unique values that appear in that column. |
+ """ |
+ column_values = {col_name: {} for col_name in columns} |
+ |
+ # For each combined column "a/b/c", add entries that point from "a" back |
+ # to "a/b/c", from "b" back to "a/b/c", and from "c" back to "a/b/c". |
+ combined_column_parts = collections.defaultdict(list) |
+ for col in columns: |
+ if '/' in col: |
+ for col_part in col.split('/'): |
+ combined_column_parts[col_part].append(col) |
+ |
+ unique_labels = set() |
+ for art in artifact_list: |
+ unique_labels.update(tracker_bizobj.GetLabels(art)) |
+ |
+ for label in unique_labels: |
+ if '-' in label: |
+ col, val = label.split('-', 1) |
+ col = col.lower() |
+ if col in column_values: |
+ column_values[col][val.lower()] = val |
+ if col in combined_column_parts: |
+ for combined_column in combined_column_parts[col]: |
+ column_values[combined_column][val.lower()] = val |
+ else: |
+ if 'summary' in column_values: |
+ column_values['summary'][label.lower()] = label |
+ |
+ # TODO(jrobbins): Consider refacting some of this to tracker_bizobj |
+ # or a new builtins.py to reduce duplication. |
+ if 'reporter' in column_values: |
+ for art in artifact_list: |
+ reporter_id = art.reporter_id |
+ if reporter_id and reporter_id in users_by_id: |
+ reporter_username = users_by_id[reporter_id].display_name |
+ column_values['reporter'][reporter_username] = reporter_username |
+ |
+ if 'owner' in column_values: |
+ for art in artifact_list: |
+ owner_id = tracker_bizobj.GetOwnerId(art) |
+ if owner_id and owner_id in users_by_id: |
+ owner_username = users_by_id[owner_id].display_name |
+ column_values['owner'][owner_username] = owner_username |
+ |
+ if 'cc' in column_values: |
+ for art in artifact_list: |
+ cc_ids = tracker_bizobj.GetCcIds(art) |
+ for cc_id in cc_ids: |
+ if cc_id and cc_id in users_by_id: |
+ cc_username = users_by_id[cc_id].display_name |
+ column_values['cc'][cc_username] = cc_username |
+ |
+ if 'component' in column_values: |
+ for art in artifact_list: |
+ all_comp_ids = list(art.component_ids) + list(art.derived_component_ids) |
+ for component_id in all_comp_ids: |
+ cd = tracker_bizobj.FindComponentDefByID(component_id, config) |
+ if cd: |
+ column_values['component'][cd.path] = cd.path |
+ |
+ if 'stars' in column_values: |
+ for art in artifact_list: |
+ star_count = art.star_count |
+ column_values['stars'][star_count] = star_count |
+ |
+ if 'status' in column_values: |
+ for art in artifact_list: |
+ status = tracker_bizobj.GetStatus(art) |
+ if status: |
+ column_values['status'][status.lower()] = status |
+ |
+ # TODO(jrobbins): merged into, blocked on, and blocking. And, the ability |
+ # to parse a user query on those fields and do a SQL search. |
+ |
+ if 'attachments' in column_values: |
+ for art in artifact_list: |
+ attachment_count = art.attachment_count |
+ column_values['attachments'][attachment_count] = attachment_count |
+ |
+ # Add all custom field values if the custom field name is a shown column. |
+ field_id_to_col = {} |
+ for art in artifact_list: |
+ for fv in art.field_values: |
+ field_col, field_type = field_id_to_col.get(fv.field_id, (None, None)) |
+ if field_col == 'NOT_SHOWN': |
+ continue |
+ if field_col is None: |
+ fd = tracker_bizobj.FindFieldDefByID(fv.field_id, config) |
+ if not fd: |
+ field_id_to_col[fv.field_id] = 'NOT_SHOWN', None |
+ continue |
+ field_col = fd.field_name.lower() |
+ field_type = fd.field_type |
+ if field_col not in column_values: |
+ field_id_to_col[fv.field_id] = 'NOT_SHOWN', None |
+ continue |
+ field_id_to_col[fv.field_id] = field_col, field_type |
+ |
+ if field_type == tracker_pb2.FieldTypes.ENUM_TYPE: |
+ continue # Already handled by label parsing |
+ elif field_type == tracker_pb2.FieldTypes.INT_TYPE: |
+ val = fv.int_value |
+ elif field_type == tracker_pb2.FieldTypes.STR_TYPE: |
+ val = fv.str_value |
+ elif field_type == tracker_pb2.FieldTypes.USER_TYPE: |
+ user = users_by_id.get(fv.user_id) |
+ val = user.email if user else framework_constants.NO_USER_NAME |
+ elif field_type == tracker_pb2.FieldTypes.DATE_TYPE: |
+ val = fv.int_value # TODO(jrobbins): convert to date |
+ elif field_type == tracker_pb2.FieldTypes.BOOL_TYPE: |
+ val = 'Yes' if fv.int_value else 'No' |
+ |
+ column_values[field_col][val] = val |
+ |
+ # TODO(jrobbins): make the capitalization of well-known unique label and |
+ # status values match the way it is written in the issue config. |
+ |
+ # Return EZTItems for each column in left-to-right display order. |
+ result = [] |
+ for i, col_name in enumerate(columns): |
+ # TODO(jrobbins): sort each set of column values top-to-bottom, by the |
+ # order specified in the project artifact config. For now, just sort |
+ # lexicographically to make expected output defined. |
+ sorted_col_values = sorted(column_values[col_name].values()) |
+ result.append(template_helpers.EZTItem( |
+ col_index=i, column_name=col_name, filter_values=sorted_col_values)) |
+ |
+ return result |
+ |
+ |
+def MakeTableData( |
+ visible_results, logged_in_user_id, starred_items, |
+ lower_columns, lower_group_by, users_by_id, cell_factories, |
+ id_accessor, related_issues, config): |
+ """Return a list of list row objects for display by EZT. |
+ |
+ Args: |
+ visible_results: list of artifacts to display on one pagination page. |
+ logged_in_user_id: user ID of the signed in user, or None. |
+ starred_items: list of IDs/names of items in the current project |
+ that the signed in user has starred. |
+ lower_columns: list of column names to display, all lowercase. These can |
+ be combined column names, e.g., 'priority/pri'. |
+ lower_group_by: list of column names that define row groups, all lowercase. |
+ users_by_id: dict mapping user IDs to UserViews. |
+ cell_factories: dict of functions that each create TableCell objects. |
+ id_accessor: function that maps from an artifact to the ID/name that might |
+ be in the starred items list. |
+ related_issues: dict {issue_id: issue} of pre-fetched related issues. |
+ config: ProjectIssueConfig PB for the current project. |
+ |
+ Returns: |
+ A list of TableRow objects, one for each visible result. |
+ """ |
+ table_data = [] |
+ |
+ group_cell_factories = [ |
+ ChooseCellFactory(group.strip('-'), cell_factories, config) |
+ for group in lower_group_by] |
+ |
+ # Make a list of cell factories, one for each column. |
+ factories_to_use = [ |
+ ChooseCellFactory(col, cell_factories, config) for col in lower_columns] |
+ |
+ current_group = None |
+ for idx, art in enumerate(visible_results): |
+ owner_is_me = ezt.boolean( |
+ logged_in_user_id and |
+ tracker_bizobj.GetOwnerId(art) == logged_in_user_id) |
+ row = MakeRowData( |
+ art, lower_columns, owner_is_me, users_by_id, factories_to_use, |
+ related_issues, config) |
+ row.starred = ezt.boolean(id_accessor(art) in starred_items) |
+ row.idx = idx # EZT does not have loop counters, so add idx. |
+ table_data.append(row) |
+ row.group = None |
+ |
+ # Also include group information for the first row in each group. |
+ # TODO(jrobbins): This seems like more overhead than we need for the |
+ # common case where no new group heading row is to be inserted. |
+ group = MakeRowData( |
+ art, [group_name.strip('-') for group_name in lower_group_by], |
+ owner_is_me, users_by_id, group_cell_factories, related_issues, |
+ config) |
+ for cell, group_name in zip(group.cells, lower_group_by): |
+ cell.group_name = group_name |
+ if group == current_group: |
+ current_group.rows_in_group += 1 |
+ else: |
+ row.group = group |
+ current_group = group |
+ current_group.rows_in_group = 1 |
+ |
+ return table_data |
+ |
+ |
+def MakeRowData( |
+ art, columns, owner_is_me, users_by_id, cell_factory_list, |
+ related_issues, config): |
+ """Make a TableRow for use by EZT when rendering HTML table of results. |
+ |
+ Args: |
+ art: a project artifact PB |
+ columns: list of lower-case column names |
+ owner_is_me: boolean indicating that the logged in user is the owner |
+ of the current artifact |
+ users_by_id: dictionary {user_id: UserView} with each UserView having |
+ a "display_name" member. |
+ cell_factory_list: list of functions that each create TableCell |
+ objects for a given column. |
+ related_issues: dict {issue_id: issue} of pre-fetched related issues. |
+ config: ProjectIssueConfig PB for the current project. |
+ |
+ Returns: |
+ A TableRow object for use by EZT to render a table of results. |
+ """ |
+ ordered_row_data = [] |
+ non_col_labels = [] |
+ label_values = collections.defaultdict(list) |
+ |
+ flattened_columns = set() |
+ for col in columns: |
+ if '/' in col: |
+ flattened_columns.update(col.split('/')) |
+ else: |
+ flattened_columns.add(col) |
+ |
+ # Group all "Key-Value" labels by key, and separate the "OneWord" labels. |
+ _AccumulateLabelValues( |
+ art.labels, flattened_columns, label_values, non_col_labels) |
+ |
+ _AccumulateLabelValues( |
+ art.derived_labels, flattened_columns, label_values, |
+ non_col_labels, is_derived=True) |
+ |
+ # Build up a list of TableCell objects for this row. |
+ for i, col in enumerate(columns): |
+ factory = cell_factory_list[i] |
+ new_cell = factory( |
+ art, col, users_by_id, non_col_labels, label_values, related_issues, |
+ config) |
+ new_cell.col_index = i |
+ ordered_row_data.append(new_cell) |
+ |
+ return TableRow(ordered_row_data, owner_is_me) |
+ |
+ |
+def _AccumulateLabelValues( |
+ labels, columns, label_values, non_col_labels, is_derived=False): |
+ """Parse OneWord and Key-Value labels for display in a list page. |
+ |
+ Args: |
+ labels: a list of label strings. |
+ columns: a list of column names. |
+ label_values: mutable dictionary {key: [value, ...]} of label values |
+ seen so far. |
+ non_col_labels: mutable list of OneWord labels seen so far. |
+ is_derived: true if these labels were derived via rules. |
+ |
+ Returns: |
+ Nothing. But, the given label_values dictionary will grow to hold |
+ the values of the key-value labels passed in, and the non_col_labels |
+ list will grow to hold the OneWord labels passed in. These are shown |
+ in label columns, and in the summary column, respectively |
+ """ |
+ for label_name in labels: |
+ if '-' in label_name: |
+ parts = label_name.split('-') |
+ for pivot in range(1, len(parts)): |
+ column_name = '-'.join(parts[:pivot]) |
+ value = '-'.join(parts[pivot:]) |
+ column_name = column_name.lower() |
+ if column_name in columns: |
+ label_values[column_name].append((value, is_derived)) |
+ else: |
+ non_col_labels.append((label_name, is_derived)) |
+ |
+ |
+class TableRow(object): |
+ """A tiny auxiliary class to represent a row in an HTML table.""" |
+ |
+ def __init__(self, cells, owner_is_me): |
+ """Initialize the table row with the given data.""" |
+ self.cells = cells |
+ self.owner_is_me = ezt.boolean(owner_is_me) # Shows tiny ">" on my issues. |
+ # Used by MakeTableData for layout. |
+ self.idx = None |
+ self.group = None |
+ self.rows_in_group = None |
+ self.starred = None |
+ |
+ def __cmp__(self, other): |
+ """A row is == if each cell is == to the cells in the other row.""" |
+ return cmp(self.cells, other.cells) if other else -1 |
+ |
+ def DebugString(self): |
+ """Return a string that is useful for on-page debugging.""" |
+ return 'TR(%s)' % self.cells |
+ |
+ |
+# TODO(jrobbins): also add unsortable... or change this to a list of operations |
+# that can be done. |
+CELL_TYPE_ID = 'ID' |
+CELL_TYPE_SUMMARY = 'summary' |
+CELL_TYPE_ATTR = 'attr' |
+CELL_TYPE_UNFILTERABLE = 'unfilterable' |
+ |
+ |
+class TableCell(object): |
+ """Helper class to represent a table cell when rendering using EZT.""" |
+ |
+ # Should instances of this class be rendered with whitespace:nowrap? |
+ # Subclasses can override this constant, e.g., issuelist TableCellOwner. |
+ NOWRAP = ezt.boolean(False) |
+ |
+ def __init__(self, cell_type, explicit_values, |
+ derived_values=None, non_column_labels=None, align='', |
+ sort_values=True): |
+ """Store all the given data for later access by EZT.""" |
+ self.type = cell_type |
+ self.align = align |
+ self.col_index = 0 # Is set afterward |
+ self.values = [] |
+ if non_column_labels: |
+ self.non_column_labels = [ |
+ template_helpers.EZTItem(value=v, is_derived=ezt.boolean(d)) |
+ for v, d in non_column_labels] |
+ else: |
+ self.non_column_labels = [] |
+ |
+ for v in (sorted(explicit_values) if sort_values else explicit_values): |
+ self.values.append(CellItem(v)) |
+ |
+ if derived_values: |
+ for v in (sorted(derived_values) if sort_values else derived_values): |
+ self.values.append(CellItem(v, is_derived=True)) |
+ |
+ def __cmp__(self, other): |
+ """A cell is == if each value is == to the values in the other cells.""" |
+ return cmp(self.values, other.values) if other else -1 |
+ |
+ def DebugString(self): |
+ return 'TC(%r, %r, %r)' % ( |
+ self.type, |
+ [v.DebugString() for v in self.values], |
+ self.non_column_labels) |
+ |
+ |
+def CompositeTableCell(columns_to_combine, cell_factories): |
+ """Cell factory that combines multiple cells in a combined column.""" |
+ |
+ class FactoryClass(TableCell): |
+ def __init__(self, art, _col, users_by_id, |
+ non_col_labels, label_values, related_issues, config): |
+ TableCell.__init__(self, CELL_TYPE_UNFILTERABLE, []) |
+ |
+ for sub_col in columns_to_combine: |
+ sub_factory = ChooseCellFactory(sub_col, cell_factories, config) |
+ sub_cell = sub_factory( |
+ art, sub_col, users_by_id, non_col_labels, label_values, |
+ related_issues, config) |
+ self.non_column_labels.extend(sub_cell.non_column_labels) |
+ self.values.extend(sub_cell.values) |
+ |
+ return FactoryClass |
+ |
+ |
+class CellItem(object): |
+ """Simple class to display one part of a table cell's value, with style.""" |
+ |
+ def __init__(self, item, is_derived=False): |
+ self.item = item |
+ self.is_derived = ezt.boolean(is_derived) |
+ |
+ def __cmp__(self, other): |
+ return cmp(self.item, other.item) if other else -1 |
+ |
+ def DebugString(self): |
+ if self.is_derived: |
+ return 'CI(derived: %r)' % self.item |
+ else: |
+ return 'CI(%r)' % self.item |
+ |
+ |
+class TableCellKeyLabels(TableCell): |
+ """TableCell subclass specifically for showing user-defined label values.""" |
+ |
+ def __init__( |
+ self, _art, col, _users_by_id, _non_col_labels, |
+ label_values, _related_issues, _config): |
+ label_value_pairs = label_values.get(col, []) |
+ explicit_values = [value for value, is_derived in label_value_pairs |
+ if not is_derived] |
+ derived_values = [value for value, is_derived in label_value_pairs |
+ if is_derived] |
+ TableCell.__init__(self, CELL_TYPE_ATTR, explicit_values, |
+ derived_values=derived_values) |
+ |
+ |
+class TableCellProject(TableCell): |
+ """TableCell subclass for showing an artifact's project name.""" |
+ |
+ # pylint: disable=unused-argument |
+ def __init__( |
+ self, art, col, users_by_id, non_col_labels, label_values, |
+ _related_issues, _config): |
+ TableCell.__init__( |
+ self, CELL_TYPE_ATTR, [art.project_name]) |
+ |
+ |
+class TableCellStars(TableCell): |
+ """TableCell subclass for showing an artifact's star count.""" |
+ |
+ # pylint: disable=unused-argument |
+ def __init__( |
+ self, art, col, users_by_id, non_col_labels, label_values, |
+ _related_issues, _config): |
+ TableCell.__init__( |
+ self, CELL_TYPE_ATTR, [art.star_count], align='right') |
+ |
+ |
+class TableCellSummary(TableCell): |
+ """TableCell subclass for showing an artifact's summary.""" |
+ |
+ # pylint: disable=unused-argument |
+ def __init__( |
+ self, art, col, users_by_id, non_col_labels, label_values, |
+ _related_issues, _config): |
+ TableCell.__init__( |
+ self, CELL_TYPE_SUMMARY, [art.summary], |
+ non_column_labels=non_col_labels) |
+ |
+ |
+class TableCellCustom(TableCell): |
+ """Abstract TableCell subclass specifically for showing custom fields.""" |
+ |
+ def __init__( |
+ self, art, col, users_by_id, _non_col_labels, |
+ _label_values, _related_issues, config): |
+ explicit_values = [] |
+ derived_values = [] |
+ for fv in art.field_values: |
+ # TODO(jrobbins): for cross-project search this could be a list. |
+ fd = tracker_bizobj.FindFieldDefByID(fv.field_id, config) |
+ if fd.field_name.lower() == col: |
+ val = self.ExtractValue(fv, users_by_id) |
+ if fv.derived: |
+ derived_values.append(val) |
+ else: |
+ explicit_values.append(val) |
+ |
+ TableCell.__init__(self, CELL_TYPE_ATTR, explicit_values, |
+ derived_values=derived_values) |
+ |
+ def ExtractValue(self, fv, _users_by_id): |
+ return 'field-id-%d-not-implemented-yet' % fv.field_id |
+ |
+ |
+class TableCellCustomInt(TableCellCustom): |
+ """TableCell subclass specifically for showing custom int fields.""" |
+ |
+ def ExtractValue(self, fv, _users_by_id): |
+ return fv.int_value |
+ |
+ |
+class TableCellCustomStr(TableCellCustom): |
+ """TableCell subclass specifically for showing custom str fields.""" |
+ |
+ def ExtractValue(self, fv, _users_by_id): |
+ return fv.str_value |
+ |
+ |
+class TableCellCustomUser(TableCellCustom): |
+ """TableCell subclass specifically for showing custom user fields.""" |
+ |
+ def ExtractValue(self, fv, users_by_id): |
+ if fv.user_id in users_by_id: |
+ return users_by_id[fv.user_id].email |
+ return 'USER_%d' % fv.user_id |
+ |
+ |
+class TableCellCustomDate(TableCellCustom): |
+ """TableCell subclass specifically for showing custom date fields.""" |
+ |
+ def ExtractValue(self, fv, _users_by_id): |
+ # TODO(jrobbins): convert timestamp to formatted date and time |
+ return fv.int_value |
+ |
+ |
+class TableCellCustomBool(TableCellCustom): |
+ """TableCell subclass specifically for showing custom int fields.""" |
+ |
+ def ExtractValue(self, fv, _users_by_id): |
+ return 'Yes' if fv.int_value else 'No' |
+ |
+ |
+_CUSTOM_FIELD_CELL_FACTORIES = { |
+ tracker_pb2.FieldTypes.ENUM_TYPE: TableCellKeyLabels, |
+ tracker_pb2.FieldTypes.INT_TYPE: TableCellCustomInt, |
+ tracker_pb2.FieldTypes.STR_TYPE: TableCellCustomStr, |
+ tracker_pb2.FieldTypes.USER_TYPE: TableCellCustomUser, |
+ tracker_pb2.FieldTypes.DATE_TYPE: TableCellCustomDate, |
+ tracker_pb2.FieldTypes.BOOL_TYPE: TableCellCustomBool, |
+} |
+ |
+ |
+def ChooseCellFactory(col, cell_factories, config): |
+ """Return the CellFactory to use for the given column.""" |
+ if col in cell_factories: |
+ return cell_factories[col] |
+ |
+ if '/' in col: |
+ return CompositeTableCell(col.split('/'), cell_factories) |
+ |
+ fd = tracker_bizobj.FindFieldDef(col, config) |
+ if fd: |
+ return _CUSTOM_FIELD_CELL_FACTORIES[fd.field_type] |
+ |
+ return TableCellKeyLabels |