Commit 9c4d9f98 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch 'resource-iteration-events-rest-endpoint' into 'master'

Add endpoint to access resource iteration events

See merge request gitlab-org/gitlab!40850
parents 16cf0fc9 fba3bc02
......@@ -12,7 +12,8 @@ assignee changes, there will be a corresponding system note).
## Resource events
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/38096) in GitLab 13.3 for state, milestone, and weight events.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/38096) in GitLab 13.3 for state, milestone, and weight events.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40850) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.4 for iteration events.
Some system notes are not part of this API, but are recorded as separate events:
......@@ -20,6 +21,7 @@ Some system notes are not part of this API, but are recorded as separate events:
- [Resource state events](resource_state_events.md)
- [Resource milestone events](resource_milestone_events.md)
- [Resource weight events](resource_weight_events.md) **(STARTER)**
- [Resource iteration events](resource_iteration_events.md) **(STARTER)**
## Notes pagination
......
---
stage: Plan
group: Project Management
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Resource iteration events API **(STARTER)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40850) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.4.
> - It's [deployed behind a feature flag](../user/feature_flags.md), enabled by default.
> - It's enabled on GitLab.com.
> - It's recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-iterations-events-tracking).
NOTE: **Note:**
This feature might not be available to you. Check the **version history** note above for details.
Resource iteration events keep track of what happens to GitLab [issues](../user/project/issues/).
Use them to track which iteration was set, who did it, and when it happened.
## Issues
### List project issue iteration events
Gets a list of all iteration events for a single issue.
```plaintext
GET /projects/:id/issues/:issue_iid/resource_iteration_events
```
| Attribute | Type | Required | Description |
| ----------- | -------------- | -------- | ------------------------------------------------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `issue_iid` | integer | yes | The IID of an issue |
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/issues/11/resource_iteration_events"
```
Example response:
```json
[
{
"id": 142,
"user": {
"id": 1,
"name": "Administrator",
"username": "root",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/root"
},
"created_at": "2018-08-20T13:38:20.077Z",
"resource_type": "Issue",
"resource_id": 253,
"iteration": {
"id": 50,
"iid": 9,
"group_id": 5,
"title": "Iteration I",
"description": "Ipsum Lorem",
"state": 1,
"created_at": "2020-01-27T05:07:12.573Z",
"updated_at": "2020-01-27T05:07:12.573Z",
"due_date": null,
"start_date": null
},
"action": "add"
},
{
"id": 143,
"user": {
"id": 1,
"name": "Administrator",
"username": "root",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/root"
},
"created_at": "2018-08-21T14:38:20.077Z",
"resource_type": "Issue",
"resource_id": 253,
"iteration": {
"id": 53,
"iid": 13,
"group_id": 5,
"title": "Iteration II",
"description": "Ipsum Lorem ipsum",
"state": 2,
"created_at": "2020-01-27T05:07:12.573Z",
"updated_at": "2020-01-27T05:07:12.573Z",
"due_date": null,
"start_date": null
},
"action": "remove"
}
]
```
### Get single issue iteration event
Returns a single iteration event for a specific project issue.
```plaintext
GET /projects/:id/issues/:issue_iid/resource_iteration_events/:resource_iteration_event_id
```
Parameters:
| Attribute | Type | Required | Description |
| ----------------------------- | -------------- | -------- | ------------------------------------------------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path](README.md#namespaced-path-encoding) of the project |
| `issue_iid` | integer | yes | The IID of an issue |
| `resource_iteration_event_id` | integer | yes | The ID of an iteration event |
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/issues/11/resource_iteration_events/143"
```
Example response:
```json
{
"id": 143,
"user": {
"id": 1,
"name": "Administrator",
"username": "root",
"state": "active",
"avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://gitlab.example.com/root"
},
"created_at": "2018-08-21T14:38:20.077Z",
"resource_type": "Issue",
"resource_id": 253,
"iteration": {
"id": 53,
"iid": 13,
"group_id": 5,
"title": "Iteration II",
"description": "Ipsum Lorem ipsum",
"state": 2,
"created_at": "2020-01-27T05:07:12.573Z",
"updated_at": "2020-01-27T05:07:12.573Z",
"due_date": null,
"start_date": null
},
"action": "remove"
}
```
### Enable or disable iterations events tracking **(STARTER)**
Iterations events tracking is under development but ready for production use.
It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
can opt to disable it.
To enable it:
```ruby
Feature.enable(:track_iteration_change_events)
```
To disable it:
```ruby
Feature.disable(:track_iteration_change_events)
```
......@@ -14,6 +14,7 @@ module EE
include Elastic::ApplicationVersionedSearch
include DeprecatedApprovalsBeforeMerge
include UsageStatistics
include IterationEventable
has_many :approvers, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :approver_users, through: :approvers, source: :user
......
......@@ -2,4 +2,6 @@
class ResourceIterationEvent < ResourceTimeboxEvent
belongs_to :iteration
scope :with_api_entity_associations, -> { preload(:iteration, :user) }
end
---
title: Add REST endpoint to access resource iteration events
merge_request: 40850
author:
type: added
# frozen_string_literal: true
module API
class ResourceIterationEvents < Grape::API::Instance
include PaginationParams
helpers ::API::Helpers::NotesHelpers
before { authenticate! }
[Issue].each do |eventable_type|
parent_type = eventable_type.parent_class.to_s.underscore
eventables_str = eventable_type.to_s.underscore.pluralize
params do
requires :id, type: String, desc: "The ID of a #{parent_type}"
end
resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Get a list of #{eventable_type.to_s.downcase} resource iteration events" do
detail 'This feature was introduced in GitLab 13.4'
success EE::API::Entities::ResourceIterationEvent
end
params do
requires :eventable_id, types: [Integer, String], desc: 'The ID of the eventable'
use :pagination
end
get ":id/#{eventables_str}/:eventable_id/resource_iteration_events" do
eventable = find_noteable(eventable_type, params[:eventable_id])
events = eventable.resource_iteration_events.with_api_entity_associations
present paginate(events), with: EE::API::Entities::ResourceIterationEvent
end
desc "Get a single #{eventable_type.to_s.downcase} resource iteration event" do
detail 'This feature was introduced in GitLab 13.4'
success EE::API::Entities::ResourceIterationEvent
end
params do
requires :event_id, type: String, desc: 'The ID of a resource iteration event'
requires :eventable_id, types: [Integer, String], desc: 'The ID of the eventable'
end
get ":id/#{eventables_str}/:eventable_id/resource_iteration_events/:event_id" do
eventable = find_noteable(eventable_type, params[:eventable_id])
event = eventable.resource_iteration_events.find(params[:event_id])
present event, with: EE::API::Entities::ResourceIterationEvent
end
end
end
end
end
......@@ -50,6 +50,7 @@ module EE
mount ::API::Analytics::GroupActivityAnalytics
mount ::API::ProtectedEnvironments
mount ::API::ResourceWeightEvents
mount ::API::ResourceIterationEvents
end
end
end
......
# frozen_string_literal: true
module EE
module API
module Entities
class Iteration < Grape::Entity
expose :id, :iid
expose :project_id, if: -> (entity, options) { entity&.project_id }
expose :group_id, if: -> (entity, options) { entity&.group_id }
expose :title, :description
expose :state_enum, as: :state
expose :created_at, :updated_at
expose :start_date, :due_date
end
end
end
end
# frozen_string_literal: true
module EE
module API
module Entities
class ResourceIterationEvent < Grape::Entity
expose :id
expose :user, using: ::API::Entities::UserBasic
expose :created_at
expose :resource_type do |event, _options|
event.issuable.class.name
end
expose :resource_id do |event, _options|
event.issuable.id
end
expose :iteration, using: Entities::Iteration
expose :action
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::ResourceIterationEvents do
let!(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:project) { create(:project, :public, namespace: group) }
let!(:iteration) { create(:iteration, group: group) }
before do
project.add_developer(user)
end
RSpec.shared_examples 'resource_iteration_events API' do |parent_type, eventable_type, id_name|
describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_iteration_events" do
let!(:event) { create_event(iteration) }
it 'returns an array of resource iteration events' do
url = "/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_iteration_events"
get api(url, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(event.id)
expect(json_response.first['resource_id']).to eq(eventable.id)
expect(json_response.first['iteration']['id']).to eq(event.iteration.id)
expect(json_response.first['action']).to eq(event.action)
end
it 'returns a 404 error when eventable id not found' do
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{non_existing_record_id}/resource_iteration_events", user)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns 404 when not authorized' do
parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
non_member = create(:user)
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_iteration_events", non_member)
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe "GET /#{parent_type}/:id/#{eventable_type}/:noteable_id/resource_iteration_events/:event_id" do
let!(:event) { create_event(iteration) }
it 'returns a resource iteration event by id' do
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_iteration_events/#{event.id}", user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq(event.id)
expect(json_response['iteration']['id']).to eq(event.iteration.id)
expect(json_response['action']).to eq(event.action)
end
it 'returns 404 when not authorized' do
parent.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
non_member = create(:user)
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_iteration_events/#{event.id}", non_member)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns a 404 error if resource iteration event not found' do
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_iteration_events/#{non_existing_record_id}", user)
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe 'pagination' do
let!(:event1) { create_event(iteration) }
let!(:event2) { create_event(iteration) }
it 'returns the second page' do
get api("/#{parent_type}/#{parent.id}/#{eventable_type}/#{eventable[id_name]}/resource_iteration_events?page=2&per_page=1", user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.count).to eq(1)
expect(json_response.first['id']).to eq(event2.id)
end
end
def create_event(iteration, action: :add)
create(:resource_iteration_event, eventable.class.name.underscore => eventable, iteration: iteration, action: action)
end
end
context 'when eventable is an Issue' do
it_behaves_like 'resource_iteration_events API', 'projects', 'issues', 'iid' do
let(:parent) { project }
let(:eventable) { create(:issue, project: project, author: user) }
end
end
end
......@@ -130,6 +130,7 @@ merge_requests:
- resource_label_events
- resource_milestone_events
- resource_state_events
- resource_iteration_events
- label_links
- labels
- last_edited_by
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment