Ejemplo n.º 1
0
  def issues_insert(self, request):
    """Add a new issue."""
    mar = self.mar_factory(request)
    if not mar.perms.CanUsePerm(
        permissions.CREATE_ISSUE, mar.auth.effective_ids, mar.project, []):
      raise permissions.PermissionException(
          'The requester %s is not allowed to create issues for project %s.' %
          (mar.auth.email, mar.project_name))

    owner_id = None
    if request.owner:
      try:
        owner_id = self._services.user.LookupUserID(
            mar.cnxn, request.owner.name)
      except user_svc.NoSuchUserException:
        raise endpoints.BadRequestException(
            'The specified owner %s does not exist.' % request.owner.name)

    cc_ids = []
    if request.cc:
      cc_ids = self._services.user.LookupUserIDs(
          mar.cnxn, [ap.name for ap in request.cc],
          autocreate=True).values()
    comp_ids = api_pb2_v1_helpers.convert_component_ids(
        mar.config, request.components)
    fields_add, _, _, fields_labels, _ = (
        api_pb2_v1_helpers.convert_field_values(
            request.fieldValues, mar, self._services))
    field_helpers.ValidateCustomFields(
        mar, self._services, fields_add, mar.config, mar.errors)
    if mar.errors.AnyErrors():
      raise endpoints.BadRequestException(
          'Invalid field values: %s' % mar.errors.custom_fields)

    local_id = self._services.issue.CreateIssue(
        mar.cnxn, self._services, mar.project_id,
        request.summary, request.status, owner_id,
        cc_ids, request.labels + fields_labels, fields_add,
        comp_ids, mar.auth.user_id, request.description,
        blocked_on=api_pb2_v1_helpers.convert_issueref_pbs(
            request.blockedOn, mar, self._services),
        blocking=api_pb2_v1_helpers.convert_issueref_pbs(
            request.blocking, mar, self._services))
    new_issue = self._services.issue.GetIssueByLocalID(
        mar.cnxn, mar.project_id, local_id)

    self._services.issue_star.SetStar(
        mar.cnxn, self._services, mar.config, new_issue.issue_id,
        mar.auth.user_id, True)

    if request.sendEmail:
      notify.PrepareAndSendIssueChangeNotification(
          new_issue.issue_id, framework_helpers.GetHostPort(),
          new_issue.reporter_id, 0)

    return api_pb2_v1_helpers.convert_issue(
        api_pb2_v1.IssuesGetInsertResponse, new_issue, mar, self._services)
Ejemplo n.º 2
0
    def testPrepareAndSendIssueChangeNotification(self):
        notify.PrepareAndSendIssueChangeNotification(
            issue_id=78901,
            hostport='testbed-test.appspotmail.com',
            commenter_id=1,
            seq_num=0,
            old_owner_id=2,
            send_email=True)

        tasks = self.taskqueue_stub.get_filtered_tasks(
            url=urls.NOTIFY_ISSUE_CHANGE_TASK + '.do')
        self.assertEqual(1, len(tasks))
Ejemplo n.º 3
0
    def setupAndCallRun(self, allow_edit):
        comments = ['comment 1', 'comment 2', 'comment 3']

        self.mox.StubOutWithMock(self.services.issue, 'GetCommentsForIssue')
        self.services.issue.GetCommentsForIssue(
            self.cnxn, self.issue.issue_id).AndReturn(comments)
        self.mox.StubOutWithMock(notify,
                                 'PrepareAndSendIssueChangeNotification')
        notify.PrepareAndSendIssueChangeNotification(
            self.issue.issue_id,
            80,
            101,
            len(comments) - 1,
            old_owner_id=self.issue.owner_id)
        self.mox.ReplayAll()

        self.uia.Parse(
            self.cnxn,
            self.project.project_name,
            101, ['summary:something', 'status:New', '> line 1', '> line 2'],
            self.services,
            hostport=80)
        self.uia.Run(self.cnxn, self.services, allow_edit=allow_edit)
        self.mox.VerifyAll()
