Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/tracker/tablecell.py b/tracker/tablecell.py
new file mode 100644
index 0000000..afb6468
--- /dev/null
+++ b/tracker/tablecell.py
@@ -0,0 +1,506 @@
+# 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 that generate value cells in the issue list table."""
+
+from __future__ import division
+from __future__ import print_function
+from __future__ import absolute_import
+
+import logging
+import time
+import ezt
+
+from framework import framework_constants
+from framework import table_view_helpers
+from framework import template_helpers
+from framework import urls
+from tracker import tracker_bizobj
+from tracker import tracker_helpers
+
+# pylint: disable=unused-argument
+
+
+class TableCellNote(table_view_helpers.TableCell):
+ """TableCell subclass specifically for showing a hotlist issue's note."""
+
+ def __init__(self, issue, note=None, **_kw):
+ if note:
+ display_note = [note]
+ else:
+ display_note = []
+ table_view_helpers.TableCell.__init__(
+ self, table_view_helpers.CELL_TYPE_NOTE, display_note)
+
+
+class TableCellDateAdded(table_view_helpers.TableCell):
+ """TableCell subclass specifically for showing the date added of an issue."""
+
+ def __init__(self, issue, date_added=None, **_kw):
+ table_view_helpers.TableCell.__init__(
+ self, table_view_helpers.CELL_TYPE_ATTR, [date_added])
+
+
+class TableCellAdderID(table_view_helpers.TableCell):
+ """TableCell subclass specifically for showing an issue's adder_id."""
+
+ def __init__(self, issue, adder_id=None, users_by_id=None, **_kw):
+ if adder_id:
+ display_name = [users_by_id[adder_id].display_name]
+ else:
+ display_name = [None]
+ table_view_helpers.TableCell.__init__(
+ self, table_view_helpers.CELL_TYPE_ATTR,
+ display_name)
+
+
+class TableCellRank(table_view_helpers.TableCell):
+ """TableCell subclass specifically for showing issue rank."""
+
+ def __init__(self, issue, issue_rank=None, **_kw):
+ table_view_helpers.TableCell.__init__(
+ self, table_view_helpers.CELL_TYPE_ATTR, [issue_rank])
+
+
+class TableCellID(table_view_helpers.TableCell):
+ """TableCell subclass specifically for showing issue IDs."""
+
+ def __init__(self, issue, **_kw):
+ 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, **_kws):
+ 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."""
+
+ def __init__(self, issue, users_by_id=None, **_kw):
+ 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."""
+
+ def __init__(self, issue, users_by_id=None, **_kw):
+ 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, users_by_id=None, **_kw):
+ 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, **_kw):
+ table_view_helpers.TableCell.__init__(
+ self, table_view_helpers.CELL_TYPE_ATTR, [issue.attachment_count],
+ align='right')
+
+
+class TableCellOpened(table_view_helpers.TableCellDate):
+ """TableCell subclass specifically for showing issue opened date."""
+
+ def __init__(self, issue, **_kw):
+ table_view_helpers.TableCellDate.__init__(self, issue.opened_timestamp)
+
+
+class TableCellClosed(table_view_helpers.TableCellDate):
+ """TableCell subclass specifically for showing issue closed date."""
+
+ def __init__(self, issue, **_kw):
+ table_view_helpers.TableCellDate.__init__(self, issue.closed_timestamp)
+
+
+class TableCellModified(table_view_helpers.TableCellDate):
+ """TableCell subclass specifically for showing issue modified date."""
+
+ def __init__(self, issue, **_kw):
+ table_view_helpers.TableCellDate.__init__(self, issue.modified_timestamp)
+
+
+class TableCellOwnerModified(table_view_helpers.TableCellDate):
+ """TableCell subclass specifically for showing owner modified age."""
+
+ def __init__(self, issue, **_kw):
+ table_view_helpers.TableCellDate.__init__(
+ self, issue.owner_modified_timestamp, days_only=True)
+
+
+class TableCellStatusModified(table_view_helpers.TableCellDate):
+ """TableCell subclass specifically for showing status modified age."""
+
+ def __init__(self, issue, **_kw):
+ table_view_helpers.TableCellDate.__init__(
+ self, issue.status_modified_timestamp, days_only=True)
+
+
+class TableCellComponentModified(table_view_helpers.TableCellDate):
+ """TableCell subclass specifically for showing component modified age."""
+
+ def __init__(self, issue, **_kw):
+ table_view_helpers.TableCellDate.__init__(
+ self, issue.component_modified_timestamp, days_only=True)
+
+
+class TableCellOwnerLastVisit(table_view_helpers.TableCellDate):
+ """TableCell subclass specifically for showing owner last visit days ago."""
+
+ def __init__(self, issue, users_by_id=None, **_kw):
+ owner_view = users_by_id.get(issue.owner_id or issue.derived_owner_id)
+ last_visit = None
+ if owner_view:
+ last_visit = owner_view.user.last_visit_timestamp
+ table_view_helpers.TableCellDate.__init__(
+ self, last_visit, days_only=True)
+
+def _make_issue_view(default_pn, config, viewable_iids_set, ref_issue):
+ viewable = ref_issue.issue_id in viewable_iids_set
+ return template_helpers.EZTItem(
+ id=tracker_bizobj.FormatIssueRef(
+ (ref_issue.project_name, ref_issue.local_id),
+ default_project_name=default_pn),
+ href=tracker_helpers.FormatRelativeIssueURL(
+ ref_issue.project_name, urls.ISSUE_DETAIL, id=ref_issue.local_id),
+ closed=ezt.boolean(
+ viewable and
+ not tracker_helpers.MeansOpenInProject(ref_issue.status, config)),
+ title=ref_issue.summary if viewable else "")
+
+
+class TableCellBlockedOn(table_view_helpers.TableCell):
+ """TableCell subclass for listing issues the current issue is blocked on."""
+
+ def __init__(self, issue, related_issues=None, **_kw):
+ ref_issues = [related_issues[iid] for iid in issue.blocked_on_iids
+ if iid in related_issues]
+ values = [_make_issue_view(issue.project_name, _kw["config"],
+ _kw["viewable_iids_set"], ref_issue)
+ for ref_issue in ref_issues]
+ values.sort(key=lambda x: (x.closed, x.id))
+ table_view_helpers.TableCell.__init__(
+ self, table_view_helpers.CELL_TYPE_ISSUES, values, sort_values=False)
+
+
+class TableCellBlocking(table_view_helpers.TableCell):
+ """TableCell subclass for listing issues the current issue is blocking."""
+
+ def __init__(self, issue, related_issues=None, **_kw):
+ ref_issues = [related_issues[iid] for iid in issue.blocking_iids
+ if iid in related_issues]
+ values = [_make_issue_view(issue.project_name, _kw["config"],
+ _kw["viewable_iids_set"], ref_issue)
+ for ref_issue in ref_issues]
+ values.sort(key=lambda x: (x.closed, x.id))
+ table_view_helpers.TableCell.__init__(
+ self, table_view_helpers.CELL_TYPE_ISSUES, values, sort_values=False)
+
+
+class TableCellBlocked(table_view_helpers.TableCell):
+ """TableCell subclass for showing whether an issue is blocked."""
+
+ def __init__(self, issue, **_kw):
+ 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, related_issues=None, **_kw):
+ if issue.merged_into:
+ ref_issue = related_issues[issue.merged_into]
+ values = [_make_issue_view(issue.project_name, _kw["config"],
+ _kw["viewable_iids_set"], ref_issue)]
+ else: # Note: None means not merged into any issue.
+ values = []
+ table_view_helpers.TableCell.__init__(
+ self, table_view_helpers.CELL_TYPE_ISSUES, values)
+
+
+class TableCellComponent(table_view_helpers.TableCell):
+ """TableCell subclass for showing components."""
+
+ def __init__(self, issue, config=None, **_kw):
+ 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)
+
+
+class TableCellAllLabels(table_view_helpers.TableCell):
+ """TableCell subclass specifically for showing all labels on an issue."""
+
+ def __init__(self, issue, **_kw):
+ 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)
+
+
+# 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,
+ 'ownermodified': TableCellOwnerModified,
+ 'statusmodified': TableCellStatusModified,
+ 'componentmodified': TableCellComponentModified,
+ 'ownerlastvisit': TableCellOwnerLastVisit,
+ 'rank': TableCellRank,
+ 'added': TableCellDateAdded,
+ 'adder': TableCellAdderID,
+ 'note': TableCellNote,
+ 'alllabels': TableCellAllLabels,
+ }
+
+
+# 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 TableCellOpenedCSV(table_view_helpers.TableCell):
+ """TableCell subclass specifically for showing issue opened date."""
+
+ def __init__(self, issue, **_kw):
+ 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, **_kw):
+ 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, **_kw):
+ 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, **_kw):
+ 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, **_kw):
+ 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, **_kw):
+ table_view_helpers.TableCell.__init__(
+ self, table_view_helpers.CELL_TYPE_UNFILTERABLE,
+ [issue.closed_timestamp])
+
+
+class TableCellOwnerModifiedCSV(table_view_helpers.TableCell):
+ """TableCell subclass specifically for showing owner modified date."""
+
+ def __init__(self, issue, **_kw):
+ values = []
+ if issue.modified_timestamp:
+ values = [TimeStringForCSV(issue.owner_modified_timestamp)]
+
+ table_view_helpers.TableCell.__init__(
+ self, table_view_helpers.CELL_TYPE_UNFILTERABLE, values)
+
+
+class TableCellOwnerModifiedTimestamp(table_view_helpers.TableCell):
+ """TableCell subclass specifically for showing owner modified timestamp."""
+
+ def __init__(self, issue, **_kw):
+ table_view_helpers.TableCell.__init__(
+ self, table_view_helpers.CELL_TYPE_UNFILTERABLE,
+ [issue.owner_modified_timestamp])
+
+
+class TableCellStatusModifiedCSV(table_view_helpers.TableCell):
+ """TableCell subclass specifically for showing status modified date."""
+
+ def __init__(self, issue, **_kw):
+ values = []
+ if issue.modified_timestamp:
+ values = [TimeStringForCSV(issue.status_modified_timestamp)]
+
+ table_view_helpers.TableCell.__init__(
+ self, table_view_helpers.CELL_TYPE_UNFILTERABLE, values)
+
+
+class TableCellStatusModifiedTimestamp(table_view_helpers.TableCell):
+ """TableCell subclass specifically for showing status modified timestamp."""
+
+ def __init__(self, issue, **_kw):
+ table_view_helpers.TableCell.__init__(
+ self, table_view_helpers.CELL_TYPE_UNFILTERABLE,
+ [issue.status_modified_timestamp])
+
+
+class TableCellComponentModifiedCSV(table_view_helpers.TableCell):
+ """TableCell subclass specifically for showing component modified date."""
+
+ def __init__(self, issue, **_kw):
+ values = []
+ if issue.modified_timestamp:
+ values = [TimeStringForCSV(issue.component_modified_timestamp)]
+
+ table_view_helpers.TableCell.__init__(
+ self, table_view_helpers.CELL_TYPE_UNFILTERABLE, values)
+
+
+class TableCellComponentModifiedTimestamp(table_view_helpers.TableCell):
+ """TableCell subclass for showing component modified timestamp."""
+
+ def __init__(self, issue, **_kw):
+ table_view_helpers.TableCell.__init__(
+ self, table_view_helpers.CELL_TYPE_UNFILTERABLE,
+ [issue.component_modified_timestamp])
+
+
+class TableCellOwnerLastVisitDaysAgo(table_view_helpers.TableCell):
+ """TableCell subclass specifically for showing owner last visit days ago."""
+
+ def __init__(self, issue, users_by_id=None, **_kw):
+ owner_view = users_by_id.get(issue.owner_id or issue.derived_owner_id)
+ last_visit_days_ago = None
+ if owner_view and owner_view.user.last_visit_timestamp:
+ secs_ago = int(time.time()) - owner_view.user.last_visit_timestamp
+ last_visit_days_ago = secs_ago // framework_constants.SECS_PER_DAY
+ table_view_helpers.TableCell.__init__(
+ self, table_view_helpers.CELL_TYPE_UNFILTERABLE, [last_visit_days_ago])
+
+
+# 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({
+ 'opened': TableCellOpenedCSV,
+ 'openedtimestamp': TableCellOpenedTimestamp,
+ 'closed': TableCellClosedCSV,
+ 'closedtimestamp': TableCellClosedTimestamp,
+ 'modified': TableCellModifiedCSV,
+ 'modifiedtimestamp': TableCellModifiedTimestamp,
+ 'ownermodified': TableCellOwnerModifiedCSV,
+ 'ownermodifiedtimestamp': TableCellOwnerModifiedTimestamp,
+ 'statusmodified': TableCellStatusModifiedCSV,
+ 'statusmodifiedtimestamp': TableCellStatusModifiedTimestamp,
+ 'componentmodified': TableCellComponentModifiedCSV,
+ 'componentmodifiedtimestamp': TableCellComponentModifiedTimestamp,
+ 'ownerlastvisitdaysago': TableCellOwnerLastVisitDaysAgo,
+ })