# Copyright (c) 2008-2012 Phusion # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. module DefaultValueFor class NormalValueContainer def initialize(value) @value = value end def evaluate(instance) if @value.duplicable? return @value.dup else return @value end end end class BlockValueContainer def initialize(block) @block = block end def evaluate(instance) if @block.arity == 0 return @block.call else return @block.call(instance) end end end module ClassMethods # Declares a default value for the given attribute. # # Sets the default value to the given options parameter unless the given options equal { :value => ... } # # The options can be used to specify the following things: # * value - Sets the default value. # * allows_nil (default: true) - Sets explicitly passed nil values if option is set to true. def default_value_for(attribute, options = {}, &block) value = options allows_nil = true if options.is_a?(Hash) opts = options.stringify_keys value = opts.fetch('value', options) allows_nil = opts.fetch('allows_nil', true) end if !method_defined?(:set_default_values) include(InstanceMethods) after_initialize :set_default_values class_attribute :_default_attribute_values class_attribute :_default_attribute_values_not_allowing_nil extend(DelayedClassMethods) init_hash = true else init_hash = !singleton_methods(false).include?(:_default_attribute_values) end if init_hash self._default_attribute_values = {} self._default_attribute_values_not_allowing_nil = [] end if block_given? container = BlockValueContainer.new(block) else container = NormalValueContainer.new(value) end _default_attribute_values[attribute.to_s] = container _default_attribute_values_not_allowing_nil << attribute.to_s unless allows_nil end def default_values(values) values.each_pair do |key, options| options = options.stringify_keys if options.is_a?(Hash) value = options.is_a?(Hash) && options.has_key?('value') ? options['value'] : options if value.kind_of? Proc default_value_for(key, options.is_a?(Hash) ? options : {}, &value) else default_value_for(key, options) end end end end module DelayedClassMethods def _all_default_attribute_values return _default_attribute_values unless superclass.respond_to?(:_default_attribute_values) superclass._all_default_attribute_values.merge(_default_attribute_values) end def _all_default_attribute_values_not_allowing_nil return _default_attribute_values_not_allowing_nil unless superclass.respond_to?(:_default_attribute_values_not_allowing_nil) result = superclass._all_default_attribute_values_not_allowing_nil + _default_attribute_values_not_allowing_nil result.uniq! result end end module InstanceMethods def initialize(attributes = nil, options = {}) attributes = attributes.to_h if attributes.is_a?(ActionController::Parameters) @initialization_attributes = attributes.is_a?(Hash) ? attributes.stringify_keys : {} unless options[:without_protection] if respond_to?(:mass_assignment_options, true) && options.has_key?(:as) @initialization_attributes = sanitize_for_mass_assignment(@initialization_attributes, options[:as]) elsif respond_to?(:sanitize_for_mass_assignment, true) @initialization_attributes = sanitize_for_mass_assignment(@initialization_attributes) else @initialization_attributes = remove_attributes_protected_from_mass_assignment(@initialization_attributes) end end if self.class.respond_to? :protected_attributes super(attributes, options) else super(attributes) end end def attributes_for_create(attribute_names) attribute_names += self.class._all_default_attribute_values.keys.map(&:to_s).find_all { |name| self.class.columns_hash.key?(name) } super end def set_default_values self.class._all_default_attribute_values.each do |attribute, container| next unless new_record? || self.class._all_default_attribute_values_not_allowing_nil.include?(attribute) connection_default_value_defined = new_record? && respond_to?("#{attribute}_changed?") && !__send__("#{attribute}_changed?") attribute_blank = if attributes.has_key?(attribute) column = self.class.columns_hash[attribute] if column && column.type == :boolean attributes[attribute].nil? else attributes[attribute].blank? end elsif respond_to?(attribute) send(attribute).nil? else instance_variable_get("@#{attribute}").nil? end next unless connection_default_value_defined || attribute_blank # allow explicitly setting nil through allow nil option next if @initialization_attributes.is_a?(Hash) && ( @initialization_attributes.has_key?(attribute) || ( @initialization_attributes.has_key?("#{attribute}_attributes") && nested_attributes_options.stringify_keys[attribute] ) ) && !self.class._all_default_attribute_values_not_allowing_nil.include?(attribute) __send__("#{attribute}=", container.evaluate(self)) if respond_to?(:clear_attribute_changes, true) clear_attribute_changes [attribute] if has_attribute?(attribute) else changed_attributes.delete(attribute) end end end end end if defined?(Rails::Railtie) require 'default_value_for/railtie' else # For anybody is using AS and AR without Railties, i.e. Padrino. ActiveRecord::Base.extend(DefaultValueFor::ClassMethods) end