module CustomAttributes module ActsAsCustomizable extend ActiveSupport::Concern included do end module ClassMethods def acts_as_customizable(options = {}) cattr_accessor :customizable_options self.customizable_options = options has_many :custom_values, -> { includes(:custom_field).order("#{CustomField.table_name}.position") }, as: :customizable, inverse_of: :customizable, dependent: :delete_all, validate: false include CustomAttributes::ActsAsCustomizable::InstanceMethods validate :validate_custom_field_values after_save :save_custom_field_values end # Helper function to index custom values # Use this in mapping context and pass `self` as parameter def index_custom_values(context) return unless context.class.name == 'Elasticsearch::Model::Indexing::Mappings' context.indexes :visible_in_search, type: 'boolean' context.indexes :custom_values, type: 'nested' do context.indexes :value, type: 'text', fields: { raw: { type: 'keyword' } } context.indexes :custom_field_id, type: 'integer' end end def available_custom_fields CustomField.where("model_type = '#{self.name}CustomField'").sorted.to_a end def default_fields [] end end module InstanceMethods # Set JSON representation in elasticsearch. # Default is to decorate the models JSON presentation with custom values def use_custom_value_json(hash = {}) to_json = { methods: :visible_in_search, include: { custom_values: { only: %i[custom_field_id value] } } }.merge(hash) as_json( to_json ) end # Helper function to access available custom fields from an instance def available_custom_fields self.class.available_custom_fields end # Override this to have control over entity visibility in search. # Entities that return false here will be filtered out by default. def visible_in_search true end # Sets the values of the object's custom fields # values is an array like [{'id' => 1, 'value' => 'foo'}, {'id' => 2, 'value' => 'bar'}] def assign_custom_values=(values) values_to_hash = values.each_with_object({}) do |v, hash| v = v.stringify_keys hash[v['id']] = v['value'] if v['id'] && v.key?('value') end self.custom_field_values = values_to_hash end # Sets the values of the object's custom fields # values is a hash like {'1' => 'foo', 2 => 'bar'} def custom_field_values=(values) values = values.stringify_keys custom_field_values.each do |custom_field_value| key = custom_field_value.custom_field_id.to_s slug = custom_field_value.custom_field_slug if values.key?(key) custom_field_value.value = values[key] elsif values.key?(slug) custom_field_value.value = values[slug] end end @custom_field_values_changed = true end # Accessor for custom fields, returns array of CustomFieldValues def custom_field_values @custom_field_values ||= available_custom_fields.collect do |field| populate_custom_field_value(field) end end def populated_custom_field_value(c) field_id = (c.is_a?(CustomField) ? c.id : c.to_i) field = available_custom_fields.select { |field| field.id == field_id }.first return populate_custom_field_value(field) unless field.nil? CustomAttributes::CustomFieldValue.new end def populate_custom_field_value(field) x = CustomAttributes::CustomFieldValue.new x.custom_field = field x.customizable = self if field.multiple? values = custom_values.select { |v| v.custom_field == field } if values.empty? values << custom_values.build(customizable: self, custom_field: field) end x.instance_variable_set('@value', values.map(&:value)) else cv = custom_values.detect { |v| v.custom_field == field } cv ||= custom_values.build(customizable: self, custom_field: field) x.instance_variable_set('@value', cv.value) end x.value_was = x.value.dup if x.value x end def visible_custom_field_values custom_field_values.select(&:visible?) end def custom_field_values_changed? @custom_field_values_changed == true end # Returns a CustomValue object for the passed CustomField object or ID def custom_value_for(c) field_id = (c.is_a?(CustomField) ? c.id : c.to_i) custom_values.detect { |v| v.custom_field_id == field_id } end # Returns the value for the passed CustomField object or ID def custom_field_value(c) field_id = (c.is_a?(CustomField) ? c.id : c.to_i) custom_field_values.detect { |v| v.custom_field_id == field_id }.try(:value) end # Extends model validation # 1. Calls .validate_value on each CustomFieldValue # 2. .validate_value calls .validate_custom_value on the CustomField, # 3. which calls the validate_custom_value method on the assigned FieldType. # # The FieldType is therefor responsible for CustomValue validation. def validate_custom_field_values if new_record? || custom_field_values_changed? custom_field_values.each(&:validate_value) end end # Called *after* save of the extended model, so validation should already be over. # This method is responsible for persisting the values that have been written to # the CustomFieldValues and handle and save CustomValues correctly. def save_custom_field_values target_custom_values = [] custom_field_values.each do |custom_field_value| if custom_field_value.value.is_a?(Array) custom_field_value.value.each do |v| target = custom_values.detect { |cv| cv.custom_field == custom_field_value.custom_field && cv.value == v } target ||= custom_values.build(customizable: self, custom_field: custom_field_value.custom_field, value: v) target_custom_values << target end else target = custom_values.detect { |cv| cv.custom_field == custom_field_value.custom_field } target ||= custom_values.build(customizable: self, custom_field: custom_field_value.custom_field) target.value = custom_field_value.value target_custom_values << target end end self.custom_values = target_custom_values custom_values.each(&:save) @custom_field_values_changed = false true end def reassign_custom_field_values if @custom_field_values values = @custom_field_values.each_with_object({}) { |v, h| h[v.custom_field_id] = v.value; } @custom_field_values = nil self.custom_field_values = values end end def reset_custom_values! @custom_field_values = nil @custom_field_values_changed = true end def reload(*args) @custom_field_values = nil @custom_field_values_changed = false super end end end end