blob: 3a1044335511f25ec439ee89c1ec318d0c029ba7 [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
Copybara854996b2021-09-07 19:36:02 +000017import json
18import logging
19import time
20import ezt
21
22import settings
23from api import converters
24from businesslogic import work_env
25from features import features_bizobj
26from features import send_notifications
27from features import hotlist_helpers
28from features import hotlist_views
29from framework import exceptions
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +020030from framework import flaskservlet
Copybara854996b2021-09-07 19:36:02 +000031from 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
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200162 # def GetFlipperNextRedirectPage(self, **kwargs):
163 # self.next_handler = True
164 # return self.handler(**kwargs)
165
Copybara854996b2021-09-07 19:36:02 +0000166
167class FlipperPrev(FlipperRedirectBase):
168 next_handler = False
169
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200170 # def GetFlipperPrevRedirectPage(self, **kwargs):
171 # self.next_handler = False
172 # return self.handler(**kwargs)
173
Copybara854996b2021-09-07 19:36:02 +0000174
175class FlipperList(servlet.Servlet):
176 # pylint: disable=arguments-differ
177 # pylint: disable=unused-argument
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200178 # TODO: (monorail:6511)change to get(self) when convert to flask
Copybara854996b2021-09-07 19:36:02 +0000179 def get(self, project_name=None, viewed_username=None, hotlist_id=None):
180 with work_env.WorkEnv(self.mr, self.services) as we:
181 hotlist_id = self.mr.GetIntParam('hotlist_id')
182 current_issue = we.GetIssueByLocalID(self.mr.project_id, self.mr.local_id,
183 use_cache=False)
184 hotlist = None
185 if hotlist_id:
186 try:
187 hotlist = self.services.features.GetHotlist(self.mr.cnxn, hotlist_id)
188 except features_svc.NoSuchHotlistException:
189 pass
190
191 config = we.GetProjectConfig(self.mr.project_id)
192
193 if hotlist:
194 self.mr.ComputeColSpec(hotlist)
195 else:
196 self.mr.ComputeColSpec(config)
197
198 url = _ComputeBackToListURL(self.mr, current_issue, config,
199 hotlist, self.services)
200 self.redirect(url)
201
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200202 # def GetFlipperList(self, **kwargs):
203 # return self.handler(**kwargs)
Copybara854996b2021-09-07 19:36:02 +0000204
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200205
206# TODO: (monorail:6511) change to flaskJsonFeed when convert to flask
Copybara854996b2021-09-07 19:36:02 +0000207class FlipperIndex(jsonfeed.JsonFeed):
208 """Return a JSON object of an issue's index in search.
209
210 This is a distinct JSON endpoint because it can be expensive to compute.
211 """
212 CHECK_SECURITY_TOKEN = False
213
214 def HandleRequest(self, mr):
215 hotlist_id = mr.GetIntParam('hotlist_id')
216 list_url = None
217 with work_env.WorkEnv(mr, self.services) as we:
218 if not _ShouldShowFlipper(mr, self.services):
219 return {}
220 issue = we.GetIssueByLocalID(mr.project_id, mr.local_id, use_cache=False)
221 hotlist = None
222
223 if hotlist_id:
224 hotlist = self.services.features.GetHotlist(mr.cnxn, hotlist_id)
225
226 if not features_bizobj.IssueIsInHotlist(hotlist, issue.issue_id):
227 raise exceptions.InvalidHotlistException()
228
229 if not permissions.CanViewHotlist(
230 mr.auth.effective_ids, mr.perms, hotlist):
231 raise permissions.PermissionException()
232
233 (prev_iid, cur_index, next_iid, total_count
234 ) = we.GetIssuePositionInHotlist(
235 issue, hotlist, mr.can, mr.sort_spec, mr.group_by_spec)
236 else:
237 (prev_iid, cur_index, next_iid, total_count
238 ) = we.FindIssuePositionInSearch(issue)
239
240 config = we.GetProjectConfig(self.mr.project_id)
241
242 if hotlist:
243 mr.ComputeColSpec(hotlist)
244 else:
245 mr.ComputeColSpec(config)
246
247 list_url = _ComputeBackToListURL(mr, issue, config, hotlist,
248 self.services)
249
250 prev_url = None
251 next_url = None
252
253 recognized_params = [(name, mr.GetParam(name)) for name in
254 framework_helpers.RECOGNIZED_PARAMS]
255 if prev_iid:
256 prev_issue = we.services.issue.GetIssue(mr.cnxn, prev_iid)
257 path = '/p/%s%s' % (prev_issue.project_name, urls.ISSUE_DETAIL)
258 prev_url = framework_helpers.FormatURL(
259 recognized_params, path, id=prev_issue.local_id)
260
261 if next_iid:
262 next_issue = we.services.issue.GetIssue(mr.cnxn, next_iid)
263 path = '/p/%s%s' % (next_issue.project_name, urls.ISSUE_DETAIL)
264 next_url = framework_helpers.FormatURL(
265 recognized_params, path, id=next_issue.local_id)
266
267 return {
268 'prev_iid': prev_iid,
269 'prev_url': prev_url,
270 'cur_index': cur_index,
271 'next_iid': next_iid,
272 'next_url': next_url,
273 'list_url': list_url,
274 'total_count': total_count,
275 }
276
Adrià Vilanova Martínezde942802022-07-15 14:06:55 +0200277 # def GetFlipperIndex(self, **kwargs):
278 # return self.handler(**kwargs)
279
280 # def PostFlipperIndex(self, **kwargs):
281 # return self.handler(**kwargs)
282
Copybara854996b2021-09-07 19:36:02 +0000283
284def _ShouldShowFlipper(mr, services):
285 """Return True if we should show the flipper."""
286
287 # Check if the user entered a specific issue ID of an existing issue.
288 if tracker_constants.JUMP_RE.match(mr.query):
289 return False
290
291 # Check if the user came directly to an issue without specifying any
292 # query or sort. E.g., through crbug.com. Generating the issue ref
293 # list can be too expensive in projects that have a large number of
294 # issues. The all and open issues cans are broad queries, other
295 # canned queries should be narrow enough to not need this special
296 # treatment.
297 if (not mr.query and not mr.sort_spec and
298 mr.can in [tracker_constants.ALL_ISSUES_CAN,
299 tracker_constants.OPEN_ISSUES_CAN]):
300 num_issues_in_project = services.issue.GetHighestLocalID(
301 mr.cnxn, mr.project_id)
302 if num_issues_in_project > settings.threshold_to_suppress_prev_next:
303 return False
304
305 return True
306
307
308def GetAdjacentIssue(
309 mr, we, issue, hotlist=None, next_issue=False):
310 """Compute next or previous issue given params of current issue.
311
312 Args:
313 mr: MonorailRequest, including can and sorting/grouping order.
314 we: A WorkEnv instance.
315 issue: The current issue (from which to compute prev/next).
316 hotlist (optional): The current hotlist.
317 next_issue (bool): If True, return next, issue, else return previous issue.
318
319 Returns:
320 The adjacent issue.
321
322 Raises:
323 NoSuchIssueException when there is no adjacent issue in the list.
324 """
325 if hotlist:
326 (prev_iid, _cur_index, next_iid, _total_count
327 ) = we.GetIssuePositionInHotlist(
328 issue, hotlist, mr.can, mr.sort_spec, mr.group_by_spec)
329 else:
330 (prev_iid, _cur_index, next_iid, _total_count
331 ) = we.FindIssuePositionInSearch(issue)
332 iid = next_iid if next_issue else prev_iid
333 if iid is None:
334 raise exceptions.NoSuchIssueException()
335 return we.GetIssue(iid)