Project

General

Profile

Feature #44233 » 0002-Add-REST-API-support-for-create-on-custom-fields.patch

Mizuki ISHIKAWA, 2026-08-24 03:13

View differences:

app/controllers/custom_fields_controller.rb
24 24
  before_action :require_admin
25 25
  before_action :build_new_custom_field, :only => [:new, :create]
26 26
  before_action :find_custom_field, :only => [:show, :edit, :update, :destroy]
27
  accept_api_auth :index, :show, :update, :destroy
27
  accept_api_auth :index, :show, :create, :update, :destroy
28 28

  
29 29
  def index
30 30
    respond_to do |format|
......
46 46

  
47 47
  def show
48 48
    respond_to do |format|
49
      format.html {render_404}
49
      format.html {head :not_acceptable}
50 50
      format.api
51 51
    end
52 52
  end
......
58 58

  
59 59
  def create
60 60
    if @custom_field.save
61
      flash[:notice] = l(:notice_successful_create)
62 61
      call_hook(:controller_custom_fields_new_after_save, :params => params, :custom_field => @custom_field)
63
      if params[:continue]
64
        redirect_to new_custom_field_path({:type => @custom_field.type})
65
      else
66
        redirect_to custom_fields_path({:tab => @custom_field.type})
62
      respond_to do |format|
63
        format.html do
64
          flash[:notice] = l(:notice_successful_create)
65
          if params[:continue]
66
            redirect_to new_custom_field_path({:type => @custom_field.type})
67
          else
68
            redirect_to custom_fields_path({:tab => @custom_field.type})
69
          end
70
        end
71
        format.api do
72
          render :action => 'show', :status => :created, :location => custom_field_url(@custom_field)
73
        end
67 74
      end
68 75
    else
69
      render :action => 'new'
76
      respond_to do |format|
77
        format.html {render :action => 'new'}
78
        format.api {render_validation_errors(@custom_field)}
79
      end
70 80
    end
71 81
  end
72 82

  
......
124 134
  private
125 135

  
126 136
  def build_new_custom_field
127
    @custom_field = CustomField.new_subclass_instance(params[:type])
137
    @custom_field = CustomField.new_subclass_instance(custom_field_type_from_params)
128 138
    if @custom_field.nil?
129
      render :action => 'select_type'
139
      if api_request?
140
        render_api_errors("#{l(:field_type)} #{l('activerecord.errors.messages.invalid')}")
141
      else
142
        render :action => 'select_type'
143
      end
130 144
    else
131 145
      if params[:copy].present? && (@copy_from = CustomField.find_by(id: params[:copy]))
132 146
        @custom_field.copy_from(@copy_from)
......
135 149
    end
136 150
  end
137 151

  
152
  # The HTML form and the query string pass type as a top-level parameter,
153
  # while an XML body has to nest it under custom_field, so API requests also
154
  # look there.
155
  def custom_field_type_from_params
156
    return params[:type] if params[:type].present?
157
    return unless api_request?
158

  
159
    custom_field_params = params[:custom_field]
160
    custom_field_params[:type] if custom_field_params.is_a?(ActionController::Parameters)
161
  end
162

  
138 163
  def find_custom_field
139 164
    @custom_field = CustomField.find(params[:id])
140 165
  rescue ActiveRecord::RecordNotFound
test/functional/custom_fields_controller_test.rb
31 31
    assert_select 'table.custom_fields'
32 32
  end
33 33

  
34
  def test_show_html_format_should_respond_with_404
34
  def test_show_html_format_should_respond_with_406
35 35
    get :show, :params => {:id => 1}
36
    assert_response :not_found
36
    assert_response :not_acceptable
37 37
  end
38 38

  
39 39
  def test_new_without_type_should_render_select_type
......
602 602
    assert_nil CustomValue.find_by_custom_field_id(1)
603 603
  end
604 604

  
605
  def test_create_should_ignore_the_type_nested_in_custom_field_params
606
    assert_no_difference 'CustomField.count' do
