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, |
+ }) |