blob: 4024892d8af686c3bf71015e13981298a516929d [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# Copyright 2020 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 collections
10import itertools
11import logging
12import time
13
14from google.protobuf import timestamp_pb2
15
16from api import resource_name_converters as rnc
17from api.v3.api_proto import feature_objects_pb2
18from api.v3.api_proto import issues_pb2
19from api.v3.api_proto import issue_objects_pb2
20from api.v3.api_proto import project_objects_pb2
21from api.v3.api_proto import user_objects_pb2
22
23from framework import exceptions
24from framework import filecontent
25from framework import framework_bizobj
26from framework import framework_constants
27from framework import framework_helpers
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010028from mrproto import tracker_pb2
Copybara854996b2021-09-07 19:36:02 +000029from project import project_helpers
30from tracker import attachment_helpers
31from tracker import field_helpers
32from tracker import tracker_bizobj as tbo
33from tracker import tracker_helpers
34
35Choice = project_objects_pb2.FieldDef.EnumTypeSettings.Choice
36
37# Ingest/convert dicts for ApprovalStatus.
38_V3_APPROVAL_STATUS = issue_objects_pb2.ApprovalValue.ApprovalStatus.Value
39_APPROVAL_STATUS_INGEST = {
40 _V3_APPROVAL_STATUS('APPROVAL_STATUS_UNSPECIFIED'): None,
41 _V3_APPROVAL_STATUS('NOT_SET'): tracker_pb2.ApprovalStatus.NOT_SET,
42 _V3_APPROVAL_STATUS('NEEDS_REVIEW'): tracker_pb2.ApprovalStatus.NEEDS_REVIEW,
43 _V3_APPROVAL_STATUS('NA'): tracker_pb2.ApprovalStatus.NA,
44 _V3_APPROVAL_STATUS('REVIEW_REQUESTED'):
45 tracker_pb2.ApprovalStatus.REVIEW_REQUESTED,
46 _V3_APPROVAL_STATUS('REVIEW_STARTED'):
47 tracker_pb2.ApprovalStatus.REVIEW_STARTED,
48 _V3_APPROVAL_STATUS('NEED_INFO'): tracker_pb2.ApprovalStatus.NEED_INFO,
49 _V3_APPROVAL_STATUS('APPROVED'): tracker_pb2.ApprovalStatus.APPROVED,
50 _V3_APPROVAL_STATUS('NOT_APPROVED'): tracker_pb2.ApprovalStatus.NOT_APPROVED,
51}
52_APPROVAL_STATUS_CONVERT = {
53 val: key for key, val in _APPROVAL_STATUS_INGEST.items()}
54
55
56class Converter(object):
57 """Class to manage converting objects between the API and backend layer."""
58
59 def __init__(self, mc, services):
60 # type: (MonorailContext, Services) -> Converter
61 """Create a Converter with the given MonorailContext and Services.
62
63 Args:
64 mc: MonorailContext object containing the MonorailConnection to the DB
65 and the requester's AuthData object.
66 services: Services object for connections to backend services.
67 """
68 self.cnxn = mc.cnxn
69 self.user_auth = mc.auth
70 self.services = services
71
72 # Hotlists
73
74 def ConvertHotlist(self, hotlist):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010075 # type: (mrproto.feature_objects_pb2.Hotlist)
Copybara854996b2021-09-07 19:36:02 +000076 # -> api_proto.feature_objects_pb2.Hotlist
77 """Convert a protorpc Hotlist into a protoc Hotlist."""
78
79 hotlist_resource_name = rnc.ConvertHotlistName(hotlist.hotlist_id)
80 members_by_id = rnc.ConvertUserNames(
81 hotlist.owner_ids + hotlist.editor_ids)
82 default_columns = self._ComputeIssuesListColumns(hotlist.default_col_spec)
83 if hotlist.is_private:
84 hotlist_privacy = feature_objects_pb2.Hotlist.HotlistPrivacy.Value(
85 'PRIVATE')
86 else:
87 hotlist_privacy = feature_objects_pb2.Hotlist.HotlistPrivacy.Value(
88 'PUBLIC')
89
90 return feature_objects_pb2.Hotlist(
91 name=hotlist_resource_name,
92 display_name=hotlist.name,
93 owner=members_by_id.get(hotlist.owner_ids[0]),
94 editors=[
95 members_by_id.get(editor_id) for editor_id in hotlist.editor_ids
96 ],
97 summary=hotlist.summary,
98 description=hotlist.description,
99 default_columns=default_columns,
100 hotlist_privacy=hotlist_privacy)
101
102 def ConvertHotlists(self, hotlists):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100103 # type: (Sequence[mrproto.feature_objects_pb2.Hotlist])
Copybara854996b2021-09-07 19:36:02 +0000104 # -> Sequence[api_proto.feature_objects_pb2.Hotlist]
105 """Convert protorpc Hotlists into protoc Hotlists."""
106 return [self.ConvertHotlist(hotlist) for hotlist in hotlists]
107
108 def ConvertHotlistItems(self, hotlist_id, items):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100109 # type: (int, Sequence[mrproto.features_pb2.HotlistItem]) ->
Copybara854996b2021-09-07 19:36:02 +0000110 # Sequence[api_proto.feature_objects_pb2.Hotlist]
111 """Convert a Sequence of protorpc HotlistItems into a Sequence of protoc
112 HotlistItems.
113
114 Args:
115 hotlist_id: ID of the Hotlist the items belong to.
116 items: Sequence of HotlistItem protorpc objects.
117
118 Returns:
119 Sequence of protoc HotlistItems in the same order they are given in
120 `items`.
121 In the rare event that any issues in `items` are not found, they will be
122 omitted from the result.
123 """
124 issue_ids = [item.issue_id for item in items]
125 # Converting HotlistItemNames and IssueNames both require looking up the
126 # issues in the hotlist. However, we want to keep the code clean and
127 # readable so we keep the two processes separate.
128 resource_names_dict = rnc.ConvertHotlistItemNames(
129 self.cnxn, hotlist_id, issue_ids, self.services)
130 issue_names_dict = rnc.ConvertIssueNames(
131 self.cnxn, issue_ids, self.services)
132 adders_by_id = rnc.ConvertUserNames([item.adder_id for item in items])
133
134 # Filter out items whose issues were not found.
135 found_items = [
136 item for item in items if resource_names_dict.get(item.issue_id) and
137 issue_names_dict.get(item.issue_id)
138 ]
139 if len(items) != len(found_items):
140 found_ids = [item.issue_id for item in found_items]
141 missing_ids = [iid for iid in issue_ids if iid not in found_ids]
142 logging.info('HotlistItem issues %r not found' % missing_ids)
143
144 # Generate user friendly ranks (0, 1, 2, 3,...) that are exposed to API
145 # clients, instead of using padded ranks (1, 11, 21, 31,...).
146 sorted_ranks = sorted(item.rank for item in found_items)
147 friendly_ranks_dict = {
148 rank: friendly_rank for friendly_rank, rank in enumerate(sorted_ranks)
149 }
150
151 api_items = []
152 for item in found_items:
153 api_item = feature_objects_pb2.HotlistItem(
154 name=resource_names_dict.get(item.issue_id),
155 issue=issue_names_dict.get(item.issue_id),
156 rank=friendly_ranks_dict[item.rank],
157 adder=adders_by_id.get(item.adder_id),
158 note=item.note)
159 if item.date_added:
160 api_item.create_time.FromSeconds(item.date_added)
161 api_items.append(api_item)
162
163 return api_items
164
165 # Issues
166
167 def _ConvertComponentValues(self, issue):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100168 # mrproto.tracker_pb2.Issue ->
Copybara854996b2021-09-07 19:36:02 +0000169 # Sequence[api_proto.issue_objects_pb2.Issue.ComponentValue]
170 """Convert the status string on issue into a ComponentValue."""
171 component_values = []
172 component_ids = itertools.chain(
173 issue.component_ids, issue.derived_component_ids)
174 ids_to_names = rnc.ConvertComponentDefNames(
175 self.cnxn, component_ids, issue.project_id, self.services)
176
177 for component_id in issue.component_ids:
178 if component_id in ids_to_names:
179 component_values.append(
180 issue_objects_pb2.Issue.ComponentValue(
181 component=ids_to_names[component_id],
182 derivation=issue_objects_pb2.Derivation.Value(
183 'EXPLICIT')))
184 for derived_component_id in issue.derived_component_ids:
185 if derived_component_id in ids_to_names:
186 component_values.append(
187 issue_objects_pb2.Issue.ComponentValue(
188 component=ids_to_names[derived_component_id],
189 derivation=issue_objects_pb2.Derivation.Value('RULE')))
190
191 return component_values
192
193 def _ConvertStatusValue(self, issue):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100194 # mrproto.tracker_pb2.Issue -> api_proto.issue_objects_pb2.Issue.StatusValue
Copybara854996b2021-09-07 19:36:02 +0000195 """Convert the status string on issue into a StatusValue."""
196 derivation = issue_objects_pb2.Derivation.Value(
197 'DERIVATION_UNSPECIFIED')
198 if issue.status:
199 derivation = issue_objects_pb2.Derivation.Value('EXPLICIT')
200 else:
201 derivation = issue_objects_pb2.Derivation.Value('RULE')
202 return issue_objects_pb2.Issue.StatusValue(
203 status=issue.status or issue.derived_status, derivation=derivation)
204
205 def _ConvertAmendments(self, amendments, user_display_names):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100206 # type: (Sequence[mrproto.tracker_pb2.Amendment], Mapping[int, str]) ->
Copybara854996b2021-09-07 19:36:02 +0000207 # Sequence[api_proto.issue_objects_pb2.Comment.Amendment]
208 """Convert protorpc Amendments to protoc Amendments.
209
210 Args:
211 amendments: the amendments to convert
212 user_display_names: map from user_id to display name for all users
213 involved in the amendments.
214
215 Returns:
216 The converted amendments.
217 """
218 results = []
219 for amendment in amendments:
220 field_name = tbo.GetAmendmentFieldName(amendment)
221 new_value = tbo.AmendmentString_New(amendment, user_display_names)
222 results.append(
223 issue_objects_pb2.Comment.Amendment(
224 field_name=field_name,
225 new_or_delta_value=new_value,
226 old_value=amendment.oldvalue))
227 return results
228
229 def _ConvertAttachments(self, attachments, project_name):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100230 # type: (Sequence[mrproto.tracker_pb2.Attachment], str) ->
Copybara854996b2021-09-07 19:36:02 +0000231 # Sequence[api_proto.issue_objects_pb2.Comment.Attachment]
232 """Convert protorpc Attachments to protoc Attachments."""
233 results = []
234 for attach in attachments:
235 if attach.deleted:
236 state = issue_objects_pb2.IssueContentState.Value('DELETED')
237 size, thumbnail_uri, view_uri, download_uri = None, None, None, None
238 else:
239 state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
240 size = attach.filesize
241 download_uri = attachment_helpers.GetDownloadURL(attach.attachment_id)
242 view_uri = attachment_helpers.GetViewURL(
243 attach, download_uri, project_name)
244 thumbnail_uri = attachment_helpers.GetThumbnailURL(attach, download_uri)
245 results.append(
246 issue_objects_pb2.Comment.Attachment(
247 filename=attach.filename,
248 state=state,
249 size=size,
250 media_type=attach.mimetype,
251 thumbnail_uri=thumbnail_uri,
252 view_uri=view_uri,
253 download_uri=download_uri))
254 return results
255
256 def ConvertComments(self, issue_id, comments):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100257 # type: (int, Sequence[mrproto.tracker_pb2.IssueComment])
Copybara854996b2021-09-07 19:36:02 +0000258 # -> Sequence[api_proto.issue_objects_pb2.Comment]
259 """Convert protorpc IssueComments from issue into protoc Comments."""
260 issue = self.services.issue.GetIssue(self.cnxn, issue_id)
261 users_by_id = self.services.user.GetUsersByIDs(
262 self.cnxn, tbo.UsersInvolvedInCommentList(comments))
263 (user_display_names,
264 _user_display_emails) = framework_bizobj.CreateUserDisplayNamesAndEmails(
265 self.cnxn, self.services, self.user_auth, users_by_id.values())
266 comment_names_dict = rnc.CreateCommentNames(
267 issue.local_id, issue.project_name,
268 [comment.sequence for comment in comments])
269 approval_ids = [
270 comment.approval_id
271 for comment in comments
272 if comment.approval_id is not None # In case of a 0 approval_id.
273 ]
274 approval_ids_to_names = rnc.ConvertApprovalDefNames(
275 self.cnxn, approval_ids, issue.project_id, self.services)
276
277 converted_comments = []
278 for comment in comments:
279 if comment.is_spam:
280 state = issue_objects_pb2.IssueContentState.Value('SPAM')
281 elif comment.deleted_by:
282 state = issue_objects_pb2.IssueContentState.Value('DELETED')
283 else:
284 state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
285 comment_type = issue_objects_pb2.Comment.Type.Value('COMMENT')
286 if comment.is_description:
287 comment_type = issue_objects_pb2.Comment.Type.Value('DESCRIPTION')
288 converted_attachments = self._ConvertAttachments(
289 comment.attachments, issue.project_name)
290 converted_amendments = self._ConvertAmendments(
291 comment.amendments, user_display_names)
292 converted_comment = issue_objects_pb2.Comment(
293 name=comment_names_dict[comment.sequence],
294 state=state,
295 type=comment_type,
296 create_time=timestamp_pb2.Timestamp(seconds=comment.timestamp),
297 attachments=converted_attachments,
298 amendments=converted_amendments)
299 if comment.content:
300 converted_comment.content = comment.content
301 if comment.user_id:
302 converted_comment.commenter = rnc.ConvertUserName(comment.user_id)
303 if comment.inbound_message:
304 converted_comment.inbound_message = comment.inbound_message
305 if comment.approval_id and comment.approval_id in approval_ids_to_names:
306 converted_comment.approval = approval_ids_to_names[comment.approval_id]
307 converted_comments.append(converted_comment)
308 return converted_comments
309
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100310 def ConvertIssue(self, issue, migrated_id=None):
311 # type: (mrproto.tracker_pb2.Issue) -> api_proto.issue_objects_pb2.Issue
Copybara854996b2021-09-07 19:36:02 +0000312 """Convert a protorpc Issue into a protoc Issue."""
313 issues = self.ConvertIssues([issue])
314 if len(issues) < 1:
315 raise exceptions.NoSuchIssueException()
316 if len(issues) > 1:
317 logging.warning('More than one converted issue returned: %s', issues)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100318 ret_issue = issues[0]
319 if migrated_id:
320 ret_issue.migrated_id = migrated_id
321 return ret_issue
Copybara854996b2021-09-07 19:36:02 +0000322
323 def ConvertIssues(self, issues):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100324 # type: (Sequence[mrproto.tracker_pb2.Issue]) ->
Copybara854996b2021-09-07 19:36:02 +0000325 # Sequence[api_proto.issue_objects_pb2.Issue]
326 """Convert protorpc Issues into protoc Issues."""
327 issue_ids = [issue.issue_id for issue in issues]
328 issue_names_dict = rnc.ConvertIssueNames(
329 self.cnxn, issue_ids, self.services)
330 found_issues = [
331 issue for issue in issues if issue.issue_id in issue_names_dict
332 ]
333 converted_issues = []
334 for issue in found_issues:
335 status = self._ConvertStatusValue(issue)
336 content_state = issue_objects_pb2.IssueContentState.Value(
337 'STATE_UNSPECIFIED')
338 if issue.is_spam:
339 content_state = issue_objects_pb2.IssueContentState.Value('SPAM')
340 elif issue.deleted:
341 content_state = issue_objects_pb2.IssueContentState.Value('DELETED')
342 else:
343 content_state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
344
345 owner = None
346 # Explicit values override values derived from rules.
347 if issue.owner_id:
348 owner = issue_objects_pb2.Issue.UserValue(
349 derivation=issue_objects_pb2.Derivation.Value('EXPLICIT'),
350 user=rnc.ConvertUserName(issue.owner_id))
351 elif issue.derived_owner_id:
352 owner = issue_objects_pb2.Issue.UserValue(
353 derivation=issue_objects_pb2.Derivation.Value('RULE'),
354 user=rnc.ConvertUserName(issue.derived_owner_id))
355
356 cc_users = []
357 for cc_user_id in issue.cc_ids:
358 cc_users.append(
359 issue_objects_pb2.Issue.UserValue(
360 derivation=issue_objects_pb2.Derivation.Value('EXPLICIT'),
361 user=rnc.ConvertUserName(cc_user_id)))
362 for derived_cc_user_id in issue.derived_cc_ids:
363 cc_users.append(
364 issue_objects_pb2.Issue.UserValue(
365 derivation=issue_objects_pb2.Derivation.Value('RULE'),
366 user=rnc.ConvertUserName(derived_cc_user_id)))
367
368 labels = self.ConvertLabels(
369 issue.labels, issue.derived_labels, issue.project_id)
370 components = self._ConvertComponentValues(issue)
371 non_approval_fvs = self._GetNonApprovalFieldValues(
372 issue.field_values, issue.project_id)
373 field_values = self.ConvertFieldValues(
374 non_approval_fvs, issue.project_id, issue.phases)
375 field_values.extend(
376 self.ConvertEnumFieldValues(
377 issue.labels, issue.derived_labels, issue.project_id))
378 related_issue_ids = (
379 [issue.merged_into] + issue.blocked_on_iids + issue.blocking_iids)
380 issue_names_by_ids = rnc.ConvertIssueNames(
381 self.cnxn, related_issue_ids, self.services)
382 merged_into_issue_ref = None
383 if issue.merged_into and issue.merged_into in issue_names_by_ids:
384 merged_into_issue_ref = issue_objects_pb2.IssueRef(
385 issue=issue_names_by_ids[issue.merged_into])
386 if issue.merged_into_external:
387 merged_into_issue_ref = issue_objects_pb2.IssueRef(
388 ext_identifier=issue.merged_into_external)
389
390 blocked_on_issue_refs = [
391 issue_objects_pb2.IssueRef(issue=issue_names_by_ids[iid])
392 for iid in issue.blocked_on_iids
393 if iid in issue_names_by_ids
394 ]
395 blocked_on_issue_refs.extend(
396 issue_objects_pb2.IssueRef(
397 ext_identifier=blocked_on.ext_issue_identifier)
398 for blocked_on in issue.dangling_blocked_on_refs)
399
400 blocking_issue_refs = [
401 issue_objects_pb2.IssueRef(issue=issue_names_by_ids[iid])
402 for iid in issue.blocking_iids
403 if iid in issue_names_by_ids
404 ]
405 blocking_issue_refs.extend(
406 issue_objects_pb2.IssueRef(
407 ext_identifier=blocking.ext_issue_identifier)
408 for blocking in issue.dangling_blocking_refs)
409 # All other timestamps were set when the issue was created.
410 close_time = None
411 if issue.closed_timestamp:
412 close_time = timestamp_pb2.Timestamp(seconds=issue.closed_timestamp)
413
414 phases = self._ComputePhases(issue.phases)
415
416 result = issue_objects_pb2.Issue(
417 name=issue_names_dict[issue.issue_id],
418 summary=issue.summary,
419 state=content_state,
420 status=status,
421 reporter=rnc.ConvertUserName(issue.reporter_id),
422 owner=owner,
423 cc_users=cc_users,
424 labels=labels,
425 components=components,
426 field_values=field_values,
427 merged_into_issue_ref=merged_into_issue_ref,
428 blocked_on_issue_refs=blocked_on_issue_refs,
429 blocking_issue_refs=blocking_issue_refs,
430 create_time=timestamp_pb2.Timestamp(seconds=issue.opened_timestamp),
431 close_time=close_time,
432 modify_time=timestamp_pb2.Timestamp(seconds=issue.modified_timestamp),
433 component_modify_time=timestamp_pb2.Timestamp(
434 seconds=issue.component_modified_timestamp),
435 status_modify_time=timestamp_pb2.Timestamp(
436 seconds=issue.status_modified_timestamp),
437 owner_modify_time=timestamp_pb2.Timestamp(
438 seconds=issue.owner_modified_timestamp),
439 star_count=issue.star_count,
440 phases=phases)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100441 if hasattr(issue, 'migrated_id'):
442 result.migrated_id = issue.migrated_id
Copybara854996b2021-09-07 19:36:02 +0000443 # TODO(crbug.com/monorail/5857): Set attachment_count unconditionally
444 # after the underlying source of negative attachment counts has been
445 # resolved and database has been repaired.
446 if issue.attachment_count >= 0:
447 result.attachment_count = issue.attachment_count
448 converted_issues.append(result)
449 return converted_issues
450
451 def IngestAttachmentUploads(self, attachment_uploads):
452 # type: (Sequence[api_proto.issues_pb2.AttachmentUpload] ->
453 # Sequence[framework_helpers.AttachmentUpload])
454 """Ingests protoc AttachmentUploads into framework_helpers.AttachUploads."""
455 ingested_uploads = []
456 with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
457 for up in attachment_uploads:
458 if not up.filename or not up.content:
459 err_agg.AddErrorMessage(
460 'Uploaded atachment missing filename or content')
461 mimetype = filecontent.GuessContentTypeFromFilename(up.filename)
462 ingested_uploads.append(
463 framework_helpers.AttachmentUpload(
464 up.filename, up.content, mimetype))
465
466 return ingested_uploads
467
468 def IngestIssueDeltas(self, issue_deltas):
469 # type: (Sequence[api_proto.issues_pb2.IssueDelta]) ->
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100470 # Sequence[Tuple[int, mrproto.tracker_pb2.IssueDelta]]
Copybara854996b2021-09-07 19:36:02 +0000471 """Ingests protoc IssueDeltas, into protorpc IssueDeltas.
472
473 Args:
474 issue_deltas: the protoc IssueDeltas to ingest.
475
476 Returns:
477 A list of (issue_id, tracker_pb2.IssueDelta) tuples that contain
478 values found in issue_deltas, ignoring all OUTPUT_ONLY and masked
479 fields.
480
481 Raises:
482 InputException: if any fields in the approval_deltas were invalid.
483 NoSuchProjectException: if any parent projects are not found.
484 NoSuchIssueException: if any issues are not found.
485 NoSuchComponentException: if any components are not found.
486 """
487 issue_names = [delta.issue.name for delta in issue_deltas]
488 issue_ids = rnc.IngestIssueNames(self.cnxn, issue_names, self.services)
489 issues_dict, misses = self.services.issue.GetIssuesDict(
490 self.cnxn, issue_ids)
491 if misses:
492 logging.info(
493 'Issues not found for supposedly valid issue_ids: %r' % misses)
494 raise ValueError('Could not fetch some issues.')
495 configs_by_pid = self.services.config.GetProjectConfigs(
496 self.cnxn, {issue.project_id for issue in issues_dict.values()})
497
498 with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
499 for api_delta in issue_deltas:
500 if not api_delta.HasField('update_mask'):
501 err_agg.AddErrorMessage(
502 '`update_mask` must be set for {} delta.', api_delta.issue.name)
503 elif not api_delta.update_mask.IsValidForDescriptor(
504 issue_objects_pb2.Issue.DESCRIPTOR):
505 err_agg.AddErrorMessage(
506 'Invalid `update_mask` for {} delta.', api_delta.issue.name)
507
508 ingested = []
509 for iid, api_delta in zip(issue_ids, issue_deltas):
510 delta = tracker_pb2.IssueDelta()
511
512 # Check non-repeated fields before MergeMessage because in an object
513 # where fields are not set and with a FieldMask applied, there is no
514 # way to tell if empty fields were explicitly listed or not listed
515 # in the FieldMask.
516 paths_set = set(api_delta.update_mask.paths)
517 if (not paths_set.isdisjoint({'status', 'status.status'}) and
518 api_delta.issue.status.status):
519 delta.status = api_delta.issue.status.status
520 elif 'status.status' in paths_set and not api_delta.issue.status.status:
521 delta.status = ''
522
523 if (not paths_set.isdisjoint({'owner', 'owner.user'}) and
524 api_delta.issue.owner.user):
525 delta.owner_id = rnc.IngestUserName(
526 self.cnxn, api_delta.issue.owner.user, self.services)
527 elif 'owner.user' in paths_set and not api_delta.issue.owner.user:
528 delta.owner_id = framework_constants.NO_USER_SPECIFIED
529
530 if 'summary' in paths_set:
531 if api_delta.issue.summary:
532 delta.summary = api_delta.issue.summary
533 else:
534 delta.summary = ''
535
536 merge_ref = api_delta.issue.merged_into_issue_ref
537 if 'merged_into_issue_ref' in paths_set:
538 if (api_delta.issue.merged_into_issue_ref.issue or
539 api_delta.issue.merged_into_issue_ref.ext_identifier):
540 ingested_ref = self._IngestIssueRef(merge_ref)
541 if isinstance(ingested_ref, tracker_pb2.DanglingIssueRef):
542 delta.merged_into_external = ingested_ref.ext_issue_identifier
543 else:
544 delta.merged_into = ingested_ref
545 elif 'merged_into_issue_ref.issue' in paths_set:
546 if api_delta.issue.merged_into_issue_ref.issue:
547 delta.merged_into = self._IngestIssueRef(merge_ref)
548 else:
549 delta.merged_into = 0
550 elif 'merged_into_issue_ref.ext_identifier' in paths_set:
551 if api_delta.issue.merged_into_issue_ref.ext_identifier:
552 ingested_ref = self._IngestIssueRef(merge_ref)
553 delta.merged_into_external = ingested_ref.ext_issue_identifier
554 else:
555 delta.merged_into_external = ''
556
557 filtered_api_issue = issue_objects_pb2.Issue()
558 api_delta.update_mask.MergeMessage(
559 api_delta.issue,
560 filtered_api_issue,
561 replace_message_field=True,
562 replace_repeated_field=True)
563
564 cc_names = [name for name in api_delta.ccs_remove] + [
565 user_value.user for user_value in filtered_api_issue.cc_users
566 ]
567 cc_ids = rnc.IngestUserNames(self.cnxn, cc_names, self.services)
568 delta.cc_ids_remove = cc_ids[:len(api_delta.ccs_remove)]
569 delta.cc_ids_add = cc_ids[len(api_delta.ccs_remove):]
570
571 comp_names = [component for component in api_delta.components_remove] + [
572 c_value.component for c_value in filtered_api_issue.components
573 ]
574 project_comp_ids = rnc.IngestComponentDefNames(
575 self.cnxn, comp_names, self.services)
576 comp_ids = [comp_id for (_pid, comp_id) in project_comp_ids]
577 delta.comp_ids_remove = comp_ids[:len(api_delta.components_remove)]
578 delta.comp_ids_add = comp_ids[len(api_delta.components_remove):]
579
580 # Added to delta below, after ShiftEnumFieldsIntoLabels.
581 labels_add = [value.label for value in filtered_api_issue.labels]
582 labels_remove = [label for label in api_delta.labels_remove]
583
584 config = configs_by_pid[issues_dict[iid].project_id]
585 fvs_add, add_enums = self._IngestFieldValues(
586 filtered_api_issue.field_values, config)
587 fvs_remove, remove_enums = self._IngestFieldValues(
588 api_delta.field_vals_remove, config)
589 field_helpers.ShiftEnumFieldsIntoLabels(
590 labels_add, labels_remove, add_enums, remove_enums, config)
591 delta.field_vals_add = fvs_add
592 delta.field_vals_remove = fvs_remove
593 delta.labels_add = labels_add
594 delta.labels_remove = labels_remove
595 assert len(add_enums) == 0 # ShiftEnumFieldsIntoLabels clears all enums.
596 assert len(remove_enums) == 0
597
598 blocked_on_iids_rm, blocked_on_dangling_rm = self._IngestIssueRefs(
599 api_delta.blocked_on_issues_remove)
600 delta.blocked_on_remove = blocked_on_iids_rm
601 delta.ext_blocked_on_remove = [
602 ref.ext_issue_identifier for ref in blocked_on_dangling_rm
603 ]
604
605 blocked_on_iids_add, blocked_on_dangling_add = self._IngestIssueRefs(
606 filtered_api_issue.blocked_on_issue_refs)
607 delta.blocked_on_add = blocked_on_iids_add
608 delta.ext_blocked_on_add = [
609 ref.ext_issue_identifier for ref in blocked_on_dangling_add
610 ]
611
612 blocking_iids_rm, blocking_dangling_rm = self._IngestIssueRefs(
613 api_delta.blocking_issues_remove)
614 delta.blocking_remove = blocking_iids_rm
615 delta.ext_blocking_remove = [
616 ref.ext_issue_identifier for ref in blocking_dangling_rm
617 ]
618
619 blocking_iids_add, blocking_dangling_add = self._IngestIssueRefs(
620 filtered_api_issue.blocking_issue_refs)
621 delta.blocking_add = blocking_iids_add
622 delta.ext_blocking_add = [
623 ref.ext_issue_identifier for ref in blocking_dangling_add
624 ]
625
626 ingested.append((iid, delta))
627
628 return ingested
629
630 def IngestApprovalDeltas(self, approval_deltas, setter_id):
631 # type: (Sequence[api_proto.issues_pb2.ApprovalDelta], int) ->
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100632 # Sequence[Tuple[int, int, mrproto.tracker_pb2.ApprovalDelta]]
Copybara854996b2021-09-07 19:36:02 +0000633 """Ingests protoc ApprovalDeltas into protorpc ApprovalDeltas.
634
635 Args:
636 approval_deltas: the protoc ApprovalDeltas to ingest.
637 setter_id: The ID for the user setting the deltas.
638
639 Returns:
640 Sequence of (issue_id, approval_id, ApprovalDelta) tuples in the order
641 provided. The ApprovalDeltas ignore all OUTPUT_ONLY and masked fields.
642 The tuples are "delta_specifications;" they identify one requested change.
643
644 Raises:
645 InputException: if any fields in the approval_delta protos were invalid.
646 NoSuchProjectException: if the parent project of any ApprovalValue isn't
647 found.
648 NoSuchIssueException: if the issue of any ApprovalValue isn't found.
649 NoSuchUserException: if any user value was provided with an invalid email.
650 Note that users specified by ID are not checked for existence.
651 """
652 delta_specifications = []
653 set_on = int(time.time()) # Use the same timestamp for all deltas.
654 for approval_delta in approval_deltas:
655 approval_name = approval_delta.approval_value.name
656 # TODO(crbug/monorail/8173): Aggregate errors.
657 project_id, issue_id, approval_id = rnc.IngestApprovalValueName(
658 self.cnxn, approval_name, self.services)
659
660 if not approval_delta.HasField('update_mask'):
661 raise exceptions.InputException(
662 '`update_mask` must be set for %s delta.' % approval_name)
663 elif not approval_delta.update_mask.IsValidForDescriptor(
664 issue_objects_pb2.ApprovalValue.DESCRIPTOR):
665 raise exceptions.InputException(
666 'Invalid `update_mask` for %s delta.' % approval_name)
667 filtered_value = issue_objects_pb2.ApprovalValue()
668 approval_delta.update_mask.MergeMessage(
669 approval_delta.approval_value,
670 filtered_value,
671 replace_message_field=True,
672 replace_repeated_field=True)
673 status = _APPROVAL_STATUS_INGEST[filtered_value.status]
674 # Approvers
675 # No autocreate.
676 # A user may try to remove all existing approvers [a, b] and add another
677 # approver [c]. If they mis-type `c` and we auto-create `c` instead of
678 # raising error, this would cause the ApprovalValue to be editable by no
679 # one but site admins.
680 approver_ids_add = rnc.IngestUserNames(
681 self.cnxn, filtered_value.approvers, self.services, autocreate=False)
682 approver_ids_remove = rnc.IngestUserNames(
683 self.cnxn,
684 approval_delta.approvers_remove,
685 self.services,
686 autocreate=False)
687
688 # Field Values.
689 config = self.services.config.GetProjectConfig(self.cnxn, project_id)
690 approval_fds_by_id = {
691 fd.field_id: fd
692 for fd in config.field_defs
693 if fd.field_type is tracker_pb2.FieldTypes.APPROVAL_TYPE
694 }
695 if approval_id not in approval_fds_by_id:
696 raise exceptions.InputException(
697 'Approval not found in project for %s' % approval_name)
698
699 sub_fvs_add, add_enums = self._IngestFieldValues(
700 filtered_value.field_values, config, approval_id_filter=approval_id)
701 sub_fvs_remove, remove_enums = self._IngestFieldValues(
702 approval_delta.field_vals_remove,
703 config,
704 approval_id_filter=approval_id)
705 labels_add = []
706 labels_remove = []
707 field_helpers.ShiftEnumFieldsIntoLabels(
708 labels_add, labels_remove, add_enums, remove_enums, config)
709 assert len(add_enums) == 0 # ShiftEnumFieldsIntoLabels clears all enums.
710 assert len(remove_enums) == 0
711 delta = tbo.MakeApprovalDelta(
712 status,
713 setter_id,
714 approver_ids_add,
715 approver_ids_remove,
716 sub_fvs_add,
717 sub_fvs_remove, [],
718 labels_add,
719 labels_remove,
720 set_on=set_on)
721 delta_specifications.append((issue_id, approval_id, delta))
722 return delta_specifications
723
724 def IngestIssue(self, issue, project_id):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100725 # type: (api_proto.issue_objects_pb2.Issue, int) ->
726 # mrproto.tracker_pb2.Issue
Copybara854996b2021-09-07 19:36:02 +0000727 """Ingest a protoc Issue into a protorpc Issue.
728
729 Args:
730 issue: the protoc issue to ingest.
731 project_id: The project into which we're ingesting `issue`.
732
733 Returns:
734 protorpc version of issue, ignoring all OUTPUT_ONLY fields.
735
736 Raises:
737 InputException: if any fields in the 'issue' proto were invalid.
738 NoSuchProjectException: if 'project_id' is not found.
739 """
740 # Get config first. We can't ingest the issue if the project isn't found.
741 config = self.services.config.GetProjectConfig(self.cnxn, project_id)
742 ingestedDict = {
743 'project_id': project_id,
744 'summary': issue.summary
745 }
746 with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
747 self._ExtractOwner(issue, ingestedDict, err_agg)
748
749 # Extract ccs.
750 try:
751 ingestedDict['cc_ids'] = rnc.IngestUserNames(
752 self.cnxn, [cc.user for cc in issue.cc_users], self.services,
753 autocreate=True)
754 except exceptions.InputException as e:
755 err_agg.AddErrorMessage('Error ingesting cc_users: {}', e)
756
757 # Extract status.
758 if issue.HasField('status') and issue.status.status:
759 ingestedDict['status'] = issue.status.status
760 else:
761 err_agg.AddErrorMessage('Status is required when creating an issue')
762
763 # Extract components.
764 try:
765 project_comp_ids = rnc.IngestComponentDefNames(
766 self.cnxn, [cv.component for cv in issue.components], self.services)
767 ingestedDict['component_ids'] = [
768 comp_id for (_pid, comp_id) in project_comp_ids]
769 except (exceptions.InputException, exceptions.NoSuchProjectException,
770 exceptions.NoSuchComponentException) as e:
771 err_agg.AddErrorMessage('Error ingesting components: {}', e)
772
773 # Extract labels and field values.
774 ingestedDict['labels'] = [lv.label for lv in issue.labels]
775 try:
776 ingestedDict['field_values'], enums = self._IngestFieldValues(
777 issue.field_values, config)
778 field_helpers.ShiftEnumFieldsIntoLabels(
779 ingestedDict['labels'], [], enums, [], config)
780 assert len(
781 enums) == 0 # ShiftEnumFieldsIntoLabels must clear all enums.
782 except exceptions.InputException as e:
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100783 err_agg.AddErrorMessage(str(e))
Copybara854996b2021-09-07 19:36:02 +0000784
785 # Ingest merged, blocking/blocked_on.
786 self._ExtractIssueRefs(issue, ingestedDict, err_agg)
787 return tracker_pb2.Issue(**ingestedDict)
788
789 def _IngestFieldValues(self, field_values, config, approval_id_filter=None):
790 # type: (Sequence[api_proto.issue_objects.FieldValue],
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100791 # mrproto.tracker_pb2.ProjectIssueConfig, Optional[int]) ->
792 # Tuple[Sequence[mrproto.tracker_pb2.FieldValue],
Copybara854996b2021-09-07 19:36:02 +0000793 # Mapping[int, Sequence[str]]]
794 """Returns protorpc FieldValues for the given protoc FieldValues.
795
796 Raises exceptions if any field could not be parsed for any reasons such as
797 unsupported field type, non-existent field, field from different
798 projects, or fields with mismatched parent approvals.
799
800 Args:
801 field_values: protoc FieldValues to ingest.
802 config: ProjectIssueConfig for the FieldValues we're ingesting.
803 approval_id_filter: an approval_id, including any FieldValues that does
804 not have this approval as a parent will trigger InputException.
805
806 Returns:
807 A pair 1) Ingested FieldValues. 2) A mapping of field ids to values
808 for ENUM_TYPE fields in 'field_values.'
809
810 Raises:
811 InputException: if any fields_values could not be parsed for any reasons
812 such as unsupported field type, non-existent field, or field from
813 different projects.
814 """
815 fds_by_id = {fd.field_id: fd for fd in config.field_defs}
816 enums = {}
817 ingestedFieldValues = []
818 with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
819 for fv in field_values:
820 try:
821 project_id, fd_id = rnc.IngestFieldDefName(
822 self.cnxn, fv.field, self.services)
823 fd = fds_by_id[fd_id]
824 # Raise if field does not belong to approval_id_filter (if provided).
825 if (approval_id_filter is not None and
826 fd.approval_id != approval_id_filter):
827 approval_name = rnc.ConvertApprovalDefNames(
828 self.cnxn, [approval_id_filter], project_id,
829 self.services)[approval_id_filter]
830 err_agg.AddErrorMessage(
831 'Field {} does not belong to approval {}', fv.field,
832 approval_name)
833 continue
834 if fd.field_type == tracker_pb2.FieldTypes.ENUM_TYPE:
835 enums.setdefault(fd_id, []).append(fv.value)
836 else:
837 ingestedFieldValues.append(self._IngestFieldValue(fv, fd))
838 except (exceptions.InputException, exceptions.NoSuchProjectException,
839 exceptions.NoSuchFieldDefException, ValueError) as e:
840 err_agg.AddErrorMessage(
841 'Could not ingest value ({}) for FieldDef ({}): {}', fv.value,
842 fv.field, e)
843 except exceptions.NoSuchUserException as e:
844 err_agg.AddErrorMessage(
845 'User ({}) not found when ingesting user field: {}', fv.value,
846 fv.field)
847 except KeyError as e:
848 err_agg.AddErrorMessage('Field {} is not in this project', fv.field)
849 return ingestedFieldValues, enums
850
851 def _IngestFieldValue(self, field_value, field_def):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100852 # type: (api_proto.issue_objects.FieldValue,
853 # mrproto.tracker_pb2.FieldDef) -> mrproto.tracker_pb2.FieldValue
Copybara854996b2021-09-07 19:36:02 +0000854 """Ingest a protoc FieldValue into a protorpc FieldValue.
855
856 Args:
857 field_value: protoc FieldValue to ingest.
858 field_def: protorpc FieldDef associated with 'field_value'.
859 BOOL_TYPE and APPROVAL_TYPE are ignored.
860 Enum values are not allowed. They must be ingested as labels.
861
862 Returns:
863 Ingested protorpc FieldValue.
864
865 Raises:
866 InputException if 'field_def' is USER_TYPE and 'field_value' does not
867 have a valid formatted resource name.
868 NoSuchUserException if specified user in field does not exist.
869 ValueError if 'field_value' could not be parsed for 'field_def'.
870 """
871 assert field_def.field_type != tracker_pb2.FieldTypes.ENUM_TYPE
872 if field_def.field_type == tracker_pb2.FieldTypes.USER_TYPE:
873 return self._ParseOneUserFieldValue(field_value.value, field_def.field_id)
874 fv = field_helpers.ParseOneFieldValue(
875 self.cnxn, self.services.user, field_def, field_value.value)
876 # ParseOneFieldValue currently ignores parsing errors, although it has TODOs
877 # to raise them.
878 if not fv:
879 raise ValueError('Could not parse %s' % field_value.value)
880 return fv
881
882 def _ParseOneUserFieldValue(self, value, field_id):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100883 # type: (str, int) -> mrproto.tracker_pb2.FieldValue
Copybara854996b2021-09-07 19:36:02 +0000884 """Replacement for the obsolete user parsing in ParseOneFieldValue."""
885 user_id = rnc.IngestUserName(self.cnxn, value, self.services)
886 return tbo.MakeFieldValue(field_id, None, None, user_id, None, None, False)
887
888 def _ExtractOwner(self, issue, ingestedDict, err_agg):
889 # type: (api_proto.issue_objects_pb2.Issue, Dict[str, Any], ErrorAggregator)
890 # -> None
891 """Fills 'owner' into `ingestedDict`, if it can be extracted."""
892 if issue.HasField('owner'):
893 try:
894 # Unlike for cc's, we require owner be an existing user, thus call we
895 # do not autocreate.
896 ingestedDict['owner_id'] = rnc.IngestUserName(
897 self.cnxn, issue.owner.user, self.services, autocreate=False)
898 except exceptions.InputException as e:
899 err_agg.AddErrorMessage(
900 'Error ingesting owner ({}): {}', issue.owner.user, e)
901 except exceptions.NoSuchUserException as e:
902 err_agg.AddErrorMessage(
903 'User ({}) not found when ingesting owner', e)
904 else:
905 ingestedDict['owner_id'] = framework_constants.NO_USER_SPECIFIED
906
907 def _ExtractIssueRefs(self, issue, ingestedDict, err_agg):
908 # type: (api_proto.issue_objects_pb2.Issue, Dict[str, Any], ErrorAggregator)
909 # -> None
910 """Fills issue relationships into `ingestedDict` from `issue`."""
911 if issue.HasField('merged_into_issue_ref'):
912 try:
913 merged_into_ref = self._IngestIssueRef(issue.merged_into_issue_ref)
914 if isinstance(merged_into_ref, tracker_pb2.DanglingIssueRef):
915 ingestedDict['merged_into_external'] = (
916 merged_into_ref.ext_issue_identifier)
917 else:
918 ingestedDict['merged_into'] = merged_into_ref
919 except exceptions.InputException as e:
920 err_agg.AddErrorMessage(
921 'Error ingesting ref {}: {}', issue.merged_into_issue_ref, e)
922 try:
923 iids, dangling_refs = self._IngestIssueRefs(issue.blocked_on_issue_refs)
924 ingestedDict['blocked_on_iids'] = iids
925 ingestedDict['dangling_blocked_on_refs'] = dangling_refs
926 except exceptions.InputException as e:
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100927 err_agg.AddErrorMessage(str(e))
Copybara854996b2021-09-07 19:36:02 +0000928 try:
929 iids, dangling_refs = self._IngestIssueRefs(issue.blocking_issue_refs)
930 ingestedDict['blocking_iids'] = iids
931 ingestedDict['dangling_blocking_refs'] = dangling_refs
932 except exceptions.InputException as e:
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100933 err_agg.AddErrorMessage(str(e))
Copybara854996b2021-09-07 19:36:02 +0000934
935 def _IngestIssueRefs(self, issue_refs):
936 # type: (api_proto.issue_objects.IssueRf) ->
937 # Tuple[Sequence[int], Sequence[tracker_pb2.DanglingIssueRef]]
938 """Given protoc IssueRefs, returns issue_ids and DanglingIssueRefs."""
939 issue_ids = []
940 external_refs = []
941 with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
942 for ref in issue_refs:
943 try:
944 ingested_ref = self._IngestIssueRef(ref)
945 if isinstance(ingested_ref, tracker_pb2.DanglingIssueRef):
946 external_refs.append(ingested_ref)
947 else:
948 issue_ids.append(ingested_ref)
949 except (exceptions.InputException, exceptions.NoSuchIssueException,
950 exceptions.NoSuchProjectException) as e:
951 err_agg.AddErrorMessage('Error ingesting ref {}: {}', ref, e)
952
953 return issue_ids, external_refs
954
955 def _IngestIssueRef(self, issue_ref):
956 # type: (api_proto.issue_objects.IssueRef) ->
957 # Union[int, tracker_pb2.DanglingIssueRef]
958 """Given a protoc IssueRef, returns an issue id or DanglingIssueRef."""
959 if issue_ref.issue and issue_ref.ext_identifier:
960 raise exceptions.InputException(
961 'IssueRefs MUST NOT have both `issue` and `ext_identifier`')
962 if issue_ref.issue:
963 return rnc.IngestIssueName(self.cnxn, issue_ref.issue, self.services)
964 if issue_ref.ext_identifier:
965 # TODO(crbug.com/monorail/7208): Handle ingestion/conversion of CodeSite
966 # refs. We may be able to avoid ever needing to ingest them.
967 return tracker_pb2.DanglingIssueRef(
968 ext_issue_identifier=issue_ref.ext_identifier
969 )
970 raise exceptions.InputException(
971 'IssueRefs MUST have one of `issue` and `ext_identifier`')
972
973 def IngestIssuesListColumns(self, issues_list_columns):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100974 # type: (Sequence[mrproto.issue_objects_pb2.IssuesListColumn] -> str
Copybara854996b2021-09-07 19:36:02 +0000975 """Ingest a list of protoc IssueListColumns and returns a string."""
976 return ' '.join([col.column for col in issues_list_columns])
977
978 def _ComputeIssuesListColumns(self, columns):
979 # type: (string) -> Sequence[api_proto.issue_objects_pb2.IssuesListColumn]
980 """Convert string representation of columns to protoc IssuesListColumns"""
981 return [
982 issue_objects_pb2.IssuesListColumn(column=col)
983 for col in columns.split()
984 ]
985
986 def IngestNotifyType(self, notify):
987 # type: (issue_pb.NotifyType) -> bool
988 """Ingest a NotifyType to boolean."""
989 if (notify == issues_pb2.NotifyType.Value('NOTIFY_TYPE_UNSPECIFIED') or
990 notify == issues_pb2.NotifyType.Value('EMAIL')):
991 return True
992 elif notify == issues_pb2.NotifyType.Value('NO_NOTIFICATION'):
993 return False
994
995 # Users
996
997 def ConvertUser(self, user):
998 # type: (protorpc.User) -> api_proto.user_objects_pb2.User
999 """Convert a protorpc User into a protoc User.
1000
1001 Args:
1002 user: protorpc User object.
1003
1004 Returns:
1005 The protoc User object.
1006 """
1007 return self.ConvertUsers([user.user_id])[user.user_id]
1008
1009
1010 # TODO(crbug/monorail/7238): Make this take in a full User object and
1011 # return a Sequence, rather than a map, after hotlist users are converted.
1012 def ConvertUsers(self, user_ids):
1013 # type: (Sequence[int]) -> Map(int, api_proto.user_objects_pb2.User)
1014 """Convert list of protorpc Users into list of protoc Users.
1015
1016 Args:
1017 user_ids: List of User IDs.
1018
1019 Returns:
1020 Dict of User IDs to User protos for given user_ids that could be found.
1021 """
1022 user_ids_to_names = {}
1023
1024 # Get display names
1025 users_by_id = self.services.user.GetUsersByIDs(self.cnxn, user_ids)
1026 (display_names_by_id,
1027 display_emails_by_id) = framework_bizobj.CreateUserDisplayNamesAndEmails(
1028 self.cnxn, self.services, self.user_auth, users_by_id.values())
1029
1030 for user_id, user in users_by_id.items():
1031 name = rnc.ConvertUserNames([user_id]).get(user_id)
1032
1033 display_name = display_names_by_id.get(user_id)
1034 display_email = display_emails_by_id.get(user_id)
1035 availability = framework_helpers.GetUserAvailability(user)
1036 availability_message, _availability_status = availability
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +02001037 last_visit_timestamp = user.last_visit_timestamp
Copybara854996b2021-09-07 19:36:02 +00001038
1039 user_ids_to_names[user_id] = user_objects_pb2.User(
1040 name=name,
1041 display_name=display_name,
1042 email=display_email,
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +02001043 availability_message=availability_message,
1044 last_visit_timestamp=last_visit_timestamp)
Copybara854996b2021-09-07 19:36:02 +00001045
1046 return user_ids_to_names
1047
1048 def ConvertProjectStars(self, user_id, projects):
1049 # type: (int, Collection[protorpc.Project]) ->
1050 # Collection[api_proto.user_objects_pb2.ProjectStar]
1051 """Convert list of protorpc Projects into protoc ProjectStars.
1052
1053 Args:
1054 user_id: The user the ProjectStar is associated with.
1055 projects: All starred projects.
1056
1057 Returns:
1058 List of ProjectStar messages.
1059 """
1060 api_project_stars = []
1061 for proj in projects:
1062 name = rnc.ConvertProjectStarName(
1063 self.cnxn, user_id, proj.project_id, self.services)
1064 star = user_objects_pb2.ProjectStar(name=name)
1065 api_project_stars.append(star)
1066 return api_project_stars
1067
1068 # Field Defs
1069
1070 def ConvertFieldDefs(self, field_defs, project_id):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001071 # type: (Sequence[mrproto.tracker_pb2.FieldDef], int) ->
Copybara854996b2021-09-07 19:36:02 +00001072 # Sequence[api_proto.project_objects_pb2.FieldDef]
1073 """Convert sequence of protorpc FieldDefs to protoc FieldDefs.
1074
1075 Args:
1076 field_defs: List of protorpc FieldDefs
1077 project_id: ID of the Project that is ancestor to all given
1078 `field_defs`.
1079
1080 Returns:
1081 Sequence of protoc FieldDef in the same order they are given in
1082 `field_defs`. In the event any field_def or the referenced approval_id
1083 in `field_defs` is not found, they will be omitted from the result.
1084 """
1085 field_ids = [fd.field_id for fd in field_defs]
1086 resource_names_dict = rnc.ConvertFieldDefNames(
1087 self.cnxn, field_ids, project_id, self.services)
1088 parent_approval_ids = [
1089 fd.approval_id for fd in field_defs if fd.approval_id is not None
1090 ]
1091 approval_names_dict = rnc.ConvertApprovalDefNames(
1092 self.cnxn, parent_approval_ids, project_id, self.services)
1093
1094 api_fds = []
1095 for fd in field_defs:
1096 # Skip over approval fields, they have their separate ApprovalDef
1097 if fd.field_type == tracker_pb2.FieldTypes.APPROVAL_TYPE:
1098 continue
1099 if fd.field_id not in resource_names_dict:
1100 continue
1101
1102 name = resource_names_dict.get(fd.field_id)
1103 display_name = fd.field_name
1104 docstring = fd.docstring
1105 field_type = self._ConvertFieldDefType(fd.field_type)
1106 applicable_issue_type = fd.applicable_type
1107 admins = rnc.ConvertUserNames(fd.admin_ids).values()
1108 editors = rnc.ConvertUserNames(fd.editor_ids).values()
1109 traits = self._ComputeFieldDefTraits(fd)
1110 approval_parent = approval_names_dict.get(fd.approval_id)
1111
1112 enum_settings = None
1113 if field_type == project_objects_pb2.FieldDef.Type.Value('ENUM'):
1114 enum_settings = project_objects_pb2.FieldDef.EnumTypeSettings(
1115 choices=self._GetEnumFieldChoices(fd))
1116
1117 int_settings = None
1118 if field_type == project_objects_pb2.FieldDef.Type.Value('INT'):
1119 int_settings = project_objects_pb2.FieldDef.IntTypeSettings(
1120 min_value=fd.min_value, max_value=fd.max_value)
1121
1122 str_settings = None
1123 if field_type == project_objects_pb2.FieldDef.Type.Value('STR'):
1124 str_settings = project_objects_pb2.FieldDef.StrTypeSettings(
1125 regex=fd.regex)
1126
1127 user_settings = None
1128 if field_type == project_objects_pb2.FieldDef.Type.Value('USER'):
1129 user_settings = project_objects_pb2.FieldDef.UserTypeSettings(
1130 role_requirements=self._ConvertRoleRequirements(fd.needs_member),
1131 notify_triggers=self._ConvertNotifyTriggers(fd.notify_on),
1132 grants_perm=fd.grants_perm,
1133 needs_perm=fd.needs_perm)
1134
1135 date_settings = None
1136 if field_type == project_objects_pb2.FieldDef.Type.Value('DATE'):
1137 date_settings = project_objects_pb2.FieldDef.DateTypeSettings(
1138 date_action=self._ConvertDateAction(fd.date_action))
1139
1140 api_fd = project_objects_pb2.FieldDef(
1141 name=name,
1142 display_name=display_name,
1143 docstring=docstring,
1144 type=field_type,
1145 applicable_issue_type=applicable_issue_type,
1146 admins=admins,
1147 traits=traits,
1148 approval_parent=approval_parent,
1149 enum_settings=enum_settings,
1150 int_settings=int_settings,
1151 str_settings=str_settings,
1152 user_settings=user_settings,
1153 date_settings=date_settings,
1154 editors=editors)
1155 api_fds.append(api_fd)
1156 return api_fds
1157
1158 def _ConvertDateAction(self, date_action):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001159 # type: (mrproto.tracker_pb2.DateAction) ->
Copybara854996b2021-09-07 19:36:02 +00001160 # api_proto.project_objects_pb2.FieldDef.DateTypeSettings.DateAction
1161 """Convert protorpc DateAction to protoc
1162 FieldDef.DateTypeSettings.DateAction"""
1163 if date_action == tracker_pb2.DateAction.NO_ACTION:
1164 return project_objects_pb2.FieldDef.DateTypeSettings.DateAction.Value(
1165 'NO_ACTION')
1166 elif date_action == tracker_pb2.DateAction.PING_OWNER_ONLY:
1167 return project_objects_pb2.FieldDef.DateTypeSettings.DateAction.Value(
1168 'NOTIFY_OWNER')
1169 elif date_action == tracker_pb2.DateAction.PING_PARTICIPANTS:
1170 return project_objects_pb2.FieldDef.DateTypeSettings.DateAction.Value(
1171 'NOTIFY_PARTICIPANTS')
1172 else:
1173 raise ValueError('Unsupported DateAction Value')
1174
1175 def _ConvertRoleRequirements(self, needs_member):
1176 # type: (bool) ->
1177 # api_proto.project_objects_pb2.FieldDef.
1178 # UserTypeSettings.RoleRequirements
1179 """Convert protorpc RoleRequirements to protoc
1180 FieldDef.UserTypeSettings.RoleRequirements"""
1181
1182 proto_user_settings = project_objects_pb2.FieldDef.UserTypeSettings
1183 if needs_member:
1184 return proto_user_settings.RoleRequirements.Value('PROJECT_MEMBER')
1185 else:
1186 return proto_user_settings.RoleRequirements.Value('NO_ROLE_REQUIREMENT')
1187
1188 def _ConvertNotifyTriggers(self, notify_trigger):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001189 # type: (mrproto.tracker_pb2.NotifyTriggers) ->
Copybara854996b2021-09-07 19:36:02 +00001190 # api_proto.project_objects_pb2.FieldDef.UserTypeSettings.NotifyTriggers
1191 """Convert protorpc NotifyTriggers to protoc
1192 FieldDef.UserTypeSettings.NotifyTriggers"""
1193 if notify_trigger == tracker_pb2.NotifyTriggers.NEVER:
1194 return project_objects_pb2.FieldDef.UserTypeSettings.NotifyTriggers.Value(
1195 'NEVER')
1196 elif notify_trigger == tracker_pb2.NotifyTriggers.ANY_COMMENT:
1197 return project_objects_pb2.FieldDef.UserTypeSettings.NotifyTriggers.Value(
1198 'ANY_COMMENT')
1199 else:
1200 raise ValueError('Unsupported NotifyTriggers Value')
1201
1202 def _ConvertFieldDefType(self, field_type):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001203 # type: (mrproto.tracker_pb2.FieldTypes) ->
Copybara854996b2021-09-07 19:36:02 +00001204 # api_proto.project_objects_pb2.FieldDef.Type
1205 """Convert protorpc FieldType to protoc FieldDef.Type
1206
1207 Args:
1208 field_type: protorpc FieldType
1209
1210 Returns:
1211 Corresponding protoc FieldDef.Type
1212
1213 Raises:
1214 ValueError if input `field_type` has no suitable supported FieldDef.Type,
1215 or input `field_type` is not a recognized enum option.
1216 """
1217 if field_type == tracker_pb2.FieldTypes.ENUM_TYPE:
1218 return project_objects_pb2.FieldDef.Type.Value('ENUM')
1219 elif field_type == tracker_pb2.FieldTypes.INT_TYPE:
1220 return project_objects_pb2.FieldDef.Type.Value('INT')
1221 elif field_type == tracker_pb2.FieldTypes.STR_TYPE:
1222 return project_objects_pb2.FieldDef.Type.Value('STR')
1223 elif field_type == tracker_pb2.FieldTypes.USER_TYPE:
1224 return project_objects_pb2.FieldDef.Type.Value('USER')
1225 elif field_type == tracker_pb2.FieldTypes.DATE_TYPE:
1226 return project_objects_pb2.FieldDef.Type.Value('DATE')
1227 elif field_type == tracker_pb2.FieldTypes.URL_TYPE:
1228 return project_objects_pb2.FieldDef.Type.Value('URL')
1229 else:
1230 raise ValueError(
1231 'Unsupported tracker_pb2.FieldType enum. Boolean types '
1232 'are unsupported and approval types are found in ApprovalDefs')
1233
1234 def _ComputeFieldDefTraits(self, field_def):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001235 # type: (mrproto.tracker_pb2.FieldDef) ->
Copybara854996b2021-09-07 19:36:02 +00001236 # Sequence[api_proto.project_objects_pb2.FieldDef.Traits]
1237 """Compute sequence of FieldDef.Traits for a given protorpc FieldDef."""
1238 trait_protos = []
1239 if field_def.is_required:
1240 trait_protos.append(project_objects_pb2.FieldDef.Traits.Value('REQUIRED'))
1241 if field_def.is_niche:
1242 trait_protos.append(
1243 project_objects_pb2.FieldDef.Traits.Value('DEFAULT_HIDDEN'))
1244 if field_def.is_multivalued:
1245 trait_protos.append(
1246 project_objects_pb2.FieldDef.Traits.Value('MULTIVALUED'))
1247 if field_def.is_phase_field:
1248 trait_protos.append(project_objects_pb2.FieldDef.Traits.Value('PHASE'))
1249 if field_def.is_restricted_field:
1250 trait_protos.append(
1251 project_objects_pb2.FieldDef.Traits.Value('RESTRICTED'))
1252 return trait_protos
1253
1254 def _GetEnumFieldChoices(self, field_def):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001255 # type: (mrproto.tracker_pb2.FieldDef) ->
Copybara854996b2021-09-07 19:36:02 +00001256 # Sequence[Choice]
1257 """Get sequence of choices for an enum field
1258
1259 Args:
1260 field_def: protorpc FieldDef
1261
1262 Returns:
1263 Sequence of valid Choices for enum field `field_def`.
1264
1265 Raises:
1266 ValueError if input `field_def` is not an enum type field.
1267 """
1268 if field_def.field_type != tracker_pb2.FieldTypes.ENUM_TYPE:
1269 raise ValueError('Cannot get value from label for non-enum-type field')
1270
1271 config = self.services.config.GetProjectConfig(
1272 self.cnxn, field_def.project_id)
1273 value_docstr_tuples = tracker_helpers._GetEnumFieldValuesAndDocstrings(
1274 field_def, config)
1275
1276 return [
1277 Choice(value=value, docstring=docstring)
1278 for value, docstring in value_docstr_tuples
1279 ]
1280
1281 # Field Values
1282
1283 def _GetNonApprovalFieldValues(self, field_values, project_id):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001284 # type: (Sequence[mrproto.tracker_pb2.FieldValue], int) ->
1285 # Sequence[mrproto.tracker_pb2.FieldValue]
Copybara854996b2021-09-07 19:36:02 +00001286 """Filter out field values that belong to an approval field."""
1287 config = self.services.config.GetProjectConfig(self.cnxn, project_id)
1288 approval_fd_ids = set(
1289 [fd.field_id for fd in config.field_defs if fd.approval_id])
1290
1291 return [fv for fv in field_values if fv.field_id not in approval_fd_ids]
1292
1293 def ConvertFieldValues(self, field_values, project_id, phases):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001294 # type: (Sequence[mrproto.tracker_pb2.FieldValue], int,
1295 # Sequence[mrproto.tracker_pb2.Phase]) ->
Copybara854996b2021-09-07 19:36:02 +00001296 # Sequence[api_proto.issue_objects_pb2.FieldValue]
1297 """Convert sequence of field_values to protoc FieldValues.
1298
1299 This method does not handle enum_type fields.
1300
1301 Args:
1302 field_values: List of FieldValues
1303 project_id: ID of the Project that is ancestor to all given
1304 `field_values`.
1305 phases: List of Phases
1306
1307 Returns:
1308 Sequence of protoc FieldValues in the same order they are given in
1309 `field_values`. In the event any field_values in `field_values` are not
1310 found, they will be omitted from the result.
1311 """
1312 phase_names_by_id = {phase.phase_id: phase.name for phase in phases}
1313 field_ids = [fv.field_id for fv in field_values]
1314 resource_names_dict = rnc.ConvertFieldDefNames(
1315 self.cnxn, field_ids, project_id, self.services)
1316
1317 api_fvs = []
1318 for fv in field_values:
1319 if fv.field_id not in resource_names_dict:
1320 continue
1321
1322 name = resource_names_dict.get(fv.field_id)
1323 value = self._ComputeFieldValueString(fv)
1324 derivation = self._ComputeFieldValueDerivation(fv)
1325 phase = phase_names_by_id.get(fv.phase_id)
1326 api_item = issue_objects_pb2.FieldValue(
1327 field=name, value=value, derivation=derivation, phase=phase)
1328 api_fvs.append(api_item)
1329
1330 return api_fvs
1331
1332 def _ComputeFieldValueString(self, field_value):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001333 # type: (mrproto.tracker_pb2.FieldValue) -> str
Copybara854996b2021-09-07 19:36:02 +00001334 """Convert a FieldValue's value to a string."""
1335 if field_value is None:
1336 raise exceptions.InputException('No FieldValue specified')
1337 elif field_value.int_value is not None:
1338 return str(field_value.int_value)
1339 elif field_value.str_value is not None:
1340 return field_value.str_value
1341 elif field_value.user_id is not None:
1342 return rnc.ConvertUserNames([field_value.user_id
1343 ]).get(field_value.user_id)
1344 elif field_value.date_value is not None:
1345 return str(field_value.date_value)
1346 elif field_value.url_value is not None:
1347 return field_value.url_value
1348 else:
1349 raise exceptions.InputException('FieldValue must have at least one value')
1350
1351 def _ComputeFieldValueDerivation(self, field_value):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001352 # type: (mrproto.tracker_pb2.FieldValue) ->
Copybara854996b2021-09-07 19:36:02 +00001353 # api_proto.issue_objects_pb2.Issue.Derivation
1354 """Convert a FieldValue's 'derived' to a protoc Issue.Derivation.
1355
1356 Args:
1357 field_value: protorpc FieldValue
1358
1359 Returns:
1360 Issue.Derivation of given `field_value`
1361 """
1362 if field_value.derived:
1363 return issue_objects_pb2.Derivation.Value('RULE')
1364 else:
1365 return issue_objects_pb2.Derivation.Value('EXPLICIT')
1366
1367 # Approval Def
1368
1369 def ConvertApprovalDefs(self, approval_defs, project_id):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001370 # type: (Sequence[mrproto.tracker_pb2.ApprovalDef], int) ->
Copybara854996b2021-09-07 19:36:02 +00001371 # Sequence[api_proto.project_objects_pb2.ApprovalDef]
1372 """Convert sequence of protorpc ApprovalDefs to protoc ApprovalDefs.
1373
1374 Args:
1375 approval_defs: List of protorpc ApprovalDefs
1376 project_id: ID of the Project the approval_defs belong to.
1377
1378 Returns:
1379 Sequence of protoc ApprovalDefs in the same order they are given in
1380 in `approval_defs`. In the event any approval_def in `approval_defs`
1381 are not found, they will be omitted from the result.
1382 """
1383 approval_ids = set([ad.approval_id for ad in approval_defs])
1384 resource_names_dict = rnc.ConvertApprovalDefNames(
1385 self.cnxn, approval_ids, project_id, self.services)
1386
1387 # Get matching field defs, needed to fill out protoc ApprovalDefs
1388 config = self.services.config.GetProjectConfig(self.cnxn, project_id)
1389 fd_by_id = {}
1390 for fd in config.field_defs:
1391 if (fd.field_type == tracker_pb2.FieldTypes.APPROVAL_TYPE and
1392 fd.field_id in approval_ids):
1393 fd_by_id[fd.field_id] = fd
1394
1395 all_users = tbo.UsersInvolvedInApprovalDefs(
1396 approval_defs, fd_by_id.values())
1397 user_resource_names_dict = rnc.ConvertUserNames(all_users)
1398
1399 api_ads = []
1400 for ad in approval_defs:
1401 if (ad.approval_id not in resource_names_dict or
1402 ad.approval_id not in fd_by_id):
1403 continue
1404 matching_fd = fd_by_id.get(ad.approval_id)
1405 name = resource_names_dict.get(ad.approval_id)
1406 display_name = matching_fd.field_name
1407 docstring = matching_fd.docstring
1408 survey = ad.survey
1409 approvers = [
1410 user_resource_names_dict.get(approver_id)
1411 for approver_id in ad.approver_ids
1412 ]
1413 admins = [
1414 user_resource_names_dict.get(admin_id)
1415 for admin_id in matching_fd.admin_ids
1416 ]
1417
1418 api_ad = project_objects_pb2.ApprovalDef(
1419 name=name,
1420 display_name=display_name,
1421 docstring=docstring,
1422 survey=survey,
1423 approvers=approvers,
1424 admins=admins)
1425 api_ads.append(api_ad)
1426 return api_ads
1427
1428 def ConvertApprovalValues(self, approval_values, field_values, phases,
1429 issue_id=None, project_id=None):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001430 # type: (Sequence[mrproto.tracker_pb2.ApprovalValue],
1431 # Sequence[mrproto.tracker_pb2.FieldValue],
1432 # Sequence[mrproto.tracker_pb2.Phase], Optional[int], Optional[int]) ->
Copybara854996b2021-09-07 19:36:02 +00001433 # Sequence[api_proto.issue_objects_pb2.ApprovalValue]
1434 """Convert sequence of approval_values to protoc ApprovalValues.
1435
1436 `approval_values` may belong to a template or an issue. If they belong to a
1437 template, `project_id` should be given for the project the template is in.
1438 If these are issue `approval_values` `issue_id` should be given`.
1439 So, one of `issue_id` or `project_id` must be provided.
1440 If both are given, we ignore `project_id` and assume the `approval_values`
1441 belong to an issue.
1442
1443 Args:
1444 approval_values: List of ApprovalValues.
1445 field_values: List of FieldValues that may belong to the approval_values.
1446 phases: List of Phases that may be associated with the approval_values.
1447 issue_id: ID of the Issue that the `approval_values` belong to.
1448 project_id: ID of the Project that the `approval_values`
1449 template belongs to.
1450
1451 Returns:
1452 Sequence of protoc ApprovalValues in the same order they are given in
1453 in `approval_values`. In the event any approval definitions in
1454 `approval_values` are not found, they will be omitted from the result.
1455
1456 Raises:
1457 InputException if neither `issue_id` nor `project_id` is given.
1458 """
1459
1460 approval_ids = [av.approval_id for av in approval_values]
1461 resource_names_dict = {}
1462 if issue_id is not None:
1463 # Only issue approval_values have resource names.
1464 resource_names_dict = rnc.ConvertApprovalValueNames(
1465 self.cnxn, issue_id, self.services)
1466 project_id = self.services.issue.GetIssue(self.cnxn, issue_id).project_id
1467 elif project_id is None:
1468 raise exceptions.InputException(
1469 'One `issue_id` or `project_id` must be given.')
1470
1471 phase_names_by_id = {phase.phase_id: phase.name for phase in phases}
1472 ad_names_dict = rnc.ConvertApprovalDefNames(
1473 self.cnxn, approval_ids, project_id, self.services)
1474
1475 # Organize the field values by the approval values they are
1476 # associated with.
1477 config = self.services.config.GetProjectConfig(self.cnxn, project_id)
1478 fds_by_id = {fd.field_id: fd for fd in config.field_defs}
1479 fvs_by_parent_approvals = collections.defaultdict(list)
1480 for fv in field_values:
1481 fd = fds_by_id.get(fv.field_id)
1482 if fd and fd.approval_id:
1483 fvs_by_parent_approvals[fd.approval_id].append(fv)
1484
1485 api_avs = []
1486 for av in approval_values:
1487 # We only skip missing approval names if we are converting issue approval
1488 # values.
1489 if issue_id is not None and av.approval_id not in resource_names_dict:
1490 continue
1491
1492 name = resource_names_dict.get(av.approval_id)
1493 approval_def = ad_names_dict.get(av.approval_id)
1494 approvers = rnc.ConvertUserNames(av.approver_ids).values()
1495 status = self._ComputeApprovalValueStatus(av.status)
1496 setter = rnc.ConvertUserName(av.setter_id)
1497 phase = phase_names_by_id.get(av.phase_id)
1498
1499 field_values = self.ConvertFieldValues(
1500 fvs_by_parent_approvals[av.approval_id], project_id, phases)
1501
1502 api_item = issue_objects_pb2.ApprovalValue(
1503 name=name,
1504 approval_def=approval_def,
1505 approvers=approvers,
1506 status=status,
1507 setter=setter,
1508 field_values=field_values,
1509 phase=phase)
1510 if av.set_on:
1511 api_item.set_time.FromSeconds(av.set_on)
1512 api_avs.append(api_item)
1513
1514 return api_avs
1515
1516 def _ComputeApprovalValueStatus(self, status):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001517 # type: (mrproto.tracker_pb2.ApprovalStatus) ->
Copybara854996b2021-09-07 19:36:02 +00001518 # api_proto.issue_objects_pb2.Issue.ApprovalStatus
1519 """Convert a protorpc ApprovalStatus to a protoc Issue.ApprovalStatus."""
1520 try:
1521 return _APPROVAL_STATUS_CONVERT[status]
1522 except KeyError:
1523 raise ValueError('Unrecognized tracker_pb2.ApprovalStatus enum')
1524
1525 # Projects
1526
1527 def ConvertIssueTemplates(self, project_id, templates):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001528 # type: (int, Sequence[mrproto.tracker_pb2.TemplateDef]) ->
Copybara854996b2021-09-07 19:36:02 +00001529 # Sequence[api_proto.project_objects_pb2.IssueTemplate]
1530 """Convert a Sequence of TemplateDefs to protoc IssueTemplates.
1531
1532 Args:
1533 project_id: ID of the Project the templates belong to.
1534 templates: Sequence of TemplateDef protorpc objects.
1535
1536 Returns:
1537 Sequence of protoc IssueTemplate in the same order they are given in
1538 `templates`. In the rare event that any templates are not found,
1539 they will be omitted from the result.
1540 """
1541 api_templates = []
1542
1543 resource_names_dict = rnc.ConvertTemplateNames(
1544 self.cnxn, project_id, [template.template_id for template in templates],
1545 self.services)
1546
1547 for template in templates:
1548 if template.template_id not in resource_names_dict:
1549 continue
1550 name = resource_names_dict.get(template.template_id)
1551 summary_must_be_edited = template.summary_must_be_edited
1552 template_privacy = self._ComputeTemplatePrivacy(template)
1553 default_owner = self._ComputeTemplateDefaultOwner(template)
1554 component_required = template.component_required
1555 admins = rnc.ConvertUserNames(template.admin_ids).values()
1556 issue = self._FillIssueFromTemplate(template, project_id)
1557 approval_values = self.ConvertApprovalValues(
1558 template.approval_values, template.field_values, template.phases,
1559 project_id=project_id)
1560 api_templates.append(
1561 project_objects_pb2.IssueTemplate(
1562 name=name,
1563 display_name=template.name,
1564 issue=issue,
1565 approval_values=approval_values,
1566 summary_must_be_edited=summary_must_be_edited,
1567 template_privacy=template_privacy,
1568 default_owner=default_owner,
1569 component_required=component_required,
1570 admins=admins))
1571
1572 return api_templates
1573
1574 def _FillIssueFromTemplate(self, template, project_id):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001575 # type: (mrproto.tracker_pb2.TemplateDef, int) ->
Copybara854996b2021-09-07 19:36:02 +00001576 # api_proto.issue_objects_pb2.Issue
1577 """Convert a TemplateDef to its embedded protoc Issue.
1578
1579 IssueTemplate does not set the following fields:
1580 name
1581 reporter
1582 cc_users
1583 blocked_on_issue_refs
1584 blocking_issue_refs
1585 create_time
1586 close_time
1587 modify_time
1588 component_modify_time
1589 status_modify_time
1590 owner_modify_time
1591 attachment_count
1592 star_count
1593
1594 Args:
1595 template: TemplateDef protorpc objects.
1596 project_id: ID of the Project the template belongs to.
1597
1598 Returns:
1599 protoc Issue filled with data from given `template`.
1600 """
1601 summary = template.summary
1602 state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
1603 status = issue_objects_pb2.Issue.StatusValue(
1604 status=template.status,
1605 derivation=issue_objects_pb2.Derivation.Value('EXPLICIT'))
1606 owner = None
1607 if template.owner_id is not None:
1608 owner = issue_objects_pb2.Issue.UserValue(
1609 user=rnc.ConvertUserNames([template.owner_id]).get(template.owner_id))
1610 labels = self.ConvertLabels(template.labels, [], project_id)
1611 components_dict = rnc.ConvertComponentDefNames(
1612 self.cnxn, template.component_ids, project_id, self.services)
1613 components = []
1614 for component_resource_name in components_dict.values():
1615 components.append(
1616 issue_objects_pb2.Issue.ComponentValue(
1617 component=component_resource_name,
1618 derivation=issue_objects_pb2.Derivation.Value('EXPLICIT')))
1619 non_approval_fvs = self._GetNonApprovalFieldValues(
1620 template.field_values, project_id)
1621 field_values = self.ConvertFieldValues(
1622 non_approval_fvs, project_id, template.phases)
1623 field_values.extend(
1624 self.ConvertEnumFieldValues(template.labels, [], project_id))
1625 phases = self._ComputePhases(template.phases)
1626
1627 filled_issue = issue_objects_pb2.Issue(
1628 summary=summary,
1629 state=state,
1630 status=status,
1631 owner=owner,
1632 labels=labels,
1633 components=components,
1634 field_values=field_values,
1635 phases=phases)
1636 return filled_issue
1637
1638 def _ComputeTemplatePrivacy(self, template):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001639 # type: (mrproto.tracker_pb2.TemplateDef) ->
Copybara854996b2021-09-07 19:36:02 +00001640 # api_proto.project_objects_pb2.IssueTemplate.TemplatePrivacy
1641 """Convert a protorpc TemplateDef to its protoc TemplatePrivacy."""
1642 if template.members_only:
1643 return project_objects_pb2.IssueTemplate.TemplatePrivacy.Value(
1644 'MEMBERS_ONLY')
1645 else:
1646 return project_objects_pb2.IssueTemplate.TemplatePrivacy.Value('PUBLIC')
1647
1648 def _ComputeTemplateDefaultOwner(self, template):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001649 # type: (mrproto.tracker_pb2.TemplateDef) ->
Copybara854996b2021-09-07 19:36:02 +00001650 # api_proto.project_objects_pb2.IssueTemplate.DefaultOwner
1651 """Convert a protorpc TemplateDef to its protoc DefaultOwner."""
1652 if template.owner_defaults_to_member:
1653 return project_objects_pb2.IssueTemplate.DefaultOwner.Value(
1654 'PROJECT_MEMBER_REPORTER')
1655 else:
1656 return project_objects_pb2.IssueTemplate.DefaultOwner.Value(
1657 'DEFAULT_OWNER_UNSPECIFIED')
1658
1659 def _ComputePhases(self, phases):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001660 # type: (mrproto.tracker_pb2.TemplateDef) -> Sequence[str]
Copybara854996b2021-09-07 19:36:02 +00001661 """Convert a protorpc TemplateDef to its sorted string phases."""
1662 sorted_phases = sorted(phases, key=lambda phase: phase.rank)
1663 return [phase.name for phase in sorted_phases]
1664
1665 def ConvertLabels(self, labels, derived_labels, project_id):
1666 # type: (Sequence[str], Sequence[str], int) ->
1667 # Sequence[api_proto.issue_objects_pb2.Issue.LabelValue]
1668 """Convert string labels to LabelValues for non-enum-field labels
1669
1670 Args:
1671 labels: Sequence of string labels
1672 project_id: ID of the Project these labels belong to.
1673
1674 Return:
1675 Sequence of protoc IssueValues for given `labels` that
1676 do not represent enum field values.
1677 """
1678 config = self.services.config.GetProjectConfig(self.cnxn, project_id)
1679 non_fd_labels, non_fd_der_labels = tbo.ExplicitAndDerivedNonMaskedLabels(
1680 labels, derived_labels, config)
1681 api_labels = []
1682 for label in non_fd_labels:
1683 api_labels.append(
1684 issue_objects_pb2.Issue.LabelValue(
1685 label=label,
1686 derivation=issue_objects_pb2.Derivation.Value('EXPLICIT')))
1687 for label in non_fd_der_labels:
1688 api_labels.append(
1689 issue_objects_pb2.Issue.LabelValue(
1690 label=label,
1691 derivation=issue_objects_pb2.Derivation.Value('RULE')))
1692 return api_labels
1693
1694 def ConvertEnumFieldValues(self, labels, derived_labels, project_id):
1695 # type: (Sequence[str], Sequence[str], int) ->
1696 # Sequence[api_proto.issue_objects_pb2.FieldValue]
1697 """Convert string labels to FieldValues for enum-field labels
1698
1699 Args:
1700 labels: Sequence of string labels
1701 project_id: ID of the Project these labels belong to.
1702
1703 Return:
1704 Sequence of protoc FieldValues only for given `labels` that
1705 represent enum field values.
1706 """
1707 config = self.services.config.GetProjectConfig(self.cnxn, project_id)
1708 enum_ids_by_name = {
1709 fd.field_name.lower(): fd.field_id
1710 for fd in config.field_defs
1711 if fd.field_type is tracker_pb2.FieldTypes.ENUM_TYPE
1712 and not fd.is_deleted
1713 }
1714 resource_names_dict = rnc.ConvertFieldDefNames(
1715 self.cnxn, enum_ids_by_name.values(), project_id, self.services)
1716
1717 api_fvs = []
1718
1719 labels_by_prefix = tbo.LabelsByPrefix(labels, enum_ids_by_name.keys())
1720 for lower_field_name, values in labels_by_prefix.items():
1721 field_id = enum_ids_by_name.get(lower_field_name)
1722 resource_name = resource_names_dict.get(field_id)
1723 if not resource_name:
1724 continue
1725 api_fvs.extend(
1726 [
1727 issue_objects_pb2.FieldValue(
1728 field=resource_name,
1729 value=value,
1730 derivation=issue_objects_pb2.Derivation.Value(
1731 'EXPLICIT')) for value in values
1732 ])
1733
1734 der_labels_by_prefix = tbo.LabelsByPrefix(
1735 derived_labels, enum_ids_by_name.keys())
1736 for lower_field_name, values in der_labels_by_prefix.items():
1737 field_id = enum_ids_by_name.get(lower_field_name)
1738 resource_name = resource_names_dict.get(field_id)
1739 if not resource_name:
1740 continue
1741 api_fvs.extend(
1742 [
1743 issue_objects_pb2.FieldValue(
1744 field=resource_name,
1745 value=value,
1746 derivation=issue_objects_pb2.Derivation.Value('RULE'))
1747 for value in values
1748 ])
1749
1750 return api_fvs
1751
1752 def ConvertProject(self, project):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001753 # type: (mrproto.project_pb2.Project) ->
Copybara854996b2021-09-07 19:36:02 +00001754 # api_proto.project_objects_pb2.Project
1755 """Convert a protorpc Project to its protoc Project."""
1756
1757 return project_objects_pb2.Project(
1758 name=rnc.ConvertProjectName(
1759 self.cnxn, project.project_id, self.services),
1760 display_name=project.project_name,
1761 summary=project.summary,
1762 thumbnail_url=project_helpers.GetThumbnailUrl(project.logo_gcs_id))
1763
1764 def ConvertProjects(self, projects):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001765 # type: (Sequence[mrproto.project_pb2.Project]) ->
Copybara854996b2021-09-07 19:36:02 +00001766 # Sequence[api_proto.project_objects_pb2.Project]
1767 """Convert a Sequence of protorpc Projects to protoc Projects."""
1768 return [self.ConvertProject(proj) for proj in projects]
1769
1770 def ConvertProjectConfig(self, project_config):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001771 # type: (mrproto.tracker_pb2.ProjectIssueConfig) ->
Copybara854996b2021-09-07 19:36:02 +00001772 # api_proto.project_objects_pb2.ProjectConfig
1773 """Convert protorpc ProjectIssueConfig to protoc ProjectConfig."""
1774 project = self.services.project.GetProject(
1775 self.cnxn, project_config.project_id)
1776 project_grid_config = project_objects_pb2.ProjectConfig.GridViewConfig(
1777 default_x_attr=project_config.default_x_attr,
1778 default_y_attr=project_config.default_y_attr)
1779 template_names = rnc.ConvertTemplateNames(
1780 self.cnxn, project_config.project_id, [
1781 project_config.default_template_for_developers,
1782 project_config.default_template_for_users
1783 ], self.services)
1784 return project_objects_pb2.ProjectConfig(
1785 name=rnc.ConvertProjectConfigName(
1786 self.cnxn, project_config.project_id, self.services),
1787 exclusive_label_prefixes=project_config.exclusive_label_prefixes,
1788 member_default_query=project_config.member_default_query,
1789 default_sort=project_config.default_sort_spec,
1790 default_columns=self._ComputeIssuesListColumns(
1791 project_config.default_col_spec),
1792 project_grid_config=project_grid_config,
1793 member_default_template=template_names.get(
1794 project_config.default_template_for_developers),
1795 non_members_default_template=template_names.get(
1796 project_config.default_template_for_users),
1797 revision_url_format=project.revision_url_format,
1798 custom_issue_entry_url=project_config.custom_issue_entry_url)
1799
1800 def CreateProjectMember(self, cnxn, project_id, user_id, role):
1801 # type: (MonorailContext, int, int, str) ->
1802 # api_proto.project_objects_pb2.ProjectMember
1803 """Creates a ProjectMember object from specified parameters.
1804
1805 Args:
1806 cnxn: MonorailConnection object.
1807 project_id: ID of the Project the User is a member of.
1808 user_id: ID of the user who is a member.
1809 role: str specifying the user's role based on a ProjectRole value.
1810
1811 Return:
1812 A protoc ProjectMember object.
1813 """
1814 name = rnc.ConvertProjectMemberName(
1815 cnxn, project_id, user_id, self.services)
1816 return project_objects_pb2.ProjectMember(
1817 name=name,
1818 role=project_objects_pb2.ProjectMember.ProjectRole.Value(role))
1819
1820 def ConvertLabelDefs(self, label_defs, project_id):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001821 # type: (Sequence[mrproto.tracker_pb2.LabelDef], int) ->
Copybara854996b2021-09-07 19:36:02 +00001822 # Sequence[api_proto.project_objects_pb2.LabelDef]
1823 """Convert protorpc LabelDefs to protoc LabelDefs"""
1824 resource_names_dict = rnc.ConvertLabelDefNames(
1825 self.cnxn, [ld.label for ld in label_defs], project_id, self.services)
1826
1827 api_lds = []
1828 for ld in label_defs:
1829 state = project_objects_pb2.LabelDef.LabelDefState.Value('ACTIVE')
1830 if ld.deprecated:
1831 state = project_objects_pb2.LabelDef.LabelDefState.Value('DEPRECATED')
1832 api_lds.append(
1833 project_objects_pb2.LabelDef(
1834 name=resource_names_dict.get(ld.label),
1835 value=ld.label,
1836 docstring=ld.label_docstring,
1837 state=state))
1838 return api_lds
1839
1840 def ConvertStatusDefs(self, status_defs, project_id):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001841 # type: (Sequence[mrproto.tracker_pb2.StatusDef], int) ->
Copybara854996b2021-09-07 19:36:02 +00001842 # Sequence[api_proto.project_objects_pb2.StatusDef]
1843 """Convert protorpc StatusDefs to protoc StatusDefs
1844
1845 Args:
1846 status_defs: Sequence of StatusDefs.
1847 project_id: ID of the Project these belong to.
1848
1849 Returns:
1850 Sequence of protoc StatusDefs in the same order they are given in
1851 `status_defs`.
1852 """
1853 resource_names_dict = rnc.ConvertStatusDefNames(
1854 self.cnxn, [sd.status for sd in status_defs], project_id, self.services)
1855 config = self.services.config.GetProjectConfig(self.cnxn, project_id)
1856 mergeable_statuses = set(config.statuses_offer_merge)
1857
1858 # Rank is only surfaced as positional value in well_known_statuses
1859 rank_by_status = {}
1860 for rank, sd in enumerate(config.well_known_statuses):
1861 rank_by_status[sd.status] = rank
1862
1863 api_sds = []
1864 for sd in status_defs:
1865 state = project_objects_pb2.StatusDef.StatusDefState.Value('ACTIVE')
1866 if sd.deprecated:
1867 state = project_objects_pb2.StatusDef.StatusDefState.Value('DEPRECATED')
1868
1869 if sd.means_open:
1870 status_type = project_objects_pb2.StatusDef.StatusDefType.Value('OPEN')
1871 else:
1872 if sd.status in mergeable_statuses:
1873 status_type = project_objects_pb2.StatusDef.StatusDefType.Value(
1874 'MERGED')
1875 else:
1876 status_type = project_objects_pb2.StatusDef.StatusDefType.Value(
1877 'CLOSED')
1878
1879 api_sd = project_objects_pb2.StatusDef(
1880 name=resource_names_dict.get(sd.status),
1881 value=sd.status,
1882 type=status_type,
1883 rank=rank_by_status[sd.status],
1884 docstring=sd.status_docstring,
1885 state=state,
1886 )
1887 api_sds.append(api_sd)
1888 return api_sds
1889
1890 def ConvertComponentDef(self, component_def):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001891 # type: (mrproto.tracker_pb2.ComponentDef) ->
Copybara854996b2021-09-07 19:36:02 +00001892 # api_proto.project_objects.ComponentDef
1893 """Convert a protorpc ComponentDef to a protoc ComponentDef."""
1894 return self.ConvertComponentDefs([component_def],
1895 component_def.project_id)[0]
1896
1897 def ConvertComponentDefs(self, component_defs, project_id):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001898 # type: (Sequence[mrproto.tracker_pb2.ComponentDef], int) ->
Copybara854996b2021-09-07 19:36:02 +00001899 # Sequence[api_proto.project_objects.ComponentDef]
1900 """Convert sequence of protorpc ComponentDefs to protoc ComponentDefs
1901
1902 Args:
1903 component_defs: Sequence of protoc ComponentDefs.
1904 project_id: ID of the Project these belong to.
1905
1906 Returns:
1907 Sequence of protoc ComponentDefs in the same order they are given in
1908 `component_defs`.
1909 """
1910 resource_names_dict = rnc.ConvertComponentDefNames(
1911 self.cnxn, [cd.component_id for cd in component_defs], project_id,
1912 self.services)
1913 involved_user_ids = tbo.UsersInvolvedInComponents(component_defs)
1914 user_resource_names_dict = rnc.ConvertUserNames(involved_user_ids)
1915
1916 all_label_ids = set()
1917 for cd in component_defs:
1918 all_label_ids.update(cd.label_ids)
1919
1920 # If this becomes a performance issue, we should add bulk look up.
1921 labels_by_id = {
1922 label_id: self.services.config.LookupLabel(
1923 self.cnxn, project_id, label_id) for label_id in all_label_ids
1924 }
1925
1926 api_cds = []
1927 for cd in component_defs:
1928 state = project_objects_pb2.ComponentDef.ComponentDefState.Value('ACTIVE')
1929 if cd.deprecated:
1930 state = project_objects_pb2.ComponentDef.ComponentDefState.Value(
1931 'DEPRECATED')
1932
1933 api_cd = project_objects_pb2.ComponentDef(
1934 name=resource_names_dict.get(cd.component_id),
1935 value=cd.path,
1936 docstring=cd.docstring,
1937 state=state,
1938 admins=[
1939 user_resource_names_dict.get(admin_id)
1940 for admin_id in cd.admin_ids
1941 ],
1942 ccs=[user_resource_names_dict.get(cc_id) for cc_id in cd.cc_ids],
1943 creator=user_resource_names_dict.get(cd.creator_id),
1944 modifier=user_resource_names_dict.get(cd.modifier_id),
1945 create_time=timestamp_pb2.Timestamp(seconds=cd.created),
1946 modify_time=timestamp_pb2.Timestamp(seconds=cd.modified),
1947 labels=[labels_by_id[label_id] for label_id in cd.label_ids],
1948 )
1949 api_cds.append(api_cd)
1950 return api_cds
1951
1952 def ConvertProjectSavedQueries(self, saved_queries, project_id):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001953 # type: (Sequence[mrproto.tracker_pb2.SavedQuery], int) ->
Copybara854996b2021-09-07 19:36:02 +00001954 # Sequence(api_proto.project_objects.ProjectSavedQuery)
1955 """Convert sequence of protorpc SavedQueries to protoc ProjectSavedQueries
1956
1957 Args:
1958 saved_queries: Sequence of SavedQueries.
1959 project_id: ID of the Project these belong to.
1960
1961 Returns:
1962 Sequence of protoc ProjectSavedQueries in the same order they are given in
1963 `saved_queries`. In the event any items in `saved_queries` are not found
1964 or don't belong to the project, they will be omitted from the result.
1965 """
1966 resource_names_dict = rnc.ConvertProjectSavedQueryNames(
1967 self.cnxn, [sq.query_id for sq in saved_queries], project_id,
1968 self.services)
1969 api_psqs = []
1970 for sq in saved_queries:
1971 if sq.query_id not in resource_names_dict:
1972 continue
1973
1974 # TODO(crbug/monorail/7756): Remove base_query_id, avoid confusions.
1975 # Until then we have to expand the query by including base_query_id.
1976 # base_query_id can only be in the set of DEFAULT_CANNED_QUERIES.
1977 if sq.base_query_id:
1978 query = '{} {}'.format(tbo.GetBuiltInQuery(sq.base_query_id), sq.query)
1979 else:
1980 query = sq.query
1981
1982 api_psqs.append(
1983 project_objects_pb2.ProjectSavedQuery(
1984 name=resource_names_dict.get(sq.query_id),
1985 display_name=sq.name,
1986 query=query))
1987 return api_psqs