607
      post(
608
        :create,
609
        :params => {
610
          :custom_field => {
611
            :type => 'IssueCustomField',
612
            :name => 'New field',
613
            :field_format => 'string'
614
          }
615
        }
616
      )
617
    end
618
    assert_response :success
619
    assert_select 'input[name=type]', CustomFieldsHelper::CUSTOM_FIELDS_TABS.size
620
  end
621

  
622
  def test_destroy_failure_should_display_an_error
623
    CustomField.any_instance.stubs(:destroy).returns(false)
624

  
625
    assert_no_difference 'CustomField.count' do
626
      delete(:destroy, :params => {:id => 1})
627
    end
628

  
629
    assert_redirected_to '/custom_fields?tab=IssueCustomField'
630
    assert_equal 'Unable to delete custom field', flash[:error]
631
  end
632

  
605 633
  def custom_field_classes
606 634
    classes =
607 635
      Dir.glob(Rails.root.join('app/models/*_custom_field.rb')).map do |f|
test/integration/api_test/custom_fields_test.rb
172 172
    assert_response :not_found
173 173
  end
174 174

  
175
  test "GET /custom_fields/:id.xml should respond with 403 for a non administrator" do
176
    get '/custom_fields/1.xml', :headers => credentials('jsmith')
175
  test "GET /custom_fields/:id should respond with 406 for the HTML format" do
176
    get '/custom_fields/1', :headers => credentials('admin')
177
    assert_response :not_acceptable
178
  end
179

  
180
  test "GET /custom_fields/:id.xml should respond with 401 without credentials" do
181
    get '/custom_fields/1.xml'
182
    assert_response :unauthorized
183
  end
184

  
185
  test "POST /custom_fields.xml should create the custom field" do
186
    assert_difference 'IssueCustomField.count' do
187
      post(
188
        '/custom_fields.xml',
189
        :params => {
190
          :type => 'IssueCustomField',
191
          :custom_field => {
192
            :name => 'New field',
193
            :field_format => 'string',
194
            :is_required => '1',
195
            :tracker_ids => ['1', '2']
196
          }
197
        },
198
        :headers => credentials('admin')
199
      )
200
    end
201
    assert_response :created
202
    assert_equal 'application/xml', response.media_type
203

  
204
    field = IssueCustomField.order(:id => :desc).first
205
    assert_equal 'New field', field.name
206
    assert_equal 'string', field.field_format
207
    assert field.is_required?
208
    assert_equal [1, 2], field.tracker_ids.sort
