attribute.rb 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  1. class ObjectManager::Attribute < ApplicationModel
  2. include ChecksClientNotification
  3. include CanSeed
  4. self.table_name = 'object_manager_attributes'
  5. belongs_to :object_lookup
  6. validates :name, presence: true
  7. store :screens
  8. store :data_option
  9. store :data_option_new
  10. =begin
  11. list of all attributes
  12. result = ObjectManager::Attribute.list_full
  13. result = [
  14. {
  15. name: 'some name',
  16. display: '...',
  17. }.
  18. ],
  19. =end
  20. def self.list_full
  21. result = ObjectManager::Attribute.all.order('position ASC, name ASC')
  22. attributes = []
  23. assets = {}
  24. result.each do |item|
  25. attribute = item.attributes
  26. attribute[:object] = ObjectLookup.by_id(item.object_lookup_id)
  27. attribute.delete('object_lookup_id')
  28. attributes.push attribute
  29. end
  30. attributes
  31. end
  32. =begin
  33. add a new attribute entry for an object
  34. ObjectManager::Attribute.add(
  35. object: 'Ticket',
  36. name: 'group_id',
  37. display: 'Group',
  38. data_type: 'select',
  39. data_option: {
  40. relation: 'Group',
  41. relation_condition: { access: 'full' },
  42. multiple: false,
  43. null: true,
  44. translate: false,
  45. },
  46. active: true,
  47. screens: {
  48. create: {
  49. '-all-' => {
  50. required: true,
  51. },
  52. },
  53. edit: {
  54. 'ticket.agent' => {
  55. required: true,
  56. },
  57. },
  58. },
  59. position: 20,
  60. created_by_id: 1,
  61. updated_by_id: 1,
  62. created_at: '2014-06-04 10:00:00',
  63. updated_at: '2014-06-04 10:00:00',
  64. force: true
  65. editable: false,
  66. to_migrate: false,
  67. to_create: false,
  68. to_delete: false,
  69. to_config: false,
  70. )
  71. preserved name are
  72. /(_id|_ids)$/
  73. possible types
  74. # input
  75. data_type: 'input',
  76. data_option: {
  77. default: '',
  78. type: 'text', # text|email|url|tel
  79. maxlength: 200,
  80. null: true,
  81. note: 'some additional comment', # optional
  82. },
  83. # select
  84. data_type: 'select',
  85. data_option: {
  86. default: 'aa',
  87. options: {
  88. 'aa' => 'aa (comment)',
  89. 'bb' => 'bb (comment)',
  90. },
  91. maxlength: 200,
  92. nulloption: true,
  93. null: false,
  94. multiple: false, # currently only "false" supported
  95. translate: true, # optional
  96. note: 'some additional comment', # optional
  97. },
  98. # tree_select
  99. data_type: 'tree_select',
  100. data_option: {
  101. default: 'aa',
  102. options: [
  103. {
  104. 'value' => 'aa',
  105. 'name' => 'aa (comment)',
  106. 'children' => [
  107. {
  108. 'value' => 'aaa',
  109. 'name' => 'aaa (comment)',
  110. },
  111. {
  112. 'value' => 'aab',
  113. 'name' => 'aab (comment)',
  114. },
  115. {
  116. 'value' => 'aac',
  117. 'name' => 'aac (comment)',
  118. },
  119. ]
  120. },
  121. {
  122. 'value' => 'bb',
  123. 'name' => 'bb (comment)',
  124. 'children' => [
  125. {
  126. 'value' => 'bba',
  127. 'name' => 'aaa (comment)',
  128. },
  129. {
  130. 'value' => 'bbb',
  131. 'name' => 'bbb (comment)',
  132. },
  133. {
  134. 'value' => 'bbc',
  135. 'name' => 'bbc (comment)',
  136. },
  137. ]
  138. },
  139. ],
  140. maxlength: 200,
  141. nulloption: true,
  142. null: false,
  143. multiple: false, # currently only "false" supported
  144. translate: true, # optional
  145. note: 'some additional comment', # optional
  146. },
  147. # checkbox
  148. data_type: 'checkbox',
  149. data_option: {
  150. default: 'aa',
  151. options: {
  152. 'aa' => 'aa (comment)',
  153. 'bb' => 'bb (comment)',
  154. },
  155. null: false,
  156. translate: true, # optional
  157. note: 'some additional comment', # optional
  158. },
  159. # integer
  160. data_type: 'integer',
  161. data_option: {
  162. default: 5,
  163. min: 15,
  164. max: 999,
  165. null: false,
  166. note: 'some additional comment', # optional
  167. },
  168. # boolean
  169. data_type: 'boolean',
  170. data_option: {
  171. default: true,
  172. options: {
  173. true => 'aa',
  174. false => 'bb',
  175. },
  176. null: false,
  177. translate: true, # optional
  178. note: 'some additional comment', # optional
  179. },
  180. # datetime
  181. data_type: 'datetime',
  182. data_option: {
  183. future: true, # true|false
  184. past: true, # true|false
  185. diff: 12, # in hours
  186. null: false,
  187. note: 'some additional comment', # optional
  188. },
  189. # date
  190. data_type: 'date',
  191. data_option: {
  192. future: true, # true|false
  193. past: true, # true|false
  194. diff: 15, # in days
  195. null: false,
  196. note: 'some additional comment', # optional
  197. },
  198. # textarea
  199. data_type: 'textarea',
  200. data_option: {
  201. default: '',
  202. rows: 15,
  203. null: false,
  204. note: 'some additional comment', # optional
  205. },
  206. # richtext
  207. data_type: 'richtext',
  208. data_option: {
  209. default: '',
  210. null: false,
  211. note: 'some additional comment', # optional
  212. },
  213. =end
  214. def self.add(data)
  215. force = data[:force]
  216. data.delete(:force)
  217. # lookups
  218. if data[:object]
  219. data[:object_lookup_id] = ObjectLookup.by_name(data[:object])
  220. end
  221. data.delete(:object)
  222. data[:name].downcase!
  223. # check new entry - is needed
  224. record = ObjectManager::Attribute.find_by(
  225. object_lookup_id: data[:object_lookup_id],
  226. name: data[:name],
  227. )
  228. if record
  229. # do not allow to overwrite certain attributes
  230. if !force
  231. data.delete(:editable)
  232. data.delete(:to_create)
  233. data.delete(:to_migrate)
  234. data.delete(:to_delete)
  235. data.delete(:to_config)
  236. end
  237. # if data_option has changed, store it for next migration
  238. if !force
  239. %i[name display data_type position active].each do |key|
  240. next if record[key] == data[key]
  241. 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
  242. data[:to_config] = true
  243. break
  244. end
  245. if record[:data_option] != data[:data_option]
  246. # do we need a database migration?
  247. if record[:data_option][:maxlength] && data[:data_option][:maxlength] && record[:data_option][:maxlength].to_s != data[:data_option][:maxlength].to_s
  248. data[:to_migrate] = true
  249. end
  250. record[:data_option_new] = data[:data_option]
  251. data.delete(:data_option)
  252. data[:to_config] = true
  253. end
  254. end
  255. # update attributes
  256. data.each do |key, value|
  257. record[key.to_sym] = value
  258. end
  259. # check editable & name
  260. if !force
  261. record.check_editable
  262. record.check_name
  263. end
  264. record.check_datatype
  265. record.save!
  266. return record
  267. end
  268. # do not allow to overwrite certain attributes
  269. if !force
  270. data[:editable] = true
  271. data[:to_create] = true
  272. data[:to_migrate] = true
  273. data[:to_delete] = false
  274. end
  275. record = ObjectManager::Attribute.new(data)
  276. # check editable & name
  277. if !force
  278. record.check_editable
  279. record.check_name
  280. end
  281. record.check_datatype
  282. record.save!
  283. record
  284. end
  285. =begin
  286. remove attribute entry for an object
  287. ObjectManager::Attribute.remove(
  288. object: 'Ticket',
  289. name: 'group_id',
  290. )
  291. use "force: true" to delete also not editable fields
  292. =end
  293. def self.remove(data)
  294. # lookups
  295. if data[:object]
  296. data[:object_lookup_id] = ObjectLookup.by_name(data[:object])
  297. end
  298. data[:name].downcase!
  299. # check newest entry - is needed
  300. record = ObjectManager::Attribute.find_by(
  301. object_lookup_id: data[:object_lookup_id],
  302. name: data[:name],
  303. )
  304. if !record
  305. raise "ERROR: No such field #{data[:object]}.#{data[:name]}"
  306. end
  307. if !data[:force] && !record.editable
  308. raise "ERROR: #{data[:object]}.#{data[:name]} can't be removed!"
  309. end
  310. # if record is to create, just destroy it
  311. if record.to_create
  312. record.destroy
  313. return true
  314. end
  315. record.to_delete = true
  316. record.save
  317. end
  318. =begin
  319. get the attribute model based on object and name
  320. attribute = ObjectManager::Attribute.get(
  321. object: 'Ticket',
  322. name: 'group_id',
  323. )
  324. =end
  325. def self.get(data)
  326. # lookups
  327. if data[:object]
  328. data[:object_lookup_id] = ObjectLookup.by_name(data[:object])
  329. end
  330. data[:name].downcase!
  331. ObjectManager::Attribute.find_by(
  332. object_lookup_id: data[:object_lookup_id],
  333. name: data[:name],
  334. )
  335. end
  336. =begin
  337. get user based list of used object attributes
  338. attribute_list = ObjectManager::Attribute.by_object('Ticket', user)
  339. returns:
  340. [
  341. { name: 'api_key', display: 'API KEY', tag: 'input', null: true, edit: true, maxlength: 32 },
  342. { name: 'api_ip_regexp', display: 'API IP RegExp', tag: 'input', null: true, edit: true },
  343. { name: 'api_ip_max', display: 'API IP Max', tag: 'input', null: true, edit: true },
  344. ]
  345. =end
  346. def self.by_object(object, user)
  347. # lookups
  348. if object
  349. object_lookup_id = ObjectLookup.by_name(object)
  350. end
  351. # get attributes in right order
  352. result = ObjectManager::Attribute.where(
  353. object_lookup_id: object_lookup_id,
  354. active: true,
  355. to_create: false,
  356. to_delete: false,
  357. ).order('position ASC, name ASC')
  358. attributes = []
  359. result.each do |item|
  360. data = {
  361. name: item.name,
  362. display: item.display,
  363. tag: item.data_type,
  364. #:null => item.null,
  365. }
  366. if item.data_option[:permission]&.any?
  367. next if !user
  368. hint = false
  369. item.data_option[:permission].each do |permission|
  370. next if !user.permissions?(permission)
  371. hint = true
  372. break
  373. end
  374. next if !hint
  375. end
  376. if item.screens
  377. data[:screen] = {}
  378. item.screens.each do |screen, permission_options|
  379. data[:screen][screen] = {}
  380. permission_options.each do |permission, options|
  381. if permission == '-all-'
  382. data[:screen][screen] = options
  383. elsif user&.permissions?(permission)
  384. data[:screen][screen] = options
  385. end
  386. end
  387. end
  388. end
  389. if item.data_option
  390. data = data.merge(item.data_option.symbolize_keys)
  391. end
  392. attributes.push data
  393. end
  394. attributes
  395. end
  396. =begin
  397. get user based list of object attributes as hash
  398. attribute_list = ObjectManager::Attribute.by_object_as_hash('Ticket', user)
  399. returns:
  400. {
  401. 'api_key' => { name: 'api_key', display: 'API KEY', tag: 'input', null: true, edit: true, maxlength: 32 },
  402. 'api_ip_regexp' => { name: 'api_ip_regexp', display: 'API IP RegExp', tag: 'input', null: true, edit: true },
  403. 'api_ip_max' => { name: 'api_ip_max', display: 'API IP Max', tag: 'input', null: true, edit: true },
  404. }
  405. =end
  406. def self.by_object_as_hash(object, user)
  407. list = by_object(object, user)
  408. hash = {}
  409. list.each do |item|
  410. hash[ item[:name] ] = item
  411. end
  412. hash
  413. end
  414. =begin
  415. discard migration changes
  416. ObjectManager::Attribute.discard_changes
  417. returns
  418. true|false
  419. =end
  420. def self.discard_changes
  421. ObjectManager::Attribute.where('to_create = ?', true).each(&:destroy)
  422. ObjectManager::Attribute.where('to_delete = ? OR to_config = ?', true, true).each do |attribute|
  423. attribute.to_migrate = false
  424. attribute.to_delete = false
  425. attribute.to_config = false
  426. attribute.data_option_new = {}
  427. attribute.save
  428. end
  429. true
  430. end
  431. =begin
  432. check if we have pending migrations of attributes
  433. ObjectManager::Attribute.pending_migration?
  434. returns
  435. true|false
  436. =end
  437. def self.pending_migration?
  438. return false if migrations.blank?
  439. true
  440. end
  441. =begin
  442. get list of pending attributes migrations
  443. ObjectManager::Attribute.migrations
  444. returns
  445. [record1, record2, ...]
  446. =end
  447. def self.migrations
  448. ObjectManager::Attribute.where('to_create = ? OR to_migrate = ? OR to_delete = ? OR to_config = ?', true, true, true, true)
  449. end
  450. =begin
  451. start migration of pending attribute migrations
  452. ObjectManager::Attribute.migration_execute
  453. returns
  454. [record1, record2, ...]
  455. to send no browser reload event, pass false
  456. ObjectManager::Attribute.migration_execute(false)
  457. =end
  458. def self.migration_execute(send_event = true)
  459. # check if field already exists
  460. execute_db_count = 0
  461. execute_config_count = 0
  462. migrations.each do |attribute|
  463. model = Kernel.const_get(attribute.object_lookup.name)
  464. # remove field
  465. if attribute.to_delete
  466. if model.column_names.include?(attribute.name)
  467. ActiveRecord::Migration.remove_column model.table_name, attribute.name
  468. reset_database_info(model)
  469. end
  470. execute_db_count += 1
  471. attribute.destroy
  472. next
  473. end
  474. # config changes
  475. if attribute.to_config
  476. execute_config_count += 1
  477. attribute.data_option = attribute.data_option_new
  478. attribute.data_option_new = {}
  479. attribute.to_config = false
  480. attribute.save!
  481. next if !attribute.to_create && !attribute.to_migrate && !attribute.to_delete
  482. end
  483. data_type = nil
  484. if attribute.data_type.match?(/^input|select|tree_select|richtext|textarea|checkbox$/)
  485. data_type = :string
  486. elsif attribute.data_type.match?(/^integer|user_autocompletion$/)
  487. data_type = :integer
  488. elsif attribute.data_type.match?(/^boolean|active$/)
  489. data_type = :boolean
  490. elsif attribute.data_type.match?(/^datetime$/)
  491. data_type = :datetime
  492. elsif attribute.data_type.match?(/^date$/)
  493. data_type = :date
  494. end
  495. # change field
  496. if model.column_names.include?(attribute.name)
  497. if attribute.data_type.match?(/^input|select|tree_select|richtext|textarea|checkbox$/)
  498. ActiveRecord::Migration.change_column(
  499. model.table_name,
  500. attribute.name,
  501. data_type,
  502. limit: attribute.data_option[:maxlength],
  503. null: true
  504. )
  505. elsif attribute.data_type.match?(/^integer|user_autocompletion|datetime|date$/)
  506. ActiveRecord::Migration.change_column(
  507. model.table_name,
  508. attribute.name,
  509. data_type,
  510. default: attribute.data_option[:default],
  511. null: true
  512. )
  513. elsif attribute.data_type.match?(/^boolean|active$/)
  514. ActiveRecord::Migration.change_column(
  515. model.table_name,
  516. attribute.name,
  517. data_type,
  518. default: attribute.data_option[:default],
  519. null: true
  520. )
  521. else
  522. raise "Unknown attribute.data_type '#{attribute.data_type}', can't update attribute"
  523. end
  524. # restart processes
  525. attribute.to_create = false
  526. attribute.to_migrate = false
  527. attribute.to_delete = false
  528. attribute.save!
  529. reset_database_info(model)
  530. execute_db_count += 1
  531. next
  532. end
  533. # create field
  534. if attribute.data_type.match?(/^input|select|tree_select|richtext|textarea|checkbox$/)
  535. ActiveRecord::Migration.add_column(
  536. model.table_name,
  537. attribute.name,
  538. data_type,
  539. limit: attribute.data_option[:maxlength],
  540. null: true
  541. )
  542. elsif attribute.data_type.match?(/^integer|user_autocompletion$/)
  543. ActiveRecord::Migration.add_column(
  544. model.table_name,
  545. attribute.name,
  546. data_type,
  547. default: attribute.data_option[:default],
  548. null: true
  549. )
  550. elsif attribute.data_type.match?(/^boolean|active$/)
  551. ActiveRecord::Migration.add_column(
  552. model.table_name,
  553. attribute.name,
  554. data_type,
  555. default: attribute.data_option[:default],
  556. null: true
  557. )
  558. elsif attribute.data_type.match?(/^datetime|date$/)
  559. ActiveRecord::Migration.add_column(
  560. model.table_name,
  561. attribute.name,
  562. data_type,
  563. default: attribute.data_option[:default],
  564. null: true
  565. )
  566. else
  567. raise "Unknown attribute.data_type '#{attribute.data_type}', can't create attribute"
  568. end
  569. # restart processes
  570. attribute.to_create = false
  571. attribute.to_migrate = false
  572. attribute.to_delete = false
  573. attribute.save!
  574. reset_database_info(model)
  575. execute_db_count += 1
  576. end
  577. # sent maintenance message to clients
  578. if send_event
  579. if execute_db_count.nonzero?
  580. if ENV['APP_RESTART_CMD']
  581. AppVersion.set(true, 'restart_auto')
  582. sleep 4
  583. Delayed::Job.enqueue(Observer::AppVersionRestartJob.new(ENV['APP_RESTART_CMD']))
  584. else
  585. AppVersion.set(true, 'restart_manual')
  586. end
  587. elsif execute_config_count.nonzero?
  588. AppVersion.set(true, 'config_changed')
  589. end
  590. end
  591. true
  592. end
  593. def self.reset_database_info(model)
  594. model.connection.schema_cache.clear!
  595. model.reset_column_information
  596. # rebuild columns cache to reduce the risk of
  597. # race conditions in re-setting it with outdated data
  598. model.columns
  599. end
  600. def check_name
  601. return if !name
  602. raise 'Name can\'t get used, *_id and *_ids are not allowed' if name.match?(/_(id|ids)$/i) || name.match?(/^id$/i)
  603. raise 'Spaces in name are not allowed' if name.match?(/\s/)
  604. raise 'Only letters from a-z, numbers from 0-9, and _ are allowed' if !name.match?(/^[a-z0-9_]+$/)
  605. raise 'At least one letters is needed' if !name.match?(/[a-z]/)
  606. # do not allow model method names as attributes
  607. reserved_words = %w[destroy true false integer select drop create alter index table varchar blob date datetime timestamp]
  608. raise "#{name} is a reserved word, please choose a different one" if name.match?(/^(#{reserved_words.join('|')})$/)
  609. record = object_lookup.name.constantize.new
  610. return true if !record.respond_to?(name.to_sym)
  611. return true if record.attributes.key?(name)
  612. raise "#{name} is a reserved word, please choose a different one"
  613. end
  614. def check_editable
  615. return if editable
  616. raise 'Attribute not editable!'
  617. end
  618. def check_datatype
  619. if !data_type
  620. raise 'Need data_type param'
  621. end
  622. if data_type !~ /^(input|user_autocompletion|checkbox|select|tree_select|datetime|date|tag|richtext|textarea|integer|autocompletion_ajax|boolean|user_permission|active)$/
  623. raise "Invalid data_type param '#{data_type}'"
  624. end
  625. if !data_option
  626. raise 'Need data_type param'
  627. end
  628. if data_option[:null].nil?
  629. raise 'Need data_option[:null] param with true or false'
  630. end
  631. # validate data_option
  632. if data_type == 'input'
  633. raise 'Need data_option[:type] param' if !data_option[:type]
  634. raise "Invalid data_option[:type] param '#{data_option[:type]}'" if data_option[:type] !~ /^(text|password|tel|fax|email|url)$/
  635. raise 'Need data_option[:maxlength] param' if !data_option[:maxlength]
  636. raise "Invalid data_option[:maxlength] param #{data_option[:maxlength]}" if data_option[:maxlength].to_s !~ /^\d+?$/
  637. end
  638. if data_type == 'richtext'
  639. raise 'Need data_option[:maxlength] param' if !data_option[:maxlength]
  640. raise "Invalid data_option[:maxlength] param #{data_option[:maxlength]}" if data_option[:maxlength].to_s !~ /^\d+?$/
  641. end
  642. if data_type == 'integer'
  643. %i[min max].each do |item|
  644. raise "Need data_option[#{item.inspect}] param" if !data_option[item]
  645. raise "Invalid data_option[#{item.inspect}] param #{data_option[item]}" if data_option[item].to_s !~ /^\d+?$/
  646. end
  647. end
  648. if data_type == 'select' || data_type == 'tree_select' || data_type == 'checkbox'
  649. raise 'Need data_option[:default] param' if !data_option.key?(:default)
  650. raise 'Invalid data_option[:options] or data_option[:relation] param' if data_option[:options].nil? && data_option[:relation].nil?
  651. if !data_option.key?(:maxlength)
  652. data_option[:maxlength] = 255
  653. end
  654. if !data_option.key?(:nulloption)
  655. data_option[:nulloption] = true
  656. end
  657. end
  658. if data_type == 'boolean'
  659. raise 'Need data_option[:default] param true|false|undefined' if !data_option.key?(:default)
  660. raise 'Invalid data_option[:options] param' if data_option[:options].nil?
  661. end
  662. if data_type == 'datetime'
  663. raise 'Need data_option[:future] param true|false' if data_option[:future].nil?
  664. raise 'Need data_option[:past] param true|false' if data_option[:past].nil?
  665. raise 'Need data_option[:diff] param in hours' if data_option[:diff].nil?
  666. end
  667. if data_type == 'date'
  668. raise 'Need data_option[:future] param true|false' if data_option[:future].nil?
  669. raise 'Need data_option[:past] param true|false' if data_option[:past].nil?
  670. raise 'Need data_option[:diff] param in days' if data_option[:diff].nil?
  671. end
  672. true
  673. end
  674. end