def _GetComponentIDs(proj_config, components): comps = ['Infra'] if components: components = components.strip() if components: comps = [c.strip() for c in components.split(',')] return tracker_helpers.LookupComponentIDs(comps, proj_config)
def testProcessAlert_NewIssue(self, fake_pasicn, fake_pasibn): """When an alert for a new incident comes in, create a new issue.""" self.mox.StubOutWithMock(tracker_helpers, 'LookupComponentIDs') tracker_helpers.LookupComponentIDs(['Infra'], mox.IgnoreArg()).AndReturn([1]) self.mox.StubOutWithMock(self.services.config, 'LookupLabelID') self.services.config.LookupLabelID( self.cnxn, self.project.project_id, 'Incident-Id-incident-1').AndReturn(None) # Mock command parsing. mock_uia = commitlogcommands.UpdateIssueAction(101) self.mox.StubOutWithMock(commitlogcommands, 'UpdateIssueAction') commitlogcommands.UpdateIssueAction(101).AndReturn(mock_uia) self.mox.StubOutWithMock(mock_uia, 'Parse') mock_uia.Parse(self.cnxn, self.project.project_name, 111, ['issue body'], self.services, strip_quoted_lines=True) self.mox.ReplayAll() auth = authdata.AuthData(user_id=111, email='*****@*****.**') ret = self.inbound.ProcessAlert(self.cnxn, self.project, self.project_addr, '*****@*****.**', auth, 'issue title', 'issue body', 'incident-1') self.mox.VerifyAll() self.assertIsNone(ret) actual_issue = self.services.issue.GetIssueByLocalID( self.cnxn, self.project.project_id, 101) actual_comments = self.services.issue.GetCommentsForIssue( self.cnxn, actual_issue.issue_id) self.assertEqual('issue title', actual_issue.summary) self.assertEqual('Available', actual_issue.status) self.assertEqual(111, actual_issue.reporter_id) self.assertEqual([1], actual_issue.component_ids) self.assertEqual(None, actual_issue.owner_id) self.assertEqual([ 'Infra-Troopers-Alerts', 'Restrict-View-Google', 'Pri-2', 'Incident-Id-incident-1' ], actual_issue.labels) self.assertEqual( 'Filed by [email protected] on behalf of [email protected]\n\nissue body', actual_comments[0].content) self.assertEqual(1, len(fake_pasicn.mock_calls)) self.assertEqual(1, len(fake_pasibn.mock_calls))
def testDefaultComponent(self, header_value): """Checks if the default component is Infra.""" self.test_msg.replace_header(AlertEmailHeader.COMPONENT, header_value) self.mox.StubOutWithMock(tracker_helpers, 'LookupComponentIDs') tracker_helpers.LookupComponentIDs( ['Infra'], mox.IgnoreArg()).AndReturn([self.component_id]) self.mox.ReplayAll() props = alert2issue.GetAlertProperties(self.services, self.cnxn, self.project_id, self.incident_id, self.trooper_queue, self.test_msg) self.assertEqual(props['component_ids'], [self.component_id]) self.mox.VerifyAll()
def testProcessNotification_IfFromWhitelistedSender(self): self.mox.StubOutWithMock(alert2issue, 'IsWhitelisted') alert2issue.IsWhitelisted(self.from_addr).AndReturn(True) self.mox.StubOutWithMock(tracker_helpers, 'LookupComponentIDs') tracker_helpers.LookupComponentIDs(['Infra'], mox.IgnoreArg()).AndReturn([1]) self.mox.StubOutWithMock(self.services.issue, 'CreateIssueComment') self.mox.StubOutWithMock(self.services.issue, 'CreateIssue') self.mox.ReplayAll() # Either of the methods should be called, if the sender is whitelisted. with self.assertRaises(mox.UnexpectedMethodCallError): alert2issue.ProcessEmailNotification( self.services, self.cnxn, self.project, self.project_addr, self.from_addr, self.auth, self.msg_subject, self.msg_body, self.incident_label, self.msg, self.trooper_queue) self.mox.VerifyAll()
def testProcessAlert_NewIssue_Codesearch(self, fake_pasicn, fake_pasibn): """When an alert for a new incident comes in, create a new issue. If the body contains the string 'codesearch' then we should auto-assign to the Infra>Codesearch component.""" self.mox.StubOutWithMock(tracker_helpers, 'LookupComponentIDs') tracker_helpers.LookupComponentIDs(['Infra>Codesearch'], mox.IgnoreArg()).AndReturn([2]) self.mox.StubOutWithMock(self.services.config, 'LookupLabelID') self.services.config.LookupLabelID( self.cnxn, self.project.project_id, 'Incident-Id-incident-1').AndReturn(None) # Mock command parsing. mock_uia = commitlogcommands.UpdateIssueAction(101) self.mox.StubOutWithMock(commitlogcommands, 'UpdateIssueAction') commitlogcommands.UpdateIssueAction(101).AndReturn(mock_uia) self.mox.StubOutWithMock(mock_uia, 'Parse') mock_uia.Parse(self.cnxn, self.project.project_name, 111, ['issue body codesearch'], self.services, strip_quoted_lines=True) self.mox.ReplayAll() auth = authdata.AuthData(user_id=111, email='*****@*****.**') ret = self.inbound.ProcessAlert(self.cnxn, self.project, self.project_addr, '*****@*****.**', auth, 'issue title', 'issue body codesearch', 'incident-1') self.mox.VerifyAll() self.assertIsNone(ret) actual_issue = self.services.issue.GetIssueByLocalID( self.cnxn, self.project.project_id, 101) self.assertEqual([2], actual_issue.component_ids) self.assertEqual(1, len(fake_pasicn.mock_calls)) self.assertEqual(1, len(fake_pasibn.mock_calls))
def testGetComponentIDs(self, components, expected_component_ids): """Tests _GetComponentIDs.""" self.test_msg.replace_header(AlertEmailHeader.COMPONENT, ','.join(sorted(components.keys()))) self.mox.StubOutWithMock(tracker_helpers, 'LookupComponentIDs') tracker_helpers.LookupComponentIDs(sorted( components.keys()), mox.IgnoreArg()).AndReturn([ components[key] for key in sorted(components.keys()) if components[key] ]) self.mox.ReplayAll() props = alert2issue.GetAlertProperties(self.services, self.cnxn, self.project_id, self.incident_id, self.trooper_queue, self.test_msg) self.assertEqual(sorted(props['component_ids']), sorted(expected_component_ids)) self.mox.VerifyAll()
def GetTemplateInfoFromParsed(mr, services, parsed, config): """Get Template field info and PBs from a ParsedTemplate.""" admin_ids, _ = tracker_helpers.ParseAdminUsers(mr.cnxn, parsed.admin_str, services.user) owner_id = 0 if parsed.owner_str: try: user_id = services.user.LookupUserID(mr.cnxn, parsed.owner_str) auth = authdata.AuthData.FromUserID(mr.cnxn, user_id, services) if framework_bizobj.UserIsInProject(mr.project, auth.effective_ids): owner_id = user_id else: mr.errors.owner = 'User is not a member of this project.' except exceptions.NoSuchUserException: mr.errors.owner = 'Owner not found.' component_ids = tracker_helpers.LookupComponentIDs(parsed.component_paths, config, mr.errors) # TODO(jojwang): monorail:4678 Process phase field values. phase_field_val_strs = {} field_values = field_helpers.ParseFieldValues(mr.cnxn, services.user, parsed.field_val_strs, phase_field_val_strs, config) for fv in field_values: logging.info('field_value is %r: %r', fv.field_id, tracker_bizobj.GetFieldValue(fv, {})) phases = [] approvals = [] if parsed.add_approvals: phases, approvals = _GetPhasesAndApprovalsFromParsed( mr, parsed.phase_names, parsed.approvals_to_phase_idx, parsed.required_approval_ids) return admin_ids, owner_id, component_ids, field_values, phases, approvals
def ProcessFormData(self, mr, post_data): """Process the issue entry form. Args: mr: commonly used info parsed from the request. post_data: The post_data dict for the current request. Returns: String URL to redirect the user to after processing. """ 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[:] 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) is_member = framework_bizobj.UserIsInProject(mr.project, mr.auth.effective_ids) template = self._GetTemplate(mr.cnxn, config, parsed.template_name, is_member) (approval_values, phases) = issue_tmpl_helpers.FilterApprovalsAndPhases( template.approval_values or [], template.phases, config) phase_ids_by_name = { phase.name.lower(): [phase.phase_id] for phase in template.phases } field_values = field_helpers.ParseFieldValues( mr.cnxn, self.services.user, parsed.fields.vals, parsed.fields.phase_vals, config, phase_ids_by_name=phase_ids_by_name) labels = _DiscardUnusedTemplateLabelPrefixes(parsed.labels) component_ids = tracker_helpers.LookupComponentIDs( parsed.components.paths, config, mr.errors) # TODO(jrobbins): consider captcha 3 score in API if not parsed.summary.strip() or parsed.summary == PLACEHOLDER_SUMMARY: mr.errors.summary = 'Summary is required' if not parsed.comment.strip(): mr.errors.comment = 'A description is required' if len(parsed.comment) > tracker_constants.MAX_COMMENT_CHARS: mr.errors.comment = 'Comment is too long' if len(parsed.summary) > tracker_constants.MAX_SUMMARY_CHARS: mr.errors.summary = 'Summary is too long' if _MatchesTemplate(parsed.comment, template): mr.errors.comment = 'Template must be filled out.' 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 None in parsed.users.cc_ids: mr.errors.cc = 'Invalid Cc username' field_helpers.ValidateCustomFields(mr, self.services, field_values, config, mr.errors) hotlist_pbs = ProcessParsedHotlistRefs(mr, self.services, parsed.hotlists.hotlist_refs) if not mr.errors.AnyErrors(): with work_env.WorkEnv(mr, self.services) as we: try: if parsed.attachments: new_bytes_used = tracker_helpers.ComputeNewQuotaBytesUsed( mr.project, parsed.attachments) # TODO(jrobbins): Make quota be calculated and stored as # part of applying the comment. self.services.project.UpdateProject( mr.cnxn, mr.project.project_id, attachment_bytes_used=new_bytes_used) marked_description = tracker_helpers.MarkupDescriptionOnInput( parsed.comment, template.content) has_star = 'star' in post_data and post_data['star'] == '1' if approval_values: _AttachDefaultApprovers(config, approval_values) issue, _ = we.CreateIssue( mr.project_id, parsed.summary, parsed.status, parsed.users.owner_id, parsed.users.cc_ids, labels, field_values, component_ids, marked_description, blocked_on=parsed.blocked_on.iids, blocking=parsed.blocking.iids, attachments=parsed.attachments, approval_values=approval_values, phases=phases) if has_star: we.StarIssue(issue, True) if hotlist_pbs: hotlist_ids = { hotlist.hotlist_id for hotlist in hotlist_pbs } issue_tuple = (issue.issue_id, mr.auth.user_id, int(time.time()), '') self.services.features.AddIssueToHotlists( mr.cnxn, hotlist_ids, issue_tuple, self.services.issue, self.services.chart) except tracker_helpers.OverAttachmentQuota: mr.errors.attachments = 'Project attachment quota exceeded.' mr.template_name = parsed.template_name if mr.errors.AnyErrors(): self.PleaseCorrect( mr, initial_summary=parsed.summary, initial_status=parsed.status, initial_owner=parsed.users.owner_username, initial_cc=', '.join(parsed.users.cc_usernames), initial_components=', '.join(parsed.components.paths), initial_comment=parsed.comment, labels=bounce_labels, fields=bounce_fields, template_name=parsed.template_name, initial_blocked_on=parsed.blocked_on.entered_str, initial_blocking=parsed.blocking.entered_str, initial_hotlists=parsed.hotlists.entered_str, component_required=ezt.boolean(template.component_required)) return # format a redirect url return framework_helpers.FormatAbsoluteURL(mr, urls.ISSUE_DETAIL, id=issue.local_id)
def _ParseTemplate(self, post_data, mr, i, orig_template, config): """Parse an issue template. Return orig_template if cannot edit.""" if not self._CanEditTemplate(mr, orig_template): return orig_template name = post_data['name_%s' % i] if name == tracker_constants.DELETED_TEMPLATE_NAME: return None members_only = False if ('members_only_%s' % i) in post_data: members_only = (post_data['members_only_%s' % i] == 'yes') summary = '' if ('summary_%s' % i) in post_data: summary = post_data['summary_%s' % i] summary_must_be_edited = False if ('summary_must_be_edited_%s' % i) in post_data: summary_must_be_edited = (post_data['summary_must_be_edited_%s' % i] == 'yes') content = '' if ('content_%s' % i) in post_data: content = post_data['content_%s' % i] # wrap="hard" has no effect on the content because we copy it to # a hidden form field before submission. So, server-side word wrap. content = framework_helpers.WordWrapSuperLongLines(content, max_cols=75) status = '' if ('status_%s' % i) in post_data: status = post_data['status_%s' % i] owner_id = 0 if ('owner_%s' % i) in post_data: owner = post_data['owner_%s' % i] if owner: user_id = self.services.user.LookupUserID(mr.cnxn, owner) auth = monorailrequest.AuthData.FromUserID( mr.cnxn, user_id, self.services) if framework_bizobj.UserIsInProject(mr.project, auth.effective_ids): owner_id = user_id labels = post_data.getall('label_%s' % i) labels_remove = [] field_val_strs = collections.defaultdict(list) for fd in config.field_defs: field_value_key = 'field_value_%d_%d' % (i, fd.field_id) if post_data.get(field_value_key): field_val_strs[fd.field_id].append(post_data[field_value_key]) field_helpers.ShiftEnumFieldsIntoLabels(labels, labels_remove, field_val_strs, {}, config) field_values = field_helpers.ParseFieldValues(mr.cnxn, self.services.user, field_val_strs, config) for fv in field_values: logging.info('field_value is %r: %r', fv.field_id, tracker_bizobj.GetFieldValue(fv, {})) admin_ids = [] if ('admin_names_%s' % i) in post_data: admin_ids, _admin_str = tracker_helpers.ParseAdminUsers( mr.cnxn, post_data['admin_names_%s' % i], self.services.user) component_ids = [] if ('components_%s' % i) in post_data: component_paths = [] for component_path in post_data['components_%s' % i].split(','): if component_path.strip() not in component_paths: component_paths.append(component_path.strip()) component_ids = tracker_helpers.LookupComponentIDs( component_paths, config, mr.errors) owner_defaults_to_member = False if ('owner_defaults_to_member_%s' % i) in post_data: owner_defaults_to_member = ( post_data['owner_defaults_to_member_%s' % i] == 'yes') component_required = False if ('component_required_%s' % i) in post_data: component_required = post_data['component_required_%s' % i] == 'yes' template = tracker_bizobj.MakeIssueTemplate( name, summary, status, owner_id, content, labels, field_values, admin_ids, component_ids, summary_must_be_edited=summary_must_be_edited, owner_defaults_to_member=owner_defaults_to_member, component_required=component_required, members_only=members_only) template_id = int(post_data['template_id_%s' % i]) if template_id: # new templates have ID 0, so leave that None in PB. template.template_id = template_id logging.info('template is %r', template) return template
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 ProcessFormData(self, mr, post_data): """Process the issue entry form. Args: mr: commonly used info parsed from the request. post_data: The post_data dict for the current request. Returns: String URL to redirect the user to after processing. """ 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[:] bounce_fields = tracker_views.MakeBounceFieldValueViews( parsed.fields.vals, config) field_helpers.ShiftEnumFieldsIntoLabels( parsed.labels, parsed.labels_remove, parsed.fields.vals, parsed.fields.vals_remove, config) field_values = field_helpers.ParseFieldValues( mr.cnxn, self.services.user, parsed.fields.vals, config) labels = _DiscardUnusedTemplateLabelPrefixes(parsed.labels) component_ids = tracker_helpers.LookupComponentIDs( parsed.components.paths, config, mr.errors) reporter_id = mr.auth.user_id self.CheckCaptcha(mr, post_data) if not parsed.summary.strip() or parsed.summary == PLACEHOLDER_SUMMARY: mr.errors.summary = 'Summary is required' if not parsed.comment.strip(): mr.errors.comment = 'A description is required' if len(parsed.comment) > tracker_constants.MAX_COMMENT_CHARS: mr.errors.comment = 'Comment is too long' if len(parsed.summary) > tracker_constants.MAX_SUMMARY_CHARS: mr.errors.summary = 'Summary is too long' if _MatchesTemplate(parsed.comment, config): mr.errors.comment = 'Template must be filled out.' 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 None in parsed.users.cc_ids: mr.errors.cc = 'Invalid Cc username' field_helpers.ValidateCustomFields( mr, self.services, field_values, config, mr.errors) new_local_id = None if not mr.errors.AnyErrors(): try: if parsed.attachments: new_bytes_used = tracker_helpers.ComputeNewQuotaBytesUsed( mr.project, parsed.attachments) self.services.project.UpdateProject( mr.cnxn, mr.project.project_id, attachment_bytes_used=new_bytes_used) template_content = '' for wkp in config.templates: if wkp.name == parsed.template_name: template_content = wkp.content marked_comment = _MarkupDescriptionOnInput( parsed.comment, template_content) has_star = 'star' in post_data and post_data['star'] == '1' new_local_id = self.services.issue.CreateIssue( mr.cnxn, self.services, mr.project_id, parsed.summary, parsed.status, parsed.users.owner_id, parsed.users.cc_ids, labels, field_values, component_ids, reporter_id, marked_comment, blocked_on=parsed.blocked_on.iids, blocking=parsed.blocking.iids, attachments=parsed.attachments) self.services.project.UpdateRecentActivity( mr.cnxn, mr.project.project_id) issue = self.services.issue.GetIssueByLocalID( mr.cnxn, mr.project_id, new_local_id) if has_star: self.services.issue_star.SetStar( mr.cnxn, self.services, config, issue.issue_id, reporter_id, True) except tracker_helpers.OverAttachmentQuota: mr.errors.attachments = 'Project attachment quota exceeded.' counts = {actionlimit.ISSUE_COMMENT: 1, actionlimit.ISSUE_ATTACHMENT: len(parsed.attachments)} self.CountRateLimitedActions(mr, counts) if mr.errors.AnyErrors(): component_required = False for wkp in config.templates: if wkp.name == parsed.template_name: component_required = wkp.component_required self.PleaseCorrect( mr, initial_summary=parsed.summary, initial_status=parsed.status, initial_owner=parsed.users.owner_username, initial_cc=', '.join(parsed.users.cc_usernames), initial_components=', '.join(parsed.components.paths), initial_comment=parsed.comment, labels=bounce_labels, fields=bounce_fields, initial_blocked_on=parsed.blocked_on.entered_str, initial_blocking=parsed.blocking.entered_str, component_required=ezt.boolean(component_required)) return # Initial description is comment 0. notify.PrepareAndSendIssueChangeNotification( issue.issue_id, mr.request.host, reporter_id, 0) notify.PrepareAndSendIssueBlockingNotification( issue.issue_id, mr.request.host, parsed.blocked_on.iids, reporter_id) # format a redirect url return framework_helpers.FormatAbsoluteURL( mr, urls.ISSUE_DETAIL, id=new_local_id)
def ProcessAlert(self, cnxn, project, project_addr, from_addr, auth, subject, body, incident_id, owner_email=None, labels=None): """Examine an an alert issue email and create an issue based on the email. Args: cnxn: connection to SQL database. project: Project PB for the project containing the issue. project_addr: string email address the alert email was sent to. from_addr: string email address of the user who sent the alert email to our server. auth: AuthData object with user_id and email address of the user who will file the alert issue. body: string email body text of the reply email. incident_id: string containing an optional unique incident used to de-dupe alert issues. owner_email: string email address of the user the bug will be assigned to. 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. """ # Make sure the email address is whitelisted. if not self.IsWhitelisted(from_addr): logging.info('Unauthorized %s tried to send alert to %s', from_addr, project_addr) return None # Create the actual issue from the email data. # TODO(zhangtiff): Set labels, components, etc based on email content. cc_ids = [] status = 'Available' if not labels: labels = ['Infra-Troopers-Alerts'] labels += ['Restrict-View-Google', 'Pri-2'] field_values = [] # TODO(zhangtiff): Remove this special casing once components can be set via # the email header. if 'codesearch' in body: components = ['Infra>Codesearch'] else: components = ['Infra'] formatted_body = 'Filed by %s on behalf of %s\n\n%s' % ( auth.email, from_addr, body) # Lookup components. config = self.services.config.GetProjectConfig(cnxn, project.project_id) component_ids = tracker_helpers.LookupComponentIDs(components, config) mc = monorailcontext.MonorailContext(self.services, auth=auth, cnxn=cnxn) mc.LookupLoggedInUserPerms(project) with work_env.WorkEnv(mc, self.services) as we: updated_issue = None owner_id = None if owner_email: owner_id = self.services.user.LookupUserID(cnxn, owner_email, autocreate=True) status = 'Assigned' if incident_id: incident_label = 'Incident-Id-' + incident_id labels.append(incident_label) label_id = self.services.config.LookupLabelID( cnxn, project.project_id, incident_label) if label_id: issue_ids = self.services.issue.GetIIDsByLabelIDs( cnxn, [label_id], project.project_id, None) issues, _ = self.services.issue.GetOpenAndClosedIssues( cnxn, issue_ids) latest_issue = None # Find the most recently modified open issue. for issue in issues: if not latest_issue: latest_issue = issue elif issue.modified_timestamp > latest_issue.modified_timestamp: latest_issue = issue if latest_issue: updated_issue = latest_issue # Find all comments on the issue by the current user. comments = self.services.issue.GetComments( cnxn, issue_id=[updated_issue.issue_id], commenter_id=[auth.user_id]) # Timestamp for 24 hours ago in seconds from epoch. yesterday = int(time.time()) - 24 * 60 * 60 for comment in comments: # Stop early if we find a comment created in the last 24 hours. if comment.timestamp > yesterday: logging.info( 'Alert fired again with incident id: %s', incident_id) return None # Add a reply to the existing issue for this incident. self.services.issue.CreateIssueComment( cnxn, updated_issue, auth.user_id, formatted_body) if not updated_issue: updated_issue, _ = we.CreateIssue(project.project_id, subject, status, owner_id, cc_ids, labels, field_values, component_ids, formatted_body) # Update issue using commands. lines = body.strip().split('\n') uia = commitlogcommands.UpdateIssueAction(updated_issue.local_id) commands_found = uia.Parse(cnxn, project.project_name, auth.user_id, lines, self.services, strip_quoted_lines=True) if commands_found: uia.Run(cnxn, self.services, allow_edit=True)