209
    assert_match %r{/custom_fields/#{field.id}\z}, response.headers['Location']
210

  
211
    assert_select 'custom_field' do
212
      assert_select 'id', :text => field.id.to_s
213
      assert_select 'name', :text => 'New field'
214
      assert_select 'customized_type', :text => 'issue'
215
    end
216
  end
217

  
218
  test "POST /custom_fields.xml should create a project custom field" do
219
    assert_difference 'ProjectCustomField.count' do
220
      post(
221
        '/custom_fields.xml',
222
        :params => {
223
          :type => 'ProjectCustomField',
224
          :custom_field => {:name => 'New field', :field_format => 'string'}
225
        },
226
        :headers => credentials('admin')
227
      )
228
    end
229
    assert_response :created
230
    assert_equal 'New field', ProjectCustomField.order(:id => :desc).first.name
231
  end
232

  
233
  test "POST /custom_fields.xml should create a list custom field with possible values" do
234
    assert_difference 'IssueCustomField.count' do
235
      post(
236
        '/custom_fields.xml',
237
        :params => {
238
          :type => 'IssueCustomField',
239
          :custom_field => {
240
            :name => 'New list field',
241
            :field_format => 'list',
242
            :possible_values => ['Foo', 'Bar']
243
          }
244
        },
245
        :headers => credentials('admin')
246
      )
247
    end
248
    assert_response :created
249
    assert_equal ['Foo', 'Bar'], IssueCustomField.order(:id => :desc).first.possible_values
250
  end
251

  
252
  test "POST /custom_fields.xml without type should respond with errors" do
253
    assert_no_difference 'CustomField.count' do
254
      post(
255
        '/custom_fields.xml',
256
        :params => {:custom_field => {:name => 'New field', :field_format => 'string'}},
257
        :headers => credentials('admin')
258
      )
259
    end
260
    assert_response :unprocessable_content
261
    assert_select 'errors error', :text => 'Type is invalid'
262
  end
263

  
264
  test "POST /custom_fields.json with type in custom field parameters should create the custom field" do
265
    assert_difference 'IssueCustomField.count' do
266
      post(
267
        '/custom_fields.json',
268
        :params => {
269
          :custom_field => {
270
            :type => 'IssueCustomField',
271
            :name => 'New field',
272
            :field_format => 'string'
273
          }
274
        },
275
        :headers => credentials('admin'),
276
        :as => :json
277
      )
278
    end
279
    assert_response :created
280

  
281
    field = IssueCustomField.order(:id => :desc).first
282
    assert_equal 'New field', field.name
283
    assert_equal 'string', field.field_format
284
  end
285

  
286
  test "POST /custom_fields.xml with type in custom field parameters should create the custom field" do
287
    payload = <<~XML
288
      <?xml version="1.0" encoding="UTF-8" ?>
289
      <custom_field>
290
        <type>IssueCustomField</type>
291
        <name>New XML field</name>
292
        <field_format>string</field_format>
293
      </custom_field>
294
    XML
295

  
296
    assert_difference 'IssueCustomField.count' do
297
      post(
298
        '/custom_fields.xml',
299
        :params => payload,
300
        :headers => {"CONTENT_TYPE" => 'application/xml'}.merge(credentials('admin'))
301
      )
302
    end
303
    assert_response :created
304

  
305
    field = IssueCustomField.order(:id => :desc).first
306
    assert_equal 'New XML field', field.name
307
    assert_equal 'string', field.field_format
308
  end
309

  
310
  test "POST /custom_fields.xml with an invalid type should respond with errors" do
311
    assert_no_difference 'CustomField.count' do
312
      post(
313
        '/custom_fields.xml',
314
        :params => {:type => 'UnknownCustomField', :custom_field => {:name => 'New field'}},
315
        :headers => credentials('admin')
316
      )
317
    end
318
    assert_response :unprocessable_content
319
    assert_select 'errors error', :text => 'Type is invalid'
320
  end
321

  
322
  test "POST /custom_fields.xml with a scalar custom field parameter should respond with errors" do
323
    assert_no_difference 'CustomField.count' do
324
      post(
325
        '/custom_fields.xml?custom_field=foo',
326
        :headers => credentials('admin')
327
      )
328
    end
329
    assert_response :unprocessable_content
330
    assert_select 'errors error', :text => 'Type is invalid'
331
  end
332

  
333
  test "POST /custom_fields.xml with invalid parameters should respond with errors" do
334
    assert_no_difference 'CustomField.count' do
335
      post(
336
        '/custom_fields.xml',
337
        :params => {
338
          :type => 'IssueCustomField',
339
          :custom_field => {:name => '', :field_format => 'string'}
340
        },
341
        :headers => credentials('admin')
342
      )
343
    end
344
    assert_response :unprocessable_content
345
    assert_select 'errors error', :text => "Name cannot be blank"
346
  end
347

  
348
  test "POST /custom_fields.xml should respond with 403 for a non administrator" do
349
    assert_no_difference 'CustomField.count' do
350
      post(
351
        '/custom_fields.xml',
352
        :params => {
353
          :type => 'IssueCustomField',
354
          :custom_field => {:name => 'New field', :field_format => 'string'}
355
        },
356
        :headers => credentials('jsmith')
357
      )
358
    end
177 359
    assert_response :forbidden
178 360
  end
179 361

  
362
  test "POST /custom_fields.xml should copy a custom field" do
363
    source = IssueCustomField.find(1)
364

  
365
    assert_difference 'IssueCustomField.count' do
366
      post(
367
        "/custom_fields.xml?copy=#{source.id}",
368
        :params => {
369
          :type => 'IssueCustomField',
370
          :custom_field => {:name => 'Copied field'}
371
        },
372
        :headers => credentials('admin')
373
      )
374
    end
375
    assert_response :created
376

  
377
    field = IssueCustomField.order(:id => :desc).first
378
    assert_equal 'Copied field', field.name
379
    assert_equal source.field_format, field.field_format
380
    assert_equal source.possible_values, field.possible_values
381
  end
382

  
383
  test "POST /custom_fields.xml should save roles and projects when visibility is restricted" do
384
    assert_difference 'IssueCustomField.count' do
385
      post(
386
        '/custom_fields.xml',
387
        :params => {
388
          :type => 'IssueCustomField',
389
          :custom_field => {
390
            :name => 'Restricted field',
391
            :field_format => 'string',
392
            :visible => '0',
393
            :role_ids => ['1', '2'],
394
            :is_for_all => '0',
395
            :project_ids => ['1', '2']
396
          }
397
        },
398
        :headers => credentials('admin')
399
      )
400
    end
401
    assert_response :created
402

  
403
    field = IssueCustomField.order(:id => :desc).first
404
    assert_equal [1, 2], field.role_ids.sort
405
    assert_equal [1, 2], field.project_ids.sort
406
  end
407

  
180 408
  test "PUT /custom_fields/:id.xml should update the custom field" do
181 409
    put(
182 410
      '/custom_fields/1.xml',
......
197 425
    assert_select 'errors error', :text => "Name cannot be blank"
198 426
  end
199 427

  
200
  test "PUT /custom_fields/:id.xml should respond with 403 for a non administrator" do
201
    put(
202
      '/custom_fields/1.xml',
203
      :params => {:custom_field => {:name => 'Renamed'}},
204
      :headers => credentials('jsmith')
205
    )
206
    assert_response :forbidden
207
  end
208

  
209 428
  test "DELETE /custom_fields/:id.xml should destroy the custom field" do
210 429
    assert_difference 'CustomField.count', -1 do
211 430
      delete '/custom_fields/1.xml', :headers => credentials('admin')
......
214 433
    assert_nil CustomField.find_by_id(1)
215 434
  end
216 435

  
217
  test "DELETE /custom_fields/:id.xml should respond with 403 for a non administrator" do
436
  test "DELETE /custom_fields/:id.xml with failure should return errors" do
437
    CustomField.any_instance.stubs(:destroy).returns(false)
438

  
218 439
    assert_no_difference 'CustomField.count' do
219
      delete '/custom_fields/1.xml', :headers => credentials('jsmith')
440
      delete '/custom_fields/1.xml', :headers => credentials('admin')
220 441
    end
221
    assert_response :forbidden
442
    assert_response :unprocessable_content
443
    assert_select 'errors error', :text => 'Unable to delete custom field'
444
  end
445

  
446
  test "DELETE /custom_fields/:id.xml raising an exception should return errors" do
447
    CustomField.any_instance.stubs(:destroy).raises(RuntimeError)
448

  
449
    assert_no_difference 'CustomField.count' do
450
      delete '/custom_fields/1.xml', :headers => credentials('admin')
451
    end
452
    assert_response :unprocessable_content
453
    assert_select 'errors error', :text => 'Unable to delete custom field'
222 454
  end
223 455
end
test/integration/routing/custom_fields_test.rb
25 25
    should_route 'GET /custom_fields/new' => 'custom_fields#new'
26 26
    should_route 'POST /custom_fields' => 'custom_fields#create'
27 27

  
28
    should_route 'GET /custom_fields/2' => 'custom_fields#show', :id => '2'
28 29
    should_route 'GET /custom_fields/2/edit' => 'custom_fields#edit', :id => '2'
29 30
    should_route 'PUT /custom_fields/2' => 'custom_fields#update', :id => '2'
30 31
    should_route 'DELETE /custom_fields/2' => 'custom_fields#destroy', :id => '2'
(2-2/2)