Ejemplo n.º 4
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)
Ejemplo n.º 5
0
  def issues_comments_insert(self, request):
    """Add a comment."""
    mar = self.mar_factory(request)
    issue = self._services.issue.GetIssueByLocalID(
        mar.cnxn, mar.project_id, request.issueId)
    old_owner_id = tracker_bizobj.GetOwnerId(issue)
    if not permissions.CanCommentIssue(
        mar.auth.effective_ids, mar.perms, mar.project, issue,
        mar.granted_perms):
      raise permissions.PermissionException(
          'User is not allowed to comment this issue (%s, %d)' %
          (request.projectId, request.issueId))

    updates_dict = {}
    if request.updates:
      if request.updates.moveToProject:
        move_to = request.updates.moveToProject.lower()
        move_to_project = issuedetail.CheckMoveIssueRequest(
            self._services, mar, issue, True, move_to, mar.errors)
        if mar.errors.AnyErrors():
          raise endpoints.BadRequestException(mar.errors.move_to)
        updates_dict['move_to_project'] = move_to_project

      updates_dict['summary'] = request.updates.summary
      updates_dict['status'] = request.updates.status
      if request.updates.owner:
        if request.updates.owner == framework_constants.NO_USER_NAME:
          updates_dict['owner'] = framework_constants.NO_USER_SPECIFIED
        else:
          updates_dict['owner'] = self._services.user.LookupUserID(
              mar.cnxn, request.updates.owner)
      updates_dict['cc_add'], updates_dict['cc_remove'] = (
          api_pb2_v1_helpers.split_remove_add(request.updates.cc))
      updates_dict['cc_add'] = self._services.user.LookupUserIDs(
          mar.cnxn, updates_dict['cc_add'], autocreate=True).values()
      updates_dict['cc_remove'] = self._services.user.LookupUserIDs(
          mar.cnxn, updates_dict['cc_remove']).values()
      updates_dict['labels_add'], updates_dict['labels_remove'] = (
          api_pb2_v1_helpers.split_remove_add(request.updates.labels))
      blocked_on_add_strs, blocked_on_remove_strs = (
          api_pb2_v1_helpers.split_remove_add(request.updates.blockedOn))
      updates_dict['blocked_on_add'] = api_pb2_v1_helpers.issue_global_ids(
          blocked_on_add_strs, issue.project_id, mar,
          self._services)
      updates_dict['blocked_on_remove'] = api_pb2_v1_helpers.issue_global_ids(
          blocked_on_remove_strs, issue.project_id, mar,
          self._services)
      blocking_add_strs, blocking_remove_strs = (
          api_pb2_v1_helpers.split_remove_add(request.updates.blocking))
      updates_dict['blocking_add'] = api_pb2_v1_helpers.issue_global_ids(
          blocking_add_strs, issue.project_id, mar,
          self._services)
      updates_dict['blocking_remove'] = api_pb2_v1_helpers.issue_global_ids(
          blocking_remove_strs, issue.project_id, mar,
          self._services)
      components_add_strs, components_remove_strs = (
          api_pb2_v1_helpers.split_remove_add(request.updates.components))
      updates_dict['components_add'] = (
          api_pb2_v1_helpers.convert_component_ids(
              mar.config, components_add_strs))
      updates_dict['components_remove'] = (
          api_pb2_v1_helpers.convert_component_ids(
              mar.config, components_remove_strs))
      if request.updates.mergedInto:
        merge_project_name, merge_local_id = tracker_bizobj.ParseIssueRef(
            request.updates.mergedInto)
        merge_into_project = self._services.project.GetProjectByName(
            mar.cnxn, merge_project_name or issue.project_name)
        merge_into_issue = self._services.issue.GetIssueByLocalID(
            mar.cnxn, merge_into_project.project_id, merge_local_id)
        merge_allowed = tracker_helpers.IsMergeAllowed(
            merge_into_issue, mar, self._services)
        if not merge_allowed:
          raise permissions.PermissionException(
            'User is not allowed to merge into issue %s:%s' %
            (merge_into_issue.project_name, merge_into_issue.local_id))
        updates_dict['merged_into'] = merge_into_issue.issue_id
      (updates_dict['field_vals_add'], updates_dict['field_vals_remove'],
       updates_dict['fields_clear'], updates_dict['fields_labels_add'],
       updates_dict['fields_labels_remove']) = (
          api_pb2_v1_helpers.convert_field_values(
              request.updates.fieldValues, mar, self._services))

    field_helpers.ValidateCustomFields(
        mar, self._services,
        (updates_dict.get('field_vals_add', []) +
         updates_dict.get('field_vals_remove', [])),
        mar.config, mar.errors)
    if mar.errors.AnyErrors():
      raise endpoints.BadRequestException(
          'Invalid field values: %s' % mar.errors.custom_fields)

    _, comment = self._services.issue.DeltaUpdateIssue(
        cnxn=mar.cnxn, services=self._services,
        reporter_id=mar.auth.user_id,
        project_id=mar.project_id, config=mar.config, issue=issue,
        status=updates_dict.get('status'), owner_id=updates_dict.get('owner'),
        cc_add=updates_dict.get('cc_add', []),
        cc_remove=updates_dict.get('cc_remove', []),
        comp_ids_add=updates_dict.get('components_add', []),
        comp_ids_remove=updates_dict.get('components_remove', []),
        labels_add=(updates_dict.get('labels_add', []) +
                    updates_dict.get('fields_labels_add', [])),
        labels_remove=(updates_dict.get('labels_remove', []) +
                       updates_dict.get('fields_labels_remove', [])),
        field_vals_add=updates_dict.get('field_vals_add', []),
        field_vals_remove=updates_dict.get('field_vals_remove', []),
        fields_clear=updates_dict.get('fields_clear', []),
        blocked_on_add=updates_dict.get('blocked_on_add', []),
        blocked_on_remove=updates_dict.get('blocked_on_remove', []),
        blocking_add=updates_dict.get('blocking_add', []),
        blocking_remove=updates_dict.get('blocking_remove', []),
        merged_into=updates_dict.get('merged_into'),
        index_now=False,
        comment=request.content,
        summary=updates_dict.get('summary'),
    )

    move_comment = None
    if 'move_to_project' in updates_dict:
      move_to_project = updates_dict['move_to_project']
      old_text_ref = 'issue %s:%s' % (issue.project_name, issue.local_id)
      tracker_fulltext.UnindexIssues([issue.issue_id])
      moved_back_iids = self._services.issue.MoveIssues(
          mar.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)
      move_comment = self._services.issue.CreateIssueComment(
        mar.cnxn, move_to_project.project_id, issue.local_id, mar.auth.user_id,
        content, amendments=[
            tracker_bizobj.MakeProjectAmendment(move_to_project.project_name)])

    if 'merged_into' in updates_dict:
      new_starrers = tracker_helpers.GetNewIssueStarrers(
          mar.cnxn, self._services, issue.issue_id, merge_into_issue.issue_id)
      tracker_helpers.AddIssueStarrers(
          mar.cnxn, self._services, mar,
          merge_into_issue.issue_id, merge_into_project, new_starrers)
      _merge_comment = tracker_helpers.MergeCCsAndAddComment(
        self._services, mar, issue, merge_into_project, merge_into_issue)
      merge_into_issue_cmnts = self._services.issue.GetCommentsForIssue(
          mar.cnxn, merge_into_issue.issue_id)
      notify.PrepareAndSendIssueChangeNotification(
          merge_into_issue.issue_id, framework_helpers.GetHostPort(),
          mar.auth.user_id, len(merge_into_issue_cmnts) - 1, send_email=True)

    tracker_fulltext.IndexIssues(
        mar.cnxn, [issue], self._services.user, self._services.issue,
        self._services.config)

    comment = comment or move_comment
    if comment is None:
      return api_pb2_v1.IssuesCommentsInsertResponse()

    cmnts = self._services.issue.GetCommentsForIssue(mar.cnxn, issue.issue_id)
    seq = len(cmnts) - 1

    if request.sendEmail:
      notify.PrepareAndSendIssueChangeNotification(
          issue.issue_id, framework_helpers.GetHostPort(),
          comment.user_id, seq, send_email=True, old_owner_id=old_owner_id)

    can_delete = permissions.CanDelete(
      mar.auth.user_id, mar.auth.effective_ids, mar.perms,
      comment.deleted_by, comment.user_id, mar.project,
      permissions.GetRestrictions(issue), granted_perms=mar.granted_perms)
    return api_pb2_v1.IssuesCommentsInsertResponse(
        id=seq,
        kind='monorail#issueComment',
        author=api_pb2_v1_helpers.convert_person(
            comment.user_id, mar.cnxn, self._services),
        content=comment.content,
        published=datetime.datetime.fromtimestamp(comment.timestamp),
        updates=api_pb2_v1_helpers.convert_amendments(
            issue, comment.amendments, mar, self._services),
        canDelete=can_delete)
