vex 0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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