| Index: appengine/monorail/tracker/tablecell.py
|
| diff --git a/appengine/monorail/tracker/tablecell.py b/appengine/monorail/tracker/tablecell.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..ad84a89a66c35f6ab6814ca4c87c472e5bcc9137
|
| --- /dev/null
|
| +++ b/appengine/monorail/tracker/tablecell.py
|
| @@ -0,0 +1,422 @@
|
| +# 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 that generate value cells in the issue list table."""
|
| +
|
| +import logging
|
| +import time
|
| +from third_party import ezt
|
| +
|
| +from framework import table_view_helpers
|
| +from framework import timestr
|
| +from tracker import tracker_bizobj
|
| +
|
| +# pylint: disable=unused-argument
|
| +
|
| +
|
| +class TableCellID(table_view_helpers.TableCell):
|
| + """TableCell subclass specifically for showing issue IDs."""
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + _related_issues, _config):
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_ID, [str(issue.local_id)])
|
| +
|
| +
|
| +class TableCellStatus(table_view_helpers.TableCell):
|
| + """TableCell subclass specifically for showing issue status values."""
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + _related_issues, _config):
|
| + values = []
|
| + derived_values = []
|
| + if issue.status:
|
| + values = [issue.status]
|
| + if issue.derived_status:
|
| + derived_values = [issue.derived_status]
|
| +
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_ATTR, values,
|
| + derived_values=derived_values)
|
| +
|
| +
|
| +class TableCellOwner(table_view_helpers.TableCell):
|
| + """TableCell subclass specifically for showing issue owner name."""
|
| +
|
| + # Make instances of this class render with whitespace:nowrap.
|
| + NOWRAP = ezt.boolean(True)
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + _related_issues, _config):
|
| + values = []
|
| + derived_values = []
|
| + if issue.owner_id:
|
| + values = [users_by_id[issue.owner_id].display_name]
|
| + if issue.derived_owner_id:
|
| + derived_values = [users_by_id[issue.derived_owner_id].display_name]
|
| +
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_ATTR, values,
|
| + derived_values=derived_values)
|
| +
|
| +
|
| +class TableCellReporter(table_view_helpers.TableCell):
|
| + """TableCell subclass specifically for showing issue reporter name."""
|
| +
|
| + # Make instances of this class render with whitespace:nowrap.
|
| + NOWRAP = ezt.boolean(True)
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + _related_issues, _config):
|
| + try:
|
| + values = [users_by_id[issue.reporter_id].display_name]
|
| + except KeyError:
|
| + logging.info('issue reporter %r not found', issue.reporter_id)
|
| + values = ['deleted?']
|
| +
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_ATTR, values)
|
| +
|
| +
|
| +class TableCellCc(table_view_helpers.TableCell):
|
| + """TableCell subclass specifically for showing issue Cc user names."""
|
| +
|
| + def __init__(
|
| + self, issue, _col, users_by_id, _non_col_labels,
|
| + _label_values, _related_issues, _config):
|
| + values = [users_by_id[cc_id].display_name
|
| + for cc_id in issue.cc_ids]
|
| +
|
| + derived_values = [users_by_id[cc_id].display_name
|
| + for cc_id in issue.derived_cc_ids]
|
| +
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_ATTR, values,
|
| + derived_values=derived_values)
|
| +
|
| +
|
| +class TableCellAttachments(table_view_helpers.TableCell):
|
| + """TableCell subclass specifically for showing issue attachment count."""
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + _related_issues, _config):
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_ATTR, [issue.attachment_count],
|
| + align='right')
|
| +
|
| +
|
| +class TableCellOpened(table_view_helpers.TableCell):
|
| + """TableCell subclass specifically for showing issue opened date."""
|
| +
|
| + # Make instances of this class render with whitespace:nowrap.
|
| + NOWRAP = ezt.boolean(True)
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + _related_issues, _config):
|
| + date_str = timestr.FormatRelativeDate(
|
| + issue.opened_timestamp, recent_only=True)
|
| + if not date_str:
|
| + date_str = timestr.FormatAbsoluteDate(issue.opened_timestamp)
|
| +
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_UNFILTERABLE, [date_str])
|
| +
|
| +
|
| +class TableCellClosed(table_view_helpers.TableCell):
|
| + """TableCell subclass specifically for showing issue closed date."""
|
| +
|
| + # Make instances of this class render with whitespace:nowrap.
|
| + NOWRAP = ezt.boolean(True)
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + _related_issues, _config):
|
| + values = []
|
| + if issue.closed_timestamp:
|
| + date_str = timestr.FormatRelativeDate(
|
| + issue.closed_timestamp, recent_only=True)
|
| + if not date_str:
|
| + date_str = timestr.FormatAbsoluteDate(issue.closed_timestamp)
|
| + values = [date_str]
|
| +
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_UNFILTERABLE, values)
|
| +
|
| +
|
| +class TableCellModified(table_view_helpers.TableCell):
|
| + """TableCell subclass specifically for showing issue modified date."""
|
| +
|
| + # Make instances of this class render with whitespace:nowrap.
|
| + NOWRAP = ezt.boolean(True)
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + _related_issues, _config):
|
| + values = []
|
| + if issue.modified_timestamp:
|
| + date_str = timestr.FormatRelativeDate(
|
| + issue.modified_timestamp, recent_only=True)
|
| + if not date_str:
|
| + date_str = timestr.FormatAbsoluteDate(issue.modified_timestamp)
|
| + values = [date_str]
|
| +
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_UNFILTERABLE, values)
|
| +
|
| +
|
| +class TableCellBlockedOn(table_view_helpers.TableCell):
|
| + """TableCell subclass for listing issues the current issue is blocked on."""
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + related_issues, _config):
|
| + ref_issues = [related_issues[iid] for iid in issue.blocked_on_iids
|
| + if iid in related_issues]
|
| + default_pn = issue.project_name
|
| + # TODO(jrobbins): in cross-project searches, leave default_pn = None.
|
| + values = [
|
| + tracker_bizobj.FormatIssueRef(
|
| + (ref_issue.project_name, ref_issue.local_id),
|
| + default_project_name=default_pn)
|
| + for ref_issue in ref_issues]
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_ATTR, values)
|
| +
|
| +
|
| +class TableCellBlocking(table_view_helpers.TableCell):
|
| + """TableCell subclass for listing issues the current issue is blocking."""
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + related_issues, _config):
|
| + ref_issues = [related_issues[iid] for iid in issue.blocking_iids
|
| + if iid in related_issues]
|
| + default_pn = issue.project_name
|
| + # TODO(jrobbins): in cross-project searches, leave default_pn = None.
|
| + values = [
|
| + tracker_bizobj.FormatIssueRef(
|
| + (ref_issue.project_name, ref_issue.local_id),
|
| + default_project_name=default_pn)
|
| + for ref_issue in ref_issues]
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_ATTR, values)
|
| +
|
| +
|
| +class TableCellBlocked(table_view_helpers.TableCell):
|
| + """TableCell subclass for showing whether an issue is blocked."""
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + _related_issues, _config):
|
| + if issue.blocked_on_iids:
|
| + value = 'Yes'
|
| + else:
|
| + value = 'No'
|
| +
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_ATTR, [value])
|
| +
|
| +
|
| +class TableCellMergedInto(table_view_helpers.TableCell):
|
| + """TableCell subclass for showing whether an issue is blocked."""
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + related_issues, _config):
|
| + if issue.merged_into:
|
| + ref_issue = related_issues[issue.merged_into]
|
| + ref = ref_issue.project_name, ref_issue.local_id
|
| + default_pn = issue.project_name
|
| + # TODO(jrobbins): in cross-project searches, leave default_pn = None.
|
| + values = [
|
| + tracker_bizobj.FormatIssueRef(ref, default_project_name=default_pn)]
|
| + else: # Note: None means not merged into any issue.
|
| + values = []
|
| +
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_ATTR, values)
|
| +
|
| +
|
| +class TableCellComponent(table_view_helpers.TableCell):
|
| + """TableCell subclass for showing components."""
|
| +
|
| + def __init__(
|
| + self, issue, _col, _users_by_id, _non_col_labels,
|
| + _label_values, _related_issues, config):
|
| + explicit_paths = []
|
| + for component_id in issue.component_ids:
|
| + cd = tracker_bizobj.FindComponentDefByID(component_id, config)
|
| + if cd:
|
| + explicit_paths.append(cd.path)
|
| +
|
| + derived_paths = []
|
| + for component_id in issue.derived_component_ids:
|
| + cd = tracker_bizobj.FindComponentDefByID(component_id, config)
|
| + if cd:
|
| + derived_paths.append(cd.path)
|
| +
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_ATTR, explicit_paths,
|
| + derived_values=derived_paths)
|
| +
|
| +
|
| +# This maps column names to factories/constructors that make table cells.
|
| +# Subclasses can override this mapping, so any additions to this mapping
|
| +# should also be added to subclasses.
|
| +CELL_FACTORIES = {
|
| + 'id': TableCellID,
|
| + 'project': table_view_helpers.TableCellProject,
|
| + 'component': TableCellComponent,
|
| + 'summary': table_view_helpers.TableCellSummary,
|
| + 'status': TableCellStatus,
|
| + 'owner': TableCellOwner,
|
| + 'reporter': TableCellReporter,
|
| + 'cc': TableCellCc,
|
| + 'stars': table_view_helpers.TableCellStars,
|
| + 'attachments': TableCellAttachments,
|
| + 'opened': TableCellOpened,
|
| + 'closed': TableCellClosed,
|
| + 'modified': TableCellModified,
|
| + 'blockedon': TableCellBlockedOn,
|
| + 'blocking': TableCellBlocking,
|
| + 'blocked': TableCellBlocked,
|
| + 'mergedinto': TableCellMergedInto,
|
| + }
|
| +
|
| +
|
| +# Time format that spreadsheets seem to understand.
|
| +# E.g.: "May 19 2008 13:30:23". Tested with MS Excel 2003,
|
| +# OpenOffice.org, NeoOffice, and Google Spreadsheets.
|
| +CSV_DATE_TIME_FMT = '%b %d, %Y %H:%M:%S'
|
| +
|
| +
|
| +def TimeStringForCSV(timestamp):
|
| + """Return a timestamp in a format that spreadsheets understand."""
|
| + return time.strftime(CSV_DATE_TIME_FMT, time.gmtime(timestamp))
|
| +
|
| +
|
| +class TableCellSummaryCSV(table_view_helpers.TableCell):
|
| + """TableCell subclass for showing issue summaries escaped for CSV."""
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + _related, _config):
|
| + escaped_summary = issue.summary.replace('"', '""')
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_SUMMARY, [escaped_summary],
|
| + non_column_labels=non_col_labels)
|
| +
|
| +
|
| +class TableCellAllLabels(table_view_helpers.TableCell):
|
| + """TableCell subclass specifically for showing all labels on an issue."""
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + _related, _config):
|
| + values = []
|
| + derived_values = []
|
| + if issue.labels:
|
| + values = issue.labels[:]
|
| + if issue.derived_labels:
|
| + derived_values = issue.derived_labels[:]
|
| +
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_ATTR, values,
|
| + derived_values=derived_values)
|
| +
|
| +
|
| +class TableCellOpenedCSV(table_view_helpers.TableCell):
|
| + """TableCell subclass specifically for showing issue opened date."""
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + _related, _config):
|
| + date_str = TimeStringForCSV(issue.opened_timestamp)
|
| +
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_UNFILTERABLE, [date_str])
|
| +
|
| +
|
| +class TableCellOpenedTimestamp(table_view_helpers.TableCell):
|
| + """TableCell subclass specifically for showing issue opened timestamp."""
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + _related, _config):
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_UNFILTERABLE,
|
| + [issue.opened_timestamp])
|
| +
|
| +
|
| +class TableCellModifiedCSV(table_view_helpers.TableCell):
|
| + """TableCell subclass specifically for showing issue modified date."""
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + _related, _config):
|
| + values = []
|
| + if issue.modified_timestamp:
|
| + values = [TimeStringForCSV(issue.modified_timestamp)]
|
| +
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_UNFILTERABLE, values)
|
| +
|
| +
|
| +class TableCellModifiedTimestamp(table_view_helpers.TableCell):
|
| + """TableCell subclass specifically for showing issue modified timestamp."""
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + _related, _config):
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_UNFILTERABLE,
|
| + [issue.modified_timestamp])
|
| +
|
| +
|
| +class TableCellClosedCSV(table_view_helpers.TableCell):
|
| + """TableCell subclass specifically for showing issue closed date."""
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + _related, _config):
|
| + values = []
|
| + if issue.closed_timestamp:
|
| + values = [TimeStringForCSV(issue.closed_timestamp)]
|
| +
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_UNFILTERABLE, values)
|
| +
|
| +
|
| +class TableCellClosedTimestamp(table_view_helpers.TableCell):
|
| + """TableCell subclass specifically for showing issue closed timestamp."""
|
| +
|
| + def __init__(
|
| + self, issue, col, users_by_id, non_col_labels, label_values,
|
| + _related, _config):
|
| + table_view_helpers.TableCell.__init__(
|
| + self, table_view_helpers.CELL_TYPE_UNFILTERABLE,
|
| + [issue.closed_timestamp])
|
| +
|
| +
|
| +# Maps column names to factories/constructors that make table cells.
|
| +# Uses the defaults in issuelist.py but changes the factory for the
|
| +# summary cell to properly escape the data for CSV files.
|
| +CSV_CELL_FACTORIES = CELL_FACTORIES.copy()
|
| +CSV_CELL_FACTORIES.update({
|
| + 'summary': TableCellSummaryCSV,
|
| + 'alllabels': TableCellAllLabels,
|
| + 'opened': TableCellOpenedCSV,
|
| + 'openedtimestamp': TableCellOpenedTimestamp,
|
| + 'closed': TableCellClosedCSV,
|
| + 'closedtimestamp': TableCellClosedTimestamp,
|
| + 'modified': TableCellModifiedCSV,
|
| + 'modifiedtimestamp': TableCellModifiedTimestamp,
|
| + })
|
|
|