| # Copyright 2016 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed 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. |
| """ |
| from __future__ import print_function |
| from __future__ import division |
| from __future__ import absolute_import |
| |
| import collections |
| import itertools |
| import logging |
| |
| from functools import total_ordering |
| |
| import ezt |
| |
| from framework import framework_constants |
| from framework import template_helpers |
| from framework import timestr |
| from proto import tracker_pb2 |
| from tracker import tracker_bizobj |
| from tracker import tracker_constants |
| |
| |
| 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) |
| |
| phase_names = set(itertools.chain.from_iterable( |
| (phase.name.lower() for phase in result.phases) for result in results)) |
| # 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 fd.is_phase_field: |
| for name in phase_names: |
| phase_field_col = name + '.' + field_lower |
| if (phase_field_col not in shown_set and |
| phase_field_col not in unshown_set): |
| unshown_list.append(phase_field_col) |
| unshown_set.add(phase_field_col) |
| elif field_lower not in shown_set and field_lower not in unshown_set: |
| unshown_list.append(fd.field_name) |
| unshown_set.add(field_lower) |
| |
| if fd.field_type == tracker_pb2.FieldTypes.APPROVAL_TYPE: |
| approval_lower_approver = ( |
| field_lower + tracker_constants.APPROVER_COL_SUFFIX) |
| if (approval_lower_approver not in shown_set and |
| approval_lower_approver not in unshown_set): |
| unshown_list.append( |
| fd.field_name + tracker_constants.APPROVER_COL_SUFFIX) |
| unshown_set.add(approval_lower_approver) |
| |
| # 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, related_issues, hotlist_context_dict=None): |
| """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. |
| related_issues: dict {issue_id: issue} of pre-fetched related issues. |
| hotlist_context_dict: dict for building a hotlist grid table |
| |
| 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 |
| |
| if 'project' in column_values: |
| for art in artifact_list: |
| project_name = art.project_name |
| column_values['project'][project_name] = project_name |
| |
| if 'mergedinto' in column_values: |
| for art in artifact_list: |
| if art.merged_into and art.merged_into != 0: |
| merged_issue = related_issues[art.merged_into] |
| merged_issue_ref = tracker_bizobj.FormatIssueRef(( |
| merged_issue.project_name, merged_issue.local_id)) |
| column_values['mergedinto'][merged_issue_ref] = merged_issue_ref |
| |
| if 'blocked' in column_values: |
| for art in artifact_list: |
| if art.blocked_on_iids: |
| column_values['blocked']['is_blocked'] = 'Yes' |
| else: |
| column_values['blocked']['is_not_blocked'] = 'No' |
| |
| if 'blockedon' in column_values: |
| for art in artifact_list: |
| if art.blocked_on_iids: |
| for blocked_on_iid in art.blocked_on_iids: |
| blocked_on_issue = related_issues[blocked_on_iid] |
| blocked_on_ref = tracker_bizobj.FormatIssueRef(( |
| blocked_on_issue.project_name, blocked_on_issue.local_id)) |
| column_values['blockedon'][blocked_on_ref] = blocked_on_ref |
| |
| if 'blocking' in column_values: |
| for art in artifact_list: |
| if art.blocking_iids: |
| for blocking_iid in art.blocking_iids: |
| blocking_issue = related_issues[blocking_iid] |
| blocking_ref = tracker_bizobj.FormatIssueRef(( |
| blocking_issue.project_name, blocking_issue.local_id)) |
| column_values['blocking'][blocking_ref] = blocking_ref |
| |
| if 'added' in column_values: |
| for art in artifact_list: |
| if hotlist_context_dict and hotlist_context_dict[art.issue_id]: |
| issue_dict = hotlist_context_dict[art.issue_id] |
| date_added = issue_dict['date_added'] |
| column_values['added'][date_added] = date_added |
| |
| if 'adder' in column_values: |
| for art in artifact_list: |
| if hotlist_context_dict and hotlist_context_dict[art.issue_id]: |
| issue_dict = hotlist_context_dict[art.issue_id] |
| adder_id = issue_dict['adder_id'] |
| adder = users_by_id[adder_id].display_name |
| column_values['adder'][adder] = adder |
| |
| if 'note' in column_values: |
| for art in artifact_list: |
| if hotlist_context_dict and hotlist_context_dict[art.issue_id]: |
| issue_dict = hotlist_context_dict[art.issue_id] |
| note = issue_dict['note'] |
| if issue_dict['note']: |
| column_values['note'][note] = note |
| |
| 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, starred_items, lower_columns, lower_group_by, |
| users_by_id, cell_factories, id_accessor, related_issues, |
| viewable_iids_set, config, context_for_all_issues=None): |
| """Return a list of list row objects for display by EZT. |
| |
| Args: |
| visible_results: list of artifacts to display on one pagination page. |
| 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. |
| viewable_iids_set: set of issue ids that can be viewed by the user. |
| config: ProjectIssueConfig PB for the current project. |
| context_for_all_issues: A dictionary of dictionaries containing values |
| passed in to cell factory functions to create TableCells. Dictionary |
| form: {issue_id: {'rank': issue_rank, 'issue_info': info_value, ..}, |
| issue_id: {'rank': issue_rank}, ..} |
| |
| 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): |
| row = MakeRowData( |
| art, lower_columns, users_by_id, factories_to_use, related_issues, |
| viewable_iids_set, config, context_for_all_issues) |
| 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], |
| users_by_id, group_cell_factories, related_issues, viewable_iids_set, |
| config, context_for_all_issues) |
| 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, users_by_id, cell_factory_list, related_issues, |
| viewable_iids_set, config, context_for_all_issues): |
| """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 |
| 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. |
| viewable_iids_set: set of issue ids that can be viewed by the user. |
| config: ProjectIssueConfig PB for the current project. |
| context_for_all_issues: A dictionary of dictionaries containing values |
| passed in to cell factory functions to create TableCells. Dictionary |
| form: {issue_id: {'rank': issue_rank, 'issue_info': info_value, ..}, |
| issue_id: {'rank': issue_rank}, ..} |
| |
| Returns: |
| A TableRow object for use by EZT to render a table of results. |
| """ |
| if context_for_all_issues is None: |
| context_for_all_issues = {} |
| 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] |
| kw = { |
| 'col': col, |
| 'users_by_id': users_by_id, |
| 'non_col_labels': non_col_labels, |
| 'label_values': label_values, |
| 'related_issues': related_issues, |
| 'viewable_iids_set': viewable_iids_set, |
| 'config': config, |
| } |
| kw.update(context_for_all_issues.get(art.issue_id, {})) |
| new_cell = factory(art, **kw) |
| new_cell.col_index = i |
| ordered_row_data.append(new_cell) |
| |
| return TableRow(ordered_row_data) |
| |
| |
| 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)) |
| |
| |
| @total_ordering |
| class TableRow(object): |
| """A tiny auxiliary class to represent a row in an HTML table.""" |
| |
| def __init__(self, cells): |
| """Initialize the table row with the given data.""" |
| self.cells = cells |
| # Used by MakeTableData for layout. |
| self.idx = None |
| self.group = None |
| self.rows_in_group = None |
| self.starred = None |
| |
| def __eq__(self, other): |
| """A row is == if each cell is == to the cells in the other row.""" |
| return other and self.cells == other.cells |
| |
| def __ne__(self, other): |
| return not other and self.cells != other.cells |
| |
| def __lt__(self, other): |
| return other and self.cells < other.cells |
| |
| 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' |
| CELL_TYPE_NOTE = 'note' |
| CELL_TYPE_PROJECT = 'project' |
| CELL_TYPE_URL = 'url' |
| CELL_TYPE_ISSUES = 'issues' |
| |
| |
| @total_ordering |
| 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. |
| NOWRAP = ezt.boolean(True) |
| |
| 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 __eq__(self, other): |
| """A row is == if each cell is == to the cells in the other row.""" |
| return other and self.values == other.values |
| |
| def __ne__(self, other): |
| return not other and self.values != other.values |
| |
| def __lt__(self, other): |
| return other and self.values < other.values |
| |
| def DebugString(self): |
| return 'TC(%r, %r, %r)' % ( |
| self.type, |
| [v.DebugString() for v in self.values], |
| self.non_column_labels) |
| |
| |
| def CompositeFactoryTableCell(factory_col_list_arg): |
| """Cell factory that combines multiple cells in a combined column.""" |
| |
| class FactoryClass(TableCell): |
| factory_col_list = factory_col_list_arg |
| |
| def __init__(self, art, **kw): |
| TableCell.__init__(self, CELL_TYPE_UNFILTERABLE, []) |
| |
| for sub_factory, sub_col in self.factory_col_list: |
| kw['col'] = sub_col |
| sub_cell = sub_factory(art, **kw) |
| self.non_column_labels.extend(sub_cell.non_column_labels) |
| self.values.extend(sub_cell.values) |
| return FactoryClass |
| |
| |
| def CompositeColTableCell(columns_to_combine, cell_factories, config): |
| """Cell factory that combines multiple cells in a combined column.""" |
| factory_col_list = [] |
| for sub_col in columns_to_combine: |
| sub_factory = ChooseCellFactory(sub_col, cell_factories, config) |
| factory_col_list.append((sub_factory, sub_col)) |
| return CompositeFactoryTableCell(factory_col_list) |
| |
| |
| @total_ordering |
| 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 __eq__(self, other): |
| """A row is == if each cell is == to the item in the other row.""" |
| return other and self.item == other.item |
| |
| def __ne__(self, other): |
| return not other and self.item != other.item |
| |
| def __lt__(self, other): |
| return other and self.item < other.item |
| |
| 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=None, label_values=None, **_kw): |
| 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.""" |
| |
| def __init__(self, art, **_kw): |
| TableCell.__init__( |
| self, CELL_TYPE_PROJECT, [art.project_name]) |
| |
| |
| class TableCellStars(TableCell): |
| """TableCell subclass for showing an artifact's star count.""" |
| |
| def __init__(self, art, **_kw): |
| TableCell.__init__( |
| self, CELL_TYPE_ATTR, [art.star_count], align='right') |
| |
| |
| class TableCellSummary(TableCell): |
| """TableCell subclass for showing an artifact's summary.""" |
| |
| def __init__(self, art, non_col_labels=None, **_kw): |
| TableCell.__init__( |
| self, CELL_TYPE_SUMMARY, [art.summary], |
| non_column_labels=non_col_labels) |
| |
| |
| class TableCellDate(TableCell): |
| """TableCell subclass for showing any kind of date timestamp.""" |
| |
| # Make instances of this class render with whitespace:nowrap. |
| NOWRAP = ezt.boolean(True) |
| |
| def __init__(self, timestamp, days_only=False): |
| values = [] |
| if timestamp: |
| date_str = timestr.FormatRelativeDate(timestamp, days_only=days_only) |
| if not date_str: |
| date_str = timestr.FormatAbsoluteDate(timestamp) |
| values = [date_str] |
| |
| TableCell.__init__(self, CELL_TYPE_UNFILTERABLE, values) |
| |
| |
| class TableCellCustom(TableCell): |
| """Abstract TableCell subclass specifically for showing custom fields.""" |
| |
| def __init__(self, art, col=None, users_by_id=None, config=None, **_kw): |
| explicit_values = [] |
| derived_values = [] |
| cell_type = CELL_TYPE_ATTR |
| phase_names_by_id = { |
| phase.phase_id: phase.name.lower() for phase in art.phases} |
| phase_name = None |
| # Check if col represents a phase field value in the form <phase>.<field> |
| if '.' in col: |
| phase_name, col = col.split('.', 1) |
| 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 not fd: |
| # TODO(jrobbins): This can happen if an issue with a custom |
| # field value is moved to a different project. |
| logging.warn('Issue ID %r has undefined field value %r', |
| art.issue_id, fv) |
| elif fd.field_name.lower() == col and ( |
| phase_names_by_id.get(fv.phase_id) == phase_name): |
| if fd.field_type == tracker_pb2.FieldTypes.URL_TYPE: |
| cell_type = CELL_TYPE_URL |
| if fd.field_type == tracker_pb2.FieldTypes.STR_TYPE: |
| self.NOWRAP = ezt.boolean(False) |
| val = tracker_bizobj.GetFieldValue(fv, users_by_id) |
| if fv.derived: |
| derived_values.append(val) |
| else: |
| explicit_values.append(val) |
| |
| TableCell.__init__(self, cell_type, explicit_values, |
| derived_values=derived_values) |
| |
| def ExtractValue(self, fv, _users_by_id): |
| return 'field-id-%d-not-implemented-yet' % fv.field_id |
| |
| class TableCellApprovalStatus(TableCell): |
| """Abstract TableCell subclass specifically for showing approval fields.""" |
| |
| def __init__(self, art, col=None, config=None, **_kw): |
| explicit_values = [] |
| for av in art.approval_values: |
| fd = tracker_bizobj.FindFieldDef(col, config) |
| ad = tracker_bizobj.FindApprovalDef(col, config) |
| if not (ad and fd): |
| logging.warn('Issue ID %r has undefined field value %r', |
| art.issue_id, av) |
| elif av.approval_id == fd.field_id: |
| explicit_values.append(av.status.name) |
| break |
| |
| TableCell.__init__(self, CELL_TYPE_ATTR, explicit_values) |
| |
| |
| class TableCellApprovalApprover(TableCell): |
| """TableCell subclass specifically for showing approval approvers.""" |
| |
| def __init__(self, art, col=None, config=None, users_by_id=None, **_kw): |
| explicit_values = [] |
| approval_name = col[:-len(tracker_constants.APPROVER_COL_SUFFIX)] |
| for av in art.approval_values: |
| fd = tracker_bizobj.FindFieldDef(approval_name, config) |
| ad = tracker_bizobj.FindApprovalDef(approval_name, config) |
| if not (ad and fd): |
| logging.warn('Issue ID %r has undefined field value %r', |
| art.issue_id, av) |
| elif av.approval_id == fd.field_id: |
| explicit_values = [users_by_id.get(approver_id).display_name |
| for approver_id in av.approver_ids |
| if users_by_id.get(approver_id)] |
| break |
| |
| TableCell.__init__(self, CELL_TYPE_ATTR, explicit_values) |
| |
| 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 CompositeColTableCell(col.split('/'), cell_factories, config) |
| |
| is_approver_col = False |
| possible_field_name = col |
| if col.endswith(tracker_constants.APPROVER_COL_SUFFIX): |
| possible_field_name = col[:-len(tracker_constants.APPROVER_COL_SUFFIX)] |
| is_approver_col = True |
| # Check if col represents a phase field value in the form <phase>.<field> |
| elif '.' in possible_field_name: |
| possible_field_name = possible_field_name.split('.')[-1] |
| |
| fd = tracker_bizobj.FindFieldDef(possible_field_name, config) |
| if fd: |
| # We cannot assume that non-enum_type field defs do not share their |
| # names with label prefixes. So we need to group them with |
| # TableCellKeyLabels to make sure we catch appropriate labels values. |
| if fd.field_type == tracker_pb2.FieldTypes.APPROVAL_TYPE: |
| if is_approver_col: |
| # Combined cell for 'FieldName-approver' to hold approvers |
| # belonging to FieldName and values belonging to labels with |
| # 'FieldName-approver' as the key. |
| return CompositeFactoryTableCell( |
| [(TableCellApprovalApprover, col), (TableCellKeyLabels, col)]) |
| return CompositeFactoryTableCell( |
| [(TableCellApprovalStatus, col), (TableCellKeyLabels, col)]) |
| elif fd.field_type != tracker_pb2.FieldTypes.ENUM_TYPE: |
| return CompositeFactoryTableCell( |
| [(TableCellCustom, col), (TableCellKeyLabels, col)]) |
| |
| return TableCellKeyLabels |