Commit fba3bc02 authored by Alexandru Croitor's avatar Alexandru Croitor

Add resource iteration events REST endpoint

As we are tracing iteration events the system notes are synthetically
generated out of the events, so we expose a way to get the events
through API.
parent ea9078dd
......@@ -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