├── .gitignore ├── Gemfile ├── gemfiles ├── rails_41.gemfile ├── rails_42.gemfile ├── rails_50.gemfile ├── rails_51.gemfile ├── rails_52.gemfile ├── rails_32.gemfile └── rails_40.gemfile ├── Rakefile ├── Appraisals ├── LICENSE.TXT ├── init.rb ├── default_value_for.gemspec ├── lib ├── default_value_for │ └── railtie.rb └── default_value_for.rb ├── test.rb └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.lock 3 | *.gem 4 | gemfiles/.bundle 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | platforms :jruby do 6 | gem 'activerecord-jdbcsqlite3-adapter', '~> 1.3.3' 7 | gem 'jruby-openssl', '~> 0.9.4' 8 | end 9 | 10 | platforms :ruby do 11 | gem 'sqlite3', '~> 1.3.8' 12 | end 13 | -------------------------------------------------------------------------------- /gemfiles/rails_41.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 4.1.8" 6 | gem "actionpack", "~> 4.1.8" 7 | 8 | platforms :jruby do 9 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.3" 10 | gem "jruby-openssl", "~> 0.9.4" 11 | end 12 | 13 | platforms :ruby do 14 | gem "sqlite3", "~> 1.3.8" 15 | end 16 | 17 | gemspec path: "../" 18 | -------------------------------------------------------------------------------- /gemfiles/rails_42.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 4.2.0" 6 | gem "actionpack", "~> 4.2.0" 7 | 8 | platforms :jruby do 9 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.3" 10 | gem "jruby-openssl", "~> 0.9.4" 11 | end 12 | 13 | platforms :ruby do 14 | gem "sqlite3", "~> 1.3.8" 15 | end 16 | 17 | gemspec path: "../" 18 | -------------------------------------------------------------------------------- /gemfiles/rails_50.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 5.0.0" 6 | gem "actionpack", "~> 5.0.0" 7 | 8 | platforms :jruby do 9 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.3" 10 | gem "jruby-openssl", "~> 0.9.4" 11 | end 12 | 13 | platforms :ruby do 14 | gem "sqlite3", "~> 1.3.8" 15 | end 16 | 17 | gemspec path: "../" 18 | -------------------------------------------------------------------------------- /gemfiles/rails_51.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 5.1.0" 6 | gem "actionpack", "~> 5.1.0" 7 | 8 | platforms :jruby do 9 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.3" 10 | gem "jruby-openssl", "~> 0.9.4" 11 | end 12 | 13 | platforms :ruby do 14 | gem "sqlite3", "~> 1.3.8" 15 | end 16 | 17 | gemspec path: "../" 18 | -------------------------------------------------------------------------------- /gemfiles/rails_52.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 5.2.0" 6 | gem "actionpack", "~> 5.2.0" 7 | 8 | platforms :jruby do 9 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.3" 10 | gem "jruby-openssl", "~> 0.9.4" 11 | end 12 | 13 | platforms :ruby do 14 | gem "sqlite3", "~> 1.3.8" 15 | end 16 | 17 | gemspec path: "../" 18 | -------------------------------------------------------------------------------- /gemfiles/rails_32.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 3.2.21" 6 | gem "actionpack", "~> 3.2.21" 7 | 8 | platforms :jruby do 9 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.3" 10 | gem "jruby-openssl", "~> 0.9.4" 11 | end 12 | 13 | platforms :ruby do 14 | gem "sqlite3", "~> 1.3.8" 15 | end 16 | 17 | gemspec path: "../" 18 | -------------------------------------------------------------------------------- /gemfiles/rails_40.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "activerecord", "~> 4.0.12" 6 | gem "actionpack", "~> 4.0.12" 7 | 8 | platforms :jruby do 9 | gem "activerecord-jdbcsqlite3-adapter", "~> 1.3.3" 10 | gem "jruby-openssl", "~> 0.9.4" 11 | end 12 | 13 | platforms :ruby do 14 | gem "sqlite3", "~> 1.3.8" 15 | end 16 | 17 | gemspec path: "../" 18 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | task :default => :test 2 | 3 | desc "Run unit tests." 4 | task :test do 5 | ruby "test.rb" 6 | end 7 | 8 | ['3.2', '4.0', '4.1', '4.2', '5.0', '5.1', '5.2'].each do |version| 9 | dotless = version.delete('.') 10 | 11 | namespace :bundle do 12 | desc "Bundle with Rails #{version}.x" 13 | task :"rails#{dotless}" do 14 | ENV['BUNDLE_GEMFILE'] = "gemfiles/rails_#{dotless}.gemfile" 15 | sh "bundle" 16 | end 17 | end 18 | 19 | namespace :test do 20 | desc "Test with Rails #{version}.x" 21 | task :"rails#{dotless}" do 22 | ENV['BUNDLE_GEMFILE'] = "gemfiles/rails_#{dotless}.gemfile" 23 | ruby "test.rb" 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise 'rails-32' do 2 | gem 'activerecord', '~> 3.2.21' 3 | gem 'actionpack', '~> 3.2.21' 4 | end 5 | 6 | appraise 'rails-40' do 7 | gem 'activerecord', '~> 4.0.12' 8 | gem 'actionpack', '~> 4.0.12' 9 | end 10 | 11 | appraise 'rails-41' do 12 | gem 'activerecord', '~> 4.1.8' 13 | gem 'actionpack', '~> 4.1.8' 14 | end 15 | 16 | appraise 'rails-42' do 17 | gem 'activerecord', '~> 4.2.0' 18 | gem 'actionpack', '~> 4.2.0' 19 | end 20 | 21 | appraise 'rails-50' do 22 | gem 'activerecord', '~> 5.0.0' 23 | gem 'actionpack', '~> 5.0.0' 24 | end 25 | 26 | appraise 'rails-51' do 27 | gem 'activerecord', '~> 5.1.0' 28 | gem 'actionpack', '~> 5.1.0' 29 | end 30 | 31 | appraise 'rails-52' do 32 | gem 'activerecord', '~> 5.2.0' 33 | gem 'actionpack', '~> 5.2.0' 34 | end 35 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2008, 2009, 2010 Phusion 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008, 2009, 2010 Phusion 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'default_value_for' 22 | -------------------------------------------------------------------------------- /default_value_for.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = %q{default_value_for} 3 | s.version = "3.2.0" 4 | s.summary = %q{Provides a way to specify default values for ActiveRecord models} 5 | s.description = %q{The default_value_for plugin allows one to define default values for ActiveRecord models in a declarative manner} 6 | s.email = %q{software-signing@phusion.nl} 7 | s.homepage = %q{https://github.com/FooBarWidget/default_value_for} 8 | s.authors = ["Hongli Lai"] 9 | s.license = 'MIT' 10 | s.required_ruby_version = '>= 1.9.3' 11 | s.files = ['default_value_for.gemspec', 12 | 'LICENSE.TXT', 'Rakefile', 'README.md', 'test.rb', 13 | 'init.rb', 14 | 'lib/default_value_for.rb', 15 | 'lib/default_value_for/railtie.rb' 16 | ] 17 | s.add_dependency 'activerecord', '>= 3.2.0', '< 6.0' 18 | s.add_development_dependency 'railties', '>= 3.2.0', '< 6.0' 19 | s.add_development_dependency 'minitest', '>= 4.2' 20 | s.add_development_dependency 'minitest-around' 21 | s.add_development_dependency 'appraisal' 22 | s.add_development_dependency 'actionpack', '>= 3.2.0', '< 6.0' 23 | end 24 | -------------------------------------------------------------------------------- /lib/default_value_for/railtie.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008, 2009, 2010, 2011 Phusion 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | module DefaultValueFor 22 | def self.initialize_railtie 23 | ActiveSupport.on_load :active_record do 24 | DefaultValueFor.initialize_active_record_extensions 25 | end 26 | end 27 | 28 | def self.initialize_active_record_extensions 29 | ActiveRecord::Base.extend(DefaultValueFor::ClassMethods) 30 | end 31 | 32 | class Railtie < Rails::Railtie 33 | initializer 'default_value_for.insert_into_active_record' do 34 | DefaultValueFor.initialize_railtie 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/default_value_for.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2012 Phusion 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | module DefaultValueFor 22 | class NormalValueContainer 23 | def initialize(value) 24 | @value = value 25 | end 26 | 27 | def evaluate(instance) 28 | if @value.duplicable? 29 | return @value.dup 30 | else 31 | return @value 32 | end 33 | end 34 | end 35 | 36 | class BlockValueContainer 37 | def initialize(block) 38 | @block = block 39 | end 40 | 41 | def evaluate(instance) 42 | if @block.arity == 0 43 | return @block.call 44 | else 45 | return @block.call(instance) 46 | end 47 | end 48 | end 49 | 50 | module ClassMethods 51 | # Declares a default value for the given attribute. 52 | # 53 | # Sets the default value to the given options parameter unless the given options equal { :value => ... } 54 | # 55 | # The options can be used to specify the following things: 56 | # * value - Sets the default value. 57 | # * allows_nil (default: true) - Sets explicitly passed nil values if option is set to true. 58 | def default_value_for(attribute, options = {}, &block) 59 | value = options 60 | allows_nil = true 61 | 62 | if options.is_a?(Hash) 63 | opts = options.stringify_keys 64 | value = opts.fetch('value', options) 65 | allows_nil = opts.fetch('allows_nil', true) 66 | end 67 | 68 | if !method_defined?(:set_default_values) 69 | include(InstanceMethods) 70 | 71 | after_initialize :set_default_values 72 | 73 | class_attribute :_default_attribute_values 74 | class_attribute :_default_attribute_values_not_allowing_nil 75 | 76 | extend(DelayedClassMethods) 77 | init_hash = true 78 | else 79 | init_hash = !singleton_methods(false).include?(:_default_attribute_values) 80 | end 81 | 82 | if init_hash 83 | self._default_attribute_values = {} 84 | self._default_attribute_values_not_allowing_nil = [] 85 | end 86 | 87 | if block_given? 88 | container = BlockValueContainer.new(block) 89 | else 90 | container = NormalValueContainer.new(value) 91 | end 92 | _default_attribute_values[attribute.to_s] = container 93 | _default_attribute_values_not_allowing_nil << attribute.to_s unless allows_nil 94 | end 95 | 96 | def default_values(values) 97 | values.each_pair do |key, options| 98 | options = options.stringify_keys if options.is_a?(Hash) 99 | 100 | value = options.is_a?(Hash) && options.has_key?('value') ? options['value'] : options 101 | 102 | if value.kind_of? Proc 103 | default_value_for(key, options.is_a?(Hash) ? options : {}, &value) 104 | else 105 | default_value_for(key, options) 106 | end 107 | end 108 | end 109 | end 110 | 111 | module DelayedClassMethods 112 | def _all_default_attribute_values 113 | return _default_attribute_values unless superclass.respond_to?(:_default_attribute_values) 114 | superclass._all_default_attribute_values.merge(_default_attribute_values) 115 | end 116 | 117 | def _all_default_attribute_values_not_allowing_nil 118 | return _default_attribute_values_not_allowing_nil unless superclass.respond_to?(:_default_attribute_values_not_allowing_nil) 119 | result = superclass._all_default_attribute_values_not_allowing_nil + _default_attribute_values_not_allowing_nil 120 | result.uniq! 121 | result 122 | end 123 | end 124 | 125 | module InstanceMethods 126 | def initialize(attributes = nil, options = {}) 127 | attributes = attributes.to_h if attributes.respond_to?(:to_h) 128 | @initialization_attributes = attributes.is_a?(Hash) ? attributes.stringify_keys : {} 129 | 130 | unless options[:without_protection] 131 | if respond_to?(:mass_assignment_options, true) && options.has_key?(:as) 132 | @initialization_attributes = sanitize_for_mass_assignment(@initialization_attributes, options[:as]) 133 | elsif respond_to?(:sanitize_for_mass_assignment, true) 134 | @initialization_attributes = sanitize_for_mass_assignment(@initialization_attributes) 135 | else 136 | @initialization_attributes = remove_attributes_protected_from_mass_assignment(@initialization_attributes) 137 | end 138 | end 139 | 140 | if self.class.respond_to? :protected_attributes 141 | super(attributes, options) 142 | else 143 | super(attributes) 144 | end 145 | end 146 | 147 | def attributes_for_create(attribute_names) 148 | attribute_names += self.class._all_default_attribute_values.keys.map(&:to_s).find_all { |name| 149 | self.class.columns_hash.key?(name) 150 | } 151 | super 152 | end 153 | 154 | def set_default_values 155 | self.class._all_default_attribute_values.each do |attribute, container| 156 | next unless new_record? || self.class._all_default_attribute_values_not_allowing_nil.include?(attribute) 157 | 158 | connection_default_value_defined = new_record? && respond_to?("#{attribute}_changed?") && !__send__("#{attribute}_changed?") 159 | 160 | attribute_blank = if attributes.has_key?(attribute) 161 | column = self.class.columns_hash[attribute] 162 | if column && column.type == :boolean 163 | attributes[attribute].nil? 164 | else 165 | attributes[attribute].blank? 166 | end 167 | elsif respond_to?(attribute) 168 | send(attribute).nil? 169 | else 170 | instance_variable_get("@#{attribute}").nil? 171 | end 172 | next unless connection_default_value_defined || attribute_blank 173 | 174 | # allow explicitly setting nil through allow nil option 175 | next if @initialization_attributes.is_a?(Hash) && 176 | ( 177 | @initialization_attributes.has_key?(attribute) || 178 | ( 179 | @initialization_attributes.has_key?("#{attribute}_attributes") && 180 | nested_attributes_options.stringify_keys[attribute] 181 | ) 182 | ) && 183 | !self.class._all_default_attribute_values_not_allowing_nil.include?(attribute) 184 | 185 | __send__("#{attribute}=", container.evaluate(self)) 186 | if respond_to?(:clear_attribute_changes, true) 187 | clear_attribute_changes [attribute] if has_attribute?(attribute) 188 | else 189 | changed_attributes.delete(attribute) 190 | end 191 | end 192 | end 193 | end 194 | end 195 | 196 | if defined?(Rails::Railtie) 197 | require 'default_value_for/railtie' 198 | else 199 | # For anybody is using AS and AR without Railties, i.e. Padrino. 200 | ActiveRecord::Base.extend(DefaultValueFor::ClassMethods) 201 | end 202 | -------------------------------------------------------------------------------- /test.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2008-2012 Phusion 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a copy 4 | # of this software and associated documentation files (the "Software"), to deal 5 | # in the Software without restriction, including without limitation the rights 6 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | # copies of the Software, and to permit persons to whom the Software is 8 | # furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in 11 | # all copies or substantial portions of the Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | # THE SOFTWARE. 20 | 21 | require 'bundler/setup' 22 | require 'minitest/autorun' 23 | require 'minitest/around/unit' 24 | require 'active_record' 25 | require 'action_pack' 26 | 27 | if ActiveSupport::VERSION::MAJOR == 3 28 | require 'active_support/core_ext/logger' 29 | end 30 | 31 | if ActionPack::VERSION::MAJOR > 3 32 | require 'action_controller' 33 | end 34 | 35 | # Handle an edge-case when using Arel 5 (i.e. Rails <= 4.1) with Ruby >= 2.4: 36 | # See: https://github.com/rails/arel/commit/dc85a6e9c74942945ad696f5da4d82490a85b865 37 | # See: https://stackoverflow.com/a/51481088 38 | rails_match_data = RUBY_VERSION.match(/\A(?\d+).(?\d+)/) 39 | rails_2_4_or_newer = rails_match_data[:major].to_i > 2 || (rails_match_data[:major].to_i == 2 && rails_match_data[:minor].to_i >= 4) 40 | arel_match_data = Arel::VERSION.match(/\A(?\d+).(?\d+)/) 41 | arel_older_than_7_1 = arel_match_data[:major].to_i < 7 || (arel_match_data[:major].to_i == 7 && arel_match_data[:minor].to_i < 1) 42 | 43 | if rails_2_4_or_newer && arel_older_than_7_1 44 | module Arel 45 | module Visitors 46 | class DepthFirst < Arel::Visitors::Visitor 47 | alias :visit_Integer :terminal 48 | end 49 | 50 | class Dot < Arel::Visitors::Visitor 51 | alias :visit_Integer :visit_String 52 | end 53 | 54 | # The super class for ToSql changed with Arel 6 55 | # See: https://github.com/rails/arel/commit/a6a7c75ff486657909e20e2f48764136caa5e87e#diff-3538aead5b80677372eea0e903ff728eR7 56 | class ToSql < (Arel::VERSION[/\A\d+/].to_i >= 6 ? Arel::Visitors::Reduce : Arel::Visitors::Visitor) 57 | alias :visit_Integer :literal 58 | end 59 | end 60 | end 61 | end 62 | 63 | begin 64 | TestCaseClass = MiniTest::Test 65 | rescue NameError 66 | TestCaseClass = MiniTest::Unit::TestCase 67 | end 68 | 69 | require 'default_value_for' 70 | 71 | puts "\nTesting with Active Record version #{ActiveRecord::VERSION::STRING}" 72 | puts "\nTesting with Action Pack version #{ActionPack::VERSION::STRING}\n\n" 73 | 74 | ActiveRecord::Base.default_timezone = :local 75 | ActiveRecord::Base.logger = Logger.new(STDERR) 76 | ActiveRecord::Base.logger.level = Logger::WARN 77 | 78 | ActiveRecord::Base.establish_connection( 79 | :adapter => RUBY_PLATFORM == 'java' ? 'jdbcsqlite3' : 'sqlite3', 80 | :database => ':memory:' 81 | ) 82 | 83 | ActiveRecord::Base.connection.create_table(:users, :force => true) do |t| 84 | t.string :username 85 | t.integer :default_number 86 | t.text :settings 87 | end 88 | 89 | ActiveRecord::Base.connection.create_table(:books, :force => true) do |t| 90 | t.string :type 91 | t.integer :number 92 | t.integer :count, :null => false, :default => 1 93 | t.integer :user_id 94 | t.timestamp :timestamp 95 | t.text :stuff 96 | t.boolean :flag 97 | end 98 | 99 | if defined?(Rails::Railtie) 100 | DefaultValueFor.initialize_railtie 101 | DefaultValueFor.initialize_active_record_extensions 102 | end 103 | 104 | class DefaultValuePluginTest < TestCaseClass 105 | def around 106 | Object.const_set(:User, Class.new(ActiveRecord::Base)) 107 | Object.const_set(:Book, Class.new(ActiveRecord::Base)) 108 | Object.const_set(:Novel, Class.new(Book)) 109 | User.has_many :books 110 | Book.belongs_to :user 111 | 112 | ActiveRecord::Base.transaction do 113 | yield 114 | raise ActiveRecord::Rollback 115 | end 116 | ensure 117 | Object.send(:remove_const, :User) 118 | Object.send(:remove_const, :Book) 119 | Object.send(:remove_const, :Novel) 120 | ActiveSupport::Dependencies.clear 121 | end 122 | 123 | def test_default_value_on_attribute_methods 124 | Book.class_eval do 125 | serialize :stuff 126 | default_value_for :color, :green 127 | def color; (self.stuff || {})[:color]; end 128 | def color=(val) 129 | self.stuff ||= {} 130 | self.stuff[:color] = val 131 | end 132 | end 133 | assert_equal :green, Book.create.color 134 | end 135 | 136 | def test_default_value_can_be_passed_as_argument 137 | Book.default_value_for(:number, 1234) 138 | assert_equal 1234, Book.new.number 139 | end 140 | 141 | def test_default_value_can_be_passed_as_block 142 | Book.default_value_for(:number) { 1234 } 143 | assert_equal 1234, Book.new.number 144 | end 145 | 146 | def test_works_with_create 147 | Book.default_value_for :number, 1234 148 | 149 | object = Book.create 150 | refute_nil Book.find_by_number(1234) 151 | 152 | # allows nil for existing records 153 | object.update_attribute(:number, nil) 154 | assert_nil Book.find_by_number(1234) 155 | assert_nil Book.find(object.id).number 156 | end 157 | 158 | def test_does_not_allow_nil_sets_default_value_on_existing_nils 159 | Book.default_value_for(:number, :allows_nil => false) { 1234 } 160 | object = Book.create 161 | object.update_attribute(:number, nil) 162 | assert_nil Book.find_by_number(1234) 163 | assert_equal 1234, Book.find(object.id).number 164 | end 165 | 166 | def test_overwrites_db_default 167 | Book.default_value_for :count, 1234 168 | assert_equal 1234, Book.new.count 169 | end 170 | 171 | def test_doesnt_overwrite_values_provided_by_mass_assignment 172 | Book.default_value_for :number, 1234 173 | assert_equal 1, Book.new(:number => 1, :count => 2).number 174 | end 175 | 176 | def test_doesnt_overwrite_values_provided_by_multiparameter_assignment 177 | Book.default_value_for :timestamp, Time.mktime(2000, 1, 1) 178 | timestamp = Time.mktime(2009, 1, 1) 179 | object = Book.new('timestamp(1i)' => '2009', 'timestamp(2i)' => '1', 'timestamp(3i)' => '1') 180 | assert_equal timestamp, object.timestamp 181 | end 182 | 183 | def test_doesnt_overwrite_values_provided_by_constructor_block 184 | Book.default_value_for :number, 1234 185 | object = Book.new do |x| 186 | x.number = 1 187 | x.count = 2 188 | end 189 | assert_equal 1, object.number 190 | end 191 | 192 | def test_doesnt_overwrite_explicitly_provided_nil_values_in_mass_assignment 193 | Book.default_value_for :number, 1234 194 | assert_nil Book.new(:number => nil).number 195 | end 196 | 197 | def test_overwrites_explicitly_provided_nil_values_in_mass_assignment 198 | Book.default_value_for :number, :value => 1234, :allows_nil => false 199 | assert_equal 1234, Book.new(:number => nil).number 200 | end 201 | 202 | def test_default_values_are_inherited 203 | Book.default_value_for :number, 1234 204 | assert_equal 1234, Novel.new.number 205 | end 206 | 207 | def test_default_values_in_superclass_are_saved_in_subclass 208 | Book.default_value_for :number, 1234 209 | Novel.default_value_for :flag, true 210 | object = Novel.create! 211 | assert_equal object.id, Novel.find_by_number(1234).id 212 | assert_equal object.id, Novel.find_by_flag(true).id 213 | end 214 | 215 | def test_default_values_in_subclass 216 | Novel.default_value_for :number, 5678 217 | assert_equal 5678, Novel.new.number 218 | assert_nil Book.new.number 219 | end 220 | 221 | def test_multiple_default_values_in_subclass_with_default_values_in_parent_class 222 | Book.class_eval do 223 | default_value_for :other_number, nil 224 | attr_accessor :other_number 225 | end 226 | Novel.default_value_for :number, 5678 227 | 228 | # Ensure second call in this class doesn't reset _default_attribute_values, 229 | # and also doesn't consider the parent class' _default_attribute_values when 230 | # making that check. 231 | Novel.default_value_for :user_id, 9999 232 | 233 | object = Novel.new 234 | assert_nil object.other_number 235 | assert_equal 5678, object.number 236 | assert_equal 9999, object.user_id 237 | end 238 | 239 | def test_override_default_values_in_subclass 240 | Book.default_value_for :number, 1234 241 | Novel.default_value_for :number, 5678 242 | assert_equal 5678, Novel.new.number 243 | assert_equal 1234, Book.new.number 244 | end 245 | 246 | def test_default_values_in_subclass_do_not_affect_parent_class 247 | Book.default_value_for :number, 1234 248 | Novel.class_eval do 249 | default_value_for :hello, "hi" 250 | attr_accessor :hello 251 | end 252 | 253 | assert Book.new 254 | assert !Book._default_attribute_values.include?(:hello) 255 | end 256 | 257 | def test_doesnt_set_default_on_saved_records 258 | Book.create(:number => 9876) 259 | Book.default_value_for :number, 1234 260 | assert_equal 9876, Book.first.number 261 | end 262 | 263 | def test_also_works_on_attributes_that_arent_database_columns 264 | Book.class_eval do 265 | default_value_for :hello, "hi" 266 | attr_accessor :hello 267 | end 268 | assert_equal 'hi', Book.new.hello 269 | end 270 | 271 | def test_works_on_attributes_that_only_have_writers 272 | Book.class_eval do 273 | default_value_for :hello, "hi" 274 | attr_writer :hello 275 | end 276 | assert_equal 'hi', Book.new.instance_variable_get('@hello') 277 | end 278 | 279 | def test_doesnt_conflict_with_overrided_initialize_method_in_model_class 280 | Book.class_eval do 281 | def initialize(attrs = {}) 282 | @initialized = true 283 | super(:count => 5678) 284 | end 285 | 286 | default_value_for :number, 1234 287 | end 288 | object = Book.new 289 | assert_equal 1234, object.number 290 | assert_equal 5678, object.count 291 | assert object.instance_variable_get('@initialized') 292 | end 293 | 294 | def test_model_instance_is_passed_to_the_given_block 295 | instance = nil 296 | Book.default_value_for :number do |n| 297 | instance = n 298 | end 299 | object = Book.new 300 | assert_same object.object_id, instance.object_id 301 | end 302 | 303 | def test_can_specify_default_value_via_association 304 | user = User.create(:username => 'Kanako', :default_number => 123) 305 | Book.default_value_for :number do |n| 306 | n.user.default_number 307 | end 308 | assert_equal 123, user.books.create!.number 309 | end 310 | 311 | def test_default_values 312 | Book.default_values({ 313 | :type => "normal", 314 | :number => lambda { 10 + 5 }, 315 | :timestamp => lambda {|_| Time.now } 316 | }) 317 | 318 | object = Book.new 319 | assert_equal("normal", object.type) 320 | assert_equal(15, object.number) 321 | end 322 | 323 | def test_default_value_order 324 | Book.default_value_for :count, 5 325 | Book.default_value_for :number do |this| 326 | this.count * 2 327 | end 328 | object = Book.new 329 | assert_equal(5, object.count) 330 | assert_equal(10, object.number) 331 | end 332 | 333 | def test_attributes_with_default_values_are_not_marked_as_changed 334 | Book.default_value_for :count, 5 335 | Book.default_value_for :number, 2 336 | 337 | object = Book.new 338 | assert(!object.changed?) 339 | assert_equal([], object.changed) 340 | 341 | object.type = "foo" 342 | assert(object.changed?) 343 | assert_equal(["type"], object.changed) 344 | end 345 | 346 | def test_default_values_are_duplicated 347 | User.default_value_for :username, "hello" 348 | user1 = User.new 349 | user1.username << " world" 350 | user2 = User.new 351 | assert_equal("hello", user2.username) 352 | end 353 | 354 | def test_default_values_are_shallow_copied 355 | User.class_eval do 356 | attr_accessor :hash 357 | default_value_for :hash, { 1 => [] } 358 | end 359 | user1 = User.new 360 | user1.hash[1] << 1 361 | user2 = User.new 362 | assert_equal([1], user2.hash[1]) 363 | end 364 | 365 | def test_constructor_does_not_affect_the_hash_passed_to_it 366 | Book.default_value_for :count, 5 367 | options = { :count => 5, :user_id => 1 } 368 | options_dup = options.dup 369 | Book.new(options) 370 | assert_equal(options_dup, options) 371 | end 372 | 373 | def test_subclass_find 374 | Book.default_value_for :number, 5678 375 | n = Novel.create 376 | assert Novel.find(n.id) 377 | end 378 | 379 | def test_does_not_see_false_as_blank_at_boolean_columns_for_existing_records 380 | Book.default_value_for(:flag, :allows_nil => false) { true } 381 | 382 | object = Book.create 383 | 384 | # allows nil for existing records 385 | object.update_attribute(:flag, false) 386 | assert_equal false, Book.find(object.id).flag 387 | end 388 | 389 | def test_works_with_nested_attributes 390 | User.accepts_nested_attributes_for :books 391 | User.default_value_for :books do 392 | [Book.create!(:number => 0)] 393 | end 394 | 395 | user = User.create! :books_attributes => [{:number => 1}] 396 | assert_equal 1, Book.all.first.number 397 | end 398 | 399 | def test_works_with_stored_attribute_accessors_when_initializing_value_that_does_not_allow_nil 400 | User.store :settings, :accessors => :bio 401 | User.default_value_for :bio, :value => 'None given', :allows_nil => false 402 | 403 | user = User.create!(:bio => 'This is a bio') 404 | assert_equal 'This is a bio', user.bio 405 | end 406 | 407 | if ActionPack::VERSION::MAJOR > 3 408 | def test_doesnt_overwrite_explicitly_provided_nil_values_in_mass_assignment_with_action_controller_parameters 409 | Book.default_value_for :number, 1234 410 | 411 | assert_nil Book.new(ActionController::Parameters.new(:number => nil).permit!).number 412 | end 413 | 414 | def test_overwrites_explicitly_provided_nil_values_in_mass_assignment_with_action_controller_parameters 415 | Book.default_value_for :number, :value => 1234, :allows_nil => false 416 | 417 | assert_equal 1234, Book.new(ActionController::Parameters.new(:number => nil).permit!).number 418 | end 419 | 420 | def test_works_with_nested_attributes_with_action_controller_parameters 421 | User.accepts_nested_attributes_for :books 422 | User.default_value_for :books do 423 | [Book.create!(:number => 0)] 424 | end 425 | 426 | user = User.create!(ActionController::Parameters.new(:books_attributes => [{:number => 1}]).permit!) 427 | assert_equal 1, Book.all.first.number 428 | end 429 | 430 | def test_works_with_stored_attribute_accessors_when_initializing_value_that_does_not_allow_nil_with_action_controller_parameters 431 | User.store :settings, :accessors => :bio 432 | User.default_value_for :bio, :value => 'None given', :allows_nil => false 433 | 434 | user = User.create!(ActionController::Parameters.new(:bio => 'This is a bio').permit!) 435 | assert_equal 'This is a bio', user.bio 436 | end 437 | end 438 | 439 | if ActiveRecord::VERSION::MAJOR == 3 440 | def test_constructor_ignores_forbidden_mass_assignment_attributes 441 | Book.class_eval do 442 | default_value_for :number, 1234 443 | attr_protected :number 444 | end 445 | object = Book.new(:number => 5678, :count => 987) 446 | assert_equal 1234, object.number 447 | assert_equal 987, object.count 448 | end 449 | 450 | def test_constructor_respects_without_protection_option 451 | Book.class_eval do 452 | default_value_for :number, 1234 453 | attr_protected :number 454 | end 455 | 456 | object = Book.create!({:number => 5678, :count => 987}, :without_protection => true) 457 | assert_equal 5678, object.number 458 | assert_equal 987, object.count 459 | end 460 | end 461 | end 462 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | The default_value_for plugin allows one to define default values for ActiveRecord 4 | models in a declarative manner. For example: 5 | 6 | ```ruby 7 | class User < ActiveRecord::Base 8 | default_value_for :name, "(no name)" 9 | default_value_for :last_seen do 10 | Time.now 11 | end 12 | end 13 | 14 | u = User.new 15 | u.name # => "(no name)" 16 | u.last_seen # => Mon Sep 22 17:28:38 +0200 2008 17 | ``` 18 | 19 | *Note*: critics might be interested in the "When (not) to use default_value_for?" section. Please read on. 20 | 21 | ## Installation 22 | 23 | ### Rails 3.2 - 4.2 / Ruby 1.9.3 and higher 24 | 25 | The current version of default_value_for (3.0.x) is compatible with Rails 3.2 or higher, and Ruby 1.9.3 and higher. 26 | 27 | Add it to your Gemfile: 28 | 29 | ```ruby 30 | gem "default_value_for", "~> 3.0.0" 31 | ``` 32 | 33 | This gem is signed using PGP with the Phusion Software Signing key: http://www.phusion.nl/about/gpg. That key in turn is signed by the rubygems-openpgp Certificate Authority: http://www.rubygems-openpgp-ca.org/. 34 | 35 | You can verify the authenticity of the gem by following The Complete Guide to Verifying Gems with rubygems-openpgp: http://www.rubygems-openpgp-ca.org/blog/the-complete-guide-to-verifying-gems-with-rubygems-openpgp.html 36 | 37 | ## Rails 3.0 - 3.1 / Ruby 1.9.3 and lower 38 | 39 | To use default_value_for with older versions of Ruby and Rails, you must use the previous stable release, 2.0.3. This version works with Rails 3.0, 3.1, and 3.2; and Ruby 1.8.7 and higher. It **does not** work with Rails 4. 40 | 41 | ```ruby 42 | gem "default_value_for", "~> 2.0.3" 43 | ``` 44 | 45 | ### Rails 2 46 | 47 | To use default_value_for with Rails 2.x you must use an older version: 48 | 49 | ```shell 50 | ./script/plugin install git://github.com/FooBarWidget/default_value_for.git -r release-1.0.7 51 | ``` 52 | 53 | ## The default_value_for method 54 | 55 | The `default_value_for` method is available in all ActiveRecord model classes. 56 | 57 | The first argument is the name of the attribute for which a default value should 58 | be set. This may either be a Symbol or a String. 59 | 60 | The default value itself may either be passed as the second argument: 61 | 62 | ```ruby 63 | default_value_for :age, 20 64 | ``` 65 | 66 | ...or it may be passed as the return value of a block: 67 | 68 | ```ruby 69 | default_value_for :age do 70 | if today_is_sunday? 71 | 20 72 | else 73 | 30 74 | end 75 | end 76 | ``` 77 | 78 | If you pass a value argument, then the default value is static and never changes. However, if you pass a block, then the default value is retrieved by calling the block. This block is called not once, but every time a new record is instantiated and default values need to be filled in. 79 | 80 | The latter form is especially useful if your model has a UUID column. One can generate a new, random UUID for every newly instantiated record: 81 | 82 | ```ruby 83 | class User < ActiveRecord::Base 84 | default_value_for :uuid do 85 | UuidGenerator.new.generate_uuid 86 | end 87 | end 88 | 89 | User.new.uuid # => "51d6d6846f1d1b5c9a...." 90 | User.new.uuid # => "ede292289e3484cb88...." 91 | ``` 92 | 93 | Note that record is passed to the block as an argument, in case you need it for whatever reason: 94 | 95 | ```ruby 96 | class User < ActiveRecord::Base 97 | default_value_for :uuid do |x| 98 | x # <--- a User object 99 | UuidGenerator.new.generate_uuid 100 | end 101 | end 102 | ``` 103 | 104 | ## default_value_for options 105 | 106 | * allows_nil (default: true) - Sets explicitly passed nil values if option is set to true. 107 | 108 | You can pass this options hash as 2nd parameter and have to pass the default value through the :value option in this case e.g.: 109 | 110 | ```ruby 111 | default_value_for :age, :value => 20, :allows_nil => false 112 | ``` 113 | 114 | You can still pass the default value through a block: 115 | 116 | ```ruby 117 | default_value_for :uuid, :allows_nil => false do 118 | UuidGenerator.new.generate_uuid 119 | end 120 | ```` 121 | 122 | ## The default_values method 123 | 124 | As a shortcut, you can use +default_values+ to set multiple default values at once. 125 | 126 | ```ruby 127 | default_values :age => 20, 128 | :uuid => lambda { UuidGenerator.new.generate_uuid } 129 | ``` 130 | 131 | If you like to override default_value_for options for each attribute you can do so: 132 | 133 | ```ruby 134 | default_values :age => { :value => 20 }, 135 | :uuid => { :value => lambda { UuidGenerator.new.generate_uuid }, :allows_nil => false } 136 | ``` 137 | 138 | The difference is purely aesthetic. If you have lots of default values which are constants or constructed with one-line blocks, +default_values+ may look nicer. If you have default values constructed by longer blocks, `default_value_for` suit you better. Feel free to mix and match. 139 | 140 | As a side note, due to specifics of Ruby's parser, you cannot say, 141 | 142 | ```ruby 143 | default_value_for :uuid { UuidGenerator.new.generate_uuid } 144 | ``` 145 | 146 | because it will not parse. One needs to write 147 | 148 | ```ruby 149 | default_value_for(:uuid) { UuidGenerator.new.generate_uuid } 150 | ``` 151 | 152 | instead. This is in part the inspiration for the +default_values+ syntax. 153 | 154 | ## Rules 155 | 156 | ### Instantiation of new record 157 | 158 | Upon instantiating a new record, the declared default values are filled into 159 | the record. You've already seen this in the above examples. 160 | 161 | ### Retrieval of existing record 162 | 163 | Upon retrieving an existing record in the following case, the declared default values are _not_ filled into the record. Consider the example with the UUID: 164 | 165 | ```ruby 166 | user = User.create 167 | user.uuid # => "529c91b8bbd3e..." 168 | 169 | user = User.find(user.id) 170 | # UUID remains unchanged because it's retrieved from the database! 171 | user.uuid # => "529c91b8bbd3e..." 172 | ``` 173 | 174 | But when the declared default value is set to not allow nil and nil is passed the default values will be set on retrieval. 175 | Consider this example: 176 | 177 | ```ruby 178 | default_value_for(:number, :allows_nil => false) { 123 } 179 | 180 | user = User.create 181 | 182 | # manual SQL by-passing active record and the default value for gem logic through ActiveRecord's after_initialize callback 183 | user.update_attribute(:number, nil) 184 | 185 | # declared default value should be set 186 | User.find(user.id).number # => 123 # = declared default value 187 | ``` 188 | 189 | ### Mass-assignment 190 | 191 | If a certain attribute is being assigned via the model constructor's 192 | mass-assignment argument, that the default value for that attribute will _not_ 193 | be filled in: 194 | 195 | ```ruby 196 | user = User.new(:uuid => "hello") 197 | user.uuid # => "hello" 198 | ``` 199 | 200 | However, if that attribute is protected by +attr_protected+ or +attr_accessible+, 201 | then it will be filled in: 202 | 203 | ```ruby 204 | class User < ActiveRecord::Base 205 | default_value_for :name, 'Joe' 206 | attr_protected :name 207 | end 208 | 209 | user = User.new(:name => "Jane") 210 | user.name # => "Joe" 211 | 212 | # the without protection option will work as expected 213 | user = User.new({:name => "Jane"}, :without_protection => true) 214 | user.name # => "Jane" 215 | ``` 216 | 217 | Explicitly set nil values for accessible attributes will be accepted: 218 | 219 | ```ruby 220 | class User < ActiveRecord::Base 221 | default_value_for :name, 'Joe' 222 | end 223 | 224 | user = User(:name => nil) 225 | user.name # => nil 226 | 227 | ... unless the accessible attribute is set to not allowing nil: 228 | 229 | class User < ActiveRecord::Base 230 | default_value_for :name, 'Joe', :allows_nil => false 231 | end 232 | 233 | user = User(:name => nil) 234 | user.name # => "Joe" 235 | ``` 236 | 237 | ### Inheritance 238 | 239 | Inheritance works as expected. All default values are inherited by the child 240 | class: 241 | 242 | ```ruby 243 | class User < ActiveRecord::Base 244 | default_value_for :name, 'Joe' 245 | end 246 | 247 | class SuperUser < User 248 | end 249 | 250 | SuperUser.new.name # => "Joe" 251 | ``` 252 | 253 | ### Attributes that aren't database columns 254 | 255 | `default_value_for` also works with attributes that aren't database columns. 256 | It works with anything for which there's an assignment method: 257 | 258 | ```ruby 259 | # Suppose that your 'users' table only has a 'name' column. 260 | class User < ActiveRecord::Base 261 | default_value_for :name, 'Joe' 262 | default_value_for :age, 20 263 | default_value_for :registering, true 264 | 265 | attr_accessor :age 266 | 267 | def registering=(value) 268 | @registering = true 269 | end 270 | end 271 | 272 | user = User.new 273 | user.age # => 20 274 | user.instance_variable_get('@registering') # => true 275 | ``` 276 | 277 | ### Default values are duplicated 278 | 279 | The given default values are duplicated when they are filled in, so if you mutate a value that was filled in with a default value, then it will not affect all subsequent default values: 280 | 281 | ```ruby 282 | class Author < ActiveRecord::Base 283 | # This model only has a 'name' attribute. 284 | end 285 | 286 | class Book < ActiveRecord::Base 287 | belongs_to :author 288 | 289 | # By default, a Book belongs to a new, unsaved author. 290 | default_value_for :author, Author.new 291 | end 292 | 293 | book1 = Book.new 294 | book1.author.name # => nil 295 | # This does not mutate the default value: 296 | book1.author.name = "John" 297 | 298 | book2 = Book.new 299 | book2.author.name # => nil 300 | ``` 301 | 302 | However the duplication is shallow. If you modify any objects that are referenced by the default value then it will affect subsequent default values: 303 | 304 | ```ruby 305 | class Author < ActiveRecord::Base 306 | attr_accessor :useless_hash 307 | default_value_for :useless_hash, { :foo => [] } 308 | end 309 | 310 | author1 = Author.new 311 | author1.useless_hash # => { :foo => [] } 312 | # This mutates the referred array: 313 | author1.useless_hash[:foo] << 1 314 | 315 | author2 = Author.new 316 | author2.useless_hash # => { :foo => [1] } 317 | ``` 318 | 319 | You can prevent this from happening by passing a block to `default_value_for`, which returns a new object instance with fresh references every time: 320 | 321 | ```ruby 322 | class Author < ActiveRecord::Base 323 | attr_accessor :useless_hash 324 | default_value_for :useless_hash do 325 | { :foo => [] } 326 | end 327 | end 328 | 329 | author1 = Author.new 330 | author1.useless_hash # => { :foo => [] } 331 | author1.useless_hash[:foo] << 1 332 | 333 | author2 = Author.new 334 | author2.useless_hash # => { :foo => [] } 335 | ``` 336 | 337 | ### Caveats 338 | 339 | A conflict can occur if your model class overrides the 'initialize' method, because this plugin overrides 'initialize' as well to do its job. 340 | 341 | ```ruby 342 | class User < ActiveRecord::Base 343 | def initialize # <-- this constructor causes problems 344 | super(:name => 'Name cannot be changed in constructor') 345 | end 346 | end 347 | ``` 348 | 349 | We recommend you to alias chain your initialize method in models where you use `default_value_for`: 350 | 351 | ```ruby 352 | class User < ActiveRecord::Base 353 | default_value_for :age, 20 354 | 355 | def initialize_with_my_app 356 | initialize_without_my_app(:name => 'Name cannot be changed in constructor') 357 | end 358 | 359 | alias_method_chain :initialize, :my_app 360 | end 361 | ``` 362 | 363 | Also, stick with the following rules: 364 | 365 | * There is no need to +alias_method_chain+ your initialize method in models that don't use `default_value_for`. 366 | 367 | * Make sure that +alias_method_chain+ is called *after* the last `default_value_for` occurrence. 368 | 369 | If your default value is accidentally similar to default_value_for's options hash wrap your default value like this: 370 | 371 | ```ruby 372 | default_value_for :attribute_name, :value => { :value => 123, :other_value => 1234 } 373 | ``` 374 | 375 | ## When (not) to use default_value_for? 376 | 377 | You can also specify default values in the database schema. For example, you can specify a default value in a migration as follows: 378 | 379 | ```ruby 380 | create_table :users do |t| 381 | t.string :username, :null => false, :default => 'default username' 382 | t.integer :age, :null => false, :default => 20 383 | end 384 | ``` 385 | 386 | This has similar effects as passing the default value as the second argument to `default_value_for`: 387 | 388 | ```ruby 389 | default_value_for(:username, 'default_username') 390 | default_value_for(:age, 20) 391 | ``` 392 | 393 | Default values are filled in whether you use the schema defaults or the default_value_for defaults: 394 | 395 | ```ruby 396 | user = User.new 397 | user.username # => 'default username' 398 | user.age # => 20 399 | ``` 400 | 401 | It's recommended that you use this over `default_value_for` whenever possible. 402 | 403 | However, it's not possible to specify a schema default for serialized columns. With `default_value_for`, you can: 404 | 405 | ```ruby 406 | class User < ActiveRecord::Base 407 | serialize :color 408 | default_value_for :color, [255, 0, 0] 409 | end 410 | ``` 411 | 412 | And if schema defaults don't provide the flexibility that you need, then `default_value_for` is the perfect choice. For example, with `default_value_for` you could specify a per-environment default: 413 | 414 | ```ruby 415 | class User < ActiveRecord::Base 416 | if Rails.env ## "development" 417 | default_value_for :is_admin, true 418 | end 419 | end 420 | ``` 421 | 422 | Or, as you've seen in an earlier example, you can use `default_value_for` to generate a default random UUID: 423 | 424 | ```ruby 425 | class User < ActiveRecord::Base 426 | default_value_for :uuid do 427 | UuidGenerator.new.generate_uuid 428 | end 429 | end 430 | ``` 431 | 432 | Or you could use it to generate a timestamp that's relative to the time at which the record is instantiated: 433 | 434 | ```ruby 435 | class User < ActiveRecord::Base 436 | default_value_for :account_expires_at do 437 | 3.years.from_now 438 | end 439 | end 440 | 441 | User.new.account_expires_at # => Mon Sep 22 18:43:42 +0200 2008 442 | sleep(2) 443 | User.new.account_expires_at # => Mon Sep 22 18:43:44 +0200 2008 444 | ``` 445 | 446 | Finally, it's also possible to specify a default via an association: 447 | 448 | ```ruby 449 | # Has columns: 'name' and 'default_price' 450 | class SuperMarket < ActiveRecord::Base 451 | has_many :products 452 | end 453 | 454 | # Has columns: 'name' and 'price' 455 | class Product < ActiveRecord::Base 456 | belongs_to :super_market 457 | 458 | default_value_for :price do |product| 459 | product.super_market.default_price 460 | end 461 | end 462 | 463 | super_market = SuperMarket.create(:name => 'Albert Zwijn', :default_price => 100) 464 | soap = super_market.products.create(:name => 'Soap') 465 | soap.price # => 100 466 | ``` 467 | 468 | ### What about before_validate/before_save? 469 | 470 | True, +before_validate+ and +before_save+ does what we want if we're only interested in filling in a default before saving. However, if one wants to be able to access the default value even before saving, then be prepared to write a lot of code. Suppose that we want to be able to access a new record's UUID, even before it's saved. We could end up with the following code: 471 | 472 | ```ruby 473 | # In the controller 474 | def create 475 | @user = User.new(params[:user]) 476 | @user.generate_uuid 477 | email_report_to_admin("#{@user.username} with UUID #{@user.uuid} created.") 478 | @user.save! 479 | end 480 | 481 | # Model 482 | class User < ActiveRecord::Base 483 | before_save :generate_uuid_if_necessary 484 | 485 | def generate_uuid 486 | self.uuid = ... 487 | end 488 | 489 | private 490 | def generate_uuid_if_necessary 491 | if uuid.blank? 492 | generate_uuid 493 | end 494 | end 495 | end 496 | ``` 497 | 498 | The need to manually call +generate_uuid+ here is ugly, and one can easily forget to do that. Can we do better? Let's see: 499 | 500 | ```ruby 501 | # Controller 502 | def create 503 | @user = User.new(params[:user]) 504 | email_report_to_admin("#{@user.username} with UUID #{@user.uuid} created.") 505 | @user.save! 506 | end 507 | 508 | # Model 509 | class User < ActiveRecord::Base 510 | before_save :generate_uuid_if_necessary 511 | 512 | def uuid 513 | value = read_attribute('uuid') 514 | if !value 515 | value = generate_uuid 516 | write_attribute('uuid', value) 517 | end 518 | value 519 | end 520 | 521 | # We need to override this too, otherwise User.new.attributes won't return 522 | # a default UUID value. I've never tested with User.create() so maybe we 523 | # need to override even more things. 524 | def attributes 525 | uuid 526 | super 527 | end 528 | 529 | private 530 | def generate_uuid_if_necessary 531 | uuid # Reader method automatically generates UUID if it doesn't exist 532 | end 533 | end 534 | ``` 535 | 536 | That's an awful lot of code. Using `default_value_for` is easier, don't you think? 537 | 538 | ### What about other plugins? 539 | 540 | I've only been able to find 2 similar plugins: 541 | 542 | * Default Value: http://agilewebdevelopment.com/plugins/default_value 543 | 544 | * ActiveRecord Defaults: http://agilewebdevelopment.com/plugins/activerecord_defaults 545 | 546 | 'Default Value' appears to be unmaintained; its SVN link is broken. This leaves only 'ActiveRecord Defaults'. However, it is semantically dubious, which leaves it wide open for corner cases. For example, it is not clearly specified what ActiveRecord Defaults will do when attributes are protected by +attr_protected+ or +attr_accessible+. It is also not clearly specified what one is supposed to do if one needs a custom +initialize+ method in the model. 547 | 548 | I've taken my time to thoroughly document default_value_for's behavior. 549 | 550 | ## Credits 551 | 552 | I've wanted such functionality for a while now and it baffled me that ActiveRecord doesn't provide a clean way for me to specify default values. After reading http://groups.google.com/group/rubyonrails-core/browse_thread/thread/b509a2fe2b62ac5/3e8243fa1954a935, it became clear that someone needs to write a plugin. This is the result. 553 | 554 | Thanks to Pratik Naik for providing the initial code snippet on which this plugin is based on: http://m.onkey.org/2007/7/24/how-to-set-default-values-in-your-model 555 | 556 | Thanks to Matthew Draper for Rails 5 support. 557 | 558 | Thanks to Norman Clarke and Tom Mango for Rails 4 support. 559 | --------------------------------------------------------------------------------