Merge branch 'api-delete-note' into 'master'
Delete notes via API Supports deleting issues, snippets, and merge requests via the API. * Closes #14944 * Closes #14845 * Closes #6060 @zj I did not see that you assigned yourself in #6060. Hopefully, you did not start yet. @rymai In #6060 this is targeted for 8.7 release. Could you review that and maybe this still lands in 8.7. See merge request !3557
This commit is contained in:
		
						commit
						e322b993e7
					
				| 
						 | 
					@ -24,6 +24,7 @@ v 8.7.0 (unreleased)
 | 
				
			||||||
  - Ensure empty recipients are rejected in BuildsEmailService
 | 
					  - Ensure empty recipients are rejected in BuildsEmailService
 | 
				
			||||||
  - API: Ability to filter milestones by state `active` and `closed` (Robert Schilling)
 | 
					  - API: Ability to filter milestones by state `active` and `closed` (Robert Schilling)
 | 
				
			||||||
  - API: Fix milestone filtering by `iid` (Robert Schilling)
 | 
					  - API: Fix milestone filtering by `iid` (Robert Schilling)
 | 
				
			||||||
 | 
					  - API: Delete notes of issues, snippets, and merge requests (Robert Schilling)
 | 
				
			||||||
  - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
 | 
					  - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
 | 
				
			||||||
  - Better errors handling when creating milestones inside groups
 | 
					  - Better errors handling when creating milestones inside groups
 | 
				
			||||||
  - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
 | 
					  - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,8 +39,7 @@ class Projects::NotesController < Projects::ApplicationController
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def destroy
 | 
					  def destroy
 | 
				
			||||||
    if note.editable?
 | 
					    if note.editable?
 | 
				
			||||||
      note.destroy
 | 
					      Notes::DeleteService.new(project, current_user).execute(note)
 | 
				
			||||||
      note.reset_events_cache
 | 
					 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    respond_to do |format|
 | 
					    respond_to do |format|
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,8 @@
 | 
				
			||||||
 | 
					module Notes
 | 
				
			||||||
 | 
					  class DeleteService < BaseService
 | 
				
			||||||
 | 
					    def execute(note)
 | 
				
			||||||
 | 
					      note.destroy
 | 
				
			||||||
 | 
					      note.reset_events_cache
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
							
								
								
									
										141
									
								
								doc/api/notes.md
								
								
								
								
							
							
						
						
									
										141
									
								
								doc/api/notes.md
								
								
								
								
							| 
						 | 
					@ -105,6 +105,53 @@ Parameters:
 | 
				
			||||||
- `note_id` (required) - The ID of a note
 | 
					- `note_id` (required) - The ID of a note
 | 
				
			||||||