Ejemplo n.º 6
0
    def Run(self, cnxn, services, allow_edit=True):
        """Updates an issue based on the parsed commands."""
        try:
            issue = services.issue.GetIssueByLocalID(cnxn,
                                                     self.project.project_id,
                                                     self.local_id)
        except issue_svc.NoSuchIssueException:
            return  # Issue does not exist, so do nothing

        old_owner_id = issue.owner_id
        new_summary = self.parser.summary or issue.summary

        if self.parser.status is None:
            new_status = issue.status
        else:
            new_status = self.parser.status

        if self.parser.owner_id is None:
            new_owner_id = issue.owner_id
        else:
            new_owner_id = self.parser.owner_id

        new_cc_ids = [
            cc for cc in list(issue.cc_ids) + list(self.parser.cc_add)
            if cc not in self.parser.cc_remove
        ]
        (new_labels, _update_add,
         _update_remove) = framework_bizobj.MergeLabels(
             issue.labels, self.parser.labels_add, self.parser.labels_remove,
             self.config.exclusive_label_prefixes)

        new_field_values = issue.field_values  # TODO(jrobbins): edit custom ones

        if not allow_edit:
            # If user can't edit, then only consider the plain-text comment,
            # and set all other fields back to their original values.
            logging.info('Processed reply from user who can not edit issue')
            new_summary = issue.summary
            new_status = issue.status
            new_owner_id = issue.owner_id
            new_cc_ids = issue.cc_ids
            new_labels = issue.labels
            new_field_values = issue.field_values

        amendments, _comment_pb = services.issue.ApplyIssueComment(
            cnxn,
            services,
            self.commenter_id,
            self.project.project_id,
            issue.local_id,
            new_summary,
            new_status,
            new_owner_id,
            new_cc_ids,
            new_labels,
            new_field_values,
            issue.component_ids,
            issue.blocked_on_iids,
            issue.blocking_iids,
            issue.dangling_blocked_on_refs,
            issue.dangling_blocking_refs,
            issue.merged_into,
            comment=self.description,
            inbound_message=self.inbound_message)

        logging.info('Updated issue %s:%s w/ amendments %r',
                     self.project.project_name, issue.local_id, amendments)

        if amendments or self.description:  # Avoid completely empty comments.
            cmnts = services.issue.GetCommentsForIssue(cnxn, issue.issue_id)
            notify.PrepareAndSendIssueChangeNotification(
                issue.issue_id,
                self.hostport,
                self.commenter_id,
                len(cmnts) - 1,
                old_owner_id=old_owner_id)
