blob: 233ea5778e49c2adb8cb3b311e7a69f3dc08887d [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# 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.
Copybara854996b2021-09-07 19:36:02 +00004
5"""Classes that implement the issue detail page and related forms.
6
7Summary 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"""
12from __future__ import print_function
13from __future__ import division
14from __future__ import absolute_import
15
Copybara854996b2021-09-07 19:36:02 +000016import settings
Copybara854996b2021-09-07 19:36:02 +000017from businesslogic import work_env
18from features import features_bizobj
Copybara854996b2021-09-07 19:36:02 +000019from features import hotlist_helpers
Copybara854996b2021-09-07 19:36:02 +000020from framework import exceptions
Copybara854996b2021-09-07 19:36:02 +000021from framework import framework_helpers
Copybara854996b2021-09-07 19:36:02 +000022from framework import jsonfeed
Copybara854996b2021-09-07 19:36:02 +000023from framework import permissions
24from framework import servlet
25from framework import servlet_helpers
Copybara854996b2021-09-07 19:36:02 +000026from framework import urls
Copybara854996b2021-09-07 19:36:02 +000027from services import features_svc
Copybara854996b2021-09-07 19:36:02 +000028from tracker import tracker_constants
29from tracker import tracker_helpers
Copybara854996b2021-09-07 19:36:02 +000030
31
32def 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
89def _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
101class FlipperRedirectBase(servlet.Servlet):
102
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100103 def get(self):
Copybara854996b2021-09-07 19:36:02 +0000104 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
131class FlipperNext(FlipperRedirectBase):
132 next_handler = True
133
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100134 def GetFlipperNextRedirectPage(self, **kwargs):
135 self.next_handler = True
136 return self.handler(**kwargs)
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200137
Copybara854996b2021-09-07 19:36:02 +0000138
139class FlipperPrev(FlipperRedirectBase):
140 next_handler = False
141
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100142 def GetFlipperPrevRedirectPage(self, **kwargs):
143 self.next_handler = False
144 return self.handler(**kwargs)
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200145
Copybara854996b2021-09-07 19:36:02 +0000146
147class FlipperList(servlet.Servlet):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100148
149 def get(self):
Copybara854996b2021-09-07 19:36:02 +0000150 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ínezf19ea432024-01-23 20:20:52 +0100172 def GetFlipperList(self, **kwargs):
173 return self.handler(**kwargs)
Copybara854996b2021-09-07 19:36:02 +0000174
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200175
Copybara854996b2021-09-07 19:36:02 +0000176class 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ínezf19ea432024-01-23 20:20:52 +0100246 def GetFlipperIndex(self, **kwargs):
247 return self.handler(**kwargs)
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200248
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100249 def PostFlipperIndex(self, **kwargs):
250 return self.handler(**kwargs)
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200251
Copybara854996b2021-09-07 19:36:02 +0000252
253def _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
277def 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)