Refactor validations and callbacks on ObjectManager::Attribute (fixes #2159)
This commit is contained in:
parent
1bc9f245e6
commit
f848f8d151
5 changed files with 240 additions and 173 deletions
|
@ -2,18 +2,38 @@ class ObjectManager::Attribute < ApplicationModel
|
||||||
include ChecksClientNotification
|
include ChecksClientNotification
|
||||||
include CanSeed
|
include CanSeed
|
||||||
|
|
||||||
|
DATA_TYPES = %w[
|
||||||
|
input
|
||||||
|
user_autocompletion
|
||||||
|
checkbox
|
||||||
|
select
|
||||||
|
tree_select
|
||||||
|
datetime
|
||||||
|
date
|
||||||
|
tag
|
||||||
|
richtext
|
||||||
|
textarea
|
||||||
|
integer
|
||||||
|
autocompletion_ajax
|
||||||
|
boolean
|
||||||
|
user_permission
|
||||||
|
active
|
||||||
|
].freeze
|
||||||
|
|
||||||
self.table_name = 'object_manager_attributes'
|
self.table_name = 'object_manager_attributes'
|
||||||
|
|
||||||
belongs_to :object_lookup
|
belongs_to :object_lookup
|
||||||
|
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
|
validates :data_type, inclusion: { in: DATA_TYPES, msg: '%{value} is not a valid data type' }
|
||||||
|
validate :data_option_must_have_appropriate_values
|
||||||
|
validate :data_type_must_not_change, on: :update
|
||||||
|
|
||||||
store :screens
|
store :screens
|
||||||
store :data_option
|
store :data_option
|
||||||
store :data_option_new
|
store :data_option_new
|
||||||
|
|
||||||
before_create :check_datatype
|
before_validation :set_base_options
|
||||||
before_update :check_datatype, :verify_possible_type_change
|
|
||||||
|
|
||||||
=begin
|
=begin
|
||||||
|
|
||||||
|
@ -892,88 +912,87 @@ is certain attribute used by triggers, overviews or schedulers
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def check_datatype
|
# when setting default values for boolean fields,
|
||||||
local_data_option = data_option
|
# favor #nil? tests over ||= (which will overwrite `false`)
|
||||||
if to_config == true
|
def set_base_options
|
||||||
local_data_option = data_option_new
|
local_data_option[:null] = true if local_data_option[:null].nil?
|
||||||
end
|
|
||||||
if !data_type
|
|
||||||
raise 'Need data_type param'
|
|
||||||
end
|
|
||||||
if !data_type.match?(/^(input|user_autocompletion|checkbox|select|tree_select|datetime|date|tag|richtext|textarea|integer|autocompletion_ajax|boolean|user_permission|active)$/)
|
|
||||||
raise "Invalid data_type param '#{data_type}'"
|
|
||||||
end
|
|
||||||
|
|
||||||
if local_data_option.blank?
|
case data_type
|
||||||
raise 'Need data_option param'
|
when /^((tree_)?select|checkbox)$/
|
||||||
end
|
local_data_option[:nulloption] = true if local_data_option[:nulloption].nil?
|
||||||
if local_data_option[:null].nil?
|
local_data_option[:maxlength] ||= 255
|
||||||
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 !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 !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 !local_data_option[item]
|
|
||||||
raise "Invalid data_option[#{item.inspect}] param #{data_option[item]}" if local_data_option[item].to_s !~ /^\d+?$/
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if data_type == 'select' || data_type == 'tree_select' || data_type == 'checkbox'
|
def data_option_must_have_appropriate_values
|
||||||
raise 'Need data_option[:default] param' if !local_data_option.key?(:default)
|
data_option_validations
|
||||||
raise 'Invalid data_option[:options] or data_option[:relation] param' if local_data_option[:options].nil? && local_data_option[:relation].nil?
|
.select { |validation| validation[:failed] }
|
||||||
if !local_data_option.key?(:maxlength)
|
.each { |validation| errors.add(local_data_attr, validation[:message]) }
|
||||||
local_data_option[:maxlength] = 255
|
|
||||||
end
|
|
||||||
if !local_data_option.key?(:nulloption)
|
|
||||||
local_data_option[:nulloption] = true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if data_type == 'boolean'
|
def data_type_must_not_change
|
||||||
raise 'Need data_option[:default] param true|false|undefined' if !local_data_option.key?(:default)
|
allowable_changes = %w[tree_select select input checkbox]
|
||||||
raise 'Invalid data_option[:options] param' if local_data_option[:options].nil?
|
|
||||||
|
return if !data_type_changed?
|
||||||
|
return if (data_type_change - allowable_changes).empty?
|
||||||
|
|
||||||
|
errors.add(:data_type, "can't be altered after creation " \
|
||||||
|
'(delete the attribute and create another with the desired value)')
|
||||||
end
|
end
|
||||||
|
|
||||||
if data_type == 'datetime'
|
def local_data_option
|
||||||
raise 'Need data_option[:future] param true|false' if local_data_option[:future].nil?
|
@local_data_option ||= send(local_data_attr)
|
||||||
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
|
end
|
||||||
|
|
||||||
if data_type == 'date'
|
def local_data_attr
|
||||||
raise 'Need data_option[:future] param true|false' if local_data_option[:future].nil?
|
@local_data_attr ||= to_config ? :data_option_new : :data_option
|
||||||
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
|
end
|
||||||
|
|
||||||
true
|
def local_data_option=(val)
|
||||||
|
send("#{local_data_attr}=", val)
|
||||||
end
|
end
|
||||||
|
|
||||||
def verify_possible_type_change
|
def data_option_validations
|
||||||
return true if changes_to_save['data_type'].blank?
|
case data_type
|
||||||
|
when 'input'
|
||||||
possible = {
|
[{ failed: %w[text password tel fax email url].exclude?(local_data_option[:type]),
|
||||||
'select' => %w[tree_select select input checkbox],
|
message: 'must have one of text/password/tel/fax/email/url for :type' },
|
||||||
'tree_select' => %w[tree_select select input checkbox],
|
{ failed: !local_data_option[:maxlength].to_s.match?(/^\d+$/),
|
||||||
'checkbox' => %w[tree_select select input checkbox],
|
message: 'must have integer for :maxlength' }]
|
||||||
'input' => %w[tree_select select input checkbox],
|
when 'richtext'
|
||||||
}
|
[{ failed: !local_data_option[:maxlength].to_s.match?(/^\d+$/),
|
||||||
|
message: 'must have integer for :maxlength' }]
|
||||||
return true if possible[changes_to_save['data_type'][0]]&.include?(changes_to_save['data_type'][1])
|
when 'integer'
|
||||||
|
[{ failed: !local_data_option[:min].to_s.match?(/^\d+$/),
|
||||||
raise 'Can\'t be changed data_type of attribute. Drop the attribute and recreate it with new data_type.'
|
message: 'must have integer for :min' },
|
||||||
|
{ failed: !local_data_option[:max].to_s.match?(/^\d+$/),
|
||||||
|
message: 'must have integer for :max' }]
|
||||||
|
when /^((tree_)?select|checkbox)$/
|
||||||
|
[{ failed: !local_data_option.key?(:default),
|
||||||
|
message: 'must have value for :default' },
|
||||||
|
{ failed: local_data_option[:options].nil? && local_data_option[:relation].nil?,
|
||||||
|
message: 'must have non-nil value for either :options or :relation' }]
|
||||||
|
when 'boolean'
|
||||||
|
[{ failed: !local_data_option.key?(:default),
|
||||||
|
message: 'must have boolean/undefined value for :default' },
|
||||||
|
{ failed: local_data_option[:options].nil?,
|
||||||
|
message: 'must have non-nil value for :options' }]
|
||||||
|
when 'datetime'
|
||||||
|
[{ failed: local_data_option[:future].nil?,
|
||||||
|
message: 'must have boolean value for :future' },
|
||||||
|
{ failed: local_data_option[:past].nil?,
|
||||||
|
message: 'must have boolean value for :past' },
|
||||||
|
{ failed: local_data_option[:diff].nil?,
|
||||||
|
message: 'must have integer value for :diff (in hours)' }]
|
||||||
|
when 'date'
|
||||||
|
[{ failed: local_data_option[:future].nil?,
|
||||||
|
message: 'must have boolean value for :future' },
|
||||||
|
{ failed: local_data_option[:past].nil?,
|
||||||
|
message: 'must have boolean value for :past' },
|
||||||
|
{ failed: local_data_option[:diff].nil?,
|
||||||
|
message: 'must have integer value for :diff (in days)' }]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,40 +8,32 @@ RSpec.describe CheckForObjectAttributes, type: :db_migration do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'valid [:data_option]' do
|
context 'with a valid #data_option hash' do
|
||||||
|
|
||||||
it 'does not change converted text attribute' do
|
it 'does not change converted text attribute' do
|
||||||
attribute = create(:object_manager_attribute_text)
|
attribute = create(:object_manager_attribute_text)
|
||||||
|
|
||||||
expect do
|
expect { migrate }
|
||||||
migrate
|
.not_to change { attribute.reload.data_option }
|
||||||
end.not_to change {
|
|
||||||
attribute.reload.data_option
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not change select attribute' do
|
it 'does not change select attribute' do
|
||||||
attribute = create(:object_manager_attribute_select)
|
attribute = create(:object_manager_attribute_select)
|
||||||
|
|
||||||
expect do
|
expect { migrate }
|
||||||
migrate
|
.not_to change { attribute.reload.data_option }
|
||||||
end.not_to change {
|
|
||||||
attribute.reload.data_option
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not change tree_select attribute' do
|
it 'does not change tree_select attribute' do
|
||||||
attribute = create(:object_manager_attribute_tree_select)
|
attribute = create(:object_manager_attribute_tree_select)
|
||||||
|
|
||||||
expect do
|
expect { migrate }
|
||||||
migrate
|
.not_to change { attribute.reload.data_option }
|
||||||
end.not_to change {
|
|
||||||
attribute.reload.data_option
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context '[:data_option][:options]' do
|
context 'for #data_option key:' do
|
||||||
|
context ':options' do
|
||||||
|
|
||||||
it 'converts String to Hash' do
|
it 'converts String to Hash' do
|
||||||
wrong = {
|
wrong = {
|
||||||
|
@ -62,7 +54,7 @@ RSpec.describe CheckForObjectAttributes, type: :db_migration do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context '[:data_option][:relation]' do
|
context ':relation' do
|
||||||
|
|
||||||
it 'ensures an empty String' do
|
it 'ensures an empty String' do
|
||||||
wrong = {
|
wrong = {
|
||||||
|
@ -98,4 +90,26 @@ RSpec.describe CheckForObjectAttributes, type: :db_migration do
|
||||||
expect(attribute[:data_option][:relation]).to be_blank
|
expect(attribute[:data_option][:relation]).to be_blank
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# see https://github.com/zammad/zammad/issues/2159
|
||||||
|
context ':null' do
|
||||||
|
|
||||||
|
it 'does not fail on missing values' do
|
||||||
|
wrong = {
|
||||||
|
default: '',
|
||||||
|
options: '', # <- this is not the attribute under test,
|
||||||
|
relation: '', # but it must be invalid
|
||||||
|
type: 'text', # to trigger a #save in the migration.
|
||||||
|
maxlength: 255,
|
||||||
|
}
|
||||||
|
|
||||||
|
# rubocop:disable Rails/SkipsModelValidations
|
||||||
|
create(:object_manager_attribute_text)
|
||||||
|
.update_columns(data_option: wrong)
|
||||||
|
# rubocop:enable Rails/SkipsModelValidations
|
||||||
|
|
||||||
|
expect { migrate }.not_to raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
52
spec/models/object_manager/attribute_spec.rb
Normal file
52
spec/models/object_manager/attribute_spec.rb
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe ObjectManager::Attribute, type: :model do
|
||||||
|
describe 'callbacks' do
|
||||||
|
context 'for setting default values on local data options' do
|
||||||
|
let(:subject) { described_class.new }
|
||||||
|
|
||||||
|
context ':null' do
|
||||||
|
it 'sets nil values to true' do
|
||||||
|
expect { subject.validate }
|
||||||
|
.to change { subject.data_option[:null] }.to(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not overwrite false values' do
|
||||||
|
subject.data_option[:null] = false
|
||||||
|
|
||||||
|
expect { subject.validate }
|
||||||
|
.not_to change { subject.data_option[:null] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context ':maxlength' do
|
||||||
|
context 'for data_type: select / tree_select / checkbox' do
|
||||||
|
let(:subject) { described_class.new(data_type: 'select') }
|
||||||
|
|
||||||
|
it 'sets nil values to 255' do
|
||||||
|
expect { subject.validate }
|
||||||
|
.to change { subject.data_option[:maxlength] }.to(255)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context ':nulloption' do
|
||||||
|
context 'for data_type: select / tree_select / checkbox' do
|
||||||
|
let(:subject) { described_class.new(data_type: 'select') }
|
||||||
|
|
||||||
|
it 'sets nil values to true' do
|
||||||
|
expect { subject.validate }
|
||||||
|
.to change { subject.data_option[:nulloption] }.to(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not overwrite false values' do
|
||||||
|
subject.data_option[:nulloption] = false
|
||||||
|
|
||||||
|
expect { subject.validate }
|
||||||
|
.not_to change { subject.data_option[:nulloption] }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1076,7 +1076,7 @@ class ObjectManagerAttributesControllerTest < ActionDispatch::IntegrationTest
|
||||||
assert_response(422)
|
assert_response(422)
|
||||||
result = JSON.parse(@response.body)
|
result = JSON.parse(@response.body)
|
||||||
assert(result)
|
assert(result)
|
||||||
assert(result['error']['Can\'t be changed data_type of attribute. Drop the attribute and recreate it with new data_type.'])
|
assert(result['error'])
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -109,7 +109,7 @@ class ObjectManagerTest < ActiveSupport::TestCase
|
||||||
updated_by_id: 1,
|
updated_by_id: 1,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
assert_raises(RuntimeError) do
|
assert_raises(ActiveRecord::RecordInvalid) do
|
||||||
attribute4 = ObjectManager::Attribute.add(
|
attribute4 = ObjectManager::Attribute.add(
|
||||||
object: 'Ticket',
|
object: 'Ticket',
|
||||||
name: 'test4',
|
name: 'test4',
|
||||||
|
@ -153,7 +153,7 @@ class ObjectManagerTest < ActiveSupport::TestCase
|
||||||
name: 'test5',
|
name: 'test5',
|
||||||
)
|
)
|
||||||
|
|
||||||
assert_raises(RuntimeError) do
|
assert_raises(ActiveRecord::RecordInvalid) do
|
||||||
attribute6 = ObjectManager::Attribute.add(
|
attribute6 = ObjectManager::Attribute.add(
|
||||||
object: 'Ticket',
|
object: 'Ticket',
|
||||||
name: 'test6',
|
name: 'test6',
|
||||||
|
@ -200,7 +200,7 @@ class ObjectManagerTest < ActiveSupport::TestCase
|
||||||
name: 'test7',
|
name: 'test7',
|
||||||
)
|
)
|
||||||
|
|
||||||
assert_raises(RuntimeError) do
|
assert_raises(ActiveRecord::RecordInvalid) do
|
||||||
attribute8 = ObjectManager::Attribute.add(
|
attribute8 = ObjectManager::Attribute.add(
|
||||||
object: 'Ticket',
|
object: 'Ticket',
|
||||||
name: 'test8',
|
name: 'test8',
|
||||||
|
@ -242,7 +242,7 @@ class ObjectManagerTest < ActiveSupport::TestCase
|
||||||
name: 'test9',
|
name: 'test9',
|
||||||
)
|
)
|
||||||
|
|
||||||
assert_raises(RuntimeError) do
|
assert_raises(ActiveRecord::RecordInvalid) do
|
||||||
attribute10 = ObjectManager::Attribute.add(
|
attribute10 = ObjectManager::Attribute.add(
|
||||||
object: 'Ticket',
|
object: 'Ticket',
|
||||||
name: 'test10',
|
name: 'test10',
|
||||||
|
@ -285,7 +285,7 @@ class ObjectManagerTest < ActiveSupport::TestCase
|
||||||
name: 'test11',
|
name: 'test11',
|
||||||
)
|
)
|
||||||
|
|
||||||
assert_raises(RuntimeError) do
|
assert_raises(ActiveRecord::RecordInvalid) do
|
||||||
attribute12 = ObjectManager::Attribute.add(
|
attribute12 = ObjectManager::Attribute.add(
|
||||||
object: 'Ticket',
|
object: 'Ticket',
|
||||||
name: 'test12',
|
name: 'test12',
|
||||||
|
@ -368,27 +368,9 @@ class ObjectManagerTest < ActiveSupport::TestCase
|
||||||
end
|
end
|
||||||
assert_equal(false, ObjectManager::Attribute.pending_migration?)
|
assert_equal(false, ObjectManager::Attribute.pending_migration?)
|
||||||
|
|
||||||
assert_raises(RuntimeError) do
|
# Test case #16 invalidated after callback added to set default #data_option[:null] value
|
||||||
attribute16 = ObjectManager::Attribute.add(
|
|
||||||
object: 'Ticket',
|
|
||||||
name: 'test16',
|
|
||||||
display: 'Test 16',
|
|
||||||
data_type: 'integer',
|
|
||||||
data_option: {
|
|
||||||
default: 2,
|
|
||||||
min: 1,
|
|
||||||
max: 999,
|
|
||||||
},
|
|
||||||
active: true,
|
|
||||||
screens: {},
|
|
||||||
position: 20,
|
|
||||||
created_by_id: 1,
|
|
||||||
updated_by_id: 1,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
assert_equal(false, ObjectManager::Attribute.pending_migration?)
|
|
||||||
|
|
||||||
assert_raises(RuntimeError) do
|
assert_raises(ActiveRecord::RecordInvalid) do
|
||||||
attribute17 = ObjectManager::Attribute.add(
|
attribute17 = ObjectManager::Attribute.add(
|
||||||
object: 'Ticket',
|
object: 'Ticket',
|
||||||
name: 'test17',
|
name: 'test17',
|
||||||
|
@ -861,7 +843,7 @@ class ObjectManagerTest < ActiveSupport::TestCase
|
||||||
|
|
||||||
assert(ObjectManager::Attribute.migration_execute)
|
assert(ObjectManager::Attribute.migration_execute)
|
||||||
|
|
||||||
assert_raises(RuntimeError) do
|
assert_raises(ActiveRecord::RecordInvalid) do
|
||||||
ObjectManager::Attribute.add(
|
ObjectManager::Attribute.add(
|
||||||
object: 'Ticket',
|
object: 'Ticket',
|
||||||
name: 'example_1',
|
name: 'example_1',
|
||||||
|
|
Loading…
Reference in a new issue