| # 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) |