attribute.rb 28 KB

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