├── .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 |
--------------------------------------------------------------------------------