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