Commit 85fc31b4 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Add REST API for listing iterations

This will be used for the filter bar autocomplete. We do not have
project iterations right now but I created the project iterations
endpoiont for retrieving ancestor iterations just like we do with the
GraphQL API.
parent fff3b3aa
......@@ -38,6 +38,7 @@ The following API resources are available in the project context:
| [Issues Statistics](issues_statistics.md) | `/projects/:id/issues_statistics` (also available for groups and standalone) |
| [Issue boards](boards.md) | `/projects/:id/boards` |
| [Issue links](issue_links.md) **(STARTER)** | `/projects/:id/issues/.../links` |
| [Iterations](iterations.md) **(STARTER)** | `/projects/:id/iterations` (also available for groups) |
| [Jobs](jobs.md) | `/projects/:id/jobs`, `/projects/:id/pipelines/.../jobs` |
| [Labels](labels.md) | `/projects/:id/labels` |
| [Managed licenses](managed_licenses.md) **(ULTIMATE)** | `/projects/:id/managed_licenses` |
......@@ -97,6 +98,7 @@ The following API resources are available in the group context:
| [Groups](groups.md) | `/groups`, `/groups/.../subgroups` |
| [Group badges](group_badges.md) | `/groups/:id/badges` |
| [Group issue boards](group_boards.md) | `/groups/:id/boards` |
| [Group iterations](group_iterations.md) **(STARTER)** | `/groups/:id/iterations` (also available for projects) |
| [Group labels](group_labels.md) | `/groups/:id/labels` |
| [Group-level variables](group_level_variables.md) | `/groups/:id/variables` |
| [Group milestones](group_milestones.md) | `/groups/:id/milestones` |
......
---
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
---
# Group iterations API **(STARTER)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118742) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.5.
This page describes the group iterations API.
There's a separate [project iterations API](./iterations.md) page.
## List group iterations
Returns a list of group iterations.
```plaintext
GET /groups/:id/iterations
GET /groups/:id/iterations?state=opened
GET /groups/:id/iterations?state=closed
GET /groups/:id/iterations?title=1.0
GET /groups/:id/iterations?search=version
```
| Attribute | Type | Required | Description |
| ------------------- | ------- | -------- | ----------- |
| `state` | string | no | Return only `opened`, `upcoming`, `started`, `closed`, or `all` iterations. Defaults to `all`. |
| `search` | string | no | Return only iterations with a title matching the provided string. |
| `include_ancestors` | boolean | no | Include iterations from parent group and its ancestors. Defaults to `true`. |
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/5/iterations"
```
Example response:
```json
[
{
"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": "2020-02-01",
"start_date": "2020-02-14"
}
]
```
......@@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/12819) in GitLab 9.5.
This page describes the group milestones API.
There's a separate [project milestones API](./group_milestones.md) page.
There's a separate [project milestones API](./milestones.md) page.
## List group milestones
......
---
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
---
# Project iterations API **(STARTER)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118742) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.5.
This page describes the project iterations API.
There's a separate [group iterations API](./group_iterations.md) page.
As of GitLab 13.5, we don't have project-level iterations, but you can use this endpoint to fetch the iterations of the project's ancestor groups.
## List project iterations
Returns a list of project iterations.
```plaintext
GET /projects/:id/iterations
GET /projects/:id/iterations?state=opened
GET /projects/:id/iterations?state=closed
GET /projects/:id/iterations?title=1.0
GET /projects/:id/iterations?search=version
```
| Attribute | Type | Required | Description |
| ------------------- | ------- | -------- | ----------- |
| `state` | string | no | Return only `opened`, `upcoming`, `started`, `closed`, or `all` iterations. Defaults to `all`. |
| `search` | string | no | Return only iterations with a title matching the provided string. |
| `include_ancestors` | boolean | no | Include iterations from parent group and its ancestors. Defaults to `true`. |
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/iterations"
```
Example response:
```json
[
{
"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": "2020-02-01",
"start_date": "2020-02-14"
}
]
```
......@@ -15,6 +15,27 @@ class IterationsFinder
attr_reader :params, :current_user
class << self
def params_for_parent(parent, include_ancestors: false)
case parent
when Group
if include_ancestors
{ group_ids: parent.self_and_ancestors.select(:id) }
else
{ group_ids: parent.id }
end
when Project
if include_ancestors && parent.parent_id.present?
{ group_ids: parent.parent.self_and_ancestors.select(:id), project_ids: parent.id }
else
{ project_ids: parent.id }
end
else
raise ArgumentError, 'Invalid parent class. Only Project and Group are supported.'
end
end
end
def initialize(current_user, params = {})
@params = params
@current_user = current_user
......
......@@ -41,36 +41,20 @@ module Resolvers
private
def iterations_finder_params(args)
{
IterationsFinder.params_for_parent(parent, include_ancestors: args[:include_ancestors]).merge!(
id: args[:id],
iid: args[:iid],
state: args[:state] || 'all',
start_date: args[:start_date],
end_date: args[:end_date],
search_title: args[:title]
}.merge(parent_id_parameter(args[:include_ancestors]))
)
end
def parent
@parent ||= object.respond_to?(:sync) ? object.sync : object
end
def parent_id_parameter(include_ancestors)
if parent.is_a?(Group)
if include_ancestors
{ group_ids: parent.self_and_ancestors.select(:id) }
else
{ group_ids: parent.id }
end
elsif parent.is_a?(Project)
if include_ancestors && parent.parent_id.present?
{ group_ids: parent.parent.self_and_ancestors.select(:id), project_ids: parent.id }
else
{ project_ids: parent.id }
end
end
end
def authorize!
Ability.allowed?(context[:current_user], :read_iteration, parent) || raise_resource_not_available_error!
end
......
---
title: Add REST API for listing iterations
merge_request: 44685
author:
type: added
# frozen_string_literal: true
module API
class Iterations < ::API::Base
include PaginationParams
helpers do
params :list_params do
optional :state, type: String, values: %w[opened upcoming started closed all], default: 'all',
desc: 'Return "opened", "upcoming", "started", "closed", or "all" milestones'
optional :search, type: String, desc: 'The search criteria for the title of the iteration'
optional :include_ancestors, type: Grape::API::Boolean, default: true,
desc: 'Include iterations from parent and its ancestors'
use :pagination
end
def list_iterations_for(parent)
iterations = IterationsFinder.new(current_user, iterations_finder_params(parent)).execute
present paginate(iterations), with: EE::API::Entities::Iteration
end
def iterations_finder_params(parent)
IterationsFinder.params_for_parent(parent, include_ancestors: params[:include_ancestors]).merge!(
state: params[:state],
search_title: params[:search]
)
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of project iterations' do
detail 'This feature was introduced in GitLab 13.5'
success EE::API::Entities::Iteration
end
params do
use :list_params
end
get ":id/iterations" do
authorize! :read_iteration, user_project
list_iterations_for(user_project)
end
end
params do
requires :id, type: String, desc: 'The ID of a group'
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of group iterations' do
detail 'This feature was introduced in GitLab 13.5'
success EE::API::Entities::Iteration
end
params do
use :list_params
end
get ":id/iterations" do
authorize! :read_iteration, user_group
list_iterations_for(user_group)
end
end
end
end
......@@ -47,6 +47,7 @@ module EE
mount ::API::ProtectedEnvironments
mount ::API::ResourceWeightEvents
mount ::API::ResourceIterationEvents
mount ::API::Iterations
end
end
end
......
......@@ -135,9 +135,9 @@ module EE
end
def find_iterations(project, params = {})
group_ids = project.group.self_and_ancestors.map(&:id) if project.group
parent_params = ::IterationsFinder.params_for_parent(project, include_ancestors: true)
::IterationsFinder.new(current_user, params.merge(project_ids: [project.id], group_ids: group_ids)).execute
::IterationsFinder.new(current_user, params.merge(parent_params)).execute
end
desc _('Publish to status page')
......
......@@ -159,5 +159,59 @@ RSpec.describe IterationsFinder do
expect(finder.find_by(iid: iteration_from_project_1.iid)).to eq(iteration_from_project_1)
end
end
describe '.params_for_parent' do
let_it_be(:parent_group) { create(:group) }
let_it_be(:group) { create(:group, parent: parent_group) }
let_it_be(:project) { create(:project, group: group) }
context 'when parent is a project' do
subject { described_class.params_for_parent(project, include_ancestors: include_ancestors) }
context 'when include_ancestors is true' do
let(:include_ancestors) { true }
it 'returns project and ancestor group ids' do
expect(subject).to match(group_ids: contain_exactly(group, parent_group), project_ids: project.id)
end
end
context 'when include_ancestors is false' do
let(:include_ancestors) { false }
it 'returns project id' do
expect(subject).to eq(project_ids: project.id)
end
end
end
context 'when parent is a group' do
subject { described_class.params_for_parent(group, include_ancestors: include_ancestors) }
context 'when include_ancestors is true' do
let(:include_ancestors) { true }
it 'returns group and ancestor ids' do
expect(subject).to match(group_ids: contain_exactly(group, parent_group))
end
end
context 'when include_ancestors is false' do
let(:include_ancestors) { false }
it 'returns group id' do
expect(subject).to eq(group_ids: group.id)
end
end
end
context 'when parent is invalid' do
subject { described_class.params_for_parent(double(User)) }
it 'raises an ArgumentError' do
expect { subject }.to raise_error(ArgumentError, 'Invalid parent class. Only Project and Group are supported.')
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::Iterations do
let_it_be(:user) { create(:user) }
let_it_be(:parent_group) { create(:group, :private) }
let_it_be(:group) { create(:group, :private, parent: parent_group) }
let_it_be(:iteration) { create(:iteration, group: group, title: 'search_title') }
let_it_be(:closed_iteration) { create(:iteration, :closed, group: group) }
let_it_be(:ancestor_iteration) { create(:iteration, group: parent_group) }
before_all do
parent_group.add_guest(user)
end
shared_examples 'iterations list' do
context 'when user does not have access' do
it 'returns 404' do
get api(api_path, nil)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when user has access' do
it 'returns a list of iterations' do
get api(api_path, user)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(3)
expect(json_response.map { |i| i['id'] }).to contain_exactly(iteration.id, closed_iteration.id, ancestor_iteration.id)
end
it 'returns iterations filtered by state' do
get api(api_path, user), params: { state: 'closed' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(1)
expect(json_response.first['id']).to eq(closed_iteration.id)
end
it 'returns iterations filtered by title' do
get api(api_path, user), params: { search: 'search_' }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(1)
expect(json_response.first['id']).to eq(iteration.id)
end
it 'returns 400 when param is invalid' do
get api(api_path, user), params: { state: 'non-existent-state' }
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
describe 'GET /groups/:id/iterations' do
let(:api_path) { "/groups/#{group.id}/iterations" }
it_behaves_like 'iterations list'
it 'excludes ancestor iterations when include_ancestors is set to false' do
get api(api_path, user), params: { include_ancestors: false }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(2)
expect(json_response.map { |i| i['id'] }).to contain_exactly(iteration.id, closed_iteration.id)
end
end
describe 'GET /projects/:id/iterations' do
let_it_be(:project) { create(:project, :private, group: group) }
let(:api_path) { "/projects/#{project.id}/iterations" }
it_behaves_like 'iterations list'
it 'excludes ancestor iterations when include_ancestors is set to false' do
get api(api_path, user), params: { include_ancestors: false }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(0)
end
end
end
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