Exemple #1
0
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)
Exemple #2
0
    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))
Exemple #3
0
    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()
Exemple #4
0
    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()
Exemple #5
0
    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))
Exemple #6
0
    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()
Exemple #7
0
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
Exemple #8
0
    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
Exemple #10
0
    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()))
Exemple #11
0
  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)