Ejemplo n.º 1
0
 def testShiftEnumFieldsIntoLabels_FoundSomeEnumFields(self):
     self.config.field_defs.append(
         tracker_bizobj.MakeFieldDef(
             123, 789, 'Component', tracker_pb2.FieldTypes.ENUM_TYPE, None,
             '', False, False, False, None, None, '', False, '', '',
             tracker_pb2.NotifyTriggers.NEVER, 'no_action',
             'What HW part is affected?', False))
     self.config.field_defs.append(
         tracker_bizobj.MakeFieldDef(
             234, 789, 'Size', tracker_pb2.FieldTypes.ENUM_TYPE, None, '',
             False, False, False, None, None, '', False, '', '',
             tracker_pb2.NotifyTriggers.NEVER, 'no_action',
             'How big is this work item?', False))
     labels = ['Security', 'Performance', 'Pri-1', 'M-2']
     labels_remove = ['ReleaseBlock']
     field_val_strs = {123: ['CPU']}
     field_val_strs_remove = {234: ['Small']}
     field_helpers.ShiftEnumFieldsIntoLabels(labels, labels_remove,
                                             field_val_strs,
                                             field_val_strs_remove,
                                             self.config)
     self.assertEqual(
         ['Security', 'Performance', 'Pri-1', 'M-2', 'Component-CPU'],
         labels)
     self.assertEqual(['ReleaseBlock', 'Size-Small'], labels_remove)
     self.assertEqual({}, field_val_strs)
     self.assertEqual({}, field_val_strs_remove)
Ejemplo n.º 2
0
 def testShiftEnumFieldsIntoLabels_NoOp(self):
     labels = ['Security', 'Performance', 'Pri-1', 'M-2']
     labels_remove = ['ReleaseBlock']
     field_val_strs = {123: ['CPU']}
     field_val_strs_remove = {234: ['Small']}
     field_helpers.ShiftEnumFieldsIntoLabels(labels, labels_remove,
                                             field_val_strs,
                                             field_val_strs_remove,
                                             self.config)
     self.assertEqual(['Security', 'Performance', 'Pri-1', 'M-2'], labels)
     self.assertEqual(['ReleaseBlock'], labels_remove)
     self.assertEqual({123: ['CPU']}, field_val_strs)
     self.assertEqual({234: ['Small']}, field_val_strs_remove)
Ejemplo n.º 3
0
 def testShiftEnumFieldsIntoLabels_Empty(self):
     labels = []
     labels_remove = []
     field_val_strs = {}
     field_val_strs_remove = {}
     field_helpers.ShiftEnumFieldsIntoLabels(labels, labels_remove,
                                             field_val_strs,
                                             field_val_strs_remove,
                                             self.config)
     self.assertEqual([], labels)
     self.assertEqual([], labels_remove)
     self.assertEqual({}, field_val_strs)
     self.assertEqual({}, field_val_strs_remove)
Ejemplo n.º 4
0
def _RedistributeEnumFieldsIntoLabels(labels_add, labels_remove,
                                      field_vals_add, field_vals_remove,
                                      config):
    """Look at the custom field values and treat enum fields as labels.

  Args:
    labels_add: list of labels to add/set on the issue.
    labels_remove: list of labels to remove from the issue.
    field_val_add: list of protoc FieldValues to be added.
    field_val_remove: list of protoc FieldValues to be removed.
        remove.
    config: ProjectIssueConfig PB including custom field definitions.

  Returns:
    Two revised lists of protoc FieldValues to be added and removed,
      without enum_types.

  SIDE-EFFECT: the labels and labels_remove lists will be extended with
  key-value labels corresponding to the enum field values.  Those field
  entries will be removed from field_vals and field_vals_remove.
  """
    field_val_strs_add = {}
    for field_val in field_vals_add:
        field_val_strs_add.setdefault(field_val.field_ref.field_id,
                                      []).append(field_val.value)

    field_val_strs_remove = {}
    for field_val in field_vals_remove:
        field_val_strs_remove.setdefault(field_val.field_ref.field_id,
                                         []).append(field_val.value)

    field_helpers.ShiftEnumFieldsIntoLabels(labels_add, labels_remove,
                                            field_val_strs_add,
                                            field_val_strs_remove, config)

    # Filter out the fields that were shifted into labels
    updated_field_vals_add = [
        fv for fv in field_vals_add
        if fv.field_ref.field_id in field_val_strs_add
    ]
    updated_field_vals_remove = [
        fv for fv in field_vals_remove
        if fv.field_ref.field_id in field_val_strs_remove
    ]

    return updated_field_vals_add, updated_field_vals_remove
