Fixed issue #2099 - Changing data type of new object attributes will lead to errors.
This commit is contained in:
parent
4bae98e55a
commit
40a663198c
7 changed files with 310 additions and 82 deletions
|
@ -28,6 +28,12 @@ class App.UiElement.object_manager_attribute extends App.UiElement.ApplicationUi
|
|||
boolean: 'Boolean'
|
||||
integer: 'Integer'
|
||||
|
||||
# if attribute already exists, do not allow to change it anymore
|
||||
if params.data_type
|
||||
for key, value of options
|
||||
if key isnt params.data_type
|
||||
delete options[key]
|
||||
|
||||
configureAttributes = [
|
||||
{ name: attribute.name, display: '', tag: 'select', null: false, options: options, translate: true, default: 'input', disabled: attribute.disabled },
|
||||
]
|
||||
|
|
|
@ -12,6 +12,9 @@ class ObjectManager::Attribute < ApplicationModel
|
|||
store :data_option
|
||||
store :data_option_new
|
||||
|
||||
before_create :check_datatype
|
||||
before_update :check_datatype, :verify_possible_type_change
|
||||
|
||||
=begin
|
||||
|
||||
list of all attributes
|
||||
|
@ -324,7 +327,6 @@ possible types
|
|||
record.check_editable
|
||||
record.check_name
|
||||
end
|
||||
record.check_datatype
|
||||
record.save!
|
||||
return record
|
||||
end
|
||||
|
@ -344,7 +346,6 @@ possible types
|
|||
record.check_editable
|
||||
record.check_name
|
||||
end
|
||||
record.check_datatype
|
||||
record.save!
|
||||
record
|
||||
end
|
||||
|
@ -878,7 +879,13 @@ is certain attribute used by triggers, overviews or schedulers
|
|||
raise 'Attribute not editable!'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_datatype
|
||||
local_data_option = data_option
|
||||
if to_config == true
|
||||
local_data_option = data_option_new
|
||||
end
|
||||
if !data_type
|
||||
raise 'Need data_type param'
|
||||
end
|
||||
|
@ -886,62 +893,76 @@ is certain attribute used by triggers, overviews or schedulers
|
|||
raise "Invalid data_type param '#{data_type}'"
|
||||
end
|
||||
|
||||
if !data_option
|
||||
raise 'Need data_type param'
|
||||
if local_data_option.blank?
|
||||
raise 'Need data_option param'
|
||||
end
|
||||
if data_option[:null].nil?
|
||||
if local_data_option[:null].nil?
|
||||
raise 'Need data_option[:null] param with true or false'
|
||||
end
|
||||
|
||||
# validate data_option
|
||||
if data_type == 'input'
|
||||
raise 'Need data_option[:type] param e. g. (text|password|tel|fax|email|url)' if !data_option[:type]
|
||||
raise "Invalid data_option[:type] param '#{data_option[:type]}' (text|password|tel|fax|email|url)" if data_option[:type] !~ /^(text|password|tel|fax|email|url)$/
|
||||
raise 'Need data_option[:maxlength] param' if !data_option[:maxlength]
|
||||
raise "Invalid data_option[:maxlength] param #{data_option[:maxlength]}" if data_option[:maxlength].to_s !~ /^\d+?$/
|
||||
raise 'Need data_option[:type] param e. g. (text|password|tel|fax|email|url)' if !local_data_option[:type]
|
||||
raise "Invalid data_option[:type] param '#{local_data_option[:type]}' (text|password|tel|fax|email|url)" if local_data_option[:type] !~ /^(text|password|tel|fax|email|url)$/
|
||||
raise 'Need data_option[:maxlength] param' if !local_data_option[:maxlength]
|
||||
raise "Invalid data_option[:maxlength] param #{local_data_option[:maxlength]}" if local_data_option[:maxlength].to_s !~ /^\d+?$/
|
||||
end
|
||||
|
||||
if data_type == 'richtext'
|
||||
raise 'Need data_option[:maxlength] param' if !data_option[:maxlength]
|
||||
raise "Invalid data_option[:maxlength] param #{data_option[:maxlength]}" if data_option[:maxlength].to_s !~ /^\d+?$/
|
||||
raise 'Need data_option[:maxlength] param' if !local_data_option[:maxlength]
|
||||
raise "Invalid data_option[:maxlength] param #{local_data_option[:maxlength]}" if local_data_option[:maxlength].to_s !~ /^\d+?$/
|
||||
end
|
||||
|
||||
if data_type == 'integer'
|
||||
%i[min max].each do |item|
|
||||
raise "Need data_option[#{item.inspect}] param" if !data_option[item]
|
||||
raise "Invalid data_option[#{item.inspect}] param #{data_option[item]}" if data_option[item].to_s !~ /^\d+?$/
|
||||
raise "Need data_option[#{item.inspect}] param" if !local_data_option[item]
|
||||
raise "Invalid data_option[#{item.inspect}] param #{data_option[item]}" if local_data_option[item].to_s !~ /^\d+?$/
|
||||
end
|
||||
end
|
||||
|
||||
if data_type == 'select' || data_type == 'tree_select' || data_type == 'checkbox'
|
||||
raise 'Need data_option[:default] param' if !data_option.key?(:default)
|
||||
raise 'Invalid data_option[:options] or data_option[:relation] param' if data_option[:options].nil? && data_option[:relation].nil?
|
||||
if !data_option.key?(:maxlength)
|
||||
data_option[:maxlength] = 255
|
||||
raise 'Need data_option[:default] param' if !local_data_option.key?(:default)
|
||||
raise 'Invalid data_option[:options] or data_option[:relation] param' if local_data_option[:options].nil? && local_data_option[:relation].nil?
|
||||
if !local_data_option.key?(:maxlength)
|
||||
local_data_option[:maxlength] = 255
|
||||
end
|
||||
if !data_option.key?(:nulloption)
|
||||
data_option[:nulloption] = true
|
||||
if !local_data_option.key?(:nulloption)
|
||||
local_data_option[:nulloption] = true
|
||||
end
|
||||
end
|
||||
|
||||
if data_type == 'boolean'
|
||||
raise 'Need data_option[:default] param true|false|undefined' if !data_option.key?(:default)
|
||||
raise 'Invalid data_option[:options] param' if data_option[:options].nil?
|
||||
raise 'Need data_option[:default] param true|false|undefined' if !local_data_option.key?(:default)
|
||||
raise 'Invalid data_option[:options] param' if local_data_option[:options].nil?
|
||||
end
|
||||
|
||||
if data_type == 'datetime'
|
||||
raise 'Need data_option[:future] param true|false' if data_option[:future].nil?
|
||||
raise 'Need data_option[:past] param true|false' if data_option[:past].nil?
|
||||
raise 'Need data_option[:diff] param in hours' if data_option[:diff].nil?
|
||||
raise 'Need data_option[:future] param true|false' if local_data_option[:future].nil?
|
||||
raise 'Need data_option[:past] param true|false' if local_data_option[:past].nil?
|
||||
raise 'Need data_option[:diff] param in hours' if local_data_option[:diff].nil?
|
||||
end
|
||||
|
||||
if data_type == 'date'
|
||||
raise 'Need data_option[:future] param true|false' if data_option[:future].nil?
|
||||
raise 'Need data_option[:past] param true|false' if data_option[:past].nil?
|
||||
raise 'Need data_option[:diff] param in days' if data_option[:diff].nil?
|
||||
raise 'Need data_option[:future] param true|false' if local_data_option[:future].nil?
|
||||
raise 'Need data_option[:past] param true|false' if local_data_option[:past].nil?
|
||||
raise 'Need data_option[:diff] param in days' if local_data_option[:diff].nil?
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def verify_possible_type_change
|
||||
return true if changes_to_save['data_type'].blank?
|
||||
|
||||
possible = {
|
||||
'select' => %w[tree_select select input checkbox],
|
||||
'tree_select' => %w[tree_select select input checkbox],
|
||||
'checkbox' => %w[tree_select select input checkbox],
|
||||
'input' => %w[tree_select select input checkbox],
|
||||
}
|
||||
|
||||
return true if possible[changes_to_save['data_type'][0]]&.include?(changes_to_save['data_type'][1])
|
||||
|
||||
raise 'Can\'t be changed data_type of attribute. Drop the attribute and recreate it with new data_type.'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -41,32 +41,15 @@ RSpec.describe CheckForObjectAttributes, type: :db_migration do
|
|||
end
|
||||
end
|
||||
|
||||
context '[:data_option]' do
|
||||
|
||||
it 'ensures an empty Hash' do
|
||||
attribute = create(:object_manager_attribute_text, data_option: nil)
|
||||
migrate
|
||||
attribute.reload
|
||||
|
||||
expect(attribute[:data_option]).to be_a(Hash)
|
||||
end
|
||||
end
|
||||
|
||||
context '[:data_option][:options]' do
|
||||
|
||||
it 'ensures an empty Hash' do
|
||||
attribute = create(:object_manager_attribute_text, data_option: {})
|
||||
migrate
|
||||
attribute.reload
|
||||
|
||||
expect(attribute[:data_option][:options]).to be_a(Hash)
|
||||
end
|
||||
|
||||
it 'converts String to Hash' do
|
||||
wrong = {
|
||||
default: '',
|
||||
options: '',
|
||||
relation: '',
|
||||
type: 'text',
|
||||
maxlength: 255,
|
||||
null: true
|
||||
}
|
||||
|
||||
|
@ -85,6 +68,8 @@ RSpec.describe CheckForObjectAttributes, type: :db_migration do
|
|||
wrong = {
|
||||
default: '',
|
||||
options: {},
|
||||
type: 'text',
|
||||
maxlength: 255,
|
||||
null: true
|
||||
}
|
||||
|
||||
|
@ -100,6 +85,8 @@ RSpec.describe CheckForObjectAttributes, type: :db_migration do
|
|||
default: '',
|
||||
options: {},
|
||||
relation: {},
|
||||
type: 'text',
|
||||
maxlength: 255,
|
||||
null: true
|
||||
}
|
||||
|
||||
|
|
|
@ -175,7 +175,7 @@ RSpec.describe Issue1660FixTreeSelectConfigurations, type: :db_migration do
|
|||
}
|
||||
]
|
||||
|
||||
attribute = create(:object_manager_attribute_tree_select, data_option: { options: broken })
|
||||
attribute = create(:object_manager_attribute_tree_select, data_option: { options: broken, null: true, default: '' })
|
||||
|
||||
expect do
|
||||
migrate
|
||||
|
@ -192,18 +192,8 @@ RSpec.describe Issue1660FixTreeSelectConfigurations, type: :db_migration do
|
|||
end
|
||||
end
|
||||
|
||||
it 'skips blank data_option' do
|
||||
attribute = create(:object_manager_attribute_tree_select, data_option: {})
|
||||
|
||||
expect do
|
||||
migrate
|
||||
end.not_to change {
|
||||
attribute.reload.data_option
|
||||
}
|
||||
end
|
||||
|
||||
it 'skips blank data_option options' do
|
||||
attribute = create(:object_manager_attribute_tree_select, data_option: { options: [] })
|
||||
attribute = create(:object_manager_attribute_tree_select, data_option: { options: [], null: true, default: '' })
|
||||
|
||||
expect do
|
||||
migrate
|
||||
|
|
|
@ -3610,7 +3610,10 @@ wait untill text in selector disabppears
|
|||
css: '.content.active a[href="#system/object_manager"]',
|
||||
mute_log: true,
|
||||
)
|
||||
sleep 4
|
||||
watch_for(
|
||||
browser: instance,
|
||||
css: '.content.active .js-new',
|
||||
)
|
||||
click(
|
||||
browser: instance,
|
||||
css: '.content.active .js-new',
|
||||
|
@ -3745,8 +3748,10 @@ wait untill text in selector disabppears
|
|||
css: '.content.active a[href="#system/object_manager"]',
|
||||
mute_log: true,
|
||||
)
|
||||
sleep 4
|
||||
|
||||
watch_for(
|
||||
browser: instance,
|
||||
css: '.content.active .js-new',
|
||||
)
|
||||
instance.execute_script("$(\".content.active td:contains('#{data[:name]}')\").first().click()")
|
||||
modal_ready(browser: instance)
|
||||
element = instance.find_elements(css: '.modal input[name=display]')[0]
|
||||
|
@ -3758,6 +3763,12 @@ wait untill text in selector disabppears
|
|||
value: data[:data_type],
|
||||
mute_log: true,
|
||||
)
|
||||
|
||||
# if attribute is created, do not be able to select other types anymore
|
||||
if instance.find_elements(css: '.modal select[name="data_type"] option').count > 1
|
||||
assert(false, 'able to change the data_type of existing attribute which should not be allowed')
|
||||
end
|
||||
|
||||
if data[:data_option]
|
||||
if data[:data_option][:options]
|
||||
if data[:data_type] == 'Boolean'
|
||||
|
|
|
@ -1014,4 +1014,138 @@ class ObjectManagerAttributesControllerTest < ActionDispatch::IntegrationTest
|
|||
assert @response.body.include?('cannot be deleted!')
|
||||
end
|
||||
|
||||
test '07 verify if attribute type can not be changed' do
|
||||
credentials = ActionController::HttpAuthentication::Basic.encode_credentials('tickets-admin@example.com', 'adminpw')
|
||||
|
||||
params = {
|
||||
'name': "customerdescription_#{rand(999_999_999)}",
|
||||
'object': 'Ticket',
|
||||
'display': "custom description #{rand(999_999_999)}",
|
||||
'active': true,
|
||||
'data_type': 'boolean',
|
||||
'data_option': {
|
||||
'options': {
|
||||
'true': '',
|
||||
'false': '',
|
||||
},
|
||||
'default': 'false',
|
||||
'screens': {
|
||||
'create_middle': {
|
||||
'ticket.customer': {
|
||||
'shown': true,
|
||||
'item_class': 'column'
|
||||
},
|
||||
'ticket.agent': {
|
||||
'shown': true,
|
||||
'item_class': 'column'
|
||||
}
|
||||
},
|
||||
'edit': {
|
||||
'ticket.customer': {
|
||||
'shown': true
|
||||
},
|
||||
'ticket.agent': {
|
||||
'shown': true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
post '/api/v1/object_manager_attributes', params: params.to_json, headers: @headers.merge('Authorization' => credentials)
|
||||
|
||||
assert_response(201) # created
|
||||
result = JSON.parse(@response.body)
|
||||
|
||||
assert(result)
|
||||
assert_not(result['data_option']['default'])
|
||||
assert_equal(result['data_option']['default'], false)
|
||||
assert_equal(result['data_type'], 'boolean')
|
||||
|
||||
migration = ObjectManager::Attribute.migration_execute
|
||||
assert_equal(migration, true)
|
||||
|
||||
params['data_type'] = 'input'
|
||||
params['data_option'] = {
|
||||
'default': 'test',
|
||||
'type': 'text',
|
||||
'maxlength': 120
|
||||
}
|
||||
|
||||
put "/api/v1/object_manager_attributes/#{result['id']}", params: params.to_json, headers: @headers.merge('Authorization' => credentials)
|
||||
assert_response(422)
|
||||
result = JSON.parse(@response.body)
|
||||
assert(result)
|
||||
assert(result['error']['Can\'t be changed data_type of attribute. Drop the attribute and recreate it with new data_type.'])
|
||||
|
||||
end
|
||||
|
||||
test '08 verify if attribute type can be changed' do
|
||||
credentials = ActionController::HttpAuthentication::Basic.encode_credentials('tickets-admin@example.com', 'adminpw')
|
||||
|
||||
params = {
|
||||
'name': "customerdescription_#{rand(999_999_999)}",
|
||||
'object': 'Ticket',
|
||||
'display': "custom description #{rand(999_999_999)}",
|
||||
'active': true,
|
||||
'data_type': 'input',
|
||||
'data_option': {
|
||||
'default': 'test',
|
||||
'type': 'text',
|
||||
'maxlength': 120,
|
||||
},
|
||||
'screens': {
|
||||
'create_middle': {
|
||||
'ticket.customer': {
|
||||
'shown': true,
|
||||
'item_class': 'column'
|
||||
},
|
||||
'ticket.agent': {
|
||||
'shown': true,
|
||||
'item_class': 'column'
|
||||
}
|
||||
},
|
||||
'edit': {
|
||||
'ticket.customer': {
|
||||
'shown': true
|
||||
},
|
||||
'ticket.agent': {
|
||||
'shown': true
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
post '/api/v1/object_manager_attributes', params: params.to_json, headers: @headers.merge('Authorization' => credentials)
|
||||
|
||||
assert_response(201) # created
|
||||
result = JSON.parse(@response.body)
|
||||
|
||||
assert(result)
|
||||
assert_equal(result['data_option']['default'], 'test')
|
||||
assert_equal(result['data_type'], 'input')
|
||||
|
||||
migration = ObjectManager::Attribute.migration_execute
|
||||
assert_equal(migration, true)
|
||||
|
||||
params['data_type'] = 'select'
|
||||
params['data_option'] = {
|
||||
'default': 'fuu',
|
||||
'options': {
|
||||
'key1': 'foo',
|
||||
'key2': 'fuu',
|
||||
}
|
||||
}
|
||||
|
||||
put "/api/v1/object_manager_attributes/#{result['id']}", params: params.to_json, headers: @headers.merge('Authorization' => credentials)
|
||||
|
||||
assert_response(200)
|
||||
result = JSON.parse(@response.body)
|
||||
assert(result)
|
||||
assert_equal(result['data_option']['default'], 'test')
|
||||
assert_equal(result['data_option_new']['default'], 'fuu')
|
||||
assert_equal(result['data_type'], 'select')
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -788,4 +788,83 @@ class ObjectManagerTest < ActiveSupport::TestCase
|
|||
assert_equal(1, overview[:count])
|
||||
assert_equal(ticket1.id, overview[:tickets][0][:id])
|
||||
end
|
||||
|
||||
test 'd object manager attribute - update attribute type' do
|
||||
|
||||
attribute1 = ObjectManager::Attribute.add(
|
||||
object: 'Ticket',
|
||||
name: 'example_1',
|
||||
display: 'example_1',
|
||||
data_type: 'input',
|
||||
data_option: {
|
||||
default: '',
|
||||
maxlength: 200,
|
||||
type: 'text',
|
||||
null: true,
|
||||
options: {},
|
||||
},
|
||||
active: true,
|
||||
screens: {},
|
||||
position: 20,
|
||||
created_by_id: 1,
|
||||
updated_by_id: 1,
|
||||
)
|
||||
|
||||
assert_equal(true, ObjectManager::Attribute.pending_migration?)
|
||||
assert_equal(1, ObjectManager::Attribute.migrations.count)
|
||||
|
||||
assert(ObjectManager::Attribute.migration_execute)
|
||||
|
||||
assert_raises(RuntimeError) do
|
||||
ObjectManager::Attribute.add(
|
||||
object: 'Ticket',
|
||||
name: 'example_1',
|
||||
display: 'example_1',
|
||||
data_type: 'boolean',
|
||||
data_option: {
|
||||
default: true,
|
||||
options: {
|
||||
true: 'Yes',
|
||||
false: 'No',
|
||||
},
|
||||
null: false,
|
||||
},
|
||||
active: true,
|
||||
screens: {},
|
||||
position: 200,
|
||||
created_by_id: 1,
|
||||
updated_by_id: 1,
|
||||
)
|
||||
end
|
||||
|
||||
attribute2 = ObjectManager::Attribute.add(
|
||||
object: 'Ticket',
|
||||
name: 'example_1',
|
||||
display: 'example_1',
|
||||
data_type: 'select',
|
||||
data_option: {
|
||||
default: '',
|
||||
maxlength: 200,
|
||||
type: 'text',
|
||||
null: true,
|
||||
options: {
|
||||
aa: 'aa',
|
||||
bb: 'bb',
|
||||
},
|
||||
},
|
||||
active: true,
|
||||
screens: {},
|
||||
position: 20,
|
||||
created_by_id: 1,
|
||||
updated_by_id: 1,
|
||||
)
|
||||
|
||||
assert_equal(attribute1.id, attribute2.id)
|
||||
assert_equal(true, ObjectManager::Attribute.pending_migration?)
|
||||
assert_equal(1, ObjectManager::Attribute.migrations.count)
|
||||
|
||||
assert(ObjectManager::Attribute.migration_execute)
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue