/app/models/concerns/has_custom_fields.rb

https://gitlab.com/Ruwan-Ranganath/discourse · Ruby · 222 lines · 170 code · 46 blank · 6 comment · 24 complexity · 79d75932f88d8227f09f54bdd6302351 MD5 · raw file

  1. module HasCustomFields
  2. extend ActiveSupport::Concern
  3. module Helpers
  4. def self.append_field(target, key, value, types)
  5. if target.has_key?(key)
  6. target[key] = [target[key]] if !target[key].is_a? Array
  7. target[key] << cast_custom_field(key, value, types)
  8. else
  9. target[key] = cast_custom_field(key, value, types)
  10. end
  11. end
  12. CUSTOM_FIELD_TRUE ||= ['1', 't', 'true', 'T', 'True', 'TRUE'].freeze
  13. def self.get_custom_field_type(types, key)
  14. return unless types
  15. sorted_types = types.keys.select { |k| k.end_with?("*") }
  16. .sort_by(&:length)
  17. .reverse
  18. sorted_types.each do |t|
  19. return types[t] if key =~ /^#{t}/i
  20. end
  21. types[key]
  22. end
  23. def self.cast_custom_field(key, value, types)
  24. return value unless type = get_custom_field_type(types, key)
  25. case type
  26. when :boolean then !!CUSTOM_FIELD_TRUE.include?(value)
  27. when :integer then value.to_i
  28. when :json then ::JSON.parse(value)
  29. else
  30. value
  31. end
  32. end
  33. end
  34. included do
  35. has_many :_custom_fields, dependent: :destroy, :class_name => "#{name}CustomField"
  36. after_save :save_custom_fields
  37. attr_accessor :preloaded_custom_fields
  38. # To avoid n+1 queries, use this function to retrieve lots of custom fields in one go
  39. # and create a "sideloaded" version for easy querying by id.
  40. def self.custom_fields_for_ids(ids, whitelisted_fields)
  41. klass = "#{name}CustomField".constantize
  42. foreign_key = "#{name.underscore}_id".to_sym
  43. result = {}
  44. return result if whitelisted_fields.blank?
  45. klass.where(foreign_key => ids, :name => whitelisted_fields)
  46. .pluck(foreign_key, :name, :value).each do |cf|
  47. result[cf[0]] ||= {}
  48. append_custom_field(result[cf[0]], cf[1], cf[2])
  49. end
  50. result
  51. end
  52. def self.append_custom_field(target, key, value)
  53. HasCustomFields::Helpers.append_field(target, key, value, @custom_field_types)
  54. end
  55. def self.register_custom_field_type(name, type)
  56. @custom_field_types ||= {}
  57. @custom_field_types[name] = type
  58. end
  59. def self.preload_custom_fields(objects, fields)
  60. if objects.present?
  61. map = {}
  62. empty = {}
  63. fields.each do |field|
  64. empty[field] = nil
  65. end
  66. objects.each do |obj|
  67. map[obj.id] = obj
  68. obj.preloaded_custom_fields = empty.dup
  69. end
  70. fk = (name.underscore << "_id")
  71. "#{name}CustomField".constantize
  72. .where("#{fk} in (?)", map.keys)
  73. .where("name in (?)", fields)
  74. .pluck(fk, :name, :value).each do |id, name, value|
  75. preloaded = map[id].preloaded_custom_fields
  76. if preloaded[name].nil?
  77. preloaded.delete(name)
  78. end
  79. HasCustomFields::Helpers.append_field(preloaded, name, value, @custom_field_types)
  80. end
  81. end
  82. end
  83. end
  84. def reload(options = nil)
  85. clear_custom_fields
  86. super
  87. end
  88. def custom_field_preloaded?(name)
  89. @preloaded_custom_fields && @preloaded_custom_fields.key?(name)
  90. end
  91. def clear_custom_fields
  92. @custom_fields = nil
  93. @custom_fields_orig = nil
  94. end
  95. class PreloadedProxy
  96. def initialize(preloaded)
  97. @preloaded = preloaded
  98. end
  99. def [](key)
  100. if @preloaded.key?(key)
  101. @preloaded[key]
  102. else
  103. # for now you can not mix preload an non preload, it better just to fail
  104. raise StandardError, "Attempting to access a non preloaded custom field, this is disallowed to prevent N+1 queries."
  105. end
  106. end
  107. end
  108. def custom_fields
  109. if @preloaded_custom_fields
  110. return @preloaded_proxy ||= PreloadedProxy.new(@preloaded_custom_fields)
  111. end
  112. @custom_fields ||= refresh_custom_fields_from_db.dup
  113. end
  114. def custom_fields=(data)
  115. custom_fields.replace(data)
  116. end
  117. def custom_fields_clean?
  118. # Check whether the cached version has been changed on this model
  119. !@custom_fields || @custom_fields_orig == @custom_fields
  120. end
  121. def save_custom_fields(force=false)
  122. if force || !custom_fields_clean?
  123. dup = @custom_fields.dup
  124. array_fields = {}
  125. _custom_fields.each do |f|
  126. if dup[f.name].is_a? Array
  127. # we need to collect Arrays fully before we can compare them
  128. if !array_fields.has_key?(f.name)
  129. array_fields[f.name] = [f]
  130. else
  131. array_fields[f.name] << f
  132. end
  133. elsif dup[f.name].is_a? Hash
  134. if dup[f.name].to_json != f.value
  135. f.destroy
  136. else
  137. dup.delete(f.name)
  138. end
  139. else
  140. if dup[f.name] != f.value
  141. f.destroy
  142. else
  143. dup.delete(f.name)
  144. end
  145. end
  146. end
  147. # let's iterate through our arrays and compare them
  148. array_fields.each do |field_name, fields|
  149. if fields.length == dup[field_name].length && fields.map(&:value) == dup[field_name]
  150. dup.delete(field_name)
  151. else
  152. fields.each(&:destroy)
  153. end
  154. end
  155. dup.each do |k,v|
  156. if v.is_a? Array
  157. v.each { |subv| _custom_fields.create(name: k, value: subv) }
  158. elsif v.is_a? Hash
  159. _custom_fields.create(name: k, value: v.to_json)
  160. else
  161. _custom_fields.create(name: k, value: v)
  162. end
  163. end
  164. refresh_custom_fields_from_db
  165. end
  166. end
  167. protected
  168. def refresh_custom_fields_from_db
  169. target = Hash.new
  170. _custom_fields.pluck(:name,:value).each do |key, value|
  171. self.class.append_custom_field(target, key, value)
  172. end
  173. @custom_fields_orig = target
  174. @custom_fields = @custom_fields_orig.dup
  175. end
  176. end