From f117c032ac6c414e6c1dfeab98184363c1f61608 Mon Sep 17 00:00:00 2001 From: Alexandru Croitor Date: Wed, 24 Apr 2019 16:08:14 +0300 Subject: Add params validations and remove extra params support Remove label_name and milestone_title params support Add mutually_exclusive validation for author_id and author_username Add mutually_exclusive validation for assignee_id and assignee_username Add validation to allow single value for asignee_username on CE Add separate issue_stats_params helper for statistics params and reuse in issues_params. --- app/finders/issuable_finder.rb | 10 +- ...e-57402-add-issues-statistics-api-endpoints.yml | 5 + doc/api/issues.md | 66 +++++-- doc/api/issues_statistics.md | 190 +++++++++++++++++++++ lib/api/helpers/issues_helpers.rb | 5 +- lib/api/issues.rb | 41 +++-- lib/api/validations/check_assignees_count.rb | 36 ++++ spec/requests/api/issues/get_group_issues_spec.rb | 26 ++- .../requests/api/issues/get_project_issues_spec.rb | 26 ++- spec/requests/api/issues/issues_spec.rb | 27 ++- .../requests/api/issues_shared_example_spec.rb | 26 +-- 11 files changed, 383 insertions(+), 75 deletions(-) create mode 100644 changelogs/unreleased/ce-57402-add-issues-statistics-api-endpoints.yml create mode 100644 doc/api/issues_statistics.md create mode 100644 lib/api/validations/check_assignees_count.rb diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index fcb333c6fcc..52b6e828cfa 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -275,14 +275,6 @@ class IssuableFinder params[:assignee_username].present? end - def assignee_username - if params[:assignee_username].is_a?(Array) - params[:assignee_username].first - else - params[:assignee_username] - end - end - # rubocop: disable CodeReuse/ActiveRecord def assignee return @assignee if defined?(@assignee) @@ -291,7 +283,7 @@ class IssuableFinder if assignee_id? User.find_by(id: params[:assignee_id]) elsif assignee_username? - User.find_by_username(assignee_username) + User.find_by_username(params[:assignee_username]) else nil end diff --git a/changelogs/unreleased/ce-57402-add-issues-statistics-api-endpoints.yml b/changelogs/unreleased/ce-57402-add-issues-statistics-api-endpoints.yml new file mode 100644 index 00000000000..a626193dc27 --- /dev/null +++ b/changelogs/unreleased/ce-57402-add-issues-statistics-api-endpoints.yml @@ -0,0 +1,5 @@ +--- +title: Add issues_statistics api endpoints and extend issues search api +merge_request: 27366 +author: +type: added diff --git a/doc/api/issues.md b/doc/api/issues.md index cb5789e76b7..936caef7601 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -37,23 +37,26 @@ GET /issues?confidential=true | Attribute | Type | Required | Description | | ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| `state` | string | no | Return all issues or just those that are `opened` or `closed` | +| `state` | string | no | Return `all` issues or just those that are `opened` or `closed` | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. | +| `with_labels_data` | Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. | | `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`
For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.
_([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | -| `author_id` | integer | no | Return issues created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | -| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. | +| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ | | `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | | `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` | | `search` | string | no | Search issues against their `title` and `description` | -| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` | +| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` | | `created_after` | datetime | no | Return issues created on or after the given time | | `created_before` | datetime | no | Return issues created on or before the given time | | `updated_after` | datetime | no | Return issues updated on or after the given time | | `updated_before` | datetime | no | Return issues updated on or before the given time | -| `confidential ` | Boolean | no | Filter confidential or public issues. | +| `confidential ` | Boolean | no | Filter confidential or public issues. | ```bash curl --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/issues @@ -109,7 +112,7 @@ Example response: "title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.", "created_at" : "2016-01-04T15:31:51.081Z", "iid" : 6, - "labels" : [], + "labels" : ["foo", "bar"], "upvotes": 4, "downvotes": 0, "merge_requests_count": 0, @@ -122,8 +125,17 @@ Example response: "human_time_estimate": null, "human_total_time_spent": null }, + "has_tasks": true, + "task_status": "10 of 15 tasks completed", "confidential": false, - "discussion_locked": false + "discussion_locked": false, + "_links":{ + "self":"http://example.com/api/v4/projects/1/issues/76", + "notes":"`http://example.com/`api/v4/projects/1/issues/76/notes", + "award_emoji":"http://example.com/api/v4/projects/1/issues/76/award_emoji", + "project":"http://example.com/api/v4/projects/1" + }, + "subscribed": false } ] ``` @@ -158,11 +170,14 @@ GET /groups/:id/issues?confidential=true | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user | | `state` | string | no | Return all issues or just those that are `opened` or `closed` | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. | +| `with_labels_data` | Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. | | `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | | `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.
For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.
_([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | -| `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | -| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. | +| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ | | `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` | @@ -221,7 +236,7 @@ Example response: "id" : 9, "name" : "Dr. Luella Kovacek" }, - "labels" : [], + "labels" : ["foo", "bar"], "upvotes": 4, "downvotes": 0, "merge_requests_count": 0, @@ -240,8 +255,17 @@ Example response: "human_time_estimate": null, "human_total_time_spent": null }, + "has_tasks": true, + "task_status": "10 of 15 tasks completed", "confidential": false, - "discussion_locked": false + "discussion_locked": false, + "_links":{ + "self":"http://example.com/api/v4/projects/4/issues/41", + "notes":"`http://example.com/`api/v4/projects/4/issues/41/notes", + "award_emoji":"http://example.com/api/v4/projects/4/issues/41/award_emoji", + "project":"http://example.com/api/v4/projects/4" + }, + "subscribed": false } ] ``` @@ -277,10 +301,13 @@ GET /projects/:id/issues?confidential=true | `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` | | `state` | string | no | Return all issues or just those that are `opened` or `closed` | | `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. | +| `with_labels_data` | Boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:text_color`. Default is `false`. | | `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | | `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.
For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.
_([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | -| `author_id` | integer | no | Return issues created by the given user `id` _([Introduced][ce-13004] in GitLab 9.5)_ | -| `assignee_id` | integer | no | Return issues assigned to the given user `id`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. | +| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | | `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ | | `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` | | `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` | @@ -340,7 +367,7 @@ Example response: "id" : 9, "name" : "Dr. Luella Kovacek" }, - "labels" : [], + "labels" : ["foo", "bar"], "upvotes": 4, "downvotes": 0, "merge_requests_count": 0, @@ -366,8 +393,17 @@ Example response: "human_time_estimate": null, "human_total_time_spent": null }, + "has_tasks": true, + "task_status": "10 of 15 tasks completed", "confidential": false, - "discussion_locked": false + "discussion_locked": false, + "_links":{ + "self":"http://example.com/api/v4/projects/4/issues/41", + "notes":"`http://example.com/`api/v4/projects/4/issues/41/notes", + "award_emoji":"http://example.com/api/v4/projects/4/issues/41/award_emoji", + "project":"http://example.com/api/v4/projects/4" + }, + "subscribed": false } ] ``` diff --git a/doc/api/issues_statistics.md b/doc/api/issues_statistics.md new file mode 100644 index 00000000000..1eeb1338104 --- /dev/null +++ b/doc/api/issues_statistics.md @@ -0,0 +1,190 @@ +# Issues Statistics API + +Every API call to issues_statistics must be authenticated. + +If a user is not a member of a project and the project is private, a `GET` +request on that project will result to a `404` status code. + +## Get issues statistics + +Gets issues count statistics on all issues the authenticated user has access to. By default it +returns only issues created by the current user. To get all issues, +use parameter `scope=all`. + +``` +GET /issues_statistics +GET /issues_statistics?labels=foo +GET /issues_statistics?labels=foo,bar +GET /issues_statistics?labels=foo,bar&state=opened +GET /issues_statistics?milestone=1.0.0 +GET /issues_statistics?milestone=1.0.0&state=opened +GET /issues_statistics?iids[]=42&iids[]=43 +GET /issues_statistics?author_id=5 +GET /issues_statistics?assignee_id=5 +GET /issues_statistics?my_reaction_emoji=star +GET /issues_statistics?search=foo&in=title +GET /issues_statistics?confidential=true +``` + +| Attribute | Type | Required | Description | +| ------------------- | ---------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. | +| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | +| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`
For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.
_([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | +| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. | +| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | +| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ | +| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | +| `search` | string | no | Search issues against their `title` and `description` | +| `in` | string | no | Modify the scope of the `search` attribute. `title`, `description`, or a string joining them with comma. Default is `title,description` | +| `created_after` | datetime | no | Return issues created on or after the given time | +| `created_before` | datetime | no | Return issues created on or before the given time | +| `updated_after` | datetime | no | Return issues updated on or after the given time | +| `updated_before` | datetime | no | Return issues updated on or before the given time | +| `confidential ` | Boolean | no | Filter confidential or public issues. | + +```bash +curl --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/issues_statistics +``` + +Example response: + +```json +{ + "statistics": { + "counts": { + "all": 20, + "closed": 5, + "opened": 15 + } + } +} +``` + +**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. + +**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists. + +## List group issues + +Get a list of a group's issues. + +``` +GET /groups/:id/issues_statistics +GET /groups/:id/issues_statistics?labels=foo +GET /groups/:id/issues_statistics?labels=foo,bar +GET /groups/:id/issues_statistics?labels=foo,bar&state=opened +GET /groups/:id/issues_statistics?milestone=1.0.0 +GET /groups/:id/issues_statistics?milestone=1.0.0&state=opened +GET /groups/:id/issues_statistics?iids[]=42&iids[]=43 +GET /groups/:id/issues_statistics?search=issue+title+or+description +GET /groups/:id/issues_statistics?author_id=5 +GET /groups/:id/issues_statistics?assignee_id=5 +GET /groups/:id/issues_statistics?my_reaction_emoji=star +GET /groups/:id/issues_statistics?confidential=true +``` + +| 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 | +| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. | +| `iids[]` | Array[integer] | no | Return only the issues having the given `iid` | +| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | +| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.
For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.
_([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | +| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. | +| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | +| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ | +| `search` | string | no | Search group issues against their `title` and `description` | +| `created_after` | datetime | no | Return issues created on or after the given time | +| `created_before` | datetime | no | Return issues created on or before the given time | +| `updated_after` | datetime | no | Return issues updated on or after the given time | +| `updated_before` | datetime | no | Return issues updated on or before the given time | +| `confidential ` | Boolean | no | Filter confidential or public issues. | + +```bash +curl --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/groups/4/issues_statistics +``` + +Example response: + +```json +{ + "statistics": { + "counts": { + "all": 20, + "closed": 5, + "opened": 15 + } + } +} +``` + +**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. + +**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists. + +## List project issues + +Get a list of a project's issues. + +``` +GET /projects/:id/issues_statistics +GET /projects/:id/issues_statistics?labels=foo +GET /projects/:id/issues_statistics?labels=foo,bar +GET /projects/:id/issues_statistics?labels=foo,bar&state=opened +GET /projects/:id/issues_statistics?milestone=1.0.0 +GET /projects/:id/issues_statistics?milestone=1.0.0&state=opened +GET /projects/:id/issues_statistics?iids[]=42&iids[]=43 +GET /projects/:id/issues_statistics?search=issue+title+or+description +GET /projects/:id/issues_statistics?author_id=5 +GET /projects/:id/issues_statistics?assignee_id=5 +GET /projects/:id/issues_statistics?my_reaction_emoji=star +GET /projects/:id/issues_statistics?confidential=true +``` + +| 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 | +| `iids[]` | Array[integer] | no | Return only the milestone having the given `iid` | +| `labels` | string | no | Comma-separated list of label names, issues must have all labels to be returned. `None` lists all issues with no labels. `Any` lists all issues with at least one label. `No+Label` (Deprecated) lists all issues with no labels. Predefined names are case-insensitive. | +| `milestone` | string | no | The milestone title. `None` lists all issues with no milestone. `Any` lists all issues that have an assigned milestone. | +| `scope` | string | no | Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`.
For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.
_([Introduced][ce-13004] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ | +| `author_id` | integer | no | Return issues created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `author_username` | string | no | Return issues created by the given `username`. Simillar to `author_id` and mutually exclusive with `author_id`. | +| `assignee_id` | integer | no | Return issues assigned to the given user `id`. Mutually exclusive with `assignee_username`. `None` returns unassigned issues. `Any` returns issues with an assignee. _([Introduced][ce-13004] in GitLab 9.5)_ | +| `assignee_username` | Array[String] | no | Return issues assigned to the given `username`. Simillar to `assignee_id` and mutually exclusive with `assignee_id`. In CE version `assignee_username` array should only contain a single value or an invalid param error will be returned otherwise. | +| `my_reaction_emoji` | string | no | Return issues reacted by the authenticated user by the given `emoji`. `None` returns issues not given a reaction. `Any` returns issues given at least one reaction. _([Introduced][ce-14016] in GitLab 10.0)_ | +| `search` | string | no | Search project issues against their `title` and `description` | +| `created_after` | datetime | no | Return issues created on or after the given time | +| `created_before` | datetime | no | Return issues created on or before the given time | +| `updated_after` | datetime | no | Return issues updated on or after the given time | +| `updated_before` | datetime | no | Return issues updated on or before the given time | +| `confidential ` | Boolean | no | Filter confidential or public issues. | + + +```bash +curl --header "PRIVATE-TOKEN: " https://gitlab.example.com/api/v4/projects/4/issues_statistics +``` + +Example response: + +```json +{ + "statistics": { + "counts": { + "all": 20, + "closed": 5, + "opened": 15 + } + } +} +``` + +**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API. + +**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists. + diff --git a/lib/api/helpers/issues_helpers.rb b/lib/api/helpers/issues_helpers.rb index 12bbc598532..fb791691069 100644 --- a/lib/api/helpers/issues_helpers.rb +++ b/lib/api/helpers/issues_helpers.rb @@ -24,7 +24,6 @@ module API args.delete(:id) args[:milestone_title] ||= args.delete(:milestone) - args[:milestone_title] ||= args.delete(:milestone_title) args[:label_name] ||= args.delete(:labels) args[:scope] = args[:scope].underscore if args[:scope] @@ -35,10 +34,8 @@ module API # rubocop: disable CodeReuse/ActiveRecord finder = issue_finder(args) issues = finder.execute.with_api_entity_associations - order_by = declared_params[:sort].present? && %w(asc desc).include?(declared_params[:sort].downcase) - issues = issues.reorder(order_options_with_tie_breaker) if order_by - issues + issues.reorder(order_options_with_tie_breaker) # rubocop: enable CodeReuse/ActiveRecord end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 6977657e356..9fd2d959c22 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -19,15 +19,10 @@ module API end end - params :issues_params do - optional :labels, :label_name, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names' - optional :with_labels_data, type: Boolean, desc: 'Return more label data than just lable title', default: false + params :issues_stats_params do + optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names' optional :milestone, type: String, desc: 'Milestone title' - optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at', - desc: 'Return issues ordered by `created_at` or `updated_at` fields.' - optional :sort, type: String, default: 'desc', - desc: 'Return issues sorted in `asc` or `desc` order.' - optional :milestone, :milestone_title, type: String, desc: 'Return issues for a specific milestone' + optional :milestone, type: String, desc: 'Return issues for a specific milestone' optional :iids, type: Array[Integer], desc: 'The IID array of issues' optional :search, type: String, desc: 'Search issues for text present in the title, description, or any combination of these' optional :in, type: String, desc: '`title`, `description`, or a string joining them with comma' @@ -35,23 +30,39 @@ module API optional :created_before, type: DateTime, desc: 'Return issues created before the specified time' optional :updated_after, type: DateTime, desc: 'Return issues updated after the specified time' optional :updated_before, type: DateTime, desc: 'Return issues updated before the specified time' + optional :author_id, type: Integer, desc: 'Return issues which are authored by the user with the given ID' optional :author_username, type: String, desc: 'Return issues which are authored by the user with the given username' + mutually_exclusive :author_id, :author_username + optional :assignee_id, types: [Integer, String], integer_none_any: true, desc: 'Return issues which are assigned to the user with the given ID' - optional :assignee_username, type: Array[String], + optional :assignee_username, type: Array[String], check_assignees_count: true, + coerce_with: Validations::CheckAssigneesCount.coerce, desc: 'Return issues which are assigned to the user with the given username' + mutually_exclusive :assignee_id, :assignee_username + optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`' optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji' optional :confidential, type: Boolean, desc: 'Filter confidential or public issues' - optional :state, type: String, values: %w[opened closed all], default: 'all', - desc: 'Return opened, closed, or all issues' - use :pagination use :issues_params_ee if Gitlab.ee? end + params :issues_params do + optional :with_labels_data, type: Boolean, desc: 'Return more label data than just lable title', default: false + optional :state, type: String, values: %w[opened closed all], default: 'all', + desc: 'Return opened, closed, or all issues' + optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at', + desc: 'Return issues ordered by `created_at` or `updated_at` fields.' + optional :sort, type: String, values: %w[asc desc], default: 'desc', + desc: 'Return issues sorted in `asc` or `desc` order.' + + use :issues_stats_params + use :pagination + end + params :issue_params do optional :description, type: String, desc: 'The description of an issue' optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue' @@ -68,7 +79,7 @@ module API desc "Get currently authenticated user's issues statistics" params do - use :issues_params + use :issues_stats_params optional :scope, type: String, values: %w[created-by-me assigned-to-me created_by_me assigned_to_me all], default: 'created_by_me', desc: 'Return issues for the given scope: `created_by_me`, `assigned_to_me` or `all`' end @@ -131,7 +142,7 @@ module API desc 'Get statistics for the list of group issues' params do - use :issues_params + use :issues_stats_params end get ":id/issues_statistics" do group = find_group!(params[:id]) @@ -172,7 +183,7 @@ module API desc 'Get statistics for the list of project issues' params do - use :issues_params + use :issues_stats_params end get ":id/issues_statistics" do project = find_project!(params[:id]) diff --git a/lib/api/validations/check_assignees_count.rb b/lib/api/validations/check_assignees_count.rb new file mode 100644 index 00000000000..e19c88e97b1 --- /dev/null +++ b/lib/api/validations/check_assignees_count.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module API + module Validations + class CheckAssigneesCount < Grape::Validations::Base + def self.coerce + lambda do |value| + case value + when String + [value] + when Array + value + when CheckAssigneesCount + value + else + [] + end + end + end + + def validate_param!(attr_name, params) + unless param_allowed?(attr_name, params) + raise Grape::Exceptions::Validation, + params: [@scope.full_name(attr_name)], + message: "allows one value, but found #{params[attr_name].size}: #{params[attr_name].join(", ")}" + end + end + + private + + def param_allowed?(attr_name, params) + params[attr_name].size <= 1 + end + end + end +end diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb index aafe0f56dfb..8b02cf56e9f 100644 --- a/spec/requests/api/issues/get_group_issues_spec.rb +++ b/spec/requests/api/issues/get_group_issues_spec.rb @@ -619,11 +619,33 @@ describe API::Issues do let!(:issue2) { create(:issue, author: user2, project: group_project, created_at: 2.days.ago) } let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: group_project, created_at: 1.day.ago) } - it 'returns issues with multiple assignees' do - get api("/groups/#{group.id}/issues", user), params: { assignee_username: [assignee.username, another_assignee.username] } + it 'returns issues with by assignee_username' do + get api(base_url, user), params: { assignee_username: [assignee.username], scope: 'all' } + expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id]) expect_paginated_array_response([issue3.id, group_confidential_issue.id]) end + + it 'returns issues by assignee_username as string' do + get api(base_url, user), params: { assignee_username: assignee.username, scope: 'all' } + + expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id]) + expect_paginated_array_response([issue3.id, group_confidential_issue.id]) + end + + it 'returns error when multiple assignees are passed' do + get api(base_url, user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' } + + expect(response).to have_gitlab_http_status(400) + expect(json_response["error"]).to include("allows one value, but found 2") + end + + it 'returns error when assignee_username and assignee_id are passed together' do + get api(base_url, user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' } + + expect(response).to have_gitlab_http_status(400) + expect(json_response["error"]).to include("mutually exclusive") + end end end end diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb index afd5bd2257f..a07d7673345 100644 --- a/spec/requests/api/issues/get_project_issues_spec.rb +++ b/spec/requests/api/issues/get_project_issues_spec.rb @@ -490,11 +490,33 @@ describe API::Issues do let!(:issue2) { create(:issue, author: user2, project: project, created_at: 2.days.ago) } let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: project, created_at: 1.day.ago) } - it 'returns issues with multiple assignees' do - get api("#{base_url}/issues", user), params: { assignee_username: [assignee.username, another_assignee.username] } + it 'returns issues by assignee_username' do + get api("/issues", user), params: { assignee_username: [assignee.username], scope: 'all' } + expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id]) expect_paginated_array_response([confidential_issue.id, issue3.id]) end + + it 'returns issues by assignee_username as string' do + get api("/issues", user), params: { assignee_username: assignee.username, scope: 'all' } + + expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id]) + expect_paginated_array_response([confidential_issue.id, issue3.id]) + end + + it 'returns error when multiple assignees are passed' do + get api("/issues", user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' } + + expect(response).to have_gitlab_http_status(400) + expect(json_response["error"]).to include("allows one value, but found 2") + end + + it 'returns error when assignee_username and assignee_id are passed together' do + get api("/issues", user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' } + + expect(response).to have_gitlab_http_status(400) + expect(json_response["error"]).to include("mutually exclusive") + end end end diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb index 6f916212672..24c53d9c68f 100644 --- a/spec/requests/api/issues/issues_spec.rb +++ b/spec/requests/api/issues/issues_spec.rb @@ -691,12 +691,33 @@ describe API::Issues do let!(:issue2) { create(:issue, author: user2, project: project, created_at: 2.days.ago) } let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: project, created_at: 1.day.ago) } - it 'returns issues with multiple assignees' do - get api("/issues", user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' } + it 'returns issues with by assignee_username' do + get api("/issues", user), params: { assignee_username: [assignee.username], scope: 'all' } + + expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id]) + expect_paginated_array_response([confidential_issue.id, issue3.id]) + end + + it 'returns issues by assignee_username as string' do + get api("/issues", user), params: { assignee_username: assignee.username, scope: 'all' } - expect(issue3.reload.assignees).to eq([assignee, another_assignee]) + expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id]) expect_paginated_array_response([confidential_issue.id, issue3.id]) end + + it 'returns error when multiple assignees are passed' do + get api("/issues", user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' } + + expect(response).to have_gitlab_http_status(400) + expect(json_response["error"]).to include("allows one value, but found 2") + end + + it 'returns error when assignee_username and assignee_id are passed together' do + get api("/issues", user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' } + + expect(response).to have_gitlab_http_status(400) + expect(json_response["error"]).to include("mutually exclusive") + end end end end diff --git a/spec/support/shared_examples/requests/api/issues_shared_example_spec.rb b/spec/support/shared_examples/requests/api/issues_shared_example_spec.rb index 2454b1a9761..d1c8e2208a3 100644 --- a/spec/support/shared_examples/requests/api/issues_shared_example_spec.rb +++ b/spec/support/shared_examples/requests/api/issues_shared_example_spec.rb @@ -28,19 +28,7 @@ shared_examples 'labeled issues with labels and label_name params' do it_behaves_like 'returns label names' end - context 'array of labeled issues when all labels match the label_name param' do - let(:params) { { label_name: "#{label.title},#{label_b.title},#{label_c.title}" } } - - it_behaves_like 'returns label names' - end - - context 'array of labeled issues when all labels match with label_name param as array' do - let(:params) { { label_name: [label.title, label_b.title, label_c.title] } } - - it_behaves_like 'returns label names' - end - - context 'with labels data' do + context 'when with_labels_data provided' do context 'array of labeled issues when all labels match' do let(:params) { { labels: "#{label.title},#{label_b.title},#{label_c.title}", with_labels_data: true } } @@ -52,17 +40,5 @@ shared_examples 'labeled issues with labels and label_name params' do it_behaves_like 'returns basic label entity' end - - context 'array of labeled issues when all labels match the label_name param' do - let(:params) { { label_name: "#{label.title},#{label_b.title},#{label_c.title}", with_labels_data: true } } - - it_behaves_like 'returns basic label entity' - end - - context 'array of labeled issues when all labels match with label_name param as array' do - let(:params) { { label_name: [label.title, label_b.title, label_c.title], with_labels_data: true } } - - it_behaves_like 'returns basic label entity' - end end end -- cgit v1.2.1