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)