attribute.rb 26 KB

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