270 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			270 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Ruby
		
	
	
	
| # frozen_string_literal: true
 | |
| 
 | |
| module API
 | |
|   class Discussions < ::API::Base
 | |
|     include PaginationParams
 | |
|     helpers ::API::Helpers::NotesHelpers
 | |
|     helpers ::RendersNotes
 | |
| 
 | |
|     before { authenticate! }
 | |
| 
 | |
|     urgency :low, [
 | |
|       '/projects/:id/merge_requests/:noteable_id/discussions',
 | |
|       '/projects/:id/merge_requests/:noteable_id/discussions/:discussion_id',
 | |
|       '/projects/:id/merge_requests/:noteable_id/discussions/:discussion_id/notes',
 | |
|       '/projects/:id/merge_requests/:noteable_id/discussions/:discussion_id/notes/:note_id'
 | |
|     ]
 | |
| 
 | |
|     Helpers::DiscussionsHelpers.feature_category_per_noteable_type.each do |noteable_type, feature_category|
 | |
|       parent_type = noteable_type.parent_class.to_s.underscore
 | |
|       noteables_str = noteable_type.to_s.underscore.pluralize
 | |
|       noteables_path = noteable_type == Commit ? "repository/#{noteables_str}" : noteables_str
 | |
| 
 | |
|       params do
 | |
|         requires :id, type: String, desc: "The ID of a #{parent_type}"
 | |
|       end
 | |
|       resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
 | |
|         desc "Get a list of #{noteable_type.to_s.downcase} discussions" do
 | |
|           success Entities::Discussion
 | |
|         end
 | |
|         params do
 | |
|           requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
 | |
|           use :pagination
 | |
|         end
 | |
| 
 | |
|         get ":id/#{noteables_path}/:noteable_id/discussions", feature_category: feature_category do
 | |
|           noteable = find_noteable(noteable_type, params[:noteable_id])
 | |
| 
 | |
|           discussion_ids = paginate(noteable.discussion_ids_relation)
 | |
|           notes = readable_discussion_notes(noteable, discussion_ids)
 | |
| 
 | |
|           present Discussion.build_collection(notes, noteable), 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, types: [Integer, String], desc: 'The ID of the noteable'
 | |
|         end
 | |
|         get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id", feature_category: feature_category do
 | |
|           noteable = find_noteable(noteable_type, params[:noteable_id])
 | |
|           notes = readable_discussion_notes(noteable, params[:discussion_id])
 | |
| 
 | |
|           if notes.empty?
 | |
|             break 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, types: [Integer, String], 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'
 | |
|           optional :position, type: Hash do
 | |
|             requires :base_sha, type: String, desc: 'Base commit SHA in the source branch'
 | |
|             requires :start_sha, type: String, desc: 'SHA referencing commit in target branch'
 | |
|             requires :head_sha, type: String, desc: 'SHA referencing HEAD of this merge request'
 | |
|             requires :position_type, type: String, desc: 'Type of the position reference', values: %w(text image)
 | |
|             optional :new_path, type: String, desc: 'File path after change'
 | |
|             optional :new_line, type: Integer, desc: 'Line number after change'
 | |
|             optional :old_path, type: String, desc: 'File path before change'
 | |
|             optional :old_line, type: Integer, desc: 'Line number before change'
 | |
|             optional :width, type: Integer, desc: 'Width of the image'
 | |
|             optional :height, type: Integer, desc: 'Height of the image'
 | |
|             optional :x, type: Integer, desc: 'X coordinate in the image'
 | |
|             optional :y, type: Integer, desc: 'Y coordinate in the image'
 | |
| 
 | |
|             optional :line_range, type: Hash, desc: 'Multi-line start and end' do
 | |
|               optional :start, type: Hash do
 | |
|                 optional :line_code, type: String, desc: 'Start line code for multi-line note'
 | |
|                 optional :type, type: String, desc: 'Start line type for multi-line note'
 | |
|                 optional :old_line, type: String, desc: 'Start old_line line number'
 | |
|                 optional :new_line, type: String, desc: 'Start new_line line number'
 | |
|               end
 | |
|               optional :end, type: Hash do
 | |
|                 optional :line_code, type: String, desc: 'End line code for multi-line note'
 | |
|                 optional :type, type: String, desc: 'End line type for multi-line note'
 | |
|                 optional :old_line, type: String, desc: 'End old_line line number'
 | |
|                 optional :new_line, type: String, desc: 'End new_line line number'
 | |
|               end
 | |
|             end
 | |
|           end
 | |
|         end
 | |
|         post ":id/#{noteables_path}/:noteable_id/discussions", feature_category: feature_category do
 | |
|           noteable = find_noteable(noteable_type, params[:noteable_id])
 | |
|           type = params[:position] ? 'DiffNote' : 'DiscussionNote'
 | |
|           id_key = noteable.is_a?(Commit) ? :commit_id : :noteable_id
 | |
| 
 | |