Ejemplo n.º 7
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, or None if response was already sent.
    """
    cmd = post_data.get('cmd', '')
    send_email = 'send_email' in post_data
    comment = post_data.get('comment', '')
    slot_used = int(post_data.get('slot_used', 1))
    page_generation_time = long(post_data['pagegen'])
    issue = self._GetIssue(mr)
    old_owner_id = tracker_bizobj.GetOwnerId(issue)
    config = self.services.config.GetProjectConfig(mr.cnxn, mr.project_id)

    summary, status, owner_id, cc_ids, labels = commands.ParseQuickEditCommand(
        mr.cnxn, cmd, issue, config, mr.auth.user_id, self.services)
    component_ids = issue.component_ids  # TODO(jrobbins): component commands
    field_values = issue.field_values  # TODO(jrobbins): edit custom fields

    permit_edit = permissions.CanEditIssue(
        mr.auth.effective_ids, mr.perms, mr.project, issue)
    if not permit_edit:
      raise permissions.PermissionException(
          'User is not allowed to edit this issue')

    amendments, _ = self.services.issue.ApplyIssueComment(
        mr.cnxn, self.services, mr.auth.user_id,
        mr.project_id, mr.local_id, summary, status, owner_id, cc_ids,
        labels, field_values, component_ids, issue.blocked_on_iids,
        issue.blocking_iids, issue.dangling_blocked_on_refs,
        issue.dangling_blocking_refs, issue.merged_into,
        page_gen_ts=page_generation_time, comment=comment)
    self.services.project.UpdateRecentActivity(
        mr.cnxn, mr.project.project_id)

    if send_email:
      if amendments or comment.strip():
        cmnts = self.services.issue.GetCommentsForIssue(
            mr.cnxn, issue.issue_id)
        notify.PrepareAndSendIssueChangeNotification(
            issue.issue_id, mr.request.host, mr.auth.user_id, len(cmnts) - 1,
            send_email=send_email, old_owner_id=old_owner_id)

    # TODO(jrobbins): allow issue merge via quick-edit.

    self.services.features.StoreRecentCommand(
        mr.cnxn, mr.auth.user_id, mr.project_id, slot_used, cmd, comment)

    # TODO(jrobbins): this is very similar to a block of code in issuebulkedit.
    mr.can = int(post_data['can'])
    mr.query = post_data.get('q', '')
    mr.col_spec = post_data.get('colspec', '')
    mr.sort_spec = post_data.get('sort', '')
    mr.group_by_spec = post_data.get('groupby', '')
    mr.start = int(post_data['start'])
    mr.num = int(post_data['num'])
    preview_issue_ref_str = '%s:%d' % (issue.project_name, issue.local_id)
    return tracker_helpers.FormatIssueListURL(
        mr, config, preview=preview_issue_ref_str, updated=mr.local_id,
        ts=int(time.time()))