summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorJan Provaznik <jprovaznik@gitlab.com>2018-02-28 08:48:23 +0100
committerJan Provaznik <jprovaznik@gitlab.com>2018-03-07 12:27:50 +0100
commitdcdfa04b322db3905f6871a6857e7055c556547f (patch)
treeb3d006e8410b7182c63bfd620b99a71392c77aff /lib
parent8a0052c037f025b64159ca8cfe0d3451261c1edb (diff)
downloadgitlab-ce-dcdfa04b322db3905f6871a6857e7055c556547f.tar.gz
Add discussion APIjprovazn-api
* adds basic discussions API for issues and snippets * reorganizes notes specs (so same tests can be used for all noteable types - issues, MRs, snippets)
Diffstat (limited to 'lib')
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/discussions.rb195
-rw-r--r--lib/api/entities.rb7
-rw-r--r--lib/api/helpers/notes_helpers.rb76
-rw-r--r--lib/api/notes.rb94
5 files changed, 307 insertions, 66 deletions
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 754549f72f0..8d8332c889a 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -134,6 +134,7 @@ module API
mount ::API::MergeRequests
mount ::API::Namespaces
mount ::API::Notes
+ mount ::API::Discussions
mount ::API::NotificationSettings
mount ::API::PagesDomains
mount ::API::Pipelines
diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb
new file mode 100644
index 00000000000..6abd575b6ad
--- /dev/null
+++ b/lib/api/discussions.rb
@@ -0,0 +1,195 @@
+module API
+ class Discussions < Grape::API
+ include PaginationParams
+ helpers ::API::Helpers::NotesHelpers
+
+ before { authenticate! }
+
+ NOTEABLE_TYPES = [Issue, Snippet].freeze
+
+ NOTEABLE_TYPES.each do |noteable_type|
+ parent_type = noteable_type.parent_class.to_s.underscore
+ noteables_str = noteable_type.to_s.underscore.pluralize
+
+ params do
+ requires :id, type: String, desc: "The ID of a #{parent_type}"
+ end
+ resource parent_type.pluralize.to_sym, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ desc "Get a list of #{noteable_type.to_s.downcase} discussions" do
+ success Entities::Discussion
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ use :pagination
+ end
+ get ":id/#{noteables_str}/:noteable_id/discussions" do
+ noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
+
+ return not_found!("Discussions") unless can?(current_user, noteable_read_ability_name(noteable), noteable)
+
+ notes = noteable.notes
+ .inc_relations_for_view
+ .includes(:noteable)
+ .fresh
+
+ notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
+ discussions = Kaminari.paginate_array(Discussion.build_collection(notes, noteable))
+
+ present paginate(discussions), with: Entities::Discussion
+ end
+
+ desc "Get a single #{noteable_type.to_s.downcase} discussion" do
+ success Entities::Discussion
+ end
+ params do
+ requires :discussion_id, type: String, desc: 'The ID of a discussion'
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ end
+ get ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id" do
+ noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
+ notes = readable_discussion_notes(noteable, params[:discussion_id])
+
+ if notes.empty? || !can?(current_user, noteable_read_ability_name(noteable), noteable)
+ return not_found!("Discussion")
+ end
+
+ discussion = Discussion.build(notes, noteable)
+
+ present discussion, with: Entities::Discussion
+ end
+
+ desc "Create a new #{noteable_type.to_s.downcase} discussion" do
+ success Entities::Discussion
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :body, type: String, desc: 'The content of a note'
+ optional :created_at, type: String, desc: 'The creation date of the note'
+ end
+ post ":id/#{noteables_str}/:noteable_id/discussions" do
+ noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
+
+ opts = {
+ note: params[:body],
+ created_at: params[:created_at],
+ type: 'DiscussionNote',
+ noteable_type: noteables_str.classify,
+ noteable_id: noteable.id
+ }
+
+ note = create_note(noteable, opts)
+
+ if note.valid?
+ present note.discussion, with: Entities::Discussion
+ else
+ bad_request!("Note #{note.errors.messages}")
+ end
+ end
+
+ desc "Get comments in a single #{noteable_type.to_s.downcase} discussion" do
+ success Entities::Discussion
+ end
+ params do
+ requires :discussion_id, type: String, desc: 'The ID of a discussion'
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ end
+ get ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes" do
+ noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
+ notes = readable_discussion_notes(noteable, params[:discussion_id])
+
+ if notes.empty? || !can?(current_user, noteable_read_ability_name(noteable), noteable)
+ return not_found!("Notes")
+ end
+
+ present notes, with: Entities::Note
+ end
+
+ desc "Add a comment to a #{noteable_type.to_s.downcase} discussion" do
+ success Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :discussion_id, type: String, desc: 'The ID of a discussion'
+ requires :body, type: String, desc: 'The content of a note'
+ optional :created_at, type: String, desc: 'The creation date of the note'
+ end
+ post ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes" do
+ noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
+ notes = readable_discussion_notes(noteable, params[:discussion_id])
+
+ return not_found!("Discussion") if notes.empty?
+ return bad_request!("Discussion is an individual note.") unless notes.first.part_of_discussion?
+
+ opts = {
+ note: params[:body],
+ type: 'DiscussionNote',
+ in_reply_to_discussion_id: params[:discussion_id],
+ created_at: params[:created_at]
+ }
+ note = create_note(noteable, opts)
+
+ if note.valid?
+ present note, with: Entities::Note
+ else
+ bad_request!("Note #{note.errors.messages}")
+ end
+ end
+
+ desc "Get a comment in a #{noteable_type.to_s.downcase} discussion" do
+ success Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :discussion_id, type: String, desc: 'The ID of a discussion'
+ requires :note_id, type: Integer, desc: 'The ID of a note'
+ end
+ get ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
+ noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
+
+ get_note(noteable, params[:note_id])
+ end
+
+ desc "Edit a comment in a #{noteable_type.to_s.downcase} discussion" do
+ success Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :discussion_id, type: String, desc: 'The ID of a discussion'
+ requires :note_id, type: Integer, desc: 'The ID of a note'
+ requires :body, type: String, desc: 'The content of a note'
+ end
+ put ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
+ noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
+
+ update_note(noteable, params[:note_id])
+ end
+
+ desc "Delete a comment in a #{noteable_type.to_s.downcase} discussion" do
+ success Entities::Note
+ end
+ params do
+ requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
+ requires :discussion_id, type: String, desc: 'The ID of a discussion'
+ requires :note_id, type: Integer, desc: 'The ID of a note'
+ end
+ delete ":id/#{noteables_str}/:noteable_id/discussions/:discussion_id/notes/:note_id" do
+ noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
+
+ delete_note(noteable, params[:note_id])
+ end
+ end
+ end
+
+ helpers do
+ def readable_discussion_notes(noteable, discussion_id)
+ notes = noteable.notes
+ .where(discussion_id: discussion_id)
+ .inc_relations_for_view
+ .includes(:noteable)
+ .fresh
+
+ notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
+ end
+ end
+ end
+end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 0c8ec7dd5f5..aa96033898b 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -629,6 +629,7 @@ module API
NOTEABLE_TYPES_WITH_IID = %w(Issue MergeRequest).freeze
expose :id
+ expose :type
expose :note, as: :body
expose :attachment_identifier, as: :attachment
expose :author, using: Entities::UserBasic
@@ -640,6 +641,12 @@ module API
expose(:noteable_iid) { |note| note.noteable.iid if NOTEABLE_TYPES_WITH_IID.include?(note.noteable_type) }
end
+ class Discussion < Grape::Entity
+ expose :id
+ expose :individual_note?, as: :individual_note
+ expose :notes, using: Entities::Note
+ end
+
class AwardEmoji < Grape::Entity
expose :id
expose :name
diff --git a/lib/api/helpers/notes_helpers.rb b/lib/api/helpers/notes_helpers.rb
new file mode 100644
index 00000000000..cd91df1ecd8
--- /dev/null
+++ b/lib/api/helpers/notes_helpers.rb
@@ -0,0 +1,76 @@
+module API
+ module Helpers
+ module NotesHelpers
+ def update_note(noteable, note_id)
+ note = noteable.notes.find(params[:note_id])
+
+ authorize! :admin_note, note
+
+ opts = {
+ note: params[:body]
+ }
+ parent = noteable_parent(noteable)
+ project = parent if parent.is_a?(Project)
+
+ note = ::Notes::UpdateService.new(project, current_user, opts).execute(note)
+
+ if note.valid?
+ present note, with: Entities::Note
+ else
+ bad_request!("Failed to save note #{note.errors.messages}")
+ end
+ end
+
+ def delete_note(noteable, note_id)
+ note = noteable.notes.find(note_id)
+
+ authorize! :admin_note, note
+
+ parent = noteable_parent(noteable)
+ project = parent if parent.is_a?(Project)
+ destroy_conditionally!(note) do |note|
+ ::Notes::DestroyService.new(project, current_user).execute(note)
+ end
+ end
+
+ def get_note(noteable, note_id)
+ note = noteable.notes.with_metadata.find(params[:note_id])
+ can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
+
+ if can_read_note
+ present note, with: Entities::Note
+ else
+ not_found!("Note")
+ end
+ end
+
+ def noteable_read_ability_name(noteable)
+ "read_#{noteable.class.to_s.underscore}".to_sym
+ end
+
+ def find_noteable(parent, noteables_str, noteable_id)
+ public_send("find_#{parent}_#{noteables_str.singularize}", noteable_id) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def noteable_parent(noteable)
+ public_send("user_#{noteable.class.parent_class.to_s.underscore}") # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def create_note(noteable, opts)
+ noteables_str = noteable.model_name.to_s.underscore.pluralize
+
+ return not_found!(noteables_str) unless can?(current_user, noteable_read_ability_name(noteable), noteable)
+
+ authorize! :create_note, noteable
+
+ parent = noteable_parent(noteable)
+ if opts[:created_at]
+ opts.delete(:created_at) unless current_user.admin? || parent.owner == current_user
+ end
+
+ project = parent if parent.is_a?(Project)
+ ::Notes::CreateService.new(project, current_user, opts).execute
+ end
+ end
+ end
+end
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 3588dc85c9e..69f1df6b341 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -1,19 +1,23 @@
module API
class Notes < Grape::API
include PaginationParams
+ helpers ::API::Helpers::NotesHelpers
before { authenticate! }
NOTEABLE_TYPES = [Issue, MergeRequest, Snippet].freeze
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
- NOTEABLE_TYPES.each do |noteable_type|
+ NOTEABLE_TYPES.each do |noteable_type|
+ parent_type = noteable_type.parent_class.to_s.underscore
+ noteables_str = noteable_type.to_s.underscore.pluralize
+
+ params do
+ requires :id, type: String, desc: "The ID of a #{parent_type}"
+ end
+ resource parent_type.pluralize.to_sym, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
noteables_str = noteable_type.to_s.underscore.pluralize
- desc 'Get a list of project +noteable+ notes' do
+ desc "Get a list of #{noteable_type.to_s.downcase} notes" do
success Entities::Note
end
params do
@@ -25,7 +29,7 @@ module API
use :pagination
end
get ":id/#{noteables_str}/:noteable_id/notes" do
- noteable = find_project_noteable(noteables_str, params[:noteable_id])
+ noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
if can?(current_user, noteable_read_ability_name(noteable), noteable)
# We exclude notes that are cross-references and that cannot be viewed
@@ -46,7 +50,7 @@ module API
end
end
- desc 'Get a single +noteable+ note' do
+ desc "Get a single #{noteable_type.to_s.downcase} note" do
success Entities::Note
end
params do
@@ -54,18 +58,11 @@ module API
requires :noteable_id, type: Integer, desc: 'The ID of the noteable'
end
get ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
- noteable = find_project_noteable(noteables_str, params[:noteable_id])
- note = noteable.notes.with_metadata.find(params[:note_id])
- can_read_note = can?(current_user, noteable_read_ability_name(noteable), noteable) && !note.cross_reference_not_visible_for?(current_user)
-
- if can_read_note
- present note, with: Entities::Note
- else
- not_found!("Note")
- end
+ noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
+ get_note(noteable, params[:note_id])
end
- desc 'Create a new +noteable+ note' do
+ desc "Create a new #{noteable_type.to_s.downcase} note" do
success Entities::Note
end
params do
@@ -74,34 +71,25 @@ module API
optional :created_at, type: String, desc: 'The creation date of the note'
end
post ":id/#{noteables_str}/:noteable_id/notes" do
- noteable = find_project_noteable(noteables_str, params[:noteable_id])
+ noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
opts = {
note: params[:body],
noteable_type: noteables_str.classify,
- noteable_id: noteable.id
+ noteable_id: noteable.id,
+ created_at: params[:created_at]
}
- if can?(current_user, noteable_read_ability_name(noteable), noteable)
- authorize! :create_note, noteable
+ note = create_note(noteable, opts)
- if params[:created_at] && (current_user.admin? || user_project.owner == current_user)
- opts[:created_at] = params[:created_at]
- end
-
- note = ::Notes::CreateService.new(user_project, current_user, opts).execute
-
- if note.valid?
- present note, with: Entities.const_get(note.class.name)
- else
- not_found!("Note #{note.errors.messages}")
- end
+ if note.valid?
+ present note, with: Entities.const_get(note.class.name)
else
- not_found!("Note")
+ bad_request!("Note #{note.errors.messages}")
end
end
- desc 'Update an existing +noteable+ note' do
+ desc "Update an existing #{noteable_type.to_s.downcase} note" do
success Entities::Note
end
params do
@@ -110,24 +98,12 @@ module API
requires :body, type: String, desc: 'The content of a note'
end
put ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
- note = user_project.notes.find(params[:note_id])
+ noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
- authorize! :admin_note, note
-
- opts = {
- note: params[:body]
- }
-
- note = ::Notes::UpdateService.new(user_project, current_user, opts).execute(note)
-
- if note.valid?
- present note, with: Entities::Note
- else
- render_api_error!("Failed to save note #{note.errors.messages}", 400)
- end
+ update_note(noteable, params[:note_id])
end
- desc 'Delete a +noteable+ note' do
+ desc "Delete a #{noteable_type.to_s.downcase} note" do
success Entities::Note
end
params do
@@ -135,25 +111,11 @@ module API
requires :note_id, type: Integer, desc: 'The ID of a note'
end
delete ":id/#{noteables_str}/:noteable_id/notes/:note_id" do
- note = user_project.notes.find(params[:note_id])
-
- authorize! :admin_note, note
+ noteable = find_noteable(parent_type, noteables_str, params[:noteable_id])
- destroy_conditionally!(note) do |note|
- ::Notes::DestroyService.new(user_project, current_user).execute(note)
- end
+ delete_note(noteable, params[:note_id])
end
end
end
-
- helpers do
- def find_project_noteable(noteables_str, noteable_id)
- public_send("find_project_#{noteables_str.singularize}", noteable_id) # rubocop:disable GitlabSecurity/PublicSend
- end
-
- def noteable_read_ability_name(noteable)
- "read_#{noteable.class.to_s.underscore}".to_sym
- end
- end
end
end