123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740 |
- class ObjectManager::Attribute < ApplicationModel
- self.table_name = 'object_manager_attributes'
- belongs_to :object_lookup, class_name: 'ObjectLookup'
- validates :name, presence: true
- store :screens
- store :data_option
- store :data_option_new
- notify_clients_support
- =begin
- list of all attributes
- result = ObjectManager::Attribute.list_full
- result = [
- {
- name: 'some name',
- display: '...',
- }.
- ],
- =end
- def self.list_full
- result = ObjectManager::Attribute.all.order('position ASC, name ASC')
- attributes = []
- assets = {}
- result.each { |item|
- attribute = item.attributes
- attribute[:object] = ObjectLookup.by_id(item.object_lookup_id)
- attribute.delete('object_lookup_id')
- attributes.push attribute
- }
- attributes
- end
- =begin
- add a new attribute entry for an object
- ObjectManager::Attribute.add(
- object: 'Ticket',
- name: 'group_id',
- display: 'Group',
- data_type: 'select',
- data_option: {
- relation: 'Group',
- relation_condition: { access: 'rw' },
- multiple: false,
- null: true,
- translate: false,
- },
- active: true,
- screens: {
- create: {
- '-all-' => {
- required: true,
- },
- },
- edit: {
- Agent : {
- required: true,
- },
- },
- },
- position: 20,
- created_by_id: 1,
- updated_by_id: 1,
- created_at: '2014-06-04 10:00:00',
- updated_at: '2014-06-04 10:00:00',
- force: true
- editable: false,
- to_migrate: false,
- to_create: false,
- to_delete: false,
- to_config: false,
- )
- preserved name are
- /(_id|_ids)$/
- possible types
- # input
- data_type: 'input',
- data_option: {
- default: '',
- type: 'text', # text|email|url|tel
- maxlength: 200,
- null: true,
- note: 'some additional comment', # optional
- },
- # select
- data_type: 'select',
- data_option: {
- default: 'aa',
- options: {
- 'aa' => 'aa (comment)',
- 'bb' => 'bb (comment)',
- },
- maxlength: 200,
- nulloption: true,
- null: false,
- multiple: false, # currently only "false" supported
- translate: true, # optional
- note: 'some additional comment', # optional
- },
- # checkbox
- data_type: 'checkbox',
- data_option: {
- default: 'aa',
- options: {
- 'aa' => 'aa (comment)',
- 'bb' => 'bb (comment)',
- },
- null: false,
- translate: true, # optional
- note: 'some additional comment', # optional
- },
- # integer
- data_type: 'integer',
- data_option: {
- default: 5,
- min: 15,
- max: 999,
- null: false,
- note: 'some additional comment', # optional
- },
- # boolean
- data_type: 'boolean',
- data_option: {
- default: true,
- options: {
- true => 'aa',
- false => 'bb',
- },
- null: false,
- translate: true, # optional
- note: 'some additional comment', # optional
- },
- # datetime
- data_type: 'datetime',
- data_option: {
- future: true, # true|false
- past: true, # true|false
- diff: 12, # in hours
- null: false,
- note: 'some additional comment', # optional
- },
- # date
- data_type: 'date',
- data_option: {
- future: true, # true|false
- past: true, # true|false
- diff: 15, # in days
- null: false,
- note: 'some additional comment', # optional
- },
- # textarea
- data_type: 'textarea',
- data_option: {
- default: '',
- rows: 15,
- null: false,
- note: 'some additional comment', # optional
- },
- # richtext
- data_type: 'richtext',
- data_option: {
- default: '',
- null: false,
- note: 'some additional comment', # optional
- },
- =end
- def self.add(data)
- force = data[:force]
- data.delete(:force)
- # lookups
- if data[:object]
- data[:object_lookup_id] = ObjectLookup.by_name(data[:object])
- end
- data.delete(:object)
- data[:name].downcase!
- # check new entry - is needed
- record = ObjectManager::Attribute.find_by(
- object_lookup_id: data[:object_lookup_id],
- name: data[:name],
- )
- if record
- # do not allow to overwrite certain attributes
- if !force
- data.delete(:editable)
- data.delete(:to_create)
- data.delete(:to_migrate)
- data.delete(:to_delete)
- data.delete(:to_config)
- end
- # if data_option has changed, store it for next migration
- if !force
- if record[:data_option] != data[:data_option]
- # do we need a database migration?
- if record[:data_option][:maxlength] && data[:data_option][:maxlength] && record[:data_option][:maxlength].to_s != data[:data_option][:maxlength].to_s
- data[:to_migrate] = true
- end
- record[:data_option_new] = data[:data_option]
- data.delete(:data_option)
- data[:to_config] = true
- end
- end
- # update attributes
- data.each { |key, value|
- record[key.to_sym] = value
- }
- # check editable & name
- if !force
- record.check_editable
- record.check_name
- end
- record.check_datatype
- record.save!
- return record
- end
- # do not allow to overwrite certain attributes
- if !force
- data[:editable] = true
- data[:to_create] = true
- data[:to_migrate] = true
- data[:to_delete] = false
- end
- record = ObjectManager::Attribute.new(data)
- # check editable & name
- if !force
- record.check_editable
- record.check_name
- end
- record.check_datatype
- record.save!
- record
- end
- =begin
- remove attribute entry for an object
- ObjectManager::Attribute.remove(
- object: 'Ticket',
- name: 'group_id',
- )
- use "force: true" to delete also not editable fields
- =end
- def self.remove(data)
- # lookups
- if data[:object]
- data[:object_lookup_id] = ObjectLookup.by_name(data[:object])
- end
- data[:name].downcase!
- # check newest entry - is needed
- record = ObjectManager::Attribute.find_by(
- object_lookup_id: data[:object_lookup_id],
- name: data[:name],
- )
- if !record
- raise "ERROR: No such field #{data[:object]}.#{data[:name]}"
- end
- if !data[:force] && !record.editable
- raise "ERROR: #{data[:object]}.#{data[:name]} can't be removed!"
- end
- # if record is to create, just destroy it
- if record.to_create
- record.destroy
- return true
- end
- record.to_delete = true
- record.save
- end
- =begin
- get the attribute model based on object and name
- attribute = ObjectManager::Attribute.get(
- object: 'Ticket',
- name: 'group_id',
- )
- =end
- def self.get(data)
- # lookups
- if data[:object]
- data[:object_lookup_id] = ObjectLookup.by_name(data[:object])
- end
- data[:name].downcase!
- ObjectManager::Attribute.find_by(
- object_lookup_id: data[:object_lookup_id],
- name: data[:name],
- )
- end
- =begin
- get user based list of used object attributes
- attribute_list = ObjectManager::Attribute.by_object('Ticket', user)
- returns:
- [
- { name: 'api_key', display: 'API KEY', tag: 'input', null: true, edit: true, maxlength: 32 },
- { name: 'api_ip_regexp', display: 'API IP RegExp', tag: 'input', null: true, edit: true },
- { name: 'api_ip_max', display: 'API IP Max', tag: 'input', null: true, edit: true },
- ]
- =end
- def self.by_object(object, user)
- # lookups
- if object
- object_lookup_id = ObjectLookup.by_name(object)
- end
- # get attributes in right order
- result = ObjectManager::Attribute.where(
- object_lookup_id: object_lookup_id,
- active: true,
- to_create: false,
- to_delete: false,
- ).order('position ASC, name ASC')
- attributes = []
- result.each { |item|
- data = {
- name: item.name,
- display: item.display,
- tag: item.data_type,
- #:null => item.null,
- }
- if item.screens
- data[:screen] = {}
- item.screens.each { |screen, roles_options|
- data[:screen][screen] = {}
- roles_options.each { |role, options|
- if role == '-all-'
- data[:screen][screen] = options
- elsif user && user.role?(role)
- data[:screen][screen] = options
- end
- }
- }
- end
- if item.data_option
- data = data.merge(item.data_option.symbolize_keys)
- end
- attributes.push data
- }
- attributes
- end
- =begin
- get user based list of object attributes as hash
- attribute_list = ObjectManager::Attribute.by_object_as_hash('Ticket', user)
- returns:
- {
- 'api_key' => { name: 'api_key', display: 'API KEY', tag: 'input', null: true, edit: true, maxlength: 32 },
- 'api_ip_regexp' => { name: 'api_ip_regexp', display: 'API IP RegExp', tag: 'input', null: true, edit: true },
- 'api_ip_max' => { name: 'api_ip_max', display: 'API IP Max', tag: 'input', null: true, edit: true },
- }
- =end
- def self.by_object_as_hash(object, user)
- list = by_object(object, user)
- hash = {}
- list.each { |item|
- hash[ item[:name] ] = item
- }
- hash
- end
- =begin
- discard migration changes
- ObjectManager::Attribute.discard_changes
- returns
- true|false
- =end
- def self.discard_changes
- ObjectManager::Attribute.where('to_create = ?', true).each(&:destroy)
- ObjectManager::Attribute.where('to_delete = ? OR to_config = ?', true, true).each { |attribute|
- attribute.to_migrate = false
- attribute.to_delete = false
- attribute.to_config = false
- attribute.data_option_new = {}
- attribute.save
- }
- true
- end
- =begin
- check if we have pending migrations of attributes
- ObjectManager::Attribute.pending_migration?
- returns
- true|false
- =end
- def self.pending_migration?
- return false if migrations.empty?
- true
- end
- =begin
- get list of pending attributes migrations
- ObjectManager::Attribute.migrations
- returns
- [record1, record2, ...]
- =end
- def self.migrations
- ObjectManager::Attribute.where('to_create = ? OR to_migrate = ? OR to_delete = ? OR to_config = ?', true, true, true, true)
- end
- =begin
- start migration of pending attribute migrations
- ObjectManager::Attribute.migration_execute
- returns
- [record1, record2, ...]
- to send no browser reload event, pass false
- ObjectManager::Attribute.migration_execute(false)
- =end
- def self.migration_execute(send_event = true)
- # check if field already exists
- execute_db_count = 0
- execute_config_count = 0
- migrations.each { |attribute|
- model = Kernel.const_get(attribute.object_lookup.name)
- # remove field
- if attribute.to_delete
- if model.column_names.include?(attribute.name)
- ActiveRecord::Migration.remove_column model.table_name, attribute.name
- reset_database_info(model)
- end
- execute_db_count += 1
- attribute.destroy
- next
- end
- # config changes
- if attribute.to_config
- execute_config_count += 1
- attribute.data_option = attribute.data_option_new
- attribute.data_option_new = {}
- attribute.to_config = false
- attribute.save!
- next if !attribute.to_create && !attribute.to_migrate && !attribute.to_delete
- end
- data_type = nil
- if attribute.data_type =~ /^input|select|richtext|textarea|checkbox$/
- data_type = :string
- elsif attribute.data_type =~ /^integer$/
- data_type = :integer
- elsif attribute.data_type =~ /^boolean|active$/
- data_type = :boolean
- elsif attribute.data_type =~ /^datetime$/
- data_type = :datetime
- elsif attribute.data_type =~ /^date$/
- data_type = :date
- end
- # change field
- if model.column_names.include?(attribute.name)
- if attribute.data_type =~ /^input|select|richtext|textarea|checkbox$/
- ActiveRecord::Migration.change_column(
- model.table_name,
- attribute.name,
- data_type,
- limit: attribute.data_option[:maxlength],
- null: true
- )
- elsif attribute.data_type =~ /^integer|datetime|date$/
- ActiveRecord::Migration.change_column(
- model.table_name,
- attribute.name,
- data_type,
- default: attribute.data_option[:default],
- null: true
- )
- elsif attribute.data_type =~ /^boolean|active$/
- ActiveRecord::Migration.change_column(
- model.table_name,
- attribute.name,
- data_type,
- default: attribute.data_option[:default],
- null: true
- )
- else
- raise "Unknown attribute.data_type '#{attribute.data_type}', can't update attribute"
- end
- # restart processes
- attribute.to_migrate = false
- attribute.save!
- reset_database_info(model)
- execute_db_count += 1
- next
- end
- # create field
- if attribute.data_type =~ /^input|select|richtext|textarea|checkbox$/
- ActiveRecord::Migration.add_column(
- model.table_name,
- attribute.name,
- data_type,
- limit: attribute.data_option[:maxlength],
- null: true
- )
- elsif attribute.data_type =~ /^integer$/
- ActiveRecord::Migration.add_column(
- model.table_name,
- attribute.name,
- data_type,
- default: attribute.data_option[:default],
- null: true
- )
- elsif attribute.data_type =~ /^boolean|active$/
- ActiveRecord::Migration.add_column(
- model.table_name,
- attribute.name,
- data_type,
- default: attribute.data_option[:default],
- null: true
- )
- elsif attribute.data_type =~ /^datetime|date$/
- ActiveRecord::Migration.add_column(
- model.table_name,
- attribute.name,
- data_type,
- default: attribute.data_option[:default],
- null: true
- )
- else
- raise "Unknown attribute.data_type '#{attribute.data_type}', can't create attribute"
- end
- # restart processes
- attribute.to_create = false
- attribute.to_migrate = false
- attribute.to_delete = false
- attribute.save!
- reset_database_info(model)
- execute_db_count += 1
- }
- # sent maintenance message to clients
- if send_event
- if execute_db_count.nonzero?
- if ENV['APP_RESTART_CMD']
- AppVersion.set(true, 'restart_auto')
- sleep 4
- Delayed::Job.enqueue(Observer::AppVersionRestartJob.new(ENV['APP_RESTART_CMD']))
- else
- AppVersion.set(true, 'restart_manual')
- end
- elsif execute_config_count.nonzero?
- AppVersion.set(true, 'config_changed')
- end
- end
- true
- end
- def self.reset_database_info(model)
- model.connection.schema_cache.clear!
- model.reset_column_information
- end
- def check_name
- return if !name
- if name =~ /_(id|ids)$/i || name =~ /^id$/i
- raise 'Name can\'t get used, *_id and *_ids are not allowed'
- elsif name =~ /\s/
- raise 'Spaces in name are not allowed'
- elsif name !~ /^[a-z0-9_]+$/
- raise 'Only letters from a-z, numbers from 0-9, and _ are allowed'
- elsif name !~ /[a-z]/
- raise 'At least one letters is needed'
- elsif name =~ /^(destroy|true|false|integer|select|drop|create|alter|index|table|varchar|blob|date|datetime|timestamp)$/
- raise "#{name} is a reserved word, please choose a different one"
- end
- true
- end
- def check_editable
- return if editable
- raise 'Attribute not editable!'
- end
- def check_datatype
- if !data_type
- raise 'Need data_type param'
- end
- if data_type !~ /^(input|user_autocompletion|checkbox|select|datetime|date|tag|richtext|textarea|integer|autocompletion_ajax|boolean|user_permission|active)$/
- raise "Invalid data_type param '#{data_type}'"
- end
- if !data_option
- raise 'Need data_type param'
- end
- if data_option[:null].nil?
- raise 'Need data_option[:null] param with true or false'
- end
- # validate data_option
- if data_type == 'input'
- raise 'Need data_option[:type] param' if !data_option[:type]
- raise "Invalid data_option[:type] param '#{data_option[:type]}'" if data_option[:type] !~ /^(text|password|tel|fax|email|url)$/
- raise 'Need data_option[:maxlength] param' if !data_option[:maxlength]
- raise "Invalid data_option[:maxlength] param #{data_option[:maxlength]}" if data_option[:maxlength].to_s !~ /^\d+?$/
- end
- if data_type == 'richtext'
- raise 'Need data_option[:maxlength] param' if !data_option[:maxlength]
- raise "Invalid data_option[:maxlength] param #{data_option[:maxlength]}" if data_option[:maxlength].to_s !~ /^\d+?$/
- end
- if data_type == 'integer'
- [:min, :max].each { |item|
- raise "Need data_option[#{item.inspect}] param" if !data_option[item]
- raise "Invalid data_option[#{item.inspect}] param #{data_option[item]}" if data_option[item].to_s !~ /^\d+?$/
- }
- end
- if data_type == 'select' || data_type == 'checkbox'
- raise 'Need data_option[:default] param' if !data_option.key?(:default)
- raise 'Invalid data_option[:options] or data_option[:relation] param' if data_option[:options].nil? && data_option[:relation].nil?
- if !data_option.key?(:maxlength)
- data_option[:maxlength] = 255
- end
- if !data_option.key?(:nulloption)
- data_option[:nulloption] = true
- end
- end
- if data_type == 'boolean'
- raise 'Need data_option[:default] param true|false|undefined' if !data_option.key?(:default)
- raise 'Invalid data_option[:options] param' if data_option[:options].nil?
- end
- if data_type == 'datetime'
- raise 'Need data_option[:future] param true|false' if data_option[:future].nil?
- raise 'Need data_option[:past] param true|false' if data_option[:past].nil?
- raise 'Need data_option[:diff] param in hours' if data_option[:diff].nil?
- end
- if data_type == 'date'
- raise 'Need data_option[:future] param true|false' if data_option[:future].nil?
- raise 'Need data_option[:past] param true|false' if data_option[:past].nil?
- raise 'Need data_option[:diff] param in days' if data_option[:diff].nil?
- end
- end
- end
|