summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRémy Coutable <remy@rymai.me>2016-07-01 15:36:05 +0000
committerRémy Coutable <remy@rymai.me>2016-07-01 15:36:05 +0000
commit557ca2b31ff503b36a4b65af2641fcd0f5682d5b (patch)
treeb285eeba2b013e0c3b3046bc718e55efc38a2566
parentc368cb605bfb586cf93cff3879212906812f327b (diff)
parent87ac9c9850d602fd18654498ab3fa005d2b85ac7 (diff)
downloadgitlab-ce-557ca2b31ff503b36a4b65af2641fcd0f5682d5b.tar.gz
Merge branch 'todos-api' into 'master'
Add Todos API * Closes #14068 * Closes #14675 - [x] Implementation - [x] Tests - [x] Documentation - [x] Changelog See merge request !3188
-rw-r--r--CHANGELOG1
-rw-r--r--app/finders/todos_finder.rb21
-rw-r--r--app/models/todo.rb11
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/issues.md93
-rw-r--r--doc/api/merge_requests.md98
-rw-r--r--doc/api/todos.md444
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/entities.rb25
-rw-r--r--lib/api/todos.rb82
-rw-r--r--spec/factories/todos.rb4
-rw-r--r--spec/requests/api/todos_spec.rb190
12 files changed, 970 insertions, 1 deletions
diff --git a/CHANGELOG b/CHANGELOG
index a019f56c0fa..ac14af9d2d0 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -19,6 +19,7 @@ v 8.10.0 (unreleased)
- Fix changing issue state columns in milestone view
- Add notification settings dropdown for groups
- Allow importing from Github using Personal Access Tokens. (Eric K Idema)
+ - API: Todos !3188 (Robert Schilling)
- Fix user creation with stronger minimum password requirements !4054 (nathan-pmt)
- PipelinesFinder uses git cache data
- Check for conflicts with existing Project's wiki path when creating a new project.
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 58a00f88af7..7806d9e4cc5 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -25,6 +25,7 @@ class TodosFinder
def execute
items = current_user.todos
items = by_action_id(items)
+ items = by_action(items)
items = by_author(items)
items = by_project(items)
items = by_state(items)
@@ -43,6 +44,18 @@ class TodosFinder
params[:action_id]
end
+ def to_action_id
+ Todo::ACTION_NAMES.key(action.to_sym)
+ end
+
+ def action?
+ action.present? && to_action_id
+ end
+
+ def action
+ params[:action]
+ end
+
def author?
params[:author_id].present?
end
@@ -96,6 +109,14 @@ class TodosFinder
params[:type]
end
+ def by_action(items)
+ if action?
+ items = items.where(action: to_action_id)
+ end
+
+ items
+ end
+
def by_action_id(items)
if action_id?
items = items.where(action: action_id)
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 2792fa9b9a8..3ba67078d48 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -4,6 +4,13 @@ class Todo < ActiveRecord::Base
BUILD_FAILED = 3
MARKED = 4
+ ACTION_NAMES = {
+ ASSIGNED => :assigned,
+ MENTIONED => :mentioned,
+ BUILD_FAILED => :build_failed,
+ MARKED => :marked
+ }
+
belongs_to :author, class_name: "User"
belongs_to :note
belongs_to :project
@@ -34,6 +41,10 @@ class Todo < ActiveRecord::Base
action == BUILD_FAILED
end
+ def action_name
+ ACTION_NAMES[action]
+ end
+
def body
if note.present?
note.note
diff --git a/doc/api/README.md b/doc/api/README.md
index 288f7f9ee69..d1e6c54c521 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -36,6 +36,7 @@ following locations:
- [System Hooks](system_hooks.md)
- [Tags](tags.md)
- [Users](users.md)
+- [Todos](todos.md)
### Internal CI API
diff --git a/doc/api/issues.md b/doc/api/issues.md
index 708fc691f67..3ced787b23e 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -594,12 +594,103 @@ Example response:
"id": 11,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
- "web_url": "http://lgitlab.example.com/u/orville"
+ "web_url": "https://gitlab.example.com/u/orville"
},
"subscribed": false
}
```
+## Create a todo
+
+Manually creates a todo for the current user on an issue. If the request is
+successful, status code `200` together with the created todo is returned. If
+there already exists a todo for the user on that issue, status code `304` is
+returned.
+
+```
+POST /projects/:id/issues/:issue_id/todo
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `issue_id` | integer | yes | The ID of a project's issue |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/todo
+```
+
+Example response:
+
+```json
+{
+ "id": 112,
+ "project": {
+ "id": 5,
+ "name": "Gitlab Ci",
+ "name_with_namespace": "Gitlab Org / Gitlab Ci",
+ "path": "gitlab-ci",
+ "path_with_namespace": "gitlab-org/gitlab-ci"
+ },
+ "author": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "action_name": "marked",
+ "target_type": "Issue",
+ "target": {
+ "id": 93,
+ "iid": 10,
+ "project_id": 5,
+ "title": "Vel voluptas atque dicta mollitia adipisci qui at.",
+ "description": "Tempora laboriosam sint magni sed voluptas similique.",
+ "state": "closed",
+ "created_at": "2016-06-17T07:47:39.486Z",
+ "updated_at": "2016-07-01T11:09:13.998Z",
+ "labels": [],
+ "milestone": {
+ "id": 26,
+ "iid": 1,
+ "project_id": 5,
+ "title": "v0.0",
+ "description": "Accusantium nostrum rerum quae quia quis nesciunt suscipit id.",
+ "state": "closed",
+ "created_at": "2016-06-17T07:47:33.832Z",
+ "updated_at": "2016-06-17T07:47:33.832Z",
+ "due_date": null
+ },
+ "assignee": {
+ "name": "Jarret O'Keefe",
+ "username": "francisca",
+ "id": 14,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/francisca"
+ },
+ "author": {
+ "name": "Maxie Medhurst",
+ "username": "craig_rutherford",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ },
+ "subscribed": true,
+ "user_notes_count": 7,
+ "upvotes": 0,
+ "downvotes": 0
+ },
+ "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/issues/10",
+ "body": "Vel voluptas atque dicta mollitia adipisci qui at.",
+ "state": "pending",
+ "created_at": "2016-07-01T11:09:13.992Z"
+}
+```
+
## Comments on issues
Comments are done via the [notes](notes.md) resource.
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 2930f615fc1..f60b0d0ebc6 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -776,3 +776,101 @@ Example response:
"subscribed": false
}
```
+
+## Create a todo
+
+Manually creates a todo for the current user on a merge request. If the
+request is successful, status code `200` together with the created todo is
+returned. If there already exists a todo for the user on that merge request,
+status code `304` is returned.
+
+```
+POST /projects/:id/merge_requests/:merge_request_id/todo
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `merge_request_id` | integer | yes | The ID of the merge request |
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/27/todo
+```
+
+Example response:
+
+```json
+{
+ "id": 113,
+ "project": {
+ "id": 3,
+ "name": "Gitlab Ci",
+ "name_with_namespace": "Gitlab Org / Gitlab Ci",
+ "path": "gitlab-ci",
+ "path_with_namespace": "gitlab-org/gitlab-ci"
+ },
+ "author": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "action_name": "marked",
+ "target_type": "MergeRequest",
+ "target": {
+ "id": 27,
+ "iid": 7,
+ "project_id": 3,
+ "title": "Et voluptas laudantium minus nihil recusandae ut accusamus earum aut non.",
+ "description": "Veniam sunt nihil modi earum cumque illum delectus. Nihil ad quis distinctio quia. Autem eligendi at quibusdam repellendus.",
+ "state": "opened",
+ "created_at": "2016-06-17T07:48:04.330Z",
+ "updated_at": "2016-07-01T11:14:15.537Z",
+ "target_branch": "allow_regex_for_project_skip_ref",
+ "source_branch": "backup",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "name": "Jarret O'Keefe",
+ "username": "francisca",
+ "id": 14,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/francisca"
+ },
+ "assignee": {
+ "name": "Dr. Gabrielle Strosin",
+ "username": "barrett.krajcik",
+ "id": 4,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/733005fcd7e6df12d2d8580171ccb966?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/barrett.krajcik"
+ },
+ "source_project_id": 3,
+ "target_project_id": 3,
+ "labels": [],
+ "work_in_progress": false,
+ "milestone": {
+ "id": 27,
+ "iid": 2,
+ "project_id": 3,
+ "title": "v1.0",
+ "description": "Quis ea accusantium animi hic fuga assumenda.",
+ "state": "active",
+ "created_at": "2016-06-17T07:47:33.840Z",
+ "updated_at": "2016-06-17T07:47:33.840Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_status": "unchecked",
+ "subscribed": true,
+ "user_notes_count": 7
+ },
+ "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/merge_requests/7",
+ "body": "Et voluptas laudantium minus nihil recusandae ut accusamus earum aut non.",
+ "state": "pending",
+ "created_at": "2016-07-01T11:14:15.530Z"
+}
+```
diff --git a/doc/api/todos.md b/doc/api/todos.md
new file mode 100644
index 00000000000..29e73664410
--- /dev/null
+++ b/doc/api/todos.md
@@ -0,0 +1,444 @@
+# Todos
+
+**Note:** This feature was [introduced][ce-3188] in GitLab 8.10
+
+## Get a list of todos
+
+Returns a list of todos. When no filter is applied, it returns all pending todos
+for the current user. Different filters allow the user to precise the request.
+
+```
+GET /todos
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `action` | string | no | The action to be filtered. Can be `assigned`, `mentioned`, `build_failed`, or `marked`. |
+| `author_id` | integer | no | The ID of an author |
+| `project_id` | integer | no | The ID of a project |
+| `state` | string | no | The state of the todo. Can be either `pending` or `done` |
+| `type` | string | no | The type of a todo. Can be either `Issue` or `MergeRequest` |
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos
+```
+
+Example Response:
+
+```json
+[
+ {
+ "id": 102,
+ "project": {
+ "id": 2,
+ "name": "Gitlab Ce",
+ "name_with_namespace": "Gitlab Org / Gitlab Ce",
+ "path": "gitlab-ce",
+ "path_with_namespace": "gitlab-org/gitlab-ce"
+ },
+ "author": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "action_name": "marked",
+ "target_type": "MergeRequest",
+ "target": {
+ "id": 34,
+ "iid": 7,
+ "project_id": 2,
+ "title": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "description": "Et ea et omnis illum cupiditate. Dolor aspernatur tenetur ducimus facilis est nihil. Quo esse cupiditate molestiae illo corrupti qui quidem dolor.",
+ "state": "opened",
+ "created_at": "2016-06-17T07:49:24.419Z",
+ "updated_at": "2016-06-17T07:52:43.484Z",
+ "target_branch": "tutorials_git_tricks",
+ "source_branch": "DNSBL_docs",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "name": "Maxie Medhurst",
+ "username": "craig_rutherford",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ },
+ "assignee": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "source_project_id": 2,
+ "target_project_id": 2,
+ "labels": [],
+ "work_in_progress": false,
+ "milestone": {
+ "id": 32,
+ "iid": 2,
+ "project_id": 2,
+ "title": "v1.0",
+ "description": "Assumenda placeat ea voluptatem voluptate qui.",
+ "state": "active",
+ "created_at": "2016-06-17T07:47:34.163Z",
+ "updated_at": "2016-06-17T07:47:34.163Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_status": "cannot_be_merged",
+ "subscribed": true,
+ "user_notes_count": 7
+ },
+ "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ce/merge_requests/7",
+ "body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "state": "pending",
+ "created_at": "2016-06-17T07:52:35.225Z"
+ },
+ {
+ "id": 98,
+ "project": {
+ "id": 2,
+ "name": "Gitlab Ce",
+ "name_with_namespace": "Gitlab Org / Gitlab Ce",
+ "path": "gitlab-ce",
+ "path_with_namespace": "gitlab-org/gitlab-ce"
+ },
+ "author": {
+ "name": "Maxie Medhurst",
+ "username": "craig_rutherford",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ },
+ "action_name": "assigned",
+ "target_type": "MergeRequest",
+ "target": {
+ "id": 34,
+ "iid": 7,
+ "project_id": 2,
+ "title": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "description": "Et ea et omnis illum cupiditate. Dolor aspernatur tenetur ducimus facilis est nihil. Quo esse cupiditate molestiae illo corrupti qui quidem dolor.",
+ "state": "opened",
+ "created_at": "2016-06-17T07:49:24.419Z",
+ "updated_at": "2016-06-17T07:52:43.484Z",
+ "target_branch": "tutorials_git_tricks",
+ "source_branch": "DNSBL_docs",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "name": "Maxie Medhurst",
+ "username": "craig_rutherford",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ },
+ "assignee": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "source_project_id": 2,
+ "target_project_id": 2,
+ "labels": [],
+ "work_in_progress": false,
+ "milestone": {
+ "id": 32,
+ "iid": 2,
+ "project_id": 2,
+ "title": "v1.0",
+ "description": "Assumenda placeat ea voluptatem voluptate qui.",
+ "state": "active",
+ "created_at": "2016-06-17T07:47:34.163Z",
+ "updated_at": "2016-06-17T07:47:34.163Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_status": "cannot_be_merged",
+ "subscribed": true,
+ "user_notes_count": 7
+ },
+ "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ce/merge_requests/7",
+ "body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "state": "pending",
+ "created_at": "2016-06-17T07:49:24.624Z"
+ }
+]
+```
+
+## Mark a todo as done
+
+Marks a single pending todo given by its ID for the current user as done. The
+todo marked as done is returned in the response.
+
+```
+DELETE /todos/:id
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a todo |
+
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos/130
+```
+
+Example Response:
+
+```json
+{
+ "id": 102,
+ "project": {
+ "id": 2,
+ "name": "Gitlab Ce",
+ "name_with_namespace": "Gitlab Org / Gitlab Ce",
+ "path": "gitlab-ce",
+ "path_with_namespace": "gitlab-org/gitlab-ce"
+ },
+ "author": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "action_name": "marked",
+ "target_type": "MergeRequest",
+ "target": {
+ "id": 34,
+ "iid": 7,
+ "project_id": 2,
+ "title": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "description": "Et ea et omnis illum cupiditate. Dolor aspernatur tenetur ducimus facilis est nihil. Quo esse cupiditate molestiae illo corrupti qui quidem dolor.",
+ "state": "opened",
+ "created_at": "2016-06-17T07:49:24.419Z",
+ "updated_at": "2016-06-17T07:52:43.484Z",
+ "target_branch": "tutorials_git_tricks",
+ "source_branch": "DNSBL_docs",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "name": "Maxie Medhurst",
+ "username": "craig_rutherford",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ },
+ "assignee": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "source_project_id": 2,
+ "target_project_id": 2,
+ "labels": [],
+ "work_in_progress": false,
+ "milestone": {
+ "id": 32,
+ "iid": 2,
+ "project_id": 2,
+ "title": "v1.0",
+ "description": "Assumenda placeat ea voluptatem voluptate qui.",
+ "state": "active",
+ "created_at": "2016-06-17T07:47:34.163Z",
+ "updated_at": "2016-06-17T07:47:34.163Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_status": "cannot_be_merged",
+ "subscribed": true,
+ "user_notes_count": 7
+ },
+ "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ce/merge_requests/7",
+ "body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "state": "done",
+ "created_at": "2016-06-17T07:52:35.225Z"
+}
+```
+
+## Mark all todos as done
+
+Marks all pending todos for the current user as done. All todos marked as done
+are returned in the response.
+
+```
+DELETE /todos
+```
+
+```bash
+curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/todos
+```
+
+Example Response:
+
+```json
+[
+ {
+ "id": 102,
+ "project": {
+ "id": 2,
+ "name": "Gitlab Ce",
+ "name_with_namespace": "Gitlab Org / Gitlab Ce",
+ "path": "gitlab-ce",
+ "path_with_namespace": "gitlab-org/gitlab-ce"
+ },
+ "author": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "action_name": "marked",
+ "target_type": "MergeRequest",
+ "target": {
+ "id": 34,
+ "iid": 7,
+ "project_id": 2,
+ "title": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "description": "Et ea et omnis illum cupiditate. Dolor aspernatur tenetur ducimus facilis est nihil. Quo esse cupiditate molestiae illo corrupti qui quidem dolor.",
+ "state": "opened",
+ "created_at": "2016-06-17T07:49:24.419Z",
+ "updated_at": "2016-06-17T07:52:43.484Z",
+ "target_branch": "tutorials_git_tricks",
+ "source_branch": "DNSBL_docs",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "name": "Maxie Medhurst",
+ "username": "craig_rutherford",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ },
+ "assignee": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "source_project_id": 2,
+ "target_project_id": 2,
+ "labels": [],
+ "work_in_progress": false,
+ "milestone": {
+ "id": 32,
+ "iid": 2,
+ "project_id": 2,
+ "title": "v1.0",
+ "description": "Assumenda placeat ea voluptatem voluptate qui.",
+ "state": "active",
+ "created_at": "2016-06-17T07:47:34.163Z",
+ "updated_at": "2016-06-17T07:47:34.163Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_status": "cannot_be_merged",
+ "subscribed": true,
+ "user_notes_count": 7
+ },
+ "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ce/merge_requests/7",
+ "body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "state": "done",
+ "created_at": "2016-06-17T07:52:35.225Z"
+ },
+ {
+ "id": 98,
+ "project": {
+ "id": 2,
+ "name": "Gitlab Ce",
+ "name_with_namespace": "Gitlab Org / Gitlab Ce",
+ "path": "gitlab-ce",
+ "path_with_namespace": "gitlab-org/gitlab-ce"
+ },
+ "author": {
+ "name": "Maxie Medhurst",
+ "username": "craig_rutherford",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ },
+ "action_name": "assigned",
+ "target_type": "MergeRequest",
+ "target": {
+ "id": 34,
+ "iid": 7,
+ "project_id": 2,
+ "title": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "description": "Et ea et omnis illum cupiditate. Dolor aspernatur tenetur ducimus facilis est nihil. Quo esse cupiditate molestiae illo corrupti qui quidem dolor.",
+ "state": "opened",
+ "created_at": "2016-06-17T07:49:24.419Z",
+ "updated_at": "2016-06-17T07:52:43.484Z",
+ "target_branch": "tutorials_git_tricks",
+ "source_branch": "DNSBL_docs",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "name": "Maxie Medhurst",
+ "username": "craig_rutherford",
+ "id": 12,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/a0d477b3ea21970ce6ffcbb817b0b435?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/craig_rutherford"
+ },
+ "assignee": {
+ "name": "Administrator",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "web_url": "https://gitlab.example.com/u/root"
+ },
+ "source_project_id": 2,
+ "target_project_id": 2,
+ "labels": [],
+ "work_in_progress": false,
+ "milestone": {
+ "id": 32,
+ "iid": 2,
+ "project_id": 2,
+ "title": "v1.0",
+ "description": "Assumenda placeat ea voluptatem voluptate qui.",
+ "state": "active",
+ "created_at": "2016-06-17T07:47:34.163Z",
+ "updated_at": "2016-06-17T07:47:34.163Z",
+ "due_date": null
+ },
+ "merge_when_build_succeeds": false,
+ "merge_status": "cannot_be_merged",
+ "subscribed": true,
+ "user_notes_count": 7
+ },
+ "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ce/merge_requests/7",
+ "body": "Dolores in voluptatem tenetur praesentium omnis repellendus voluptatem quaerat.",
+ "state": "done",
+ "created_at": "2016-06-17T07:49:24.624Z"
+ },
+]
+```
+
+[ce-3188]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3188
diff --git a/lib/api/api.rb b/lib/api/api.rb
index c3fff8b2f8f..3d7d67510a8 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -58,6 +58,7 @@ module API
mount ::API::SystemHooks
mount ::API::Tags
mount ::API::Templates
+ mount ::API::Todos
mount ::API::Triggers
mount ::API::Users
mount ::API::Variables
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 4e2a43e45e2..8cc4368b5c2 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -272,6 +272,31 @@ module API
expose :id, :project_id, :group_id, :group_access
end
+ class Todo < Grape::Entity
+ expose :id
+ expose :project, using: Entities::BasicProjectDetails
+ expose :author, using: Entities::UserBasic
+ expose :action_name
+ expose :target_type
+
+ expose :target do |todo, options|
+ Entities.const_get(todo.target_type).represent(todo.target, options)
+ end
+
+ expose :target_url do |todo, options|
+ target_type = todo.target_type.underscore
+ target_url = "namespace_project_#{target_type}_url"
+ target_anchor = "note_#{todo.note_id}" if todo.note_id?
+
+ Gitlab::Application.routes.url_helpers.public_send(target_url,
+ todo.project.namespace, todo.project, todo.target, anchor: target_anchor)
+ end
+
+ expose :body
+ expose :state
+ expose :created_at
+ end
+
class Namespace < Grape::Entity
expose :id, :path, :kind
end
diff --git a/lib/api/todos.rb b/lib/api/todos.rb
new file mode 100644
index 00000000000..2a6bfa98ca4
--- /dev/null
+++ b/lib/api/todos.rb
@@ -0,0 +1,82 @@
+module API
+ # Todos API
+ class Todos < Grape::API
+ before { authenticate! }
+
+ ISSUABLE_TYPES = {
+ 'merge_requests' => ->(id) { user_project.merge_requests.find(id) },
+ 'issues' => ->(id) { find_project_issue(id) }
+ }
+
+ resource :projects do
+ ISSUABLE_TYPES.each do |type, finder|
+ type_id_str = "#{type.singularize}_id".to_sym
+
+ # Create a todo on an issuable
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # issuable_id (required) - The ID of an issuable
+ # Example Request:
+ # POST /projects/:id/issues/:issuable_id/todo
+ # POST /projects/:id/merge_requests/:issuable_id/todo
+ post ":id/#{type}/:#{type_id_str}/todo" do
+ issuable = instance_exec(params[type_id_str], &finder)
+ todo = TodoService.new.mark_todo(issuable, current_user).first
+
+ if todo
+ present todo, with: Entities::Todo, current_user: current_user
+ else
+ not_modified!
+ end
+ end
+ end
+ end
+
+ resource :todos do
+ helpers do
+ def find_todos
+ TodosFinder.new(current_user, params).execute
+ end
+ end
+
+ # Get a todo list
+ #
+ # Example Request:
+ # GET /todos
+ #
+ get do
+ todos = find_todos
+
+ present paginate(todos), with: Entities::Todo, current_user: current_user
+ end
+
+ # Mark a todo as done
+ #
+ # Parameters:
+ # id: (required) - The ID of the todo being marked as done
+ #
+ # Example Request:
+ # DELETE /todos/:id
+ #
+ delete ':id' do
+ todo = current_user.todos.find(params[:id])
+ todo.done
+
+ present todo, with: Entities::Todo, current_user: current_user
+ end
+
+ # Mark all todos as done
+ #
+ # Example Request:
+ # DELETE /todos
+ #
+ delete do
+ todos = find_todos
+ todos.each(&:done)
+
+ present paginate(Kaminari.paginate_array(todos)), with: Entities::Todo, current_user: current_user
+ end
+ end
+ end
+end
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index f426e27afed..7fc20cd5555 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -22,5 +22,9 @@ FactoryGirl.define do
trait :build_failed do
action { Todo::BUILD_FAILED }
end
+
+ trait :done do
+ state :done
+ end
end
end
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
new file mode 100644
index 00000000000..92a4fa216cd
--- /dev/null
+++ b/spec/requests/api/todos_spec.rb
@@ -0,0 +1,190 @@
+require 'spec_helper'
+
+describe API::Todos, api: true do
+ include ApiHelpers
+
+ let(:project_1) { create(:project) }
+ let(:project_2) { create(:project) }
+ let(:author_1) { create(:user) }
+ let(:author_2) { create(:user) }
+ let(:john_doe) { create(:user, username: 'john_doe') }
+ let(:merge_request) { create(:merge_request, source_project: project_1) }
+ let!(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe) }
+ let!(:pending_2) { create(:todo, project: project_2, author: author_2, user: john_doe) }
+ let!(:pending_3) { create(:todo, project: project_1, author: author_2, user: john_doe) }
+ let!(:done) { create(:todo, :done, project: project_1, author: author_1, user: john_doe) }
+
+ before do
+ project_1.team << [john_doe, :developer]
+ project_2.team << [john_doe, :developer]
+ end
+
+ describe 'GET /todos' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ get api('/todos')
+
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns an array of pending todos for current user' do
+ get api('/todos', john_doe)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(json_response[0]['id']).to eq(pending_3.id)
+ expect(json_response[0]['project']).to be_a Hash
+ expect(json_response[0]['author']).to be_a Hash
+ expect(json_response[0]['target_type']).to be_present
+ expect(json_response[0]['target']).to be_a Hash
+ expect(json_response[0]['target_url']).to be_present
+ expect(json_response[0]['body']).to be_present
+ expect(json_response[0]['state']).to eq('pending')
+ expect(json_response[0]['action_name']).to eq('assigned')
+ expect(json_response[0]['created_at']).to be_present
+ end
+
+ context 'and using the author filter' do
+ it 'filters based on author_id param' do
+ get api('/todos', john_doe), { author_id: author_2.id }
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ end
+ end
+
+ context 'and using the type filter' do
+ it 'filters based on type param' do
+ create(:todo, project: project_1, author: author_2, user: john_doe, target: merge_request)
+
+ get api('/todos', john_doe), { type: 'MergeRequest' }
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+ end
+
+ context 'and using the state filter' do
+ it 'filters based on state param' do
+ get api('/todos', john_doe), { state: 'done' }
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+ end
+
+ context 'and using the project filter' do
+ it 'filters based on project_id param' do
+ get api('/todos', john_doe), { project_id: project_2.id }
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+ end
+
+ context 'and using the action filter' do
+ it 'filters based on action param' do
+ get api('/todos', john_doe), { action: 'mentioned' }
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+ end
+ end
+ end
+
+ describe 'DELETE /todos/:id' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ delete api("/todos/#{pending_1.id}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'marks a todo as done' do
+ delete api("/todos/#{pending_1.id}", john_doe)
+
+ expect(response.status).to eq(200)
+ expect(pending_1.reload).to be_done
+ end
+ end
+ end
+
+ describe 'DELETE /todos' do
+ context 'when unauthenticated' do
+ it 'returns authentication error' do
+ delete api('/todos')
+
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'marks all todos as done' do
+ delete api('/todos', john_doe)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(3)
+ expect(pending_1.reload).to be_done
+ expect(pending_2.reload).to be_done
+ expect(pending_3.reload).to be_done
+ end
+ end
+ end
+
+ shared_examples 'an issuable' do |issuable_type|
+ it 'creates a todo on an issuable' do
+ post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", john_doe)
+
+ expect(response.status).to eq(201)
+ expect(json_response['project']).to be_a Hash
+ expect(json_response['author']).to be_a Hash
+ expect(json_response['target_type']).to eq(issuable.class.name)
+ expect(json_response['target']).to be_a Hash
+ expect(json_response['target_url']).to be_present
+ expect(json_response['body']).to be_present
+ expect(json_response['state']).to eq('pending')
+ expect(json_response['action_name']).to eq('marked')
+ expect(json_response['created_at']).to be_present
+ end
+
+ it 'returns 304 there already exist a todo on that issuable' do
+ create(:todo, project: project_1, author: author_1, user: john_doe, target: issuable)
+
+ post api("/projects/#{project_1.id}/#{issuable_type}/#{issuable.id}/todo", john_doe)
+
+ expect(response.status).to eq(304)
+ end
+
+ it 'returns 404 if the issuable is not found' do
+ post api("/projects/#{project_1.id}/#{issuable_type}/123/todo", john_doe)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ describe 'POST :id/issuable_type/:issueable_id/todo' do
+ context 'for an issue' do
+ it_behaves_like 'an issuable', 'issues' do
+ let(:issuable) { create(:issue, author: author_1, project: project_1) }
+ end
+ end
+
+ context 'for a merge request' do
+ it_behaves_like 'an issuable', 'merge_requests' do
+ let(:issuable) { merge_request }
+ end
+ end
+ end
+end