| 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
|
|
|