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