diff --git a/CHANGELOG b/CHANGELOG index c939e159..4b28da46 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,7 @@ v 3.2.0 + - [API] create notes for snippets and issues + - [API] list notes for snippets and issues + - [API] list project wall notes - Remove project code - use path instead - added username field to user - rake task to fill usernames based on emails create namespaces for users @@ -10,7 +13,7 @@ v 3.2.0 - Fixes commit patches getting escaped (see #2036) - Support diff and patch generation for commits and merge request - MergeReqest doesn't generate a temporary file for the patch any more - - Update the UI to allow downloading Patch or Diff + - Update the UI to allow downloading Patch or Diff v 3.1.0 - Updated gems @@ -48,7 +51,7 @@ v 3.0.0 - Fixed bug with gitolite keys - UI improved - Increased perfomance of application - - Show user avatar in last commit when browsing Files + - Show user avatar in last commit when browsing Files - Refactored Gitlab::Merge - Use Font Awsome for icons - Separate observing of Note and MergeRequestsa diff --git a/app/views/help/api.html.haml b/app/views/help/api.html.haml index 00085166..3f16637d 100644 --- a/app/views/help/api.html.haml +++ b/app/views/help/api.html.haml @@ -21,6 +21,8 @@ = link_to "Issues", "#issues", 'data-toggle' => 'tab' %li = link_to "Milestones", "#milestones", 'data-toggle' => 'tab' + %li + = link_to "Notes", "#notes", 'data-toggle' => 'tab' .tab-content .tab-pane.active#README @@ -94,3 +96,12 @@ .file_content.wiki = preserve do = markdown File.read(Rails.root.join("doc", "api", "milestones.md")) + + .tab-pane#notes + .file_holder + .file_title + %i.icon-file + Notes + .file_content.wiki + = preserve do + = markdown File.read(Rails.root.join("doc", "api", "notes.md")) diff --git a/doc/api/notes.md b/doc/api/notes.md new file mode 100644 index 00000000..3d8309d3 --- /dev/null +++ b/doc/api/notes.md @@ -0,0 +1,121 @@ +## List notes + +### List project wall notes + +Get a list of project wall notes. + +``` +GET /projects/:id/notes +``` + +```json +[ + { + "id": 522, + "body": "The solution is rather tricky", + "author": { + "id": 1, + "email": "john@example.com", + "name": "John Smith", + "blocked": false, + "created_at": "2012-05-23T08:00:58Z" + }, + "updated_at":"2012-11-27T19:16:44Z", + "created_at":"2012-11-27T19:16:44Z" + } +] +``` + +Parameters: + ++ `id` (required) - The ID or code name of a project + +### List issue notes + +Get a list of issue notes. + +``` +GET /projects/:id/issues/:issue_id/notes +``` + +Parameters: + ++ `id` (required) - The ID or code name of a project ++ `issue_id` (required) - The ID of an issue + +### List snippet notes + +Get a list of snippet notes. + +``` +GET /projects/:id/snippets/:snippet_id/notes +``` + +Parameters: + ++ `id` (required) - The ID or code name of a project ++ `snippet_id` (required) - The ID of a snippet + +## Single note + +### Single issue note + +Get an issue note. + +``` +GET /projects/:id/issues/:issue_id/:notes/:note_id +``` + +Parameters: + ++ `id` (required) - The ID or code name of a project ++ `issue_id` (required) - The ID of a project issue ++ `note_id` (required) - The ID of an issue note + +### Single snippet note + +Get a snippet note. + +``` +GET /projects/:id/issues/:snippet_id/:notes/:note_id +``` + +Parameters: + ++ `id` (required) - The ID or code name of a project ++ `snippet_id` (required) - The ID of a project snippet ++ `note_id` (required) - The ID of an snippet note + +## New note + +### New issue note + +Create a new issue note. + +``` +POST /projects/:id/issues/:issue_id/notes +``` + +Parameters: + ++ `id` (required) - The ID or code name of a project ++ `issue_id` (required) - The ID of an issue ++ `body` (required) - The content of a note + +Will return created note with status `201 Created` on success, or `404 Not found` on fail. + +### New snippet note + +Create a new snippet note. + +``` +POST /projects/:id/snippets/:snippet_id/notes +``` + +Parameters: + ++ `id` (required) - The ID or code name of a project ++ `snippet_id` (required) - The ID of an snippet ++ `body` (required) - The content of a note + +Will return created note with status `201 Created` on success, or `404 Not found` on fail. diff --git a/lib/api.rb b/lib/api.rb index 7a184544..99e2074f 100644 --- a/lib/api.rb +++ b/lib/api.rb @@ -19,5 +19,6 @@ module Gitlab mount Milestones mount Session mount MergeRequests + mount Notes end end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 9e605a60..b792d3fb 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -70,8 +70,15 @@ module Gitlab end class Note < Grape::Entity + expose :id + expose :note, as: :body expose :author, using: Entities::UserBasic + expose :updated_at, :created_at + end + + class MRNote < Grape::Entity expose :note + expose :author, using: Entities::UserBasic end end end diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index d8f2c512..1fa0c549 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -4,9 +4,9 @@ module Gitlab before { authenticate! } resource :projects do - + # List merge requests - # + # # Parameters: # id (required) - The ID or code name of a project # @@ -15,24 +15,24 @@ module Gitlab # get ":id/merge_requests" do authorize! :read_merge_request, user_project - + present paginate(user_project.merge_requests), with: Entities::MergeRequest end - + # Show MR - # + # # Parameters: # id (required) - The ID or code name of a project # merge_request_id (required) - The ID of MR - # + # # Example: # GET /projects/:id/merge_request/:merge_request_id # get ":id/merge_request/:merge_request_id" do merge_request = user_project.merge_requests.find(params[:merge_request_id]) - + authorize! :read_merge_request, merge_request - + present merge_request, with: Entities::MergeRequest end @@ -45,17 +45,17 @@ module Gitlab # target_branch (required) - The target branch # assignee_id - Assignee user ID # title (required) - Title of MR - # + # # Example: # POST /projects/:id/merge_requests # post ":id/merge_requests" do authorize! :write_merge_request, user_project - + attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title] merge_request = user_project.merge_requests.new(attrs) merge_request.author = current_user - + if merge_request.save merge_request.reload_code present merge_request, with: Entities::MergeRequest @@ -80,9 +80,9 @@ module Gitlab put ":id/merge_request/:merge_request_id" do attrs = attributes_for_keys [:source_branch, :target_branch, :assignee_id, :title, :closed] merge_request = user_project.merge_requests.find(params[:merge_request_id]) - + authorize! :modify_merge_request, merge_request - + if merge_request.update_attributes attrs merge_request.reload_code merge_request.mark_as_unchecked @@ -98,7 +98,7 @@ module Gitlab # id (required) - The ID or code name of a project # merge_request_id (required) - ID of MR # note (required) - Text of comment - # Examples: + # Examples: # POST /projects/:id/merge_request/:merge_request_id/comments # post ":id/merge_request/:merge_request_id/comments" do @@ -107,7 +107,7 @@ module Gitlab note.author = current_user if note.save - present note, with: Entities::Note + present note, with: Entities::MRNote else not_found! end diff --git a/lib/api/notes.rb b/lib/api/notes.rb new file mode 100644 index 00000000..924eeaa3 --- /dev/null +++ b/lib/api/notes.rb @@ -0,0 +1,76 @@ +module Gitlab + # Notes API + class Notes < Grape::API + before { authenticate! } + + NOTEABLE_TYPES = [Issue, Snippet] + + resource :projects do + # Get a list of project wall notes + # + # Parameters: + # id (required) - The ID or code name of a project + # Example Request: + # GET /projects/:id/notes + get ":id/notes" do + @notes = user_project.common_notes + present paginate(@notes), with: Entities::Note + end + + NOTEABLE_TYPES.each do |noteable_type| + noteables_str = noteable_type.to_s.underscore.pluralize + noteable_id_str = "#{noteable_type.to_s.underscore}_id" + + # Get a list of project +noteable+ notes + # + # Parameters: + # id (required) - The ID or code name of a project + # noteable_id (required) - The ID of an issue or snippet + # Example Request: + # GET /projects/:id/issues/:noteable_id/notes + # GET /projects/:id/snippets/:noteable_id/notes + get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do + @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) + present paginate(@noteable.notes), with: Entities::Note + end + + # Get a single +noteable+ note + # + # Parameters: + # id (required) - The ID or code name of a project + # noteable_id (required) - The ID of an issue or snippet + # note_id (required) - The ID of a note + # Example Request: + # GET /projects/:id/issues/:noteable_id/notes/:note_id + # GET /projects/:id/snippets/:noteable_id/notes/:note_id + get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do + @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) + @note = @noteable.notes.find(params[:note_id]) + present @note, with: Entities::Note + end + + # Create a new +noteable+ note + # + # Parameters: + # id (required) - The ID or code name of a project + # noteable_id (required) - The ID of an issue or snippet + # body (required) - The content of a note + # Example Request: + # POST /projects/:id/issues/:noteable_id/notes + # POST /projects/:id/snippets/:noteable_id/notes + post ":id/#{noteables_str}/:#{noteable_id_str}/notes" do + @noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"]) + @note = @noteable.notes.new(note: params[:body]) + @note.author = current_user + @note.project = user_project + + if @note.save + present @note, with: Entities::Note + else + not_found! + end + end + end + end + end +end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb new file mode 100644 index 00000000..b7c8ffaf --- /dev/null +++ b/spec/requests/api/notes_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +describe Gitlab::API do + include ApiHelpers + + let(:user) { create(:user) } + let!(:project) { create(:project, owner: user) } + let!(:issue) { create(:issue, project: project, author: user) } + let!(:snippet) { create(:snippet, project: project, author: user) } + let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) } + let!(:snippet_note) { create(:note, noteable: snippet, project: project, author: user) } + let!(:wall_note) { create(:note, project: project, author: user) } + before { project.add_access(user, :read) } + + describe "GET /projects/:id/notes" do + context "when unauthenticated" do + it "should return authentication error" do + get api("/projects/#{project.id}/notes") + response.status.should == 401 + end + end + + context "when authenticated" do + it "should return project wall notes" do + get api("/projects/#{project.id}/notes", user) + response.status.should == 200 + json_response.should be_an Array + json_response.first['body'].should == wall_note.note + end + end + end + + describe "GET /projects/:id/noteable/:noteable_id/notes" do + context "when noteable is an Issue" do + it "should return an array of issue notes" do + get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) + response.status.should == 200 + json_response.should be_an Array + json_response.first['body'].should == issue_note.note + end + end + + context "when noteable is a Snippet" do + it "should return an array of snippet notes" do + get api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user) + response.status.should == 200 + json_response.should be_an Array + json_response.first['body'].should == snippet_note.note + end + end + end + + describe "GET /projects/:id/noteable/:noteable_id/notes/:note_id" do + context "when noteable is an Issue" do + it "should return an issue note by id" do + get api("/projects/#{project.id}/issues/#{issue.id}/notes/#{issue_note.id}", user) + response.status.should == 200 + json_response['body'].should == issue_note.note + end + end + + context "when noteable is a Snippet" do + it "should return a snippet note by id" do + get api("/projects/#{project.id}/snippets/#{snippet.id}/notes/#{snippet_note.id}", user) + response.status.should == 200 + json_response['body'].should == snippet_note.note + end + end + end + + describe "POST /projects/:id/noteable/:noteable_id/notes" do + context "when noteable is an Issue" do + it "should create a new issue note" do + post api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' + response.status.should == 201 + json_response['body'].should == 'hi!' + json_response['author']['email'].should == user.email + end + end + + context "when noteable is a Snippet" do + it "should create a new snippet note" do + post api("/projects/#{project.id}/snippets/#{snippet.id}/notes", user), body: 'hi!' + response.status.should == 201 + json_response['body'].should == 'hi!' + json_response['author']['email'].should == user.email + end + end + end +end