Commit c8560894 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'api-subscribe-issue-mr' into 'master'

API: Subscribe and unsubscribe from issues and merge requests

* Closes #6024 

This MR is based on !3611, which must be merged first.


See merge request !3615
parents 3463ffde a9200d93
...@@ -12,6 +12,7 @@ v 8.7.0 (unreleased) ...@@ -12,6 +12,7 @@ v 8.7.0 (unreleased)
- Fix `signed_in_ip` being set to 127.0.0.1 when using a reverse proxy !3524 - Fix `signed_in_ip` being set to 127.0.0.1 when using a reverse proxy !3524
- Improved Markdown rendering performance !3389 (Yorick Peterse) - Improved Markdown rendering performance !3389 (Yorick Peterse)
- Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu) - Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu)
- API: Ability to subscribe and unsubscribe from issues and merge requests (Robert Schilling)
- Expose project badges in project settings - Expose project badges in project settings
- Preserve time notes/comments have been updated at when moving issue - Preserve time notes/comments have been updated at when moving issue
- Make HTTP(s) label consistent on clone bar (Stan Hu) - Make HTTP(s) label consistent on clone bar (Stan Hu)
......
...@@ -406,6 +406,115 @@ Example response: ...@@ -406,6 +406,115 @@ Example response:
} }
``` ```
## Subscribe to an issue
Subscribes the authenticated user to an issue to receive notifications. If the
operation is successful, status code `201` together with the updated issue is
returned. If the user is already subscribed to the issue, the status code `304`
is returned. If the project or issue is not found, status code `404` is
returned.
```
POST /projects/:id/issues/:issue_id/subscription
```
| 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/subscription
```
Example response:
```json
{
"id": 92,
"iid": 11,
"project_id": 5,
"title": "Sit voluptas tempora quisquam aut doloribus et.",
"description": "Repellat voluptas quibusdam voluptatem exercitationem.",
"state": "opened",
"created_at": "2016-04-05T21:41:45.652Z",
"updated_at": "2016-04-07T12:20:17.596Z",
"labels": [],
"milestone": null,
"assignee": {
"name": "Miss Monserrate Beier",
"username": "axel.block",
"id": 12,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/axel.block"
},
"author": {
"name": "Kris Steuber",
"username": "solon.cremin",
"id": 10,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/solon.cremin"
}
}
```
## Unsubscribe from an issue
Unsubscribes the authenticated user from the issue to not receive notifications
from it. If the operation is successful, status code `200` together with the
updated issue is returned. If the user is not subscribed to the issue, the
status code `304` is returned. If the project or issue is not found, status code
`404` is returned.
```
DELETE /projects/:id/issues/:issue_id/subscription
```
| 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 DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/93/subscription
```
Example response:
```json
{
"id": 93,
"iid": 12,
"project_id": 5,
"title": "Incidunt et rerum ea expedita iure quibusdam.",
"description": "Et cumque architecto sed aut ipsam.",
"state": "opened",
"created_at": "2016-04-05T21:41:45.217Z",
"updated_at": "2016-04-07T13:02:37.905Z",
"labels": [],
"milestone": null,
"assignee": {
"name": "Edwardo Grady",
"username": "keyon",
"id": 21,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/keyon"
},
"author": {
"name": "Vivian Hermann",
"username": "orville",
"id": 11,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
"web_url": "http://lgitlab.example.com/u/orville"
},
"subscribed": false
}
```
## Comments on issues ## Comments on issues
Comments are done via the [notes](notes.md) resource. Comments are done via the [notes](notes.md) resource.
...@@ -606,3 +606,151 @@ Example response: ...@@ -606,3 +606,151 @@ Example response:
}, },
] ]
``` ```
## Subscribe to a merge request
Subscribes the authenticated user to a merge request to receive notification. If
the operation is successful, status code `201` together with the updated merge
request is returned. If the user is already subscribed to the merge request, the
status code `304` is returned. If the project or merge request is not found,
status code `404` is returned.
```
POST /projects/:id/merge_requests/:merge_request_id/subscription
```
| 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/17/subscription
```
Example response:
```json
{
"id": 17,
"iid": 1,
"project_id": 5,
"title": "Et et sequi est impedit nulla ut rem et voluptatem.",
"description": "Consequatur velit eos rerum optio autem. Quia id officia quaerat dolorum optio. Illo laudantium aut ipsum dolorem.",
"state": "opened",
"created_at": "2016-04-05T21:42:23.233Z",
"updated_at": "2016-04-05T22:11:52.900Z",
"target_branch": "ui-dev-kit",
"source_branch": "version-1-9",
"upvotes": 0,
"downvotes": 0,
"author": {
"name": "Eileen Skiles",
"username": "leila",
"id": 19,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/leila"
},
"assignee": {
"name": "Celine Wehner",
"username": "carli",
"id": 16,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/carli"
},
"source_project_id": 5,
"target_project_id": 5,
"labels": [],
"work_in_progress": false,
"milestone": {
"id": 7,
"iid": 1,
"project_id": 5,
"title": "v2.0",
"description": "Corrupti eveniet et velit occaecati dolorem est rerum aut.",
"state": "closed",
"created_at": "2016-04-05T21:41:40.905Z",
"updated_at": "2016-04-05T21:41:40.905Z",
"due_date": null
},
"merge_when_build_succeeds": false,
"merge_status": "cannot_be_merged",
"subscribed": true
}
```
## Unsubscribe from a merge request
Unsubscribes the authenticated user from a merge request to not receive
notifications from that merge request. If the operation is successful, status
code `200` together with the updated merge request is returned. If the user is
not subscribed to the merge request, the status code `304` is returned. If the
project or merge request is not found, status code `404` is returned.
```
DELETE /projects/:id/merge_requests/:merge_request_id/subscription
```
| 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 DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/17/subscription
```
Example response:
```json
{
"id": 17,
"iid": 1,
"project_id": 5,
"title": "Et et sequi est impedit nulla ut rem et voluptatem.",
"description": "Consequatur velit eos rerum optio autem. Quia id officia quaerat dolorum optio. Illo laudantium aut ipsum dolorem.",
"state": "opened",
"created_at": "2016-04-05T21:42:23.233Z",
"updated_at": "2016-04-05T22:11:52.900Z",
"target_branch": "ui-dev-kit",
"source_branch": "version-1-9",
"upvotes": 0,
"downvotes": 0,
"author": {
"name": "Eileen Skiles",
"username": "leila",
"id": 19,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/39ce4a2822cc896933ffbd68c1470e55?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/leila"
},
"assignee": {
"name": "Celine Wehner",
"username": "carli",
"id": 16,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/f4cd5605b769dd2ce405a27c6e6f2684?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/carli"
},
"source_project_id": 5,
"target_project_id": 5,
"labels": [],
"work_in_progress": false,
"milestone": {
"id": 7,
"iid": 1,
"project_id": 5,
"title": "v2.0",
"description": "Corrupti eveniet et velit occaecati dolorem est rerum aut.",
"state": "closed",
"created_at": "2016-04-05T21:41:40.905Z",
"updated_at": "2016-04-05T21:41:40.905Z",
"due_date": null
},
"merge_when_build_succeeds": false,
"merge_status": "cannot_be_merged",
"subscribed": false
}
```
...@@ -231,6 +231,42 @@ module API ...@@ -231,6 +231,42 @@ module API
authorize!(:destroy_issue, issue) authorize!(:destroy_issue, issue)
issue.destroy issue.destroy
end end
# Subscribes to a project issue
#
# Parameters:
# id (required) - The ID of a project
# issue_id (required) - The ID of a project issue
# Example Request:
# POST /projects/:id/issues/:issue_id/subscription
post ':id/issues/:issue_id/subscription' do
issue = user_project.issues.find(params[:issue_id])
if issue.subscribed?(current_user)
not_modified!
else
issue.toggle_subscription(current_user)
present issue, with: Entities::Issue, current_user: current_user
end
end
# Unsubscribes from a project issue
#
# Parameters:
# id (required) - The ID of a project
# issue_id (required) - The ID of a project issue
# Example Request:
# DELETE /projects/:id/issues/:issue_id/subscription
delete ':id/issues/:issue_id/subscription' do
issue = user_project.issues.find(params[:issue_id])
if issue.subscribed?(current_user)
issue.unsubscribe(current_user)
present issue, with: Entities::Issue, current_user: current_user
else
not_modified!
end
end
end end
end end
end end
...@@ -327,6 +327,42 @@ module API ...@@ -327,6 +327,42 @@ module API
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
present paginate(issues), with: Entities::Issue, current_user: current_user present paginate(issues), with: Entities::Issue, current_user: current_user
end end
# Subscribes to a merge request
#
# Parameters:
# id (required) - The ID of a project
# merge_request_id (required) - The ID of a merge request
# Example Request:
# POST /projects/:id/issues/:merge_request_id/subscription
post "#{path}/subscription" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
if merge_request.subscribed?(current_user)
not_modified!
else
merge_request.toggle_subscription(current_user)
present merge_request, with: Entities::MergeRequest, current_user: current_user
end
end
# Unsubscribes from a merge request
#
# Parameters:
# id (required) - The ID of a project
# merge_request_id (required) - The ID of a merge request
# Example Request:
# DELETE /projects/:id/merge_requests/:merge_request_id/subscription
delete "#{path}/subscription" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
if merge_request.subscribed?(current_user)
merge_request.unsubscribe(current_user)
present merge_request, with: Entities::MergeRequest, current_user: current_user
else
not_modified!
end
end
end end
end end
end end
......
...@@ -3,6 +3,7 @@ require 'spec_helper' ...@@ -3,6 +3,7 @@ require 'spec_helper'
describe API::API, api: true do describe API::API, api: true do
include ApiHelpers include ApiHelpers
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:non_member) { create(:user) } let(:non_member) { create(:user) }
let(:author) { create(:author) } let(:author) { create(:author) }
let(:assignee) { create(:assignee) } let(:assignee) { create(:assignee) }
...@@ -569,4 +570,46 @@ describe API::API, api: true do ...@@ -569,4 +570,46 @@ describe API::API, api: true do
end end
end end
end end
describe 'POST :id/issues/:issue_id/subscription' do
it 'subscribes to an issue' do
post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
expect(response.status).to eq(201)
expect(json_response['subscribed']).to eq(true)
end
it 'returns 304 if already subscribed' do
post api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
expect(response.status).to eq(304)
end
it 'returns 404 if the issue is not found' do
post api("/projects/#{project.id}/issues/123/subscription", user)
expect(response.status).to eq(404)
end
end
describe 'DELETE :id/issues/:issue_id/subscription' do
it 'unsubscribes from an issue' do
delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user)
expect(response.status).to eq(200)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
delete api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2)
expect(response.status).to eq(304)
end
it 'returns 404 if the issue is not found' do
delete api("/projects/#{project.id}/issues/123/subscription", user)
expect(response.status).to eq(404)
end
end
end end
...@@ -516,6 +516,48 @@ describe API::API, api: true do ...@@ -516,6 +516,48 @@ describe API::API, api: true do
end end
end end
describe 'POST :id/merge_requests/:merge_request_id/subscription' do
it 'subscribes to a merge request' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
expect(response.status).to eq(201)
expect(json_response['subscribed']).to eq(true)
end
it 'returns 304 if already subscribed' do
post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
expect(response.status).to eq(304)
end
it 'returns 404 if the merge request is not found' do
post api("/projects/#{project.id}/merge_requests/123/subscription", user)
expect(response.status).to eq(404)
end
end
describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do
it 'unsubscribes from a merge request' do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user)
expect(response.status).to eq(200)
expect(json_response['subscribed']).to eq(false)
end
it 'returns 304 if not subscribed' do
delete api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin)
expect(response.status).to eq(304)
end
it 'returns 404 if the merge request is not found' do
post api("/projects/#{project.id}/merge_requests/123/subscription", user)
expect(response.status).to eq(404)
end
end
def mr_with_later_created_and_updated_at_time def mr_with_later_created_and_updated_at_time
merge_request merge_request
merge_request.created_at += 1.hour merge_request.created_at += 1.hour
......
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