attribute.rb 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076
  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. data[:name].downcase!
  398. ObjectManager::Attribute.find_by(
  399. object_lookup_id: data[:object_lookup_id],
  400. name: data[:name],
  401. )
  402. end
  403. =begin
  404. discard migration changes
  405. ObjectManager::Attribute.discard_changes
  406. returns
  407. true|false
  408. =end
  409. def self.discard_changes
  410. ObjectManager::Attribute.where(to_create: true).each(&:destroy)
  411. ObjectManager::Attribute.where('to_delete = ? OR to_config = ?', true, true).each do |attribute|
  412. attribute.to_migrate = false
  413. attribute.to_delete = false
  414. attribute.to_config = false
  415. attribute.data_option_new = {}
  416. attribute.save
  417. end
  418. true
  419. end
  420. =begin
  421. check if we have pending migrations of attributes
  422. ObjectManager::Attribute.pending_migration?
  423. returns
  424. true|false
  425. =end
  426. def self.pending_migration?
  427. return false if migrations.blank?
  428. true
  429. end
  430. =begin
  431. get list of pending attributes migrations
  432. ObjectManager::Attribute.migrations
  433. returns
  434. [record1, record2, ...]
  435. =end
  436. def self.migrations
  437. ObjectManager::Attribute.where('to_create = ? OR to_migrate = ? OR to_delete = ? OR to_config = ?', true, true, true, true)
  438. end
  439. def self.attribute_historic_options(attribute)
  440. historical_options = attribute.data_option[:historical_options] || {}
  441. if attribute.data_option[:options].present?
  442. historical_options = historical_options.merge(data_options_hash(attribute.data_option[:options]))
  443. end
  444. if attribute.data_option_new[:options].present?
  445. historical_options = historical_options.merge(data_options_hash(attribute.data_option_new[:options]))
  446. end
  447. historical_options
  448. end
  449. def self.data_options_hash(options, result = {})
  450. return options if options.is_a?(Hash)
  451. return {} if !options.is_a?(Array)
  452. options.each do |option|
  453. result[ option[:value] ] = option[:name]
  454. if option[:children].present?
  455. data_options_hash(option[:children], result)
  456. end
  457. end
  458. result
  459. end
  460. =begin
  461. start migration of pending attribute migrations
  462. ObjectManager::Attribute.migration_execute
  463. returns
  464. [record1, record2, ...]
  465. to send no browser reload event, pass false
  466. ObjectManager::Attribute.migration_execute(false)
  467. =end
  468. def self.migration_execute(send_event = true)
  469. # check if field already exists
  470. execute_db_count = 0
  471. execute_config_count = 0
  472. migrations.each do |attribute|
  473. model = attribute.object_lookup.name.constantize
  474. # remove field
  475. if attribute.to_delete
  476. if model.column_names.include?(attribute.name)
  477. ActiveRecord::Migration.remove_column model.table_name, attribute.name
  478. reset_database_info(model)
  479. end
  480. execute_db_count += 1
  481. attribute.destroy
  482. next
  483. end
  484. # config changes
  485. if attribute.to_config
  486. execute_config_count += 1
  487. if attribute.data_type =~ %r{^(multi|tree_)?select$} && attribute.data_option[:options]
  488. attribute.data_option_new[:historical_options] = attribute_historic_options(attribute)
  489. end
  490. attribute.data_option = attribute.data_option_new
  491. attribute.data_option_new = {}
  492. attribute.to_config = false
  493. attribute.save!
  494. next if !attribute.to_create && !attribute.to_migrate && !attribute.to_delete
  495. end
  496. if %r{^(multi|tree_)?select$}.match?(attribute.data_type)
  497. attribute.data_option[:historical_options] = attribute_historic_options(attribute)
  498. end
  499. data_type = nil
  500. case attribute.data_type
  501. when %r{^(input|select|tree_select|richtext|textarea|checkbox)$}
  502. data_type = :string
  503. when 'autocompletion_ajax_external_data_source'
  504. data_type = :jsonb
  505. when %r{^(multiselect|multi_tree_select)$}
  506. data_type = if Rails.application.config.db_column_array
  507. :string
  508. else
  509. :json
  510. end
  511. when %r{^(integer|user_autocompletion)$}
  512. data_type = :integer
  513. when %r{^(boolean|active)$}
  514. data_type = :boolean
  515. when %r{^datetime$}
  516. data_type = :datetime
  517. when %r{^date$}
  518. data_type = :date
  519. end
  520. # change field
  521. if model.column_names.include?(attribute.name)
  522. case attribute.data_type
  523. when %r{^(input|select|tree_select|richtext|textarea|checkbox)$}
  524. ActiveRecord::Migration.change_column(
  525. model.table_name,
  526. attribute.name,
  527. data_type,
  528. limit: attribute.data_option[:maxlength],
  529. null: true
  530. )
  531. when %r{^(multiselect|multi_tree_select)$}
  532. options = {
  533. null: true,
  534. }
  535. if Rails.application.config.db_column_array
  536. options[:array] = true
  537. end
  538. ActiveRecord::Migration.change_column(
  539. model.table_name,
  540. attribute.name,
  541. data_type,
  542. options,
  543. )
  544. when 'autocompletion_ajax_external_data_source'
  545. options = {
  546. null: true,
  547. }
  548. ActiveRecord::Migration.change_column(
  549. model.table_name,
  550. attribute.name,
  551. data_type,
  552. options,
  553. )
  554. when %r{^(integer|user_autocompletion|datetime|date)$}, %r{^(boolean|active)$}
  555. ActiveRecord::Migration.change_column(
  556. model.table_name,
  557. attribute.name,
  558. data_type,
  559. default: attribute.data_option[:default],
  560. null: true
  561. )
  562. else
  563. raise "Unknown attribute.data_type '#{attribute.data_type}', can't update attribute"
  564. end
  565. # restart processes
  566. attribute.to_create = false
  567. attribute.to_migrate = false
  568. attribute.to_delete = false
  569. attribute.save!
  570. reset_database_info(model)
  571. execute_db_count += 1
  572. next
  573. end
  574. # create field
  575. case attribute.data_type
  576. when %r{^(input|select|tree_select|richtext|textarea|checkbox)$}
  577. ActiveRecord::Migration.add_column(
  578. model.table_name,
  579. attribute.name,
  580. data_type,
  581. limit: attribute.data_option[:maxlength],
  582. null: true
  583. )
  584. when %r{^(multiselect|multi_tree_select)$}
  585. options = {
  586. null: true,
  587. }
  588. if Rails.application.config.db_column_array
  589. options[:array] = true
  590. end
  591. ActiveRecord::Migration.add_column(
  592. model.table_name,
  593. attribute.name,
  594. data_type,
  595. **options,
  596. )
  597. when 'autocompletion_ajax_external_data_source'
  598. options = {
  599. null: true,
  600. }
  601. ActiveRecord::Migration.add_column(
  602. model.table_name,
  603. attribute.name,
  604. data_type,
  605. **options,
  606. )
  607. when %r{^(integer|user_autocompletion)$}, %r{^(boolean|active)$}, %r{^(datetime|date)$}
  608. ActiveRecord::Migration.add_column(
  609. model.table_name,
  610. attribute.name,
  611. data_type,
  612. default: attribute.data_option[:default],
  613. null: true
  614. )
  615. else
  616. raise "Unknown attribute.data_type '#{attribute.data_type}', can't create attribute"
  617. end
  618. # restart processes
  619. attribute.to_create = false
  620. attribute.to_migrate = false
  621. attribute.to_delete = false
  622. attribute.save!
  623. reset_database_info(model)
  624. execute_db_count += 1
  625. end
  626. # Clear caches so new attribute defaults can be set (#5075)
  627. Rails.cache.clear
  628. # sent maintenance message to clients
  629. if send_event
  630. if execute_db_count.nonzero?
  631. Zammad::Restart.perform
  632. elsif execute_config_count.nonzero?
  633. AppVersion.set(true, 'config_changed')
  634. end
  635. end
  636. true
  637. end
  638. =begin
  639. where attributes are used in conditions
  640. result = ObjectManager::Attribute.attribute_to_references_hash
  641. result = {
  642. ticket.category: {
  643. Trigger: ['abc', 'xyz'],
  644. Overview: ['abc1', 'abc2'],
  645. },
  646. ticket.field_b: {
  647. Trigger: ['abc'],
  648. Overview: ['abc1', 'abc2'],
  649. },
  650. },
  651. =end
  652. def self.attribute_to_references_hash
  653. attribute_list = {}
  654. attribute_to_references_hash_objects
  655. .map { |elem| elem.select(:name, :condition) }
  656. .flatten
  657. .each do |item|
  658. item.condition.each_key do |condition_key|
  659. attribute_list[condition_key] ||= {}
  660. attribute_list[condition_key][item.class.name] ||= []
  661. next if attribute_list[condition_key][item.class.name].include?(item.name)
  662. attribute_list[condition_key][item.class.name].push item.name
  663. end
  664. end
  665. attribute_list
  666. end
  667. =begin
  668. models that may reference attributes
  669. =end
  670. def self.attribute_to_references_hash_objects
  671. Models.all.keys.select { |elem| elem.include? ChecksConditionValidation }
  672. end
  673. =begin
  674. is certain attribute used by triggers, overviews or schedulers
  675. ObjectManager::Attribute.attribute_used_by_references?('Ticket', 'attribute_name')
  676. =end
  677. def self.attribute_used_by_references?(object_name, attribute_name, references = attribute_to_references_hash)
  678. references.each_key do |reference_key|
  679. local_object, local_attribute = reference_key.split('.')
  680. next if local_object != object_name.downcase
  681. next if local_attribute != attribute_name
  682. return true
  683. end
  684. false
  685. end
  686. =begin
  687. is certain attribute used by triggers, overviews or schedulers
  688. result = ObjectManager::Attribute.attribute_used_by_references('Ticket', 'attribute_name')
  689. result = {
  690. Trigger: ['abc', 'xyz'],
  691. Overview: ['abc1', 'abc2'],
  692. }
  693. =end
  694. def self.attribute_used_by_references(object_name, attribute_name, references = attribute_to_references_hash)
  695. result = {}
  696. references.each do |reference_key, relations|
  697. local_object, local_attribute = reference_key.split('.')
  698. next if local_object != object_name.downcase
  699. next if local_attribute != attribute_name
  700. relations.each do |relation, relation_names|
  701. result[relation] ||= []
  702. result[relation].push relation_names.sort
  703. end
  704. break
  705. end
  706. result
  707. end
  708. =begin
  709. is certain attribute used by triggers, overviews or schedulers
  710. text = ObjectManager::Attribute.attribute_used_by_references_humaniced('Ticket', 'attribute_name', references)
  711. =end
  712. def self.attribute_used_by_references_humaniced(object_name, attribute_name, references = nil)
  713. result = if references.present?
  714. ObjectManager::Attribute.attribute_used_by_references(object_name, attribute_name, references)
  715. else
  716. ObjectManager::Attribute.attribute_used_by_references(object_name, attribute_name)
  717. end
  718. not_deletable_reason = ''
  719. result.each do |relation, relation_names|
  720. if not_deletable_reason.present?
  721. not_deletable_reason += '; '
  722. end
  723. not_deletable_reason += "#{relation}: #{relation_names.sort.join(',')}"
  724. end
  725. not_deletable_reason
  726. end
  727. def self.reset_database_info(model)
  728. model.connection.schema_cache.clear!
  729. model.reset_column_information
  730. # rebuild columns cache to reduce the risk of
  731. # race conditions in re-setting it with outdated data
  732. model.columns
  733. end
  734. def check_name
  735. return if !name
  736. if name.match?(%r{.+?_(id|ids)$}i)
  737. errors.add(:name, __("can't be used because *_id and *_ids are not allowed"))
  738. end
  739. if name.match?(%r{\s})
  740. errors.add(:name, __('spaces are not allowed'))
  741. end
  742. if !name.match?(%r{^[a-z0-9_]+$})
  743. errors.add(:name, __("only lowercase letters, numbers, and '_' are allowed"))
  744. end
  745. if !name.match?(%r{[a-z]})
  746. errors.add(:name, __('at least one letter is required'))
  747. end
  748. # do not allow model method names as attributes
  749. 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]
  750. if name.match?(%r{^(#{reserved_words.join('|')})$})
  751. errors.add(:name, __('%{name} is a reserved word'), name: name)
  752. end
  753. # fixes issue #2236 - Naming an attribute "attribute" causes ActiveRecord failure
  754. begin
  755. ObjectLookup.by_id(object_lookup_id).constantize.instance_method_already_implemented? name
  756. rescue ActiveRecord::DangerousAttributeError
  757. errors.add(:name, __('%{name} is a reserved word'), name: name)
  758. end
  759. record = object_lookup.name.constantize.new
  760. if new_record? && (record.respond_to?(name.to_sym) || record.attributes.key?(name))
  761. errors.add(:name, __('%{name} already exists'), name: name)
  762. end
  763. if errors.present?
  764. raise ActiveRecord::RecordInvalid, self
  765. end
  766. true
  767. end
  768. def check_editable
  769. return if editable
  770. errors.add(:name, __('attribute is not editable'))
  771. raise ActiveRecord::RecordInvalid, self
  772. end
  773. private
  774. # when setting default values for boolean fields,
  775. # favor #nil? tests over ||= (which will overwrite `false`)
  776. def set_base_options
  777. local_data_option[:null] = true if local_data_option[:null].nil?
  778. case data_type
  779. when %r{^((multi|tree_)?select|checkbox)$}
  780. local_data_option[:nulloption] = true if local_data_option[:nulloption].nil?
  781. local_data_option[:maxlength] ||= 255
  782. when 'autocompletion_ajax_external_data_source'
  783. local_data_option[:nulloption] = true if local_data_option[:nulloption].nil?
  784. end
  785. end
  786. def data_option_must_have_appropriate_values
  787. data_option_validations
  788. .select { |validation| validation[:failed] }
  789. .each { |validation| errors.add(local_data_attr, validation[:message]) }
  790. end
  791. def inactive_must_be_unused_by_references
  792. return if !ObjectManager::Attribute.attribute_used_by_references?(object_lookup.name, name)
  793. human_reference = ObjectManager::Attribute.attribute_used_by_references_humaniced(object_lookup.name, name)
  794. text = "#{object_lookup.name}.#{name} is referenced by #{human_reference} and thus cannot be set to inactive!"
  795. # Adding as `base` to prevent `Active` prefix which does not look good on error message shown at the top of the form.
  796. errors.add(:base, text)
  797. end
  798. def data_type_must_not_change
  799. allowable_changes = %w[tree_select multi_tree_select select multiselect input checkbox]
  800. return if !data_type_changed?
  801. return if (data_type_change - allowable_changes).empty?
  802. errors.add(:data_type, __("can't be altered after creation (you can delete the attribute and create another with the desired value)"))
  803. end
  804. def json_field_only_on_postgresql
  805. return if data_type != 'autocompletion_ajax_external_data_source'
  806. return if ActiveRecord::Base.connection_db_config.configuration_hash[:adapter] == 'postgresql'
  807. errors.add(:data_type, __('can only be created on postgresql databases'))
  808. end
  809. def local_data_option
  810. @local_data_option ||= send(local_data_attr)
  811. end
  812. def local_data_attr
  813. @local_data_attr ||= to_config ? :data_option_new : :data_option
  814. end
  815. def local_data_option=(val)
  816. send(:"#{local_data_attr}=", val)
  817. end
  818. def data_option_maxlength_check
  819. [{ failed: !local_data_option[:maxlength].to_s.match?(%r{^\d+$}), message: 'must have integer for :maxlength' }]
  820. end
  821. def data_option_type_check
  822. [{ 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' }]
  823. end
  824. def data_option_min_max_check
  825. min = local_data_option[:min]
  826. max = local_data_option[:max]
  827. [
  828. { failed: !VALIDATE_INTEGER_REGEXP.match?(min.to_s), message: 'must have integer for :min' },
  829. { failed: !VALIDATE_INTEGER_REGEXP.match?(max.to_s), message: 'must have integer for :max' },
  830. { failed: !(min.is_a?(Integer) && min >= VALIDATE_INTEGER_MIN), message: 'min must be higher than -2147483648' },
  831. { failed: !(min.is_a?(Integer) && min <= VALIDATE_INTEGER_MAX), message: 'min must be lower than 2147483648' },
  832. { failed: !(max.is_a?(Integer) && max >= VALIDATE_INTEGER_MIN), message: 'max must be higher than -2147483648' },
  833. { failed: !(max.is_a?(Integer) && max <= VALIDATE_INTEGER_MAX), message: 'max must be lower than 2147483648' },
  834. { failed: !(max.is_a?(Integer) && min.is_a?(Integer) && min <= max), message: 'min must be lower than max' }
  835. ]
  836. end
  837. def data_option_default_check
  838. [{ failed: !local_data_option.key?(:default), message: 'must have value for :default' }]
  839. end
  840. def data_option_relation_check
  841. [{ failed: local_data_option[:options].nil? && local_data_option[:relation].nil?, message: 'must have non-nil value for either :options or :relation' }]
  842. end
  843. def data_option_nil_check
  844. [{ failed: local_data_option[:options].nil?, message: 'must have non-nil value for :options' }]
  845. end
  846. def data_option_future_check
  847. [{ failed: local_data_option[:future].nil?, message: 'must have boolean value for :future' }]
  848. end
  849. def data_option_past_check
  850. [{ failed: local_data_option[:past].nil?, message: 'must have boolean value for :past' }]
  851. end
  852. def data_option_validations
  853. case data_type
  854. when 'input'
  855. data_option_type_check + data_option_maxlength_check
  856. when %r{^(textarea|richtext)$}
  857. data_option_maxlength_check
  858. when 'integer'
  859. data_option_min_max_check
  860. when %r{^((multi_)?tree_select|(multi)?select|checkbox)$}
  861. data_option_default_check + data_option_relation_check
  862. when 'boolean'
  863. data_option_default_check + data_option_nil_check
  864. when 'datetime'
  865. data_option_future_check + data_option_past_check
  866. else
  867. []
  868. end
  869. end
  870. def ensure_multiselect
  871. return if data_type != 'multiselect' && data_type != 'multi_tree_select'
  872. return if data_option && data_option[:multiple] == true
  873. data_option[:multiple] = true
  874. end
  875. end