attribute.rb 27 KB

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