attribute.rb 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822
  1. class ObjectManager::Attribute < ApplicationModel
  2. include ChecksClientNotification
  3. include CanSeed
  4. self.table_name = 'object_manager_attributes'
  5. belongs_to :object_lookup, class_name: 'ObjectLookup'
  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. [:name, :display, :data_type, :position, :active].each do |key|
  240. next if record[key] == data[key]
  241. data[:to_config] = true
  242. break
  243. end
  244. if record[:data_option] != data[:data_option]
  245. # do we need a database migration?
  246. if record[:data_option][:maxlength] && data[:data_option][:maxlength] && record[:data_option][:maxlength].to_s != data[:data_option][:maxlength].to_s
  247. data[:to_migrate] = true
  248. end
  249. record[:data_option_new] = data[:data_option]
  250. data.delete(:data_option)
  251. data[:to_config] = true
  252. end
  253. end
  254. # update attributes
  255. data.each do |key, value|
  256. record[key.to_sym] = value
  257. end
  258. # check editable & name
  259. if !force
  260. record.check_editable
  261. record.check_name
  262. end
  263. record.check_datatype
  264. record.save!
  265. return record
  266. end
  267. # do not allow to overwrite certain attributes
  268. if !force
  269. data[:editable] = true
  270. data[:to_create] = true
  271. data[:to_migrate] = true
  272. data[:to_delete] = false
  273. end
  274. record = ObjectManager::Attribute.new(data)
  275. # check editable & name
  276. if !force
  277. record.check_editable
  278. record.check_name
  279. end
  280. record.check_datatype
  281. record.save!
  282. record
  283. end
  284. =begin
  285. remove attribute entry for an object
  286. ObjectManager::Attribute.remove(
  287. object: 'Ticket',
  288. name: 'group_id',
  289. )
  290. use "force: true" to delete also not editable fields
  291. =end
  292. def self.remove(data)
  293. # lookups
  294. if data[:object]
  295. data[:object_lookup_id] = ObjectLookup.by_name(data[:object])
  296. end
  297. data[:name].downcase!
  298. # check newest entry - is needed
  299. record = ObjectManager::Attribute.find_by(
  300. object_lookup_id: data[:object_lookup_id],
  301. name: data[:name],
  302. )
  303. if !record
  304. raise "ERROR: No such field #{data[:object]}.#{data[:name]}"
  305. end
  306. if !data[:force] && !record.editable
  307. raise "ERROR: #{data[:object]}.#{data[:name]} can't be removed!"
  308. end
  309. # if record is to create, just destroy it
  310. if record.to_create
  311. record.destroy
  312. return true
  313. end
  314. record.to_delete = true
  315. record.save
  316. end
  317. =begin
  318. get the attribute model based on object and name
  319. attribute = ObjectManager::Attribute.get(
  320. object: 'Ticket',
  321. name: 'group_id',
  322. )
  323. =end
  324. def self.get(data)
  325. # lookups
  326. if data[:object]
  327. data[:object_lookup_id] = ObjectLookup.by_name(data[:object])
  328. end
  329. data[:name].downcase!
  330. ObjectManager::Attribute.find_by(
  331. object_lookup_id: data[:object_lookup_id],
  332. name: data[:name],
  333. )
  334. end
  335. =begin
  336. get user based list of used object attributes
  337. attribute_list = ObjectManager::Attribute.by_object('Ticket', user)
  338. returns:
  339. [
  340. { name: 'api_key', display: 'API KEY', tag: 'input', null: true, edit: true, maxlength: 32 },
  341. { name: 'api_ip_regexp', display: 'API IP RegExp', tag: 'input', null: true, edit: true },
  342. { name: 'api_ip_max', display: 'API IP Max', tag: 'input', null: true, edit: true },
  343. ]
  344. =end
  345. def self.by_object(object, user)
  346. # lookups
  347. if object
  348. object_lookup_id = ObjectLookup.by_name(object)
  349. end
  350. # get attributes in right order
  351. result = ObjectManager::Attribute.where(
  352. object_lookup_id: object_lookup_id,
  353. active: true,
  354. to_create: false,
  355. to_delete: false,
  356. ).order('position ASC, name ASC')
  357. attributes = []
  358. result.each do |item|
  359. data = {
  360. name: item.name,
  361. display: item.display,
  362. tag: item.data_type,
  363. #:null => item.null,
  364. }
  365. if item.data_option[:permission] && item.data_option[:permission].any?
  366. next if !user
  367. hint = false
  368. item.data_option[:permission].each do |permission|
  369. next if !user.permissions?(permission)
  370. hint = true
  371. break
  372. end
  373. next if !hint
  374. end
  375. if item.screens
  376. data[:screen] = {}
  377. item.screens.each do |screen, permission_options|
  378. data[:screen][screen] = {}
  379. permission_options.each do |permission, options|
  380. if permission == '-all-'
  381. data[:screen][screen] = options
  382. elsif user && user.permissions?(permission)
  383. data[:screen][screen] = options
  384. end
  385. end
  386. end
  387. end
  388. if item.data_option
  389. data = data.merge(item.data_option.symbolize_keys)
  390. end
  391. attributes.push data
  392. end
  393. attributes
  394. end
  395. =begin
  396. get user based list of object attributes as hash
  397. attribute_list = ObjectManager::Attribute.by_object_as_hash('Ticket', user)
  398. returns:
  399. {
  400. 'api_key' => { name: 'api_key', display: 'API KEY', tag: 'input', null: true, edit: true, maxlength: 32 },
  401. 'api_ip_regexp' => { name: 'api_ip_regexp', display: 'API IP RegExp', tag: 'input', null: true, edit: true },
  402. 'api_ip_max' => { name: 'api_ip_max', display: 'API IP Max', tag: 'input', null: true, edit: true },
  403. }
  404. =end
  405. def self.by_object_as_hash(object, user)
  406. list = by_object(object, user)
  407. hash = {}
  408. list.each do |item|
  409. hash[ item[:name] ] = item
  410. end
  411. hash
  412. end
  413. =begin
  414. discard migration changes
  415. ObjectManager::Attribute.discard_changes
  416. returns
  417. true|false
  418. =end
  419. def self.discard_changes
  420. ObjectManager::Attribute.where('to_create = ?', true).each(&:destroy)
  421. ObjectManager::Attribute.where('to_delete = ? OR to_config = ?', true, true).each do |attribute|
  422. attribute.to_migrate = false
  423. attribute.to_delete = false
  424. attribute.to_config = false
  425. attribute.data_option_new = {}
  426. attribute.save
  427. end
  428. true
  429. end
  430. =begin
  431. check if we have pending migrations of attributes
  432. ObjectManager::Attribute.pending_migration?
  433. returns
  434. true|false
  435. =end
  436. def self.pending_migration?
  437. return false if migrations.empty?
  438. true
  439. end
  440. =begin
  441. get list of pending attributes migrations
  442. ObjectManager::Attribute.migrations
  443. returns
  444. [record1, record2, ...]
  445. =end
  446. def self.migrations
  447. ObjectManager::Attribute.where('to_create = ? OR to_migrate = ? OR to_delete = ? OR to_config = ?', true, true, true, true)
  448. end
  449. =begin
  450. start migration of pending attribute migrations
  451. ObjectManager::Attribute.migration_execute
  452. returns
  453. [record1, record2, ...]
  454. to send no browser reload event, pass false
  455. ObjectManager::Attribute.migration_execute(false)
  456. =end
  457. def self.migration_execute(send_event = true)
  458. # check if field already exists
  459. execute_db_count = 0
  460. execute_config_count = 0
  461. migrations.each do |attribute|
  462. model = Kernel.const_get(attribute.object_lookup.name)
  463. # remove field
  464. if attribute.to_delete
  465. if model.column_names.include?(attribute.name)
  466. ActiveRecord::Migration.remove_column model.table_name, attribute.name
  467. reset_database_info(model)
  468. end
  469. execute_db_count += 1
  470. attribute.destroy
  471. next
  472. end
  473. # config changes
  474. if attribute.to_config
  475. execute_config_count += 1
  476. attribute.data_option = attribute.data_option_new
  477. attribute.data_option_new = {}
  478. attribute.to_config = false
  479. attribute.save!
  480. next if !attribute.to_create && !attribute.to_migrate && !attribute.to_delete
  481. end
  482. data_type = nil
  483. if attribute.data_type =~ /^input|select|tree_select|richtext|textarea|checkbox$/
  484. data_type = :string
  485. elsif attribute.data_type =~ /^integer|user_autocompletion$/
  486. data_type = :integer
  487. elsif attribute.data_type =~ /^boolean|active$/
  488. data_type = :boolean
  489. elsif attribute.data_type =~ /^datetime$/
  490. data_type = :datetime
  491. elsif attribute.data_type =~ /^date$/
  492. data_type = :date
  493. end
  494. # change field
  495. if model.column_names.include?(attribute.name)
  496. if attribute.data_type =~ /^input|select|tree_select|richtext|textarea|checkbox$/
  497. ActiveRecord::Migration.change_column(
  498. model.table_name,
  499. attribute.name,
  500. data_type,
  501. limit: attribute.data_option[:maxlength],
  502. null: true
  503. )
  504. elsif attribute.data_type =~ /^integer|user_autocompletion|datetime|date$/
  505. ActiveRecord::Migration.change_column(
  506. model.table_name,
  507. attribute.name,
  508. data_type,
  509. default: attribute.data_option[:default],
  510. null: true
  511. )
  512. elsif attribute.data_type =~ /^boolean|active$/
  513. ActiveRecord::Migration.change_column(
  514. model.table_name,
  515. attribute.name,
  516. data_type,
  517. default: attribute.data_option[:default],
  518. null: true
  519. )
  520. else
  521. raise "Unknown attribute.data_type '#{attribute.data_type}', can't update attribute"
  522. end
  523. # restart processes
  524. attribute.to_create = false
  525. attribute.to_migrate = false
  526. attribute.to_delete = false
  527. attribute.save!
  528. reset_database_info(model)
  529. execute_db_count += 1
  530. next
  531. end
  532. # create field
  533. if attribute.data_type =~ /^input|select|tree_select|richtext|textarea|checkbox$/
  534. ActiveRecord::Migration.add_column(
  535. model.table_name,
  536. attribute.name,
  537. data_type,
  538. limit: attribute.data_option[:maxlength],
  539. null: true
  540. )
  541. elsif attribute.data_type =~ /^integer|user_autocompletion$/
  542. ActiveRecord::Migration.add_column(
  543. model.table_name,
  544. attribute.name,
  545. data_type,
  546. default: attribute.data_option[:default],
  547. null: true
  548. )
  549. elsif attribute.data_type =~ /^boolean|active$/
  550. ActiveRecord::Migration.add_column(
  551. model.table_name,
  552. attribute.name,
  553. data_type,
  554. default: attribute.data_option[:default],
  555. null: true
  556. )
  557. elsif attribute.data_type =~ /^datetime|date$/
  558. ActiveRecord::Migration.add_column(
  559. model.table_name,
  560. attribute.name,
  561. data_type,
  562. default: attribute.data_option[:default],
  563. null: true
  564. )
  565. else
  566. raise "Unknown attribute.data_type '#{attribute.data_type}', can't create attribute"
  567. end
  568. # restart processes
  569. attribute.to_create = false
  570. attribute.to_migrate = false
  571. attribute.to_delete = false
  572. attribute.save!
  573. reset_database_info(model)
  574. execute_db_count += 1
  575. end
  576. # sent maintenance message to clients
  577. if send_event
  578. if execute_db_count.nonzero?
  579. if ENV['APP_RESTART_CMD']
  580. AppVersion.set(true, 'restart_auto')
  581. sleep 4
  582. Delayed::Job.enqueue(Observer::AppVersionRestartJob.new(ENV['APP_RESTART_CMD']))
  583. else
  584. AppVersion.set(true, 'restart_manual')
  585. end
  586. elsif execute_config_count.nonzero?
  587. AppVersion.set(true, 'config_changed')
  588. end
  589. end
  590. true
  591. end
  592. def self.reset_database_info(model)
  593. model.connection.schema_cache.clear!
  594. model.reset_column_information
  595. # rebuild columns cache to reduce the risk of
  596. # race conditions in re-setting it with outdated data
  597. model.columns
  598. end
  599. def check_name
  600. return if !name
  601. if name =~ /_(id|ids)$/i || name =~ /^id$/i
  602. raise 'Name can\'t get used, *_id and *_ids are not allowed'
  603. elsif name =~ /\s/
  604. raise 'Spaces in name are not allowed'
  605. elsif name !~ /^[a-z0-9_]+$/
  606. raise 'Only letters from a-z, numbers from 0-9, and _ are allowed'
  607. elsif name !~ /[a-z]/
  608. raise 'At least one letters is needed'
  609. elsif name =~ /^(destroy|true|false|integer|select|drop|create|alter|index|table|varchar|blob|date|datetime|timestamp)$/
  610. raise "#{name} is a reserved word, please choose a different one"
  611. # do not allow model method names as attributes
  612. else
  613. model = Kernel.const_get(object_lookup.name)
  614. record = model.new
  615. if record.respond_to?(name.to_sym) && !record.attributes.key?(name)
  616. raise "#{name} is a reserved word, please choose a different one"
  617. end
  618. end
  619. true
  620. end
  621. def check_editable
  622. return if editable
  623. raise 'Attribute not editable!'
  624. end
  625. def check_datatype
  626. if !data_type
  627. raise 'Need data_type param'
  628. end
  629. if data_type !~ /^(input|user_autocompletion|checkbox|select|tree_select|datetime|date|tag|richtext|textarea|integer|autocompletion_ajax|boolean|user_permission|active)$/
  630. raise "Invalid data_type param '#{data_type}'"
  631. end
  632. if !data_option
  633. raise 'Need data_type param'
  634. end
  635. if data_option[:null].nil?
  636. raise 'Need data_option[:null] param with true or false'
  637. end
  638. # validate data_option
  639. if data_type == 'input'
  640. raise 'Need data_option[:type] param' if !data_option[:type]
  641. raise "Invalid data_option[:type] param '#{data_option[:type]}'" if data_option[:type] !~ /^(text|password|tel|fax|email|url)$/
  642. raise 'Need data_option[:maxlength] param' if !data_option[:maxlength]
  643. raise "Invalid data_option[:maxlength] param #{data_option[:maxlength]}" if data_option[:maxlength].to_s !~ /^\d+?$/
  644. end
  645. if data_type == 'richtext'
  646. raise 'Need data_option[:maxlength] param' if !data_option[:maxlength]
  647. raise "Invalid data_option[:maxlength] param #{data_option[:maxlength]}" if data_option[:maxlength].to_s !~ /^\d+?$/
  648. end
  649. if data_type == 'integer'
  650. [:min, :max].each do |item|
  651. raise "Need data_option[#{item.inspect}] param" if !data_option[item]
  652. raise "Invalid data_option[#{item.inspect}] param #{data_option[item]}" if data_option[item].to_s !~ /^\d+?$/
  653. end
  654. end
  655. if data_type == 'select' || data_type == 'tree_select' || data_type == 'checkbox'
  656. raise 'Need data_option[:default] param' if !data_option.key?(:default)
  657. raise 'Invalid data_option[:options] or data_option[:relation] param' if data_option[:options].nil? && data_option[:relation].nil?
  658. if !data_option.key?(:maxlength)
  659. data_option[:maxlength] = 255
  660. end
  661. if !data_option.key?(:nulloption)
  662. data_option[:nulloption] = true
  663. end
  664. end
  665. if data_type == 'boolean'
  666. raise 'Need data_option[:default] param true|false|undefined' if !data_option.key?(:default)
  667. raise 'Invalid data_option[:options] param' if data_option[:options].nil?
  668. end
  669. if data_type == 'datetime'
  670. raise 'Need data_option[:future] param true|false' if data_option[:future].nil?
  671. raise 'Need data_option[:past] param true|false' if data_option[:past].nil?
  672. raise 'Need data_option[:diff] param in hours' if data_option[:diff].nil?
  673. end
  674. if data_type == 'date'
  675. raise 'Need data_option[:future] param true|false' if data_option[:future].nil?
  676. raise 'Need data_option[:past] param true|false' if data_option[:past].nil?
  677. raise 'Need data_option[:diff] param in days' if data_option[:diff].nil?
  678. end
  679. end
  680. end