vex 0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (114) hide show
  1. data/Manifest +112 -0
  2. data/Rakefile +53 -0
  3. data/VERSION +1 -0
  4. data/config/README +2 -0
  5. data/config/dependencies.rb +10 -0
  6. data/config/gem.yml +7 -0
  7. data/init.rb +36 -0
  8. data/lib/nokogiri/nokogiri_ext.rb +96 -0
  9. data/lib/vex.rb +5 -0
  10. data/lib/vex/action_controller.rb +4 -0
  11. data/lib/vex/action_controller/verify_action.rb +97 -0
  12. data/lib/vex/action_controller/whitelisted_actions.rb +45 -0
  13. data/lib/vex/active_record.rb +3 -0
  14. data/lib/vex/active_record/__init__.rb +0 -0
  15. data/lib/vex/active_record/advisory_lock.rb +11 -0
  16. data/lib/vex/active_record/advisory_lock/mysql_adapter.rb +16 -0
  17. data/lib/vex/active_record/advisory_lock/sqlite_adapter.rb +78 -0
  18. data/lib/vex/active_record/belongs_to_many.rb +143 -0
  19. data/lib/vex/active_record/find_by_extension.rb +70 -0
  20. data/lib/vex/active_record/gem.rb +8 -0
  21. data/lib/vex/active_record/lite_table.rb +139 -0
  22. data/lib/vex/active_record/lite_view.rb +140 -0
  23. data/lib/vex/active_record/mass_load.rb +65 -0
  24. data/lib/vex/active_record/mysql_backup.rb +21 -0
  25. data/lib/vex/active_record/plugins/default_value_for/LICENSE.TXT +19 -0
  26. data/lib/vex/active_record/plugins/default_value_for/README.rdoc +421 -0
  27. data/lib/vex/active_record/plugins/default_value_for/Rakefile +6 -0
  28. data/lib/vex/active_record/plugins/default_value_for/init.rb +91 -0
  29. data/lib/vex/active_record/plugins/default_value_for/test.rb +279 -0
  30. data/lib/vex/active_record/plugins/default_value_for/test.sqlite3 +0 -0
  31. data/lib/vex/active_record/random_id.rb +56 -0
  32. data/lib/vex/active_record/resolver.rb +49 -0
  33. data/lib/vex/active_record/serialize_hash.rb +125 -0
  34. data/lib/vex/active_record/to_html.rb +53 -0
  35. data/lib/vex/active_record/validate.rb +76 -0
  36. data/lib/vex/active_record/validation_error_ext.rb +68 -0
  37. data/lib/vex/base.rb +2 -0
  38. data/lib/vex/base/app.rb +75 -0
  39. data/lib/vex/base/array/at_random.rb +17 -0
  40. data/lib/vex/base/array/cross.rb +26 -0
  41. data/lib/vex/base/array/each_batch.rb +32 -0
  42. data/lib/vex/base/array/parallel_map.rb +98 -0
  43. data/lib/vex/base/deprecation.rb +41 -0
  44. data/lib/vex/base/enumerable/deep.rb +95 -0
  45. data/lib/vex/base/enumerable/enumerable_ext.rb +59 -0
  46. data/lib/vex/base/enumerable/progress.rb +71 -0
  47. data/lib/vex/base/filesystem/fast_copy.rb +61 -0
  48. data/lib/vex/base/filesystem/grep.rb +34 -0
  49. data/lib/vex/base/filesystem/lock.rb +43 -0
  50. data/lib/vex/base/filesystem/lock.rb.test.lck +0 -0
  51. data/lib/vex/base/filesystem/lock.rb.test.pid +1 -0
  52. data/lib/vex/base/filesystem/make_dirs.rb +94 -0
  53. data/lib/vex/base/filesystem/parse_filename.rb +36 -0
  54. data/lib/vex/base/filesystem/tmp_file.rb +87 -0
  55. data/lib/vex/base/filesystem/write.rb +43 -0
  56. data/lib/vex/base/hash/compact.rb +38 -0
  57. data/lib/vex/base/hash/cross.rb +117 -0
  58. data/lib/vex/base/hash/easy_access.rb +141 -0
  59. data/lib/vex/base/hash/ensure_keys.rb +18 -0
  60. data/lib/vex/base/hash/extract.rb +71 -0
  61. data/lib/vex/base/hash/extras.rb +62 -0
  62. data/lib/vex/base/hash/inspect.rb +17 -0
  63. data/lib/vex/base/hash/simple_access_methods.rb +74 -0
  64. data/lib/vex/base/invalid_argument/invalid_argument.rb +97 -0
  65. data/lib/vex/base/local_conf.rb +35 -0
  66. data/lib/vex/base/net/http_ext.rb +227 -0
  67. data/lib/vex/base/net/socket_ext.rb +43 -0
  68. data/lib/vex/base/object/insp.rb +123 -0
  69. data/lib/vex/base/object/multiple_attributes.rb +58 -0
  70. data/lib/vex/base/object/singleton_methods.rb +23 -0
  71. data/lib/vex/base/object/with_benchmark.rb +110 -0
  72. data/lib/vex/base/range_array.rb +40 -0
  73. data/lib/vex/base/range_ext.rb +28 -0
  74. data/lib/vex/base/safe_token.rb +156 -0
  75. data/lib/vex/base/string/string_ext.rb +136 -0
  76. data/lib/vex/base/thread/deferred.rb +52 -0
  77. data/lib/vex/base/thread/sleep.rb +11 -0
  78. data/lib/vex/base/time/date_ext.rb +12 -0
  79. data/lib/vex/boot.rb +40 -0
  80. data/lib/vex/boot/array.rb +22 -0
  81. data/lib/vex/boot/blank.rb +41 -0
  82. data/lib/vex/boot/string.rb +60 -0
  83. data/migration/create_request_log.rb +28 -0
  84. data/r.rb +35 -0
  85. data/script/console +19 -0
  86. data/script/rebuild +7 -0
  87. data/tasks/echoe.rake +52 -0
  88. data/tasks/validate_db.rake +14 -0
  89. data/test/ar.rb +30 -0
  90. data/test/auto.rb +31 -0
  91. data/test/base-tests/local_conf.rb +25 -0
  92. data/test/base.rb +2 -0
  93. data/test/boot.rb +3 -0
  94. data/test/config/local.defaults.yml +4 -0
  95. data/test/config/local.yml +8 -0
  96. data/test/test.sqlite3 +0 -0
  97. data/test/test.sqlite3.Class#create.lck +0 -0
  98. data/test/test.sqlite3.Class#create.lck.lck +0 -0
  99. data/test/test.sqlite3.Class#create.lck.pid +1 -0
  100. data/test/test.sqlite3.Class#create.pid +1 -0
  101. data/test/test.sqlite3.LiteView.make.holders__view_dummy.lck +0 -0
  102. data/test/test.sqlite3.LiteView.make.holders__view_dummy.lck.lck +0 -0
  103. data/test/test.sqlite3.LiteView.make.holders__view_dummy.lck.pid +1 -0
  104. data/test/test.sqlite3.LiteView.make.holders__view_dummy.pid +1 -0
  105. data/test/test.sqlite3.vex.lck +0 -0
  106. data/test/test_helper.rb +49 -0
  107. data/test/tmp/copy.dat +1 -0
  108. data/test/tmp/lock.sqlite3 +0 -0
  109. data/test/tmp/lock.sqlite3.etest.lck +0 -0
  110. data/test/tmp/lock.sqlite3.etest.pid +1 -0
  111. data/test/tmp/somedata.dat +61 -0
  112. data/vex.gemspec +49 -0
  113. data/vex.tmproj +186 -0
  114. metadata +305 -0
