1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
|
# frozen_string_literal: true
module API
class Discussions < Grape::API
include PaginationParams
helpers ::API::Helpers::NotesHelpers
helpers ::RendersNotes
before { authenticate! }
Helpers::DiscussionsHelpers.noteable_types.each do |noteable_type|
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" do
noteable = find_noteable(noteable_type, params[:noteable_id])
notes = readable_discussion_notes(noteable)
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, types: [Integer, String], desc: 'The ID of the noteable'
end
get ":id/#{noteables_path}/:noteable_id/discussions/:discussion_id" 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'
end
end
post ":id/#{noteables_path}/:noteable_id/discussions" 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
}
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" 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" 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" 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" 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" 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" 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_id = nil)
notes = noteable.notes
notes = notes.where(discussion_id: discussion_id) if discussion_id
notes = notes
.inc_relations_for_view
.includes(:noteable)
.fresh
# Without RendersActions#prepare_notes_for_rendering,
# Note#cross_reference_not_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.visible_for?(current_user) }
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
end
|