12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- class ObjectManager::Attribute < ApplicationModel
- include HasDefaultModelUserRelations
- include ChecksClientNotification
- include CanSeed
- DATA_TYPES = %w[
- input
- user_autocompletion
- checkbox
- select
- multiselect
- tree_select
- multi_tree_select
- datetime
- date
- tag
- richtext
- textarea
- integer
- autocompletion_ajax
- autocompletion_ajax_customer_organization
- autocompletion_ajax_external_data_source
- boolean
- user_permission
- group_permissions
- active
- ].freeze
- VALIDATE_INTEGER_MIN = -2_147_483_647
- VALIDATE_INTEGER_MAX = 2_147_483_647
- VALIDATE_INTEGER_REGEXP = %r{^-?\d+$}
- self.table_name = 'object_manager_attributes'
- belongs_to :object_lookup, optional: true
- validates :name, presence: true
- validates :data_type, inclusion: { in: DATA_TYPES, msg: '%{value} is not a valid data type' }
- validate :inactive_must_be_unused_by_references, unless: :active?
- validate :data_option_must_have_appropriate_values
- validate :data_type_must_not_change, on: :update
- validate :json_field_only_on_postgresql, on: :create
- store :screens
- store :data_option
- store :data_option_new
- before_validation :set_base_options
- before_create :ensure_multiselect
- before_update :ensure_multiselect
- scope :active, -> { where(active: true) }
- scope :editable, -> { where(editable: true) }
- scope :for_object, lambda { |name_or_klass|
- id = ObjectLookup.lookup(name: name_or_klass.to_s)
- where(object_lookup_id: id)
- }
- =begin
- list of all attributes
- result = ObjectManager::Attribute.list_full
- result = [
- {
- name: 'some name',
- display: '...',
- }.
- ],
- =end
- def self.list_full
- result = ObjectManager::Attribute.reorder('position ASC, name ASC')
- references = ObjectManager::Attribute.attribute_to_references_hash
- attributes = []
- result.each do |item|
- attribute = item.attributes
- attribute[:object] = ObjectLookup.by_id(item.object_lookup_id)
- attribute.delete('object_lookup_id')
- # an attribute is deletable if it is both editable and not referenced by other Objects (Triggers, Overviews, Schedulers)
- deletable = true
- not_deletable_reason = ''
- if ObjectManager::Attribute.attribute_used_by_references?(attribute[:object], attribute['name'], references)
- deletable = false
- not_deletable_reason = ObjectManager::Attribute.attribute_used_by_references_humaniced(attribute[:object], attribute['name'], references)
- end
- attribute[:deletable] = attribute['editable'] && deletable == true
- if not_deletable_reason.present?
- attribute[:not_deletable_reason] = "This attribute is referenced by #{not_deletable_reason} and thus cannot be deleted!"
- end
- attributes.push attribute
- end
- attributes
- end
- =begin
- add a new attribute entry for an object
- ObjectManager::Attribute.add(
- object: 'Ticket',
- name: 'group_id',
- display: __('Group'),
- data_type: 'tree_select',
- data_option: {
- relation: 'Group',
- relation_condition: { access: 'full' },
- multiple: false,
- null: true,
- translate: false,
- belongs_to: 'group',
- },
- active: true,
- screens: {
- create: {
- '-all-' => {
- required: true,
- },
- },
- edit: {
- 'ticket.agent' => {
- required: true,
- },
- },
- },
- position: 20,
- created_by_id: 1,
- updated_by_id: 1,
- created_at: '2014-06-04 10:00:00',
- updated_at: '2014-06-04 10:00:00',
- force: true
- editable: false,
- to_migrate: false,
- to_create: false,
- to_delete: false,
- to_config: false,
- )
- preserved name are
- /(_id|_ids)$/
- possible types
- # input
- data_type: 'input',
- data_option: {
- default: '',
- type: 'text', # text|email|url|tel
- maxlength: 200,
- null: true,
- note: 'some additional comment', # optional
- link_template: '', # optional
- },
- # select
- data_type: 'select',
- data_option: {
- default: 'aa',
- options: {
- 'aa' => 'aa (comment)',
- 'bb' => 'bb (comment)',
- },
- maxlength: 200,
- nulloption: true,
- null: false,
- multiple: false, # currently only "false" supported
- translate: true, # optional
- note: 'some additional comment', # optional
- link_template: '', # optional
- },
- # tree_select
- data_type: 'tree_select',
- data_option: {
- default: 'aa',
- options: [
- {
- 'value' => 'aa',
- 'name' => 'aa (comment)',
- 'children' => [
- {
- 'value' => 'aaa',
- 'name' => 'aaa (comment)',
- },
- {
- 'value' => 'aab',
- 'name' => 'aab (comment)',
- },
- {
- 'value' => 'aac',
- 'name' => 'aac (comment)',
- },
- ]
- },
- {
- 'value' => 'bb',
- 'name' => 'bb (comment)',
- 'children' => [
- {
- 'value' => 'bba',
- 'name' => 'aaa (comment)',
- },
- {
- 'value' => 'bbb',
- 'name' => 'bbb (comment)',
- },
- {
- 'value' => 'bbc',
- 'name' => 'bbc (comment)',
- },
- ]
- },
- ],
- maxlength: 200,
- nulloption: true,
- null: false,
- multiple: false, # currently only "false" supported
- translate: true, # optional
- note: 'some additional comment', # optional
- },
- # checkbox
- data_type: 'checkbox',
- data_option: {
- default: 'aa',
- options: {
- 'aa' => 'aa (comment)',
- 'bb' => 'bb (comment)',
- },
- null: false,
- translate: true, # optional
- note: 'some additional comment', # optional
- },
- # integer
- data_type: 'integer',
- data_option: {
- default: 5,
- min: 15,
- max: 999,
- null: false,
- note: 'some additional comment', # optional
- },
- # boolean
- data_type: 'boolean',
- data_option: {
- default: true,
- options: {
- true => 'aa',
- false => 'bb',
- },
- null: false,
- translate: true, # optional
- note: 'some additional comment', # optional
- },
- # datetime
- data_type: 'datetime',
- data_option: {
- future: true, # true|false
- past: true, # true|false
- diff: 12, # in hours
- null: false,
- note: 'some additional comment', # optional
- },
- # date
- data_type: 'date',
- data_option: {
- future: true, # true|false
- past: true, # true|false
- diff: 15, # in days
- null: false,
- note: 'some additional comment', # optional
- },
- # textarea
- data_type: 'textarea',
- data_option: {
- default: '',
- rows: 15,
- null: false,
- note: 'some additional comment', # optional
- },
- # richtext
- data_type: 'richtext',
- data_option: {
- default: '',
- null: false,
- note: 'some additional comment', # optional
- },
- =end
- def self.add(data)
- force = data[:force]
- data.delete(:force)
- # lookups
- if data[:object]
- data[:object_lookup_id] = ObjectLookup.by_name(data[:object])
- end
- data.delete(:object)
- data[:name].downcase!
- # check new entry - is needed
- record = ObjectManager::Attribute.find_by(
- object_lookup_id: data[:object_lookup_id],
- name: data[:name],
- )
- if record
- # do not allow to overwrite certain attributes
- if !force
- data.delete(:editable)
- data.delete(:to_create)
- data.delete(:to_migrate)
- data.delete(:to_delete)
- data.delete(:to_config)
- end
- # if data_option has changed, store it for next migration
- if !force
- %i[name display data_type position active].each do |key|
- next if record[key] == data[key]
- record[:data_option_new] = data[:data_option] if data[:data_option] # bring the data options over as well, when there are changes to the fields above
- data[:to_config] = true
- break
- end
- if record[:data_option] != data[:data_option]
- # do we need a database migration?
- if record[:data_option][:maxlength] && data[:data_option][:maxlength] && record[:data_option][:maxlength].to_s != data[:data_option][:maxlength].to_s
- data[:to_migrate] = true
- end
- record[:data_option_new] = data[:data_option]
- data.delete(:data_option)
- data[:to_config] = true
- end
- end
- # update attributes
- data.each do |key, value|
- record[key.to_sym] = value
- end
- # check editable & name
- if !force
- record.check_editable
- record.check_name
- end
- record.save!
- return record
- end
- # add maximum position only for new records with blank position
- if !record && data[:position].blank?
- maximum_position = where(object_lookup_id: data[:object_lookup_id]).maximum(:position)
- data[:position] = maximum_position.present? ? maximum_position + 1 : 1
- end
- # do not allow to overwrite certain attributes
- if !force
- data[:editable] = true
- data[:to_create] = true
- data[:to_migrate] = true
- data[:to_delete] = false
- end
- record = ObjectManager::Attribute.new(data)
- # check editable & name
- if !force
- record.check_editable
- record.check_name
- end
- record.save!
- record
- end
- =begin
- remove attribute entry for an object
- ObjectManager::Attribute.remove(
- object: 'Ticket',
- name: 'group_id',
- )
- use "force: true" to delete also not editable fields
- =end
- def self.remove(data)
- # lookups
- if data[:object]
- data[:object_lookup_id] = ObjectLookup.by_name(data[:object])
- elsif data[:object_lookup_id]
- data[:object] = ObjectLookup.by_id(data[:object_lookup_id])
- else
- raise 'need object or object_lookup_id param!'
- end
- data[:name].downcase!
- # check newest entry - is needed
- record = ObjectManager::Attribute.find_by(
- object_lookup_id: data[:object_lookup_id],
- name: data[:name],
- )
- if !record
- raise "No such field #{data[:object]}.#{data[:name]}"
- end
- if !data[:force] && !record.editable
- raise "#{data[:object]}.#{data[:name]} can't be removed!"
- end
- # check to make sure that no triggers, overviews, or schedulers references this attribute
- if ObjectManager::Attribute.attribute_used_by_references?(data[:object], data[:name])
- text = ObjectManager::Attribute.attribute_used_by_references_humaniced(data[:object], data[:name])
- raise "#{data[:object]}.#{data[:name]} is referenced by #{text} and thus cannot be deleted!"
- end
- # if record is to create, just destroy it
- if record.to_create
- record.destroy
- return true
- end
- record.to_delete = true
- record.save
- end
- =begin
- get the attribute model based on object and name
- attribute = ObjectManager::Attribute.get(
- object: 'Ticket',
- name: 'group_id',
- )
- =end
- def self.get(data)
- # lookups
- if data[:object]
- data[:object_lookup_id] = ObjectLookup.by_name(data[:object])
- end
- data[:name].downcase!
- ObjectManager::Attribute.find_by(
- object_lookup_id: data[:object_lookup_id],
- name: data[:name],
- )
- end
- =begin
- discard migration changes
- ObjectManager::Attribute.discard_changes
- returns
- true|false
- =end
- def self.discard_changes
- ObjectManager::Attribute.where(to_create: true).each(&:destroy)
- ObjectManager::Attribute.where('to_delete = ? OR to_config = ?', true, true).each do |attribute|
- attribute.to_migrate = false
- attribute.to_delete = false
- attribute.to_config = false
- attribute.data_option_new = {}
- attribute.save
- end
- true
- end
- =begin
- check if we have pending migrations of attributes
- ObjectManager::Attribute.pending_migration?
- returns
- true|false
- =end
- def self.pending_migration?
- return false if migrations.blank?
- true
- end
- =begin
- get list of pending attributes migrations
- ObjectManager::Attribute.migrations
- returns
- [record1, record2, ...]
- =end
- def self.migrations
- ObjectManager::Attribute.where('to_create = ? OR to_migrate = ? OR to_delete = ? OR to_config = ?', true, true, true, true)
- end
- def self.attribute_historic_options(attribute)
- historical_options = attribute.data_option[:historical_options] || {}
- if attribute.data_option[:options].present?
- historical_options = historical_options.merge(data_options_hash(attribute.data_option[:options]))
- end
- if attribute.data_option_new[:options].present?
- historical_options = historical_options.merge(data_options_hash(attribute.data_option_new[:options]))
- end
- historical_options
- end
- def self.data_options_hash(options, result = {})
- return options if options.is_a?(Hash)
- return {} if !options.is_a?(Array)
- options.each do |option|
- result[ option[:value] ] = option[:name]
- if option[:children].present?
- data_options_hash(option[:children], result)
- end
- end
- result
- end
- =begin
- start migration of pending attribute migrations
- ObjectManager::Attribute.migration_execute
- returns
- [record1, record2, ...]
- to send no browser reload event, pass false
- ObjectManager::Attribute.migration_execute(false)
- =end
- def self.migration_execute(send_event = true)
- # check if field already exists
- execute_db_count = 0
- execute_config_count = 0
- migrations.each do |attribute|
- model = attribute.object_lookup.name.constantize
- # remove field
- if attribute.to_delete
- if model.column_names.include?(attribute.name)
- ActiveRecord::Migration.remove_column model.table_name, attribute.name
- reset_database_info(model)
- end
- execute_db_count += 1
- attribute.destroy
- next
- end
- # config changes
- if attribute.to_config
- execute_config_count += 1
- if attribute.data_type =~ %r{^(multi|tree_)?select$} && attribute.data_option[:options]
- attribute.data_option_new[:historical_options] = attribute_historic_options(attribute)
- end
- attribute.data_option = attribute.data_option_new
- attribute.data_option_new = {}
- attribute.to_config = false
- attribute.save!
- next if !attribute.to_create && !attribute.to_migrate && !attribute.to_delete
- end
- if %r{^(multi|tree_)?select$}.match?(attribute.data_type)
- attribute.data_option[:historical_options] = attribute_historic_options(attribute)
- end
- data_type = nil
- case attribute.data_type
- when %r{^(input|select|tree_select|richtext|textarea|checkbox)$}
- data_type = :string
- when 'autocompletion_ajax_external_data_source'
- data_type = :jsonb
- when %r{^(multiselect|multi_tree_select)$}
- data_type = if Rails.application.config.db_column_array
- :string
- else
- :json
- end
- when %r{^(integer|user_autocompletion)$}
- data_type = :integer
- when %r{^(boolean|active)$}
- data_type = :boolean
- when %r{^datetime$}
- data_type = :datetime
- when %r{^date$}
- data_type = :date
- end
- # change field
- if model.column_names.include?(attribute.name)
- case attribute.data_type
- when %r{^(input|select|tree_select|richtext|textarea|checkbox)$}
- ActiveRecord::Migration.change_column(
- model.table_name,
- attribute.name,
- data_type,
- limit: attribute.data_option[:maxlength],
- null: true
- )
- when %r{^(multiselect|multi_tree_select)$}
- options = {
- null: true,
- }
- if Rails.application.config.db_column_array
- options[:array] = true
- end
- ActiveRecord::Migration.change_column(
- model.table_name,
- attribute.name,
- data_type,
- options,
- )
- when 'autocompletion_ajax_external_data_source'
- options = {
- null: true,
- }
- ActiveRecord::Migration.change_column(
- model.table_name,
- attribute.name,
- data_type,
- options,
- )
- when %r{^(integer|user_autocompletion|datetime|date)$}, %r{^(boolean|active)$}
- ActiveRecord::Migration.change_column(
- model.table_name,
- attribute.name,
- data_type,
- default: attribute.data_option[:default],
- null: true
- )
- else
- raise "Unknown attribute.data_type '#{attribute.data_type}', can't update attribute"
- end
- # restart processes
- attribute.to_create = false
- attribute.to_migrate = false
- attribute.to_delete = false
- attribute.save!
- reset_database_info(model)
- execute_db_count += 1
- next
- end
- # create field
- case attribute.data_type
- when %r{^(input|select|tree_select|richtext|textarea|checkbox)$}
- ActiveRecord::Migration.add_column(
- model.table_name,
- attribute.name,
- data_type,
- limit: attribute.data_option[:maxlength],
- null: true
- )
- when %r{^(multiselect|multi_tree_select)$}
- options = {
- null: true,
- }
- if Rails.application.config.db_column_array
- options[:array] = true
- end
- ActiveRecord::Migration.add_column(
- model.table_name,
- attribute.name,
- data_type,
- **options,
- )
- when 'autocompletion_ajax_external_data_source'
- options = {
- null: true,
- }
- ActiveRecord::Migration.add_column(
- model.table_name,
- attribute.name,
- data_type,
- **options,
- )
- when %r{^(integer|user_autocompletion)$}, %r{^(boolean|active)$}, %r{^(datetime|date)$}
- ActiveRecord::Migration.add_column(
- model.table_name,
- attribute.name,
- data_type,
- default: attribute.data_option[:default],
- null: true
- )
- else
- raise "Unknown attribute.data_type '#{attribute.data_type}', can't create attribute"
- end
- # restart processes
- attribute.to_create = false
- attribute.to_migrate = false
- attribute.to_delete = false
- attribute.save!
- reset_database_info(model)
- execute_db_count += 1
- end
- # Clear caches so new attribute defaults can be set (#5075)
- Rails.cache.clear
- # sent maintenance message to clients
- if send_event
- if execute_db_count.nonzero?
- Zammad::Restart.perform
- elsif execute_config_count.nonzero?
- AppVersion.set(true, 'config_changed')
- end
- end
- true
- end
- =begin
- where attributes are used in conditions
- result = ObjectManager::Attribute.attribute_to_references_hash
- result = {
- ticket.category: {
- Trigger: ['abc', 'xyz'],
- Overview: ['abc1', 'abc2'],
- },
- ticket.field_b: {
- Trigger: ['abc'],
- Overview: ['abc1', 'abc2'],
- },
- },
- =end
- def self.attribute_to_references_hash
- attribute_list = {}
- attribute_to_references_hash_objects
- .map { |elem| elem.select(:name, :condition) }
- .flatten
- .each do |item|
- item.condition.each_key do |condition_key|
- attribute_list[condition_key] ||= {}
- attribute_list[condition_key][item.class.name] ||= []
- next if attribute_list[condition_key][item.class.name].include?(item.name)
- attribute_list[condition_key][item.class.name].push item.name
- end
- end
- attribute_list
- end
- =begin
- models that may reference attributes
- =end
- def self.attribute_to_references_hash_objects
- Models.all.keys.select { |elem| elem.include? ChecksConditionValidation }
- end
- =begin
- is certain attribute used by triggers, overviews or schedulers
- ObjectManager::Attribute.attribute_used_by_references?('Ticket', 'attribute_name')
- =end
- def self.attribute_used_by_references?(object_name, attribute_name, references = attribute_to_references_hash)
- references.each_key do |reference_key|
- local_object, local_attribute = reference_key.split('.')
- next if local_object != object_name.downcase
- next if local_attribute != attribute_name
- return true
- end
- false
- end
- =begin
- is certain attribute used by triggers, overviews or schedulers
- result = ObjectManager::Attribute.attribute_used_by_references('Ticket', 'attribute_name')
- result = {
- Trigger: ['abc', 'xyz'],
- Overview: ['abc1', 'abc2'],
- }
- =end
- def self.attribute_used_by_references(object_name, attribute_name, references = attribute_to_references_hash)
- result = {}
- references.each do |reference_key, relations|
- local_object, local_attribute = reference_key.split('.')
- next if local_object != object_name.downcase
- next if local_attribute != attribute_name
- relations.each do |relation, relation_names|
- result[relation] ||= []
- result[relation].push relation_names.sort
- end
- break
- end
- result
- end
- =begin
- is certain attribute used by triggers, overviews or schedulers
- text = ObjectManager::Attribute.attribute_used_by_references_humaniced('Ticket', 'attribute_name', references)
- =end
- def self.attribute_used_by_references_humaniced(object_name, attribute_name, references = nil)
- result = if references.present?
- ObjectManager::Attribute.attribute_used_by_references(object_name, attribute_name, references)
- else
- ObjectManager::Attribute.attribute_used_by_references(object_name, attribute_name)
- end
- not_deletable_reason = ''
- result.each do |relation, relation_names|
- if not_deletable_reason.present?
- not_deletable_reason += '; '
- end
- not_deletable_reason += "#{relation}: #{relation_names.sort.join(',')}"
- end
- not_deletable_reason
- end
- def self.reset_database_info(model)
- model.connection.schema_cache.clear!
- model.reset_column_information
- # rebuild columns cache to reduce the risk of
- # race conditions in re-setting it with outdated data
- model.columns
- end
- def check_name
- return if !name
- if name.match?(%r{.+?_(id|ids)$}i)
- errors.add(:name, __("can't be used because *_id and *_ids are not allowed"))
- end
- if name.match?(%r{\s})
- errors.add(:name, __('spaces are not allowed'))
- end
- if !name.match?(%r{^[a-z0-9_]+$})
- errors.add(:name, __("only lowercase letters, numbers, and '_' are allowed"))
- end
- if !name.match?(%r{[a-z]})
- errors.add(:name, __('at least one letter is required'))
- end
- # do not allow model method names as attributes
- reserved_words = %w[destroy true false integer select drop create alter index table varchar blob date datetime timestamp url icon initials avatar permission validate subscribe unsubscribe translate search _type _doc _id id action]
- if name.match?(%r{^(#{reserved_words.join('|')})$})
- errors.add(:name, __('%{name} is a reserved word'), name: name)
- end
- # fixes issue #2236 - Naming an attribute "attribute" causes ActiveRecord failure
- begin
- ObjectLookup.by_id(object_lookup_id).constantize.instance_method_already_implemented? name
- rescue ActiveRecord::DangerousAttributeError
- errors.add(:name, __('%{name} is a reserved word'), name: name)
- end
- record = object_lookup.name.constantize.new
- if new_record? && (record.respond_to?(name.to_sym) || record.attributes.key?(name))
- errors.add(:name, __('%{name} already exists'), name: name)
- end
- if errors.present?
- raise ActiveRecord::RecordInvalid, self
- end
- true
- end
- def check_editable
- return if editable
- errors.add(:name, __('attribute is not editable'))
- raise ActiveRecord::RecordInvalid, self
- end
- private
- # when setting default values for boolean fields,
- # favor #nil? tests over ||= (which will overwrite `false`)
- def set_base_options
- local_data_option[:null] = true if local_data_option[:null].nil?
- case data_type
- when %r{^((multi|tree_)?select|checkbox)$}
- local_data_option[:nulloption] = true if local_data_option[:nulloption].nil?
- local_data_option[:maxlength] ||= 255
- when 'autocompletion_ajax_external_data_source'
- local_data_option[:nulloption] = true if local_data_option[:nulloption].nil?
- end
- end
- def data_option_must_have_appropriate_values
- data_option_validations
- .select { |validation| validation[:failed] }
- .each { |validation| errors.add(local_data_attr, validation[:message]) }
- end
- def inactive_must_be_unused_by_references
- return if !ObjectManager::Attribute.attribute_used_by_references?(object_lookup.name, name)
- human_reference = ObjectManager::Attribute.attribute_used_by_references_humaniced(object_lookup.name, name)
- text = "#{object_lookup.name}.#{name} is referenced by #{human_reference} and thus cannot be set to inactive!"
- # Adding as `base` to prevent `Active` prefix which does not look good on error message shown at the top of the form.
- errors.add(:base, text)
- end
- def data_type_must_not_change
- allowable_changes = %w[tree_select multi_tree_select select multiselect input checkbox]
- return if !data_type_changed?
- return if (data_type_change - allowable_changes).empty?
- errors.add(:data_type, __("can't be altered after creation (you can delete the attribute and create another with the desired value)"))
- end
- def json_field_only_on_postgresql
- return if data_type != 'autocompletion_ajax_external_data_source'
- return if ActiveRecord::Base.connection_db_config.configuration_hash[:adapter] == 'postgresql'
- errors.add(:data_type, __('can only be created on postgresql databases'))
- end
- def local_data_option
- @local_data_option ||= send(local_data_attr)
- end
- def local_data_attr
- @local_data_attr ||= to_config ? :data_option_new : :data_option
- end
- def local_data_option=(val)
- send(:"#{local_data_attr}=", val)
- end
- def data_option_maxlength_check
- [{ failed: !local_data_option[:maxlength].to_s.match?(%r{^\d+$}), message: 'must have integer for :maxlength' }]
- end
- def data_option_type_check
- [{ failed: %w[text password tel fax email url].exclude?(local_data_option[:type]), message: 'must have one of text/password/tel/fax/email/url for :type' }]
- end
- def data_option_min_max_check
- min = local_data_option[:min]
- max = local_data_option[:max]
- [
- { failed: !VALIDATE_INTEGER_REGEXP.match?(min.to_s), message: 'must have integer for :min' },
- { failed: !VALIDATE_INTEGER_REGEXP.match?(max.to_s), message: 'must have integer for :max' },
- { failed: !(min.is_a?(Integer) && min >= VALIDATE_INTEGER_MIN), message: 'min must be higher than -2147483648' },
- { failed: !(min.is_a?(Integer) && min <= VALIDATE_INTEGER_MAX), message: 'min must be lower than 2147483648' },
- { failed: !(max.is_a?(Integer) && max >= VALIDATE_INTEGER_MIN), message: 'max must be higher than -2147483648' },
- { failed: !(max.is_a?(Integer) && max <= VALIDATE_INTEGER_MAX), message: 'max must be lower than 2147483648' },
- { failed: !(max.is_a?(Integer) && min.is_a?(Integer) && min <= max), message: 'min must be lower than max' }
- ]
- end
- def data_option_default_check
- [{ failed: !local_data_option.key?(:default), message: 'must have value for :default' }]
- end
- def data_option_relation_check
- [{ failed: local_data_option[:options].nil? && local_data_option[:relation].nil?, message: 'must have non-nil value for either :options or :relation' }]
- end
- def data_option_nil_check
- [{ failed: local_data_option[:options].nil?, message: 'must have non-nil value for :options' }]
- end
- def data_option_future_check
- [{ failed: local_data_option[:future].nil?, message: 'must have boolean value for :future' }]
- end
- def data_option_past_check
- [{ failed: local_data_option[:past].nil?, message: 'must have boolean value for :past' }]
- end
- def data_option_validations
- case data_type
- when 'input'
- data_option_type_check + data_option_maxlength_check
- when %r{^(textarea|richtext)$}
- data_option_maxlength_check
- when 'integer'
- data_option_min_max_check
- when %r{^((multi_)?tree_select|(multi)?select|checkbox)$}
- data_option_default_check + data_option_relation_check
- when 'boolean'
- data_option_default_check + data_option_nil_check
- when 'datetime'
- data_option_future_check + data_option_past_check
- else
- []
- end
- end
- def ensure_multiselect
- return if data_type != 'multiselect' && data_type != 'multi_tree_select'
- return if data_option && data_option[:multiple] == true
- data_option[:multiple] = true
- end
- end
|