attribute.rb 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038
  1. # Copyright (C) 2012-2023 Zammad Foundation, https://zammad-foundation.org/
  2. class ObjectManager::Attribute < ApplicationModel
  3. include HasDefaultModelUserRelations
  4. include ChecksClientNotification
  5. include CanSeed
  6. DATA_TYPES = %w[
  7. input
  8. user_autocompletion
  9. checkbox
  10. select
  11. multiselect
  12. tree_select
  13. multi_tree_select
  14. datetime
  15. date
  16. tag
  17. richtext
  18. textarea
  19. integer
  20. autocompletion_ajax
  21. autocompletion_ajax_customer_organization
  22. boolean
  23. user_permission
  24. active
  25. ].freeze
  26. VALIDATE_INTEGER_MIN = -2_147_483_647
  27. VALIDATE_INTEGER_MAX = 2_147_483_647
  28. VALIDATE_INTEGER_REGEXP = %r{^-?\d+$}
  29. self.table_name = 'object_manager_attributes'
  30. belongs_to :object_lookup, optional: true
  31. validates :name, presence: true
  32. validates :data_type, inclusion: { in: DATA_TYPES, msg: '%{value} is not a valid data type' }
  33. validate :inactive_must_be_unused_by_references, unless: :active?
  34. validate :data_option_must_have_appropriate_values
  35. validate :data_type_must_not_change, on: :update
  36. store :screens
  37. store :data_option
  38. store :data_option_new
  39. before_validation :set_base_options
  40. before_create :ensure_multiselect
  41. before_update :ensure_multiselect
  42. scope :active, -> { where(active: true) }
  43. scope :editable, -> { where(editable: true) }
  44. scope :for_object, lambda { |name_or_klass|
  45. id = ObjectLookup.lookup(name: name_or_klass.to_s)
  46. where(object_lookup_id: id)
  47. }
  48. =begin
  49. list of all attributes
  50. result = ObjectManager::Attribute.list_full
  51. result = [
  52. {
  53. name: 'some name',
  54. display: '...',
  55. }.
  56. ],
  57. =end
  58. def self.list_full
  59. result = ObjectManager::Attribute.all.reorder('position ASC, name ASC')
  60. references = ObjectManager::Attribute.attribute_to_references_hash
  61. attributes = []
  62. result.each do |item|
  63. attribute = item.attributes
  64. attribute[:object] = ObjectLookup.by_id(item.object_lookup_id)
  65. attribute.delete('object_lookup_id')
  66. # an attribute is deletable if it is both editable and not referenced by other Objects (Triggers, Overviews, Schedulers)
  67. deletable = true
  68. not_deletable_reason = ''
  69. if ObjectManager::Attribute.attribute_used_by_references?(attribute[:object], attribute['name'], references)
  70. deletable = false
  71. not_deletable_reason = ObjectManager::Attribute.attribute_used_by_references_humaniced(attribute[:object], attribute['name'], references)
  72. end
  73. attribute[:deletable] = attribute['editable'] && deletable == true
  74. if not_deletable_reason.present?
  75. attribute[:not_deletable_reason] = "This attribute is referenced by #{not_deletable_reason} and thus cannot be deleted!"
  76. end
  77. attributes.push attribute
  78. end
  79. attributes
  80. end
  81. =begin
  82. add a new attribute entry for an object
  83. ObjectManager::Attribute.add(
  84. object: 'Ticket',
  85. name: 'group_id',
  86. display: __('Group'),
  87. data_type: 'select',
  88. data_option: {
  89. relation: 'Group',
  90. relation_condition: { access: 'full' },
  91. multiple: false,
  92. null: true,
  93. translate: false,
  94. belongs_to: 'group',
  95. },
  96. active: true,
  97. screens: {
  98. create: {
  99. '-all-' => {
  100. required: true,
  101. },
  102. },
  103. edit: {
  104. 'ticket.agent' => {
  105. required: true,
  106. },
  107. },
  108. },
  109. position: 20,
  110. created_by_id: 1,
  111. updated_by_id: 1,
  112. created_at: '2014-06-04 10:00:00',
  113. updated_at: '2014-06-04 10:00:00',
  114. force: true
  115. editable: false,
  116. to_migrate: false,
  117. to_create: false,
  118. to_delete: false,
  119. to_config: false,
  120. )
  121. preserved name are
  122. /(_id|_ids)$/
  123. possible types
  124. # input
  125. data_type: 'input',
  126. data_option: {
  127. default: '',
  128. type: 'text', # text|email|url|tel
  129. maxlength: 200,
  130. null: true,
  131. note: 'some additional comment', # optional
  132. link_template: '', # optional
  133. },
  134. # select
  135. data_type: 'select',
  136. data_option: {
  137. default: 'aa',
  138. options: {
  139. 'aa' => 'aa (comment)',
  140. 'bb' => 'bb (comment)',
  141. },
  142. maxlength: 200,
  143. nulloption: true,
  144. null: false,
  145. multiple: false, # currently only "false" supported
  146. translate: true, # optional
  147. note: 'some additional comment', # optional
  148. link_template: '', # optional
  149. },
  150. # tree_select
  151. data_type: 'tree_select',
  152. data_option: {
  153. default: 'aa',
  154. options: [
  155. {
  156. 'value' => 'aa',
  157. 'name' => 'aa (comment)',
  158. 'children' => [
  159. {
  160. 'value' => 'aaa',
  161. 'name' => 'aaa (comment)',
  162. },
  163. {
  164. 'value' => 'aab',
  165. 'name' => 'aab (comment)',
  166. },
  167. {
  168. 'value' => 'aac',
  169. 'name' => 'aac (comment)',
  170. },
  171. ]
  172. },
  173. {
  174. 'value' => 'bb',
  175. 'name' => 'bb (comment)',
  176. 'children' => [
  177. {
  178. 'value' => 'bba',
  179. 'name' => 'aaa (comment)',
  180. },
  181. {
  182. 'value' => 'bbb',
  183. 'name' => 'bbb (comment)',
  184. },
  185. {
  186. 'value' => 'bbc',
  187. 'name' => 'bbc (comment)',
  188. },
  189. ]
  190. },
  191. ],
  192. maxlength: 200,
  193. nulloption: true,
  194. null: false,
  195. multiple: false, # currently only "false" supported
  196. translate: true, # optional
  197. note: 'some additional comment', # optional
  198. },
  199. # checkbox
  200. data_type: 'checkbox',
  201. data_option: {
  202. default: 'aa',
  203. options: {
  204. 'aa' => 'aa (comment)',
  205. 'bb' => 'bb (comment)',
  206. },
  207. null: false,
  208. translate: true, # optional
  209. note: 'some additional comment', # optional
  210. },
  211. # integer
  212. data_type: 'integer',
  213. data_option: {
  214. default: 5,
  215. min: 15,
  216. max: 999,
  217. null: false,
  218. note: 'some additional comment', # optional
  219. },
  220. # boolean
  221. data_type: 'boolean',
  222. data_option: {
  223. default: true,
  224. options: {
  225. true => 'aa',
  226. false => 'bb',
  227. },
  228. null: false,
  229. translate: true, # optional
  230. note: 'some additional comment', # optional
  231. },
  232. # datetime
  233. data_type: 'datetime',
  234. data_option: {
  235. future: true, # true|false
  236. past: true, # true|false
  237. diff: 12, # in hours
  238. null: false,
  239. note: 'some additional comment', # optional
  240. },
  241. # date
  242. data_type: 'date',
  243. data_option: {
  244. future: true, # true|false
  245. past: true, # true|false
  246. diff: 15, # in days
  247. null: false,
  248. note: 'some additional comment', # optional
  249. },
  250. # textarea
  251. data_type: 'textarea',
  252. data_option: {
  253. default: '',
  254. rows: 15,
  255. null: false,
  256. note: 'some additional comment', # optional
  257. },
  258. # richtext
  259. data_type: 'richtext',
  260. data_option: {
  261. default: '',
  262. null: false,
  263. note: 'some additional comment', # optional
  264. },
  265. =end
  266. def self.add(data)
  267. force = data[:force]
  268. data.delete(:force)
  269. # lookups
  270. if data[:object]
  271. data[:object_lookup_id] = ObjectLookup.by_name(data[:object])
  272. end
  273. data.delete(:object)
  274. data[:name].downcase!
  275. # check new entry - is needed
  276. record = ObjectManager::Attribute.find_by(
  277. object_lookup_id: data[:object_lookup_id],
  278. name: data[:name],
  279. )
  280. if record
  281. # do not allow to overwrite certain attributes
  282. if !force
  283. data.delete(:editable)
  284. data.delete(:to_create)
  285. data.delete(:to_migrate)
  286. data.delete(:to_delete)
  287. data.delete(:to_config)
  288. end
  289. # if data_option has changed, store it for next migration
  290. if !force
  291. %i[name display data_type position active].each do |key|
  292. next if record[key] == data[key]
  293. 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
  294. data[:to_config] = true
  295. break
  296. end
  297. if record[:data_option] != data[:data_option]
  298. # do we need a database migration?
  299. if record[:data_option][:maxlength] && data[:data_option][:maxlength] && record[:data_option][:maxlength].to_s != data[:data_option][:maxlength].to_s
  300. data[:to_migrate] = true
  301. end
  302. record[:data_option_new] = data[:data_option]
  303. data.delete(:data_option)
  304. data[:to_config] = true
  305. end
  306. end
  307. # update attributes
  308. data.each do |key, value|
  309. record[key.to_sym] = value
  310. end
  311. # check editable & name
  312. if !force
  313. record.check_editable
  314. record.check_name
  315. end
  316. record.save!
  317. return record
  318. end
  319. # add maximum position only for new records with blank position
  320. if !record && data[:position].blank?
  321. maximum_position = where(object_lookup_id: data[:object_lookup_id]).maximum(:position)
  322. data[:position] = maximum_position.present? ? maximum_position + 1 : 1
  323. end
  324. # do not allow to overwrite certain attributes
  325. if !force
  326. data[:editable] = true
  327. data[:to_create] = true
  328. data[:to_migrate] = true
  329. data[:to_delete] = false
  330. end
  331. record = ObjectManager::Attribute.new(data)
  332. # check editable & name
  333. if !force
  334. record.check_editable
  335. record.check_name
  336. end
  337. record.save!
  338. record
  339. end
  340. =begin
  341. remove attribute entry for an object
  342. ObjectManager::Attribute.remove(
  343. object: 'Ticket',
  344. name: 'group_id',
  345. )
  346. use "force: true" to delete also not editable fields
  347. =end
  348. def self.remove(data)
  349. # lookups
  350. if data[:object]
  351. data[:object_lookup_id] = ObjectLookup.by_name(data[:object])
  352. elsif data[:object_lookup_id]
  353. data[:object] = ObjectLookup.by_id(data[:object_lookup_id])
  354. else
  355. raise 'need object or object_lookup_id param!'
  356. end
  357. data[:name].downcase!
  358. # check newest entry - is needed
  359. record = ObjectManager::Attribute.find_by(
  360. object_lookup_id: data[:object_lookup_id],
  361. name: data[:name],
  362. )
  363. if !record
  364. raise "No such field #{data[:object]}.#{data[:name]}"
  365. end
  366. if !data[:force] && !record.editable
  367. raise "#{data[:object]}.#{data[:name]} can't be removed!"
  368. end
  369. # check to make sure that no triggers, overviews, or schedulers references this attribute
  370. if ObjectManager::Attribute.attribute_used_by_references?(data[:object], data[:name])
  371. text = ObjectManager::Attribute.attribute_used_by_references_humaniced(data[:object], data[:name])
  372. raise "#{data[:object]}.#{data[:name]} is referenced by #{text} and thus cannot be deleted!"
  373. end
  374. # if record is to create, just destroy it
  375. if record.to_create
  376. record.destroy
  377. return true
  378. end
  379. record.to_delete = true
  380. record.save
  381. end
  382. =begin
  383. get the attribute model based on object and name
  384. attribute = ObjectManager::Attribute.get(
  385. object: 'Ticket',
  386. name: 'group_id',
  387. )
  388. =end
  389. def self.get(data)
  390. # lookups
  391. if data[:object]
  392. data[:object_lookup_id] = ObjectLookup.by_name(data[:object])
  393. end
  394. data[:name].downcase!
  395. ObjectManager::Attribute.find_by(
  396. object_lookup_id: data[:object_lookup_id],
  397. name: data[:name],
  398. )
  399. end
  400. =begin
  401. discard migration changes
  402. ObjectManager::Attribute.discard_changes
  403. returns
  404. true|false
  405. =end
  406. def self.discard_changes
  407. ObjectManager::Attribute.where(to_create: true).each(&:destroy)
  408. ObjectManager::Attribute.where('to_delete = ? OR to_config = ?', true, true).each do |attribute|
  409. attribute.to_migrate = false
  410. attribute.to_delete = false
  411. attribute.to_config = false
  412. attribute.data_option_new = {}
  413. attribute.save
  414. end
  415. true
  416. end
  417. =begin
  418. check if we have pending migrations of attributes
  419. ObjectManager::Attribute.pending_migration?
  420. returns
  421. true|false
  422. =end
  423. def self.pending_migration?
  424. return false if migrations.blank?
  425. true
  426. end
  427. =begin
  428. get list of pending attributes migrations
  429. ObjectManager::Attribute.migrations
  430. returns
  431. [record1, record2, ...]
  432. =end
  433. def self.migrations
  434. ObjectManager::Attribute.where('to_create = ? OR to_migrate = ? OR to_delete = ? OR to_config = ?', true, true, true, true)
  435. end
  436. def self.attribute_historic_options(attribute)
  437. historical_options = attribute.data_option[:historical_options] || {}
  438. if attribute.data_option[:options].present?
  439. historical_options = historical_options.merge(data_options_hash(attribute.data_option[:options]))
  440. end
  441. if attribute.data_option_new[:options].present?
  442. historical_options = historical_options.merge(data_options_hash(attribute.data_option_new[:options]))
  443. end
  444. historical_options
  445. end
  446. def self.data_options_hash(options, result = {})
  447. return options if options.is_a?(Hash)
  448. return {} if !options.is_a?(Array)
  449. options.each do |option|
  450. result[ option[:value] ] = option[:name]
  451. if option[:children].present?
  452. data_options_hash(option[:children], result)
  453. end
  454. end
  455. result
  456. end
  457. =begin
  458. start migration of pending attribute migrations
  459. ObjectManager::Attribute.migration_execute
  460. returns
  461. [record1, record2, ...]
  462. to send no browser reload event, pass false
  463. ObjectManager::Attribute.migration_execute(false)
  464. =end
  465. def self.migration_execute(send_event = true)
  466. # check if field already exists
  467. execute_db_count = 0
  468. execute_config_count = 0
  469. migrations.each do |attribute|
  470. model = attribute.object_lookup.name.constantize
  471. # remove field
  472. if attribute.to_delete
  473. if model.column_names.include?(attribute.name)
  474. ActiveRecord::Migration.remove_column model.table_name, attribute.name
  475. reset_database_info(model)
  476. end
  477. execute_db_count += 1
  478. attribute.destroy
  479. next
  480. end
  481. # config changes
  482. if attribute.to_config
  483. execute_config_count += 1
  484. if attribute.data_type =~ %r{^(multi|tree_)?select$} && attribute.data_option[:options]
  485. attribute.data_option_new[:historical_options] = attribute_historic_options(attribute)
  486. end
  487. attribute.data_option = attribute.data_option_new
  488. attribute.data_option_new = {}
  489. attribute.to_config = false
  490. attribute.save!
  491. next if !attribute.to_create && !attribute.to_migrate && !attribute.to_delete
  492. end
  493. if %r{^(multi|tree_)?select$}.match?(attribute.data_type)
  494. attribute.data_option[:historical_options] = attribute_historic_options(attribute)
  495. end
  496. data_type = nil
  497. case attribute.data_type
  498. when %r{^(input|select|tree_select|richtext|textarea|checkbox)$}
  499. data_type = :string
  500. when %r{^(multiselect|multi_tree_select)$}
  501. data_type = if Rails.application.config.db_column_array
  502. :string
  503. else
  504. :json
  505. end
  506. when %r{^(integer|user_autocompletion)$}
  507. data_type = :integer
  508. when %r{^(boolean|active)$}
  509. data_type = :boolean
  510. when %r{^datetime$}
  511. data_type = :datetime
  512. when %r{^date$}
  513. data_type = :date
  514. end
  515. # change field
  516. if model.column_names.include?(attribute.name)
  517. case attribute.data_type
  518. when %r{^(input|select|tree_select|richtext|textarea|checkbox)$}
  519. ActiveRecord::Migration.change_column(
  520. model.table_name,
  521. attribute.name,
  522. data_type,
  523. limit: attribute.data_option[:maxlength],
  524. null: true
  525. )
  526. when %r{^(multiselect|multi_tree_select)$}
  527. options = {
  528. null: true,
  529. }
  530. if Rails.application.config.db_column_array
  531. options[:array] = true
  532. end
  533. ActiveRecord::Migration.change_column(
  534. model.table_name,
  535. attribute.name,
  536. data_type,
  537. options,
  538. )
  539. when %r{^(integer|user_autocompletion|datetime|date)$}, %r{^(boolean|active)$}
  540. ActiveRecord::Migration.change_column(
  541. model.table_name,
  542. attribute.name,
  543. data_type,
  544. default: attribute.data_option[:default],
  545. null: true
  546. )
  547. else
  548. raise "Unknown attribute.data_type '#{attribute.data_type}', can't update attribute"
  549. end
  550. # restart processes
  551. attribute.to_create = false
  552. attribute.to_migrate = false
  553. attribute.to_delete = false
  554. attribute.save!
  555. reset_database_info(model)
  556. execute_db_count += 1
  557. next
  558. end
  559. # create field
  560. case attribute.data_type
  561. when %r{^(input|select|tree_select|richtext|textarea|checkbox)$}
  562. ActiveRecord::Migration.add_column(
  563. model.table_name,
  564. attribute.name,
  565. data_type,
  566. limit: attribute.data_option[:maxlength],
  567. null: true
  568. )
  569. when %r{^(multiselect|multi_tree_select)$}
  570. options = {
  571. null: true,
  572. }
  573. if Rails.application.config.db_column_array
  574. options[:array] = true
  575. end
  576. ActiveRecord::Migration.add_column(
  577. model.table_name,
  578. attribute.name,
  579. data_type,
  580. **options,
  581. )
  582. when %r{^(integer|user_autocompletion)$}, %r{^(boolean|active)$}, %r{^(datetime|date)$}
  583. ActiveRecord::Migration.add_column(
  584. model.table_name,
  585. attribute.name,
  586. data_type,
  587. default: attribute.data_option[:default],
  588. null: true
  589. )
  590. else
  591. raise "Unknown attribute.data_type '#{attribute.data_type}', can't create attribute"
  592. end
  593. # restart processes
  594. attribute.to_create = false
  595. attribute.to_migrate = false
  596. attribute.to_delete = false
  597. attribute.save!
  598. reset_database_info(model)
  599. execute_db_count += 1
  600. end
  601. # sent maintenance message to clients
  602. if send_event
  603. if execute_db_count.nonzero?
  604. Zammad::Restart.perform
  605. elsif execute_config_count.nonzero?
  606. AppVersion.set(true, 'config_changed')
  607. end
  608. end
  609. true
  610. end
  611. =begin
  612. where attributes are used in conditions
  613. result = ObjectManager::Attribute.attribute_to_references_hash
  614. result = {
  615. ticket.category: {
  616. Trigger: ['abc', 'xyz'],
  617. Overview: ['abc1', 'abc2'],
  618. },
  619. ticket.field_b: {
  620. Trigger: ['abc'],
  621. Overview: ['abc1', 'abc2'],
  622. },
  623. },
  624. =end
  625. def self.attribute_to_references_hash
  626. attribute_list = {}
  627. attribute_to_references_hash_objects
  628. .map { |elem| elem.select(:name, :condition) }
  629. .flatten
  630. .each do |item|
  631. item.condition.each do |condition_key, _condition_attributes|
  632. attribute_list[condition_key] ||= {}
  633. attribute_list[condition_key][item.class.name] ||= []
  634. next if attribute_list[condition_key][item.class.name].include?(item.name)
  635. attribute_list[condition_key][item.class.name].push item.name
  636. end
  637. end
  638. attribute_list
  639. end
  640. =begin
  641. models that may reference attributes
  642. =end
  643. def self.attribute_to_references_hash_objects
  644. Models.all.keys.select { |elem| elem.include? ChecksConditionValidation }
  645. end
  646. =begin
  647. is certain attribute used by triggers, overviews or schedulers
  648. ObjectManager::Attribute.attribute_used_by_references?('Ticket', 'attribute_name')
  649. =end
  650. def self.attribute_used_by_references?(object_name, attribute_name, references = attribute_to_references_hash)
  651. references.each do |reference_key, _relations|
  652. local_object, local_attribute = reference_key.split('.')
  653. next if local_object != object_name.downcase
  654. next if local_attribute != attribute_name
  655. return true
  656. end
  657. false
  658. end
  659. =begin
  660. is certain attribute used by triggers, overviews or schedulers
  661. result = ObjectManager::Attribute.attribute_used_by_references('Ticket', 'attribute_name')
  662. result = {
  663. Trigger: ['abc', 'xyz'],
  664. Overview: ['abc1', 'abc2'],
  665. }
  666. =end
  667. def self.attribute_used_by_references(object_name, attribute_name, references = attribute_to_references_hash)
  668. result = {}
  669. references.each do |reference_key, relations|
  670. local_object, local_attribute = reference_key.split('.')
  671. next if local_object != object_name.downcase
  672. next if local_attribute != attribute_name
  673. relations.each do |relation, relation_names|
  674. result[relation] ||= []
  675. result[relation].push relation_names.sort
  676. end
  677. break
  678. end
  679. result
  680. end
  681. =begin
  682. is certain attribute used by triggers, overviews or schedulers
  683. text = ObjectManager::Attribute.attribute_used_by_references_humaniced('Ticket', 'attribute_name', references)
  684. =end
  685. def self.attribute_used_by_references_humaniced(object_name, attribute_name, references = nil)
  686. result = if references.present?
  687. ObjectManager::Attribute.attribute_used_by_references(object_name, attribute_name, references)
  688. else
  689. ObjectManager::Attribute.attribute_used_by_references(object_name, attribute_name)
  690. end
  691. not_deletable_reason = ''
  692. result.each do |relation, relation_names|
  693. if not_deletable_reason.present?
  694. not_deletable_reason += '; '
  695. end
  696. not_deletable_reason += "#{relation}: #{relation_names.sort.join(',')}"
  697. end
  698. not_deletable_reason
  699. end
  700. def self.reset_database_info(model)
  701. model.connection.schema_cache.clear!
  702. model.reset_column_information
  703. # rebuild columns cache to reduce the risk of
  704. # race conditions in re-setting it with outdated data
  705. model.columns
  706. end
  707. def check_name
  708. return if !name
  709. if name.match?(%r{.+?_(id|ids)$}i)
  710. errors.add(:name, __("can't be used because *_id and *_ids are not allowed"))
  711. end
  712. if name.match?(%r{\s})
  713. errors.add(:name, __('spaces are not allowed'))
  714. end
  715. if !name.match?(%r{^[a-z0-9_]+$})
  716. errors.add(:name, __("only lowercase letters, numbers, and '_' are allowed"))
  717. end
  718. if !name.match?(%r{[a-z]})
  719. errors.add(:name, __('at least one letter is required'))
  720. end
  721. # do not allow model method names as attributes
  722. 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]
  723. if name.match?(%r{^(#{reserved_words.join('|')})$})
  724. errors.add(:name, __('%{name} is a reserved word'), name: name)
  725. end
  726. # fixes issue #2236 - Naming an attribute "attribute" causes ActiveRecord failure
  727. begin
  728. ObjectLookup.by_id(object_lookup_id).constantize.instance_method_already_implemented? name
  729. rescue ActiveRecord::DangerousAttributeError
  730. errors.add(:name, __('%{name} is a reserved word'), name: name)
  731. end
  732. record = object_lookup.name.constantize.new
  733. if new_record? && (record.respond_to?(name.to_sym) || record.attributes.key?(name))
  734. errors.add(:name, __('%{name} already exists'), name: name)
  735. end
  736. if errors.present?
  737. raise ActiveRecord::RecordInvalid, self
  738. end
  739. true
  740. end
  741. def check_editable
  742. return if editable
  743. errors.add(:name, __('attribute is not editable'))
  744. raise ActiveRecord::RecordInvalid, self
  745. end
  746. private
  747. # when setting default values for boolean fields,
  748. # favor #nil? tests over ||= (which will overwrite `false`)
  749. def set_base_options
  750. local_data_option[:null] = true if local_data_option[:null].nil?
  751. case data_type
  752. when %r{^((multi|tree_)?select|checkbox)$}
  753. local_data_option[:nulloption] = true if local_data_option[:nulloption].nil?
  754. local_data_option[:maxlength] ||= 255
  755. end
  756. end
  757. def data_option_must_have_appropriate_values
  758. data_option_validations
  759. .select { |validation| validation[:failed] }
  760. .each { |validation| errors.add(local_data_attr, validation[:message]) }
  761. end
  762. def inactive_must_be_unused_by_references
  763. return if !ObjectManager::Attribute.attribute_used_by_references?(object_lookup.name, name)
  764. human_reference = ObjectManager::Attribute.attribute_used_by_references_humaniced(object_lookup.name, name)
  765. text = "#{object_lookup.name}.#{name} is referenced by #{human_reference} and thus cannot be set to inactive!"
  766. # Adding as `base` to prevent `Active` prefix which does not look good on error message shown at the top of the form.
  767. errors.add(:base, text)
  768. end
  769. def data_type_must_not_change
  770. allowable_changes = %w[tree_select multi_tree_select select multiselect input checkbox]
  771. return if !data_type_changed?
  772. return if (data_type_change - allowable_changes).empty?
  773. errors.add(:data_type, __("can't be altered after creation (you can delete the attribute and create another with the desired value)"))
  774. end
  775. def local_data_option
  776. @local_data_option ||= send(local_data_attr)
  777. end
  778. def local_data_attr
  779. @local_data_attr ||= to_config ? :data_option_new : :data_option
  780. end
  781. def local_data_option=(val)
  782. send("#{local_data_attr}=", val)
  783. end
  784. def data_option_maxlength_check
  785. [{ failed: !local_data_option[:maxlength].to_s.match?(%r{^\d+$}), message: 'must have integer for :maxlength' }]
  786. end
  787. def data_option_type_check
  788. [{ 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' }]
  789. end
  790. def data_option_min_max_check
  791. min = local_data_option[:min]
  792. max = local_data_option[:max]
  793. [
  794. { failed: !VALIDATE_INTEGER_REGEXP.match?(min.to_s), message: 'must have integer for :min' },
  795. { failed: !VALIDATE_INTEGER_REGEXP.match?(max.to_s), message: 'must have integer for :max' },
  796. { failed: !(min.is_a?(Integer) && min >= VALIDATE_INTEGER_MIN), message: 'min must be higher than -2147483648' },
  797. { failed: !(min.is_a?(Integer) && min <= VALIDATE_INTEGER_MAX), message: 'min must be lower than 2147483648' },
  798. { failed: !(max.is_a?(Integer) && max >= VALIDATE_INTEGER_MIN), message: 'max must be higher than -2147483648' },
  799. { failed: !(max.is_a?(Integer) && max <= VALIDATE_INTEGER_MAX), message: 'max must be lower than 2147483648' },
  800. { failed: !(max.is_a?(Integer) && min.is_a?(Integer) && min <= max), message: 'min must be lower than max' }
  801. ]
  802. end
  803. def data_option_default_check
  804. [{ failed: !local_data_option.key?(:default), message: 'must have value for :default' }]
  805. end
  806. def data_option_relation_check
  807. [{ failed: local_data_option[:options].nil? && local_data_option[:relation].nil?, message: 'must have non-nil value for either :options or :relation' }]
  808. end
  809. def data_option_nil_check
  810. [{ failed: local_data_option[:options].nil?, message: 'must have non-nil value for :options' }]
  811. end
  812. def data_option_future_check
  813. [{ failed: local_data_option[:future].nil?, message: 'must have boolean value for :future' }]
  814. end
  815. def data_option_past_check
  816. [{ failed: local_data_option[:past].nil?, message: 'must have boolean value for :past' }]
  817. end
  818. def data_option_validations
  819. case data_type
  820. when 'input'
  821. data_option_type_check + data_option_maxlength_check
  822. when %r{^(textarea|richtext)$}
  823. data_option_maxlength_check
  824. when 'integer'
  825. data_option_min_max_check
  826. when %r{^((multi_)?tree_select|(multi)?select|checkbox)$}
  827. data_option_default_check + data_option_relation_check
  828. when 'boolean'
  829. data_option_default_check + data_option_nil_check
  830. when 'datetime'
  831. data_option_future_check + data_option_past_check
  832. else
  833. []
  834. end
  835. end
  836. def ensure_multiselect
  837. return if data_type != 'multiselect' && data_type != 'multi_tree_select'
  838. return if data_option && data_option[:multiple] == true
  839. data_option[:multiple] = true
  840. end
  841. end