attribute.rb 25 KB

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