Commit a92f699c authored by Felipe Artur's avatar Felipe Artur

Add group boards API endpoint

parent bf6149a5
class LabelsFinder < UnionFinder
include Gitlab::Utils::StrongMemoize
def initialize(current_user, params = {})
@current_user = current_user
@params = params
......@@ -32,6 +34,8 @@ class LabelsFinder < UnionFinder
label_ids << project.labels
end
end
elsif only_group_labels?
label_ids << Label.where(group_id: group.id)
else
label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id))
......@@ -51,6 +55,13 @@ class LabelsFinder < UnionFinder
items.where(title: title)
end
def group
strong_memoize(:group) do
group = Group.find(params[:group_id])
authorized_to_read_labels?(group) && group
end
end
def group?
params[:group_id].present?
end
......@@ -63,6 +74,10 @@ class LabelsFinder < UnionFinder
params[:project_ids].present?
end
def only_group_labels?
params[:only_group_labels]
end
def title
params[:title] || params[:name]
end
......@@ -96,9 +111,9 @@ class LabelsFinder < UnionFinder
@projects
end
def authorized_to_read_labels?(project)
def authorized_to_read_labels?(label_parent)
return true if skip_authorization
Ability.allowed?(current_user, :read_label, project)
Ability.allowed?(current_user, :read_label, label_parent)
end
end
......@@ -33,6 +33,7 @@ class GroupPolicy < BasePolicy
rule { public_group }.policy do
enable :read_group
enable :read_list
enable :read_label
end
rule { logged_in_viewable }.enable :read_group
......@@ -41,6 +42,7 @@ class GroupPolicy < BasePolicy
enable :read_group
enable :read_list
enable :upload_file
enable :read_label
end
rule { admin } .enable :read_group
......
---
title: Add group boards API endpoint
merge_request:
author:
type: added
......@@ -28,6 +28,7 @@ following locations:
- [Group Members](members.md)
- [Issues](issues.md)
- [Issue Boards](boards.md)
- **(EEP)** [Group Issue Boards] (group_boards.md)
- [Jobs](jobs.md)
- [Keys](keys.md)
- [Labels](labels.md)
......
......@@ -15,10 +15,10 @@ GET /projects/:id/boards
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/boards
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards
```
Example response:
......@@ -28,7 +28,7 @@ Example response:
{
"id" : 1,
"project": {
"id": 3,
"id": 5,
"name": "Diaspora Project Site",
"name_with_namespace": "Diaspora / Diaspora Project Site",
"path": "diaspora-project-site",
......@@ -73,6 +73,159 @@ Example response:
]
```
## Single board
Get a single board.
```
GET /projects/:id/boards/:board_id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1
```
Example response:
```json
{
"id": 1,
"name:": "project issue board",
"project": {
"id": 5,
"name": "Diaspora Project Site",
"name_with_namespace": "Diaspora / Diaspora Project Site",
"path": "diaspora-project-site",
"path_with_namespace": "diaspora/diaspora-project-site",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site"
},
"milestone": {
"id": 12
"title": "10.0"
},
"lists" : [
{
"id" : 1,
"label" : {
"name" : "Testing",
"color" : "#F0AD4E",
"description" : null
},
"position" : 1
},
{
"id" : 2,
"label" : {
"name" : "Ready",
"color" : "#FF0000",
"description" : null
},
"position" : 2
},
{
"id" : 3,
"label" : {
"name" : "Production",
"color" : "#FF5F00",
"description" : null
},
"position" : 3
}
]
}
```
## Create a board (EES-Only)
Creates a board.
```
POST /projects/:id/boards
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the new board |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards?name=newboard
```
Example response:
```json
{
"id": 1,
"project": {
"id": 5,
"name": "Diaspora Project Site",
"name_with_namespace": "Diaspora / Diaspora Project Site",
"path": "diaspora-project-site",
"path_with_namespace": "diaspora/diaspora-project-site",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site"
},
"name": "newboard",
"milestone": {
"id": 12
"title": "10.0"
},
"lists" : [
{
"id" : 1,
"label" : {
"name" : "Testing",
"color" : "#F0AD4E",
"description" : null
},
"position" : 1
},
{
"id" : 2,
"label" : {
"name" : "Ready",
"color" : "#FF0000",
"description" : null
},
"position" : 2
},
{
"id" : 3,
"label" : {
"name" : "Production",
"color" : "#FF5F00",
"description" : null
},
"position" : 3
}
]
}
```
## Delete a board (EES-Only)
Deletes a board.
```
DELETE /projects/:id/boards/:board_id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1
```
## List board lists
Get a list of the board's lists.
......@@ -84,8 +237,8 @@ GET /projects/:id/boards/:board_id/lists
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists
......@@ -135,9 +288,9 @@ GET /projects/:id/boards/:board_id/lists/:list_id
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
| `list_id`| integer | yes | The ID of a board's list |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
| `list_id`| integer | yes | The ID of a board's list |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1
......@@ -167,9 +320,9 @@ POST /projects/:id/boards/:board_id/lists
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
| `label_id` | integer | yes | The ID of a label |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
| `label_id` | integer | yes | The ID of a label |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists?label_id=5
......@@ -199,10 +352,10 @@ PUT /projects/:id/boards/:board_id/lists/:list_id
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
| `list_id` | integer | yes | The ID of a board's list |
| `position` | integer | yes | The position of the list |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
| `list_id` | integer | yes | The ID of a board's list |
| `position` | integer | yes | The position of the list |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1?position=2
......@@ -232,9 +385,9 @@ DELETE /projects/:id/boards/:board_id/lists/:list_id
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
| `list_id` | integer | yes | The ID of a board's list |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
| `list_id` | integer | yes | The ID of a board's list |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/boards/1/lists/1
......
# Group Issue Boards API
Every API call to group boards must be authenticated.
If a user is not a member of a group and the group is private, a `GET`
request will result in `404` status code.
## Group Board
Lists Issue Boards in the given group.
```
GET /groups/:id/boards
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/5/boards
```
Example response:
```json
[
{
"id": 1,
"name:": "group issue board",
"group_id": 5,
"milestone": {
"id": 12
"title": "10.0"
},
"lists" : [
{
"id" : 1,
"label" : {
"name" : "Testing",
"color" : "#F0AD4E",
"description" : null
},
"position" : 1
},
{
"id" : 2,
"label" : {
"name" : "Ready",
"color" : "#FF0000",
"description" : null
},
"position" : 2
},
{
"id" : 3,
"label" : {
"name" : "Production",
"color" : "#FF5F00",
"description" : null
},
"position" : 3
}
]
}
]
```
## Single board
Gets a single board.
```
GET /groups/:id/boards/:board_id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/5/boards/1
```
Example response:
```json
{
"id": 1,
"name:": "group issue board",
"group_id": 5,
"milestone": {
"id": 12
"title": "10.0"
},
"lists" : [
{
"id" : 1,
"label" : {
"name" : "Testing",
"color" : "#F0AD4E",
"description" : null
},
"position" : 1
},
{
"id" : 2,
"label" : {
"name" : "Ready",
"color" : "#FF0000",
"description" : null
},
"position" : 2
},
{
"id" : 3,
"label" : {
"name" : "Production",
"color" : "#FF5F00",
"description" : null
},
"position" : 3
}
]
}
```
## Create a board
Creates a board.
```
POST /groups/:id/boards
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `name` | string | yes | The name of the new board |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/5/boards?name=newboard
```
Example response:
```json
{
"id": 1,
"name": "newboard",
"group_id": 5,
"milestone": {
"id": 12
"title": "10.0"
},
"lists" : [
{
"id" : 1,
"label" : {
"name" : "Testing",
"color" : "#F0AD4E",
"description" : null
},
"position" : 1
},
{
"id" : 2,
"label" : {
"name" : "Ready",
"color" : "#FF0000",
"description" : null
},
"position" : 2
},
{
"id" : 3,
"label" : {
"name" : "Production",
"color" : "#FF5F00",
"description" : null
},
"position" : 3
}
]
}
```
## Delete a board
Deletes a board.
```
DELETE /groups/:id/boards/:board_id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/5/boards/1
```
## List board lists
Get a list of the board's lists.
Does not include `backlog` and `closed` lists
```
GET /groups/:id/boards/:board_id/lists
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/5/boards/1/lists
```
Example response:
```json
[
{
"id" : 1,
"label" : {
"name" : "Testing",
"color" : "#F0AD4E",
"description" : null
},
"position" : 1
},
{
"id" : 2,
"label" : {
"name" : "Ready",
"color" : "#FF0000",
"description" : null
},
"position" : 2
},
{
"id" : 3,
"label" : {
"name" : "Production",
"color" : "#FF5F00",
"description" : null
},
"position" : 3
}
]
```
## Single board list
Get a single board list.
```
GET /groups/:id/boards/:board_id/lists/:list_id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
| `list_id` | integer | yes | The ID of a board's list |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/5/boards/1/lists/1
```
Example response:
```json
{
"id" : 1,
"label" : {
"name" : "Testing",
"color" : "#F0AD4E",
"description" : null
},
"position" : 1
}
```
## New board list
Creates a new Issue Board list.
```
POST /groups/:id/boards/:board_id/lists
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
| `label_id` | integer | yes | The ID of a label |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/5/boards/1/lists?label_id=5
```
Example response:
```json
{
"id" : 1,
"label" : {
"name" : "Testing",
"color" : "#F0AD4E",
"description" : null
},
"position" : 1
}
```
## Edit board list
Updates an existing Issue Board list. This call is used to change list position.
```
PUT /groups/:id/boards/:board_id/lists/:list_id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
| `list_id` | integer | yes | The ID of a board's list |
| `position` | integer | yes | The position of the list |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/group/5/boards/1/lists/1?position=2
```
Example response:
```json
{
"id" : 1,
"label" : {
"name" : "Testing",
"color" : "#F0AD4E",
"description" : null
},
"position" : 1
}
```
## Delete a board list
Only for admins and group owners. Soft deletes the board list in question.
```
DELETE /groups/:id/boards/:board_id/lists/:list_id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `board_id` | integer | yes | The ID of a board |
| `list_id` | integer | yes | The ID of a board's list |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/5/boards/1/lists/1
```
module API
class GroupBoards < Grape::API
include BoardsResponses
include EE::API::BoardsResponses
include PaginationParams
before do
authenticate!
check_group_issue_boards!
end
helpers do
def board_parent
user_group
end
def check_group_issue_boards!
forbidden! unless ::License.feature_available?(:group_issue_boards)
end
end
params do
requires :id, type: String, desc: 'The ID of a group'
end
resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
segment ':id/boards' do
desc 'Get all group boards' do
detail 'This feature was introduced in 10.4'
success Entities::Board
end
params do
use :pagination
end
get '/' do
present paginate(board_parent.boards), with: Entities::Board
end
desc 'Find a group board' do
detail 'This feature was introduced in 10.4'
success Entities::Board
end
get '/:board_id' do
present board, with: Entities::Board
end
desc 'Create a group board' do
detail 'This feature was introduced in 10.4'
success Entities::Board
end
params do
requires :name, type: String, desc: 'The board name'
end
post '/' do
authorize!(:admin_board, board_parent)
create_board
end
desc 'Delete a group board' do
detail 'This feature was introduced in 10.4'
success Entities::Board
end
delete '/:board_id' do
authorize!(:admin_board, board_parent)
delete_board
end
end
params do
requires :board_id, type: Integer, desc: 'The ID of a board'
end
segment ':id/boards/:board_id' do
desc 'Get the lists of a group board' do
detail 'Does not include backlog and closed lists. This feature was introduced in 10.4'
success Entities::List
end
params do
use :pagination
end
get '/lists' do
present paginate(board_lists), with: Entities::List
end
desc 'Get a list of a group board' do
detail 'This feature was introduced in 10.4'
success Entities::List
end
params do
requires :list_id, type: Integer, desc: 'The ID of a list'
end
get '/lists/:list_id' do
present board_lists.find(params[:list_id]), with: Entities::List
end
desc 'Create a new board list' do
detail 'This feature was introduced in 10.4'
success Entities::List
end
params do
requires :label_id, type: Integer, desc: 'The ID of an existing label'
end
post '/lists' do
unless available_labels_for(board_parent).exists?(params[:label_id])
render_api_error!({ error: 'Label not found!' }, 400)
end
authorize!(:admin_list, user_group)
create_list
end
desc 'Moves a board list to a new position' do
detail 'This feature was introduced in 10.4'
success Entities::List
end
params do
requires :list_id, type: Integer, desc: 'The ID of a list'
requires :position, type: Integer, desc: 'The position of the list'
end
put '/lists/:list_id' do
list = board_lists.find(params[:list_id])
authorize!(:admin_list, user_group)
move_list(list)
end
desc 'Delete a board list' do
detail 'This feature was introduced in 10.4'
success Entities::List
end
params do
requires :list_id, type: Integer, desc: 'The ID of a board list'
end
delete "/lists/:list_id" do
authorize!(:admin_list, user_group)
list = board_lists.find(params[:list_id])
destroy_list(list)
end
end
end
end
end
module EE
module API
class Boards < ::Grape::API
include ::API::PaginationParams
include ::API::BoardsResponses
include BoardsResponses
before { authenticate! }
helpers do
def board_parent
user_project
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: ::API::API::PROJECT_ENDPOINT_REQUIREMENTS do
segment ':id/boards' do
desc 'Get all project boards' do
detail 'This feature was introduced in 8.13'
success ::API::Entities::Board
end
params do
use :pagination
end
get '/' do
authorize!(:read_board, user_project)
present paginate(board_parent.boards), with: ::API::Entities::Board
end
desc 'Create a project board' do
detail 'This feature was introduced in 10.4'
success ::API::Entities::Board
end
params do
requires :name, type: String, desc: 'The board name'
end
post '/' do
authorize!(:admin_board, board_parent)
create_board
end
desc 'Delete a project board' do
detail 'This feature was introduced in 10.4'
success ::API::Entities::Board
end
delete '/:board_id' do
authorize!(:admin_board, board_parent)
delete_board
end
end
end
end
end
end
module EE
module API
module BoardsResponses
extend ActiveSupport::Concern
included do
helpers do
def create_board
forbidden! unless ::License.feature_available?(:multiple_issue_boards)
board =
::Boards::CreateService.new(board_parent, current_user, { name: params[:name] }).execute
present board, with: ::API::Entities::Board
end
def delete_board
forbidden! unless ::License.feature_available?(:multiple_issue_boards)
destroy_conditionally!(board) do |board|
service = ::Boards::DestroyService.new(board_parent, current_user)
service.execute(board)
end
end
end
end
end
end
end
......@@ -127,9 +127,10 @@ module API
mount ::API::Events
mount ::API::Features
mount ::API::Files
mount ::API::Groups
mount ::API::Geo
mount ::API::GeoNodes
mount ::API::Groups
mount ::API::GroupMilestones
mount ::API::Internal
mount ::API::Issues
mount ::API::IssueLinks
......@@ -144,8 +145,6 @@ module API
mount ::API::Members
mount ::API::MergeRequestDiffs
mount ::API::MergeRequests
mount ::API::ProjectMilestones
mount ::API::GroupMilestones
mount ::API::Namespaces
mount ::API::Notes
mount ::API::NotificationSettings
......@@ -155,6 +154,7 @@ module API
mount ::API::ProjectHooks
mount ::API::ProjectPushRule
mount ::API::Projects
mount ::API::ProjectMilestones
mount ::API::ProjectSnippets
mount ::API::ProtectedBranches
mount ::API::Repositories
......@@ -176,6 +176,10 @@ module API
mount ::API::Version
mount ::API::Wikis
# EE-Only
mount ::API::GroupBoards
mount ::EE::API::Boards
route :any, '*path' do
error!('404 Not Found', 404)
end
......
module API
class Boards < Grape::API
include BoardsResponses
include PaginationParams
before { authenticate! }
helpers do
def board_parent
user_project
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
desc 'Get all project boards' do
detail 'This feature was introduced in 8.13'
success Entities::Board
end
params do
use :pagination
end
get ':id/boards' do
authorize!(:read_board, user_project)
present paginate(user_project.boards), with: Entities::Board
segment ':id/boards' do
desc 'Get all project boards' do
detail 'This feature was introduced in 8.13'
success Entities::Board
end
params do
use :pagination
end
get '/' do
authorize!(:read_board, user_project)
present paginate(board_parent.boards), with: Entities::Board
end
desc 'Find a project board' do
detail 'This feature was introduced in 10.4'
success Entities::Board
end
get '/:board_id' do
present board, with: Entities::Board
end
end
params do
requires :board_id, type: Integer, desc: 'The ID of a board'
end
segment ':id/boards/:board_id' do
helpers do
def project_board
user_project.boards.find(params[:board_id])
end
def board_lists
project_board.lists.destroyable
end
end
desc 'Get the lists of a project board' do
detail 'Does not include `done` list. This feature was introduced in 8.13'
success Entities::List
......@@ -66,22 +73,13 @@ module API
requires :label_id, type: Integer, desc: 'The ID of an existing label'
end
post '/lists' do
unless available_labels.exists?(params[:label_id])
unless available_labels_for(user_project).exists?(params[:label_id])
render_api_error!({ error: 'Label not found!' }, 400)
end
authorize!(:admin_list, user_project)
service = ::Boards::Lists::CreateService.new(user_project, current_user,
{ label_id: params[:label_id] })
list = service.execute(project_board)
if list.valid?
present list, with: Entities::List
else
render_validation_error!(list)
end
create_list
end
desc 'Moves a board list to a new position' do
......@@ -97,14 +95,7 @@ module API
authorize!(:admin_list, user_project)
service = ::Boards::Lists::MoveService.new(user_project, current_user,
{ position: params[:position].to_i })
if service.execute(list)
present list, with: Entities::List
else
render_api_error!({ error: "List could not be moved!" }, 400)
end
move_list(list)
end
desc 'Delete a board list' do
......@@ -118,12 +109,7 @@ module API
authorize!(:admin_list, user_project)
list = board_lists.find(params[:list_id])
destroy_conditionally!(list) do |list|
service = ::Boards::Lists::DestroyService.new(user_project, current_user)
unless service.execute(list)
render_api_error!({ error: 'List could not be deleted!' }, 400)
end
end
destroy_list(list)
end
end
end
......
module API
module BoardsResponses
extend ActiveSupport::Concern
included do
helpers do
def board
board_parent.boards.find(params[:board_id])
end
def board_lists
board.lists.destroyable
end
def create_list
create_list_service =
::Boards::Lists::CreateService.new(board_parent, current_user, { label_id: params[:label_id] })
list = create_list_service.execute(board)
if list.valid?
present list, with: Entities::List
else
render_validation_error!(list)
end
end
def move_list(list)
move_list_service =
::Boards::Lists::MoveService.new(board_parent, current_user, { position: params[:position].to_i })
if move_list_service.execute(list)
present list, with: Entities::List
else
render_api_error!({ error: "List could not be moved!" }, 400)
end
end
def destroy_list(list)
destroy_conditionally!(list) do |list|
service = ::Boards::Lists::DestroyService.new(board_parent, current_user)
unless service.execute(list)
render_api_error!({ error: 'List could not be deleted!' }, 400)
end
end
end
end
end
end
end
......@@ -868,11 +868,12 @@ module API
class Board < Grape::Entity
expose :id
expose :name
expose :project, using: Entities::BasicProjectDetails
# EE-specific
# Default filtering configuration
expose :name
expose :group
expose :milestone, using: Entities::Milestone, if: -> (board, _) { scoped_issue_available?(board) }
expose :assignee, using: Entities::UserBasic, if: -> (board, _) { scoped_issue_available?(board) }
expose :labels, using: Entities::LabelBasic, if: -> (board, _) { scoped_issue_available?(board) }
......
......@@ -77,8 +77,15 @@ module API
page || not_found!('Wiki Page')
end
def available_labels
@available_labels ||= LabelsFinder.new(current_user, project_id: user_project.id).execute
def available_labels_for(label_parent)
search_params =
if label_parent.is_a?(Project)
{ project_id: label_parent.id }
else
{ group_id: label_parent.id, only_group_labels: true }
end
LabelsFinder.new(current_user, search_params).execute
end
def find_user(id)
......@@ -153,7 +160,9 @@ module API
end
def find_project_label(id)
label = available_labels.find_by_id(id) || available_labels.find_by_title(id)
labels = available_labels_for(user_project)
label = labels.find_by_id(id) || labels.find_by_title(id)
label || not_found!('Label')
end
......
......@@ -15,7 +15,7 @@ module API
use :pagination
end
get ':id/labels' do
present paginate(available_labels), with: Entities::Label, current_user: current_user, project: user_project
present paginate(available_labels_for(user_project)), with: Entities::Label, current_user: current_user, project: user_project
end
desc 'Create a new label' do
......@@ -30,7 +30,7 @@ module API
post ':id/labels' do
authorize! :admin_label, user_project
label = available_labels.find_by(title: params[:name])
label = available_labels_for(user_project).find_by(title: params[:name])
conflict!('Label already exists') if label
priority = params.delete(:priority)
......
......@@ -11,7 +11,7 @@ module API
success ::API::Entities::Label
end
get ':id/labels' do
present available_labels, with: ::API::Entities::Label, current_user: current_user, project: user_project
present available_labels_for(user_project), with: ::API::Entities::Label, current_user: current_user, project: user_project
end
desc 'Delete an existing label' do
......
{
"type": "object",
"allOf": [
{ "$ref": "../../../../../../fixtures/api/schemas/public_api/v4/board.json" },
{
"required" : [
"name",
"weight",
"group",
"milestone",
"assignee",
"labels"
],
"properties": {
"group": { "type": ["object", null] },
"name": { "type": "string" },
"weight": { "type": ["string", "null"] },
"assignee": {
"type": ["object", "null"]
},
"labels": {
"type": "array"
},
"milestone": {
"type": ["object", "null"],
"required": [
"id",
"title"
],
"properties": {
"id": { "type": "integer" },
"title": { "type": "string" }
},
"additionalProperties": false
},
"additional_properties": true
}
}
]
}
{
"type": "array",
"items": { "$ref": "board.json" }
}
require 'spec_helper'
describe API::Boards do
set(:user) { create(:user) }
set(:board_parent) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
set(:milestone) { create(:milestone, project: board_parent) }
set(:board) { create(:board, project: board_parent, milestone: milestone) }
it_behaves_like 'multiple and scoped issue boards', "/projects/:id/boards"
end
require 'spec_helper'
describe API::GroupBoards do
set(:user) { create(:user) }
set(:non_member) { create(:user) }
set(:guest) { create(:user) }
set(:admin) { create(:user, :admin) }
set(:board_parent) { create(:group, :public) }
before do
stub_licensed_features(group_issue_boards: true)
board_parent.add_owner(user)
end
set(:project) { create(:project, :public, namespace: board_parent ) }
set(:dev_label) do
create(:group_label, title: 'Development', color: '#FFAABB', group: board_parent)
end
set(:test_label) do
create(:group_label, title: 'Testing', color: '#FFAACC', group: board_parent)
end
set(:ux_label) do
create(:group_label, title: 'UX', color: '#FF0000', group: board_parent)
end
set(:dev_list) do
create(:list, label: dev_label, position: 1)
end
set(:test_list) do
create(:list, label: test_label, position: 2)
end
set(:milestone) { create(:milestone, group: board_parent) }
set(:board_label) { create(:group_label, group: board_parent) }
# EE only
set(:board) do
create(:board, group: board_parent,
milestone: milestone,
assignee: user,
label_ids: [board_label.id],
lists: [dev_list, test_list])
end
it_behaves_like 'group and project boards', "/groups/:id/boards", true
it_behaves_like 'multiple and scoped issue boards', "/groups/:id/boards"
describe 'POST /groups/:id/boards/lists' do
let(:url) { "/groups/#{board_parent.id}/boards/#{board.id}/lists" }
it 'does not create lists for child project labels' do
project_label = create(:label, project: project)
post api(url, user), label_id: project_label.id
expect(response).to have_gitlab_http_status(400)
end
end
end
shared_examples_for 'multiple and scoped issue boards' do |route_definition|
let(:root_url) { route_definition.gsub(":id", board_parent.id.to_s) }
context 'multiple issue boards' do
before do
board_parent.add_reporter(user)
stub_licensed_features(multiple_issue_boards: true, group_issue_boards: true)
end
describe "POST #{route_definition}" do
it 'creates a board' do
post api(root_url, user), name: "new board"
expect(response).to have_gitlab_http_status(201)
expect(response).to match_response_schema('public_api/v4/board', dir: "ee")
end
end
describe "DELETE #{route_definition}" do
let(:url) { "#{root_url}/#{board.id}" }
it 'deletes a board' do
delete api(url, user)
expect(response).to have_gitlab_http_status(204)
end
end
end
context 'with the scoped_issue_board-feature available' do
it 'returns the milestone when the `scoped_issue_board` feature is enabled' do
stub_licensed_features(scoped_issue_board: true, group_issue_boards: true)
get api(root_url, user)
expect(json_response.first["milestone"]).not_to be_nil
end
it 'hides the milestone when the `scoped_issue_board` feature is disabled' do
stub_licensed_features(scoped_issue_board: false, group_issue_boards: true)
get api(root_url, user)
expect(json_response.first["milestone"]).to be_nil
end
end
end
......@@ -56,6 +56,16 @@ describe LabelsFinder do
expect(finder.execute).to eq [group_label_2, group_label_1, project_label_5]
end
context 'when only_group_labels is true' do
it 'returns only group labels' do
group_1.add_developer(user)
finder = described_class.new(user, group_id: group_1.id, only_group_labels: true)
expect(finder.execute).to eq [group_label_2, group_label_1]
end
end
end
context 'filtering by project_id' do
......
......@@ -2,20 +2,13 @@
"type": "object",
"required" : [
"id",
"name",
"weight",
"project",
"milestone",
"assignee",
"labels",
"lists"
],
"properties" : {
"id": { "type": "integer" },
"name": { "type": "string" },
"weight": { "type": ["string", "null"] },
"project": {
"type": "object",
"type": ["object", "null"],
"required": [
"id",
"avatar_url",
......@@ -54,25 +47,6 @@
},
"additionalProperties": false
},
"assignee": {
"$ref": "user/basic.json"
},
"labels": {
"type": "array",
"items": { "$ref": "label/basic.json" }
},
"milestone": {
"type": ["object", "null"],
"required": [
"id",
"title"
],
"properties": {
"id": { "type": "integer" },
"title": { "type": "string" }
},
"additionalProperties": false
},
"lists": {
"type": "array",
"items": {
......@@ -108,5 +82,5 @@
}
}
},
"additionalProperties": false
"additionalProperties": true
}
{
"type": "object",
"type": ["object", "null"],
"required": [
"id",
"state",
......
......@@ -5,18 +5,18 @@ describe API::Boards do
set(:non_member) { create(:user) }
set(:guest) { create(:user) }
set(:admin) { create(:user, :admin) }
set(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
set(:board_parent) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
set(:dev_label) do
create(:label, title: 'Development', color: '#FFAABB', project: project)
create(:label, title: 'Development', color: '#FFAABB', project: board_parent)
end
set(:test_label) do
create(:label, title: 'Testing', color: '#FFAACC', project: project)
create(:label, title: 'Testing', color: '#FFAACC', project: board_parent)
end
set(:ux_label) do
create(:label, title: 'UX', color: '#FF0000', project: project)
create(:label, title: 'UX', color: '#FF0000', project: board_parent)
end
set(:dev_list) do
......@@ -27,201 +27,25 @@ describe API::Boards do
create(:list, label: test_label, position: 2)
end
# EE only
set(:milestone) { create(:milestone, project: project) }
set(:board_label) { create(:label, project: project) }
set(:milestone) { create(:milestone, project: board_parent) }
set(:board_label) { create(:label, project: board_parent) }
set(:board) { create(:board, project: board_parent, lists: [dev_list, test_list]) }
set(:board) do
create(:board, project: project,
milestone: milestone,
assignee: user,
label_ids: [board_label.id],
lists: [dev_list, test_list])
end
before do
project.add_reporter(user)
project.add_guest(guest)
end
describe "GET /projects/:id/boards" do
let(:base_url) { "/projects/#{project.id}/boards" }
context "when unauthenticated" do
it "returns authentication error" do
get api(base_url)
expect(response).to have_gitlab_http_status(401)
end
end
context "when authenticated" do
it "returns the project issue boards" do
get api(base_url, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(response).to match_response_schema('public_api/v4/boards')
end
end
context 'with the scoped_issue_board-feature available' do
it 'returns the milestone when the `scoped_issue_board`-feature is enabled' do
stub_licensed_features(scoped_issue_board: true)
get api(base_url, user)
expect(json_response.first["milestone"]).not_to be_nil
end
it 'hides the milestone when the `scoped_issue_board`-feature is disabled' do
stub_licensed_features(scoped_issue_board: false)
get api(base_url, user)
expect(json_response.first["milestone"]).to be_nil
end
end
end
describe "GET /projects/:id/boards/:board_id/lists" do
let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
it_behaves_like 'group and project boards', "/projects/:id/boards"
it 'returns issue board lists' do
get api(base_url, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['label']['name']).to eq(dev_label.title)
end
it 'returns 404 if board not found' do
get api("/projects/#{project.id}/boards/22343/lists", user)
expect(response).to have_gitlab_http_status(404)
end
end
describe "GET /projects/:id/boards/:board_id/lists/:list_id" do
let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
it 'returns a list' do
get api("#{base_url}/#{dev_list.id}", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(dev_list.id)
expect(json_response['label']['name']).to eq(dev_label.title)
expect(json_response['position']).to eq(1)
end
it 'returns 404 if list not found' do
get api("#{base_url}/5324", user)
expect(response).to have_gitlab_http_status(404)
end
end
describe "POST /projects/:id/board/lists" do
let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
describe "POST /projects/:id/boards/lists" do
let(:url) { "/projects/#{board_parent.id}/boards/#{board.id}/lists" }
it 'creates a new issue board list for group labels' do
group = create(:group)
group_label = create(:group_label, group: group)
project.update(group: group)
board_parent.update(group: group)
post api(base_url, user), label_id: group_label.id
post api(url, user), label_id: group_label.id
expect(response).to have_gitlab_http_status(201)
expect(json_response['label']['name']).to eq(group_label.title)
expect(json_response['position']).to eq(3)
end
it 'creates a new issue board list for project labels' do
post api(base_url, user), label_id: ux_label.id
expect(response).to have_gitlab_http_status(201)
expect(json_response['label']['name']).to eq(ux_label.title)
expect(json_response['position']).to eq(3)
end
it 'returns 400 when creating a new list if label_id is invalid' do
post api(base_url, user), label_id: 23423
expect(response).to have_gitlab_http_status(400)
end
it 'returns 403 for project members with guest role' do
put api("#{base_url}/#{test_list.id}", guest), position: 1
expect(response).to have_gitlab_http_status(403)
end
end
describe "PUT /projects/:id/boards/:board_id/lists/:list_id to update only position" do
let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
it "updates a list" do
put api("#{base_url}/#{test_list.id}", user),
position: 1
expect(response).to have_gitlab_http_status(200)
expect(json_response['position']).to eq(1)
end
it "returns 404 error if list id not found" do
put api("#{base_url}/44444", user),
position: 1
expect(response).to have_gitlab_http_status(404)
end
it "returns 403 for project members with guest role" do
put api("#{base_url}/#{test_list.id}", guest),
position: 1
expect(response).to have_gitlab_http_status(403)
end
end
describe "DELETE /projects/:id/board/lists/:list_id" do
let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
it "rejects a non member from deleting a list" do
delete api("#{base_url}/#{dev_list.id}", non_member)
expect(response).to have_gitlab_http_status(403)
end
it "rejects a user with guest role from deleting a list" do
delete api("#{base_url}/#{dev_list.id}", guest)
expect(response).to have_gitlab_http_status(403)
end
it "returns 404 error if list id not found" do
delete api("#{base_url}/44444", user)
expect(response).to have_gitlab_http_status(404)
end
context "when the user is project owner" do
set(:owner) { create(:user) }
before do
project.update(namespace: owner.namespace)
end
it "deletes the list if an admin requests it" do
delete api("#{base_url}/#{dev_list.id}", owner)
expect(response).to have_gitlab_http_status(204)
end
it_behaves_like '412 response' do
let(:request) { api("#{base_url}/#{dev_list.id}", owner) }
end
end
end
end
shared_examples_for 'group and project boards' do |route_definition, ee = false|
let(:root_url) { route_definition.gsub(":id", board_parent.id.to_s) }
before do
board_parent.add_reporter(user)
board_parent.add_guest(guest)
end
def expect_schema_match_for(response, schema_file, ee)
if ee
expect(response).to match_response_schema(schema_file, dir: "ee")
else
expect(response).to match_response_schema(schema_file)
end
end
describe "GET #{route_definition}" do
context "when unauthenticated" do
it "returns authentication error" do
get api(root_url)
expect(response).to have_gitlab_http_status(401)
end
end
context "when authenticated" do
it "returns the issue boards" do
get api(root_url, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect_schema_match_for(response, 'public_api/v4/boards', ee)
end
describe "GET #{route_definition}/:board_id" do
let(:url) { "#{root_url}/#{board.id}" }
it 'get a single board by id' do
get api(url, user)
expect_schema_match_for(response, 'public_api/v4/board', ee)
end
end
end
end
describe "GET #{route_definition}/:board_id/lists" do
let(:url) { "#{root_url}/#{board.id}/lists" }
it 'returns issue board lists' do
get api(url, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect(json_response.first['label']['name']).to eq(dev_label.title)
end
it 'returns 404 if board not found' do
get api("#{root_url}/22343/lists", user)
expect(response).to have_gitlab_http_status(404)
end
end
describe "GET #{route_definition}/:board_id/lists/:list_id" do
let(:url) { "#{root_url}/#{board.id}/lists" }
it 'returns a list' do
get api("#{url}/#{dev_list.id}", user)
expect(response).to have_gitlab_http_status(200)
expect(json_response['id']).to eq(dev_list.id)
expect(json_response['label']['name']).to eq(dev_label.title)
expect(json_response['position']).to eq(1)
end
it 'returns 404 if list not found' do
get api("#{url}/5324", user)
expect(response).to have_gitlab_http_status(404)
end
end
describe "POST #{route_definition}/lists" do
let(:url) { "#{root_url}/#{board.id}/lists" }
it 'creates a new issue board list for labels' do
post api(url, user), label_id: ux_label.id
expect(response).to have_gitlab_http_status(201)
expect(json_response['label']['name']).to eq(ux_label.title)
expect(json_response['position']).to eq(3)
end
it 'returns 400 when creating a new list if label_id is invalid' do
post api(url, user), label_id: 23423
expect(response).to have_gitlab_http_status(400)
end
it 'returns 403 for members with guest role' do
put api("#{url}/#{test_list.id}", guest), position: 1
expect(response).to have_gitlab_http_status(403)
end
end
describe "PUT #{route_definition}/:board_id/lists/:list_id to update only position" do
let(:url) { "#{root_url}/#{board.id}/lists" }
it "updates a list" do
put api("#{url}/#{test_list.id}", user),
position: 1
expect(response).to have_gitlab_http_status(200)
expect(json_response['position']).to eq(1)
end
it "returns 404 error if list id not found" do
put api("#{url}/44444", user),
position: 1
expect(response).to have_gitlab_http_status(404)
end
it "returns 403 for members with guest role" do
put api("#{url}/#{test_list.id}", guest),
position: 1
expect(response).to have_gitlab_http_status(403)
end
end
describe "DELETE #{route_definition}/lists/:list_id" do
let(:url) { "#{root_url}/#{board.id}/lists" }
it "rejects a non member from deleting a list" do
delete api("#{url}/#{dev_list.id}", non_member)
expect(response).to have_gitlab_http_status(403)
end
it "rejects a user with guest role from deleting a list" do
delete api("#{url}/#{dev_list.id}", guest)
expect(response).to have_gitlab_http_status(403)
end
it "returns 404 error if list id not found" do
delete api("#{url}/44444", user)
expect(response).to have_gitlab_http_status(404)
end
context "when the user is parent owner" do
set(:owner) { create(:user) }
before do
if board_parent.try(:namespace)
board_parent.update(namespace: owner.namespace)
else
board.parent.add_owner(owner)
end
end
it "deletes the list if an admin requests it" do
delete api("#{url}/#{dev_list.id}", owner)
expect(response).to have_gitlab_http_status(204)
end
it_behaves_like '412 response' do
let(:request) { api("#{url}/#{dev_list.id}", owner) }
end
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