Commit 2d17d6f1bc4ddfeb001dacbc427fa3dc9af694e4
Exists in
master
and in
4 other branches
Merge pull request #2123 from NARKOZ/notes-api
Notes API
Showing
8 changed files
with
326 additions
and
17 deletions
Show diff stats
CHANGELOG
| 1 | 1 | v 3.2.0 |
| 2 | + - [API] create notes for snippets and issues | |
| 3 | + - [API] list notes for snippets and issues | |
| 4 | + - [API] list project wall notes | |
| 2 | 5 | - Remove project code - use path instead |
| 3 | 6 | - added username field to user |
| 4 | 7 | - rake task to fill usernames based on emails create namespaces for users |
| ... | ... | @@ -10,7 +13,7 @@ v 3.2.0 |
| 10 | 13 | - Fixes commit patches getting escaped (see #2036) |
| 11 | 14 | - Support diff and patch generation for commits and merge request |
| 12 | 15 | - MergeReqest doesn't generate a temporary file for the patch any more |
| 13 | - - Update the UI to allow downloading Patch or Diff | |
| 16 | + - Update the UI to allow downloading Patch or Diff | |
| 14 | 17 | |
| 15 | 18 | v 3.1.0 |
| 16 | 19 | - Updated gems |
| ... | ... | @@ -48,7 +51,7 @@ v 3.0.0 |
| 48 | 51 | - Fixed bug with gitolite keys |
| 49 | 52 | - UI improved |
| 50 | 53 | - Increased perfomance of application |
| 51 | - - Show user avatar in last commit when browsing Files | |
| 54 | + - Show user avatar in last commit when browsing Files | |
| 52 | 55 | - Refactored Gitlab::Merge |
| 53 | 56 | - Use Font Awsome for icons |
| 54 | 57 | - Separate observing of Note and MergeRequestsa | ... | ... |
app/views/help/api.html.haml
| ... | ... | @@ -21,6 +21,8 @@ |
| 21 | 21 | = link_to "Issues", "#issues", 'data-toggle' => 'tab' |
| 22 | 22 | %li |
| 23 | 23 | = link_to "Milestones", "#milestones", 'data-toggle' => 'tab' |
| 24 | + %li | |
| 25 | + = link_to "Notes", "#notes", 'data-toggle' => 'tab' | |
| 24 | 26 | |
| 25 | 27 | .tab-content |
| 26 | 28 | .tab-pane.active#README |
| ... | ... | @@ -94,3 +96,12 @@ |
| 94 | 96 | .file_content.wiki |
| 95 | 97 | = preserve do |
| 96 | 98 | = markdown File.read(Rails.root.join("doc", "api", "milestones.md")) |
| 99 | + | |
| 100 | + .tab-pane#notes | |
| 101 | + .file_holder | |
| 102 | + .file_title | |
| 103 | + %i.icon-file | |
| 104 | + Notes | |
| 105 | + .file_content.wiki | |
| 106 | + = preserve do | |
| 107 | + = markdown File.read(Rails.root.join("doc", "api", "notes.md")) | ... | ... |
| ... | ... | @@ -0,0 +1,121 @@ |
| 1 | +## List notes | |
| 2 | + | |
| 3 | +### List project wall notes | |
| 4 | + | |
| 5 | +Get a list of project wall notes. | |
| 6 | + | |
| 7 | +``` | |
| 8 | +GET /projects/:id/notes | |
| 9 | +``` | |
| 10 | + | |
| 11 | +```json | |
| 12 | +[ | |
| 13 | + { | |
| 14 | + "id": 522, | |
| 15 | + "body": "The solution is rather tricky", | |
| 16 | + "author": { | |
| 17 | + "id": 1, | |
| 18 | + "email": "john@example.com", | |
| 19 | + "name": "John Smith", | |
| 20 | + "blocked": false, | |
| 21 | + "created_at": "2012-05-23T08:00:58Z" | |
| 22 | + }, | |
| 23 | + "updated_at":"2012-11-27T19:16:44Z", | |
| 24 | + "created_at":"2012-11-27T19:16:44Z" | |
| 25 | + } | |
| 26 | +] | |
| 27 | +``` | |
| 28 | + | |
| 29 | +Parameters: | |
| 30 | + | |
| 31 | ++ `id` (required) - The ID or code name of a project | |
| 32 | + | |
| 33 | +### List issue notes | |
| 34 | + | |
| 35 | +Get a list of issue notes. | |
| 36 | + | |
| 37 | +``` | |
| 38 | +GET /projects/:id/issues/:issue_id/notes | |
| 39 | +``` | |
| 40 | + | |
| 41 | +Parameters: | |
| 42 | + | |
| 43 | ++ `id` (required) - The ID or code name of a project | |
| 44 | ++ `issue_id` (required) - The ID of an issue | |
| 45 | + | |
| 46 | +### List snippet notes | |
| 47 | + | |
| 48 | +Get a list of snippet notes. | |
| 49 | + | |
| 50 | +``` | |
| 51 | +GET /projects/:id/snippets/:snippet_id/notes | |
| 52 | +``` | |
| 53 | + | |
| 54 | +Parameters: | |
| 55 | + | |
| 56 | ++ `id` (required) - The ID or code name of a project | |
| 57 | ++ `snippet_id` (required) - The ID of a snippet | |
| 58 | + | |
| 59 | +## Single note | |
| 60 | + | |
| 61 | +### Single issue note | |
| 62 | + | |
| 63 | +Get an issue note. | |
| 64 | + | |
| 65 | +``` | |
| 66 | +GET /projects/:id/issues/:issue_id/:notes/:note_id | |
| 67 | +``` | |
| 68 | + | |
| 69 | +Parameters: | |
| 70 | + | |
| 71 | ++ `id` (required) - The ID or code name of a project | |
| 72 | ++ `issue_id` (required) - The ID of a project issue | |
| 73 | ++ `note_id` (required) - The ID of an issue note | |
| 74 | + | |
| 75 | +### Single snippet note | |
| 76 | + | |
| 77 | +Get a snippet note. | |
| 78 | + | |
| 79 | +``` | |
| 80 | +GET /projects/:id/issues/:snippet_id/:notes/:note_id | |
| 81 | +``` | |
| 82 | + | |
| 83 | +Parameters: | |
| 84 | + | |
| 85 | ++ `id` (required) - The ID or code name of a project | |
| 86 | ++ `snippet_id` (required) - The ID of a project snippet | |
| 87 | ++ `note_id` (required) - The ID of an snippet note | |
| 88 | + | |
| 89 | +## New note | |
| 90 | + | |
| 91 | +### New issue note | |
| 92 | + | |
| 93 | +Create a new issue note. | |
| 94 | + | |
| 95 | +``` | |
| 96 | +POST /projects/:id/issues/:issue_id/notes | |
| 97 | +``` | |
| 98 | + | |
| 99 | +Parameters: | |
| 100 | + | |
| 101 | ++ `id` (required) - The ID or code name of a project | |
| 102 | ++ `issue_id` (required) - The ID of an issue | |
| 103 | ++ `body` (required) - The content of a note | |
| 104 | + | |
| 105 | +Will return created note with status `201 Created` on success, or `404 Not found` on fail. | |
| 106 | + | |
| 107 | +### New snippet note | |
| 108 | + | |
| 109 | +Create a new snippet note. | |
| 110 | + | |
| 111 | +``` | |
| 112 | +POST /projects/:id/snippets/:snippet_id/notes | |
| 113 | +``` | |
| 114 | + | |
| 115 | +Parameters: | |
| 116 | + | |
| 117 | ++ `id` (required) - The ID or code name of a project | |
| 118 | ++ `snippet_id` (required) - The ID of an snippet | |
| 119 | ++ `body` (required) - The content of a note | |
| 120 | + | |
| 121 | +Will return created note with status `201 Created` on success, or `404 Not found` on fail. | ... | ... |
lib/api.rb
lib/api/entities.rb
| ... | ... | @@ -70,8 +70,15 @@ module Gitlab |
| 70 | 70 | end |
| 71 | 71 | |
| 72 | 72 | class Note < Grape::Entity |
| 73 | + expose :id | |
| 74 | + expose :note, as: :body | |
| 73 | 75 | expose :author, using: Entities::UserBasic |
| 76 | + expose :updated_at, :created_at | |
| 77 | + end | |
| 78 | + | |
| 79 | + class MRNote < Grape::Entity | |
| 74 | 80 | expose :note |
| 81 | + expose :author, using: Entities::UserBasic | |
| 75 | 82 | end |
| 76 | 83 | end |
| 77 | 84 | end | ... | ... |
lib/api/merge_requests.rb
| ... | ... | @@ -4,9 +4,9 @@ module Gitlab |
| 4 | 4 | before { authenticate! } |
| 5 | 5 | |
| 6 | 6 | resource :projects do |
| 7 | - | |
| 7 | + | |
| 8 | 8 | # List merge requests |
| 9 | - # | |
| 9 | + # | |
| 10 | 10 | # Parameters: |
| 11 | 11 | # id (required) - The ID or code name of a project |
| 12 | 12 | # |
| ... | ... | @@ -15,24 +15,24 @@ module Gitlab |
| 15 | 15 | # |
| 16 | 16 | get ":id/merge_requests" do |
| 17 | 17 | authorize! :read_merge_request, user_project |
| 18 | - | |
| 18 | + | |
| 19 | 19 | present paginate(user_project.merge_requests), with: Entities::MergeRequest |
| 20 | 20 | end |
| 21 | - | |
| 21 | + | |
| 22 | 22 | # Show MR |
| 23 | - # | |
| 23 | + # | |
| 24 | 24 | # Parameters: |
| 25 | 25 | # id (required) - The ID or code name of a project |
| 26 | 26 | # merge_request_id (required) - The ID of MR |
| 27 | - # | |
| 27 | + # | |
| 28 | 28 | # Example: |
| 29 | 29 | # GET /projects/:id/merge_request/:merge_request_id |
| 30 | 30 | # |
| 31 | 31 | get ":id/merge_request/:merge_request_id" do |
| 32 | 32 | merge_request = user_project.merge_requests.find(params[:merge_request_id]) |
| 33 | - | |
| 33 | + | |
| 34 | 34 | authorize! :read_merge_request, merge_request |
| 35 | - | |
| 35 | + | |
| 36 | 36 | present merge_request, with: Entities::MergeRequest |
| 37 | 37 | end |
| 38 | 38 | |
| ... | ... | @@ -45,17 +45,17 @@ module Gitlab |
| 45 | 45 | # target_branch (required) - The target branch |
| 46 | 46 | # assignee_id - Assignee user ID |
| 47 | 47 | # title (required) - Title of MR |
| 48 | - # | |
| 48 | + # | |
| 49 | 49 | # Example: |
| 50 | 50 | # POST /projects/:id/merge_requests |
| 51 | 51 | # |
| 52 | 52 | post ":id/merge_requests" do |
| 53 | 53 | authorize! :write_merge_request, user_project |
| 54 | - | |
| 54 | + | |
| 55 | 55 | attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title] |
| 56 | 56 | merge_request = user_project.merge_requests.new(attrs) |
| 57 | 57 | merge_request.author = current_user |
| 58 | - | |
| 58 | + | |
| 59 | 59 | if merge_request.save |
| 60 | 60 | merge_request.reload_code |
| 61 | 61 | present merge_request, with: Entities::MergeRequest |
| ... | ... | @@ -80,9 +80,9 @@ module Gitlab |
| 80 | 80 | put ":id/merge_request/:merge_request_id" do |
| 81 | 81 | attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :closed] |
| 82 | 82 | merge_request = user_project.merge_requests.find(params[:merge_request_id]) |
| 83 | - | |
| 83 | + | |
| 84 | 84 | authorize! :modify_merge_request, merge_request |
| 85 | - | |
| 85 | + | |
| 86 | 86 | if merge_request.update_attributes attrs |
| 87 | 87 | merge_request.reload_code |
| 88 | 88 | merge_request.mark_as_unchecked |
| ... | ... | @@ -98,7 +98,7 @@ module Gitlab |
| 98 | 98 | # id (required) - The ID or code name of a project |
| 99 | 99 | # merge_request_id (required) - ID of MR |
| 100 | 100 | # note (required) - Text of comment |
| 101 | - # Examples: | |
| 101 | + # Examples: | |
| 102 | 102 | # POST /projects/:id/merge_request/:merge_request_id/comments |
| 103 | 103 | # |
| 104 | 104 | post ":id/merge_request/:merge_request_id/comments" do |
| ... | ... | @@ -107,7 +107,7 @@ module Gitlab |
| 107 | 107 | note.author = current_user |
| 108 | 108 | |
| 109 | 109 | if note.save |
| 110 | - present note, with: Entities::Note | |
| 110 | + present note, with: Entities::MRNote | |
| 111 | 111 | else |
| 112 | 112 | not_found! |
| 113 | 113 | end | ... | ... |
| ... | ... | @@ -0,0 +1,76 @@ |
| 1 | +module Gitlab | |
| 2 | + # Notes API | |
| 3 | + class Notes < Grape::API | |
| 4 | + before { authenticate! } | |
| 5 | + | |
| 6 | + NOTEABLE_TYPES = [Issue, Snippet] | |
| 7 | + | |
| 8 | + resource :projects do | |
| 9 | + # Get a list of project wall notes | |
| 10 | + # | |
| 11 | + # Parameters: | |
| 12 | + # id (required) - The ID or code name of a project | |
| 13 | + # Example Request: | |
| 14 | + # GET /projects/:id/notes | |
| 15 | + get ":id/notes" do | |
| 16 | + @notes = user_project.common_notes | |
| 17 | + present paginate(@notes), with: Entities::Note | |
| 18 | + end | |
| 19 | + | |
| 20 | + NOTEABLE_TYPES.each do |noteable_type| | |
| 21 | + noteables_str = noteable_type.to_s.underscore.pluralize | |
| 22 | + noteable_id_str = "#{noteable_type.to_s.underscore}_id" | |
| 23 | + | |
| 24 | + # Get a list of project +noteable+ notes | |
| 25 | + # | |
| 26 | + # Parameters: | |
| 27 | + # id (required) - The ID or code name of a project | |
| 28 | + # noteable_id (required) - The ID of an issue or snippet | |
| 29 | + # Example Request: | |
| 30 | + # GET /projects/:id/issues/:noteable_id/notes | |
| 31 | + # GET /projects/:id/snippets/:noteable_id/notes | |
| 32 | + get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do | |
| 33 | + @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) | |
| 34 | + present paginate(@noteable.notes), with: Entities::Note | |
| 35 | + end | |
| 36 | + | |
| 37 | + # Get a single +noteable+ note | |
| 38 | + # | |
| 39 | + # Parameters: | |
| 40 | + # id (required) - The ID or code name of a project | |
| 41 | + # noteable_id (required) - The ID of an issue or snippet | |
| 42 | + # note_id (required) - The ID of a note | |
| 43 | + # Example Request: | |
| 44 | + # GET /projects/:id/issues/:noteable_id/notes/:note_id | |
| 45 | + # GET /projects/:id/snippets/:noteable_id/notes/:note_id | |
| 46 | + get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do | |
| 47 | + @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) | |
| 48 | + @note = @noteable.notes.find(params[:note_id]) | |
| 49 | + present @note, with: Entities::Note | |
| 50 | + end | |
| 51 | + | |
| 52 | + # Create a new +noteable+ note | |
| 53 | + # | |
| 54 | + # Parameters: | |
| 55 | + # id (required) - The ID or code name of a project | |
| 56 | + # noteable_id (required) - The ID of an issue or snippet | |
| 57 | + # body (required) - The content of a note | |
| 58 | + # Example Request: | |
| 59 | + # POST /projects/:id/issues/:noteable_id/notes | |
| 60 | + # POST /projects/:id/snippets/:noteable_id/notes | |
| 61 | + post ":id/#{noteables_str}/:#{noteable_id_str}/notes" do | |
| 62 | + @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) | |
| 63 | + @note = @noteable.notes.new(note: params[:body]) | |
| 64 | + @note.author = current_user | |
| 65 | + @note.project = user_project | |
| 66 | + | |
| 67 | + if @note.save | |
| 68 | + present @note, with: Entities::Note | |
| 69 | + else | |
| 70 | + not_found! | |
| 71 | + end | |
| 72 | + end | |
| 73 | + end | |
| 74 | + end | |
| 75 | + end | |
| 76 | +end | ... | ... |
| ... | ... | @@ -0,0 +1,90 @@ |
| 1 | +require 'spec_helper' | |
| 2 | + | |
| 3 | +describe Gitlab::API do | |
| 4 | + include ApiHelpers | |
| 5 | + | |
| 6 | + let(:user) { create(:user) } | |
| 7 | + let!(:project) { create(:project, owner: user) } | |
| 8 | + let!(:issue) { create(:issue, project: project, author: user) } | |
| 9 | + let!(:snippet) { create(:snippet, project: project, author: user) } | |
| 10 | + let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) } | |
| 11 | + let!(:snippet_note) { create(:note, noteable: snippet, project: project, author: user) } | |
| 12 | + let!(:wall_note) { create(:note, project: project, author: user) } | |
| 13 | + before { project.add_access(user, :read) } | |
| 14 | + | |
| 15 | + describe "GET /projects/:id/notes" do | |
| 16 | + context "when unauthenticated" do | |
| 17 | + it "should return authentication error" do | |
| 18 | + get api("/projects/#{project.id}/notes") | |
| 19 | + response.status.should == 401 | |
| 20 | + end | |
| 21 | + end | |
| 22 | + | |
| 23 | + context "when authenticated" do | |
| 24 | + it "should return project wall notes" do | |
| 25 | + get api("/projects/#{project.id}/notes", user) | |
| 26 | + response.status.should == 200 | |
| 27 | + json_response.should be_an Array | |
| 28 | + json_response.first['body'].should == wall_note.note | |
| 29 | + end | |
| 30 | + end | |
| 31 | + end | |
| 32 | + | |
| 33 | + describe "GET /projects/:id/noteable/:noteable_id/notes" do | |
| 34 | + context "when noteable is an Issue" do | |
| 35 | + it "should return an array of issue notes" do | |
| 36 | + get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) | |
| 37 | + response.status.should == 200 | |
| 38 | + json_response.should be_an Array | |
| 39 | + json_response.first['body'].should == issue_note.note | |
| 40 | + end | |
| 41 | + end | |
| 42 | + | |
| 43 | + context "when noteable is a Snippet" do | |
| 44 | + it "should return an array of snippet notes" do | |
| 45 | + get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) | |
| 46 | + response.status.should == 200 | |
| 47 | + json_response.should be_an Array | |
| 48 | + json_response.first['body'].should == snippet_note.note | |
| 49 | + end | |
| 50 | + end | |
| 51 | + end | |
| 52 | + | |
| 53 | + describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do | |
| 54 | + context "when noteable is an Issue" do | |
| 55 | + it "should return an issue note by id" do | |
| 56 | + get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user) | |
| 57 | + response.status.should == 200 | |
| 58 | + json_response['body'].should == issue_note.note | |
| 59 | + end | |
| 60 | + end | |
| 61 | + | |
| 62 | + context "when noteable is a Snippet" do | |
| 63 | + it "should return a snippet note by id" do | |
| 64 | + get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user) | |
| 65 | + response.status.should == 200 | |
| 66 | + json_response['body'].should == snippet_note.note | |
| 67 | + end | |
| 68 | + end | |
| 69 | + end | |
| 70 | + | |
| 71 | + describe "POST /projects/:id/noteable/:noteable_id/notes" do | |
| 72 | + context "when noteable is an Issue" do | |
| 73 | + it "should create a new issue note" do | |
| 74 | + post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' | |
| 75 | + response.status.should == 201 | |
| 76 | + json_response['body'].should == 'hi!' | |
| 77 | + json_response['author']['email'].should == user.email | |
| 78 | + end | |
| 79 | + end | |
| 80 | + | |
| 81 | + context "when noteable is a Snippet" do | |
| 82 | + it "should create a new snippet note" do | |
| 83 | + post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!' | |
| 84 | + response.status.should == 201 | |
| 85 | + json_response['body'].should == 'hi!' | |
| 86 | + json_response['author']['email'].should == user.email | |
| 87 | + end | |
| 88 | + end | |
| 89 | + end | |
| 90 | +end | ... | ... |