blob: 60aebd7b53e9089f4842c3fe1240407a6f4a258d [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# Copyright 2020 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.
4
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
28from proto import tracker_pb2
29from 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):
75 # type: (proto.feature_objects_pb2.Hotlist)
76 # -> 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):
103 # type: (Sequence[proto.feature_objects_pb2.Hotlist])
104 # -> 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):
109 # type: (int, Sequence[proto.features_pb2.HotlistItem]) ->
110 # 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):
168 # proto.tracker_pb2.Issue ->
169 # 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):
194 # proto.tracker_pb2.Issue -> api_proto.issue_objects_pb2.Issue.StatusValue
195 """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):
206 # type: (Sequence[proto.tracker_pb2.Amendment], Mapping[int, str]) ->
207 # 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):
230 # type: (Sequence[proto.tracker_pb2.Attachment], str) ->
231 # 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):
257 # type: (int, Sequence[proto.tracker_pb2.IssueComment])
258 # -> 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
310 def ConvertIssue(self, issue):
311 # type: (proto.tracker_pb2.Issue) -> api_proto.issue_objects_pb2.Issue
312 """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)
318 return issues[0]
319
320 def ConvertIssues(self, issues):
321 # type: (Sequence[proto.tracker_pb2.Issue]) ->
322 # Sequence[api_proto.issue_objects_pb2.Issue]
323 """Convert protorpc Issues into protoc Issues."""
324 issue_ids = [issue.issue_id for issue in issues]
325 issue_names_dict = rnc.ConvertIssueNames(
326 self.cnxn, issue_ids, self.services)
327 found_issues = [
328 issue for issue in issues if issue.issue_id in issue_names_dict
329 ]
330 converted_issues = []
331 for issue in found_issues:
332 status = self._ConvertStatusValue(issue)
333 content_state = issue_objects_pb2.IssueContentState.Value(
334 'STATE_UNSPECIFIED')
335 if issue.is_spam:
336 content_state = issue_objects_pb2.IssueContentState.Value('SPAM')
337 elif issue.deleted:
338 content_state = issue_objects_pb2.IssueContentState.Value('DELETED')
339 else:
340 content_state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
341
342 owner = None
343 # Explicit values override values derived from rules.
344 if issue.owner_id:
345 owner = issue_objects_pb2.Issue.UserValue(
346 derivation=issue_objects_pb2.Derivation.Value('EXPLICIT'),
347 user=rnc.ConvertUserName(issue.owner_id))
348 elif issue.derived_owner_id:
349 owner = issue_objects_pb2.Issue.UserValue(
350 derivation=issue_objects_pb2.Derivation.Value('RULE'),
351 user=rnc.ConvertUserName(issue.derived_owner_id))
352
353 cc_users = []
354 for cc_user_id in issue.cc_ids:
355 cc_users.append(
356 issue_objects_pb2.Issue.UserValue(
357 derivation=issue_objects_pb2.Derivation.Value('EXPLICIT'),
358 user=rnc.ConvertUserName(cc_user_id)))
359 for derived_cc_user_id in issue.derived_cc_ids:
360 cc_users.append(
361 issue_objects_pb2.Issue.UserValue(
362 derivation=issue_objects_pb2.Derivation.Value('RULE'),
363 user=rnc.ConvertUserName(derived_cc_user_id)))
364
365 labels = self.ConvertLabels(
366 issue.labels, issue.derived_labels, issue.project_id)
367 components = self._ConvertComponentValues(issue)
368 non_approval_fvs = self._GetNonApprovalFieldValues(
369 issue.field_values, issue.project_id)
370 field_values = self.ConvertFieldValues(
371 non_approval_fvs, issue.project_id, issue.phases)
372 field_values.extend(
373 self.ConvertEnumFieldValues(
374 issue.labels, issue.derived_labels, issue.project_id))
375 related_issue_ids = (
376 [issue.merged_into] + issue.blocked_on_iids + issue.blocking_iids)
377 issue_names_by_ids = rnc.ConvertIssueNames(
378 self.cnxn, related_issue_ids, self.services)
379 merged_into_issue_ref = None
380 if issue.merged_into and issue.merged_into in issue_names_by_ids:
381 merged_into_issue_ref = issue_objects_pb2.IssueRef(
382 issue=issue_names_by_ids[issue.merged_into])
383 if issue.merged_into_external:
384 merged_into_issue_ref = issue_objects_pb2.IssueRef(
385 ext_identifier=issue.merged_into_external)
386
387 blocked_on_issue_refs = [
388 issue_objects_pb2.IssueRef(issue=issue_names_by_ids[iid])
389 for iid in issue.blocked_on_iids
390 if iid in issue_names_by_ids
391 ]
392 blocked_on_issue_refs.extend(
393 issue_objects_pb2.IssueRef(
394 ext_identifier=blocked_on.ext_issue_identifier)
395 for blocked_on in issue.dangling_blocked_on_refs)
396
397 blocking_issue_refs = [
398 issue_objects_pb2.IssueRef(issue=issue_names_by_ids[iid])
399 for iid in issue.blocking_iids
400 if iid in issue_names_by_ids
401 ]
402 blocking_issue_refs.extend(
403 issue_objects_pb2.IssueRef(
404 ext_identifier=blocking.ext_issue_identifier)
405 for blocking in issue.dangling_blocking_refs)
406 # All other timestamps were set when the issue was created.
407 close_time = None
408 if issue.closed_timestamp:
409 close_time = timestamp_pb2.Timestamp(seconds=issue.closed_timestamp)
410
411 phases = self._ComputePhases(issue.phases)
412
413 result = issue_objects_pb2.Issue(
414 name=issue_names_dict[issue.issue_id],
415 summary=issue.summary,
416 state=content_state,
417 status=status,
418 reporter=rnc.ConvertUserName(issue.reporter_id),
419 owner=owner,
420 cc_users=cc_users,
421 labels=labels,
422 components=components,
423 field_values=field_values,
424 merged_into_issue_ref=merged_into_issue_ref,
425 blocked_on_issue_refs=blocked_on_issue_refs,
426 blocking_issue_refs=blocking_issue_refs,
427 create_time=timestamp_pb2.Timestamp(seconds=issue.opened_timestamp),
428 close_time=close_time,
429 modify_time=timestamp_pb2.Timestamp(seconds=issue.modified_timestamp),
430 component_modify_time=timestamp_pb2.Timestamp(
431 seconds=issue.component_modified_timestamp),
432 status_modify_time=timestamp_pb2.Timestamp(
433 seconds=issue.status_modified_timestamp),
434 owner_modify_time=timestamp_pb2.Timestamp(
435 seconds=issue.owner_modified_timestamp),
436 star_count=issue.star_count,
437 phases=phases)
438 # TODO(crbug.com/monorail/5857): Set attachment_count unconditionally
439 # after the underlying source of negative attachment counts has been
440 # resolved and database has been repaired.
441 if issue.attachment_count >= 0:
442 result.attachment_count = issue.attachment_count
443 converted_issues.append(result)
444 return converted_issues
445
446 def IngestAttachmentUploads(self, attachment_uploads):
447 # type: (Sequence[api_proto.issues_pb2.AttachmentUpload] ->
448 # Sequence[framework_helpers.AttachmentUpload])
449 """Ingests protoc AttachmentUploads into framework_helpers.AttachUploads."""
450 ingested_uploads = []
451 with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
452 for up in attachment_uploads:
453 if not up.filename or not up.content:
454 err_agg.AddErrorMessage(
455 'Uploaded atachment missing filename or content')
456 mimetype = filecontent.GuessContentTypeFromFilename(up.filename)
457 ingested_uploads.append(
458 framework_helpers.AttachmentUpload(
459 up.filename, up.content, mimetype))
460
461 return ingested_uploads
462
463 def IngestIssueDeltas(self, issue_deltas):
464 # type: (Sequence[api_proto.issues_pb2.IssueDelta]) ->
465 # Sequence[Tuple[int, proto.tracker_pb2.IssueDelta]]
466 """Ingests protoc IssueDeltas, into protorpc IssueDeltas.
467
468 Args:
469 issue_deltas: the protoc IssueDeltas to ingest.
470
471 Returns:
472 A list of (issue_id, tracker_pb2.IssueDelta) tuples that contain
473 values found in issue_deltas, ignoring all OUTPUT_ONLY and masked
474 fields.
475
476 Raises:
477 InputException: if any fields in the approval_deltas were invalid.
478 NoSuchProjectException: if any parent projects are not found.
479 NoSuchIssueException: if any issues are not found.
480 NoSuchComponentException: if any components are not found.
481 """
482 issue_names = [delta.issue.name for delta in issue_deltas]
483 issue_ids = rnc.IngestIssueNames(self.cnxn, issue_names, self.services)
484 issues_dict, misses = self.services.issue.GetIssuesDict(
485 self.cnxn, issue_ids)
486 if misses:
487 logging.info(
488 'Issues not found for supposedly valid issue_ids: %r' % misses)
489 raise ValueError('Could not fetch some issues.')
490 configs_by_pid = self.services.config.GetProjectConfigs(
491 self.cnxn, {issue.project_id for issue in issues_dict.values()})
492
493 with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
494 for api_delta in issue_deltas:
495 if not api_delta.HasField('update_mask'):
496 err_agg.AddErrorMessage(
497 '`update_mask` must be set for {} delta.', api_delta.issue.name)
498 elif not api_delta.update_mask.IsValidForDescriptor(
499 issue_objects_pb2.Issue.DESCRIPTOR):
500 err_agg.AddErrorMessage(
501 'Invalid `update_mask` for {} delta.', api_delta.issue.name)
502
503 ingested = []
504 for iid, api_delta in zip(issue_ids, issue_deltas):
505 delta = tracker_pb2.IssueDelta()
506
507 # Check non-repeated fields before MergeMessage because in an object
508 # where fields are not set and with a FieldMask applied, there is no
509 # way to tell if empty fields were explicitly listed or not listed
510 # in the FieldMask.
511 paths_set = set(api_delta.update_mask.paths)
512 if (not paths_set.isdisjoint({'status', 'status.status'}) and
513 api_delta.issue.status.status):
514 delta.status = api_delta.issue.status.status
515 elif 'status.status' in paths_set and not api_delta.issue.status.status:
516 delta.status = ''
517
518 if (not paths_set.isdisjoint({'owner', 'owner.user'}) and
519 api_delta.issue.owner.user):
520 delta.owner_id = rnc.IngestUserName(
521 self.cnxn, api_delta.issue.owner.user, self.services)
522 elif 'owner.user' in paths_set and not api_delta.issue.owner.user:
523 delta.owner_id = framework_constants.NO_USER_SPECIFIED
524
525 if 'summary' in paths_set:
526 if api_delta.issue.summary:
527 delta.summary = api_delta.issue.summary
528 else:
529 delta.summary = ''
530
531 merge_ref = api_delta.issue.merged_into_issue_ref
532 if 'merged_into_issue_ref' in paths_set:
533 if (api_delta.issue.merged_into_issue_ref.issue or
534 api_delta.issue.merged_into_issue_ref.ext_identifier):
535 ingested_ref = self._IngestIssueRef(merge_ref)
536 if isinstance(ingested_ref, tracker_pb2.DanglingIssueRef):
537 delta.merged_into_external = ingested_ref.ext_issue_identifier
538 else:
539 delta.merged_into = ingested_ref
540 elif 'merged_into_issue_ref.issue' in paths_set:
541 if api_delta.issue.merged_into_issue_ref.issue:
542 delta.merged_into = self._IngestIssueRef(merge_ref)
543 else:
544 delta.merged_into = 0
545 elif 'merged_into_issue_ref.ext_identifier' in paths_set:
546 if api_delta.issue.merged_into_issue_ref.ext_identifier:
547 ingested_ref = self._IngestIssueRef(merge_ref)
548 delta.merged_into_external = ingested_ref.ext_issue_identifier
549 else:
550 delta.merged_into_external = ''
551
552 filtered_api_issue = issue_objects_pb2.Issue()
553 api_delta.update_mask.MergeMessage(
554 api_delta.issue,
555 filtered_api_issue,
556 replace_message_field=True,
557 replace_repeated_field=True)
558
559 cc_names = [name for name in api_delta.ccs_remove] + [
560 user_value.user for user_value in filtered_api_issue.cc_users
561 ]
562 cc_ids = rnc.IngestUserNames(self.cnxn, cc_names, self.services)
563 delta.cc_ids_remove = cc_ids[:len(api_delta.ccs_remove)]
564 delta.cc_ids_add = cc_ids[len(api_delta.ccs_remove):]
565
566 comp_names = [component for component in api_delta.components_remove] + [
567 c_value.component for c_value in filtered_api_issue.components
568 ]
569 project_comp_ids = rnc.IngestComponentDefNames(
570 self.cnxn, comp_names, self.services)
571 comp_ids = [comp_id for (_pid, comp_id) in project_comp_ids]
572 delta.comp_ids_remove = comp_ids[:len(api_delta.components_remove)]
573 delta.comp_ids_add = comp_ids[len(api_delta.components_remove):]
574
575 # Added to delta below, after ShiftEnumFieldsIntoLabels.
576 labels_add = [value.label for value in filtered_api_issue.labels]
577 labels_remove = [label for label in api_delta.labels_remove]
578
579 config = configs_by_pid[issues_dict[iid].project_id]
580 fvs_add, add_enums = self._IngestFieldValues(
581 filtered_api_issue.field_values, config)
582 fvs_remove, remove_enums = self._IngestFieldValues(
583 api_delta.field_vals_remove, config)
584 field_helpers.ShiftEnumFieldsIntoLabels(
585 labels_add, labels_remove, add_enums, remove_enums, config)
586 delta.field_vals_add = fvs_add
587 delta.field_vals_remove = fvs_remove
588 delta.labels_add = labels_add
589 delta.labels_remove = labels_remove
590 assert len(add_enums) == 0 # ShiftEnumFieldsIntoLabels clears all enums.
591 assert len(remove_enums) == 0
592
593 blocked_on_iids_rm, blocked_on_dangling_rm = self._IngestIssueRefs(
594 api_delta.blocked_on_issues_remove)
595 delta.blocked_on_remove = blocked_on_iids_rm
596 delta.ext_blocked_on_remove = [
597 ref.ext_issue_identifier for ref in blocked_on_dangling_rm
598 ]
599
600 blocked_on_iids_add, blocked_on_dangling_add = self._IngestIssueRefs(
601 filtered_api_issue.blocked_on_issue_refs)
602 delta.blocked_on_add = blocked_on_iids_add
603 delta.ext_blocked_on_add = [
604 ref.ext_issue_identifier for ref in blocked_on_dangling_add
605 ]
606
607 blocking_iids_rm, blocking_dangling_rm = self._IngestIssueRefs(
608 api_delta.blocking_issues_remove)
609 delta.blocking_remove = blocking_iids_rm
610 delta.ext_blocking_remove = [
611 ref.ext_issue_identifier for ref in blocking_dangling_rm
612 ]
613
614 blocking_iids_add, blocking_dangling_add = self._IngestIssueRefs(
615 filtered_api_issue.blocking_issue_refs)
616 delta.blocking_add = blocking_iids_add
617 delta.ext_blocking_add = [
618 ref.ext_issue_identifier for ref in blocking_dangling_add
619 ]
620
621 ingested.append((iid, delta))
622
623 return ingested
624
625 def IngestApprovalDeltas(self, approval_deltas, setter_id):
626 # type: (Sequence[api_proto.issues_pb2.ApprovalDelta], int) ->
627 # Sequence[Tuple[int, int, proto.tracker_pb2.ApprovalDelta]]
628 """Ingests protoc ApprovalDeltas into protorpc ApprovalDeltas.
629
630 Args:
631 approval_deltas: the protoc ApprovalDeltas to ingest.
632 setter_id: The ID for the user setting the deltas.
633
634 Returns:
635 Sequence of (issue_id, approval_id, ApprovalDelta) tuples in the order
636 provided. The ApprovalDeltas ignore all OUTPUT_ONLY and masked fields.
637 The tuples are "delta_specifications;" they identify one requested change.
638
639 Raises:
640 InputException: if any fields in the approval_delta protos were invalid.
641 NoSuchProjectException: if the parent project of any ApprovalValue isn't
642 found.
643 NoSuchIssueException: if the issue of any ApprovalValue isn't found.
644 NoSuchUserException: if any user value was provided with an invalid email.
645 Note that users specified by ID are not checked for existence.
646 """
647 delta_specifications = []
648 set_on = int(time.time()) # Use the same timestamp for all deltas.
649 for approval_delta in approval_deltas:
650 approval_name = approval_delta.approval_value.name
651 # TODO(crbug/monorail/8173): Aggregate errors.
652 project_id, issue_id, approval_id = rnc.IngestApprovalValueName(
653 self.cnxn, approval_name, self.services)
654
655 if not approval_delta.HasField('update_mask'):
656 raise exceptions.InputException(
657 '`update_mask` must be set for %s delta.' % approval_name)
658 elif not approval_delta.update_mask.IsValidForDescriptor(
659 issue_objects_pb2.ApprovalValue.DESCRIPTOR):
660 raise exceptions.InputException(
661 'Invalid `update_mask` for %s delta.' % approval_name)
662 filtered_value = issue_objects_pb2.ApprovalValue()
663 approval_delta.update_mask.MergeMessage(
664 approval_delta.approval_value,
665 filtered_value,
666 replace_message_field=True,
667 replace_repeated_field=True)
668 status = _APPROVAL_STATUS_INGEST[filtered_value.status]
669 # Approvers
670 # No autocreate.
671 # A user may try to remove all existing approvers [a, b] and add another
672 # approver [c]. If they mis-type `c` and we auto-create `c` instead of
673 # raising error, this would cause the ApprovalValue to be editable by no
674 # one but site admins.
675 approver_ids_add = rnc.IngestUserNames(
676 self.cnxn, filtered_value.approvers, self.services, autocreate=False)
677 approver_ids_remove = rnc.IngestUserNames(
678 self.cnxn,
679 approval_delta.approvers_remove,
680 self.services,
681 autocreate=False)
682
683 # Field Values.
684 config = self.services.config.GetProjectConfig(self.cnxn, project_id)
685 approval_fds_by_id = {
686 fd.field_id: fd
687 for fd in config.field_defs
688 if fd.field_type is tracker_pb2.FieldTypes.APPROVAL_TYPE
689 }
690 if approval_id not in approval_fds_by_id:
691 raise exceptions.InputException(
692 'Approval not found in project for %s' % approval_name)
693
694 sub_fvs_add, add_enums = self._IngestFieldValues(
695 filtered_value.field_values, config, approval_id_filter=approval_id)
696 sub_fvs_remove, remove_enums = self._IngestFieldValues(
697 approval_delta.field_vals_remove,
698 config,
699 approval_id_filter=approval_id)
700 labels_add = []
701 labels_remove = []
702 field_helpers.ShiftEnumFieldsIntoLabels(
703 labels_add, labels_remove, add_enums, remove_enums, config)
704 assert len(add_enums) == 0 # ShiftEnumFieldsIntoLabels clears all enums.
705 assert len(remove_enums) == 0
706 delta = tbo.MakeApprovalDelta(
707 status,
708 setter_id,
709 approver_ids_add,
710 approver_ids_remove,
711 sub_fvs_add,
712 sub_fvs_remove, [],
713 labels_add,
714 labels_remove,
715 set_on=set_on)
716 delta_specifications.append((issue_id, approval_id, delta))
717 return delta_specifications
718
719 def IngestIssue(self, issue, project_id):
720 # type: (api_proto.issue_objects_pb2.Issue, int) -> proto.tracker_pb2.Issue
721 """Ingest a protoc Issue into a protorpc Issue.
722
723 Args:
724 issue: the protoc issue to ingest.
725 project_id: The project into which we're ingesting `issue`.
726
727 Returns:
728 protorpc version of issue, ignoring all OUTPUT_ONLY fields.
729
730 Raises:
731 InputException: if any fields in the 'issue' proto were invalid.
732 NoSuchProjectException: if 'project_id' is not found.
733 """
734 # Get config first. We can't ingest the issue if the project isn't found.
735 config = self.services.config.GetProjectConfig(self.cnxn, project_id)
736 ingestedDict = {
737 'project_id': project_id,
738 'summary': issue.summary
739 }
740 with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
741 self._ExtractOwner(issue, ingestedDict, err_agg)
742
743 # Extract ccs.
744 try:
745 ingestedDict['cc_ids'] = rnc.IngestUserNames(
746 self.cnxn, [cc.user for cc in issue.cc_users], self.services,
747 autocreate=True)
748 except exceptions.InputException as e:
749 err_agg.AddErrorMessage('Error ingesting cc_users: {}', e)
750
751 # Extract status.
752 if issue.HasField('status') and issue.status.status:
753 ingestedDict['status'] = issue.status.status
754 else:
755 err_agg.AddErrorMessage('Status is required when creating an issue')
756
757 # Extract components.
758 try:
759 project_comp_ids = rnc.IngestComponentDefNames(
760 self.cnxn, [cv.component for cv in issue.components], self.services)
761 ingestedDict['component_ids'] = [
762 comp_id for (_pid, comp_id) in project_comp_ids]
763 except (exceptions.InputException, exceptions.NoSuchProjectException,
764 exceptions.NoSuchComponentException) as e:
765 err_agg.AddErrorMessage('Error ingesting components: {}', e)
766
767 # Extract labels and field values.
768 ingestedDict['labels'] = [lv.label for lv in issue.labels]
769 try:
770 ingestedDict['field_values'], enums = self._IngestFieldValues(
771 issue.field_values, config)
772 field_helpers.ShiftEnumFieldsIntoLabels(
773 ingestedDict['labels'], [], enums, [], config)
774 assert len(
775 enums) == 0 # ShiftEnumFieldsIntoLabels must clear all enums.
776 except exceptions.InputException as e:
777 err_agg.AddErrorMessage(e.message)
778
779 # Ingest merged, blocking/blocked_on.
780 self._ExtractIssueRefs(issue, ingestedDict, err_agg)
781 return tracker_pb2.Issue(**ingestedDict)
782
783 def _IngestFieldValues(self, field_values, config, approval_id_filter=None):
784 # type: (Sequence[api_proto.issue_objects.FieldValue],
785 # proto.tracker_pb2.ProjectIssueConfig, Optional[int]) ->
786 # Tuple[Sequence[proto.tracker_pb2.FieldValue],
787 # Mapping[int, Sequence[str]]]
788 """Returns protorpc FieldValues for the given protoc FieldValues.
789
790 Raises exceptions if any field could not be parsed for any reasons such as
791 unsupported field type, non-existent field, field from different
792 projects, or fields with mismatched parent approvals.
793
794 Args:
795 field_values: protoc FieldValues to ingest.
796 config: ProjectIssueConfig for the FieldValues we're ingesting.
797 approval_id_filter: an approval_id, including any FieldValues that does
798 not have this approval as a parent will trigger InputException.
799
800 Returns:
801 A pair 1) Ingested FieldValues. 2) A mapping of field ids to values
802 for ENUM_TYPE fields in 'field_values.'
803
804 Raises:
805 InputException: if any fields_values could not be parsed for any reasons
806 such as unsupported field type, non-existent field, or field from
807 different projects.
808 """
809 fds_by_id = {fd.field_id: fd for fd in config.field_defs}
810 enums = {}
811 ingestedFieldValues = []
812 with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
813 for fv in field_values:
814 try:
815 project_id, fd_id = rnc.IngestFieldDefName(
816 self.cnxn, fv.field, self.services)
817 fd = fds_by_id[fd_id]
818 # Raise if field does not belong to approval_id_filter (if provided).
819 if (approval_id_filter is not None and
820 fd.approval_id != approval_id_filter):
821 approval_name = rnc.ConvertApprovalDefNames(
822 self.cnxn, [approval_id_filter], project_id,
823 self.services)[approval_id_filter]
824 err_agg.AddErrorMessage(
825 'Field {} does not belong to approval {}', fv.field,
826 approval_name)
827 continue
828 if fd.field_type == tracker_pb2.FieldTypes.ENUM_TYPE:
829 enums.setdefault(fd_id, []).append(fv.value)
830 else:
831 ingestedFieldValues.append(self._IngestFieldValue(fv, fd))
832 except (exceptions.InputException, exceptions.NoSuchProjectException,
833 exceptions.NoSuchFieldDefException, ValueError) as e:
834 err_agg.AddErrorMessage(
835 'Could not ingest value ({}) for FieldDef ({}): {}', fv.value,
836 fv.field, e)
837 except exceptions.NoSuchUserException as e:
838 err_agg.AddErrorMessage(
839 'User ({}) not found when ingesting user field: {}', fv.value,
840 fv.field)
841 except KeyError as e:
842 err_agg.AddErrorMessage('Field {} is not in this project', fv.field)
843 return ingestedFieldValues, enums
844
845 def _IngestFieldValue(self, field_value, field_def):
846 # type: (api_proto.issue_objects.FieldValue, proto.tracker_pb2.FieldDef) ->
847 # proto.tracker_pb2.FieldValue
848 """Ingest a protoc FieldValue into a protorpc FieldValue.
849
850 Args:
851 field_value: protoc FieldValue to ingest.
852 field_def: protorpc FieldDef associated with 'field_value'.
853 BOOL_TYPE and APPROVAL_TYPE are ignored.
854 Enum values are not allowed. They must be ingested as labels.
855
856 Returns:
857 Ingested protorpc FieldValue.
858
859 Raises:
860 InputException if 'field_def' is USER_TYPE and 'field_value' does not
861 have a valid formatted resource name.
862 NoSuchUserException if specified user in field does not exist.
863 ValueError if 'field_value' could not be parsed for 'field_def'.
864 """
865 assert field_def.field_type != tracker_pb2.FieldTypes.ENUM_TYPE
866 if field_def.field_type == tracker_pb2.FieldTypes.USER_TYPE:
867 return self._ParseOneUserFieldValue(field_value.value, field_def.field_id)
868 fv = field_helpers.ParseOneFieldValue(
869 self.cnxn, self.services.user, field_def, field_value.value)
870 # ParseOneFieldValue currently ignores parsing errors, although it has TODOs
871 # to raise them.
872 if not fv:
873 raise ValueError('Could not parse %s' % field_value.value)
874 return fv
875
876 def _ParseOneUserFieldValue(self, value, field_id):
877 # type: (str, int) -> proto.tracker_pb2.FieldValue
878 """Replacement for the obsolete user parsing in ParseOneFieldValue."""
879 user_id = rnc.IngestUserName(self.cnxn, value, self.services)
880 return tbo.MakeFieldValue(field_id, None, None, user_id, None, None, False)
881
882 def _ExtractOwner(self, issue, ingestedDict, err_agg):
883 # type: (api_proto.issue_objects_pb2.Issue, Dict[str, Any], ErrorAggregator)
884 # -> None
885 """Fills 'owner' into `ingestedDict`, if it can be extracted."""
886 if issue.HasField('owner'):
887 try:
888 # Unlike for cc's, we require owner be an existing user, thus call we
889 # do not autocreate.
890 ingestedDict['owner_id'] = rnc.IngestUserName(
891 self.cnxn, issue.owner.user, self.services, autocreate=False)
892 except exceptions.InputException as e:
893 err_agg.AddErrorMessage(
894 'Error ingesting owner ({}): {}', issue.owner.user, e)
895 except exceptions.NoSuchUserException as e:
896 err_agg.AddErrorMessage(
897 'User ({}) not found when ingesting owner', e)
898 else:
899 ingestedDict['owner_id'] = framework_constants.NO_USER_SPECIFIED
900
901 def _ExtractIssueRefs(self, issue, ingestedDict, err_agg):
902 # type: (api_proto.issue_objects_pb2.Issue, Dict[str, Any], ErrorAggregator)
903 # -> None
904 """Fills issue relationships into `ingestedDict` from `issue`."""
905 if issue.HasField('merged_into_issue_ref'):
906 try:
907 merged_into_ref = self._IngestIssueRef(issue.merged_into_issue_ref)
908 if isinstance(merged_into_ref, tracker_pb2.DanglingIssueRef):
909 ingestedDict['merged_into_external'] = (
910 merged_into_ref.ext_issue_identifier)
911 else:
912 ingestedDict['merged_into'] = merged_into_ref
913 except exceptions.InputException as e:
914 err_agg.AddErrorMessage(
915 'Error ingesting ref {}: {}', issue.merged_into_issue_ref, e)
916 try:
917 iids, dangling_refs = self._IngestIssueRefs(issue.blocked_on_issue_refs)
918 ingestedDict['blocked_on_iids'] = iids
919 ingestedDict['dangling_blocked_on_refs'] = dangling_refs
920 except exceptions.InputException as e:
921 err_agg.AddErrorMessage(e.message)
922 try:
923 iids, dangling_refs = self._IngestIssueRefs(issue.blocking_issue_refs)
924 ingestedDict['blocking_iids'] = iids
925 ingestedDict['dangling_blocking_refs'] = dangling_refs
926 except exceptions.InputException as e:
927 err_agg.AddErrorMessage(e.message)
928
929 def _IngestIssueRefs(self, issue_refs):
930 # type: (api_proto.issue_objects.IssueRf) ->
931 # Tuple[Sequence[int], Sequence[tracker_pb2.DanglingIssueRef]]
932 """Given protoc IssueRefs, returns issue_ids and DanglingIssueRefs."""
933 issue_ids = []
934 external_refs = []
935 with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
936 for ref in issue_refs:
937 try:
938 ingested_ref = self._IngestIssueRef(ref)
939 if isinstance(ingested_ref, tracker_pb2.DanglingIssueRef):
940 external_refs.append(ingested_ref)
941 else:
942 issue_ids.append(ingested_ref)
943 except (exceptions.InputException, exceptions.NoSuchIssueException,
944 exceptions.NoSuchProjectException) as e:
945 err_agg.AddErrorMessage('Error ingesting ref {}: {}', ref, e)
946
947 return issue_ids, external_refs
948
949 def _IngestIssueRef(self, issue_ref):
950 # type: (api_proto.issue_objects.IssueRef) ->
951 # Union[int, tracker_pb2.DanglingIssueRef]
952 """Given a protoc IssueRef, returns an issue id or DanglingIssueRef."""
953 if issue_ref.issue and issue_ref.ext_identifier:
954 raise exceptions.InputException(
955 'IssueRefs MUST NOT have both `issue` and `ext_identifier`')
956 if issue_ref.issue:
957 return rnc.IngestIssueName(self.cnxn, issue_ref.issue, self.services)
958 if issue_ref.ext_identifier:
959 # TODO(crbug.com/monorail/7208): Handle ingestion/conversion of CodeSite
960 # refs. We may be able to avoid ever needing to ingest them.
961 return tracker_pb2.DanglingIssueRef(
962 ext_issue_identifier=issue_ref.ext_identifier
963 )
964 raise exceptions.InputException(
965 'IssueRefs MUST have one of `issue` and `ext_identifier`')
966
967 def IngestIssuesListColumns(self, issues_list_columns):
968 # type: (Sequence[proto.issue_objects_pb2.IssuesListColumn] -> str
969 """Ingest a list of protoc IssueListColumns and returns a string."""
970 return ' '.join([col.column for col in issues_list_columns])
971
972 def _ComputeIssuesListColumns(self, columns):
973 # type: (string) -> Sequence[api_proto.issue_objects_pb2.IssuesListColumn]
974 """Convert string representation of columns to protoc IssuesListColumns"""
975 return [
976 issue_objects_pb2.IssuesListColumn(column=col)
977 for col in columns.split()
978 ]
979
980 def IngestNotifyType(self, notify):
981 # type: (issue_pb.NotifyType) -> bool
982 """Ingest a NotifyType to boolean."""
983 if (notify == issues_pb2.NotifyType.Value('NOTIFY_TYPE_UNSPECIFIED') or
984 notify == issues_pb2.NotifyType.Value('EMAIL')):
985 return True
986 elif notify == issues_pb2.NotifyType.Value('NO_NOTIFICATION'):
987 return False
988
989 # Users
990
991 def ConvertUser(self, user):
992 # type: (protorpc.User) -> api_proto.user_objects_pb2.User
993 """Convert a protorpc User into a protoc User.
994
995 Args:
996 user: protorpc User object.
997
998 Returns:
999 The protoc User object.
1000 """
1001 return self.ConvertUsers([user.user_id])[user.user_id]
1002
1003
1004 # TODO(crbug/monorail/7238): Make this take in a full User object and
1005 # return a Sequence, rather than a map, after hotlist users are converted.
1006 def ConvertUsers(self, user_ids):
1007 # type: (Sequence[int]) -> Map(int, api_proto.user_objects_pb2.User)
1008 """Convert list of protorpc Users into list of protoc Users.
1009
1010 Args:
1011 user_ids: List of User IDs.
1012
1013 Returns:
1014 Dict of User IDs to User protos for given user_ids that could be found.
1015 """
1016 user_ids_to_names = {}
1017
1018 # Get display names
1019 users_by_id = self.services.user.GetUsersByIDs(self.cnxn, user_ids)
1020 (display_names_by_id,
1021 display_emails_by_id) = framework_bizobj.CreateUserDisplayNamesAndEmails(
1022 self.cnxn, self.services, self.user_auth, users_by_id.values())
1023
1024 for user_id, user in users_by_id.items():
1025 name = rnc.ConvertUserNames([user_id]).get(user_id)
1026
1027 display_name = display_names_by_id.get(user_id)
1028 display_email = display_emails_by_id.get(user_id)
1029 availability = framework_helpers.GetUserAvailability(user)
1030 availability_message, _availability_status = availability
1031
1032 user_ids_to_names[user_id] = user_objects_pb2.User(
1033 name=name,
1034 display_name=display_name,
1035 email=display_email,
1036 availability_message=availability_message)
1037
1038 return user_ids_to_names
1039
1040 def ConvertProjectStars(self, user_id, projects):
1041 # type: (int, Collection[protorpc.Project]) ->
1042 # Collection[api_proto.user_objects_pb2.ProjectStar]
1043 """Convert list of protorpc Projects into protoc ProjectStars.
1044
1045 Args:
1046 user_id: The user the ProjectStar is associated with.
1047 projects: All starred projects.
1048
1049 Returns:
1050 List of ProjectStar messages.
1051 """
1052 api_project_stars = []
1053 for proj in projects:
1054 name = rnc.ConvertProjectStarName(
1055 self.cnxn, user_id, proj.project_id, self.services)
1056 star = user_objects_pb2.ProjectStar(name=name)
1057 api_project_stars.append(star)
1058 return api_project_stars
1059
1060 # Field Defs
1061
1062 def ConvertFieldDefs(self, field_defs, project_id):
1063 # type: (Sequence[proto.tracker_pb2.FieldDef], int) ->
1064 # Sequence[api_proto.project_objects_pb2.FieldDef]
1065 """Convert sequence of protorpc FieldDefs to protoc FieldDefs.
1066
1067 Args:
1068 field_defs: List of protorpc FieldDefs
1069 project_id: ID of the Project that is ancestor to all given
1070 `field_defs`.
1071
1072 Returns:
1073 Sequence of protoc FieldDef in the same order they are given in
1074 `field_defs`. In the event any field_def or the referenced approval_id
1075 in `field_defs` is not found, they will be omitted from the result.
1076 """
1077 field_ids = [fd.field_id for fd in field_defs]
1078 resource_names_dict = rnc.ConvertFieldDefNames(
1079 self.cnxn, field_ids, project_id, self.services)
1080 parent_approval_ids = [
1081 fd.approval_id for fd in field_defs if fd.approval_id is not None
1082 ]
1083 approval_names_dict = rnc.ConvertApprovalDefNames(
1084 self.cnxn, parent_approval_ids, project_id, self.services)
1085
1086 api_fds = []
1087 for fd in field_defs:
1088 # Skip over approval fields, they have their separate ApprovalDef
1089 if fd.field_type == tracker_pb2.FieldTypes.APPROVAL_TYPE:
1090 continue
1091 if fd.field_id not in resource_names_dict:
1092 continue
1093
1094 name = resource_names_dict.get(fd.field_id)
1095 display_name = fd.field_name
1096 docstring = fd.docstring
1097 field_type = self._ConvertFieldDefType(fd.field_type)
1098 applicable_issue_type = fd.applicable_type
1099 admins = rnc.ConvertUserNames(fd.admin_ids).values()
1100 editors = rnc.ConvertUserNames(fd.editor_ids).values()
1101 traits = self._ComputeFieldDefTraits(fd)
1102 approval_parent = approval_names_dict.get(fd.approval_id)
1103
1104 enum_settings = None
1105 if field_type == project_objects_pb2.FieldDef.Type.Value('ENUM'):
1106 enum_settings = project_objects_pb2.FieldDef.EnumTypeSettings(
1107 choices=self._GetEnumFieldChoices(fd))
1108
1109 int_settings = None
1110 if field_type == project_objects_pb2.FieldDef.Type.Value('INT'):
1111 int_settings = project_objects_pb2.FieldDef.IntTypeSettings(
1112 min_value=fd.min_value, max_value=fd.max_value)
1113
1114 str_settings = None
1115 if field_type == project_objects_pb2.FieldDef.Type.Value('STR'):
1116 str_settings = project_objects_pb2.FieldDef.StrTypeSettings(
1117 regex=fd.regex)
1118
1119 user_settings = None
1120 if field_type == project_objects_pb2.FieldDef.Type.Value('USER'):
1121 user_settings = project_objects_pb2.FieldDef.UserTypeSettings(
1122 role_requirements=self._ConvertRoleRequirements(fd.needs_member),
1123 notify_triggers=self._ConvertNotifyTriggers(fd.notify_on),
1124 grants_perm=fd.grants_perm,
1125 needs_perm=fd.needs_perm)
1126
1127 date_settings = None
1128 if field_type == project_objects_pb2.FieldDef.Type.Value('DATE'):
1129 date_settings = project_objects_pb2.FieldDef.DateTypeSettings(
1130 date_action=self._ConvertDateAction(fd.date_action))
1131
1132 api_fd = project_objects_pb2.FieldDef(
1133 name=name,
1134 display_name=display_name,
1135 docstring=docstring,
1136 type=field_type,
1137 applicable_issue_type=applicable_issue_type,
1138 admins=admins,
1139 traits=traits,
1140 approval_parent=approval_parent,
1141 enum_settings=enum_settings,
1142 int_settings=int_settings,
1143 str_settings=str_settings,
1144 user_settings=user_settings,
1145 date_settings=date_settings,
1146 editors=editors)
1147 api_fds.append(api_fd)
1148 return api_fds
1149
1150 def _ConvertDateAction(self, date_action):
1151 # type: (proto.tracker_pb2.DateAction) ->
1152 # api_proto.project_objects_pb2.FieldDef.DateTypeSettings.DateAction
1153 """Convert protorpc DateAction to protoc
1154 FieldDef.DateTypeSettings.DateAction"""
1155 if date_action == tracker_pb2.DateAction.NO_ACTION:
1156 return project_objects_pb2.FieldDef.DateTypeSettings.DateAction.Value(
1157 'NO_ACTION')
1158 elif date_action == tracker_pb2.DateAction.PING_OWNER_ONLY:
1159 return project_objects_pb2.FieldDef.DateTypeSettings.DateAction.Value(
1160 'NOTIFY_OWNER')
1161 elif date_action == tracker_pb2.DateAction.PING_PARTICIPANTS:
1162 return project_objects_pb2.FieldDef.DateTypeSettings.DateAction.Value(
1163 'NOTIFY_PARTICIPANTS')
1164 else:
1165 raise ValueError('Unsupported DateAction Value')
1166
1167 def _ConvertRoleRequirements(self, needs_member):
1168 # type: (bool) ->
1169 # api_proto.project_objects_pb2.FieldDef.
1170 # UserTypeSettings.RoleRequirements
1171 """Convert protorpc RoleRequirements to protoc
1172 FieldDef.UserTypeSettings.RoleRequirements"""
1173
1174 proto_user_settings = project_objects_pb2.FieldDef.UserTypeSettings
1175 if needs_member:
1176 return proto_user_settings.RoleRequirements.Value('PROJECT_MEMBER')
1177 else:
1178 return proto_user_settings.RoleRequirements.Value('NO_ROLE_REQUIREMENT')
1179
1180 def _ConvertNotifyTriggers(self, notify_trigger):
1181 # type: (proto.tracker_pb2.NotifyTriggers) ->
1182 # api_proto.project_objects_pb2.FieldDef.UserTypeSettings.NotifyTriggers
1183 """Convert protorpc NotifyTriggers to protoc
1184 FieldDef.UserTypeSettings.NotifyTriggers"""
1185 if notify_trigger == tracker_pb2.NotifyTriggers.NEVER:
1186 return project_objects_pb2.FieldDef.UserTypeSettings.NotifyTriggers.Value(
1187 'NEVER')
1188 elif notify_trigger == tracker_pb2.NotifyTriggers.ANY_COMMENT:
1189 return project_objects_pb2.FieldDef.UserTypeSettings.NotifyTriggers.Value(
1190 'ANY_COMMENT')
1191 else:
1192 raise ValueError('Unsupported NotifyTriggers Value')
1193
1194 def _ConvertFieldDefType(self, field_type):
1195 # type: (proto.tracker_pb2.FieldTypes) ->
1196 # api_proto.project_objects_pb2.FieldDef.Type
1197 """Convert protorpc FieldType to protoc FieldDef.Type
1198
1199 Args:
1200 field_type: protorpc FieldType
1201
1202 Returns:
1203 Corresponding protoc FieldDef.Type
1204
1205 Raises:
1206 ValueError if input `field_type` has no suitable supported FieldDef.Type,
1207 or input `field_type` is not a recognized enum option.
1208 """
1209 if field_type == tracker_pb2.FieldTypes.ENUM_TYPE:
1210 return project_objects_pb2.FieldDef.Type.Value('ENUM')
1211 elif field_type == tracker_pb2.FieldTypes.INT_TYPE:
1212 return project_objects_pb2.FieldDef.Type.Value('INT')
1213 elif field_type == tracker_pb2.FieldTypes.STR_TYPE:
1214 return project_objects_pb2.FieldDef.Type.Value('STR')
1215 elif field_type == tracker_pb2.FieldTypes.USER_TYPE:
1216 return project_objects_pb2.FieldDef.Type.Value('USER')
1217 elif field_type == tracker_pb2.FieldTypes.DATE_TYPE:
1218 return project_objects_pb2.FieldDef.Type.Value('DATE')
1219 elif field_type == tracker_pb2.FieldTypes.URL_TYPE:
1220 return project_objects_pb2.FieldDef.Type.Value('URL')
1221 else:
1222 raise ValueError(
1223 'Unsupported tracker_pb2.FieldType enum. Boolean types '
1224 'are unsupported and approval types are found in ApprovalDefs')
1225
1226 def _ComputeFieldDefTraits(self, field_def):
1227 # type: (proto.tracker_pb2.FieldDef) ->
1228 # Sequence[api_proto.project_objects_pb2.FieldDef.Traits]
1229 """Compute sequence of FieldDef.Traits for a given protorpc FieldDef."""
1230 trait_protos = []
1231 if field_def.is_required:
1232 trait_protos.append(project_objects_pb2.FieldDef.Traits.Value('REQUIRED'))
1233 if field_def.is_niche:
1234 trait_protos.append(
1235 project_objects_pb2.FieldDef.Traits.Value('DEFAULT_HIDDEN'))
1236 if field_def.is_multivalued:
1237 trait_protos.append(
1238 project_objects_pb2.FieldDef.Traits.Value('MULTIVALUED'))
1239 if field_def.is_phase_field:
1240 trait_protos.append(project_objects_pb2.FieldDef.Traits.Value('PHASE'))
1241 if field_def.is_restricted_field:
1242 trait_protos.append(
1243 project_objects_pb2.FieldDef.Traits.Value('RESTRICTED'))
1244 return trait_protos
1245
1246 def _GetEnumFieldChoices(self, field_def):
1247 # type: (proto.tracker_pb2.FieldDef) ->
1248 # Sequence[Choice]
1249 """Get sequence of choices for an enum field
1250
1251 Args:
1252 field_def: protorpc FieldDef
1253
1254 Returns:
1255 Sequence of valid Choices for enum field `field_def`.
1256
1257 Raises:
1258 ValueError if input `field_def` is not an enum type field.
1259 """
1260 if field_def.field_type != tracker_pb2.FieldTypes.ENUM_TYPE:
1261 raise ValueError('Cannot get value from label for non-enum-type field')
1262
1263 config = self.services.config.GetProjectConfig(
1264 self.cnxn, field_def.project_id)
1265 value_docstr_tuples = tracker_helpers._GetEnumFieldValuesAndDocstrings(
1266 field_def, config)
1267
1268 return [
1269 Choice(value=value, docstring=docstring)
1270 for value, docstring in value_docstr_tuples
1271 ]
1272
1273 # Field Values
1274
1275 def _GetNonApprovalFieldValues(self, field_values, project_id):
1276 # type: (Sequence[proto.tracker_pb2.FieldValue], int) ->
1277 # Sequence[proto.tracker_pb2.FieldValue]
1278 """Filter out field values that belong to an approval field."""
1279 config = self.services.config.GetProjectConfig(self.cnxn, project_id)
1280 approval_fd_ids = set(
1281 [fd.field_id for fd in config.field_defs if fd.approval_id])
1282
1283 return [fv for fv in field_values if fv.field_id not in approval_fd_ids]
1284
1285 def ConvertFieldValues(self, field_values, project_id, phases):
1286 # type: (Sequence[proto.tracker_pb2.FieldValue], int,
1287 # Sequence[proto.tracker_pb2.Phase]) ->
1288 # Sequence[api_proto.issue_objects_pb2.FieldValue]
1289 """Convert sequence of field_values to protoc FieldValues.
1290
1291 This method does not handle enum_type fields.
1292
1293 Args:
1294 field_values: List of FieldValues
1295 project_id: ID of the Project that is ancestor to all given
1296 `field_values`.
1297 phases: List of Phases
1298
1299 Returns:
1300 Sequence of protoc FieldValues in the same order they are given in
1301 `field_values`. In the event any field_values in `field_values` are not
1302 found, they will be omitted from the result.
1303 """
1304 phase_names_by_id = {phase.phase_id: phase.name for phase in phases}
1305 field_ids = [fv.field_id for fv in field_values]
1306 resource_names_dict = rnc.ConvertFieldDefNames(
1307 self.cnxn, field_ids, project_id, self.services)
1308
1309 api_fvs = []
1310 for fv in field_values:
1311 if fv.field_id not in resource_names_dict:
1312 continue
1313
1314 name = resource_names_dict.get(fv.field_id)
1315 value = self._ComputeFieldValueString(fv)
1316 derivation = self._ComputeFieldValueDerivation(fv)
1317 phase = phase_names_by_id.get(fv.phase_id)
1318 api_item = issue_objects_pb2.FieldValue(
1319 field=name, value=value, derivation=derivation, phase=phase)
1320 api_fvs.append(api_item)
1321
1322 return api_fvs
1323
1324 def _ComputeFieldValueString(self, field_value):
1325 # type: (proto.tracker_pb2.FieldValue) -> str
1326 """Convert a FieldValue's value to a string."""
1327 if field_value is None:
1328 raise exceptions.InputException('No FieldValue specified')
1329 elif field_value.int_value is not None:
1330 return str(field_value.int_value)
1331 elif field_value.str_value is not None:
1332 return field_value.str_value
1333 elif field_value.user_id is not None:
1334 return rnc.ConvertUserNames([field_value.user_id
1335 ]).get(field_value.user_id)
1336 elif field_value.date_value is not None:
1337 return str(field_value.date_value)
1338 elif field_value.url_value is not None:
1339 return field_value.url_value
1340 else:
1341 raise exceptions.InputException('FieldValue must have at least one value')
1342
1343 def _ComputeFieldValueDerivation(self, field_value):
1344 # type: (proto.tracker_pb2.FieldValue) ->
1345 # api_proto.issue_objects_pb2.Issue.Derivation
1346 """Convert a FieldValue's 'derived' to a protoc Issue.Derivation.
1347
1348 Args:
1349 field_value: protorpc FieldValue
1350
1351 Returns:
1352 Issue.Derivation of given `field_value`
1353 """
1354 if field_value.derived:
1355 return issue_objects_pb2.Derivation.Value('RULE')
1356 else:
1357 return issue_objects_pb2.Derivation.Value('EXPLICIT')
1358
1359 # Approval Def
1360
1361 def ConvertApprovalDefs(self, approval_defs, project_id):
1362 # type: (Sequence[proto.tracker_pb2.ApprovalDef], int) ->
1363 # Sequence[api_proto.project_objects_pb2.ApprovalDef]
1364 """Convert sequence of protorpc ApprovalDefs to protoc ApprovalDefs.
1365
1366 Args:
1367 approval_defs: List of protorpc ApprovalDefs
1368 project_id: ID of the Project the approval_defs belong to.
1369
1370 Returns:
1371 Sequence of protoc ApprovalDefs in the same order they are given in
1372 in `approval_defs`. In the event any approval_def in `approval_defs`
1373 are not found, they will be omitted from the result.
1374 """
1375 approval_ids = set([ad.approval_id for ad in approval_defs])
1376 resource_names_dict = rnc.ConvertApprovalDefNames(
1377 self.cnxn, approval_ids, project_id, self.services)
1378
1379 # Get matching field defs, needed to fill out protoc ApprovalDefs
1380 config = self.services.config.GetProjectConfig(self.cnxn, project_id)
1381 fd_by_id = {}
1382 for fd in config.field_defs:
1383 if (fd.field_type == tracker_pb2.FieldTypes.APPROVAL_TYPE and
1384 fd.field_id in approval_ids):
1385 fd_by_id[fd.field_id] = fd
1386
1387 all_users = tbo.UsersInvolvedInApprovalDefs(
1388 approval_defs, fd_by_id.values())
1389 user_resource_names_dict = rnc.ConvertUserNames(all_users)
1390
1391 api_ads = []
1392 for ad in approval_defs:
1393 if (ad.approval_id not in resource_names_dict or
1394 ad.approval_id not in fd_by_id):
1395 continue
1396 matching_fd = fd_by_id.get(ad.approval_id)
1397 name = resource_names_dict.get(ad.approval_id)
1398 display_name = matching_fd.field_name
1399 docstring = matching_fd.docstring
1400 survey = ad.survey
1401 approvers = [
1402 user_resource_names_dict.get(approver_id)
1403 for approver_id in ad.approver_ids
1404 ]
1405 admins = [
1406 user_resource_names_dict.get(admin_id)
1407 for admin_id in matching_fd.admin_ids
1408 ]
1409
1410 api_ad = project_objects_pb2.ApprovalDef(
1411 name=name,
1412 display_name=display_name,
1413 docstring=docstring,
1414 survey=survey,
1415 approvers=approvers,
1416 admins=admins)
1417 api_ads.append(api_ad)
1418 return api_ads
1419
1420 def ConvertApprovalValues(self, approval_values, field_values, phases,
1421 issue_id=None, project_id=None):
1422 # type: (Sequence[proto.tracker_pb2.ApprovalValue],
1423 # Sequence[proto.tracker_pb2.FieldValue],
1424 # Sequence[proto.tracker_pb2.Phase], Optional[int], Optional[int]) ->
1425 # Sequence[api_proto.issue_objects_pb2.ApprovalValue]
1426 """Convert sequence of approval_values to protoc ApprovalValues.
1427
1428 `approval_values` may belong to a template or an issue. If they belong to a
1429 template, `project_id` should be given for the project the template is in.
1430 If these are issue `approval_values` `issue_id` should be given`.
1431 So, one of `issue_id` or `project_id` must be provided.
1432 If both are given, we ignore `project_id` and assume the `approval_values`
1433 belong to an issue.
1434
1435 Args:
1436 approval_values: List of ApprovalValues.
1437 field_values: List of FieldValues that may belong to the approval_values.
1438 phases: List of Phases that may be associated with the approval_values.
1439 issue_id: ID of the Issue that the `approval_values` belong to.
1440 project_id: ID of the Project that the `approval_values`
1441 template belongs to.
1442
1443 Returns:
1444 Sequence of protoc ApprovalValues in the same order they are given in
1445 in `approval_values`. In the event any approval definitions in
1446 `approval_values` are not found, they will be omitted from the result.
1447
1448 Raises:
1449 InputException if neither `issue_id` nor `project_id` is given.
1450 """
1451
1452 approval_ids = [av.approval_id for av in approval_values]
1453 resource_names_dict = {}
1454 if issue_id is not None:
1455 # Only issue approval_values have resource names.
1456 resource_names_dict = rnc.ConvertApprovalValueNames(
1457 self.cnxn, issue_id, self.services)
1458 project_id = self.services.issue.GetIssue(self.cnxn, issue_id).project_id
1459 elif project_id is None:
1460 raise exceptions.InputException(
1461 'One `issue_id` or `project_id` must be given.')
1462
1463 phase_names_by_id = {phase.phase_id: phase.name for phase in phases}
1464 ad_names_dict = rnc.ConvertApprovalDefNames(
1465 self.cnxn, approval_ids, project_id, self.services)
1466
1467 # Organize the field values by the approval values they are
1468 # associated with.
1469 config = self.services.config.GetProjectConfig(self.cnxn, project_id)
1470 fds_by_id = {fd.field_id: fd for fd in config.field_defs}
1471 fvs_by_parent_approvals = collections.defaultdict(list)
1472 for fv in field_values:
1473 fd = fds_by_id.get(fv.field_id)
1474 if fd and fd.approval_id:
1475 fvs_by_parent_approvals[fd.approval_id].append(fv)
1476
1477 api_avs = []
1478 for av in approval_values:
1479 # We only skip missing approval names if we are converting issue approval
1480 # values.
1481 if issue_id is not None and av.approval_id not in resource_names_dict:
1482 continue
1483
1484 name = resource_names_dict.get(av.approval_id)
1485 approval_def = ad_names_dict.get(av.approval_id)
1486 approvers = rnc.ConvertUserNames(av.approver_ids).values()
1487 status = self._ComputeApprovalValueStatus(av.status)
1488 setter = rnc.ConvertUserName(av.setter_id)
1489 phase = phase_names_by_id.get(av.phase_id)
1490
1491 field_values = self.ConvertFieldValues(
1492 fvs_by_parent_approvals[av.approval_id], project_id, phases)
1493
1494 api_item = issue_objects_pb2.ApprovalValue(
1495 name=name,
1496 approval_def=approval_def,
1497 approvers=approvers,
1498 status=status,
1499 setter=setter,
1500 field_values=field_values,
1501 phase=phase)
1502 if av.set_on:
1503 api_item.set_time.FromSeconds(av.set_on)
1504 api_avs.append(api_item)
1505
1506 return api_avs
1507
1508 def _ComputeApprovalValueStatus(self, status):
1509 # type: (proto.tracker_pb2.ApprovalStatus) ->
1510 # api_proto.issue_objects_pb2.Issue.ApprovalStatus
1511 """Convert a protorpc ApprovalStatus to a protoc Issue.ApprovalStatus."""
1512 try:
1513 return _APPROVAL_STATUS_CONVERT[status]
1514 except KeyError:
1515 raise ValueError('Unrecognized tracker_pb2.ApprovalStatus enum')
1516
1517 # Projects
1518
1519 def ConvertIssueTemplates(self, project_id, templates):
1520 # type: (int, Sequence[proto.tracker_pb2.TemplateDef]) ->
1521 # Sequence[api_proto.project_objects_pb2.IssueTemplate]
1522 """Convert a Sequence of TemplateDefs to protoc IssueTemplates.
1523
1524 Args:
1525 project_id: ID of the Project the templates belong to.
1526 templates: Sequence of TemplateDef protorpc objects.
1527
1528 Returns:
1529 Sequence of protoc IssueTemplate in the same order they are given in
1530 `templates`. In the rare event that any templates are not found,
1531 they will be omitted from the result.
1532 """
1533 api_templates = []
1534
1535 resource_names_dict = rnc.ConvertTemplateNames(
1536 self.cnxn, project_id, [template.template_id for template in templates],
1537 self.services)
1538
1539 for template in templates:
1540 if template.template_id not in resource_names_dict:
1541 continue
1542 name = resource_names_dict.get(template.template_id)
1543 summary_must_be_edited = template.summary_must_be_edited
1544 template_privacy = self._ComputeTemplatePrivacy(template)
1545 default_owner = self._ComputeTemplateDefaultOwner(template)
1546 component_required = template.component_required
1547 admins = rnc.ConvertUserNames(template.admin_ids).values()
1548 issue = self._FillIssueFromTemplate(template, project_id)
1549 approval_values = self.ConvertApprovalValues(
1550 template.approval_values, template.field_values, template.phases,
1551 project_id=project_id)
1552 api_templates.append(
1553 project_objects_pb2.IssueTemplate(
1554 name=name,
1555 display_name=template.name,
1556 issue=issue,
1557 approval_values=approval_values,
1558 summary_must_be_edited=summary_must_be_edited,
1559 template_privacy=template_privacy,
1560 default_owner=default_owner,
1561 component_required=component_required,
1562 admins=admins))
1563
1564 return api_templates
1565
1566 def _FillIssueFromTemplate(self, template, project_id):
1567 # type: (proto.tracker_pb2.TemplateDef, int) ->
1568 # api_proto.issue_objects_pb2.Issue
1569 """Convert a TemplateDef to its embedded protoc Issue.
1570
1571 IssueTemplate does not set the following fields:
1572 name
1573 reporter
1574 cc_users
1575 blocked_on_issue_refs
1576 blocking_issue_refs
1577 create_time
1578 close_time
1579 modify_time
1580 component_modify_time
1581 status_modify_time
1582 owner_modify_time
1583 attachment_count
1584 star_count
1585
1586 Args:
1587 template: TemplateDef protorpc objects.
1588 project_id: ID of the Project the template belongs to.
1589
1590 Returns:
1591 protoc Issue filled with data from given `template`.
1592 """
1593 summary = template.summary
1594 state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
1595 status = issue_objects_pb2.Issue.StatusValue(
1596 status=template.status,
1597 derivation=issue_objects_pb2.Derivation.Value('EXPLICIT'))
1598 owner = None
1599 if template.owner_id is not None:
1600 owner = issue_objects_pb2.Issue.UserValue(
1601 user=rnc.ConvertUserNames([template.owner_id]).get(template.owner_id))
1602 labels = self.ConvertLabels(template.labels, [], project_id)
1603 components_dict = rnc.ConvertComponentDefNames(
1604 self.cnxn, template.component_ids, project_id, self.services)
1605 components = []
1606 for component_resource_name in components_dict.values():
1607 components.append(
1608 issue_objects_pb2.Issue.ComponentValue(
1609 component=component_resource_name,
1610 derivation=issue_objects_pb2.Derivation.Value('EXPLICIT')))
1611 non_approval_fvs = self._GetNonApprovalFieldValues(
1612 template.field_values, project_id)
1613 field_values = self.ConvertFieldValues(
1614 non_approval_fvs, project_id, template.phases)
1615 field_values.extend(
1616 self.ConvertEnumFieldValues(template.labels, [], project_id))
1617 phases = self._ComputePhases(template.phases)
1618
1619 filled_issue = issue_objects_pb2.Issue(
1620 summary=summary,
1621 state=state,
1622 status=status,
1623 owner=owner,
1624 labels=labels,
1625 components=components,
1626 field_values=field_values,
1627 phases=phases)
1628 return filled_issue
1629
1630 def _ComputeTemplatePrivacy(self, template):
1631 # type: (proto.tracker_pb2.TemplateDef) ->
1632 # api_proto.project_objects_pb2.IssueTemplate.TemplatePrivacy
1633 """Convert a protorpc TemplateDef to its protoc TemplatePrivacy."""
1634 if template.members_only:
1635 return project_objects_pb2.IssueTemplate.TemplatePrivacy.Value(
1636 'MEMBERS_ONLY')
1637 else:
1638 return project_objects_pb2.IssueTemplate.TemplatePrivacy.Value('PUBLIC')
1639
1640 def _ComputeTemplateDefaultOwner(self, template):
1641 # type: (proto.tracker_pb2.TemplateDef) ->
1642 # api_proto.project_objects_pb2.IssueTemplate.DefaultOwner
1643 """Convert a protorpc TemplateDef to its protoc DefaultOwner."""
1644 if template.owner_defaults_to_member:
1645 return project_objects_pb2.IssueTemplate.DefaultOwner.Value(
1646 'PROJECT_MEMBER_REPORTER')
1647 else:
1648 return project_objects_pb2.IssueTemplate.DefaultOwner.Value(
1649 'DEFAULT_OWNER_UNSPECIFIED')
1650
1651 def _ComputePhases(self, phases):
1652 # type: (proto.tracker_pb2.TemplateDef) -> Sequence[str]
1653 """Convert a protorpc TemplateDef to its sorted string phases."""
1654 sorted_phases = sorted(phases, key=lambda phase: phase.rank)
1655 return [phase.name for phase in sorted_phases]
1656
1657 def ConvertLabels(self, labels, derived_labels, project_id):
1658 # type: (Sequence[str], Sequence[str], int) ->
1659 # Sequence[api_proto.issue_objects_pb2.Issue.LabelValue]
1660 """Convert string labels to LabelValues for non-enum-field labels
1661
1662 Args:
1663 labels: Sequence of string labels
1664 project_id: ID of the Project these labels belong to.
1665
1666 Return:
1667 Sequence of protoc IssueValues for given `labels` that
1668 do not represent enum field values.
1669 """
1670 config = self.services.config.GetProjectConfig(self.cnxn, project_id)
1671 non_fd_labels, non_fd_der_labels = tbo.ExplicitAndDerivedNonMaskedLabels(
1672 labels, derived_labels, config)
1673 api_labels = []
1674 for label in non_fd_labels:
1675 api_labels.append(
1676 issue_objects_pb2.Issue.LabelValue(
1677 label=label,
1678 derivation=issue_objects_pb2.Derivation.Value('EXPLICIT')))
1679 for label in non_fd_der_labels:
1680 api_labels.append(
1681 issue_objects_pb2.Issue.LabelValue(
1682 label=label,
1683 derivation=issue_objects_pb2.Derivation.Value('RULE')))
1684 return api_labels
1685
1686 def ConvertEnumFieldValues(self, labels, derived_labels, project_id):
1687 # type: (Sequence[str], Sequence[str], int) ->
1688 # Sequence[api_proto.issue_objects_pb2.FieldValue]
1689 """Convert string labels to FieldValues for enum-field labels
1690
1691 Args:
1692 labels: Sequence of string labels
1693 project_id: ID of the Project these labels belong to.
1694
1695 Return:
1696 Sequence of protoc FieldValues only for given `labels` that
1697 represent enum field values.
1698 """
1699 config = self.services.config.GetProjectConfig(self.cnxn, project_id)
1700 enum_ids_by_name = {
1701 fd.field_name.lower(): fd.field_id
1702 for fd in config.field_defs
1703 if fd.field_type is tracker_pb2.FieldTypes.ENUM_TYPE
1704 and not fd.is_deleted
1705 }
1706 resource_names_dict = rnc.ConvertFieldDefNames(
1707 self.cnxn, enum_ids_by_name.values(), project_id, self.services)
1708
1709 api_fvs = []
1710
1711 labels_by_prefix = tbo.LabelsByPrefix(labels, enum_ids_by_name.keys())
1712 for lower_field_name, values in labels_by_prefix.items():
1713 field_id = enum_ids_by_name.get(lower_field_name)
1714 resource_name = resource_names_dict.get(field_id)
1715 if not resource_name:
1716 continue
1717 api_fvs.extend(
1718 [
1719 issue_objects_pb2.FieldValue(
1720 field=resource_name,
1721 value=value,
1722 derivation=issue_objects_pb2.Derivation.Value(
1723 'EXPLICIT')) for value in values
1724 ])
1725
1726 der_labels_by_prefix = tbo.LabelsByPrefix(
1727 derived_labels, enum_ids_by_name.keys())
1728 for lower_field_name, values in der_labels_by_prefix.items():
1729 field_id = enum_ids_by_name.get(lower_field_name)
1730 resource_name = resource_names_dict.get(field_id)
1731 if not resource_name:
1732 continue
1733 api_fvs.extend(
1734 [
1735 issue_objects_pb2.FieldValue(
1736 field=resource_name,
1737 value=value,
1738 derivation=issue_objects_pb2.Derivation.Value('RULE'))
1739 for value in values
1740 ])
1741
1742 return api_fvs
1743
1744 def ConvertProject(self, project):
1745 # type: (proto.project_pb2.Project) ->
1746 # api_proto.project_objects_pb2.Project
1747 """Convert a protorpc Project to its protoc Project."""
1748
1749 return project_objects_pb2.Project(
1750 name=rnc.ConvertProjectName(
1751 self.cnxn, project.project_id, self.services),
1752 display_name=project.project_name,
1753 summary=project.summary,
1754 thumbnail_url=project_helpers.GetThumbnailUrl(project.logo_gcs_id))
1755
1756 def ConvertProjects(self, projects):
1757 # type: (Sequence[proto.project_pb2.Project]) ->
1758 # Sequence[api_proto.project_objects_pb2.Project]
1759 """Convert a Sequence of protorpc Projects to protoc Projects."""
1760 return [self.ConvertProject(proj) for proj in projects]
1761
1762 def ConvertProjectConfig(self, project_config):
1763 # type: (proto.tracker_pb2.ProjectIssueConfig) ->
1764 # api_proto.project_objects_pb2.ProjectConfig
1765 """Convert protorpc ProjectIssueConfig to protoc ProjectConfig."""
1766 project = self.services.project.GetProject(
1767 self.cnxn, project_config.project_id)
1768 project_grid_config = project_objects_pb2.ProjectConfig.GridViewConfig(
1769 default_x_attr=project_config.default_x_attr,
1770 default_y_attr=project_config.default_y_attr)
1771 template_names = rnc.ConvertTemplateNames(
1772 self.cnxn, project_config.project_id, [
1773 project_config.default_template_for_developers,
1774 project_config.default_template_for_users
1775 ], self.services)
1776 return project_objects_pb2.ProjectConfig(
1777 name=rnc.ConvertProjectConfigName(
1778 self.cnxn, project_config.project_id, self.services),
1779 exclusive_label_prefixes=project_config.exclusive_label_prefixes,
1780 member_default_query=project_config.member_default_query,
1781 default_sort=project_config.default_sort_spec,
1782 default_columns=self._ComputeIssuesListColumns(
1783 project_config.default_col_spec),
1784 project_grid_config=project_grid_config,
1785 member_default_template=template_names.get(
1786 project_config.default_template_for_developers),
1787 non_members_default_template=template_names.get(
1788 project_config.default_template_for_users),
1789 revision_url_format=project.revision_url_format,
1790 custom_issue_entry_url=project_config.custom_issue_entry_url)
1791
1792 def CreateProjectMember(self, cnxn, project_id, user_id, role):
1793 # type: (MonorailContext, int, int, str) ->
1794 # api_proto.project_objects_pb2.ProjectMember
1795 """Creates a ProjectMember object from specified parameters.
1796
1797 Args:
1798 cnxn: MonorailConnection object.
1799 project_id: ID of the Project the User is a member of.
1800 user_id: ID of the user who is a member.
1801 role: str specifying the user's role based on a ProjectRole value.
1802
1803 Return:
1804 A protoc ProjectMember object.
1805 """
1806 name = rnc.ConvertProjectMemberName(
1807 cnxn, project_id, user_id, self.services)
1808 return project_objects_pb2.ProjectMember(
1809 name=name,
1810 role=project_objects_pb2.ProjectMember.ProjectRole.Value(role))
1811
1812 def ConvertLabelDefs(self, label_defs, project_id):
1813 # type: (Sequence[proto.tracker_pb2.LabelDef], int) ->
1814 # Sequence[api_proto.project_objects_pb2.LabelDef]
1815 """Convert protorpc LabelDefs to protoc LabelDefs"""
1816 resource_names_dict = rnc.ConvertLabelDefNames(
1817 self.cnxn, [ld.label for ld in label_defs], project_id, self.services)
1818
1819 api_lds = []
1820 for ld in label_defs:
1821 state = project_objects_pb2.LabelDef.LabelDefState.Value('ACTIVE')
1822 if ld.deprecated:
1823 state = project_objects_pb2.LabelDef.LabelDefState.Value('DEPRECATED')
1824 api_lds.append(
1825 project_objects_pb2.LabelDef(
1826 name=resource_names_dict.get(ld.label),
1827 value=ld.label,
1828 docstring=ld.label_docstring,
1829 state=state))
1830 return api_lds
1831
1832 def ConvertStatusDefs(self, status_defs, project_id):
1833 # type: (Sequence[proto.tracker_pb2.StatusDef], int) ->
1834 # Sequence[api_proto.project_objects_pb2.StatusDef]
1835 """Convert protorpc StatusDefs to protoc StatusDefs
1836
1837 Args:
1838 status_defs: Sequence of StatusDefs.
1839 project_id: ID of the Project these belong to.
1840
1841 Returns:
1842 Sequence of protoc StatusDefs in the same order they are given in
1843 `status_defs`.
1844 """
1845 resource_names_dict = rnc.ConvertStatusDefNames(
1846 self.cnxn, [sd.status for sd in status_defs], project_id, self.services)
1847 config = self.services.config.GetProjectConfig(self.cnxn, project_id)
1848 mergeable_statuses = set(config.statuses_offer_merge)
1849
1850 # Rank is only surfaced as positional value in well_known_statuses
1851 rank_by_status = {}
1852 for rank, sd in enumerate(config.well_known_statuses):
1853 rank_by_status[sd.status] = rank
1854
1855 api_sds = []
1856 for sd in status_defs:
1857 state = project_objects_pb2.StatusDef.StatusDefState.Value('ACTIVE')
1858 if sd.deprecated:
1859 state = project_objects_pb2.StatusDef.StatusDefState.Value('DEPRECATED')
1860
1861 if sd.means_open:
1862 status_type = project_objects_pb2.StatusDef.StatusDefType.Value('OPEN')
1863 else:
1864 if sd.status in mergeable_statuses:
1865 status_type = project_objects_pb2.StatusDef.StatusDefType.Value(
1866 'MERGED')
1867 else:
1868 status_type = project_objects_pb2.StatusDef.StatusDefType.Value(
1869 'CLOSED')
1870
1871 api_sd = project_objects_pb2.StatusDef(
1872 name=resource_names_dict.get(sd.status),
1873 value=sd.status,
1874 type=status_type,
1875 rank=rank_by_status[sd.status],
1876 docstring=sd.status_docstring,
1877 state=state,
1878 )
1879 api_sds.append(api_sd)
1880 return api_sds
1881
1882 def ConvertComponentDef(self, component_def):
1883 # type: (proto.tracker_pb2.ComponentDef) ->
1884 # api_proto.project_objects.ComponentDef
1885 """Convert a protorpc ComponentDef to a protoc ComponentDef."""
1886 return self.ConvertComponentDefs([component_def],
1887 component_def.project_id)[0]
1888
1889 def ConvertComponentDefs(self, component_defs, project_id):
1890 # type: (Sequence[proto.tracker_pb2.ComponentDef], int) ->
1891 # Sequence[api_proto.project_objects.ComponentDef]
1892 """Convert sequence of protorpc ComponentDefs to protoc ComponentDefs
1893
1894 Args:
1895 component_defs: Sequence of protoc ComponentDefs.
1896 project_id: ID of the Project these belong to.
1897
1898 Returns:
1899 Sequence of protoc ComponentDefs in the same order they are given in
1900 `component_defs`.
1901 """
1902 resource_names_dict = rnc.ConvertComponentDefNames(
1903 self.cnxn, [cd.component_id for cd in component_defs], project_id,
1904 self.services)
1905 involved_user_ids = tbo.UsersInvolvedInComponents(component_defs)
1906 user_resource_names_dict = rnc.ConvertUserNames(involved_user_ids)
1907
1908 all_label_ids = set()
1909 for cd in component_defs:
1910 all_label_ids.update(cd.label_ids)
1911
1912 # If this becomes a performance issue, we should add bulk look up.
1913 labels_by_id = {
1914 label_id: self.services.config.LookupLabel(
1915 self.cnxn, project_id, label_id) for label_id in all_label_ids
1916 }
1917
1918 api_cds = []
1919 for cd in component_defs:
1920 state = project_objects_pb2.ComponentDef.ComponentDefState.Value('ACTIVE')
1921 if cd.deprecated:
1922 state = project_objects_pb2.ComponentDef.ComponentDefState.Value(
1923 'DEPRECATED')
1924
1925 api_cd = project_objects_pb2.ComponentDef(
1926 name=resource_names_dict.get(cd.component_id),
1927 value=cd.path,
1928 docstring=cd.docstring,
1929 state=state,
1930 admins=[
1931 user_resource_names_dict.get(admin_id)
1932 for admin_id in cd.admin_ids
1933 ],
1934 ccs=[user_resource_names_dict.get(cc_id) for cc_id in cd.cc_ids],
1935 creator=user_resource_names_dict.get(cd.creator_id),
1936 modifier=user_resource_names_dict.get(cd.modifier_id),
1937 create_time=timestamp_pb2.Timestamp(seconds=cd.created),
1938 modify_time=timestamp_pb2.Timestamp(seconds=cd.modified),
1939 labels=[labels_by_id[label_id] for label_id in cd.label_ids],
1940 )
1941 api_cds.append(api_cd)
1942 return api_cds
1943
1944 def ConvertProjectSavedQueries(self, saved_queries, project_id):
1945 # type: (Sequence[proto.tracker_pb2.SavedQuery], int) ->
1946 # Sequence(api_proto.project_objects.ProjectSavedQuery)
1947 """Convert sequence of protorpc SavedQueries to protoc ProjectSavedQueries
1948
1949 Args:
1950 saved_queries: Sequence of SavedQueries.
1951 project_id: ID of the Project these belong to.
1952
1953 Returns:
1954 Sequence of protoc ProjectSavedQueries in the same order they are given in
1955 `saved_queries`. In the event any items in `saved_queries` are not found
1956 or don't belong to the project, they will be omitted from the result.
1957 """
1958 resource_names_dict = rnc.ConvertProjectSavedQueryNames(
1959 self.cnxn, [sq.query_id for sq in saved_queries], project_id,
1960 self.services)
1961 api_psqs = []
1962 for sq in saved_queries:
1963 if sq.query_id not in resource_names_dict:
1964 continue
1965
1966 # TODO(crbug/monorail/7756): Remove base_query_id, avoid confusions.
1967 # Until then we have to expand the query by including base_query_id.
1968 # base_query_id can only be in the set of DEFAULT_CANNED_QUERIES.
1969 if sq.base_query_id:
1970 query = '{} {}'.format(tbo.GetBuiltInQuery(sq.base_query_id), sq.query)
1971 else:
1972 query = sq.query
1973
1974 api_psqs.append(
1975 project_objects_pb2.ProjectSavedQuery(
1976 name=resource_names_dict.get(sq.query_id),
1977 display_name=sq.name,
1978 query=query))
1979 return api_psqs