|           opts = {
 | |
|             note: params[:body],
 | |
|             created_at: params[:created_at],
 | |
|             type: type,
 | |
|             noteable_type: noteables_str.classify,
 | |
|             position: params[:position],
 | |
|             id_key => noteable.id
 | |
|           }
 | |
|           opts[:commit_id] = params[:commit_id] if noteable.is_a?(MergeRequest) && type == 'DiffNote'
 | |
| 
 | |
|           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, types: [Integer, String], desc: 'The ID of the noteable'
 | |
|         end
 | |
|         get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes", feature_category: feature_category do
 | |
|           noteable = find_noteable(noteable_type, params[:noteable_id])
 | |
|           notes = readable_discussion_notes(noteable, params[:discussion_id])
 | |
| 
 | |
|           if notes.empty?
 | |
|             break 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, types: [Integer, String], 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_path}/:noteable_id/discussions/:discussion_id/notes", feature_category: feature_category do
 | |
|           noteable = find_noteable(noteable_type, params[:noteable_id])
 | |
|           notes = readable_discussion_notes(noteable, params[:discussion_id])
 | |
|           first_note = notes.first
 | |
| 
 | |
|           break not_found!("Discussion") if notes.empty?
 | |
| 
 | |
|           unless first_note.part_of_discussion? || first_note.to_discussion.can_convert_to_discussion?
 | |
|             break bad_request!("Discussion can not be replied to.")
 | |
|           end
 | |
| 
 | |
|           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, types: [Integer, String], 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_path}/:noteable_id/discussions/:discussion_id/notes/:note_id", feature_category: feature_category do
 | |
|           noteable = find_noteable(noteable_type, 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, types: [Integer, String], 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'
 | |
|           optional :body, type: String, desc: 'The content of a note'
 | |
|           optional :resolved, type: Boolean, desc: 'Mark note resolved/unresolved'
 | |
|           exactly_one_of :body, :resolved
 | |
|         end
 | |
|         put ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id/notes/:note_id", feature_category: feature_category do
 | |
|           noteable = find_noteable(noteable_type, params[:noteable_id])
 | |
| 
 | |
|           if params[:resolved].nil?
 | |
|             update_note(noteable, params[:note_id])
 | |
|           else
 | |
|             resolve_note(noteable, params[:note_id], params[:resolved])
 | |
|           end
 | |
|         end
 | |
| 
 | |
|         desc "Delete a comment in a #{noteable_type.to_s.downcase} discussion" do
 | |
|           success Entities::Note
 | |
|         end
 | |
|         params do
 | |
|           requires :noteable_id, types: [Integer, String], 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_path}/:noteable_id/discussions/:discussion_id/notes/:note_id", feature_category: feature_category do
 | |
|           noteable = find_noteable(noteable_type, params[:noteable_id])
 | |
| 
 | |
|           delete_note(noteable, params[:note_id])
 | |
|         end
 | |
| 
 | |
|         if Noteable.resolvable_types.include?(noteable_type.to_s)
 | |
|           desc "Resolve/unresolve an existing #{noteable_type.to_s.downcase} discussion" do
 | |
|             success Entities::Discussion
 | |
|           end
 | |
|           params do
 | |
|             requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
 | |
|             requires :discussion_id, type: String, desc: 'The ID of a discussion'
 | |
|             requires :resolved, type: Boolean, desc: 'Mark discussion resolved/unresolved'
 | |
|           end
 | |
|           put ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id", feature_category: feature_category do
 | |
|             noteable = find_noteable(noteable_type, params[:noteable_id])
 | |
| 
 | |
|             resolve_discussion(noteable, params[:discussion_id], params[:resolved])
 | |
|           end
 | |
|         end
 | |
|       end
 | |
|     end
 | |
| 
 | |
|     helpers do
 | |
|       # rubocop: disable CodeReuse/ActiveRecord
 | |
|       def readable_discussion_notes(noteable, discussion_ids)
 | |
|         notes = noteable.notes
 | |
|           .with_discussion_ids(discussion_ids)
 | |
|           .inc_relations_for_view
 | |
|           .includes(:noteable)
 | |
|           .fresh
 | |
| 
 | |
|         # Without RendersActions#prepare_notes_for_rendering,
 | |
|         # Note#system_note_visible_for? will attempt to render
 | |
|         # Markdown references mentioned in the note to see whether they
 | |
|         # should be redacted. For notes that reference a commit, this
 | |
|         # would also incur a Gitaly call to verify the commit exists.
 | |
|         #
 | |
|         # With prepare_notes_for_rendering, we can avoid Gitaly calls
 | |
|         # because notes are redacted if they point to projects that
 | |
|         # cannot be accessed by the user.
 | |
|         notes = prepare_notes_for_rendering(notes)
 | |
|         notes.select { |n| n.readable_by?(current_user) }
 | |
|       end
 | |
|       # rubocop: enable CodeReuse/ActiveRecord
 | |
|     end
 | |
|   end
 | |
| end
 |