- `body` (required) - The content of a note
 | 
					- `body` (required) - The content of a note
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Delete an issue note
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Deletes an existing note of an issue. On success, this API method returns 200
 | 
				
			||||||
 | 
					and the deleted note. If the note does not exist, the API returns 404.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					DELETE /projects/:id/issues/:issue_id/notes/:note_id
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Parameters:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| Attribute | Type | Required | Description |
 | 
				
			||||||
 | 
					| --------- | ---- | -------- | ----------- |
 | 
				
			||||||
 | 
					| `id` | integer | yes | The ID of a project |
 | 
				
			||||||
 | 
					| `issue_id` | integer | yes | The ID of an issue |
 | 
				
			||||||
 | 
					| `note_id` | integer | yes | The ID of a note |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/issues/11/notes/636
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Example Response:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```json
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "id": 636,
 | 
				
			||||||
 | 
					  "body": "This is a good idea.",
 | 
				
			||||||
 | 
					  "attachment": null,
 | 
				
			||||||
 | 
					  "author": {
 | 
				
			||||||
 | 
					    "id": 1,
 | 
				
			||||||
 | 
					    "username": "pipin",
 | 
				
			||||||
 | 
					    "email": "admin@example.com",
 | 
				
			||||||
 | 
					    "name": "Pip",
 | 
				
			||||||
 | 
					    "state": "active",
 | 
				
			||||||
 | 
					    "created_at": "2013-09-30T13:46:01Z",
 | 
				
			||||||
 | 
					    "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
 | 
				
			||||||
 | 
					    "web_url": "https://gitlab.example.com/u/pipin"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "created_at": "2016-04-05T22:10:44.164Z",
 | 
				
			||||||
 | 
					  "system": false,
 | 
				
			||||||
 | 
					  "noteable_id": 11,
 | 
				
			||||||
 | 
					  "noteable_type": "Issue",
 | 
				
			||||||
 | 
					  "upvote": false,
 | 
				
			||||||
 | 
					  "downvote": false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Snippets
 | 
					## Snippets
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### List all snippet notes
 | 
					### List all snippet notes
 | 
				
			||||||
| 
						 | 
					@ -182,6 +229,53 @@ Parameters:
 | 
				
			||||||
- `note_id` (required) - The ID of a note
 | 
					- `note_id` (required) - The ID of a note
 | 
				
			||||||
- `body` (required) - The content of a note
 | 
					- `body` (required) - The content of a note
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Delete a snippet note
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Deletes an existing note of a snippet. On success, this API method returns 200
 | 
				
			||||||
 | 
					and the deleted note. If the note does not exist, the API returns 404.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					DELETE /projects/:id/snippets/:snippet_id/notes/:note_id
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Parameters:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| Attribute | Type | Required | Description |
 | 
				
			||||||
 | 
					| --------- | ---- | -------- | ----------- |
 | 
				
			||||||
 | 
					| `id` | integer | yes | The ID of a project |
 | 
				
			||||||
 | 
					| `snippet_id` | integer | yes | The ID of a snippet |
 | 
				
			||||||
 | 
					| `note_id` | integer | yes | The ID of a note |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/snippets/52/notes/1659
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Example Response:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```json
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "id": 1659,
 | 
				
			||||||
 | 
					  "body": "This is a good idea.",
 | 
				
			||||||
 | 
					  "attachment": null,
 | 
				
			||||||
 | 
					  "author": {
 | 
				
			||||||
 | 
					    "id": 1,
 | 
				
			||||||
 | 
					    "username": "pipin",
 | 
				
			||||||
 | 
					    "email": "admin@example.com",
 | 
				
			||||||
 | 
					    "name": "Pip",
 | 
				
			||||||
 | 
					    "state": "active",
 | 
				
			||||||
 | 
					    "created_at": "2013-09-30T13:46:01Z",
 | 
				
			||||||
 | 
					    "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
 | 
				
			||||||
 | 
					    "web_url": "https://gitlab.example.com/u/pipin"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "created_at": "2016-04-06T16:51:53.239Z",
 | 
				
			||||||
 | 
					  "system": false,
 | 
				
			||||||
 | 
					  "noteable_id": 52,
 | 
				
			||||||
 | 
					  "noteable_type": "Snippet",
 | 
				
			||||||
 | 
					  "upvote": false,
 | 
				
			||||||
 | 
					  "downvote": false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Merge Requests
 | 
					## Merge Requests
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### List all merge request notes
 | 
					### List all merge request notes
 | 
				
			||||||
| 
						 | 
					@ -262,3 +356,50 @@ Parameters:
 | 
				
			||||||
- `merge_request_id` (required) - The ID of a merge request
 | 
					- `merge_request_id` (required) - The ID of a merge request
 | 
				
			||||||
- `note_id` (required) - The ID of a note
 | 
					- `note_id` (required) - The ID of a note
 | 
				
			||||||
- `body` (required) - The content of a note
 | 
					- `body` (required) - The content of a note
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Delete a merge request note
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Deletes an existing note of a merge request. On success, this API method returns
 | 
				
			||||||
 | 
					200 and the deleted note. If the note does not exist, the API returns 404.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					DELETE /projects/:id/merge_requests/:merge_request_id/notes/:note_id
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Parameters:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					| Attribute | Type | Required | Description |
 | 
				
			||||||
 | 
					| --------- | ---- | -------- | ----------- |
 | 
				
			||||||
 | 
					| `id` | integer | yes | The ID of a project |
 | 
				
			||||||
 | 
					| `merge_request_id` | integer | yes | The ID of a merge request |
 | 
				
			||||||
 | 
					| `note_id` | integer | yes | The ID of a note |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					curl -X DELETE -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/merge_requests/7/notes/1602
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Example Response:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```json
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "id": 1602,
 | 
				
			||||||
 | 
					  "body": "This is a good idea.",
 | 
				
			||||||
 | 
					  "attachment": null,
 | 
				
			||||||
 | 
					  "author": {
 | 
				
			||||||
 | 
					    "id": 1,
 | 
				
			||||||
 | 
					    "username": "pipin",
 | 
				
			||||||
 | 
					    "email": "admin@example.com",
 | 
				
			||||||
 | 
					    "name": "Pip",
 | 
				
			||||||
 | 
					    "state": "active",
 | 
				
			||||||
 | 
					    "created_at": "2013-09-30T13:46:01Z",
 | 
				
			||||||
 | 
					    "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon",
 | 
				
			||||||
 | 
					    "web_url": "https://gitlab.example.com/u/pipin"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "created_at": "2016-04-05T22:11:59.923Z",
 | 
				
			||||||
 | 
					  "system": false,
 | 
				
			||||||
 | 
					  "noteable_id": 7,
 | 
				
			||||||
 | 
					  "noteable_type": "MergeRequest",
 | 
				
			||||||
 | 
					  "upvote": false,
 | 
				
			||||||
 | 
					  "downvote": false
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -112,6 +112,23 @@ module API
 | 
				
			||||||
          end
 | 
					          end
 | 
				
			||||||
        end
 | 
					        end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Delete a +noteable+ note
 | 
				
			||||||
 | 
					        #
 | 
				
			||||||
 | 
					        # Parameters:
 | 
				
			||||||
 | 
					        #   id (required) - The ID of a project
 | 
				
			||||||
 | 
					        #   noteable_id (required) - The ID of an issue, MR, or snippet
 | 
				
			||||||
 | 
					        #   node_id (required) - The ID of a note
 | 
				
			||||||
 | 
					        # Example Request:
 | 
				
			||||||
 | 
					        #   DELETE /projects/:id/issues/:noteable_id/notes/:note_id
 | 
				
			||||||
 | 
					        #   DELETE /projects/:id/snippets/:noteable_id/notes/:node_id
 | 
				
			||||||
 | 
					        delete ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do
 | 
				
			||||||
 | 
					          note = user_project.notes.find(params[:note_id])
 | 
				
			||||||
 | 
					          authorize! :admin_note, note
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          ::Notes::DeleteService.new(user_project, current_user).execute(note)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          present note, with: Entities::Note
 | 
				
			||||||
 | 
					        end
 | 
				
			||||||
      end
 | 
					      end
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -241,4 +241,65 @@ describe API::API, api: true  do
 | 
				
			||||||
    end
 | 
					    end
 | 
				
			||||||
  end
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  describe 'DELETE /projects/:id/noteable/:noteable_id/notes/:note_id' do
 | 
				
			||||||
 | 
					    context 'when noteable is an Issue' do
 | 
				
			||||||
 | 
					      it 'deletes a note' do
 | 
				
			||||||
 | 
					        delete api("/projects/#{project.id}/issues/#{issue.id}/"\
 | 
				
			||||||
 | 
					                   "notes/#{issue_note.id}", user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response.status).to eq(200)
 | 
				
			||||||
 | 
					        # Check if note is really deleted
 | 
				
			||||||
 | 
					        delete api("/projects/#{project.id}/issues/#{issue.id}/"\
 | 
				
			||||||
 | 
					                   "notes/#{issue_note.id}", user)
 | 
				
			||||||
 | 
					        expect(response.status).to eq(404)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'returns a 404 error when note id not found' do
 | 
				
			||||||
 | 
					        delete api("/projects/#{project.id}/issues/#{issue.id}/notes/123", user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response.status).to eq(404)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when noteable is a Snippet' do
 | 
				
			||||||
 | 
					      it 'deletes a note' do
 | 
				
			||||||
 | 
					        delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\
 | 
				
			||||||
 | 
					                   "notes/#{snippet_note.id}", user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response.status).to eq(200)
 | 
				
			||||||
 | 
					        # Check if note is really deleted
 | 
				
			||||||
 | 
					        delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\
 | 
				
			||||||
 | 
					                   "notes/#{snippet_note.id}", user)
 | 
				
			||||||
 | 
					        expect(response.status).to eq(404)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'returns a 404 error when note id not found' do
 | 
				
			||||||
 | 
					        delete api("/projects/#{project.id}/snippets/#{snippet.id}/"\
 | 
				
			||||||
 | 
					                   "notes/123", user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response.status).to eq(404)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    context 'when noteable is a Merge Request' do
 | 
				
			||||||
 | 
					      it 'deletes a note' do
 | 
				
			||||||
 | 
					        delete api("/projects/#{project.id}/merge_requests/"\
 | 
				
			||||||
 | 
					                   "#{merge_request.id}/notes/#{merge_request_note.id}", user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response.status).to eq(200)
 | 
				
			||||||
 | 
					        # Check if note is really deleted
 | 
				
			||||||
 | 
					        delete api("/projects/#{project.id}/merge_requests/"\
 | 
				
			||||||
 | 
					                   "#{merge_request.id}/notes/#{merge_request_note.id}", user)
 | 
				
			||||||
 | 
					        expect(response.status).to eq(404)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it 'returns a 404 error when note id not found' do
 | 
				
			||||||
 | 
					        delete api("/projects/#{project.id}/merge_requests/"\
 | 
				
			||||||
 | 
					                   "#{merge_request.id}/notes/123", user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        expect(response.status).to eq(404)
 | 
				
			||||||
 | 
					      end
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					
 | 
				
			||||||
end
 | 
					end
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,15 @@
 | 
				
			||||||
 | 
					require 'spec_helper'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe Notes::DeleteService, services: true do
 | 
				
			||||||
 | 
					  describe '#execute' do
 | 
				
			||||||
 | 
					    it 'deletes a note' do
 | 
				
			||||||
 | 
					      project = create(:empty_project)
 | 
				
			||||||
 | 
					      issue = create(:issue, project: project)
 | 
				
			||||||
 | 
					      note = create(:note, project: project, noteable: issue)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      described_class.new(project, note.author).execute(note)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      expect(project.issues.find(issue.id).notes).not_to include(note)
 | 
				
			||||||
 | 
					    end
 | 
				
			||||||
 | 
					  end
 | 
				
			||||||
 | 
					end
 | 
				
			||||||
		Loading…
	
		Reference in New Issue