module CustomAttributes module ActsAsCustomField extend ActiveSupport::Concern included do end module ClassMethods def acts_as_custom_field(_options = {}) include CustomAttributes::ActsAsCustomField::InstanceMethods scope :sorted, -> { order(:position) } serialize :possible_values attr_accessor :edit_tag_style validates_numericality_of :min_length, only_integer: true, greater_than_or_equal_to: 0 validates_numericality_of :max_length, only_integer: true, greater_than_or_equal_to: 0 validates_presence_of :name, :field_type validates_uniqueness_of :name, scope: :model_type validates_length_of :name, maximum: 30 validates_inclusion_of :field_type, in: proc { CustomAttributes::FieldType.available_types.map { |ft| ft.name.gsub(/CustomAttributes::|FieldType/, '') } } validate :validate_custom_field before_create do |field| field.set_slug field.ensure_position_integrity field.set_position end before_save do |field| field.type.before_custom_field_save(field) end end end module InstanceMethods def type @type ||= CustomAttributes::FieldType.find(field_type) end # Called upon Customizable Model validation # Actual validation handled by FieldType def validate_custom_value(custom_value) value = custom_value.value errs = type.validate_custom_value(custom_value) unless errs.any? if value.is_a?(Array) errs << ::I18n.t('activerecord.errors.messages.invalid') unless multiple? if is_required? && value.detect(&:present?).nil? errs << ::I18n.t('activerecord.errors.messages.blank') end else if is_required? && value.blank? errs << ::I18n.t('activerecord.errors.messages.blank') end end end errs end # Returns possible *options* that are determined by the *FieldType* # Don't mistake for possible_values, which is a CustomField specific dynamic setting def possible_custom_value_options(custom_value) type.possible_custom_value_options(custom_value) end # Validate the CustomField according to type rules and check if the selected default value # is indeed a valid value for this field def validate_custom_field if type.nil? errors.add :default, ::I18n.t('activerecord.errors.messages.invalid_type') return end type.validate_custom_field(self).each do |attribute, message| errors.add attribute, message end if default.present? validate_field_value(default).each do |message| errors.add :default, message end end if position.present? && self.class.where(position: position, model_type: model_type).where.not(id: id).count > 0 errors.add :position, ::I18n.t('activerecord.errors.messages.invalid_position') end end # Helper function used in validate_custom_field def validate_field_value(value) validate_custom_value(CustomAttributes::CustomFieldValue.new(custom_field: self, value: value)) end # Helper function to check if a value is a valid value for this field def valid_field_value?(value) validate_field_value(value).empty? end # Used to set the value of CustomFieldValue. No database persistance happening. # A convenient way to override how values are being parsed via FieldType def set_custom_field_value(custom_field_value, value) type.set_custom_field_value(self, custom_field_value, value) end # Called after CustomValue has been saved # Overrideable through FieldType def after_save_custom_value(custom_value) type.after_save_custom_value(self, custom_value) end # Serializer for possible values attribute def possible_values values = read_attribute(:possible_values) if values.is_a?(Array) values.each do |value| value.to_s.force_encoding('UTF-8') end values else [] end end # Returns the value in type specific form (Integer, Float, ...) def cast_value(value) type.cast_value(self, value) end # Finds a value in a field that has predefined possible values. # Returns array of values if the field supports multiple values # Comma delimited keywords possible def value_from_keyword(keyword, customized) type.value_from_keyword(self, keyword, customized) end def field_type=(arg) # cannot change type of a saved custom field if new_record? @type = nil super end end def possible_values=(arg) if arg.is_a?(Array) values = arg.compact.map { |a| a.to_s.strip }.reject(&:blank?) write_attribute(:possible_values, values) else self.possible_values = arg.to_s.split(/[\n\r]+/) end end def decrement_position change_position_by(-1) end def increment_position change_position_by(1) end def customizable_class model_type.gsub('CustomField', '').constantize rescue NameError false end protected def set_slug self.slug = create_slug end def set_position return unless position.nil? last_position = self.class.where(model_type: model_type).order(position: :desc).first.try(:position) self.position = 1 self.position = last_position + 1 unless last_position.nil? end def change_position_by(diff) new_pos = position + diff unless position.nil? swap_field = self.class.find_by(model_type: model_type, position: new_pos) if swap_field.present? swap_field.position = position swap_field.save(validate: false) self.position = new_pos save end end def create_slug(iterator = 0) new_slug = name.strip.gsub(/([^A-Za-z0-9])+/) { '_' }.downcase new_slug = "#{new_slug}_#{iterator}" unless iterator == 0 new_slug = create_slug(iterator += 1) unless CustomField.where(slug: new_slug).count == 0 new_slug end def ensure_position_integrity expected_position = 1 self.class.where(model_type: model_type).order(position: :asc).each do |field| if expected_position != field.position field.position = expected_position field.save end expected_position += 1 end end end end end