Ejemplo n.º 5
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)
Ejemplo n.º 6
0
    def ProcessFormData(self, mr, post_data):
        """Validate and store the contents of the issues tracker admin page.

    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.
    """

        config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)
        parsed = template_helpers.ParseTemplateRequest(post_data, config)
        field_helpers.ShiftEnumFieldsIntoLabels(parsed.labels, [],
                                                parsed.field_val_strs, [],
                                                config)
        template = self.services.template.GetTemplateByName(
            mr.cnxn, parsed.name, mr.project_id)
        allow_edit = permissions.CanEditTemplate(mr.auth.effective_ids,
                                                 mr.perms, mr.project,
                                                 template)
        if not allow_edit:
            raise permissions.PermissionException(
                'User is not allowed edit this issue template.')

        if 'deletetemplate' in post_data:
            self.services.template.DeleteIssueTemplateDef(
                mr.cnxn, mr.project_id, template.template_id)
            return framework_helpers.FormatAbsoluteURL(mr,
                                                       urls.ADMIN_TEMPLATES,
                                                       deleted=1,
                                                       ts=int(time.time()))

        (admin_ids, owner_id, component_ids, field_values, phases,
         approvals) = template_helpers.GetTemplateInfoFromParsed(
             mr, self.services, parsed, config)

        if mr.errors.AnyErrors():
            field_views = tracker_views.MakeAllFieldValueViews(
                config, [], [], field_values, {})

            prechecked_approvals = template_helpers.GetCheckedApprovalsFromParsed(
                parsed.approvals_to_phase_idx)

            self.PleaseCorrect(
                mr,
                initial_members_only=ezt.boolean(parsed.members_only),
                template_name=parsed.name,
                initial_summary=parsed.summary,
                initial_must_edit_summary=ezt.boolean(
                    parsed.summary_must_be_edited),
                initial_content=parsed.content,
                initial_status=parsed.status,
                initial_owner=parsed.owner_str,
                initial_owner_defaults_to_member=ezt.boolean(
                    parsed.owner_defaults_to_member),
                initial_components=', '.join(parsed.component_paths),
                initial_component_required=ezt.boolean(
                    parsed.component_required),
                initial_admins=parsed.admin_str,
                labels=parsed.labels,
                fields=[
                    view for view in field_views
                    if view.field_def.type_name is not 'APPROVAL_TYPE'
                ],
                initial_add_approvals=ezt.boolean(parsed.add_approvals),
                initial_phases=[
                    tracker_pb2.Phase(name=name) for name in parsed.phase_names
                ],
                approvals=[
                    view for view in field_views
                    if view.field_def.type_name is 'APPROVAL_TYPE'
                ],
                prechecked_approvals=prechecked_approvals,
                required_approval_ids=parsed.required_approval_ids)
            return

        labels = [label for label in parsed.labels if label]
        self.services.template.UpdateIssueTemplateDef(
            mr.cnxn,
            mr.project_id,
            template.template_id,
            name=parsed.name,
            content=parsed.content,
            summary=parsed.summary,
            summary_must_be_edited=parsed.summary_must_be_edited,
            status=parsed.status,
            members_only=parsed.members_only,
            owner_defaults_to_member=parsed.owner_defaults_to_member,
            component_required=parsed.component_required,
            owner_id=owner_id,
            labels=labels,
            component_ids=component_ids,
            admin_ids=admin_ids,
            field_values=field_values,
            phases=phases,
            approval_values=approvals)

        return framework_helpers.FormatAbsoluteURL(mr,
                                                   urls.TEMPLATE_DETAIL,
                                                   template=template.name,
                                                   saved=1,
                                                   ts=int(time.time()))
Ejemplo n.º 7
0
    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
Ejemplo n.º 8
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()))
Ejemplo n.º 9
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)