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