blob: 9b6de137204b4b3d1c7c63366861e5790fc8627c [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# Copyright 2018 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
5from __future__ import print_function
6from __future__ import division
7from __future__ import absolute_import
8
9import copy
10import logging
11
12from google.protobuf import empty_pb2
13
14import settings
15from api import monorail_servicer
16from api import converters
17from api.api_proto import issue_objects_pb2
18from api.api_proto import issues_pb2
19from api.api_proto import issues_prpc_pb2
20from businesslogic import work_env
21from features import filterrules_helpers
22from features import savedqueries_helpers
23from framework import exceptions
24from framework import framework_constants
25from framework import framework_views
26from framework import permissions
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010027from framework import sorting
28from mrproto import tracker_pb2
Copybara854996b2021-09-07 19:36:02 +000029from search import searchpipeline
30from tracker import field_helpers
31from tracker import tracker_bizobj
32from tracker import tracker_helpers
33
34
35class IssuesServicer(monorail_servicer.MonorailServicer):
36 """Handle API requests related to Issue objects.
37
38 Each API request is implemented with a method as defined in the
39 .proto file that does any request-specific validation, uses work_env
40 to safely operate on business objects, and returns a response proto.
41 """
42
43 DESCRIPTION = issues_prpc_pb2.IssuesServiceDescription
44
45 def _GetProjectIssueAndConfig(
46 self, mc, issue_ref, use_cache=True, issue_required=True,
47 view_deleted=False):
48 """Get three objects that we need for most requests with an issue_ref."""
49 issue = None
50 with work_env.WorkEnv(mc, self.services, phase='getting P, I, C') as we:
51 project = we.GetProjectByName(
52 issue_ref.project_name, use_cache=use_cache)
53 mc.LookupLoggedInUserPerms(project)
54 config = we.GetProjectConfig(project.project_id, use_cache=use_cache)
55 if issue_required or issue_ref.local_id:
56 try:
57 issue = we.GetIssueByLocalID(
58 project.project_id, issue_ref.local_id, use_cache=use_cache,
59 allow_viewing_deleted=view_deleted)
60 except exceptions.NoSuchIssueException as e:
61 issue = None
62 if issue_required:
63 raise e
64 return project, issue, config
65
66 def _GetProjectIssueIDsAndConfig(
67 self, mc, issue_refs, use_cache=True):
68 """Get info from a single project for repeated issue_refs requests."""
69 project_names = set()
70 local_ids = []
71 for issue_ref in issue_refs:
72 if not issue_ref.local_id:
73 raise exceptions.InputException('Param `local_id` required.')
74 local_ids.append(issue_ref.local_id)
75 if issue_ref.project_name:
76 project_names.add(issue_ref.project_name)
77
78 if not project_names:
79 raise exceptions.InputException('Param `project_name` required.')
80 if len(project_names) != 1:
81 raise exceptions.InputException(
82 'This method does not support cross-project issue_refs.')
83 project_name = project_names.pop()
84 with work_env.WorkEnv(mc, self.services, phase='getting P, I ids, C') as we:
85 project = we.GetProjectByName(project_name, use_cache=use_cache)
86 mc.LookupLoggedInUserPerms(project)
87 config = we.GetProjectConfig(project.project_id, use_cache=use_cache)
88 project_local_id_pairs = [(project.project_id, local_id)
89 for local_id in local_ids]
90 issue_ids, _misses = self.services.issue.LookupIssueIDs(
91 mc.cnxn, project_local_id_pairs)
92 return project, issue_ids, config
93
94 @monorail_servicer.PRPCMethod
95 def CreateIssue(self, _mc, request):
96 response = issue_objects_pb2.Issue()
97 response.CopyFrom(request.issue)
98 return response
99
100 @monorail_servicer.PRPCMethod
101 def GetIssue(self, mc, request):
102 """Return the specified issue in a response proto."""
103 issue_ref = request.issue_ref
104 project, issue, config = self._GetProjectIssueAndConfig(
105 mc, issue_ref, view_deleted=True, issue_required=False)
106
107 # Code for getting where a moved issue was moved to.
108 if issue is None:
109 moved_to_ref = self.services.issue.GetCurrentLocationOfMovedIssue(
110 mc.cnxn, project.project_id, issue_ref.local_id)
111 moved_to_project_id, moved_to_id = moved_to_ref
112 moved_to_project_name = None
113
114 if moved_to_project_id is not None:
115 with work_env.WorkEnv(mc, self.services) as we:
116 moved_to_project = we.GetProject(moved_to_project_id)
117 moved_to_project_name = moved_to_project.project_name
118 return issues_pb2.IssueResponse(moved_to_ref=converters.ConvertIssueRef(
119 (moved_to_project_name, moved_to_id)))
120
121 raise exceptions.NoSuchIssueException()
122
123 if issue.deleted:
124 return issues_pb2.IssueResponse(
125 issue=issue_objects_pb2.Issue(is_deleted=True))
126
127 with work_env.WorkEnv(mc, self.services) as we:
128 related_refs = we.GetRelatedIssueRefs([issue])
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100129 migrated_id = we.GetIssueMigratedID(
130 issue_ref.project_name, issue_ref.local_id)
Copybara854996b2021-09-07 19:36:02 +0000131 with mc.profiler.Phase('making user views'):
132 users_involved_in_issue = tracker_bizobj.UsersInvolvedInIssues([issue])
133 users_by_id = framework_views.MakeAllUserViews(
134 mc.cnxn, self.services.user, users_involved_in_issue)
135 framework_views.RevealAllEmailsToMembers(
136 mc.cnxn, self.services, mc.auth, users_by_id, project)
137
138 with mc.profiler.Phase('converting to response objects'):
139 response = issues_pb2.IssueResponse()
140 response.issue.CopyFrom(converters.ConvertIssue(
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100141 issue, users_by_id, related_refs, config, migrated_id))
Copybara854996b2021-09-07 19:36:02 +0000142
143 return response
144
145 @monorail_servicer.PRPCMethod
146 def ListIssues(self, mc, request):
147 """Return the list of issues for projects that satisfy the given query."""
148 use_cached_searches = not settings.local_mode
149 can = request.canned_query or 1
150 with work_env.WorkEnv(mc, self.services) as we:
151 start, max_items = converters.IngestPagination(request.pagination)
152 pipeline = we.ListIssues(
153 request.query, request.project_names, mc.auth.user_id, max_items,
154 start, can, request.group_by_spec, request.sort_spec,
155 use_cached_searches)
156 with mc.profiler.Phase('reveal emails to members'):
157 projects = self.services.project.GetProjectsByName(
158 mc.cnxn, request.project_names)
159 for _, p in projects.items():
160 framework_views.RevealAllEmailsToMembers(
161 mc.cnxn, self.services, mc.auth, pipeline.users_by_id, p)
162
163 converted_results = []
164 with work_env.WorkEnv(mc, self.services) as we:
165 for issue in (pipeline.visible_results or []):
166 related_refs = we.GetRelatedIssueRefs([issue])
167 converted_results.append(
168 converters.ConvertIssue(issue, pipeline.users_by_id, related_refs,
169 pipeline.harmonized_config))
170 total_results = 0
171 if hasattr(pipeline.pagination, 'total_count'):
172 total_results = pipeline.pagination.total_count
173 return issues_pb2.ListIssuesResponse(
174 issues=converted_results, total_results=total_results)
175
176
177 @monorail_servicer.PRPCMethod
178 def ListReferencedIssues(self, mc, request):
179 """Return the specified issues in a response proto."""
180 if not request.issue_refs:
181 return issues_pb2.ListReferencedIssuesResponse()
182
183 for issue_ref in request.issue_refs:
184 if not issue_ref.project_name:
185 raise exceptions.InputException('Param `project_name` required.')
186 if not issue_ref.local_id:
187 raise exceptions.InputException('Param `local_id` required.')
188
189 default_project_name = request.issue_refs[0].project_name
190 ref_tuples = [
191 (ref.project_name, ref.local_id) for ref in request.issue_refs]
192 with work_env.WorkEnv(mc, self.services) as we:
193 open_issues, closed_issues = we.ListReferencedIssues(
194 ref_tuples, default_project_name)
195 all_issues = open_issues + closed_issues
196 all_project_ids = [issue.project_id for issue in all_issues]
197 related_refs = we.GetRelatedIssueRefs(all_issues)
198 configs = we.GetProjectConfigs(all_project_ids)
199
200 with mc.profiler.Phase('making user views'):
201 users_involved = tracker_bizobj.UsersInvolvedInIssues(all_issues)
202 users_by_id = framework_views.MakeAllUserViews(
203 mc.cnxn, self.services.user, users_involved)
204 framework_views.RevealAllEmailsToMembers(
205 mc.cnxn, self.services, mc.auth, users_by_id)
206
207 with mc.profiler.Phase('converting to response objects'):
208 converted_open_issues = [
209 converters.ConvertIssue(
210 issue, users_by_id, related_refs, configs[issue.project_id])
211 for issue in open_issues]
212 converted_closed_issues = [
213 converters.ConvertIssue(
214 issue, users_by_id, related_refs, configs[issue.project_id])
215 for issue in closed_issues]
216 response = issues_pb2.ListReferencedIssuesResponse(
217 open_refs=converted_open_issues, closed_refs=converted_closed_issues)
218
219 return response
220
221 @monorail_servicer.PRPCMethod
222 def ListApplicableFieldDefs(self, mc, request):
223 """Returns specified issues' applicable field refs in a response proto."""
224 if not request.issue_refs:
225 return issues_pb2.ListApplicableFieldDefsResponse()
226
227 _project, issue_ids, config = self._GetProjectIssueIDsAndConfig(
228 mc, request.issue_refs)
229 with work_env.WorkEnv(mc, self.services) as we:
230 issues_dict = we.GetIssuesDict(issue_ids)
231 fds = field_helpers.ListApplicableFieldDefs(issues_dict.values(), config)
232
233 users_by_id = {}
234 with mc.profiler.Phase('converting to response objects'):
235 users_involved = tracker_bizobj.UsersInvolvedInConfig(config)
236 users_by_id.update(framework_views.MakeAllUserViews(
237 mc.cnxn, self.services.user, users_involved))
238 field_defs = [
239 converters.ConvertFieldDef(fd, [], users_by_id, config, True)
240 for fd in fds]
241
242 return issues_pb2.ListApplicableFieldDefsResponse(field_defs=field_defs)
243
244 @monorail_servicer.PRPCMethod
245 def UpdateIssue(self, mc, request):
246 """Apply a delta and comment to the specified issue, then return it."""
247 project, issue, config = self._GetProjectIssueAndConfig(
248 mc, request.issue_ref, use_cache=False)
249
250 with work_env.WorkEnv(mc, self.services) as we:
251 if request.HasField('delta'):
252 delta = converters.IngestIssueDelta(
253 mc.cnxn, self.services, request.delta, config, issue.phases)
254 else:
255 delta = tracker_pb2.IssueDelta() # No changes specified.
256 attachments = converters.IngestAttachmentUploads(request.uploads)
257 we.UpdateIssue(
258 issue, delta, request.comment_content, send_email=request.send_email,
259 attachments=attachments, is_description=request.is_description,
260 kept_attachments=list(request.kept_attachments))
261 related_refs = we.GetRelatedIssueRefs([issue])
262
263 with mc.profiler.Phase('making user views'):
264 users_involved_in_issue = tracker_bizobj.UsersInvolvedInIssues([issue])
265 users_by_id = framework_views.MakeAllUserViews(
266 mc.cnxn, self.services.user, users_involved_in_issue)
267 framework_views.RevealAllEmailsToMembers(
268 mc.cnxn, self.services, mc.auth, users_by_id, project)
269
270 with mc.profiler.Phase('converting to response objects'):
271 response = issues_pb2.IssueResponse()
272 response.issue.CopyFrom(converters.ConvertIssue(
273 issue, users_by_id, related_refs, config))
274
275 return response
276
277 @monorail_servicer.PRPCMethod
278 def StarIssue(self, mc, request):
279 """Star (or unstar) the specified issue."""
280 _project, issue, _config = self._GetProjectIssueAndConfig(
281 mc, request.issue_ref, use_cache=False)
282
283 with work_env.WorkEnv(mc, self.services) as we:
284 we.StarIssue(issue, request.starred)
285 # Reload the issue to get the new star count.
286 issue = we.GetIssue(issue.issue_id)
287
288 with mc.profiler.Phase('converting to response objects'):
289 response = issues_pb2.StarIssueResponse()
290 response.star_count = issue.star_count
291
292 return response
293
294 @monorail_servicer.PRPCMethod
295 def IsIssueStarred(self, mc, request):
296 """Respond true if the signed-in user has starred the specified issue."""
297 _project, issue, _config = self._GetProjectIssueAndConfig(
298 mc, request.issue_ref, use_cache=False)
299
300 with work_env.WorkEnv(mc, self.services) as we:
301 is_starred = we.IsIssueStarred(issue)
302
303 with mc.profiler.Phase('converting to response objects'):
304 response = issues_pb2.IsIssueStarredResponse()
305 response.is_starred = is_starred
306
307 return response
308
309 @monorail_servicer.PRPCMethod
310 def ListStarredIssues(self, mc, _request):
311 """Return a list of issue ids that the signed-in user has starred."""
312 with work_env.WorkEnv(mc, self.services) as we:
313 starred_issues = we.ListStarredIssueIDs()
314 starred_issues_dict = we.GetIssueRefs(starred_issues)
315
316 with mc.profiler.Phase('converting to response objects'):
317 converted_starred_issue_refs = converters.ConvertIssueRefs(
318 starred_issues, starred_issues_dict)
319 response = issues_pb2.ListStarredIssuesResponse(
320 starred_issue_refs=converted_starred_issue_refs)
321
322 return response
323
324 @monorail_servicer.PRPCMethod
325 def ListComments(self, mc, request):
326 """Return comments on the specified issue in a response proto."""
327 project, issue, config = self._GetProjectIssueAndConfig(
328 mc, request.issue_ref)
329 with work_env.WorkEnv(mc, self.services) as we:
330 comments = we.ListIssueComments(issue)
331 _, comment_reporters = we.LookupIssueFlaggers(issue)
332
333 with mc.profiler.Phase('making user views'):
334 users_involved_in_comments = tracker_bizobj.UsersInvolvedInCommentList(
335 comments)
336 users_by_id = framework_views.MakeAllUserViews(
337 mc.cnxn, self.services.user, users_involved_in_comments)
338 framework_views.RevealAllEmailsToMembers(
339 mc.cnxn, self.services, mc.auth, users_by_id, project)
340
341 with mc.profiler.Phase('converting to response objects'):
342 issue_perms = permissions.UpdateIssuePermissions(
343 mc.perms, project, issue, mc.auth.effective_ids, config=config)
344 converted_comments = converters.ConvertCommentList(
345 issue, comments, config, users_by_id, comment_reporters,
346 mc.auth.user_id, issue_perms)
347 response = issues_pb2.ListCommentsResponse(comments=converted_comments)
348
349 return response
350
351 @monorail_servicer.PRPCMethod
352 def ListActivities(self, mc, request):
353 """Return issue activities by a specified user in a response proto."""
354 converted_user = converters.IngestUserRef(mc.cnxn, request.user_ref,
355 self.services.user)
356 user = self.services.user.GetUser(mc.cnxn, converted_user)
357 comments = self.services.issue.GetIssueActivity(
358 mc.cnxn, user_ids={request.user_ref.user_id})
359 issues = self.services.issue.GetIssues(
360 mc.cnxn, {c.issue_id for c in comments})
361 project_dict = tracker_helpers.GetAllIssueProjects(
362 mc.cnxn, issues, self.services.project)
363 config_dict = self.services.config.GetProjectConfigs(
364 mc.cnxn, list(project_dict.keys()))
365 allowed_issues = tracker_helpers.FilterOutNonViewableIssues(
366 mc.auth.effective_ids, user, project_dict,
367 config_dict, issues)
368 issue_dict = {issue.issue_id: issue for issue in allowed_issues}
369 comments = [
370 c for c in comments if c.issue_id in issue_dict]
371
372 users_by_id = framework_views.MakeAllUserViews(
373 mc.cnxn, self.services.user, [request.user_ref.user_id],
374 tracker_bizobj.UsersInvolvedInCommentList(comments))
375 for project in project_dict.values():
376 framework_views.RevealAllEmailsToMembers(
377 mc.cnxn, self.services, mc.auth, users_by_id, project)
378
379 issues_by_project = {}
380 for issue in allowed_issues:
381 issues_by_project.setdefault(issue.project_id, []).append(issue)
382
383 # A dictionary {issue_id: perms} of the PermissionSet for the current user
384 # on each of the issues.
385 issue_perms_dict = {}
386 # A dictionary {comment_id: [reporter_id]} of users who have reported the
387 # comment as spam.
388 comment_reporters = {}
389 for project_id, project_issues in issues_by_project.items():
390 mc.LookupLoggedInUserPerms(project_dict[project_id])
391 issue_perms_dict.update({
392 issue.issue_id: permissions.UpdateIssuePermissions(
393 mc.perms, project_dict[issue.project_id], issue,
394 mc.auth.effective_ids, config=config_dict[issue.project_id])
395 for issue in project_issues})
396
397 with work_env.WorkEnv(mc, self.services) as we:
398 project_issue_reporters = we.LookupIssuesFlaggers(project_issues)
399 for _, issue_comment_reporters in project_issue_reporters.values():
400 comment_reporters.update(issue_comment_reporters)
401
402 with mc.profiler.Phase('converting to response objects'):
403 converted_comments = []
404 for c in comments:
405 issue = issue_dict.get(c.issue_id)
406 issue_perms = issue_perms_dict.get(c.issue_id)
407 result = converters.ConvertComment(
408 issue, c,
409 config_dict.get(issue.project_id),
410 users_by_id,
411 comment_reporters.get(c.id, []),
412 {c.id: 1} if c.is_description else {},
413 mc.auth.user_id, issue_perms)
414 converted_comments.append(result)
415 converted_issues = [issue_objects_pb2.IssueSummary(
416 project_name=issue.project_name, local_id=issue.local_id,
417 summary=issue.summary) for issue in allowed_issues]
418 response = issues_pb2.ListActivitiesResponse(
419 comments=converted_comments, issue_summaries=converted_issues)
420
421 return response
422
423 @monorail_servicer.PRPCMethod
424 def DeleteComment(self, mc, request):
425 _project, issue, _config = self._GetProjectIssueAndConfig(
426 mc, request.issue_ref, use_cache=False)
427 with work_env.WorkEnv(mc, self.services) as we:
428 all_comments = we.ListIssueComments(issue)
429 try:
430 comment = all_comments[request.sequence_num]
431 except IndexError:
432 raise exceptions.NoSuchCommentException()
433 we.DeleteComment(issue, comment, request.delete)
434
435 return empty_pb2.Empty()
436
437 @monorail_servicer.PRPCMethod
438 def BulkUpdateApprovals(self, mc, request):
439 """Update multiple issues' approval and return the updated issue_refs."""
440 if not request.issue_refs:
441 raise exceptions.InputException('Param `issue_refs` empty.')
442
443 project, issue_ids, config = self._GetProjectIssueIDsAndConfig(
444 mc, request.issue_refs)
445
446 approval_fd = tracker_bizobj.FindFieldDef(
447 request.field_ref.field_name, config)
448 if not approval_fd:
449 raise exceptions.NoSuchFieldDefException()
450 if request.HasField('approval_delta'):
451 approval_delta = converters.IngestApprovalDelta(
452 mc.cnxn, self.services.user, request.approval_delta,
453 mc.auth.user_id, config)
454 else:
455 approval_delta = tracker_pb2.ApprovalDelta()
456 # No bulk adding approval attachments for now.
457
458 with work_env.WorkEnv(mc, self.services, phase='updating approvals') as we:
459 updated_issue_ids = we.BulkUpdateIssueApprovals(
460 issue_ids, approval_fd.field_id, project, approval_delta,
461 request.comment_content, send_email=request.send_email)
462 with mc.profiler.Phase('converting to response objects'):
463 issue_ref_pairs = we.GetIssueRefs(updated_issue_ids)
464 issue_refs = [converters.ConvertIssueRef(pair)
465 for pair in issue_ref_pairs.values()]
466 response = issues_pb2.BulkUpdateApprovalsResponse(issue_refs=issue_refs)
467
468 return response
469
470 @monorail_servicer.PRPCMethod
471 def UpdateApproval(self, mc, request):
472 """Update and return an approval in a response proto."""
473 project, issue, config = self._GetProjectIssueAndConfig(
474 mc, request.issue_ref, use_cache=False)
475
476 approval_fd = tracker_bizobj.FindFieldDef(
477 request.field_ref.field_name, config)
478 if not approval_fd:
479 raise exceptions.NoSuchFieldDefException()
480 if request.HasField('approval_delta'):
481 approval_delta = converters.IngestApprovalDelta(
482 mc.cnxn, self.services.user, request.approval_delta,
483 mc.auth.user_id, config)
484 else:
485 approval_delta = tracker_pb2.ApprovalDelta()
486 attachments = converters.IngestAttachmentUploads(request.uploads)
487
488 with work_env.WorkEnv(mc, self.services) as we:
489 av, _comment, _issue = we.UpdateIssueApproval(
490 issue.issue_id,
491 approval_fd.field_id,
492 approval_delta,
493 request.comment_content,
494 request.is_description,
495 attachments=attachments,
496 send_email=request.send_email,
497 kept_attachments=list(request.kept_attachments))
498
499 with mc.profiler.Phase('converting to response objects'):
500 users_by_id = framework_views.MakeAllUserViews(
501 mc.cnxn, self.services.user, av.approver_ids, [av.setter_id])
502 framework_views.RevealAllEmailsToMembers(
503 mc.cnxn, self.services, mc.auth, users_by_id, project)
504 response = issues_pb2.UpdateApprovalResponse()
505 response.approval.CopyFrom(converters.ConvertApproval(
506 av, users_by_id, config))
507
508 return response
509
510 @monorail_servicer.PRPCMethod
511 def ConvertIssueApprovalsTemplate(self, mc, request):
512 """Update an issue's existing approvals structure to match the one of the
513 given template."""
514
515 if not request.issue_ref.local_id or not request.issue_ref.project_name:
516 raise exceptions.InputException('Param `issue_ref.local_id` empty')
517 if not request.template_name:
518 raise exceptions.InputException('Param `template_name` empty')
519
520 project, issue, config = self._GetProjectIssueAndConfig(
521 mc, request.issue_ref, use_cache=False)
522
523 with work_env.WorkEnv(mc, self.services) as we:
524 we.ConvertIssueApprovalsTemplate(
525 config, issue, request.template_name, request.comment_content,
526 send_email=request.send_email)
527 related_refs = we.GetRelatedIssueRefs([issue])
528
529 with mc.profiler.Phase('making user views'):
530 users_involved_in_issue = tracker_bizobj.UsersInvolvedInIssues([issue])
531 users_by_id = framework_views.MakeAllUserViews(
532 mc.cnxn, self.services.user, users_involved_in_issue)
533 framework_views.RevealAllEmailsToMembers(
534 mc.cnxn, self.services, mc.auth, users_by_id, project)
535
536 with mc.profiler.Phase('converting to response objects'):
537 response = issues_pb2.ConvertIssueApprovalsTemplateResponse()
538 response.issue.CopyFrom(converters.ConvertIssue(
539 issue, users_by_id, related_refs, config))
540 return response
541
542 @monorail_servicer.PRPCMethod
543 def IssueSnapshot(self, mc, request):
544 """Fetch IssueSnapshot counts for charting."""
545 warnings = []
546
547 if not request.timestamp:
548 raise exceptions.InputException('Param `timestamp` required.')
549
550 if not request.project_name and not request.hotlist_id:
551 raise exceptions.InputException('Params `project_name` or `hotlist_id` '
552 'required.')
553
554 if request.group_by == 'label' and not request.label_prefix:
555 raise exceptions.InputException('Param `label_prefix` required.')
556
557 if request.canned_query:
558 canned_query = savedqueries_helpers.SavedQueryIDToCond(
559 mc.cnxn, self.services.features, request.canned_query)
560 # TODO(jrobbins): support linked accounts me_user_ids.
561 canned_query, warnings = searchpipeline.ReplaceKeywordsWithUserIDs(
562 [mc.auth.user_id], canned_query)
563 else:
564 canned_query = None
565
566 if request.query:
567 query, warnings = searchpipeline.ReplaceKeywordsWithUserIDs(
568 [mc.auth.user_id], request.query)
569 else:
570 query = None
571
572 with work_env.WorkEnv(mc, self.services) as we:
573 try:
574 project = we.GetProjectByName(request.project_name)
575 except exceptions.NoSuchProjectException:
576 project = None
577
578 if request.hotlist_id:
579 hotlist = we.GetHotlist(request.hotlist_id)
580 else:
581 hotlist = None
582
583 results, unsupported_fields, limit_reached = we.SnapshotCountsQuery(
584 project, request.timestamp, request.group_by,
585 label_prefix=request.label_prefix, query=query,
586 canned_query=canned_query, hotlist=hotlist)
587 if request.group_by == 'owner':
588 # Map user ids to emails.
589 snapshot_counts = [
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100590 issues_pb2.IssueSnapshotCount(
591 dimension=self.services.user.GetUser(mc.cnxn, key).email,
592 count=result)
593 for key, result in sorted(results.items(), key=sorting.Python2Key)
Copybara854996b2021-09-07 19:36:02 +0000594 ]
595 else:
596 snapshot_counts = [
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100597 issues_pb2.IssueSnapshotCount(dimension=key, count=result)
598 for key, result in sorted(results.items(), key=sorting.Python2Key)
Copybara854996b2021-09-07 19:36:02 +0000599 ]
600 response = issues_pb2.IssueSnapshotResponse()
601 response.snapshot_count.extend(snapshot_counts)
602 response.unsupported_field.extend(unsupported_fields)
603 response.unsupported_field.extend(warnings)
604 response.search_limit_reached = limit_reached
605 return response
606
607 @monorail_servicer.PRPCMethod
608 def PresubmitIssue(self, mc, request):
609 """Provide the UI with warnings and suggestions."""
610 project, issue, config = self._GetProjectIssueAndConfig(
611 mc, request.issue_ref, issue_required=False)
612
613 with mc.profiler.Phase('making user views'):
614 try:
615 proposed_owner_id = converters.IngestUserRef(
616 mc.cnxn, request.issue_delta.owner_ref, self.services.user)
617 except exceptions.NoSuchUserException:
618 proposed_owner_id = 0
619
620 users_by_id = framework_views.MakeAllUserViews(
621 mc.cnxn, self.services.user, [proposed_owner_id])
622 proposed_owner_view = users_by_id[proposed_owner_id]
623
624 with mc.profiler.Phase('Applying IssueDelta'):
625 if issue:
626 proposed_issue = copy.deepcopy(issue)
627 else:
628 proposed_issue = tracker_pb2.Issue(
629 owner_id=framework_constants.NO_USER_SPECIFIED,
630 project_id=config.project_id)
631 issue_delta = converters.IngestIssueDelta(
632 mc.cnxn, self.services, request.issue_delta, config, None,
633 ignore_missing_objects=True)
634 tracker_bizobj.ApplyIssueDelta(
635 mc.cnxn, self.services.issue, proposed_issue, issue_delta, config)
636
637 with mc.profiler.Phase('applying rules'):
638 _, traces = filterrules_helpers.ApplyFilterRules(
639 mc.cnxn, self.services, proposed_issue, config)
640 logging.info('proposed_issue is now: %r', proposed_issue)
641 logging.info('traces are: %r', traces)
642
643 with mc.profiler.Phase('making derived user views'):
644 derived_users_by_id = framework_views.MakeAllUserViews(
645 mc.cnxn, self.services.user, [proposed_issue.derived_owner_id],
646 proposed_issue.derived_cc_ids)
647 framework_views.RevealAllEmailsToMembers(
648 mc.cnxn, self.services, mc.auth, derived_users_by_id, project)
649
650 with mc.profiler.Phase('pair derived values with rule explanations'):
651 (derived_labels, derived_owners, derived_ccs, warnings, errors) = (
652 tracker_helpers.PairDerivedValuesWithRuleExplanations(
653 proposed_issue, traces, derived_users_by_id))
654
655 result = issues_pb2.PresubmitIssueResponse(
656 owner_availability=proposed_owner_view.avail_message_short,
657 owner_availability_state=proposed_owner_view.avail_state,
658 derived_labels=converters.ConvertValueAndWhyList(derived_labels),
659 derived_owners=converters.ConvertValueAndWhyList(derived_owners),
660 derived_ccs=converters.ConvertValueAndWhyList(derived_ccs),
661 warnings=converters.ConvertValueAndWhyList(warnings),
662 errors=converters.ConvertValueAndWhyList(errors))
663 return result
664
665 @monorail_servicer.PRPCMethod
666 def RerankBlockedOnIssues(self, mc, request):
667 """Rerank the blocked on issues for the given issue ref."""
668 moved_issue_id, target_issue_id = converters.IngestIssueRefs(
669 mc.cnxn, [request.moved_ref, request.target_ref], self.services)
670 _project, issue, _config = self._GetProjectIssueAndConfig(
671 mc, request.issue_ref)
672
673 with work_env.WorkEnv(mc, self.services) as we:
674 we.RerankBlockedOnIssues(
675 issue, moved_issue_id, target_issue_id, request.split_above)
676
677 with work_env.WorkEnv(mc, self.services) as we:
678 issue = we.GetIssue(issue.issue_id)
679 related_refs = we.GetRelatedIssueRefs([issue])
680
681 with mc.profiler.Phase('converting to response objects'):
682 converted_issue_refs = converters.ConvertIssueRefs(
683 issue.blocked_on_iids, related_refs)
684 result = issues_pb2.RerankBlockedOnIssuesResponse(
685 blocked_on_issue_refs=converted_issue_refs)
686
687 return result
688
689 @monorail_servicer.PRPCMethod
690 def DeleteIssue(self, mc, request):
691 """Mark or unmark the given issue as deleted."""
692 _project, issue, _config = self._GetProjectIssueAndConfig(
693 mc, request.issue_ref, view_deleted=True)
694
695 with work_env.WorkEnv(mc, self.services) as we:
696 we.DeleteIssue(issue, request.delete)
697
698 result = issues_pb2.DeleteIssueResponse()
699 return result
700
701 @monorail_servicer.PRPCMethod
702 def DeleteIssueComment(self, mc, request):
703 """Mark or unmark the given comment as deleted."""
704 _project, issue, _config = self._GetProjectIssueAndConfig(
705 mc, request.issue_ref, use_cache=False)
706
707 with work_env.WorkEnv(mc, self.services) as we:
708 comments = we.ListIssueComments(issue)
709 if request.sequence_num >= len(comments):
710 raise exceptions.InputException('Invalid sequence number.')
711 we.DeleteComment(issue, comments[request.sequence_num], request.delete)
712
713 result = issues_pb2.DeleteIssueCommentResponse()
714 return result
715
716 @monorail_servicer.PRPCMethod
717 def DeleteAttachment(self, mc, request):
718 """Mark or unmark the given attachment as deleted."""
719 _project, issue, _config = self._GetProjectIssueAndConfig(
720 mc, request.issue_ref, use_cache=False)
721
722 with work_env.WorkEnv(mc, self.services) as we:
723 comments = we.ListIssueComments(issue)
724 if request.sequence_num >= len(comments):
725 raise exceptions.InputException('Invalid sequence number.')
726 we.DeleteAttachment(
727 issue, comments[request.sequence_num], request.attachment_id,
728 request.delete)
729
730 result = issues_pb2.DeleteAttachmentResponse()
731 return result
732
733 @monorail_servicer.PRPCMethod
734 def FlagIssues(self, mc, request):
735 """Flag or unflag the given issues as spam."""
736 if not request.issue_refs:
737 raise exceptions.InputException('Param `issue_refs` empty.')
738
739 _project, issue_ids, _config = self._GetProjectIssueIDsAndConfig(
740 mc, request.issue_refs)
741 with work_env.WorkEnv(mc, self.services) as we:
742 issues_by_id = we.GetIssuesDict(issue_ids, use_cache=False)
743 we.FlagIssues(list(issues_by_id.values()), request.flag)
744
745 result = issues_pb2.FlagIssuesResponse()
746 return result
747
748 @monorail_servicer.PRPCMethod
749 def FlagComment(self, mc, request):
750 """Flag or unflag the given comment as spam."""
751 _project, issue, _config = self._GetProjectIssueAndConfig(
752 mc, request.issue_ref, use_cache=False)
753
754 with work_env.WorkEnv(mc, self.services) as we:
755 comments = we.ListIssueComments(issue)
756 if request.sequence_num >= len(comments):
757 raise exceptions.InputException('Invalid sequence number.')
758 we.FlagComment(issue, comments[request.sequence_num], request.flag)
759
760 result = issues_pb2.FlagCommentResponse()
761 return result
762
763 @monorail_servicer.PRPCMethod
764 def ListIssuePermissions(self, mc, request):
765 """List the permissions for the current user in the given issue."""
766 project, issue, config = self._GetProjectIssueAndConfig(
767 mc, request.issue_ref, use_cache=False, view_deleted=True)
768
769 perms = permissions.UpdateIssuePermissions(
770 mc.perms, project, issue, mc.auth.effective_ids, config=config)
771
772 return issues_pb2.ListIssuePermissionsResponse(
773 permissions=sorted(perms.perm_names))
774
775 @monorail_servicer.PRPCMethod
776 def MoveIssue(self, mc, request):
777 """Move an issue to another project."""
778 _project, issue, _config = self._GetProjectIssueAndConfig(
779 mc, request.issue_ref, use_cache=False)
780
781 with work_env.WorkEnv(mc, self.services) as we:
782 target_project = we.GetProjectByName(request.target_project_name)
783 moved_issue = we.MoveIssue(issue, target_project)
784
785 result = issues_pb2.MoveIssueResponse(
786 new_issue_ref=converters.ConvertIssueRef(
787 (moved_issue.project_name, moved_issue.local_id)))
788 return result
789
790 @monorail_servicer.PRPCMethod
791 def CopyIssue(self, mc, request):
792 """Copy an issue."""
793 _project, issue, _config = self._GetProjectIssueAndConfig(
794 mc, request.issue_ref, use_cache=False)
795
796 with work_env.WorkEnv(mc, self.services) as we:
797 target_project = we.GetProjectByName(request.target_project_name)
798 copied_issue = we.CopyIssue(issue, target_project)
799
800 result = issues_pb2.CopyIssueResponse(
801 new_issue_ref=converters.ConvertIssueRef(
802 (copied_issue.project_name, copied_issue.local_id)))
803 return result