def Run(self, mc, services): """Updates an issue based on the parsed commands.""" try: issue = services.issue.GetIssueByLocalID(mc.cnxn, self.project.project_id, self.local_id, use_cache=False) except exceptions.NoSuchIssueException: return # Issue does not exist, so do nothing delta = tracker_pb2.IssueDelta() allow_edit = permissions.CanEditIssue(mc.auth.effective_ids, mc.perms, self.project, issue) if allow_edit: delta.summary = self.parser.summary or issue.summary if self.parser.status is None: delta.status = issue.status else: delta.status = self.parser.status if self.parser.owner_id is None: delta.owner_id = issue.owner_id else: delta.owner_id = self.parser.owner_id delta.cc_ids_add = list(self.parser.cc_add) delta.cc_ids_remove = list(self.parser.cc_remove) delta.labels_add = self.parser.labels_add delta.labels_remove = self.parser.labels_remove # TODO(jrobbins): allow editing of custom fields with work_env.WorkEnv(mc, services) as we: we.UpdateIssue(issue, delta, self.description, inbound_message=self.inbound_message) logging.info('Updated issue %s:%s', self.project.project_name, issue.local_id)
def ComputeIssueChangeAddressPermList( cnxn, ids_to_consider, project, issue, services, omit_addrs, users_by_id, pref_check_function=lambda u: u.notify_issue_change): """Return a list of user email addresses to notify of an issue change. User email addresses are determined by looking up the given user IDs in the given users_by_id dict. Args: cnxn: connection to SQL database. ids_to_consider: list of user IDs for users interested in this issue. project: Project PB for the project containing this issue. issue: Issue PB for the issue that was updated. services: Services. omit_addrs: set of strings for email addresses to not notify because they already know. users_by_id: dict {user_id: user_view} user info. pref_check_function: optional function to use to check if a certain User PB has a preference set to receive the email being sent. It defaults to "If I am in the issue's owner or cc field", but it can be set to check "If I starred the issue." Returns: A list of AddrPerm objects. """ memb_addr_perm_list = [] logging.info('Considering %r ', ids_to_consider) all_user_prefs = services.user.GetUsersPrefs(cnxn, ids_to_consider) for user_id in ids_to_consider: if user_id == framework_constants.NO_USER_SPECIFIED: continue user = services.user.GetUser(cnxn, user_id) # Notify people who have a pref set, or if they have no User PB # because the pref defaults to True. if user and not pref_check_function(user): logging.info('Not notifying %r: user preference', user.email) continue # TODO(jrobbins): doing a bulk operation would reduce DB load. auth = authdata.AuthData.FromUserID(cnxn, user_id, services) perms = permissions.GetPermissions(user, auth.effective_ids, project) config = services.config.GetProjectConfig(cnxn, project.project_id) granted_perms = tracker_bizobj.GetGrantedPerms( issue, auth.effective_ids, config) if not permissions.CanViewIssue( auth.effective_ids, perms, project, issue, granted_perms=granted_perms): logging.info('Not notifying %r: user cannot view issue', user.email) continue addr = users_by_id[user_id].email if addr in omit_addrs: logging.info('Not notifying %r: user already knows', user.email) continue recipient_is_member = bool(framework_bizobj.UserIsInProject( project, auth.effective_ids)) reply_perm = REPLY_NOT_ALLOWED if project.process_inbound_email: if permissions.CanEditIssue(auth.effective_ids, perms, project, issue): reply_perm = REPLY_MAY_UPDATE elif permissions.CanCommentIssue( auth.effective_ids, perms, project, issue): reply_perm = REPLY_MAY_COMMENT memb_addr_perm_list.append( AddrPerm(recipient_is_member, addr, user, reply_perm, all_user_prefs[user_id])) logging.info('For %s %s, will notify: %r', project.project_name, issue.local_id, [ap.address for ap in memb_addr_perm_list]) return memb_addr_perm_list
def GatherPageData(self, mr): """Build up a dictionary of data values to use when rendering the page. Args: mr: commonly used info parsed from the request. Returns: Dict of values used by EZT for rendering the page. """ if mr.local_id is None: self.abort(404, 'no issue specified') with work_env.WorkEnv(mr, self.services) as we: # Signed in users could edit the issue, so it must be fresh. use_cache = not mr.auth.user_id issue = we.GetIssueByLocalID( mr.project_id, mr.local_id, use_cache=use_cache) # We give no explanation of missing issues on the peek page. if issue.deleted: self.abort(404, 'issue not found') star_cnxn = sql.MonorailConnection() star_promise = framework_helpers.Promise( we.IsIssueStarred, issue, cnxn=star_cnxn) config = we.GetProjectConfig(mr.project_id) comments = we.ListIssueComments(issue) descriptions, visible_comments, cmnt_pagination = PaginateComments( mr, issue, comments, config, self.services) with mr.profiler.Phase('making user proxies'): involved_user_ids = tracker_bizobj.UsersInvolvedInIssues([issue]) group_ids = self.services.usergroup.DetermineWhichUserIDsAreGroups( mr.cnxn, involved_user_ids) comment_user_ids = tracker_bizobj.UsersInvolvedInCommentList( descriptions + visible_comments) users_by_id = framework_views.MakeAllUserViews( mr.cnxn, self.services.user, involved_user_ids, comment_user_ids, group_ids=group_ids) framework_views.RevealAllEmailsToMembers(mr.auth, mr.project, users_by_id) (issue_view, description_views, comment_views) = self._MakeIssueAndCommentViews( mr, issue, users_by_id, descriptions, visible_comments, config, issue_reporters=[], comment_reporters=[]) with mr.profiler.Phase('getting starring info'): starred = star_promise.WaitAndGetValue() star_cnxn.Close() permit_edit = permissions.CanEditIssue( mr.auth.effective_ids, mr.perms, mr.project, issue) mr.ComputeColSpec(config) restrict_to_known = config.restrict_to_known page_perms = self.MakePagePerms( mr, issue, permissions.CREATE_ISSUE, permissions.SET_STAR, permissions.EDIT_ISSUE, permissions.EDIT_ISSUE_SUMMARY, permissions.EDIT_ISSUE_STATUS, permissions.EDIT_ISSUE_OWNER, permissions.EDIT_ISSUE_CC, permissions.DELETE_ISSUE, permissions.ADD_ISSUE_COMMENT, permissions.DELETE_OWN, permissions.DELETE_ANY, permissions.VIEW_INBOUND_MESSAGES) page_perms.EditIssue = ezt.boolean(permit_edit) prevent_restriction_removal = ( mr.project.only_owners_remove_restrictions and not framework_bizobj.UserOwnsProject( mr.project, mr.auth.effective_ids)) cmd_slots, default_slot_num = self.services.features.GetRecentCommands( mr.cnxn, mr.auth.user_id, mr.project_id) cmd_slot_views = [ template_helpers.EZTItem( slot_num=slot_num, command=command, comment=comment) for slot_num, command, comment in cmd_slots] previous_locations = self.GetPreviousLocations(mr, issue) return { 'issue_tab_mode': 'issueDetail', 'issue': issue_view, 'description': description_views, 'comments': comment_views, 'labels': issue.labels, 'num_detail_rows': len(comment_views) + 4, 'noisy': ezt.boolean(tracker_helpers.IsNoisy( len(comment_views), issue.star_count)), 'cmnt_pagination': cmnt_pagination, 'colspec': mr.col_spec, 'searchtip': 'You can jump to any issue by number', 'starred': ezt.boolean(starred), 'pagegen': str(int(time.time() * 1000000)), 'restrict_to_known': ezt.boolean(restrict_to_known), 'prevent_restriction_removal': ezt.boolean( prevent_restriction_removal), 'statuses_offer_merge': config.statuses_offer_merge, 'page_perms': page_perms, 'cmd_slots': cmd_slot_views, 'default_slot_num': default_slot_num, 'quick_edit_submit_url': tracker_helpers.FormatRelativeIssueURL( issue.project_name, urls.ISSUE_PEEK + '.do', id=issue.local_id), 'previous_locations': previous_locations, # for template issue-meta-part shared by issuedetail servlet 'user_remaining_hotlists': [], 'user_issue_hotlists': [], 'involved_users_issue_hotlists': [], 'remaining_issue_hotlists': [], }
def ProcessFormData(self, mr, post_data): """Process the posted issue update form. Args: mr: commonly used info parsed from the request. post_data: HTML form data from the request. Returns: String URL to redirect the user to, or None if response was already sent. """ cmd = post_data.get('cmd', '') send_email = 'send_email' in post_data comment = post_data.get('comment', '') slot_used = int(post_data.get('slot_used', 1)) page_generation_time = int(post_data['pagegen']) with work_env.WorkEnv(mr, self.services) as we: issue = we.GetIssueByLocalID( mr.project_id, mr.local_id, use_cache=False) old_owner_id = tracker_bizobj.GetOwnerId(issue) config = we.GetProjectConfig(mr.project_id) summary, status, owner_id, cc_ids, labels = commands.ParseQuickEditCommand( mr.cnxn, cmd, issue, config, mr.auth.user_id, self.services) component_ids = issue.component_ids # TODO(jrobbins): component commands field_values = issue.field_values # TODO(jrobbins): edit custom fields permit_edit = permissions.CanEditIssue( mr.auth.effective_ids, mr.perms, mr.project, issue) if not permit_edit: raise permissions.PermissionException( 'User is not allowed to edit this issue') amendments, comment_pb = self.services.issue.ApplyIssueComment( mr.cnxn, self.services, mr.auth.user_id, mr.project_id, mr.local_id, summary, status, owner_id, cc_ids, labels, field_values, component_ids, issue.blocked_on_iids, issue.blocking_iids, issue.dangling_blocked_on_refs, issue.dangling_blocking_refs, issue.merged_into, page_gen_ts=page_generation_time, comment=comment) self.services.project.UpdateRecentActivity( mr.cnxn, mr.project.project_id) if send_email: if amendments or comment.strip(): send_notifications.PrepareAndSendIssueChangeNotification( issue.issue_id, mr.request.host, mr.auth.user_id, send_email=send_email, old_owner_id=old_owner_id, comment_id=comment_pb.id) # TODO(jrobbins): allow issue merge via quick-edit. self.services.features.StoreRecentCommand( mr.cnxn, mr.auth.user_id, mr.project_id, slot_used, cmd, comment) # TODO(jrobbins): this is very similar to a block of code in issuebulkedit. mr.can = int(post_data['can']) mr.query = post_data.get('q', '') mr.col_spec = post_data.get('colspec', '') mr.sort_spec = post_data.get('sort', '') mr.group_by_spec = post_data.get('groupby', '') mr.start = int(post_data['start']) mr.num = int(post_data['num']) preview_issue_ref_str = '%s:%d' % (issue.project_name, issue.local_id) return tracker_helpers.FormatIssueListURL( mr, config, preview=preview_issue_ref_str, updated=mr.local_id, ts=int(time.time()))
def convert_issue(cls, issue, mar, services): """Convert Monorail Issue PB to API IssuesGetInsertResponse.""" config = services.config.GetProjectConfig(mar.cnxn, issue.project_id) granted_perms = tracker_bizobj.GetGrantedPerms(issue, mar.auth.effective_ids, config) issue_project = services.project.GetProject(mar.cnxn, issue.project_id) component_list = [] for cd in config.component_defs: cid = cd.component_id if cid in issue.component_ids: component_list.append(cd.path) cc_list = [convert_person(p, mar.cnxn, services) for p in issue.cc_ids] cc_list = [p for p in cc_list if p is not None] field_values_list = [] fds_by_id = {fd.field_id: fd for fd in config.field_defs} phases_by_id = {phase.phase_id: phase for phase in issue.phases} for fv in issue.field_values: fd = fds_by_id.get(fv.field_id) if not fd: logging.warning('Custom field %d of project %s does not exist', fv.field_id, issue_project.project_name) continue val = None if fv.user_id: val = _get_user_email(services.user, mar.cnxn, fv.user_id) else: val = tracker_bizobj.GetFieldValue(fv, {}) if not isinstance(val, string_types): val = str(val) new_fv = api_pb2_v1.FieldValue(fieldName=fd.field_name, fieldValue=val, derived=fv.derived) if fd.approval_id: # Attach parent approval name approval_fd = fds_by_id.get(fd.approval_id) if not approval_fd: logging.warning( 'Parent approval field %d of field %s does not exist', fd.approval_id, fd.field_name) else: new_fv.approvalName = approval_fd.field_name elif fv.phase_id: # Attach phase name phase = phases_by_id.get(fv.phase_id) if not phase: logging.warning('Phase %d for field %s does not exist', fv.phase_id, fd.field_name) else: new_fv.phaseName = phase.name field_values_list.append(new_fv) approval_values_list = convert_approvals(mar.cnxn, issue.approval_values, services, config, issue.phases) phases_list = convert_phases(issue.phases) with work_env.WorkEnv(mar, services) as we: starred = we.IsIssueStarred(issue) resp = cls( kind='monorail#issue', id=issue.local_id, title=issue.summary, summary=issue.summary, projectId=issue_project.project_name, stars=issue.star_count, starred=starred, status=issue.status, state=(api_pb2_v1.IssueState.open if tracker_helpers.MeansOpenInProject( tracker_bizobj.GetStatus(issue), config) else api_pb2_v1.IssueState.closed), labels=issue.labels, components=component_list, author=convert_person(issue.reporter_id, mar.cnxn, services), owner=convert_person(issue.owner_id, mar.cnxn, services), cc=cc_list, updated=datetime.datetime.fromtimestamp(issue.modified_timestamp), published=datetime.datetime.fromtimestamp(issue.opened_timestamp), blockedOn=convert_issue_ids(issue.blocked_on_iids, mar, services), blocking=convert_issue_ids(issue.blocking_iids, mar, services), canComment=permissions.CanCommentIssue(mar.auth.effective_ids, mar.perms, issue_project, issue, granted_perms=granted_perms), canEdit=permissions.CanEditIssue(mar.auth.effective_ids, mar.perms, issue_project, issue, granted_perms=granted_perms), fieldValues=field_values_list, approvalValues=approval_values_list, phases=phases_list) if issue.closed_timestamp > 0: resp.closed = datetime.datetime.fromtimestamp(issue.closed_timestamp) if issue.merged_into: resp.mergedInto = convert_issue_ids([issue.merged_into], mar, services)[0] if issue.owner_modified_timestamp: resp.owner_modified = datetime.datetime.fromtimestamp( issue.owner_modified_timestamp) if issue.status_modified_timestamp: resp.status_modified = datetime.datetime.fromtimestamp( issue.status_modified_timestamp) if issue.component_modified_timestamp: resp.component_modified = datetime.datetime.fromtimestamp( issue.component_modified_timestamp) return resp
def ProcessIssueReply( self, cnxn, project, local_id, project_addr, from_addr, author_id, effective_ids, perms, body): """Examine an issue reply email body and add a comment to the issue. Args: cnxn: connection to SQL database. project: Project PB for the project containing the issue. local_id: int ID of the issue being replied to. project_addr: string email address used for outbound emails from that project. from_addr: string email address of the user who sent the email reply to our server. author_id: int user ID of user who sent the reply email. effective_ids: set of int user IDs for the user (including any groups), or an empty set if user is not signed in. perms: PermissionSet for the user who sent the reply email. body: string email body text of the reply email. Returns: A list of follow-up work items, e.g., to notify other users of the new comment, or to notify the user that their reply was not processed. Side-effect: Adds a new comment to the issue, if no error is reported. """ try: issue = self.services.issue.GetIssueByLocalID( cnxn, project.project_id, local_id) except issue_svc.NoSuchIssueException: issue = None if not issue or issue.deleted: # The referenced issue was not found, e.g., it might have been # deleted, or someone messed with the subject line. Reject it. return _MakeErrorMessageReplyTask( project_addr, from_addr, self._templates['no_artifact'], artifact_phrase='issue %d' % local_id, project_name=project.project_name) can_view = perms.CanUsePerm( permissions.VIEW, effective_ids, project, permissions.GetRestrictions(issue)) can_comment = perms.CanUsePerm( permissions.ADD_ISSUE_COMMENT, effective_ids, project, permissions.GetRestrictions(issue)) if not can_view or not can_comment: return _MakeErrorMessageReplyTask( project_addr, from_addr, self._templates['no_perms'], artifact_phrase='issue %d' % local_id, project_name=project.project_name) allow_edit = permissions.CanEditIssue( effective_ids, perms, project, issue) # TODO(jrobbins): if the user does not have EDIT_ISSUE and the inbound # email tries to make an edit, send back an error message. lines = body.strip().split('\n') uia = commitlogcommands.UpdateIssueAction(local_id) uia.Parse(cnxn, project.project_name, author_id, lines, self.services, strip_quoted_lines=True) uia.Run(cnxn, self.services, allow_edit=allow_edit)
def ProcessFormData(self, mr, post_data): """Process the posted issue update form. Args: mr: commonly used info parsed from the request. post_data: HTML form data from the request. Returns: String URL to redirect the user to after processing. """ if not mr.local_id_list: logging.info('missing issue local IDs, probably tampered') self.response.status = httplib.BAD_REQUEST return # Check that the user is logged in; anon users cannot update issues. if not mr.auth.user_id: logging.info('user was not logged in, cannot update issue') self.response.status = httplib.BAD_REQUEST # xxx should raise except return # Check that the user has permission to add a comment, and to enter # metadata if they are trying to do that. if not self.CheckPerm(mr, permissions.ADD_ISSUE_COMMENT): logging.info('user has no permission to add issue comment') self.response.status = httplib.BAD_REQUEST return if not self.CheckPerm(mr, permissions.EDIT_ISSUE): logging.info('user has no permission to edit issue metadata') self.response.status = httplib.BAD_REQUEST return move_to = post_data.get('move_to', '').lower() if move_to and not self.CheckPerm(mr, permissions.DELETE_ISSUE): logging.info('user has no permission to move issue') self.response.status = httplib.BAD_REQUEST return config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id) parsed = tracker_helpers.ParseIssueRequest(mr.cnxn, post_data, self.services, mr.errors, mr.project_name) bounce_labels = (parsed.labels[:] + ['-%s' % lr for lr in parsed.labels_remove]) bounce_fields = tracker_views.MakeBounceFieldValueViews( parsed.fields.vals, parsed.fields.phase_vals, config) field_helpers.ShiftEnumFieldsIntoLabels(parsed.labels, parsed.labels_remove, parsed.fields.vals, parsed.fields.vals_remove, config) issue_list = self.services.issue.GetIssuesByLocalIDs( mr.cnxn, mr.project_id, mr.local_id_list) issue_phases = list( itertools.chain.from_iterable(issue.phases for issue in issue_list)) phase_ids_by_name = collections.defaultdict(set) for phase in issue_phases: phase_ids_by_name[phase.name.lower()].add(phase.phase_id) # Note: Not all parsed phase field values will be applicable to every issue. # tracker_bizobj.ApplyFieldValueChanges will take care of not adding # phase field values to issues that don't contain the correct phase. field_vals = field_helpers.ParseFieldValues( mr.cnxn, self.services.user, parsed.fields.vals, parsed.fields.phase_vals, config, phase_ids_by_name=phase_ids_by_name) field_vals_remove = field_helpers.ParseFieldValues( mr.cnxn, self.services.user, parsed.fields.vals_remove, parsed.fields.phase_vals_remove, config, phase_ids_by_name=phase_ids_by_name) field_helpers.ValidateCustomFields(mr, self.services, field_vals, config, mr.errors) # Treat status '' as no change and explicit 'clear' as clearing the status. status = parsed.status if status == '': status = None if post_data.get('op_statusenter') == 'clear': status = '' reporter_id = mr.auth.user_id logging.info('bulk edit request by %s', reporter_id) if parsed.users.owner_id is None: mr.errors.owner = 'Invalid owner username' else: valid, msg = tracker_helpers.IsValidIssueOwner( mr.cnxn, mr.project, parsed.users.owner_id, self.services) if not valid: mr.errors.owner = msg if (status in config.statuses_offer_merge and not post_data.get('merge_into')): mr.errors.merge_into_id = 'Please enter a valid issue ID' move_to_project = None if move_to: if mr.project_name == move_to: mr.errors.move_to = 'The issues are already in project ' + move_to else: move_to_project = self.services.project.GetProjectByName( mr.cnxn, move_to) if not move_to_project: mr.errors.move_to = 'No such project: ' + move_to # Treat owner '' as no change, and explicit 'clear' as NO_USER_SPECIFIED owner_id = parsed.users.owner_id if parsed.users.owner_username == '': owner_id = None if post_data.get('op_ownerenter') == 'clear': owner_id = framework_constants.NO_USER_SPECIFIED comp_ids = tracker_helpers.LookupComponentIDs(parsed.components.paths, config, mr.errors) comp_ids_remove = tracker_helpers.LookupComponentIDs( parsed.components.paths_remove, config, mr.errors) if post_data.get('op_componententer') == 'remove': comp_ids, comp_ids_remove = comp_ids_remove, comp_ids cc_ids, cc_ids_remove = parsed.users.cc_ids, parsed.users.cc_ids_remove if post_data.get('op_memberenter') == 'remove': cc_ids, cc_ids_remove = parsed.users.cc_ids_remove, parsed.users.cc_ids issue_list_iids = {issue.issue_id for issue in issue_list} if post_data.get('op_blockedonenter') == 'append': if issue_list_iids.intersection(parsed.blocked_on.iids): mr.errors.blocked_on = 'Cannot block an issue on itself.' blocked_on_add = parsed.blocked_on.iids blocked_on_remove = [] else: blocked_on_add = [] blocked_on_remove = parsed.blocked_on.iids if post_data.get('op_blockingenter') == 'append': if issue_list_iids.intersection(parsed.blocking.iids): mr.errors.blocking = 'Cannot block an issue on itself.' blocking_add = parsed.blocking.iids blocking_remove = [] else: blocking_add = [] blocking_remove = parsed.blocking.iids iids_actually_changed = [] old_owner_ids = [] combined_amendments = [] merge_into_issue = None new_starrers = set() if not mr.errors.AnyErrors(): # Because we will modify issues, load from DB rather than cache. issue_list = self.services.issue.GetIssuesByLocalIDs( mr.cnxn, mr.project_id, mr.local_id_list, use_cache=False) # Skip any individual issues that the user is not allowed to edit. editable_issues = [ issue for issue in issue_list if permissions.CanEditIssue( mr.auth.effective_ids, mr.perms, mr.project, issue) ] # Skip any restrict issues that cannot be moved if move_to: editable_issues = [ issue for issue in editable_issues if not permissions.GetRestrictions(issue) ] # If 'Duplicate' status is specified ensure there are no permission issues # with the issue we want to merge with. if post_data.get('merge_into'): for issue in editable_issues: _, merge_into_issue = tracker_helpers.ParseMergeFields( mr.cnxn, self.services, mr.project_name, post_data, parsed.status, config, issue, mr.errors) if merge_into_issue: merge_allowed = tracker_helpers.IsMergeAllowed( merge_into_issue, mr, self.services) if not merge_allowed: mr.errors.merge_into_id = 'Target issue %s cannot be modified' % ( merge_into_issue.local_id) break # Update the new_starrers set. new_starrers.update( tracker_helpers.GetNewIssueStarrers( mr.cnxn, self.services, issue.issue_id, merge_into_issue.issue_id)) # Proceed with amendments only if there are no reported errors. if not mr.errors.AnyErrors(): # Sort the issues: we want them in this order so that the # corresponding old_owner_id are found in the same order. editable_issues.sort( lambda i1, i2: cmp(i1.local_id, i2.local_id)) iids_to_invalidate = set() rules = self.services.features.GetFilterRules( mr.cnxn, config.project_id) predicate_asts = filterrules_helpers.ParsePredicateASTs( rules, config, []) for issue in editable_issues: old_owner_id = tracker_bizobj.GetOwnerId(issue) merge_into_iid = (merge_into_issue.issue_id if merge_into_issue else None) delta = tracker_bizobj.MakeIssueDelta( status, owner_id, cc_ids, cc_ids_remove, comp_ids, comp_ids_remove, parsed.labels, parsed.labels_remove, field_vals, field_vals_remove, parsed.fields.fields_clear, blocked_on_add, blocked_on_remove, blocking_add, blocking_remove, merge_into_iid, None) amendments, _ = self.services.issue.DeltaUpdateIssue( mr.cnxn, self.services, mr.auth.user_id, mr.project_id, config, issue, delta, comment=parsed.comment, iids_to_invalidate=iids_to_invalidate, rules=rules, predicate_asts=predicate_asts) if amendments or parsed.comment: # Avoid empty comments. iids_actually_changed.append(issue.issue_id) old_owner_ids.append(old_owner_id) combined_amendments.extend(amendments) self.services.issue.InvalidateIIDs(mr.cnxn, iids_to_invalidate) self.services.project.UpdateRecentActivity( mr.cnxn, mr.project.project_id) # Add new_starrers and new CCs to merge_into_issue. if merge_into_issue: merge_into_project = self.services.project.GetProjectByName( mr.cnxn, merge_into_issue.project_name) tracker_helpers.AddIssueStarrers(mr.cnxn, self.services, mr, merge_into_issue.issue_id, merge_into_project, new_starrers) tracker_helpers.MergeCCsAndAddCommentMultipleIssues( self.services, mr, editable_issues, merge_into_issue) if move_to and editable_issues: tracker_fulltext.UnindexIssues( [issue.issue_id for issue in editable_issues]) for issue in editable_issues: old_text_ref = 'issue %s:%s' % (issue.project_name, issue.local_id) moved_back_iids = self.services.issue.MoveIssues( mr.cnxn, move_to_project, [issue], self.services.user) new_text_ref = 'issue %s:%s' % (issue.project_name, issue.local_id) if issue.issue_id in moved_back_iids: content = 'Moved %s back to %s again.' % ( old_text_ref, new_text_ref) else: content = 'Moved %s to now be %s.' % (old_text_ref, new_text_ref) self.services.issue.CreateIssueComment( mr.cnxn, issue, mr.auth.user_id, content, amendments=[ tracker_bizobj.MakeProjectAmendment( move_to_project.project_name) ]) send_email = 'send_email' in post_data users_by_id = framework_views.MakeAllUserViews( mr.cnxn, self.services.user, [owner_id], cc_ids, cc_ids_remove, old_owner_ids, tracker_bizobj.UsersInvolvedInAmendments( combined_amendments)) if move_to and editable_issues: iids_actually_changed = [ issue.issue_id for issue in editable_issues ] send_notifications.SendIssueBulkChangeNotification( iids_actually_changed, mr.request.host, old_owner_ids, parsed.comment, reporter_id, combined_amendments, send_email, users_by_id) if mr.errors.AnyErrors(): bounce_cc_parts = ( parsed.users.cc_usernames + ['-%s' % ccur for ccur in parsed.users.cc_usernames_remove]) self.PleaseCorrect( mr, initial_status=parsed.status, initial_owner=parsed.users.owner_username, initial_merge_into=post_data.get('merge_into', 0), initial_cc=', '.join(bounce_cc_parts), initial_comment=parsed.comment, initial_components=parsed.components.entered_str, labels=bounce_labels, fields=bounce_fields) return with mr.profiler.Phase('reindexing issues'): logging.info('starting reindexing') start = time.time() # Get the updated issues and index them issue_list = self.services.issue.GetIssuesByLocalIDs( mr.cnxn, mr.project_id, mr.local_id_list) tracker_fulltext.IndexIssues(mr.cnxn, issue_list, self.services.user, self.services.issue, self.services.config) logging.info('reindexing %d issues took %s sec', len(issue_list), time.time() - start) # TODO(jrobbins): These could be put into the form action attribute. mr.can = int(post_data['can']) mr.query = post_data['q'] mr.col_spec = post_data['colspec'] mr.sort_spec = post_data['sort'] mr.group_by_spec = post_data['groupby'] mr.start = int(post_data['start']) mr.num = int(post_data['num']) # TODO(jrobbins): implement bulk=N param for a better confirmation alert. return tracker_helpers.FormatIssueListURL(mr, config, saved=len(mr.local_id_list), ts=int(time.time()))
def convert_issue(cls, issue, mar, services): """Convert Monorail Issue PB to API IssuesGetInsertResponse.""" config = services.config.GetProjectConfig(mar.cnxn, issue.project_id) granted_perms = tracker_bizobj.GetGrantedPerms( issue, mar.auth.effective_ids, config) issue_project = services.project.GetProject(mar.cnxn, issue.project_id) component_list = [] for cd in config.component_defs: cid = cd.component_id if cid in issue.component_ids: component_list.append(cd.path) cc_list = [convert_person(p, mar.cnxn, services) for p in issue.cc_ids] cc_list = [p for p in cc_list if p is not None] field_values_list = [] field_id_dict = { fd.field_id: fd.field_name for fd in config.field_defs} for fv in issue.field_values: field_name = field_id_dict.get(fv.field_id) if not field_name: logging.warning('Custom field %d of project %s does not exist', fv.field_id, issue_project.project_name) continue val = None if fv.user_id: val = _get_user_email( services.user, mar.cnxn, fv.user_id) elif fv.str_value: val = fv.str_value elif fv.int_value: val = str(fv.int_value) new_fv = api_pb2_v1.FieldValue( fieldName=field_name, fieldValue=val, derived=fv.derived) field_values_list.append(new_fv) resp = cls( kind='monorail#issue', id=issue.local_id, title=issue.summary, summary=issue.summary, projectId=issue_project.project_name, stars=issue.star_count, starred=services.issue_star.IsItemStarredBy( mar.cnxn, issue.issue_id, mar.auth.user_id), status=issue.status, state=(api_pb2_v1.IssueState.open if tracker_helpers.MeansOpenInProject( tracker_bizobj.GetStatus(issue), config) else api_pb2_v1.IssueState.closed), labels=issue.labels, components=component_list, author=convert_person(issue.reporter_id, mar.cnxn, services), owner=convert_person(issue.owner_id, mar.cnxn, services), cc=cc_list, updated=datetime.datetime.fromtimestamp(issue.modified_timestamp), published=datetime.datetime.fromtimestamp(issue.opened_timestamp), blockedOn=convert_issue_ids(issue.blocked_on_iids, mar, services), blocking=convert_issue_ids(issue.blocking_iids, mar, services), canComment=permissions.CanCommentIssue( mar.auth.effective_ids, mar.perms, issue_project, issue, granted_perms=granted_perms), canEdit=permissions.CanEditIssue( mar.auth.effective_ids, mar.perms, issue_project, issue, granted_perms=granted_perms), fieldValues=field_values_list) if issue.closed_timestamp > 0: resp.closed = datetime.datetime.fromtimestamp(issue.closed_timestamp) if issue.merged_into: resp.mergedInto=convert_issue_ids([issue.merged_into], mar, services)[0] if issue.owner_modified_timestamp: resp.owner_modified = datetime.datetime.fromtimestamp( issue.owner_modified_timestamp) if issue.status_modified_timestamp: resp.status_modified = datetime.datetime.fromtimestamp( issue.status_modified_timestamp) if issue.component_modified_timestamp: resp.component_modified = datetime.datetime.fromtimestamp( issue.component_modified_timestamp) return resp