Project import generated by Copybara.
GitOrigin-RevId: d9e9e3fb4e31372ec1fb43b178994ca78fa8fe70
diff --git a/api/v3/converters.py b/api/v3/converters.py
new file mode 100644
index 0000000..60aebd7
--- /dev/null
+++ b/api/v3/converters.py
@@ -0,0 +1,1979 @@
+# Copyright 2020 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style
+# license that can be found in the LICENSE file.
+
+from __future__ import print_function
+from __future__ import division
+from __future__ import absolute_import
+
+import collections
+import itertools
+import logging
+import time
+
+from google.protobuf import timestamp_pb2
+
+from api import resource_name_converters as rnc
+from api.v3.api_proto import feature_objects_pb2
+from api.v3.api_proto import issues_pb2
+from api.v3.api_proto import issue_objects_pb2
+from api.v3.api_proto import project_objects_pb2
+from api.v3.api_proto import user_objects_pb2
+
+from framework import exceptions
+from framework import filecontent
+from framework import framework_bizobj
+from framework import framework_constants
+from framework import framework_helpers
+from proto import tracker_pb2
+from project import project_helpers
+from tracker import attachment_helpers
+from tracker import field_helpers
+from tracker import tracker_bizobj as tbo
+from tracker import tracker_helpers
+
+Choice = project_objects_pb2.FieldDef.EnumTypeSettings.Choice
+
+# Ingest/convert dicts for ApprovalStatus.
+_V3_APPROVAL_STATUS = issue_objects_pb2.ApprovalValue.ApprovalStatus.Value
+_APPROVAL_STATUS_INGEST = {
+ _V3_APPROVAL_STATUS('APPROVAL_STATUS_UNSPECIFIED'): None,
+ _V3_APPROVAL_STATUS('NOT_SET'): tracker_pb2.ApprovalStatus.NOT_SET,
+ _V3_APPROVAL_STATUS('NEEDS_REVIEW'): tracker_pb2.ApprovalStatus.NEEDS_REVIEW,
+ _V3_APPROVAL_STATUS('NA'): tracker_pb2.ApprovalStatus.NA,
+ _V3_APPROVAL_STATUS('REVIEW_REQUESTED'):
+ tracker_pb2.ApprovalStatus.REVIEW_REQUESTED,
+ _V3_APPROVAL_STATUS('REVIEW_STARTED'):
+ tracker_pb2.ApprovalStatus.REVIEW_STARTED,
+ _V3_APPROVAL_STATUS('NEED_INFO'): tracker_pb2.ApprovalStatus.NEED_INFO,
+ _V3_APPROVAL_STATUS('APPROVED'): tracker_pb2.ApprovalStatus.APPROVED,
+ _V3_APPROVAL_STATUS('NOT_APPROVED'): tracker_pb2.ApprovalStatus.NOT_APPROVED,
+}
+_APPROVAL_STATUS_CONVERT = {
+ val: key for key, val in _APPROVAL_STATUS_INGEST.items()}
+
+
+class Converter(object):
+ """Class to manage converting objects between the API and backend layer."""
+
+ def __init__(self, mc, services):
+ # type: (MonorailContext, Services) -> Converter
+ """Create a Converter with the given MonorailContext and Services.
+
+ Args:
+ mc: MonorailContext object containing the MonorailConnection to the DB
+ and the requester's AuthData object.
+ services: Services object for connections to backend services.
+ """
+ self.cnxn = mc.cnxn
+ self.user_auth = mc.auth
+ self.services = services
+
+ # Hotlists
+
+ def ConvertHotlist(self, hotlist):
+ # type: (proto.feature_objects_pb2.Hotlist)
+ # -> api_proto.feature_objects_pb2.Hotlist
+ """Convert a protorpc Hotlist into a protoc Hotlist."""
+
+ hotlist_resource_name = rnc.ConvertHotlistName(hotlist.hotlist_id)
+ members_by_id = rnc.ConvertUserNames(
+ hotlist.owner_ids + hotlist.editor_ids)
+ default_columns = self._ComputeIssuesListColumns(hotlist.default_col_spec)
+ if hotlist.is_private:
+ hotlist_privacy = feature_objects_pb2.Hotlist.HotlistPrivacy.Value(
+ 'PRIVATE')
+ else:
+ hotlist_privacy = feature_objects_pb2.Hotlist.HotlistPrivacy.Value(
+ 'PUBLIC')
+
+ return feature_objects_pb2.Hotlist(
+ name=hotlist_resource_name,
+ display_name=hotlist.name,
+ owner=members_by_id.get(hotlist.owner_ids[0]),
+ editors=[
+ members_by_id.get(editor_id) for editor_id in hotlist.editor_ids
+ ],
+ summary=hotlist.summary,
+ description=hotlist.description,
+ default_columns=default_columns,
+ hotlist_privacy=hotlist_privacy)
+
+ def ConvertHotlists(self, hotlists):
+ # type: (Sequence[proto.feature_objects_pb2.Hotlist])
+ # -> Sequence[api_proto.feature_objects_pb2.Hotlist]
+ """Convert protorpc Hotlists into protoc Hotlists."""
+ return [self.ConvertHotlist(hotlist) for hotlist in hotlists]
+
+ def ConvertHotlistItems(self, hotlist_id, items):
+ # type: (int, Sequence[proto.features_pb2.HotlistItem]) ->
+ # Sequence[api_proto.feature_objects_pb2.Hotlist]
+ """Convert a Sequence of protorpc HotlistItems into a Sequence of protoc
+ HotlistItems.
+
+ Args:
+ hotlist_id: ID of the Hotlist the items belong to.
+ items: Sequence of HotlistItem protorpc objects.
+
+ Returns:
+ Sequence of protoc HotlistItems in the same order they are given in
+ `items`.
+ In the rare event that any issues in `items` are not found, they will be
+ omitted from the result.
+ """
+ issue_ids = [item.issue_id for item in items]
+ # Converting HotlistItemNames and IssueNames both require looking up the
+ # issues in the hotlist. However, we want to keep the code clean and
+ # readable so we keep the two processes separate.
+ resource_names_dict = rnc.ConvertHotlistItemNames(
+ self.cnxn, hotlist_id, issue_ids, self.services)
+ issue_names_dict = rnc.ConvertIssueNames(
+ self.cnxn, issue_ids, self.services)
+ adders_by_id = rnc.ConvertUserNames([item.adder_id for item in items])
+
+ # Filter out items whose issues were not found.
+ found_items = [
+ item for item in items if resource_names_dict.get(item.issue_id) and
+ issue_names_dict.get(item.issue_id)
+ ]
+ if len(items) != len(found_items):
+ found_ids = [item.issue_id for item in found_items]
+ missing_ids = [iid for iid in issue_ids if iid not in found_ids]
+ logging.info('HotlistItem issues %r not found' % missing_ids)
+
+ # Generate user friendly ranks (0, 1, 2, 3,...) that are exposed to API
+ # clients, instead of using padded ranks (1, 11, 21, 31,...).
+ sorted_ranks = sorted(item.rank for item in found_items)
+ friendly_ranks_dict = {
+ rank: friendly_rank for friendly_rank, rank in enumerate(sorted_ranks)
+ }
+
+ api_items = []
+ for item in found_items:
+ api_item = feature_objects_pb2.HotlistItem(
+ name=resource_names_dict.get(item.issue_id),
+ issue=issue_names_dict.get(item.issue_id),
+ rank=friendly_ranks_dict[item.rank],
+ adder=adders_by_id.get(item.adder_id),
+ note=item.note)
+ if item.date_added:
+ api_item.create_time.FromSeconds(item.date_added)
+ api_items.append(api_item)
+
+ return api_items
+
+ # Issues
+
+ def _ConvertComponentValues(self, issue):
+ # proto.tracker_pb2.Issue ->
+ # Sequence[api_proto.issue_objects_pb2.Issue.ComponentValue]
+ """Convert the status string on issue into a ComponentValue."""
+ component_values = []
+ component_ids = itertools.chain(
+ issue.component_ids, issue.derived_component_ids)
+ ids_to_names = rnc.ConvertComponentDefNames(
+ self.cnxn, component_ids, issue.project_id, self.services)
+
+ for component_id in issue.component_ids:
+ if component_id in ids_to_names:
+ component_values.append(
+ issue_objects_pb2.Issue.ComponentValue(
+ component=ids_to_names[component_id],
+ derivation=issue_objects_pb2.Derivation.Value(
+ 'EXPLICIT')))
+ for derived_component_id in issue.derived_component_ids:
+ if derived_component_id in ids_to_names:
+ component_values.append(
+ issue_objects_pb2.Issue.ComponentValue(
+ component=ids_to_names[derived_component_id],
+ derivation=issue_objects_pb2.Derivation.Value('RULE')))
+
+ return component_values
+
+ def _ConvertStatusValue(self, issue):
+ # proto.tracker_pb2.Issue -> api_proto.issue_objects_pb2.Issue.StatusValue
+ """Convert the status string on issue into a StatusValue."""
+ derivation = issue_objects_pb2.Derivation.Value(
+ 'DERIVATION_UNSPECIFIED')
+ if issue.status:
+ derivation = issue_objects_pb2.Derivation.Value('EXPLICIT')
+ else:
+ derivation = issue_objects_pb2.Derivation.Value('RULE')
+ return issue_objects_pb2.Issue.StatusValue(
+ status=issue.status or issue.derived_status, derivation=derivation)
+
+ def _ConvertAmendments(self, amendments, user_display_names):
+ # type: (Sequence[proto.tracker_pb2.Amendment], Mapping[int, str]) ->
+ # Sequence[api_proto.issue_objects_pb2.Comment.Amendment]
+ """Convert protorpc Amendments to protoc Amendments.
+
+ Args:
+ amendments: the amendments to convert
+ user_display_names: map from user_id to display name for all users
+ involved in the amendments.
+
+ Returns:
+ The converted amendments.
+ """
+ results = []
+ for amendment in amendments:
+ field_name = tbo.GetAmendmentFieldName(amendment)
+ new_value = tbo.AmendmentString_New(amendment, user_display_names)
+ results.append(
+ issue_objects_pb2.Comment.Amendment(
+ field_name=field_name,
+ new_or_delta_value=new_value,
+ old_value=amendment.oldvalue))
+ return results
+
+ def _ConvertAttachments(self, attachments, project_name):
+ # type: (Sequence[proto.tracker_pb2.Attachment], str) ->
+ # Sequence[api_proto.issue_objects_pb2.Comment.Attachment]
+ """Convert protorpc Attachments to protoc Attachments."""
+ results = []
+ for attach in attachments:
+ if attach.deleted:
+ state = issue_objects_pb2.IssueContentState.Value('DELETED')
+ size, thumbnail_uri, view_uri, download_uri = None, None, None, None
+ else:
+ state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
+ size = attach.filesize
+ download_uri = attachment_helpers.GetDownloadURL(attach.attachment_id)
+ view_uri = attachment_helpers.GetViewURL(
+ attach, download_uri, project_name)
+ thumbnail_uri = attachment_helpers.GetThumbnailURL(attach, download_uri)
+ results.append(
+ issue_objects_pb2.Comment.Attachment(
+ filename=attach.filename,
+ state=state,
+ size=size,
+ media_type=attach.mimetype,
+ thumbnail_uri=thumbnail_uri,
+ view_uri=view_uri,
+ download_uri=download_uri))
+ return results
+
+ def ConvertComments(self, issue_id, comments):
+ # type: (int, Sequence[proto.tracker_pb2.IssueComment])
+ # -> Sequence[api_proto.issue_objects_pb2.Comment]
+ """Convert protorpc IssueComments from issue into protoc Comments."""
+ issue = self.services.issue.GetIssue(self.cnxn, issue_id)
+ users_by_id = self.services.user.GetUsersByIDs(
+ self.cnxn, tbo.UsersInvolvedInCommentList(comments))
+ (user_display_names,
+ _user_display_emails) = framework_bizobj.CreateUserDisplayNamesAndEmails(
+ self.cnxn, self.services, self.user_auth, users_by_id.values())
+ comment_names_dict = rnc.CreateCommentNames(
+ issue.local_id, issue.project_name,
+ [comment.sequence for comment in comments])
+ approval_ids = [
+ comment.approval_id
+ for comment in comments
+ if comment.approval_id is not None # In case of a 0 approval_id.
+ ]
+ approval_ids_to_names = rnc.ConvertApprovalDefNames(
+ self.cnxn, approval_ids, issue.project_id, self.services)
+
+ converted_comments = []
+ for comment in comments:
+ if comment.is_spam:
+ state = issue_objects_pb2.IssueContentState.Value('SPAM')
+ elif comment.deleted_by:
+ state = issue_objects_pb2.IssueContentState.Value('DELETED')
+ else:
+ state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
+ comment_type = issue_objects_pb2.Comment.Type.Value('COMMENT')
+ if comment.is_description:
+ comment_type = issue_objects_pb2.Comment.Type.Value('DESCRIPTION')
+ converted_attachments = self._ConvertAttachments(
+ comment.attachments, issue.project_name)
+ converted_amendments = self._ConvertAmendments(
+ comment.amendments, user_display_names)
+ converted_comment = issue_objects_pb2.Comment(
+ name=comment_names_dict[comment.sequence],
+ state=state,
+ type=comment_type,
+ create_time=timestamp_pb2.Timestamp(seconds=comment.timestamp),
+ attachments=converted_attachments,
+ amendments=converted_amendments)
+ if comment.content:
+ converted_comment.content = comment.content
+ if comment.user_id:
+ converted_comment.commenter = rnc.ConvertUserName(comment.user_id)
+ if comment.inbound_message:
+ converted_comment.inbound_message = comment.inbound_message
+ if comment.approval_id and comment.approval_id in approval_ids_to_names:
+ converted_comment.approval = approval_ids_to_names[comment.approval_id]
+ converted_comments.append(converted_comment)
+ return converted_comments
+
+ def ConvertIssue(self, issue):
+ # type: (proto.tracker_pb2.Issue) -> api_proto.issue_objects_pb2.Issue
+ """Convert a protorpc Issue into a protoc Issue."""
+ issues = self.ConvertIssues([issue])
+ if len(issues) < 1:
+ raise exceptions.NoSuchIssueException()
+ if len(issues) > 1:
+ logging.warning('More than one converted issue returned: %s', issues)
+ return issues[0]
+
+ def ConvertIssues(self, issues):
+ # type: (Sequence[proto.tracker_pb2.Issue]) ->
+ # Sequence[api_proto.issue_objects_pb2.Issue]
+ """Convert protorpc Issues into protoc Issues."""
+ issue_ids = [issue.issue_id for issue in issues]
+ issue_names_dict = rnc.ConvertIssueNames(
+ self.cnxn, issue_ids, self.services)
+ found_issues = [
+ issue for issue in issues if issue.issue_id in issue_names_dict
+ ]
+ converted_issues = []
+ for issue in found_issues:
+ status = self._ConvertStatusValue(issue)
+ content_state = issue_objects_pb2.IssueContentState.Value(
+ 'STATE_UNSPECIFIED')
+ if issue.is_spam:
+ content_state = issue_objects_pb2.IssueContentState.Value('SPAM')
+ elif issue.deleted:
+ content_state = issue_objects_pb2.IssueContentState.Value('DELETED')
+ else:
+ content_state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
+
+ owner = None
+ # Explicit values override values derived from rules.
+ if issue.owner_id:
+ owner = issue_objects_pb2.Issue.UserValue(
+ derivation=issue_objects_pb2.Derivation.Value('EXPLICIT'),
+ user=rnc.ConvertUserName(issue.owner_id))
+ elif issue.derived_owner_id:
+ owner = issue_objects_pb2.Issue.UserValue(
+ derivation=issue_objects_pb2.Derivation.Value('RULE'),
+ user=rnc.ConvertUserName(issue.derived_owner_id))
+
+ cc_users = []
+ for cc_user_id in issue.cc_ids:
+ cc_users.append(
+ issue_objects_pb2.Issue.UserValue(
+ derivation=issue_objects_pb2.Derivation.Value('EXPLICIT'),
+ user=rnc.ConvertUserName(cc_user_id)))
+ for derived_cc_user_id in issue.derived_cc_ids:
+ cc_users.append(
+ issue_objects_pb2.Issue.UserValue(
+ derivation=issue_objects_pb2.Derivation.Value('RULE'),
+ user=rnc.ConvertUserName(derived_cc_user_id)))
+
+ labels = self.ConvertLabels(
+ issue.labels, issue.derived_labels, issue.project_id)
+ components = self._ConvertComponentValues(issue)
+ non_approval_fvs = self._GetNonApprovalFieldValues(
+ issue.field_values, issue.project_id)
+ field_values = self.ConvertFieldValues(
+ non_approval_fvs, issue.project_id, issue.phases)
+ field_values.extend(
+ self.ConvertEnumFieldValues(
+ issue.labels, issue.derived_labels, issue.project_id))
+ related_issue_ids = (
+ [issue.merged_into] + issue.blocked_on_iids + issue.blocking_iids)
+ issue_names_by_ids = rnc.ConvertIssueNames(
+ self.cnxn, related_issue_ids, self.services)
+ merged_into_issue_ref = None
+ if issue.merged_into and issue.merged_into in issue_names_by_ids:
+ merged_into_issue_ref = issue_objects_pb2.IssueRef(
+ issue=issue_names_by_ids[issue.merged_into])
+ if issue.merged_into_external:
+ merged_into_issue_ref = issue_objects_pb2.IssueRef(
+ ext_identifier=issue.merged_into_external)
+
+ blocked_on_issue_refs = [
+ issue_objects_pb2.IssueRef(issue=issue_names_by_ids[iid])
+ for iid in issue.blocked_on_iids
+ if iid in issue_names_by_ids
+ ]
+ blocked_on_issue_refs.extend(
+ issue_objects_pb2.IssueRef(
+ ext_identifier=blocked_on.ext_issue_identifier)
+ for blocked_on in issue.dangling_blocked_on_refs)
+
+ blocking_issue_refs = [
+ issue_objects_pb2.IssueRef(issue=issue_names_by_ids[iid])
+ for iid in issue.blocking_iids
+ if iid in issue_names_by_ids
+ ]
+ blocking_issue_refs.extend(
+ issue_objects_pb2.IssueRef(
+ ext_identifier=blocking.ext_issue_identifier)
+ for blocking in issue.dangling_blocking_refs)
+ # All other timestamps were set when the issue was created.
+ close_time = None
+ if issue.closed_timestamp:
+ close_time = timestamp_pb2.Timestamp(seconds=issue.closed_timestamp)
+
+ phases = self._ComputePhases(issue.phases)
+
+ result = issue_objects_pb2.Issue(
+ name=issue_names_dict[issue.issue_id],
+ summary=issue.summary,
+ state=content_state,
+ status=status,
+ reporter=rnc.ConvertUserName(issue.reporter_id),
+ owner=owner,
+ cc_users=cc_users,
+ labels=labels,
+ components=components,
+ field_values=field_values,
+ merged_into_issue_ref=merged_into_issue_ref,
+ blocked_on_issue_refs=blocked_on_issue_refs,
+ blocking_issue_refs=blocking_issue_refs,
+ create_time=timestamp_pb2.Timestamp(seconds=issue.opened_timestamp),
+ close_time=close_time,
+ modify_time=timestamp_pb2.Timestamp(seconds=issue.modified_timestamp),
+ component_modify_time=timestamp_pb2.Timestamp(
+ seconds=issue.component_modified_timestamp),
+ status_modify_time=timestamp_pb2.Timestamp(
+ seconds=issue.status_modified_timestamp),
+ owner_modify_time=timestamp_pb2.Timestamp(
+ seconds=issue.owner_modified_timestamp),
+ star_count=issue.star_count,
+ phases=phases)
+ # TODO(crbug.com/monorail/5857): Set attachment_count unconditionally
+ # after the underlying source of negative attachment counts has been
+ # resolved and database has been repaired.
+ if issue.attachment_count >= 0:
+ result.attachment_count = issue.attachment_count
+ converted_issues.append(result)
+ return converted_issues
+
+ def IngestAttachmentUploads(self, attachment_uploads):
+ # type: (Sequence[api_proto.issues_pb2.AttachmentUpload] ->
+ # Sequence[framework_helpers.AttachmentUpload])
+ """Ingests protoc AttachmentUploads into framework_helpers.AttachUploads."""
+ ingested_uploads = []
+ with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
+ for up in attachment_uploads:
+ if not up.filename or not up.content:
+ err_agg.AddErrorMessage(
+ 'Uploaded atachment missing filename or content')
+ mimetype = filecontent.GuessContentTypeFromFilename(up.filename)
+ ingested_uploads.append(
+ framework_helpers.AttachmentUpload(
+ up.filename, up.content, mimetype))
+
+ return ingested_uploads
+
+ def IngestIssueDeltas(self, issue_deltas):
+ # type: (Sequence[api_proto.issues_pb2.IssueDelta]) ->
+ # Sequence[Tuple[int, proto.tracker_pb2.IssueDelta]]
+ """Ingests protoc IssueDeltas, into protorpc IssueDeltas.
+
+ Args:
+ issue_deltas: the protoc IssueDeltas to ingest.
+
+ Returns:
+ A list of (issue_id, tracker_pb2.IssueDelta) tuples that contain
+ values found in issue_deltas, ignoring all OUTPUT_ONLY and masked
+ fields.
+
+ Raises:
+ InputException: if any fields in the approval_deltas were invalid.
+ NoSuchProjectException: if any parent projects are not found.
+ NoSuchIssueException: if any issues are not found.
+ NoSuchComponentException: if any components are not found.
+ """
+ issue_names = [delta.issue.name for delta in issue_deltas]
+ issue_ids = rnc.IngestIssueNames(self.cnxn, issue_names, self.services)
+ issues_dict, misses = self.services.issue.GetIssuesDict(
+ self.cnxn, issue_ids)
+ if misses:
+ logging.info(
+ 'Issues not found for supposedly valid issue_ids: %r' % misses)
+ raise ValueError('Could not fetch some issues.')
+ configs_by_pid = self.services.config.GetProjectConfigs(
+ self.cnxn, {issue.project_id for issue in issues_dict.values()})
+
+ with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
+ for api_delta in issue_deltas:
+ if not api_delta.HasField('update_mask'):
+ err_agg.AddErrorMessage(
+ '`update_mask` must be set for {} delta.', api_delta.issue.name)
+ elif not api_delta.update_mask.IsValidForDescriptor(
+ issue_objects_pb2.Issue.DESCRIPTOR):
+ err_agg.AddErrorMessage(
+ 'Invalid `update_mask` for {} delta.', api_delta.issue.name)
+
+ ingested = []
+ for iid, api_delta in zip(issue_ids, issue_deltas):
+ delta = tracker_pb2.IssueDelta()
+
+ # Check non-repeated fields before MergeMessage because in an object
+ # where fields are not set and with a FieldMask applied, there is no
+ # way to tell if empty fields were explicitly listed or not listed
+ # in the FieldMask.
+ paths_set = set(api_delta.update_mask.paths)
+ if (not paths_set.isdisjoint({'status', 'status.status'}) and
+ api_delta.issue.status.status):
+ delta.status = api_delta.issue.status.status
+ elif 'status.status' in paths_set and not api_delta.issue.status.status:
+ delta.status = ''
+
+ if (not paths_set.isdisjoint({'owner', 'owner.user'}) and
+ api_delta.issue.owner.user):
+ delta.owner_id = rnc.IngestUserName(
+ self.cnxn, api_delta.issue.owner.user, self.services)
+ elif 'owner.user' in paths_set and not api_delta.issue.owner.user:
+ delta.owner_id = framework_constants.NO_USER_SPECIFIED
+
+ if 'summary' in paths_set:
+ if api_delta.issue.summary:
+ delta.summary = api_delta.issue.summary
+ else:
+ delta.summary = ''
+
+ merge_ref = api_delta.issue.merged_into_issue_ref
+ if 'merged_into_issue_ref' in paths_set:
+ if (api_delta.issue.merged_into_issue_ref.issue or
+ api_delta.issue.merged_into_issue_ref.ext_identifier):
+ ingested_ref = self._IngestIssueRef(merge_ref)
+ if isinstance(ingested_ref, tracker_pb2.DanglingIssueRef):
+ delta.merged_into_external = ingested_ref.ext_issue_identifier
+ else:
+ delta.merged_into = ingested_ref
+ elif 'merged_into_issue_ref.issue' in paths_set:
+ if api_delta.issue.merged_into_issue_ref.issue:
+ delta.merged_into = self._IngestIssueRef(merge_ref)
+ else:
+ delta.merged_into = 0
+ elif 'merged_into_issue_ref.ext_identifier' in paths_set:
+ if api_delta.issue.merged_into_issue_ref.ext_identifier:
+ ingested_ref = self._IngestIssueRef(merge_ref)
+ delta.merged_into_external = ingested_ref.ext_issue_identifier
+ else:
+ delta.merged_into_external = ''
+
+ filtered_api_issue = issue_objects_pb2.Issue()
+ api_delta.update_mask.MergeMessage(
+ api_delta.issue,
+ filtered_api_issue,
+ replace_message_field=True,
+ replace_repeated_field=True)
+
+ cc_names = [name for name in api_delta.ccs_remove] + [
+ user_value.user for user_value in filtered_api_issue.cc_users
+ ]
+ cc_ids = rnc.IngestUserNames(self.cnxn, cc_names, self.services)
+ delta.cc_ids_remove = cc_ids[:len(api_delta.ccs_remove)]
+ delta.cc_ids_add = cc_ids[len(api_delta.ccs_remove):]
+
+ comp_names = [component for component in api_delta.components_remove] + [
+ c_value.component for c_value in filtered_api_issue.components
+ ]
+ project_comp_ids = rnc.IngestComponentDefNames(
+ self.cnxn, comp_names, self.services)
+ comp_ids = [comp_id for (_pid, comp_id) in project_comp_ids]
+ delta.comp_ids_remove = comp_ids[:len(api_delta.components_remove)]
+ delta.comp_ids_add = comp_ids[len(api_delta.components_remove):]
+
+ # Added to delta below, after ShiftEnumFieldsIntoLabels.
+ labels_add = [value.label for value in filtered_api_issue.labels]
+ labels_remove = [label for label in api_delta.labels_remove]
+
+ config = configs_by_pid[issues_dict[iid].project_id]
+ fvs_add, add_enums = self._IngestFieldValues(
+ filtered_api_issue.field_values, config)
+ fvs_remove, remove_enums = self._IngestFieldValues(
+ api_delta.field_vals_remove, config)
+ field_helpers.ShiftEnumFieldsIntoLabels(
+ labels_add, labels_remove, add_enums, remove_enums, config)
+ delta.field_vals_add = fvs_add
+ delta.field_vals_remove = fvs_remove
+ delta.labels_add = labels_add
+ delta.labels_remove = labels_remove
+ assert len(add_enums) == 0 # ShiftEnumFieldsIntoLabels clears all enums.
+ assert len(remove_enums) == 0
+
+ blocked_on_iids_rm, blocked_on_dangling_rm = self._IngestIssueRefs(
+ api_delta.blocked_on_issues_remove)
+ delta.blocked_on_remove = blocked_on_iids_rm
+ delta.ext_blocked_on_remove = [
+ ref.ext_issue_identifier for ref in blocked_on_dangling_rm
+ ]
+
+ blocked_on_iids_add, blocked_on_dangling_add = self._IngestIssueRefs(
+ filtered_api_issue.blocked_on_issue_refs)
+ delta.blocked_on_add = blocked_on_iids_add
+ delta.ext_blocked_on_add = [
+ ref.ext_issue_identifier for ref in blocked_on_dangling_add
+ ]
+
+ blocking_iids_rm, blocking_dangling_rm = self._IngestIssueRefs(
+ api_delta.blocking_issues_remove)
+ delta.blocking_remove = blocking_iids_rm
+ delta.ext_blocking_remove = [
+ ref.ext_issue_identifier for ref in blocking_dangling_rm
+ ]
+
+ blocking_iids_add, blocking_dangling_add = self._IngestIssueRefs(
+ filtered_api_issue.blocking_issue_refs)
+ delta.blocking_add = blocking_iids_add
+ delta.ext_blocking_add = [
+ ref.ext_issue_identifier for ref in blocking_dangling_add
+ ]
+
+ ingested.append((iid, delta))
+
+ return ingested
+
+ def IngestApprovalDeltas(self, approval_deltas, setter_id):
+ # type: (Sequence[api_proto.issues_pb2.ApprovalDelta], int) ->
+ # Sequence[Tuple[int, int, proto.tracker_pb2.ApprovalDelta]]
+ """Ingests protoc ApprovalDeltas into protorpc ApprovalDeltas.
+
+ Args:
+ approval_deltas: the protoc ApprovalDeltas to ingest.
+ setter_id: The ID for the user setting the deltas.
+
+ Returns:
+ Sequence of (issue_id, approval_id, ApprovalDelta) tuples in the order
+ provided. The ApprovalDeltas ignore all OUTPUT_ONLY and masked fields.
+ The tuples are "delta_specifications;" they identify one requested change.
+
+ Raises:
+ InputException: if any fields in the approval_delta protos were invalid.
+ NoSuchProjectException: if the parent project of any ApprovalValue isn't
+ found.
+ NoSuchIssueException: if the issue of any ApprovalValue isn't found.
+ NoSuchUserException: if any user value was provided with an invalid email.
+ Note that users specified by ID are not checked for existence.
+ """
+ delta_specifications = []
+ set_on = int(time.time()) # Use the same timestamp for all deltas.
+ for approval_delta in approval_deltas:
+ approval_name = approval_delta.approval_value.name
+ # TODO(crbug/monorail/8173): Aggregate errors.
+ project_id, issue_id, approval_id = rnc.IngestApprovalValueName(
+ self.cnxn, approval_name, self.services)
+
+ if not approval_delta.HasField('update_mask'):
+ raise exceptions.InputException(
+ '`update_mask` must be set for %s delta.' % approval_name)
+ elif not approval_delta.update_mask.IsValidForDescriptor(
+ issue_objects_pb2.ApprovalValue.DESCRIPTOR):
+ raise exceptions.InputException(
+ 'Invalid `update_mask` for %s delta.' % approval_name)
+ filtered_value = issue_objects_pb2.ApprovalValue()
+ approval_delta.update_mask.MergeMessage(
+ approval_delta.approval_value,
+ filtered_value,
+ replace_message_field=True,
+ replace_repeated_field=True)
+ status = _APPROVAL_STATUS_INGEST[filtered_value.status]
+ # Approvers
+ # No autocreate.
+ # A user may try to remove all existing approvers [a, b] and add another
+ # approver [c]. If they mis-type `c` and we auto-create `c` instead of
+ # raising error, this would cause the ApprovalValue to be editable by no
+ # one but site admins.
+ approver_ids_add = rnc.IngestUserNames(
+ self.cnxn, filtered_value.approvers, self.services, autocreate=False)
+ approver_ids_remove = rnc.IngestUserNames(
+ self.cnxn,
+ approval_delta.approvers_remove,
+ self.services,
+ autocreate=False)
+
+ # Field Values.
+ config = self.services.config.GetProjectConfig(self.cnxn, project_id)
+ approval_fds_by_id = {
+ fd.field_id: fd
+ for fd in config.field_defs
+ if fd.field_type is tracker_pb2.FieldTypes.APPROVAL_TYPE
+ }
+ if approval_id not in approval_fds_by_id:
+ raise exceptions.InputException(
+ 'Approval not found in project for %s' % approval_name)
+
+ sub_fvs_add, add_enums = self._IngestFieldValues(
+ filtered_value.field_values, config, approval_id_filter=approval_id)
+ sub_fvs_remove, remove_enums = self._IngestFieldValues(
+ approval_delta.field_vals_remove,
+ config,
+ approval_id_filter=approval_id)
+ labels_add = []
+ labels_remove = []
+ field_helpers.ShiftEnumFieldsIntoLabels(
+ labels_add, labels_remove, add_enums, remove_enums, config)
+ assert len(add_enums) == 0 # ShiftEnumFieldsIntoLabels clears all enums.
+ assert len(remove_enums) == 0
+ delta = tbo.MakeApprovalDelta(
+ status,
+ setter_id,
+ approver_ids_add,
+ approver_ids_remove,
+ sub_fvs_add,
+ sub_fvs_remove, [],
+ labels_add,
+ labels_remove,
+ set_on=set_on)
+ delta_specifications.append((issue_id, approval_id, delta))
+ return delta_specifications
+
+ def IngestIssue(self, issue, project_id):
+ # type: (api_proto.issue_objects_pb2.Issue, int) -> proto.tracker_pb2.Issue
+ """Ingest a protoc Issue into a protorpc Issue.
+
+ Args:
+ issue: the protoc issue to ingest.
+ project_id: The project into which we're ingesting `issue`.
+
+ Returns:
+ protorpc version of issue, ignoring all OUTPUT_ONLY fields.
+
+ Raises:
+ InputException: if any fields in the 'issue' proto were invalid.
+ NoSuchProjectException: if 'project_id' is not found.
+ """
+ # Get config first. We can't ingest the issue if the project isn't found.
+ config = self.services.config.GetProjectConfig(self.cnxn, project_id)
+ ingestedDict = {
+ 'project_id': project_id,
+ 'summary': issue.summary
+ }
+ with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
+ self._ExtractOwner(issue, ingestedDict, err_agg)
+
+ # Extract ccs.
+ try:
+ ingestedDict['cc_ids'] = rnc.IngestUserNames(
+ self.cnxn, [cc.user for cc in issue.cc_users], self.services,
+ autocreate=True)
+ except exceptions.InputException as e:
+ err_agg.AddErrorMessage('Error ingesting cc_users: {}', e)
+
+ # Extract status.
+ if issue.HasField('status') and issue.status.status:
+ ingestedDict['status'] = issue.status.status
+ else:
+ err_agg.AddErrorMessage('Status is required when creating an issue')
+
+ # Extract components.
+ try:
+ project_comp_ids = rnc.IngestComponentDefNames(
+ self.cnxn, [cv.component for cv in issue.components], self.services)
+ ingestedDict['component_ids'] = [
+ comp_id for (_pid, comp_id) in project_comp_ids]
+ except (exceptions.InputException, exceptions.NoSuchProjectException,
+ exceptions.NoSuchComponentException) as e:
+ err_agg.AddErrorMessage('Error ingesting components: {}', e)
+
+ # Extract labels and field values.
+ ingestedDict['labels'] = [lv.label for lv in issue.labels]
+ try:
+ ingestedDict['field_values'], enums = self._IngestFieldValues(
+ issue.field_values, config)
+ field_helpers.ShiftEnumFieldsIntoLabels(
+ ingestedDict['labels'], [], enums, [], config)
+ assert len(
+ enums) == 0 # ShiftEnumFieldsIntoLabels must clear all enums.
+ except exceptions.InputException as e:
+ err_agg.AddErrorMessage(e.message)
+
+ # Ingest merged, blocking/blocked_on.
+ self._ExtractIssueRefs(issue, ingestedDict, err_agg)
+ return tracker_pb2.Issue(**ingestedDict)
+
+ def _IngestFieldValues(self, field_values, config, approval_id_filter=None):
+ # type: (Sequence[api_proto.issue_objects.FieldValue],
+ # proto.tracker_pb2.ProjectIssueConfig, Optional[int]) ->
+ # Tuple[Sequence[proto.tracker_pb2.FieldValue],
+ # Mapping[int, Sequence[str]]]
+ """Returns protorpc FieldValues for the given protoc FieldValues.
+
+ Raises exceptions if any field could not be parsed for any reasons such as
+ unsupported field type, non-existent field, field from different
+ projects, or fields with mismatched parent approvals.
+
+ Args:
+ field_values: protoc FieldValues to ingest.
+ config: ProjectIssueConfig for the FieldValues we're ingesting.
+ approval_id_filter: an approval_id, including any FieldValues that does
+ not have this approval as a parent will trigger InputException.
+
+ Returns:
+ A pair 1) Ingested FieldValues. 2) A mapping of field ids to values
+ for ENUM_TYPE fields in 'field_values.'
+
+ Raises:
+ InputException: if any fields_values could not be parsed for any reasons
+ such as unsupported field type, non-existent field, or field from
+ different projects.
+ """
+ fds_by_id = {fd.field_id: fd for fd in config.field_defs}
+ enums = {}
+ ingestedFieldValues = []
+ with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
+ for fv in field_values:
+ try:
+ project_id, fd_id = rnc.IngestFieldDefName(
+ self.cnxn, fv.field, self.services)
+ fd = fds_by_id[fd_id]
+ # Raise if field does not belong to approval_id_filter (if provided).
+ if (approval_id_filter is not None and
+ fd.approval_id != approval_id_filter):
+ approval_name = rnc.ConvertApprovalDefNames(
+ self.cnxn, [approval_id_filter], project_id,
+ self.services)[approval_id_filter]
+ err_agg.AddErrorMessage(
+ 'Field {} does not belong to approval {}', fv.field,
+ approval_name)
+ continue
+ if fd.field_type == tracker_pb2.FieldTypes.ENUM_TYPE:
+ enums.setdefault(fd_id, []).append(fv.value)
+ else:
+ ingestedFieldValues.append(self._IngestFieldValue(fv, fd))
+ except (exceptions.InputException, exceptions.NoSuchProjectException,
+ exceptions.NoSuchFieldDefException, ValueError) as e:
+ err_agg.AddErrorMessage(
+ 'Could not ingest value ({}) for FieldDef ({}): {}', fv.value,
+ fv.field, e)
+ except exceptions.NoSuchUserException as e:
+ err_agg.AddErrorMessage(
+ 'User ({}) not found when ingesting user field: {}', fv.value,
+ fv.field)
+ except KeyError as e:
+ err_agg.AddErrorMessage('Field {} is not in this project', fv.field)
+ return ingestedFieldValues, enums
+
+ def _IngestFieldValue(self, field_value, field_def):
+ # type: (api_proto.issue_objects.FieldValue, proto.tracker_pb2.FieldDef) ->
+ # proto.tracker_pb2.FieldValue
+ """Ingest a protoc FieldValue into a protorpc FieldValue.
+
+ Args:
+ field_value: protoc FieldValue to ingest.
+ field_def: protorpc FieldDef associated with 'field_value'.
+ BOOL_TYPE and APPROVAL_TYPE are ignored.
+ Enum values are not allowed. They must be ingested as labels.
+
+ Returns:
+ Ingested protorpc FieldValue.
+
+ Raises:
+ InputException if 'field_def' is USER_TYPE and 'field_value' does not
+ have a valid formatted resource name.
+ NoSuchUserException if specified user in field does not exist.
+ ValueError if 'field_value' could not be parsed for 'field_def'.
+ """
+ assert field_def.field_type != tracker_pb2.FieldTypes.ENUM_TYPE
+ if field_def.field_type == tracker_pb2.FieldTypes.USER_TYPE:
+ return self._ParseOneUserFieldValue(field_value.value, field_def.field_id)
+ fv = field_helpers.ParseOneFieldValue(
+ self.cnxn, self.services.user, field_def, field_value.value)
+ # ParseOneFieldValue currently ignores parsing errors, although it has TODOs
+ # to raise them.
+ if not fv:
+ raise ValueError('Could not parse %s' % field_value.value)
+ return fv
+
+ def _ParseOneUserFieldValue(self, value, field_id):
+ # type: (str, int) -> proto.tracker_pb2.FieldValue
+ """Replacement for the obsolete user parsing in ParseOneFieldValue."""
+ user_id = rnc.IngestUserName(self.cnxn, value, self.services)
+ return tbo.MakeFieldValue(field_id, None, None, user_id, None, None, False)
+
+ def _ExtractOwner(self, issue, ingestedDict, err_agg):
+ # type: (api_proto.issue_objects_pb2.Issue, Dict[str, Any], ErrorAggregator)
+ # -> None
+ """Fills 'owner' into `ingestedDict`, if it can be extracted."""
+ if issue.HasField('owner'):
+ try:
+ # Unlike for cc's, we require owner be an existing user, thus call we
+ # do not autocreate.
+ ingestedDict['owner_id'] = rnc.IngestUserName(
+ self.cnxn, issue.owner.user, self.services, autocreate=False)
+ except exceptions.InputException as e:
+ err_agg.AddErrorMessage(
+ 'Error ingesting owner ({}): {}', issue.owner.user, e)
+ except exceptions.NoSuchUserException as e:
+ err_agg.AddErrorMessage(
+ 'User ({}) not found when ingesting owner', e)
+ else:
+ ingestedDict['owner_id'] = framework_constants.NO_USER_SPECIFIED
+
+ def _ExtractIssueRefs(self, issue, ingestedDict, err_agg):
+ # type: (api_proto.issue_objects_pb2.Issue, Dict[str, Any], ErrorAggregator)
+ # -> None
+ """Fills issue relationships into `ingestedDict` from `issue`."""
+ if issue.HasField('merged_into_issue_ref'):
+ try:
+ merged_into_ref = self._IngestIssueRef(issue.merged_into_issue_ref)
+ if isinstance(merged_into_ref, tracker_pb2.DanglingIssueRef):
+ ingestedDict['merged_into_external'] = (
+ merged_into_ref.ext_issue_identifier)
+ else:
+ ingestedDict['merged_into'] = merged_into_ref
+ except exceptions.InputException as e:
+ err_agg.AddErrorMessage(
+ 'Error ingesting ref {}: {}', issue.merged_into_issue_ref, e)
+ try:
+ iids, dangling_refs = self._IngestIssueRefs(issue.blocked_on_issue_refs)
+ ingestedDict['blocked_on_iids'] = iids
+ ingestedDict['dangling_blocked_on_refs'] = dangling_refs
+ except exceptions.InputException as e:
+ err_agg.AddErrorMessage(e.message)
+ try:
+ iids, dangling_refs = self._IngestIssueRefs(issue.blocking_issue_refs)
+ ingestedDict['blocking_iids'] = iids
+ ingestedDict['dangling_blocking_refs'] = dangling_refs
+ except exceptions.InputException as e:
+ err_agg.AddErrorMessage(e.message)
+
+ def _IngestIssueRefs(self, issue_refs):
+ # type: (api_proto.issue_objects.IssueRf) ->
+ # Tuple[Sequence[int], Sequence[tracker_pb2.DanglingIssueRef]]
+ """Given protoc IssueRefs, returns issue_ids and DanglingIssueRefs."""
+ issue_ids = []
+ external_refs = []
+ with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
+ for ref in issue_refs:
+ try:
+ ingested_ref = self._IngestIssueRef(ref)
+ if isinstance(ingested_ref, tracker_pb2.DanglingIssueRef):
+ external_refs.append(ingested_ref)
+ else:
+ issue_ids.append(ingested_ref)
+ except (exceptions.InputException, exceptions.NoSuchIssueException,
+ exceptions.NoSuchProjectException) as e:
+ err_agg.AddErrorMessage('Error ingesting ref {}: {}', ref, e)
+
+ return issue_ids, external_refs
+
+ def _IngestIssueRef(self, issue_ref):
+ # type: (api_proto.issue_objects.IssueRef) ->
+ # Union[int, tracker_pb2.DanglingIssueRef]
+ """Given a protoc IssueRef, returns an issue id or DanglingIssueRef."""
+ if issue_ref.issue and issue_ref.ext_identifier:
+ raise exceptions.InputException(
+ 'IssueRefs MUST NOT have both `issue` and `ext_identifier`')
+ if issue_ref.issue:
+ return rnc.IngestIssueName(self.cnxn, issue_ref.issue, self.services)
+ if issue_ref.ext_identifier:
+ # TODO(crbug.com/monorail/7208): Handle ingestion/conversion of CodeSite
+ # refs. We may be able to avoid ever needing to ingest them.
+ return tracker_pb2.DanglingIssueRef(
+ ext_issue_identifier=issue_ref.ext_identifier
+ )
+ raise exceptions.InputException(
+ 'IssueRefs MUST have one of `issue` and `ext_identifier`')
+
+ def IngestIssuesListColumns(self, issues_list_columns):
+ # type: (Sequence[proto.issue_objects_pb2.IssuesListColumn] -> str
+ """Ingest a list of protoc IssueListColumns and returns a string."""
+ return ' '.join([col.column for col in issues_list_columns])
+
+ def _ComputeIssuesListColumns(self, columns):
+ # type: (string) -> Sequence[api_proto.issue_objects_pb2.IssuesListColumn]
+ """Convert string representation of columns to protoc IssuesListColumns"""
+ return [
+ issue_objects_pb2.IssuesListColumn(column=col)
+ for col in columns.split()
+ ]
+
+ def IngestNotifyType(self, notify):
+ # type: (issue_pb.NotifyType) -> bool
+ """Ingest a NotifyType to boolean."""
+ if (notify == issues_pb2.NotifyType.Value('NOTIFY_TYPE_UNSPECIFIED') or
+ notify == issues_pb2.NotifyType.Value('EMAIL')):
+ return True
+ elif notify == issues_pb2.NotifyType.Value('NO_NOTIFICATION'):
+ return False
+
+ # Users
+
+ def ConvertUser(self, user):
+ # type: (protorpc.User) -> api_proto.user_objects_pb2.User
+ """Convert a protorpc User into a protoc User.
+
+ Args:
+ user: protorpc User object.
+
+ Returns:
+ The protoc User object.
+ """
+ return self.ConvertUsers([user.user_id])[user.user_id]
+
+
+ # TODO(crbug/monorail/7238): Make this take in a full User object and
+ # return a Sequence, rather than a map, after hotlist users are converted.
+ def ConvertUsers(self, user_ids):
+ # type: (Sequence[int]) -> Map(int, api_proto.user_objects_pb2.User)
+ """Convert list of protorpc Users into list of protoc Users.
+
+ Args:
+ user_ids: List of User IDs.
+
+ Returns:
+ Dict of User IDs to User protos for given user_ids that could be found.
+ """
+ user_ids_to_names = {}
+
+ # Get display names
+ users_by_id = self.services.user.GetUsersByIDs(self.cnxn, user_ids)
+ (display_names_by_id,
+ display_emails_by_id) = framework_bizobj.CreateUserDisplayNamesAndEmails(
+ self.cnxn, self.services, self.user_auth, users_by_id.values())
+
+ for user_id, user in users_by_id.items():
+ name = rnc.ConvertUserNames([user_id]).get(user_id)
+
+ display_name = display_names_by_id.get(user_id)
+ display_email = display_emails_by_id.get(user_id)
+ availability = framework_helpers.GetUserAvailability(user)
+ availability_message, _availability_status = availability
+
+ user_ids_to_names[user_id] = user_objects_pb2.User(
+ name=name,
+ display_name=display_name,
+ email=display_email,
+ availability_message=availability_message)
+
+ return user_ids_to_names
+
+ def ConvertProjectStars(self, user_id, projects):
+ # type: (int, Collection[protorpc.Project]) ->
+ # Collection[api_proto.user_objects_pb2.ProjectStar]
+ """Convert list of protorpc Projects into protoc ProjectStars.
+
+ Args:
+ user_id: The user the ProjectStar is associated with.
+ projects: All starred projects.
+
+ Returns:
+ List of ProjectStar messages.
+ """
+ api_project_stars = []
+ for proj in projects:
+ name = rnc.ConvertProjectStarName(
+ self.cnxn, user_id, proj.project_id, self.services)
+ star = user_objects_pb2.ProjectStar(name=name)
+ api_project_stars.append(star)
+ return api_project_stars
+
+ # Field Defs
+
+ def ConvertFieldDefs(self, field_defs, project_id):
+ # type: (Sequence[proto.tracker_pb2.FieldDef], int) ->
+ # Sequence[api_proto.project_objects_pb2.FieldDef]
+ """Convert sequence of protorpc FieldDefs to protoc FieldDefs.
+
+ Args:
+ field_defs: List of protorpc FieldDefs
+ project_id: ID of the Project that is ancestor to all given
+ `field_defs`.
+
+ Returns:
+ Sequence of protoc FieldDef in the same order they are given in
+ `field_defs`. In the event any field_def or the referenced approval_id
+ in `field_defs` is not found, they will be omitted from the result.
+ """
+ field_ids = [fd.field_id for fd in field_defs]
+ resource_names_dict = rnc.ConvertFieldDefNames(
+ self.cnxn, field_ids, project_id, self.services)
+ parent_approval_ids = [
+ fd.approval_id for fd in field_defs if fd.approval_id is not None
+ ]
+ approval_names_dict = rnc.ConvertApprovalDefNames(
+ self.cnxn, parent_approval_ids, project_id, self.services)
+
+ api_fds = []
+ for fd in field_defs:
+ # Skip over approval fields, they have their separate ApprovalDef
+ if fd.field_type == tracker_pb2.FieldTypes.APPROVAL_TYPE:
+ continue
+ if fd.field_id not in resource_names_dict:
+ continue
+
+ name = resource_names_dict.get(fd.field_id)
+ display_name = fd.field_name
+ docstring = fd.docstring
+ field_type = self._ConvertFieldDefType(fd.field_type)
+ applicable_issue_type = fd.applicable_type
+ admins = rnc.ConvertUserNames(fd.admin_ids).values()
+ editors = rnc.ConvertUserNames(fd.editor_ids).values()
+ traits = self._ComputeFieldDefTraits(fd)
+ approval_parent = approval_names_dict.get(fd.approval_id)
+
+ enum_settings = None
+ if field_type == project_objects_pb2.FieldDef.Type.Value('ENUM'):
+ enum_settings = project_objects_pb2.FieldDef.EnumTypeSettings(
+ choices=self._GetEnumFieldChoices(fd))
+
+ int_settings = None
+ if field_type == project_objects_pb2.FieldDef.Type.Value('INT'):
+ int_settings = project_objects_pb2.FieldDef.IntTypeSettings(
+ min_value=fd.min_value, max_value=fd.max_value)
+
+ str_settings = None
+ if field_type == project_objects_pb2.FieldDef.Type.Value('STR'):
+ str_settings = project_objects_pb2.FieldDef.StrTypeSettings(
+ regex=fd.regex)
+
+ user_settings = None
+ if field_type == project_objects_pb2.FieldDef.Type.Value('USER'):
+ user_settings = project_objects_pb2.FieldDef.UserTypeSettings(
+ role_requirements=self._ConvertRoleRequirements(fd.needs_member),
+ notify_triggers=self._ConvertNotifyTriggers(fd.notify_on),
+ grants_perm=fd.grants_perm,
+ needs_perm=fd.needs_perm)
+
+ date_settings = None
+ if field_type == project_objects_pb2.FieldDef.Type.Value('DATE'):
+ date_settings = project_objects_pb2.FieldDef.DateTypeSettings(
+ date_action=self._ConvertDateAction(fd.date_action))
+
+ api_fd = project_objects_pb2.FieldDef(
+ name=name,
+ display_name=display_name,
+ docstring=docstring,
+ type=field_type,
+ applicable_issue_type=applicable_issue_type,
+ admins=admins,
+ traits=traits,
+ approval_parent=approval_parent,
+ enum_settings=enum_settings,
+ int_settings=int_settings,
+ str_settings=str_settings,
+ user_settings=user_settings,
+ date_settings=date_settings,
+ editors=editors)
+ api_fds.append(api_fd)
+ return api_fds
+
+ def _ConvertDateAction(self, date_action):
+ # type: (proto.tracker_pb2.DateAction) ->
+ # api_proto.project_objects_pb2.FieldDef.DateTypeSettings.DateAction
+ """Convert protorpc DateAction to protoc
+ FieldDef.DateTypeSettings.DateAction"""
+ if date_action == tracker_pb2.DateAction.NO_ACTION:
+ return project_objects_pb2.FieldDef.DateTypeSettings.DateAction.Value(
+ 'NO_ACTION')
+ elif date_action == tracker_pb2.DateAction.PING_OWNER_ONLY:
+ return project_objects_pb2.FieldDef.DateTypeSettings.DateAction.Value(
+ 'NOTIFY_OWNER')
+ elif date_action == tracker_pb2.DateAction.PING_PARTICIPANTS:
+ return project_objects_pb2.FieldDef.DateTypeSettings.DateAction.Value(
+ 'NOTIFY_PARTICIPANTS')
+ else:
+ raise ValueError('Unsupported DateAction Value')
+
+ def _ConvertRoleRequirements(self, needs_member):
+ # type: (bool) ->
+ # api_proto.project_objects_pb2.FieldDef.
+ # UserTypeSettings.RoleRequirements
+ """Convert protorpc RoleRequirements to protoc
+ FieldDef.UserTypeSettings.RoleRequirements"""
+
+ proto_user_settings = project_objects_pb2.FieldDef.UserTypeSettings
+ if needs_member:
+ return proto_user_settings.RoleRequirements.Value('PROJECT_MEMBER')
+ else:
+ return proto_user_settings.RoleRequirements.Value('NO_ROLE_REQUIREMENT')
+
+ def _ConvertNotifyTriggers(self, notify_trigger):
+ # type: (proto.tracker_pb2.NotifyTriggers) ->
+ # api_proto.project_objects_pb2.FieldDef.UserTypeSettings.NotifyTriggers
+ """Convert protorpc NotifyTriggers to protoc
+ FieldDef.UserTypeSettings.NotifyTriggers"""
+ if notify_trigger == tracker_pb2.NotifyTriggers.NEVER:
+ return project_objects_pb2.FieldDef.UserTypeSettings.NotifyTriggers.Value(
+ 'NEVER')
+ elif notify_trigger == tracker_pb2.NotifyTriggers.ANY_COMMENT:
+ return project_objects_pb2.FieldDef.UserTypeSettings.NotifyTriggers.Value(
+ 'ANY_COMMENT')
+ else:
+ raise ValueError('Unsupported NotifyTriggers Value')
+
+ def _ConvertFieldDefType(self, field_type):
+ # type: (proto.tracker_pb2.FieldTypes) ->
+ # api_proto.project_objects_pb2.FieldDef.Type
+ """Convert protorpc FieldType to protoc FieldDef.Type
+
+ Args:
+ field_type: protorpc FieldType
+
+ Returns:
+ Corresponding protoc FieldDef.Type
+
+ Raises:
+ ValueError if input `field_type` has no suitable supported FieldDef.Type,
+ or input `field_type` is not a recognized enum option.
+ """
+ if field_type == tracker_pb2.FieldTypes.ENUM_TYPE:
+ return project_objects_pb2.FieldDef.Type.Value('ENUM')
+ elif field_type == tracker_pb2.FieldTypes.INT_TYPE:
+ return project_objects_pb2.FieldDef.Type.Value('INT')
+ elif field_type == tracker_pb2.FieldTypes.STR_TYPE:
+ return project_objects_pb2.FieldDef.Type.Value('STR')
+ elif field_type == tracker_pb2.FieldTypes.USER_TYPE:
+ return project_objects_pb2.FieldDef.Type.Value('USER')
+ elif field_type == tracker_pb2.FieldTypes.DATE_TYPE:
+ return project_objects_pb2.FieldDef.Type.Value('DATE')
+ elif field_type == tracker_pb2.FieldTypes.URL_TYPE:
+ return project_objects_pb2.FieldDef.Type.Value('URL')
+ else:
+ raise ValueError(
+ 'Unsupported tracker_pb2.FieldType enum. Boolean types '
+ 'are unsupported and approval types are found in ApprovalDefs')
+
+ def _ComputeFieldDefTraits(self, field_def):
+ # type: (proto.tracker_pb2.FieldDef) ->
+ # Sequence[api_proto.project_objects_pb2.FieldDef.Traits]
+ """Compute sequence of FieldDef.Traits for a given protorpc FieldDef."""
+ trait_protos = []
+ if field_def.is_required:
+ trait_protos.append(project_objects_pb2.FieldDef.Traits.Value('REQUIRED'))
+ if field_def.is_niche:
+ trait_protos.append(
+ project_objects_pb2.FieldDef.Traits.Value('DEFAULT_HIDDEN'))
+ if field_def.is_multivalued:
+ trait_protos.append(
+ project_objects_pb2.FieldDef.Traits.Value('MULTIVALUED'))
+ if field_def.is_phase_field:
+ trait_protos.append(project_objects_pb2.FieldDef.Traits.Value('PHASE'))
+ if field_def.is_restricted_field:
+ trait_protos.append(
+ project_objects_pb2.FieldDef.Traits.Value('RESTRICTED'))
+ return trait_protos
+
+ def _GetEnumFieldChoices(self, field_def):
+ # type: (proto.tracker_pb2.FieldDef) ->
+ # Sequence[Choice]
+ """Get sequence of choices for an enum field
+
+ Args:
+ field_def: protorpc FieldDef
+
+ Returns:
+ Sequence of valid Choices for enum field `field_def`.
+
+ Raises:
+ ValueError if input `field_def` is not an enum type field.
+ """
+ if field_def.field_type != tracker_pb2.FieldTypes.ENUM_TYPE:
+ raise ValueError('Cannot get value from label for non-enum-type field')
+
+ config = self.services.config.GetProjectConfig(
+ self.cnxn, field_def.project_id)
+ value_docstr_tuples = tracker_helpers._GetEnumFieldValuesAndDocstrings(
+ field_def, config)
+
+ return [
+ Choice(value=value, docstring=docstring)
+ for value, docstring in value_docstr_tuples
+ ]
+
+ # Field Values
+
+ def _GetNonApprovalFieldValues(self, field_values, project_id):
+ # type: (Sequence[proto.tracker_pb2.FieldValue], int) ->
+ # Sequence[proto.tracker_pb2.FieldValue]
+ """Filter out field values that belong to an approval field."""
+ config = self.services.config.GetProjectConfig(self.cnxn, project_id)
+ approval_fd_ids = set(
+ [fd.field_id for fd in config.field_defs if fd.approval_id])
+
+ return [fv for fv in field_values if fv.field_id not in approval_fd_ids]
+
+ def ConvertFieldValues(self, field_values, project_id, phases):
+ # type: (Sequence[proto.tracker_pb2.FieldValue], int,
+ # Sequence[proto.tracker_pb2.Phase]) ->
+ # Sequence[api_proto.issue_objects_pb2.FieldValue]
+ """Convert sequence of field_values to protoc FieldValues.
+
+ This method does not handle enum_type fields.
+
+ Args:
+ field_values: List of FieldValues
+ project_id: ID of the Project that is ancestor to all given
+ `field_values`.
+ phases: List of Phases
+
+ Returns:
+ Sequence of protoc FieldValues in the same order they are given in
+ `field_values`. In the event any field_values in `field_values` are not
+ found, they will be omitted from the result.
+ """
+ phase_names_by_id = {phase.phase_id: phase.name for phase in phases}
+ field_ids = [fv.field_id for fv in field_values]
+ resource_names_dict = rnc.ConvertFieldDefNames(
+ self.cnxn, field_ids, project_id, self.services)
+
+ api_fvs = []
+ for fv in field_values:
+ if fv.field_id not in resource_names_dict:
+ continue
+
+ name = resource_names_dict.get(fv.field_id)
+ value = self._ComputeFieldValueString(fv)
+ derivation = self._ComputeFieldValueDerivation(fv)
+ phase = phase_names_by_id.get(fv.phase_id)
+ api_item = issue_objects_pb2.FieldValue(
+ field=name, value=value, derivation=derivation, phase=phase)
+ api_fvs.append(api_item)
+
+ return api_fvs
+
+ def _ComputeFieldValueString(self, field_value):
+ # type: (proto.tracker_pb2.FieldValue) -> str
+ """Convert a FieldValue's value to a string."""
+ if field_value is None:
+ raise exceptions.InputException('No FieldValue specified')
+ elif field_value.int_value is not None:
+ return str(field_value.int_value)
+ elif field_value.str_value is not None:
+ return field_value.str_value
+ elif field_value.user_id is not None:
+ return rnc.ConvertUserNames([field_value.user_id
+ ]).get(field_value.user_id)
+ elif field_value.date_value is not None:
+ return str(field_value.date_value)
+ elif field_value.url_value is not None:
+ return field_value.url_value
+ else:
+ raise exceptions.InputException('FieldValue must have at least one value')
+
+ def _ComputeFieldValueDerivation(self, field_value):
+ # type: (proto.tracker_pb2.FieldValue) ->
+ # api_proto.issue_objects_pb2.Issue.Derivation
+ """Convert a FieldValue's 'derived' to a protoc Issue.Derivation.
+
+ Args:
+ field_value: protorpc FieldValue
+
+ Returns:
+ Issue.Derivation of given `field_value`
+ """
+ if field_value.derived:
+ return issue_objects_pb2.Derivation.Value('RULE')
+ else:
+ return issue_objects_pb2.Derivation.Value('EXPLICIT')
+
+ # Approval Def
+
+ def ConvertApprovalDefs(self, approval_defs, project_id):
+ # type: (Sequence[proto.tracker_pb2.ApprovalDef], int) ->
+ # Sequence[api_proto.project_objects_pb2.ApprovalDef]
+ """Convert sequence of protorpc ApprovalDefs to protoc ApprovalDefs.
+
+ Args:
+ approval_defs: List of protorpc ApprovalDefs
+ project_id: ID of the Project the approval_defs belong to.
+
+ Returns:
+ Sequence of protoc ApprovalDefs in the same order they are given in
+ in `approval_defs`. In the event any approval_def in `approval_defs`
+ are not found, they will be omitted from the result.
+ """
+ approval_ids = set([ad.approval_id for ad in approval_defs])
+ resource_names_dict = rnc.ConvertApprovalDefNames(
+ self.cnxn, approval_ids, project_id, self.services)
+
+ # Get matching field defs, needed to fill out protoc ApprovalDefs
+ config = self.services.config.GetProjectConfig(self.cnxn, project_id)
+ fd_by_id = {}
+ for fd in config.field_defs:
+ if (fd.field_type == tracker_pb2.FieldTypes.APPROVAL_TYPE and
+ fd.field_id in approval_ids):
+ fd_by_id[fd.field_id] = fd
+
+ all_users = tbo.UsersInvolvedInApprovalDefs(
+ approval_defs, fd_by_id.values())
+ user_resource_names_dict = rnc.ConvertUserNames(all_users)
+
+ api_ads = []
+ for ad in approval_defs:
+ if (ad.approval_id not in resource_names_dict or
+ ad.approval_id not in fd_by_id):
+ continue
+ matching_fd = fd_by_id.get(ad.approval_id)
+ name = resource_names_dict.get(ad.approval_id)
+ display_name = matching_fd.field_name
+ docstring = matching_fd.docstring
+ survey = ad.survey
+ approvers = [
+ user_resource_names_dict.get(approver_id)
+ for approver_id in ad.approver_ids
+ ]
+ admins = [
+ user_resource_names_dict.get(admin_id)
+ for admin_id in matching_fd.admin_ids
+ ]
+
+ api_ad = project_objects_pb2.ApprovalDef(
+ name=name,
+ display_name=display_name,
+ docstring=docstring,
+ survey=survey,
+ approvers=approvers,
+ admins=admins)
+ api_ads.append(api_ad)
+ return api_ads
+
+ def ConvertApprovalValues(self, approval_values, field_values, phases,
+ issue_id=None, project_id=None):
+ # type: (Sequence[proto.tracker_pb2.ApprovalValue],
+ # Sequence[proto.tracker_pb2.FieldValue],
+ # Sequence[proto.tracker_pb2.Phase], Optional[int], Optional[int]) ->
+ # Sequence[api_proto.issue_objects_pb2.ApprovalValue]
+ """Convert sequence of approval_values to protoc ApprovalValues.
+
+ `approval_values` may belong to a template or an issue. If they belong to a
+ template, `project_id` should be given for the project the template is in.
+ If these are issue `approval_values` `issue_id` should be given`.
+ So, one of `issue_id` or `project_id` must be provided.
+ If both are given, we ignore `project_id` and assume the `approval_values`
+ belong to an issue.
+
+ Args:
+ approval_values: List of ApprovalValues.
+ field_values: List of FieldValues that may belong to the approval_values.
+ phases: List of Phases that may be associated with the approval_values.
+ issue_id: ID of the Issue that the `approval_values` belong to.
+ project_id: ID of the Project that the `approval_values`
+ template belongs to.
+
+ Returns:
+ Sequence of protoc ApprovalValues in the same order they are given in
+ in `approval_values`. In the event any approval definitions in
+ `approval_values` are not found, they will be omitted from the result.
+
+ Raises:
+ InputException if neither `issue_id` nor `project_id` is given.
+ """
+
+ approval_ids = [av.approval_id for av in approval_values]
+ resource_names_dict = {}
+ if issue_id is not None:
+ # Only issue approval_values have resource names.
+ resource_names_dict = rnc.ConvertApprovalValueNames(
+ self.cnxn, issue_id, self.services)
+ project_id = self.services.issue.GetIssue(self.cnxn, issue_id).project_id
+ elif project_id is None:
+ raise exceptions.InputException(
+ 'One `issue_id` or `project_id` must be given.')
+
+ phase_names_by_id = {phase.phase_id: phase.name for phase in phases}
+ ad_names_dict = rnc.ConvertApprovalDefNames(
+ self.cnxn, approval_ids, project_id, self.services)
+
+ # Organize the field values by the approval values they are
+ # associated with.
+ config = self.services.config.GetProjectConfig(self.cnxn, project_id)
+ fds_by_id = {fd.field_id: fd for fd in config.field_defs}
+ fvs_by_parent_approvals = collections.defaultdict(list)
+ for fv in field_values:
+ fd = fds_by_id.get(fv.field_id)
+ if fd and fd.approval_id:
+ fvs_by_parent_approvals[fd.approval_id].append(fv)
+
+ api_avs = []
+ for av in approval_values:
+ # We only skip missing approval names if we are converting issue approval
+ # values.
+ if issue_id is not None and av.approval_id not in resource_names_dict:
+ continue
+
+ name = resource_names_dict.get(av.approval_id)
+ approval_def = ad_names_dict.get(av.approval_id)
+ approvers = rnc.ConvertUserNames(av.approver_ids).values()
+ status = self._ComputeApprovalValueStatus(av.status)
+ setter = rnc.ConvertUserName(av.setter_id)
+ phase = phase_names_by_id.get(av.phase_id)
+
+ field_values = self.ConvertFieldValues(
+ fvs_by_parent_approvals[av.approval_id], project_id, phases)
+
+ api_item = issue_objects_pb2.ApprovalValue(
+ name=name,
+ approval_def=approval_def,
+ approvers=approvers,
+ status=status,
+ setter=setter,
+ field_values=field_values,
+ phase=phase)
+ if av.set_on:
+ api_item.set_time.FromSeconds(av.set_on)
+ api_avs.append(api_item)
+
+ return api_avs
+
+ def _ComputeApprovalValueStatus(self, status):
+ # type: (proto.tracker_pb2.ApprovalStatus) ->
+ # api_proto.issue_objects_pb2.Issue.ApprovalStatus
+ """Convert a protorpc ApprovalStatus to a protoc Issue.ApprovalStatus."""
+ try:
+ return _APPROVAL_STATUS_CONVERT[status]
+ except KeyError:
+ raise ValueError('Unrecognized tracker_pb2.ApprovalStatus enum')
+
+ # Projects
+
+ def ConvertIssueTemplates(self, project_id, templates):
+ # type: (int, Sequence[proto.tracker_pb2.TemplateDef]) ->
+ # Sequence[api_proto.project_objects_pb2.IssueTemplate]
+ """Convert a Sequence of TemplateDefs to protoc IssueTemplates.
+
+ Args:
+ project_id: ID of the Project the templates belong to.
+ templates: Sequence of TemplateDef protorpc objects.
+
+ Returns:
+ Sequence of protoc IssueTemplate in the same order they are given in
+ `templates`. In the rare event that any templates are not found,
+ they will be omitted from the result.
+ """
+ api_templates = []
+
+ resource_names_dict = rnc.ConvertTemplateNames(
+ self.cnxn, project_id, [template.template_id for template in templates],
+ self.services)
+
+ for template in templates:
+ if template.template_id not in resource_names_dict:
+ continue
+ name = resource_names_dict.get(template.template_id)
+ summary_must_be_edited = template.summary_must_be_edited
+ template_privacy = self._ComputeTemplatePrivacy(template)
+ default_owner = self._ComputeTemplateDefaultOwner(template)
+ component_required = template.component_required
+ admins = rnc.ConvertUserNames(template.admin_ids).values()
+ issue = self._FillIssueFromTemplate(template, project_id)
+ approval_values = self.ConvertApprovalValues(
+ template.approval_values, template.field_values, template.phases,
+ project_id=project_id)
+ api_templates.append(
+ project_objects_pb2.IssueTemplate(
+ name=name,
+ display_name=template.name,
+ issue=issue,
+ approval_values=approval_values,
+ summary_must_be_edited=summary_must_be_edited,
+ template_privacy=template_privacy,
+ default_owner=default_owner,
+ component_required=component_required,
+ admins=admins))
+
+ return api_templates
+
+ def _FillIssueFromTemplate(self, template, project_id):
+ # type: (proto.tracker_pb2.TemplateDef, int) ->
+ # api_proto.issue_objects_pb2.Issue
+ """Convert a TemplateDef to its embedded protoc Issue.
+
+ IssueTemplate does not set the following fields:
+ name
+ reporter
+ cc_users
+ blocked_on_issue_refs
+ blocking_issue_refs
+ create_time
+ close_time
+ modify_time
+ component_modify_time
+ status_modify_time
+ owner_modify_time
+ attachment_count
+ star_count
+
+ Args:
+ template: TemplateDef protorpc objects.
+ project_id: ID of the Project the template belongs to.
+
+ Returns:
+ protoc Issue filled with data from given `template`.
+ """
+ summary = template.summary
+ state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
+ status = issue_objects_pb2.Issue.StatusValue(
+ status=template.status,
+ derivation=issue_objects_pb2.Derivation.Value('EXPLICIT'))
+ owner = None
+ if template.owner_id is not None:
+ owner = issue_objects_pb2.Issue.UserValue(
+ user=rnc.ConvertUserNames([template.owner_id]).get(template.owner_id))
+ labels = self.ConvertLabels(template.labels, [], project_id)
+ components_dict = rnc.ConvertComponentDefNames(
+ self.cnxn, template.component_ids, project_id, self.services)
+ components = []
+ for component_resource_name in components_dict.values():
+ components.append(
+ issue_objects_pb2.Issue.ComponentValue(
+ component=component_resource_name,
+ derivation=issue_objects_pb2.Derivation.Value('EXPLICIT')))
+ non_approval_fvs = self._GetNonApprovalFieldValues(
+ template.field_values, project_id)
+ field_values = self.ConvertFieldValues(
+ non_approval_fvs, project_id, template.phases)
+ field_values.extend(
+ self.ConvertEnumFieldValues(template.labels, [], project_id))
+ phases = self._ComputePhases(template.phases)
+
+ filled_issue = issue_objects_pb2.Issue(
+ summary=summary,
+ state=state,
+ status=status,
+ owner=owner,
+ labels=labels,
+ components=components,
+ field_values=field_values,
+ phases=phases)
+ return filled_issue
+
+ def _ComputeTemplatePrivacy(self, template):
+ # type: (proto.tracker_pb2.TemplateDef) ->
+ # api_proto.project_objects_pb2.IssueTemplate.TemplatePrivacy
+ """Convert a protorpc TemplateDef to its protoc TemplatePrivacy."""
+ if template.members_only:
+ return project_objects_pb2.IssueTemplate.TemplatePrivacy.Value(
+ 'MEMBERS_ONLY')
+ else:
+ return project_objects_pb2.IssueTemplate.TemplatePrivacy.Value('PUBLIC')
+
+ def _ComputeTemplateDefaultOwner(self, template):
+ # type: (proto.tracker_pb2.TemplateDef) ->
+ # api_proto.project_objects_pb2.IssueTemplate.DefaultOwner
+ """Convert a protorpc TemplateDef to its protoc DefaultOwner."""
+ if template.owner_defaults_to_member:
+ return project_objects_pb2.IssueTemplate.DefaultOwner.Value(
+ 'PROJECT_MEMBER_REPORTER')
+ else:
+ return project_objects_pb2.IssueTemplate.DefaultOwner.Value(
+ 'DEFAULT_OWNER_UNSPECIFIED')
+
+ def _ComputePhases(self, phases):
+ # type: (proto.tracker_pb2.TemplateDef) -> Sequence[str]
+ """Convert a protorpc TemplateDef to its sorted string phases."""
+ sorted_phases = sorted(phases, key=lambda phase: phase.rank)
+ return [phase.name for phase in sorted_phases]
+
+ def ConvertLabels(self, labels, derived_labels, project_id):
+ # type: (Sequence[str], Sequence[str], int) ->
+ # Sequence[api_proto.issue_objects_pb2.Issue.LabelValue]
+ """Convert string labels to LabelValues for non-enum-field labels
+
+ Args:
+ labels: Sequence of string labels
+ project_id: ID of the Project these labels belong to.
+
+ Return:
+ Sequence of protoc IssueValues for given `labels` that
+ do not represent enum field values.
+ """
+ config = self.services.config.GetProjectConfig(self.cnxn, project_id)
+ non_fd_labels, non_fd_der_labels = tbo.ExplicitAndDerivedNonMaskedLabels(
+ labels, derived_labels, config)
+ api_labels = []
+ for label in non_fd_labels:
+ api_labels.append(
+ issue_objects_pb2.Issue.LabelValue(
+ label=label,
+ derivation=issue_objects_pb2.Derivation.Value('EXPLICIT')))
+ for label in non_fd_der_labels:
+ api_labels.append(
+ issue_objects_pb2.Issue.LabelValue(
+ label=label,
+ derivation=issue_objects_pb2.Derivation.Value('RULE')))
+ return api_labels
+
+ def ConvertEnumFieldValues(self, labels, derived_labels, project_id):
+ # type: (Sequence[str], Sequence[str], int) ->
+ # Sequence[api_proto.issue_objects_pb2.FieldValue]
+ """Convert string labels to FieldValues for enum-field labels
+
+ Args:
+ labels: Sequence of string labels
+ project_id: ID of the Project these labels belong to.
+
+ Return:
+ Sequence of protoc FieldValues only for given `labels` that
+ represent enum field values.
+ """
+ config = self.services.config.GetProjectConfig(self.cnxn, project_id)
+ enum_ids_by_name = {
+ fd.field_name.lower(): fd.field_id
+ for fd in config.field_defs
+ if fd.field_type is tracker_pb2.FieldTypes.ENUM_TYPE
+ and not fd.is_deleted
+ }
+ resource_names_dict = rnc.ConvertFieldDefNames(
+ self.cnxn, enum_ids_by_name.values(), project_id, self.services)
+
+ api_fvs = []
+
+ labels_by_prefix = tbo.LabelsByPrefix(labels, enum_ids_by_name.keys())
+ for lower_field_name, values in labels_by_prefix.items():
+ field_id = enum_ids_by_name.get(lower_field_name)
+ resource_name = resource_names_dict.get(field_id)
+ if not resource_name:
+ continue
+ api_fvs.extend(
+ [
+ issue_objects_pb2.FieldValue(
+ field=resource_name,
+ value=value,
+ derivation=issue_objects_pb2.Derivation.Value(
+ 'EXPLICIT')) for value in values
+ ])
+
+ der_labels_by_prefix = tbo.LabelsByPrefix(
+ derived_labels, enum_ids_by_name.keys())
+ for lower_field_name, values in der_labels_by_prefix.items():
+ field_id = enum_ids_by_name.get(lower_field_name)
+ resource_name = resource_names_dict.get(field_id)
+ if not resource_name:
+ continue
+ api_fvs.extend(
+ [
+ issue_objects_pb2.FieldValue(
+ field=resource_name,
+ value=value,
+ derivation=issue_objects_pb2.Derivation.Value('RULE'))
+ for value in values
+ ])
+
+ return api_fvs
+
+ def ConvertProject(self, project):
+ # type: (proto.project_pb2.Project) ->
+ # api_proto.project_objects_pb2.Project
+ """Convert a protorpc Project to its protoc Project."""
+
+ return project_objects_pb2.Project(
+ name=rnc.ConvertProjectName(
+ self.cnxn, project.project_id, self.services),
+ display_name=project.project_name,
+ summary=project.summary,
+ thumbnail_url=project_helpers.GetThumbnailUrl(project.logo_gcs_id))
+
+ def ConvertProjects(self, projects):
+ # type: (Sequence[proto.project_pb2.Project]) ->
+ # Sequence[api_proto.project_objects_pb2.Project]
+ """Convert a Sequence of protorpc Projects to protoc Projects."""
+ return [self.ConvertProject(proj) for proj in projects]
+
+ def ConvertProjectConfig(self, project_config):
+ # type: (proto.tracker_pb2.ProjectIssueConfig) ->
+ # api_proto.project_objects_pb2.ProjectConfig
+ """Convert protorpc ProjectIssueConfig to protoc ProjectConfig."""
+ project = self.services.project.GetProject(
+ self.cnxn, project_config.project_id)
+ project_grid_config = project_objects_pb2.ProjectConfig.GridViewConfig(
+ default_x_attr=project_config.default_x_attr,
+ default_y_attr=project_config.default_y_attr)
+ template_names = rnc.ConvertTemplateNames(
+ self.cnxn, project_config.project_id, [
+ project_config.default_template_for_developers,
+ project_config.default_template_for_users
+ ], self.services)
+ return project_objects_pb2.ProjectConfig(
+ name=rnc.ConvertProjectConfigName(
+ self.cnxn, project_config.project_id, self.services),
+ exclusive_label_prefixes=project_config.exclusive_label_prefixes,
+ member_default_query=project_config.member_default_query,
+ default_sort=project_config.default_sort_spec,
+ default_columns=self._ComputeIssuesListColumns(
+ project_config.default_col_spec),
+ project_grid_config=project_grid_config,
+ member_default_template=template_names.get(
+ project_config.default_template_for_developers),
+ non_members_default_template=template_names.get(
+ project_config.default_template_for_users),
+ revision_url_format=project.revision_url_format,
+ custom_issue_entry_url=project_config.custom_issue_entry_url)
+
+ def CreateProjectMember(self, cnxn, project_id, user_id, role):
+ # type: (MonorailContext, int, int, str) ->
+ # api_proto.project_objects_pb2.ProjectMember
+ """Creates a ProjectMember object from specified parameters.
+
+ Args:
+ cnxn: MonorailConnection object.
+ project_id: ID of the Project the User is a member of.
+ user_id: ID of the user who is a member.
+ role: str specifying the user's role based on a ProjectRole value.
+
+ Return:
+ A protoc ProjectMember object.
+ """
+ name = rnc.ConvertProjectMemberName(
+ cnxn, project_id, user_id, self.services)
+ return project_objects_pb2.ProjectMember(
+ name=name,
+ role=project_objects_pb2.ProjectMember.ProjectRole.Value(role))
+
+ def ConvertLabelDefs(self, label_defs, project_id):
+ # type: (Sequence[proto.tracker_pb2.LabelDef], int) ->
+ # Sequence[api_proto.project_objects_pb2.LabelDef]
+ """Convert protorpc LabelDefs to protoc LabelDefs"""
+ resource_names_dict = rnc.ConvertLabelDefNames(
+ self.cnxn, [ld.label for ld in label_defs], project_id, self.services)
+
+ api_lds = []
+ for ld in label_defs:
+ state = project_objects_pb2.LabelDef.LabelDefState.Value('ACTIVE')
+ if ld.deprecated:
+ state = project_objects_pb2.LabelDef.LabelDefState.Value('DEPRECATED')
+ api_lds.append(
+ project_objects_pb2.LabelDef(
+ name=resource_names_dict.get(ld.label),
+ value=ld.label,
+ docstring=ld.label_docstring,
+ state=state))
+ return api_lds
+
+ def ConvertStatusDefs(self, status_defs, project_id):
+ # type: (Sequence[proto.tracker_pb2.StatusDef], int) ->
+ # Sequence[api_proto.project_objects_pb2.StatusDef]
+ """Convert protorpc StatusDefs to protoc StatusDefs
+
+ Args:
+ status_defs: Sequence of StatusDefs.
+ project_id: ID of the Project these belong to.
+
+ Returns:
+ Sequence of protoc StatusDefs in the same order they are given in
+ `status_defs`.
+ """
+ resource_names_dict = rnc.ConvertStatusDefNames(
+ self.cnxn, [sd.status for sd in status_defs], project_id, self.services)
+ config = self.services.config.GetProjectConfig(self.cnxn, project_id)
+ mergeable_statuses = set(config.statuses_offer_merge)
+
+ # Rank is only surfaced as positional value in well_known_statuses
+ rank_by_status = {}
+ for rank, sd in enumerate(config.well_known_statuses):
+ rank_by_status[sd.status] = rank
+
+ api_sds = []
+ for sd in status_defs:
+ state = project_objects_pb2.StatusDef.StatusDefState.Value('ACTIVE')
+ if sd.deprecated:
+ state = project_objects_pb2.StatusDef.StatusDefState.Value('DEPRECATED')
+
+ if sd.means_open:
+ status_type = project_objects_pb2.StatusDef.StatusDefType.Value('OPEN')
+ else:
+ if sd.status in mergeable_statuses:
+ status_type = project_objects_pb2.StatusDef.StatusDefType.Value(
+ 'MERGED')
+ else:
+ status_type = project_objects_pb2.StatusDef.StatusDefType.Value(
+ 'CLOSED')
+
+ api_sd = project_objects_pb2.StatusDef(
+ name=resource_names_dict.get(sd.status),
+ value=sd.status,
+ type=status_type,
+ rank=rank_by_status[sd.status],
+ docstring=sd.status_docstring,
+ state=state,
+ )
+ api_sds.append(api_sd)
+ return api_sds
+
+ def ConvertComponentDef(self, component_def):
+ # type: (proto.tracker_pb2.ComponentDef) ->
+ # api_proto.project_objects.ComponentDef
+ """Convert a protorpc ComponentDef to a protoc ComponentDef."""
+ return self.ConvertComponentDefs([component_def],
+ component_def.project_id)[0]
+
+ def ConvertComponentDefs(self, component_defs, project_id):
+ # type: (Sequence[proto.tracker_pb2.ComponentDef], int) ->
+ # Sequence[api_proto.project_objects.ComponentDef]
+ """Convert sequence of protorpc ComponentDefs to protoc ComponentDefs
+
+ Args:
+ component_defs: Sequence of protoc ComponentDefs.
+ project_id: ID of the Project these belong to.
+
+ Returns:
+ Sequence of protoc ComponentDefs in the same order they are given in
+ `component_defs`.
+ """
+ resource_names_dict = rnc.ConvertComponentDefNames(
+ self.cnxn, [cd.component_id for cd in component_defs], project_id,
+ self.services)
+ involved_user_ids = tbo.UsersInvolvedInComponents(component_defs)
+ user_resource_names_dict = rnc.ConvertUserNames(involved_user_ids)
+
+ all_label_ids = set()
+ for cd in component_defs:
+ all_label_ids.update(cd.label_ids)
+
+ # If this becomes a performance issue, we should add bulk look up.
+ labels_by_id = {
+ label_id: self.services.config.LookupLabel(
+ self.cnxn, project_id, label_id) for label_id in all_label_ids
+ }
+
+ api_cds = []
+ for cd in component_defs:
+ state = project_objects_pb2.ComponentDef.ComponentDefState.Value('ACTIVE')
+ if cd.deprecated:
+ state = project_objects_pb2.ComponentDef.ComponentDefState.Value(
+ 'DEPRECATED')
+
+ api_cd = project_objects_pb2.ComponentDef(
+ name=resource_names_dict.get(cd.component_id),
+ value=cd.path,
+ docstring=cd.docstring,
+ state=state,
+ admins=[
+ user_resource_names_dict.get(admin_id)
+ for admin_id in cd.admin_ids
+ ],
+ ccs=[user_resource_names_dict.get(cc_id) for cc_id in cd.cc_ids],
+ creator=user_resource_names_dict.get(cd.creator_id),
+ modifier=user_resource_names_dict.get(cd.modifier_id),
+ create_time=timestamp_pb2.Timestamp(seconds=cd.created),
+ modify_time=timestamp_pb2.Timestamp(seconds=cd.modified),
+ labels=[labels_by_id[label_id] for label_id in cd.label_ids],
+ )
+ api_cds.append(api_cd)
+ return api_cds
+
+ def ConvertProjectSavedQueries(self, saved_queries, project_id):
+ # type: (Sequence[proto.tracker_pb2.SavedQuery], int) ->
+ # Sequence(api_proto.project_objects.ProjectSavedQuery)
+ """Convert sequence of protorpc SavedQueries to protoc ProjectSavedQueries
+
+ Args:
+ saved_queries: Sequence of SavedQueries.
+ project_id: ID of the Project these belong to.
+
+ Returns:
+ Sequence of protoc ProjectSavedQueries in the same order they are given in
+ `saved_queries`. In the event any items in `saved_queries` are not found
+ or don't belong to the project, they will be omitted from the result.
+ """
+ resource_names_dict = rnc.ConvertProjectSavedQueryNames(
+ self.cnxn, [sq.query_id for sq in saved_queries], project_id,
+ self.services)
+ api_psqs = []
+ for sq in saved_queries:
+ if sq.query_id not in resource_names_dict:
+ continue
+
+ # TODO(crbug/monorail/7756): Remove base_query_id, avoid confusions.
+ # Until then we have to expand the query by including base_query_id.
+ # base_query_id can only be in the set of DEFAULT_CANNED_QUERIES.
+ if sq.base_query_id:
+ query = '{} {}'.format(tbo.GetBuiltInQuery(sq.base_query_id), sq.query)
+ else:
+ query = sq.query
+
+ api_psqs.append(
+ project_objects_pb2.ProjectSavedQuery(
+ name=resource_names_dict.get(sq.query_id),
+ display_name=sq.name,
+ query=query))
+ return api_psqs