2019-05-02 18:17:27 -04:00
# frozen_string_literal: true
2014-04-28 04:31:51 -04:00
module HasCustomFields
extend ActiveSupport :: Concern
2014-08-18 07:04:08 -04:00
2014-06-16 22:42:12 -04:00
module Helpers
2014-08-18 07:04:08 -04:00
2014-06-16 22:42:12 -04:00
def self . append_field ( target , key , value , types )
if target . has_key? ( key )
target [ key ] = [ target [ key ] ] if ! target [ key ] . is_a? Array
2018-05-22 02:48:39 -04:00
target [ key ] << cast_custom_field ( key , value , types , _return_array = false )
2014-06-16 22:42:12 -04:00
else
target [ key ] = cast_custom_field ( key , value , types )
end
end
2015-04-23 13:33:29 -04:00
CUSTOM_FIELD_TRUE || = [ '1' , 't' , 'true' , 'T' , 'True' , 'TRUE' ] . freeze
def self . get_custom_field_type ( types , key )
return unless types
sorted_types = types . keys . select { | k | k . end_with? ( " * " ) }
. sort_by ( & :length )
. reverse
sorted_types . each do | t |
return types [ t ] if key =~ / ^ #{ t } /i
end
types [ key ]
end
2014-06-16 22:42:12 -04:00
2018-05-22 02:48:39 -04:00
def self . cast_custom_field ( key , value , types , return_array = true )
2015-04-23 13:33:29 -04:00
return value unless type = get_custom_field_type ( types , key )
2014-06-16 22:42:12 -04:00
2018-05-22 02:48:39 -04:00
array = nil
if Array === type
type = type [ 0 ]
array = true if return_array
2014-06-16 22:42:12 -04:00
end
2018-05-22 02:48:39 -04:00
result =
case type
when :boolean then ! ! CUSTOM_FIELD_TRUE . include? ( value )
when :integer then value . to_i
2018-09-13 03:59:17 -04:00
when :json then parse_json_value ( value , key )
2018-05-22 02:48:39 -04:00
else
value
end
array ? [ result ] : result
2014-06-16 22:42:12 -04:00
end
2018-09-13 03:59:17 -04:00
def self . parse_json_value ( value , key )
:: JSON . parse ( value )
rescue JSON :: ParserError
Rails . logger . warn ( " Value ' #{ value } ' for custom field ' #{ key } ' is not json, it is being ignored. " )
{ }
end
2014-06-16 22:42:12 -04:00
end
2014-04-28 04:31:51 -04:00
included do
has_many :_custom_fields , dependent : :destroy , class_name : " #{ name } CustomField "
after_save :save_custom_fields
2014-05-14 14:38:04 -04:00
2022-01-20 23:29:51 -05:00
# TODO (martin) Post 2.8 release, change to attr_reader because this is
# set by set_preloaded_custom_fields
2015-08-05 02:01:52 -04:00
attr_accessor :preloaded_custom_fields
2019-07-06 14:42:03 -04:00
def custom_fields_fk
@custom_fields_fk || = " #{ _custom_fields . reflect_on_all_associations ( :belongs_to ) [ 0 ] . name } _id "
end
2015-04-23 13:33:29 -04:00
# To avoid n+1 queries, use this function to retrieve lots of custom fields in one go
# and create a "sideloaded" version for easy querying by id.
2020-07-26 20:23:54 -04:00
def self . custom_fields_for_ids ( ids , allowed_fields )
2014-05-14 14:38:04 -04:00
klass = " #{ name } CustomField " . constantize
foreign_key = " #{ name . underscore } _id " . to_sym
result = { }
2020-07-26 20:23:54 -04:00
return result if allowed_fields . blank?
2015-04-23 13:33:29 -04:00
2020-07-26 20:23:54 -04:00
klass . where ( foreign_key = > ids , :name = > allowed_fields )
2015-04-23 13:33:29 -04:00
. pluck ( foreign_key , :name , :value ) . each do | cf |
2014-05-14 14:38:04 -04:00
result [ cf [ 0 ] ] || = { }
2014-06-16 22:42:12 -04:00
append_custom_field ( result [ cf [ 0 ] ] , cf [ 1 ] , cf [ 2 ] )
2014-05-14 14:38:04 -04:00
end
2015-04-23 13:33:29 -04:00
2014-05-14 14:38:04 -04:00
result
end
2014-06-16 22:42:12 -04:00
def self . append_custom_field ( target , key , value )
2015-04-23 13:33:29 -04:00
HasCustomFields :: Helpers . append_field ( target , key , value , @custom_field_types )
2014-05-14 14:38:04 -04:00
end
2014-06-16 22:42:12 -04:00
def self . register_custom_field_type ( name , type )
@custom_field_types || = { }
@custom_field_types [ name ] = type
end
2018-09-13 03:59:17 -04:00
def self . get_custom_field_type ( name )
@custom_field_types || = { }
@custom_field_types [ name ]
end
2015-08-05 02:01:52 -04:00
def self . preload_custom_fields ( objects , fields )
if objects . present?
map = { }
empty = { }
fields . each do | field |
empty [ field ] = nil
end
objects . each do | obj |
map [ obj . id ] = obj
2022-01-20 23:29:51 -05:00
obj . set_preloaded_custom_fields ( empty . dup )
2015-08-05 02:01:52 -04:00
end
fk = ( name . underscore << " _id " )
2015-08-05 02:09:10 -04:00
" #{ name } CustomField " . constantize
. where ( " #{ fk } in (?) " , map . keys )
. where ( " name in (?) " , fields )
2015-08-05 02:01:52 -04:00
. pluck ( fk , :name , :value ) . each do | id , name , value |
preloaded = map [ id ] . preloaded_custom_fields
if preloaded [ name ] . nil?
preloaded . delete ( name )
end
HasCustomFields :: Helpers . append_field ( preloaded , name , value , @custom_field_types )
end
end
end
2014-04-28 04:31:51 -04:00
end
2014-04-29 13:23:13 -04:00
def reload ( options = nil )
2015-05-06 12:52:09 -04:00
clear_custom_fields
super
end
2021-06-23 03:21:11 -04:00
def on_custom_fields_change
# Callback when custom fields have changed
# Override in model
end
2020-03-03 08:56:54 -05:00
def custom_fields_preloaded?
! ! @preloaded_custom_fields
end
2015-08-05 02:01:52 -04:00
def custom_field_preloaded? ( name )
@preloaded_custom_fields && @preloaded_custom_fields . key? ( name )
end
2015-05-06 12:52:09 -04:00
def clear_custom_fields
2014-04-29 13:23:13 -04:00
@custom_fields = nil
@custom_fields_orig = nil
end
2022-01-20 22:21:13 -05:00
class NotPreloadedError < StandardError ; end
2015-08-05 02:01:52 -04:00
class PreloadedProxy
2022-01-20 22:21:13 -05:00
def initialize ( preloaded , klass_with_custom_fields )
2015-08-05 02:01:52 -04:00
@preloaded = preloaded
2022-01-20 22:21:13 -05:00
@klass_with_custom_fields = klass_with_custom_fields
2015-08-05 02:01:52 -04:00
end
def [] ( key )
if @preloaded . key? ( key )
@preloaded [ key ]
else
# for now you can not mix preload an non preload, it better just to fail
2022-01-20 22:21:13 -05:00
raise NotPreloadedError , " Attempted to access the non preloaded custom field ' #{ key } ' on the ' #{ @klass_with_custom_fields } ' class. This is disallowed to prevent N+1 queries. "
2015-08-05 02:01:52 -04:00
end
end
end
2022-01-20 23:29:51 -05:00
def set_preloaded_custom_fields ( custom_fields )
@preloaded_custom_fields = custom_fields
# we have to clear this otherwise the fields are cached inside the
# already existing proxy and no new ones are added, so when we check
# for custom_fields[KEY] an error is likely to occur
@preloaded_proxy = nil
end
2014-04-28 04:31:51 -04:00
def custom_fields
2015-08-05 02:01:52 -04:00
if @preloaded_custom_fields
2022-01-20 22:21:13 -05:00
return @preloaded_proxy || = PreloadedProxy . new ( @preloaded_custom_fields , self . class . to_s )
2015-08-05 02:01:52 -04:00
end
2014-04-28 04:31:51 -04:00
@custom_fields || = refresh_custom_fields_from_db . dup
end
def custom_fields = ( data )
custom_fields . replace ( data )
end
def custom_fields_clean?
2015-04-23 13:33:29 -04:00
# Check whether the cached version has been changed on this model
2014-04-28 04:31:51 -04:00
! @custom_fields || @custom_fields_orig == @custom_fields
end
2018-03-02 12:45:34 -05:00
# `upsert_custom_fields` will only insert/update existing fields, and will not
# delete anything. It is safer under concurrency and is recommended when
# you just want to attach fields to things without maintaining a specific
# set of fields.
def upsert_custom_fields ( fields )
fields . each do | k , v |
row_count = _custom_fields . where ( name : k ) . update_all ( value : v )
if row_count == 0
_custom_fields . create! ( name : k , value : v )
end
2021-06-23 03:21:11 -04:00
2019-10-29 14:34:28 -04:00
custom_fields [ k . to_s ] = v # We normalize custom_fields as strings
2018-03-02 12:45:34 -05:00
end
2021-06-23 03:21:11 -04:00
on_custom_fields_change
2018-03-02 12:45:34 -05:00
end
2015-04-25 18:12:19 -04:00
def save_custom_fields ( force = false )
if force || ! custom_fields_clean?
2020-08-20 05:10:24 -04:00
dup = @custom_fields . dup . with_indifferent_access
2014-04-28 04:31:51 -04:00
array_fields = { }
2018-10-18 00:23:04 -04:00
ActiveRecord :: Base . transaction do
_custom_fields . reload . each do | f |
if dup [ f . name ] . is_a? ( Array )
# we need to collect Arrays fully before we can compare them
if ! array_fields . has_key? ( f . name )
array_fields [ f . name ] = [ f ]
else
array_fields [ f . name ] << f
end
elsif dup [ f . name ] . is_a? ( Hash )
if dup [ f . name ] . to_json != f . value
f . destroy!
else
dup . delete ( f . name )
end
2015-04-25 18:12:19 -04:00
else
2018-10-18 00:23:04 -04:00
t = { }
self . class . append_custom_field ( t , f . name , f . value )
2017-08-16 17:04:40 -04:00
2021-06-25 05:34:51 -04:00
if dup . has_key? ( f . name ) && dup [ f . name ] == t [ f . name ]
2018-10-18 00:23:04 -04:00
dup . delete ( f . name )
2021-06-25 05:34:51 -04:00
else
f . destroy!
2018-10-18 00:23:04 -04:00
end
2014-04-28 04:31:51 -04:00
end
end
2018-10-18 00:23:04 -04:00
# let's iterate through our arrays and compare them
array_fields . each do | field_name , fields |
if fields . length == dup [ field_name ] . length && fields . map ( & :value ) == dup [ field_name ]
dup . delete ( field_name )
else
fields . each ( & :destroy! )
end
2014-04-28 04:31:51 -04:00
end
2018-10-18 00:23:04 -04:00
dup . each do | k , v |
field_type = self . class . get_custom_field_type ( k )
2018-09-13 03:59:17 -04:00
2018-10-18 00:23:04 -04:00
if v . is_a? ( Array ) && field_type != :json
v . each { | subv | _custom_fields . create! ( name : k , value : subv ) }
else
2019-07-04 13:25:09 -04:00
create_singular ( k , v , field_type )
2018-10-18 00:23:04 -04:00
end
2014-04-28 04:31:51 -04:00
end
end
2021-06-23 03:21:11 -04:00
on_custom_fields_change
2014-04-28 04:31:51 -04:00
refresh_custom_fields_from_db
end
end
2015-01-29 17:17:32 -05:00
2021-05-20 21:43:47 -04:00
# We support unique indexes on certain fields. In the event two concurrent processes attempt to
2019-07-06 14:42:03 -04:00
# update the same custom field we should catch the error and perform an update instead.
2019-07-04 13:25:09 -04:00
def create_singular ( name , value , field_type = nil )
write_value = value . is_a? ( Hash ) || field_type == :json ? value . to_json : value
2019-07-06 14:42:03 -04:00
write_value = 't' if write_value . is_a? ( TrueClass )
write_value = 'f' if write_value . is_a? ( FalseClass )
2019-07-06 15:14:07 -04:00
row_count = DB . exec ( << ~ SQL , name : name , value : write_value , id : id , now : Time . zone . now )
2019-07-06 14:42:03 -04:00
INSERT INTO #{_custom_fields.table_name} (#{custom_fields_fk}, name, value, created_at, updated_at)
2019-07-06 15:14:07 -04:00
VALUES ( :id , :name , :value , :now , :now )
2019-07-06 14:42:03 -04:00
ON CONFLICT DO NOTHING
SQL
_custom_fields . where ( name : name ) . update_all ( value : write_value ) if row_count == 0
2019-07-04 13:25:09 -04:00
end
protected
2015-01-29 17:17:32 -05:00
def refresh_custom_fields_from_db
2020-08-25 02:09:34 -04:00
target = HashWithIndifferentAccess . new
2017-11-15 23:13:58 -05:00
_custom_fields . order ( 'id asc' ) . pluck ( :name , :value ) . each do | key , value |
2015-01-29 17:17:32 -05:00
self . class . append_custom_field ( target , key , value )
end
@custom_fields_orig = target
@custom_fields = @custom_fields_orig . dup
end
2014-05-14 14:38:04 -04:00
end