@@ -0,0 +1,140 @@
1
+ # TODO: LiteViews do not or not always support type information. Therefore values
2
+ # might be returned as strings, when in fact they are numbers. This behaviour is
3
+ # heavily database dependant.
4
+ #
5
+ # It would be great to fix it.
6
+
7
+ class ActiveRecord::LiteView < ActiveRecord::Base
8
+ lite_table do
9
+ string :name
10
+ text :sql
11
+
12
+ index :name
13
+ end
14
+
15
+ after_save do |rec|
16
+ locked(rec, &:create_view)
17
+ end
18
+
19
+ after_destroy do |rec|
20
+ locked(rec, &:drop_view)
21
+ end
22
+
23
+ private
24
+
25
+ def self.view_name(v)
26
+ case v
27
+ when Hash then "#{v[:klass].table_name}__#{v[:view]}"
28
+ when self then v.name
29
+ end
30
+ end
31
+
32
+ def drop_view
33
+ ActiveRecord::Base.connection.execute("DROP VIEW #{name}") rescue nil
34
+ end
35
+
36
+ def create_view
37
+ raise ArgumentError, "Missing SQL code" unless sql
38
+
39
+ drop_view
40
+ ActiveRecord::Base.connection.execute("CREATE VIEW #{name} AS #{sql}")
41
+ end
42
+
43
+ public
44
+
45
+ def self.drop_view(klass, view)
46
+ destroy_all :name => view_name(:klass => klass, :view => view)
47
+ end
48
+
49
+ def self.make(klass, view, sql)
50
+ name = view_name(:klass => klass, :view => view)
51
+
52
+ view = find_first(:name => name, :sql => sql)
53
+ view ||= locked(name) do
54
+ find_first(:name => name, :sql => sql) || create!(:name => name, :sql => sql)
55
+ end
56
+
57
+ # create a view class and return its name
58
+ view_klass = Class.new(ActiveRecord::Base)
59
+ view_klass.set_table_name name
60
+
61
+ klass_name = "LV_#{name.camelize}"
62
+ klass.send(:remove_const, klass_name) if klass.const_defined?(klass_name)
63
+ klass.const_set(klass_name, view_klass)
64
+
65
+ klass_name
66
+ end
67
+
68
+ private
69
+
70
+ def self.find_first(conditions, opts = {})
71
+ find :first, { :conditions => conditions }.update(opts)
72
+ end
73
+
74
+ def self.locked(lock)
75
+ name = lock.is_a?(self) ? lock.name : lock
76
+
77
+ ActiveRecord::Base.connection.locked("LiteView.make.#{name}") do
78
+ yield lock
79
+ end
80
+ end
81
+ end
82
+
83
+ class ActiveRecord::Base
84
+ def self.drop_view(view)
85
+ ActiveRecord::LiteView.drop_view(self, view)
86
+ end
87
+
88
+ def self.has_view(view, sql)
89
+ has_one view, :class_name => ActiveRecord::LiteView.make(self, view, sql)
90
+
91
+ define_method "#{view}_reset" do
92
+ instance_variable_set "@#{view}", nil
93
+ end
94
+ end
95
+ end
96
+
97
+ #
98
+ # This is a fix to rails_sql_views' Mysql schema dumper
99
+ if defined?(ActiveRecord::ConnectionAdapters::MysqlAdapter)
100
+
101
+ class ActiveRecord::ConnectionAdapters::MysqlAdapter
102
+ private
103
+
104
+ def convert_statement(s)
105
+ s.gsub(/.* AS (\(?select)/i, '\1')
106
+ end
107
+ end
108
+
109
+ end
110
+
111
+ module ActiveRecord::LiteView::Etest
112
+ class Holder < ActiveRecord::Base
113
+ lite_table do
114
+ end
115
+ end
116
+
117
+ def test_lite_view
118
+ db = ActiveRecord::Base.connection
119
+
120
+ Holder.create!
121
+ Holder.create!
122
+ Holder.create!
123
+
124
+ assert_equal(3, Holder.count)
125
+
126
+ # -- create a view
127
+ Holder.has_view :view_dummy, "SELECT id AS holder_id, 1 AS count_all FROM holders"
128
+ assert_equal("1", Holder.first.view_dummy.count_all)
129
+
130
+ # -- doesnt recreate identical view
131
+ # TODO: Check that an identical view won't be recreated
132
+ # Holder.has_view :view_dummy, "SELECT id AS holder_id, 1 AS count_all FROM holders"
133
+
134
+ assert_equal("1", Holder.first.view_dummy.count_all)
135
+
136
+ # -- create a slightly different view
137
+ Holder.has_view :view_dummy, "SELECT id AS holder_id, 2 AS count_all FROM holders"
138
+ assert_equal("2", Holder.first.view_dummy.count_all)
139
+ end
140
+ end
@@ -0,0 +1,65 @@
1
+ module ActiveRecord::MassLoad
2
+ class MissingAssociation
3
+ include Singleton
4
+ def target
5
+ nil
6
+ end
7
+ end
8
+
9
+ def mass_load_associations!(models, *associations)
10
+ if defined?(Rails) && Rails.env.development? && models.detect { |rec| !rec.is_a?(self) }
11
+ raise "Invalid models for mass_load_associations!"
12
+ end
13
+
14
+ return models if models.length < 2
15
+
16
+ associations.flatten!
17
+
18
+ models_by_id = models.inject({}) do |hash, model|
19
+ hash.update model.id => model
20
+ end
21
+ ids = models_by_id.keys
22
+ id_condition = ActiveRecord::MassLoad.get_id_condition(ids)
23
+
24
+ associations = associations.collect do |association|
25
+ case association
26
+ when Hash then association.to_a.collect do |k,v| { k => v } end
27
+ else association
28
+ end
29
+ end.flatten
30
+
31
+ associations.each do |association|
32
+ if association.is_a?(Hash)
33
+ association = association.to_a.first
34
+ other_assocs = association
35
+ association = other_assocs.shift
36
+ end
37
+
38
+ varname = "@#{association}"
39
+
40
+ base_assocs = []
41
+
42
+ find(:all, :include => association, :conditions => id_condition).each do |model|
43
+ target = models_by_id[model.id]
44
+ proxy = model.instance_variable_get(varname)
45
+ target.instance_variable_set(varname, proxy || MissingAssociation.instance)
46
+ base_assocs << proxy if proxy
47
+ end
48
+
49
+ next if !other_assocs || other_assocs.empty?
50
+ next if base_assocs.empty?
51
+
52
+ base_assocs.flatten!
53
+
54
+ base_assocs.first.class.mass_load_associations! base_assocs, *other_assocs
55
+ end
56
+
57
+ models
58
+ end
59
+
60
+ def self.get_id_condition(ids)
61
+ id_condition = { :id => ids.sort }
62
+ end
63
+ end
64
+
65
+ ActiveRecord::Base.extend ActiveRecord::MassLoad
@@ -0,0 +1,21 @@
1
+ module ActiveRecord::MysqlBackup
2
+ def purge
3
+ config = instance_variable_get("@config").easy_access
4
+
5
+ recreate_database(config.database)
6
+ end
7
+
8
+ def sqldump(file)
9
+ config = instance_variable_get("@config").easy_access
10
+
11
+ cmd_opts = ""
12
+ cmd_opts << "-h #{config.host} " if config.host?
13
+ cmd_opts << "-u #{config.username} " if config.username?
14
+ cmd_opts += "-p#{config.password} " if config.password?
15
+
16
+ cmd = "mysqldump #{cmd_opts} #{config.database} > #{file}"
17
+ STDERR.puts cmd
18
+ `#{cmd}`
19
+ end
20
+ end
21
+
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2008 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.
@@ -0,0 +1,421 @@
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
+ class User < ActiveRecord::Base
7
+ default_value_for :name, "(no name)"
8
+ default_value_for :last_seen do
9
+ Time.now
10
+ end
11
+ end
12
+
13
+ u = User.new
14
+ u.name # => "(no name)"
15
+ u.last_seen # => Mon Sep 22 17:28:38 +0200 2008
16
+
17
+ *Note*: critics might be interested in the "When (not) to use default_value_for?"
18
+ section. Please read on.
19
+
20
+
21
+ == Installation
22
+
23
+ Install with:
24
+
25
+ ./script/plugin install git://github.com/FooBarWidget/default_value_for.git
26
+
27
+
28
+ == The default_value_for method
29
+
30
+ The +default_value_for+ method is available in all ActiveRecord model classes.
31
+
32
+ The first argument is the name of the attribute for which a default value should
33
+ be set. This may either be a Symbol or a String.
34
+
35
+ The default value itself may either be passed as the second argument:
36
+
37
+ default_value_for :age, 20
38
+
39
+ ...or it may be passed as the return value of a block:
40
+
41
+ default_value_for :age do
42
+ if today_is_sunday?
43
+ 20
44
+ else
45
+ 30
46
+ end
47
+ end
48
+
49
+ If you pass a value argument, then the default value is static and never
50
+ changes. However, if you pass a block, then the default value is retrieved by
51
+ calling the block. This block is called not once, but every time a new record is
52
+ instantiated and default values need to be filled in.
53
+
54
+ The latter form is especially useful if your model has a UUID column. One can
55
+ generate a new, random UUID for every newly instantiated record:
56
+
57
+ class User < ActiveRecord::Base
58
+ default_value_for :uuid do
59
+ UuidGenerator.new.generate_uuid
60
+ end
61
+ end
62
+
63
+ User.new.uuid # => "51d6d6846f1d1b5c9a...."
64
+ User.new.uuid # => "ede292289e3484cb88...."
65
+
66
+ Note that record is passed to the block as an argument, in case you need it for
67
+ whatever reason:
68
+
69
+ class User < ActiveRecord::Base
70
+ default_value_for :uuid do |x|
71
+ x # <--- a User object
72
+ UuidGenerator.new.generate_uuid
73
+ end
74
+ end
75
+
76
+ == The default_values method
77
+
78
+ As a shortcut, you can use +default_values+ to set multiple default values at once.
79
+
80
+ default_values :age => 20
81
+ :uuid => lambda { UuidGenerator.new.generate_uuid }
82
+
83
+ 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.
84
+
85
+ As a side note, due to specifics of Ruby's parser, you cannot say,
86
+
87
+ default_value :uuid { UuidGenerator.new.generate_uuid }
88
+
89
+ because it will not parse. This is in part the inspiration for the +default_values+ syntax.
90
+
91
+ == Rules
92
+
93
+ === Instantiation of new record
94
+
95
+ Upon instantiating a new record, the declared default values are filled into
96
+ the record. You've already seen this in the above examples.
97
+
98
+ === Retrieval of existing record
99
+
100
+ Upon retrieving an existing record, the declared default values are _not_
101
+ filled into the record. Consider the example with the UUID:
102
+
103
+ user = User.create
104
+ user.uuid # => "529c91b8bbd3e..."
105
+
106
+ user = User.find(user.id)
107
+ # UUID remains unchanged because it's retrieved from the database!
108
+ user.uuid # => "529c91b8bbd3e..."
109
+
110
+ === Mass-assignment
111
+
112
+ If a certain attribute is being assigned via the model constructor's
113
+ mass-assignment argument, that the default value for that attribute will _not_
114
+ be filled in:
115
+
116
+ user = User.new(:uuid => "hello")
117
+ user.uuid # => "hello"
118
+
119
+ However, if that attribute is protected by +attr_protected+ or +attr_accessible+,
120
+ then it will be filled in:
121
+
122
+ class User < ActiveRecord::Base
123
+ default_value_for :name, 'Joe'
124
+ attr_protected :name
125
+ end
126
+
127
+ user = User.new(:name => "Jane")
128
+ user.name # => "Joe"
129
+
130
+ === Inheritance
131
+
132
+ Inheritance works as expected. All default values are inherited by the child
133
+ class:
134
+
135
+ class User < ActiveRecord::Base
136
+ default_value_for :name, 'Joe'
137
+ end
138
+
139
+ class SuperUser < User
140
+ end
141
+
142
+ SuperUser.new.name # => "Joe"
143
+
144
+ === Attributes that aren't database columns
145
+
146
+ +default_value_for+ also works with attributes that aren't database columns.
147
+ It works with anything for which there's an assignment method:
148
+
149
+ # Suppose that your 'users' table only has a 'name' column.
150
+ class User < ActiveRecord::Base
151
+ default_value_for :name, 'Joe'
152
+ default_value_for :age, 20
153
+ default_value_for :registering, true
154
+
155
+ attr_accessor :age
156
+
157
+ def registering=(value)
158
+ @registering = true
159
+ end
160
+ end
161
+
162
+ user = User.new
163
+ user.age # => 20
164
+ user.instance_variable_get('@registering') # => true
165
+
166
+ === Default values are *not* duplicated
167
+
168
+ The given default values are *not* duplicated when they are filled in, so if
169
+ you mutate a value that was filled in with a default value, then it will affect
170
+ all subsequent default values:
171
+
172
+ class Author < ActiveRecord::Base
173
+ # This model only has a 'name' attribute.
174
+ end
175
+
176
+ class Book < ActiveRecord::Base
177
+ belongs_to :author
178
+
179
+ # By default, a Book belongs to a new, unsaved author.
180
+ default_value_for :author, Author.new
181
+ end
182
+
183
+ book1 = Book.new
184
+ book1.author.name # => nil
185
+ # This mutates the default value:
186
+ book1.author.name = "John"
187
+
188
+ book2 = Book.new
189
+ book2.author.name # => "John"
190
+
191
+ You can prevent this from happening by passing a block to +default_value_for+,
192
+ which returns a new object instance every time:
193
+
194
+ class Book < ActiveRecord::Base
195
+ belongs_to :author
196
+
197
+ default_value_for :author do
198
+ Author.new
199
+ end
200
+ end
201
+
202
+ book1 = Book.new
203
+ book1.author.name # => nil
204
+ book1.author.name = "John"
205
+
206
+ book2 = Book.new
207
+ book2.author.name # => nil
208
+
209
+ The main reason why default values are not duplicated is because not all
210
+ objects can be duplicated. For example, +Fixnum+ responds to +dup+, but calling
211
+ +dup+ on a Fixnum will raise an exception.
212
+
213
+ === Caveats
214
+
215
+ A conflict can occur if your model class overrides the 'initialize' method,
216
+ because this plugin overrides 'initialize' as well to do its job.
217
+
218
+ class User < ActiveRecord::Base
219
+ def initialize # <-- this constructor causes problems
220
+ super(:name => 'Name cannot be changed in constructor')
221
+ end
222
+ end
223
+
224
+ We recommend you to alias chain your initialize method in models where you use
225
+ +default_value_for+:
226
+
227
+ class User < ActiveRecord::Base
228
+ default_value_for :age, 20
229
+
230
+ def initialize_with_my_app
231
+ initialize_without_my_app(:name => 'Name cannot be changed in constructor')
232
+ end
233
+
234
+ alias_method_chain :initialize, :my_app
235
+ end
236
+
237
+ Also, stick with the following rules:
238
+ - There is no need to +alias_method_chain+ your initialize method in models that
239
+ don't use +default_value_for+.
240
+ - Make sure that +alias_method_chain+ is called *after* the last
241
+ +default_value_for+ occurance.
242
+
243
+
244
+ == When (not) to use default_value_for?
245
+
246
+ You can also specify default values in the database schema. For example, you
247
+ can specify a default value in a migration as follows:
248
+
249
+ create_table :users do |t|
250
+ t.string :username, :null => false, :default => 'default username'
251
+ t.integer :age, :null => false, :default => 20
252
+ t.timestamp :last_seen, :null => false, :default => Time.now
253
+ end
254
+
255
+ This has the same effect as passing the default value as the second argument to
256
+ +default_value_for+:
257
+
258
+ user = User.new
259
+ user.username # => 'default username'
260
+ user.age # => 20
261
+ user.timestamp # => Mon Sep 22 18:31:47 +0200 2008
262
+
263
+ It's recommended that you use this over +default_value_for+ whenever possible.
264
+
265
+ However, it's not possible to specify a schema default for serialized columns.
266
+ With +default_value_for+, you can:
267
+
268
+ class User < ActiveRecord::Base
269
+ serialize :color
270
+ default_value_for :color, [255, 0, 0]
271
+ end
272
+
273
+ And if schema defaults don't provide the flexibility that you need, then
274
+ +default_value_for+ is the perfect choice. For example, with +default_value_for+
275
+ you could specify a per-environment default:
276
+
277
+ class User < ActiveRecord::Base
278
+ if RAILS_ENV == "development"
279
+ default_value_for :is_admin, true
280
+ end
281
+ end
282
+
283
+ Or, as you've seen in an earlier example, you can use +default_value_for+ to
284
+ generate a default random UUID:
285
+
286
+ class User < ActiveRecord::Base
287
+ default_value_for :uuid do
288
+ UuidGenerator.new.generate_uuid
289
+ end
290
+ end
291
+
292
+ Or you could use it to generate a timestamp that's relative to the time at which
293
+ the record is instantiated:
294
+
295
+ class User < ActiveRecord::Base
296
+ default_value_for :account_expires_at do
297
+ 3.years.from_now
298
+ end
299
+ end
300
+
301
+ User.new.account_expires_at # => Mon Sep 22 18:43:42 +0200 2008
302
+ sleep(2)
303
+ User.new.account_expires_at # => Mon Sep 22 18:43:44 +0200 2008
304
+
305
+ Finally, it's also possible to specify a default via an association:
306
+
307
+ # Has columns: 'name' and 'default_price'
308
+ class SuperMarket < ActiveRecord::Base
309
+ has_many :products
310
+ end
311
+
312
+ # Has columns: 'name' and 'price'
313
+ class Product < ActiveRecord::Base
314
+ belongs_to :super_market
315
+
316
+ default_value_for :price do |product|
317
+ product.super_market.default_price
318
+ end
319
+ end
320
+
321
+ super_market = SuperMarket.create(:name => 'Albert Zwijn', :default_price => 100)
322
+ soap = super_market.products.create(:name => 'Soap')
323
+ soap.price # => 100
324
+
325
+ === What about before_validate/before_save?
326
+
327
+ True, +before_validate+ and +before_save+ does what we want if we're only
328
+ interested in filling in a default before saving. However, if one wants to be
329
+ able to access the default value even before saving, then be prepared to write
330
+ a lot of code. Suppose that we want to be able to access a new record's UUID,
331
+ even before it's saved. We could end up with the following code:
332
+
333
+ # In the controller
334
+ def create
335
+ @user = User.new(params[:user])
336
+ @user.generate_uuid
337
+ email_report_to_admin("#{@user.username} with UUID #{@user.uuid} created.")
338
+ @user.save!
339
+ end
340
+
341
+ # Model
342
+ class User < ActiveRecord::Base
343
+ before_save :generate_uuid_if_necessary
344
+
345
+ def generate_uuid
346
+ self.uuid = ...
347
+ end
348
+
349
+ private
350
+ def generate_uuid_if_necessary
351
+ if uuid.blank?
352
+ generate_uuid
353
+ end
354
+ end
355
+ end
356
+
357
+ The need to manually call +generate_uuid+ here is ugly, and one can easily forget
358
+ to do that. Can we do better? Let's see:
359
+
360
+ # Controller
361
+ def create
362
+ @user = User.new(params[:user])
363
+ email_report_to_admin("#{@user.username} with UUID #{@user.uuid} created.")
364
+ @user.save!
365
+ end
366
+
367
+ # Model
368
+ class User < ActiveRecord::Base
369
+ before_save :generate_uuid_if_necessary
370
+
371
+ def uuid
372
+ value = read_attribute('uuid')
373
+ if !value
374
+ value = generate_uuid
375
+ write_attribute('uuid', value)
376
+ end
377
+ value
378
+ end
379
+
380
+ # We need to override this too, otherwise User.new.attributes won't return
381
+ # a default UUID value. I've never tested with User.create() so maybe we
382
+ # need to override even more things.
383
+ def attributes
384
+ uuid
385
+ super
386
+ end
387
+
388
+ private
389
+ def generate_uuid_if_necessary
390
+ uuid # Reader method automatically generates UUID if it doesn't exist
391
+ end
392
+ end
393
+
394
+ That's an awful lot of code. Using +default_value_for+ is easier, don't you think?
395
+
396
+ === What about other plugins?
397
+
398
+ I've only been able to find 2 similar plugins:
399
+
400
+ - Default Value: http://agilewebdevelopment.com/plugins/default_value
401
+ - ActiveRecord Defaults: http://agilewebdevelopment.com/plugins/activerecord_defaults
402
+
403
+ 'Default Value' appears to be unmaintained; its SVN link is broken. This leaves
404
+ only 'ActiveRecord Defaults'. However, it is semantically dubious, which leaves
405
+ it wide open for corner cases. For example, it is not clearly specified what
406
+ ActiveRecord Defaults will do when attributes are protected by +attr_protected+
407
+ or +attr_accessible+. It is also not clearly specified what one is supposed to
408
+ do if one needs a custom +initialize+ method in the model.
409
+
410
+ I've taken my time to thoroughly document default_value_for's behavior.
411
+
412
+
413
+ == Credits
414
+
415
+ I've wanted such functionality for a while now and it baffled me that ActiveRecord
416
+ doesn't provide a clean way for me to specify default values. After reading
417
+ http://groups.google.com/group/rubyonrails-core/browse_thread/thread/b509a2fe2b62ac5/3e8243fa1954a935,
418
+ it became clear that someone needs to write a plugin. This is the result.
419
+
420
+ Thanks to Pratik Naik for providing the initial code snippet on which this plugin
421
+ is based on: http://m.onkey.org/2007/7/24/how-to-set-default-values-in-your-model