Project import generated by Copybara.

GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/tracker/issuedetailezt.py b/tracker/issuedetailezt.py
new file mode 100644
index 0000000..9460669
--- /dev/null
+++ b/tracker/issuedetailezt.py
@@ -0,0 +1,316 @@
+# 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 implement the issue detail page and related forms.
+
+Summary of classes:
+  IssueDetailEzt: Show one issue in detail w/ all metadata and comments, and
+               process additional comments or metadata changes on it.
+  FlagSpamForm: Record the user's desire to report the issue as spam.
+"""
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import httplib
+import json
+import logging
+import time
+import ezt
+
+import settings
+from api import converters
+from businesslogic import work_env
+from features import features_bizobj
+from features import send_notifications
+from features import hotlist_helpers
+from features import hotlist_views
+from framework import exceptions
+from framework import framework_bizobj
+from framework import framework_constants
+from framework import framework_helpers
+from framework import framework_views
+from framework import jsonfeed
+from framework import paginate
+from framework import permissions
+from framework import servlet
+from framework import servlet_helpers
+from framework import sorting
+from framework import sql
+from framework import template_helpers
+from framework import urls
+from framework import xsrf
+from proto import user_pb2
+from proto import tracker_pb2
+from services import features_svc
+from services import tracker_fulltext
+from tracker import field_helpers
+from tracker import tracker_bizobj
+from tracker import tracker_constants
+from tracker import tracker_helpers
+from tracker import tracker_views
+
+from google.protobuf import json_format
+
+
+def CheckMoveIssueRequest(
+    services, mr, issue, move_selected, move_to, errors):
+  """Process the move issue portions of the issue update form.
+
+  Args:
+    services: A Services object
+    mr: commonly used info parsed from the request.
+    issue: Issue protobuf for the issue being moved.
+    move_selected: True if the user selected the Move action.
+    move_to: A project_name or url to move this issue to or None
+      if the project name wasn't sent in the form.
+    errors: The errors object for this request.
+
+    Returns:
+      The project pb for the project the issue will be moved to
+      or None if the move cannot be performed. Perhaps because
+      the project does not exist, in which case move_to and
+      move_to_project will be set on the errors object. Perhaps
+      the user does not have permission to move the issue to the
+      destination project, in which case the move_to field will be
+      set on the errors object.
+  """
+  if not move_selected:
+    return None
+
+  if not move_to:
+    errors.move_to = 'No destination project specified'
+    errors.move_to_project = move_to
+    return None
+
+  if issue.project_name == move_to:
+    errors.move_to = 'This issue is already in project ' + move_to
+    errors.move_to_project = move_to
+    return None
+
+  move_to_project = services.project.GetProjectByName(mr.cnxn, move_to)
+  if not move_to_project:
+    errors.move_to = 'No such project: ' + move_to
+    errors.move_to_project = move_to
+    return None
+
+  # permissions enforcement
+  if not servlet_helpers.CheckPermForProject(
+      mr, permissions.EDIT_ISSUE, move_to_project):
+    errors.move_to = 'You do not have permission to move issues to project'
+    errors.move_to_project = move_to
+    return None
+
+  elif permissions.GetRestrictions(issue):
+    errors.move_to = (
+        'Issues with Restrict labels are not allowed to be moved.')
+    errors.move_to_project = ''
+    return None
+
+  return move_to_project
+
+
+def _ComputeBackToListURL(mr, issue, config, hotlist, services):
+  """Construct a URL to return the user to the place that they came from."""
+  if hotlist:
+    back_to_list_url = hotlist_helpers.GetURLOfHotlist(
+        mr.cnxn, hotlist, services.user)
+  else:
+    back_to_list_url = tracker_helpers.FormatIssueListURL(
+        mr, config, cursor='%s:%d' % (issue.project_name, issue.local_id))
+
+  return back_to_list_url
+
+
+class FlipperRedirectBase(servlet.Servlet):
+
+  # pylint: disable=arguments-differ
+  # pylint: disable=unused-argument
+  def get(self, project_name=None, viewed_username=None, hotlist_id=None):
+    with work_env.WorkEnv(self.mr, self.services) as we:
+      hotlist_id = self.mr.GetIntParam('hotlist_id')
+      current_issue = we.GetIssueByLocalID(self.mr.project_id, self.mr.local_id,
+                                   use_cache=False)
+      hotlist = None
+      if hotlist_id:
+        try:
+          hotlist = self.services.features.GetHotlist(self.mr.cnxn, hotlist_id)
+        except features_svc.NoSuchHotlistException:
+          pass
+
+      try:
+        adj_issue = GetAdjacentIssue(
+            self.mr, we, current_issue, hotlist=hotlist,
+            next_issue=self.next_handler)
+        path = '/p/%s%s' % (adj_issue.project_name, urls.ISSUE_DETAIL)
+        url = framework_helpers.FormatURL(
+            [(name, self.mr.GetParam(name)) for
+             name in framework_helpers.RECOGNIZED_PARAMS],
+            path, id=adj_issue.local_id)
+      except exceptions.NoSuchIssueException:
+        config = we.GetProjectConfig(self.mr.project_id)
+        url = _ComputeBackToListURL(self.mr, current_issue, config,
+                                                 hotlist, self.services)
+      self.redirect(url)
+
+
+class FlipperNext(FlipperRedirectBase):
+  next_handler = True
+
+
+class FlipperPrev(FlipperRedirectBase):
+  next_handler = False
+
+
+class FlipperList(servlet.Servlet):
+  # pylint: disable=arguments-differ
+  # pylint: disable=unused-argument
+  def get(self, project_name=None, viewed_username=None, hotlist_id=None):
+    with work_env.WorkEnv(self.mr, self.services) as we:
+      hotlist_id = self.mr.GetIntParam('hotlist_id')
+      current_issue = we.GetIssueByLocalID(self.mr.project_id, self.mr.local_id,
+                                   use_cache=False)
+      hotlist = None
+      if hotlist_id:
+        try:
+          hotlist = self.services.features.GetHotlist(self.mr.cnxn, hotlist_id)
+        except features_svc.NoSuchHotlistException:
+          pass
+
+      config = we.GetProjectConfig(self.mr.project_id)
+
+      if hotlist:
+        self.mr.ComputeColSpec(hotlist)
+      else:
+        self.mr.ComputeColSpec(config)
+
+      url = _ComputeBackToListURL(self.mr, current_issue, config,
+                                               hotlist, self.services)
+    self.redirect(url)
+
+
+class FlipperIndex(jsonfeed.JsonFeed):
+  """Return a JSON object of an issue's index in search.
+
+  This is a distinct JSON endpoint because it can be expensive to compute.
+  """
+  CHECK_SECURITY_TOKEN = False
+
+  def HandleRequest(self, mr):
+    hotlist_id = mr.GetIntParam('hotlist_id')
+    list_url = None
+    with work_env.WorkEnv(mr, self.services) as we:
+      if not _ShouldShowFlipper(mr, self.services):
+        return {}
+      issue = we.GetIssueByLocalID(mr.project_id, mr.local_id, use_cache=False)
+      hotlist = None
+
+      if hotlist_id:
+        hotlist = self.services.features.GetHotlist(mr.cnxn, hotlist_id)
+
+        if not features_bizobj.IssueIsInHotlist(hotlist, issue.issue_id):
+          raise exceptions.InvalidHotlistException()
+
+        if not permissions.CanViewHotlist(
+            mr.auth.effective_ids, mr.perms, hotlist):
+          raise permissions.PermissionException()
+
+        (prev_iid, cur_index, next_iid, total_count
+            ) = we.GetIssuePositionInHotlist(
+                issue, hotlist, mr.can, mr.sort_spec, mr.group_by_spec)
+      else:
+        (prev_iid, cur_index, next_iid, total_count
+            ) = we.FindIssuePositionInSearch(issue)
+
+      config = we.GetProjectConfig(self.mr.project_id)
+
+      if hotlist:
+        mr.ComputeColSpec(hotlist)
+      else:
+        mr.ComputeColSpec(config)
+
+      list_url = _ComputeBackToListURL(mr, issue, config, hotlist,
+        self.services)
+
+    prev_url = None
+    next_url = None
+
+    recognized_params = [(name, mr.GetParam(name)) for name in
+                           framework_helpers.RECOGNIZED_PARAMS]
+    if prev_iid:
+      prev_issue = we.services.issue.GetIssue(mr.cnxn, prev_iid)
+      path = '/p/%s%s' % (prev_issue.project_name, urls.ISSUE_DETAIL)
+      prev_url = framework_helpers.FormatURL(
+          recognized_params, path, id=prev_issue.local_id)
+
+    if next_iid:
+      next_issue = we.services.issue.GetIssue(mr.cnxn, next_iid)
+      path = '/p/%s%s' % (next_issue.project_name, urls.ISSUE_DETAIL)
+      next_url = framework_helpers.FormatURL(
+          recognized_params, path, id=next_issue.local_id)
+
+    return {
+      'prev_iid': prev_iid,
+      'prev_url': prev_url,
+      'cur_index': cur_index,
+      'next_iid': next_iid,
+      'next_url': next_url,
+      'list_url': list_url,
+      'total_count': total_count,
+    }
+
+
+def _ShouldShowFlipper(mr, services):
+  """Return True if we should show the flipper."""
+
+  # Check if the user entered a specific issue ID of an existing issue.
+  if tracker_constants.JUMP_RE.match(mr.query):
+    return False
+
+  # Check if the user came directly to an issue without specifying any
+  # query or sort.  E.g., through crbug.com.  Generating the issue ref
+  # list can be too expensive in projects that have a large number of
+  # issues.  The all and open issues cans are broad queries, other
+  # canned queries should be narrow enough to not need this special
+  # treatment.
+  if (not mr.query and not mr.sort_spec and
+      mr.can in [tracker_constants.ALL_ISSUES_CAN,
+                 tracker_constants.OPEN_ISSUES_CAN]):
+    num_issues_in_project = services.issue.GetHighestLocalID(
+        mr.cnxn, mr.project_id)
+    if num_issues_in_project > settings.threshold_to_suppress_prev_next:
+      return False
+
+  return True
+
+
+def GetAdjacentIssue(
+    mr, we, issue, hotlist=None, next_issue=False):
+  """Compute next or previous issue given params of current issue.
+
+  Args:
+    mr: MonorailRequest, including can and sorting/grouping order.
+    we: A WorkEnv instance.
+    issue: The current issue (from which to compute prev/next).
+    hotlist (optional): The current hotlist.
+    next_issue (bool): If True, return next, issue, else return previous issue.
+
+  Returns:
+    The adjacent issue.
+
+  Raises:
+    NoSuchIssueException when there is no adjacent issue in the list.
+  """
+  if hotlist:
+    (prev_iid, _cur_index, next_iid, _total_count
+        ) = we.GetIssuePositionInHotlist(
+            issue, hotlist, mr.can, mr.sort_spec, mr.group_by_spec)
+  else:
+    (prev_iid, _cur_index, next_iid, _total_count
+        ) = we.FindIssuePositionInSearch(issue)
+  iid = next_iid if next_issue else prev_iid
+  if iid is None:
+    raise exceptions.NoSuchIssueException()
+  return we.GetIssue(iid)