blob: 3a1044335511f25ec439ee89c1ec318d0c029ba7 [file] [log] [blame]
# 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 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 flaskservlet
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
# def GetFlipperNextRedirectPage(self, **kwargs):
# self.next_handler = True
# return self.handler(**kwargs)
class FlipperPrev(FlipperRedirectBase):
next_handler = False
# def GetFlipperPrevRedirectPage(self, **kwargs):
# self.next_handler = False
# return self.handler(**kwargs)
class FlipperList(servlet.Servlet):
# pylint: disable=arguments-differ
# pylint: disable=unused-argument
# TODO: (monorail:6511)change to get(self) when convert to flask
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)
# def GetFlipperList(self, **kwargs):
# return self.handler(**kwargs)
# TODO: (monorail:6511) change to flaskJsonFeed when convert to flask
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 GetFlipperIndex(self, **kwargs):
# return self.handler(**kwargs)
# def PostFlipperIndex(self, **kwargs):
# return self.handler(**kwargs)
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)