Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 1 | # Copyright 2016 The Chromium Authors |
| 2 | # Use of this source code is governed by a BSD-style license that can be |
| 3 | # found in the LICENSE file. |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 4 | |
| 5 | """Classes that implement the issue detail page and related forms. |
| 6 | |
| 7 | Summary of classes: |
| 8 | IssueDetailEzt: Show one issue in detail w/ all metadata and comments, and |
| 9 | process additional comments or metadata changes on it. |
| 10 | FlagSpamForm: Record the user's desire to report the issue as spam. |
| 11 | """ |
| 12 | from __future__ import print_function |
| 13 | from __future__ import division |
| 14 | from __future__ import absolute_import |
| 15 | |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 16 | import settings |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 17 | from businesslogic import work_env |
| 18 | from features import features_bizobj |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 19 | from features import hotlist_helpers |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 20 | from framework import exceptions |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 21 | from framework import framework_helpers |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 22 | from framework import jsonfeed |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 23 | from framework import permissions |
| 24 | from framework import servlet |
| 25 | from framework import servlet_helpers |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 26 | from framework import urls |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 27 | from services import features_svc |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 28 | from tracker import tracker_constants |
| 29 | from tracker import tracker_helpers |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 30 | |
| 31 | |
| 32 | def CheckMoveIssueRequest( |
| 33 | services, mr, issue, move_selected, move_to, errors): |
| 34 | """Process the move issue portions of the issue update form. |
| 35 | |
| 36 | Args: |
| 37 | services: A Services object |
| 38 | mr: commonly used info parsed from the request. |
| 39 | issue: Issue protobuf for the issue being moved. |
| 40 | move_selected: True if the user selected the Move action. |
| 41 | move_to: A project_name or url to move this issue to or None |
| 42 | if the project name wasn't sent in the form. |
| 43 | errors: The errors object for this request. |
| 44 | |
| 45 | Returns: |
| 46 | The project pb for the project the issue will be moved to |
| 47 | or None if the move cannot be performed. Perhaps because |
| 48 | the project does not exist, in which case move_to and |
| 49 | move_to_project will be set on the errors object. Perhaps |
| 50 | the user does not have permission to move the issue to the |
| 51 | destination project, in which case the move_to field will be |
| 52 | set on the errors object. |
| 53 | """ |
| 54 | if not move_selected: |
| 55 | return None |
| 56 | |
| 57 | if not move_to: |
| 58 | errors.move_to = 'No destination project specified' |
| 59 | errors.move_to_project = move_to |
| 60 | return None |
| 61 | |
| 62 | if issue.project_name == move_to: |
| 63 | errors.move_to = 'This issue is already in project ' + move_to |
| 64 | errors.move_to_project = move_to |
| 65 | return None |
| 66 | |
| 67 | move_to_project = services.project.GetProjectByName(mr.cnxn, move_to) |
| 68 | if not move_to_project: |
| 69 | errors.move_to = 'No such project: ' + move_to |
| 70 | errors.move_to_project = move_to |
| 71 | return None |
| 72 | |
| 73 | # permissions enforcement |
| 74 | if not servlet_helpers.CheckPermForProject( |
| 75 | mr, permissions.EDIT_ISSUE, move_to_project): |
| 76 | errors.move_to = 'You do not have permission to move issues to project' |
| 77 | errors.move_to_project = move_to |
| 78 | return None |
| 79 | |
| 80 | elif permissions.GetRestrictions(issue): |
| 81 | errors.move_to = ( |
| 82 | 'Issues with Restrict labels are not allowed to be moved.') |
| 83 | errors.move_to_project = '' |
| 84 | return None |
| 85 | |
| 86 | return move_to_project |
| 87 | |
| 88 | |
| 89 | def _ComputeBackToListURL(mr, issue, config, hotlist, services): |
| 90 | """Construct a URL to return the user to the place that they came from.""" |
| 91 | if hotlist: |
| 92 | back_to_list_url = hotlist_helpers.GetURLOfHotlist( |
| 93 | mr.cnxn, hotlist, services.user) |
| 94 | else: |
| 95 | back_to_list_url = tracker_helpers.FormatIssueListURL( |
| 96 | mr, config, cursor='%s:%d' % (issue.project_name, issue.local_id)) |
| 97 | |
| 98 | return back_to_list_url |
| 99 | |
| 100 | |
| 101 | class FlipperRedirectBase(servlet.Servlet): |
| 102 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 103 | def get(self): |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 104 | with work_env.WorkEnv(self.mr, self.services) as we: |
| 105 | hotlist_id = self.mr.GetIntParam('hotlist_id') |
| 106 | current_issue = we.GetIssueByLocalID(self.mr.project_id, self.mr.local_id, |
| 107 | use_cache=False) |
| 108 | hotlist = None |
| 109 | if hotlist_id: |
| 110 | try: |
| 111 | hotlist = self.services.features.GetHotlist(self.mr.cnxn, hotlist_id) |
| 112 | except features_svc.NoSuchHotlistException: |
| 113 | pass |
| 114 | |
| 115 | try: |
| 116 | adj_issue = GetAdjacentIssue( |
| 117 | self.mr, we, current_issue, hotlist=hotlist, |
| 118 | next_issue=self.next_handler) |
| 119 | path = '/p/%s%s' % (adj_issue.project_name, urls.ISSUE_DETAIL) |
| 120 | url = framework_helpers.FormatURL( |
| 121 | [(name, self.mr.GetParam(name)) for |
| 122 | name in framework_helpers.RECOGNIZED_PARAMS], |
| 123 | path, id=adj_issue.local_id) |
| 124 | except exceptions.NoSuchIssueException: |
| 125 | config = we.GetProjectConfig(self.mr.project_id) |
| 126 | url = _ComputeBackToListURL(self.mr, current_issue, config, |
| 127 | hotlist, self.services) |
| 128 | self.redirect(url) |
| 129 | |
| 130 | |
| 131 | class FlipperNext(FlipperRedirectBase): |
| 132 | next_handler = True |
| 133 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 134 | def GetFlipperNextRedirectPage(self, **kwargs): |
| 135 | self.next_handler = True |
| 136 | return self.handler(**kwargs) |
Adrià Vilanova Martínez | de94280 | 2022-07-15 14:06:55 +0200 | [diff] [blame] | 137 | |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 138 | |
| 139 | class FlipperPrev(FlipperRedirectBase): |
| 140 | next_handler = False |
| 141 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 142 | def GetFlipperPrevRedirectPage(self, **kwargs): |
| 143 | self.next_handler = False |
| 144 | return self.handler(**kwargs) |
Adrià Vilanova Martínez | de94280 | 2022-07-15 14:06:55 +0200 | [diff] [blame] | 145 | |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 146 | |
| 147 | class FlipperList(servlet.Servlet): |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 148 | |
| 149 | def get(self): |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 150 | with work_env.WorkEnv(self.mr, self.services) as we: |
| 151 | hotlist_id = self.mr.GetIntParam('hotlist_id') |
| 152 | current_issue = we.GetIssueByLocalID(self.mr.project_id, self.mr.local_id, |
| 153 | use_cache=False) |
| 154 | hotlist = None |
| 155 | if hotlist_id: |
| 156 | try: |
| 157 | hotlist = self.services.features.GetHotlist(self.mr.cnxn, hotlist_id) |
| 158 | except features_svc.NoSuchHotlistException: |
| 159 | pass |
| 160 | |
| 161 | config = we.GetProjectConfig(self.mr.project_id) |
| 162 | |
| 163 | if hotlist: |
| 164 | self.mr.ComputeColSpec(hotlist) |
| 165 | else: |
| 166 | self.mr.ComputeColSpec(config) |
| 167 | |
| 168 | url = _ComputeBackToListURL(self.mr, current_issue, config, |
| 169 | hotlist, self.services) |
| 170 | self.redirect(url) |
| 171 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 172 | def GetFlipperList(self, **kwargs): |
| 173 | return self.handler(**kwargs) |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 174 | |
Adrià Vilanova Martínez | de94280 | 2022-07-15 14:06:55 +0200 | [diff] [blame] | 175 | |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 176 | class FlipperIndex(jsonfeed.JsonFeed): |
| 177 | """Return a JSON object of an issue's index in search. |
| 178 | |
| 179 | This is a distinct JSON endpoint because it can be expensive to compute. |
| 180 | """ |
| 181 | CHECK_SECURITY_TOKEN = False |
| 182 | |
| 183 | def HandleRequest(self, mr): |
| 184 | hotlist_id = mr.GetIntParam('hotlist_id') |
| 185 | list_url = None |
| 186 | with work_env.WorkEnv(mr, self.services) as we: |
| 187 | if not _ShouldShowFlipper(mr, self.services): |
| 188 | return {} |
| 189 | issue = we.GetIssueByLocalID(mr.project_id, mr.local_id, use_cache=False) |
| 190 | hotlist = None |
| 191 | |
| 192 | if hotlist_id: |
| 193 | hotlist = self.services.features.GetHotlist(mr.cnxn, hotlist_id) |
| 194 | |
| 195 | if not features_bizobj.IssueIsInHotlist(hotlist, issue.issue_id): |
| 196 | raise exceptions.InvalidHotlistException() |
| 197 | |
| 198 | if not permissions.CanViewHotlist( |
| 199 | mr.auth.effective_ids, mr.perms, hotlist): |
| 200 | raise permissions.PermissionException() |
| 201 | |
| 202 | (prev_iid, cur_index, next_iid, total_count |
| 203 | ) = we.GetIssuePositionInHotlist( |
| 204 | issue, hotlist, mr.can, mr.sort_spec, mr.group_by_spec) |
| 205 | else: |
| 206 | (prev_iid, cur_index, next_iid, total_count |
| 207 | ) = we.FindIssuePositionInSearch(issue) |
| 208 | |
| 209 | config = we.GetProjectConfig(self.mr.project_id) |
| 210 | |
| 211 | if hotlist: |
| 212 | mr.ComputeColSpec(hotlist) |
| 213 | else: |
| 214 | mr.ComputeColSpec(config) |
| 215 | |
| 216 | list_url = _ComputeBackToListURL(mr, issue, config, hotlist, |
| 217 | self.services) |
| 218 | |
| 219 | prev_url = None |
| 220 | next_url = None |
| 221 | |
| 222 | recognized_params = [(name, mr.GetParam(name)) for name in |
| 223 | framework_helpers.RECOGNIZED_PARAMS] |
| 224 | if prev_iid: |
| 225 | prev_issue = we.services.issue.GetIssue(mr.cnxn, prev_iid) |
| 226 | path = '/p/%s%s' % (prev_issue.project_name, urls.ISSUE_DETAIL) |
| 227 | prev_url = framework_helpers.FormatURL( |
| 228 | recognized_params, path, id=prev_issue.local_id) |
| 229 | |
| 230 | if next_iid: |
| 231 | next_issue = we.services.issue.GetIssue(mr.cnxn, next_iid) |
| 232 | path = '/p/%s%s' % (next_issue.project_name, urls.ISSUE_DETAIL) |
| 233 | next_url = framework_helpers.FormatURL( |
| 234 | recognized_params, path, id=next_issue.local_id) |
| 235 | |
| 236 | return { |
| 237 | 'prev_iid': prev_iid, |
| 238 | 'prev_url': prev_url, |
| 239 | 'cur_index': cur_index, |
| 240 | 'next_iid': next_iid, |
| 241 | 'next_url': next_url, |
| 242 | 'list_url': list_url, |
| 243 | 'total_count': total_count, |
| 244 | } |
| 245 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 246 | def GetFlipperIndex(self, **kwargs): |
| 247 | return self.handler(**kwargs) |
Adrià Vilanova Martínez | de94280 | 2022-07-15 14:06:55 +0200 | [diff] [blame] | 248 | |
Adrià Vilanova Martínez | f19ea43 | 2024-01-23 20:20:52 +0100 | [diff] [blame^] | 249 | def PostFlipperIndex(self, **kwargs): |
| 250 | return self.handler(**kwargs) |
Adrià Vilanova Martínez | de94280 | 2022-07-15 14:06:55 +0200 | [diff] [blame] | 251 | |
Copybara | 854996b | 2021-09-07 19:36:02 +0000 | [diff] [blame] | 252 | |
| 253 | def _ShouldShowFlipper(mr, services): |
| 254 | """Return True if we should show the flipper.""" |
| 255 | |
| 256 | # Check if the user entered a specific issue ID of an existing issue. |
| 257 | if tracker_constants.JUMP_RE.match(mr.query): |
| 258 | return False |
| 259 | |
| 260 | # Check if the user came directly to an issue without specifying any |
| 261 | # query or sort. E.g., through crbug.com. Generating the issue ref |
| 262 | # list can be too expensive in projects that have a large number of |
| 263 | # issues. The all and open issues cans are broad queries, other |
| 264 | # canned queries should be narrow enough to not need this special |
| 265 | # treatment. |
| 266 | if (not mr.query and not mr.sort_spec and |
| 267 | mr.can in [tracker_constants.ALL_ISSUES_CAN, |
| 268 | tracker_constants.OPEN_ISSUES_CAN]): |
| 269 | num_issues_in_project = services.issue.GetHighestLocalID( |
| 270 | mr.cnxn, mr.project_id) |
| 271 | if num_issues_in_project > settings.threshold_to_suppress_prev_next: |
| 272 | return False |
| 273 | |
| 274 | return True |
| 275 | |
| 276 | |
| 277 | def GetAdjacentIssue( |
| 278 | mr, we, issue, hotlist=None, next_issue=False): |
| 279 | """Compute next or previous issue given params of current issue. |
| 280 | |
| 281 | Args: |
| 282 | mr: MonorailRequest, including can and sorting/grouping order. |
| 283 | we: A WorkEnv instance. |
| 284 | issue: The current issue (from which to compute prev/next). |
| 285 | hotlist (optional): The current hotlist. |
| 286 | next_issue (bool): If True, return next, issue, else return previous issue. |
| 287 | |
| 288 | Returns: |
| 289 | The adjacent issue. |
| 290 | |
| 291 | Raises: |
| 292 | NoSuchIssueException when there is no adjacent issue in the list. |
| 293 | """ |
| 294 | if hotlist: |
| 295 | (prev_iid, _cur_index, next_iid, _total_count |
| 296 | ) = we.GetIssuePositionInHotlist( |
| 297 | issue, hotlist, mr.can, mr.sort_spec, mr.group_by_spec) |
| 298 | else: |
| 299 | (prev_iid, _cur_index, next_iid, _total_count |
| 300 | ) = we.FindIssuePositionInSearch(issue) |
| 301 | iid = next_iid if next_issue else prev_iid |
| 302 | if iid is None: |
| 303 | raise exceptions.NoSuchIssueException() |
| 304 | return we.GetIssue(iid) |