activerecord 1.6.0 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (96) hide show
  1. data/CHANGELOG +78 -0
  2. data/README +20 -29
  3. data/RUNNING_UNIT_TESTS +1 -2
  4. data/examples/validation.rb +0 -3
  5. data/install.rb +3 -16
  6. data/lib/active_record.rb +11 -4
  7. data/lib/active_record/aggregations.rb +2 -2
  8. data/lib/active_record/associations.rb +8 -8
  9. data/lib/active_record/associations/association_collection.rb +1 -1
  10. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +1 -1
  11. data/lib/active_record/base.rb +117 -43
  12. data/lib/active_record/callbacks.rb +2 -2
  13. data/lib/active_record/connection_adapters/abstract_adapter.rb +7 -14
  14. data/lib/active_record/connection_adapters/db2_adapter.rb +33 -22
  15. data/lib/active_record/connection_adapters/mysql_adapter.rb +74 -33
  16. data/lib/active_record/connection_adapters/oci_adapter.rb +265 -0
  17. data/lib/active_record/connection_adapters/postgresql_adapter.rb +23 -3
  18. data/lib/active_record/connection_adapters/sqlite_adapter.rb +13 -4
  19. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +158 -67
  20. data/lib/active_record/deprecated_associations.rb +4 -4
  21. data/lib/active_record/fixtures.rb +12 -5
  22. data/lib/active_record/locking.rb +22 -22
  23. data/lib/active_record/observer.rb +6 -3
  24. data/lib/active_record/timestamp.rb +15 -5
  25. data/lib/active_record/transactions.rb +4 -4
  26. data/lib/active_record/validations.rb +272 -189
  27. data/lib/active_record/wrappings.rb +2 -2
  28. data/rakefile +17 -2
  29. data/test/aaa_create_tables_test.rb +58 -0
  30. data/test/abstract_unit.rb +3 -2
  31. data/test/aggregations_test.rb +0 -1
  32. data/test/associations_test.rb +27 -28
  33. data/test/base_test.rb +74 -2
  34. data/test/binary_test.rb +6 -2
  35. data/test/class_inheritable_attributes_test.rb +1 -1
  36. data/test/column_alias_test.rb +9 -2
  37. data/test/connections/native_oci/connection.rb +25 -0
  38. data/test/connections/native_sqlite/connection.rb +4 -1
  39. data/test/connections/native_sqlite3/connection.rb +4 -2
  40. data/test/deprecated_associations_test.rb +4 -5
  41. data/test/finder_test.rb +20 -4
  42. data/test/fixtures/db_definitions/create_oracle_db.bat +5 -0
  43. data/test/fixtures/db_definitions/create_oracle_db.sh +5 -0
  44. data/test/fixtures/db_definitions/db2.drop.sql +18 -0
  45. data/test/fixtures/db_definitions/db2.sql +1 -0
  46. data/test/fixtures/db_definitions/db22.drop.sql +2 -0
  47. data/test/fixtures/db_definitions/db22.sql +1 -0
  48. data/test/fixtures/db_definitions/drop_oracle_tables.sql +35 -0
  49. data/test/fixtures/db_definitions/drop_oracle_tables2.sql +3 -0
  50. data/test/fixtures/db_definitions/mysql.drop.sql +18 -0
  51. data/test/fixtures/db_definitions/mysql.sql +2 -1
  52. data/test/fixtures/db_definitions/mysql2.drop.sql +2 -0
  53. data/test/fixtures/db_definitions/mysql2.sql +1 -0
  54. data/test/fixtures/db_definitions/oci.drop.sql +18 -0
  55. data/test/fixtures/db_definitions/oci.sql +167 -0
  56. data/test/fixtures/db_definitions/oci2.drop.sql +2 -0
  57. data/test/fixtures/db_definitions/oci2.sql +6 -0
  58. data/test/fixtures/db_definitions/postgresql.drop.sql +18 -0
  59. data/test/fixtures/db_definitions/postgresql.sql +2 -1
  60. data/test/fixtures/db_definitions/postgresql2.drop.sql +2 -0
  61. data/test/fixtures/db_definitions/postgresql2.sql +2 -1
  62. data/test/fixtures/db_definitions/sqlite.drop.sql +18 -0
  63. data/test/fixtures/db_definitions/sqlite.sql +2 -1
  64. data/test/fixtures/db_definitions/sqlite2.drop.sql +2 -0
  65. data/test/fixtures/db_definitions/sqlite2.sql +1 -0
  66. data/test/fixtures/db_definitions/sqlserver.drop.sql +18 -0
  67. data/test/fixtures/db_definitions/sqlserver.sql +1 -0
  68. data/test/fixtures/db_definitions/sqlserver2.drop.sql +2 -0
  69. data/test/fixtures/db_definitions/sqlserver2.sql +1 -0
  70. data/test/fixtures/fixture_database.sqlite +0 -0
  71. data/test/fixtures/fixture_database_2.sqlite +0 -0
  72. data/test/fixtures/topics.yml +3 -3
  73. data/test/lifecycle_test.rb +0 -1
  74. data/test/modules_test.rb +0 -1
  75. data/test/reflection_test.rb +0 -1
  76. data/test/validations_test.rb +229 -41
  77. metadata +36 -28
  78. data/dev-utils/eval_debugger.rb +0 -14
  79. data/lib/active_record/support/binding_of_caller.rb +0 -83
  80. data/lib/active_record/support/breakpoint.rb +0 -518
  81. data/lib/active_record/support/class_attribute_accessors.rb +0 -57
  82. data/lib/active_record/support/class_inheritable_attributes.rb +0 -117
  83. data/lib/active_record/support/clean_logger.rb +0 -10
  84. data/lib/active_record/support/core_ext.rb +0 -1
  85. data/lib/active_record/support/core_ext/hash.rb +0 -5
  86. data/lib/active_record/support/core_ext/hash/keys.rb +0 -35
  87. data/lib/active_record/support/core_ext/numeric.rb +0 -7
  88. data/lib/active_record/support/core_ext/numeric/bytes.rb +0 -33
  89. data/lib/active_record/support/core_ext/numeric/time.rb +0 -59
  90. data/lib/active_record/support/core_ext/object_and_class.rb +0 -24
  91. data/lib/active_record/support/core_ext/string.rb +0 -5
  92. data/lib/active_record/support/core_ext/string/inflections.rb +0 -45
  93. data/lib/active_record/support/dependencies.rb +0 -63
  94. data/lib/active_record/support/inflector.rb +0 -84
  95. data/lib/active_record/support/misc.rb +0 -8
  96. data/lib/active_record/support/module_attribute_accessors.rb +0 -57
@@ -1,25 +1,25 @@
1
1
  module ActiveRecord
2
+ # Active Records support optimistic locking if the field <tt>lock_version</tt> is present. Each update to the
3
+ # record increments the lock_version column and the locking facilities ensure that records instantiated twice
4
+ # will let the last one saved raise a StaleObjectError if the first was also updated. Example:
5
+ #
6
+ # p1 = Person.find(1)
7
+ # p2 = Person.find(1)
8
+ #
9
+ # p1.first_name = "Michael"
10
+ # p1.save
11
+ #
12
+ # p2.first_name = "should fail"
13
+ # p2.save # Raises a ActiveRecord::StaleObjectError
14
+ #
15
+ # You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
16
+ # or otherwise apply the business logic needed to resolve the conflict.
17
+ #
18
+ # You must ensure that your database schema defaults the lock_version column to 0.
19
+ #
20
+ # This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>.
2
21
  module Locking
3
- # Active Records support optimistic locking if the field <tt>lock_version</tt> is present. Each update to the
4
- # record increments the lock_version column and the locking facilities ensure that records instantiated twice
5
- # will let the last one saved raise a StaleObjectError if the first was also updated. Example:
6
- #
7
- # p1 = Person.find(1)
8
- # p2 = Person.find(1)
9
- #
10
- # p1.first_name = "Michael"
11
- # p1.save
12
- #
13
- # p2.first_name = "should fail"
14
- # p2.save # Raises a ActiveRecord::StaleObjectError
15
- #
16
- # You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
17
- # or otherwise apply the business logic needed to resolve the conflict.
18
- #
19
- # You must ensure that your database schema defaults the lock_version column to 0.
20
- #
21
- # This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>.
22
- def self.append_features(base)
22
+ def self.append_features(base) #:nodoc:
23
23
  super
24
24
  base.class_eval do
25
25
  alias_method :update_without_lock, :update
@@ -27,7 +27,7 @@ module ActiveRecord
27
27
  end
28
28
  end
29
29
 
30
- def update_with_lock
30
+ def update_with_lock #:nodoc:
31
31
  if locking_enabled?
32
32
  previous_value = self.lock_version
33
33
  self.lock_version = previous_value + 1
@@ -50,7 +50,7 @@ module ActiveRecord
50
50
  @@lock_optimistically = true
51
51
  cattr_accessor :lock_optimistically
52
52
 
53
- def locking_enabled?
53
+ def locking_enabled? #:nodoc:
54
54
  lock_optimistically && respond_to?(:lock_version)
55
55
  end
56
56
  end
@@ -21,7 +21,8 @@ module ActiveRecord
21
21
  # something else than the class you're interested in observing, you can implement the observed_class class method. Like this:
22
22
  #
23
23
  # class AuditObserver < ActiveRecord::Observer
24
- # def self.observed_class() Account end
24
+ # observe Account
25
+ #
25
26
  # def after_update(account)
26
27
  # AuditTrail.new(account, "UPDATED")
27
28
  # end
@@ -32,7 +33,8 @@ module ActiveRecord
32
33
  # If the audit observer needs to watch more than one kind of object, this can be specified in an array, like this:
33
34
  #
34
35
  # class AuditObserver < ActiveRecord::Observer
35
- # def self.observed_class() [ Account, Balance ] end
36
+ # observe Account, Balance
37
+ #
36
38
  # def after_update(record)
37
39
  # AuditTrail.new(record, "UPDATED")
38
40
  # end
@@ -44,6 +46,7 @@ module ActiveRecord
44
46
  class Observer
45
47
  include Singleton
46
48
 
49
+ # Attaches the observer to the supplied model classes.
47
50
  def self.observe(*models)
48
51
  define_method(:observed_class) { models }
49
52
  end
@@ -55,7 +58,7 @@ module ActiveRecord
55
58
  end
56
59
  end
57
60
 
58
- def update(callback_method, object)
61
+ def update(callback_method, object) #:nodoc:
59
62
  send(callback_method, object) if respond_to?(callback_method)
60
63
  end
61
64
 
@@ -18,8 +18,8 @@ module ActiveRecord
18
18
  end
19
19
  end
20
20
 
21
- def create_with_timestamps
22
- t = timestamps_gmt ? Time.now.gmtime : Time.now
21
+ def create_with_timestamps #:nodoc:
22
+ t = ( self.class.default_timezone == :utc ? Time.now.utc : Time.now )
23
23
  write_attribute("created_at", t) if record_timestamps && respond_to?(:created_at) && created_at.nil?
24
24
  write_attribute("created_on", t) if record_timestamps && respond_to?(:created_on) && created_on.nil?
25
25
 
@@ -29,8 +29,8 @@ module ActiveRecord
29
29
  create_without_timestamps
30
30
  end
31
31
 
32
- def update_with_timestamps
33
- t = timestamps_gmt ? Time.now.gmtime : Time.now
32
+ def update_with_timestamps #:nodoc:
33
+ t = ( self.class.default_timezone == :utc ? Time.now.utc : Time.now )
34
34
  write_attribute("updated_at", t) if record_timestamps && respond_to?(:updated_at)
35
35
  write_attribute("updated_on", t) if record_timestamps && respond_to?(:updated_on)
36
36
 
@@ -44,7 +44,17 @@ module ActiveRecord
44
44
  # if the table has columns of either of these names. This feature is turned on by default.
45
45
  @@record_timestamps = true
46
46
  cattr_accessor :record_timestamps
47
+
48
+ # deprecated: use ActiveRecord::Base.default_timezone instead.
47
49
  @@timestamps_gmt = false
48
- cattr_accessor :timestamps_gmt
50
+ def self.timestamps_gmt=( gmt ) #:nodoc:
51
+ warn "timestamps_gmt= is deprecated. use default_timezone= instead"
52
+ self.default_timezone = ( gmt ? :utc : :local )
53
+ end
54
+
55
+ def self.timestamps_gmt #:nodoc:
56
+ warn "timestamps_gmt is deprecated. use default_timezone instead"
57
+ self.default_timezone == :utc
58
+ end
49
59
  end
50
60
  end
@@ -20,7 +20,7 @@ module ActiveRecord
20
20
  end
21
21
 
22
22
  # Transactions are protective blocks where SQL statements are only permanent if they can all succeed as one atomic action.
23
- # The classic example is a transfer between two accounts where you can only have a deposit if the withdrawal succedded and
23
+ # The classic example is a transfer between two accounts where you can only have a deposit if the withdrawal succeeded and
24
24
  # vice versa. Transaction enforce the integrity of the database and guards the data against program errors or database break-downs.
25
25
  # So basically you should use transaction blocks whenever you have a number of statements that must be executed together or
26
26
  # not at all. Example:
@@ -96,14 +96,14 @@ module ActiveRecord
96
96
  end
97
97
  end
98
98
 
99
- def lock_mutex
99
+ def lock_mutex#:nodoc:
100
100
  Thread.current['open_transactions'] ||= 0
101
101
  TRANSACTION_MUTEX.lock if Thread.current['open_transactions'] == 0
102
102
  Thread.current['start_db_transaction'] = (Thread.current['open_transactions'] == 0)
103
103
  Thread.current['open_transactions'] += 1
104
104
  end
105
105
 
106
- def unlock_mutex
106
+ def unlock_mutex#:nodoc:
107
107
  Thread.current['open_transactions'] -= 1
108
108
  TRANSACTION_MUTEX.unlock if Thread.current['open_transactions'] == 0
109
109
  end
@@ -121,4 +121,4 @@ module ActiveRecord
121
121
  transaction { save_without_transactions(perform_validation) }
122
122
  end
123
123
  end
124
- end
124
+ end
@@ -1,4 +1,140 @@
1
1
  module ActiveRecord
2
+ # Active Record validation is reported to and from this object, which is used by Base#save to
3
+ # determine whether the object in a valid state to be saved. See usage example in Validations.
4
+ class Errors
5
+ def initialize(base) # :nodoc:
6
+ @base, @errors = base, {}
7
+ end
8
+
9
+ @@default_error_messages = {
10
+ :inclusion => "is not included in the list",
11
+ :invalid => "is invalid",
12
+ :confirmation => "doesn't match confirmation",
13
+ :accepted => "must be accepted",
14
+ :empty => "can't be empty",
15
+ :too_long => "is too long (max is %d characters)",
16
+ :too_short => "is too short (min is %d characters)",
17
+ :wrong_length => "is the wrong length (should be %d characters)",
18
+ :taken => "has already been taken",
19
+ }
20
+
21
+ # Holds a hash with all the default error messages, such that they can be replaced by your own copy or localizations.
22
+ cattr_accessor :default_error_messages
23
+
24
+
25
+ # Adds an error to the base object instead of any particular attribute. This is used
26
+ # to report errors that doesn't tie to any specific attribute, but rather to the object
27
+ # as a whole. These error messages doesn't get prepended with any field name when iterating
28
+ # with each_full, so they should be complete sentences.
29
+ def add_to_base(msg)
30
+ add(:base, msg)
31
+ end
32
+
33
+ # Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
34
+ # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one
35
+ # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
36
+ # If no +msg+ is supplied, "invalid" is assumed.
37
+ def add(attribute, msg = @@default_error_messages[:invalid])
38
+ @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
39
+ @errors[attribute.to_s] << msg
40
+ end
41
+
42
+ # Will add an error message to each of the attributes in +attributes+ that is empty (defined by <tt>attribute_present?</tt>).
43
+ def add_on_empty(attributes, msg = @@default_error_messages[:empty])
44
+ for attr in [attributes].flatten
45
+ value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
46
+ is_empty = value.respond_to?("empty?") ? value.empty? : false
47
+ add(attr, msg) unless !value.nil? && !is_empty
48
+ end
49
+ end
50
+
51
+ # Will add an error message to each of the attributes in +attributes+ that has a length outside of the passed boundary +range+.
52
+ # If the length is above the boundary, the too_long_msg message will be used. If below, the too_short_msg.
53
+ def add_on_boundary_breaking(attributes, range, too_long_msg = @@default_error_messages[:too_long], too_short_msg = @@default_error_messages[:too_short])
54
+ for attr in [attributes].flatten
55
+ value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
56
+ add(attr, too_short_msg % range.begin) if value && value.length < range.begin
57
+ add(attr, too_long_msg % range.end) if value && value.length > range.end
58
+ end
59
+ end
60
+
61
+ alias :add_on_boundry_breaking :add_on_boundary_breaking
62
+
63
+ # Returns true if the specified +attribute+ has errors associated with it.
64
+ def invalid?(attribute)
65
+ !@errors[attribute.to_s].nil?
66
+ end
67
+
68
+ # * Returns nil, if no errors are associated with the specified +attribute+.
69
+ # * Returns the error message, if one error is associated with the specified +attribute+.
70
+ # * Returns an array of error messages, if more than one error is associated with the specified +attribute+.
71
+ def on(attribute)
72
+ if @errors[attribute.to_s].nil?
73
+ nil
74
+ elsif @errors[attribute.to_s].length == 1
75
+ @errors[attribute.to_s].first
76
+ else
77
+ @errors[attribute.to_s]
78
+ end
79
+ end
80
+
81
+ alias :[] :on
82
+
83
+ # Returns errors assigned to base object through add_to_base according to the normal rules of on(attribute).
84
+ def on_base
85
+ on(:base)
86
+ end
87
+
88
+ # Yields each attribute and associated message per error added.
89
+ def each
90
+ @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
91
+ end
92
+
93
+ # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
94
+ # through iteration as "First name can't be empty".
95
+ def each_full
96
+ full_messages.each { |msg| yield msg }
97
+ end
98
+
99
+ # Returns all the full error messages in an array.
100
+ def full_messages
101
+ full_messages = []
102
+
103
+ @errors.each_key do |attr|
104
+ @errors[attr].each do |msg|
105
+ next if msg.nil?
106
+
107
+ if attr == "base"
108
+ full_messages << msg
109
+ else
110
+ full_messages << @base.class.human_attribute_name(attr) + " " + msg
111
+ end
112
+ end
113
+ end
114
+
115
+ return full_messages
116
+ end
117
+
118
+ # Returns true if no errors have been added.
119
+ def empty?
120
+ return @errors.empty?
121
+ end
122
+
123
+ # Removes all the errors that have been added.
124
+ def clear
125
+ @errors = {}
126
+ end
127
+
128
+ # Returns the total number of errors added. Two errors added to the same attribute will be counted as such
129
+ # with this as well.
130
+ def count
131
+ error_count = 0
132
+ @errors.each_value { |attribute| error_count += attribute.length }
133
+ error_count
134
+ end
135
+ end
136
+
137
+
2
138
  # Active Records implement validation by overwriting Base#validate (or the variations, +validate_on_create+ and
3
139
  # +validate_on_update+). Each of these methods can inspect the state of the object, which usually means ensuring
4
140
  # that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression).
@@ -19,7 +155,7 @@ module ActiveRecord
19
155
  # end
20
156
  #
21
157
  # def validate_on_update
22
- # errors.add_to_base("No changes have occured") if unchanged_attributes?
158
+ # errors.add_to_base("No changes have occurred") if unchanged_attributes?
23
159
  # end
24
160
  # end
25
161
  #
@@ -40,27 +176,38 @@ module ActiveRecord
40
176
  # Please do have a look at ActiveRecord::Validations::ClassMethods for a higher level of validations.
41
177
  module Validations
42
178
  VALIDATIONS = %w( validate validate_on_create validate_on_update )
43
-
179
+
44
180
  def self.append_features(base) # :nodoc:
45
181
  super
46
-
182
+ base.extend ClassMethods
47
183
  base.class_eval do
48
184
  alias_method :save_without_validation, :save
49
185
  alias_method :save, :save_with_validation
50
186
 
51
187
  alias_method :update_attribute_without_validation_skipping, :update_attribute
52
188
  alias_method :update_attribute, :update_attribute_with_validation_skipping
53
-
54
- VALIDATIONS.each { |vd| base.class_eval("def self.#{vd}(*methods) write_inheritable_array(\"#{vd}\", methods - (read_inheritable_attribute(\"#{vd}\") || [])) end") }
55
189
  end
56
-
57
- base.extend(ClassMethods)
58
190
  end
59
191
 
60
192
  # All of the following validations are defined in the class scope of the model that you're interested in validating.
61
193
  # They offer a more declarative way of specifying when the model is valid and when it is not. It is recommended to use
62
194
  # these over the low-level calls to validate and validate_on_create when possible.
63
195
  module ClassMethods
196
+ def validate(*methods, &block)
197
+ methods << block if block_given?
198
+ write_inheritable_set(:validate, methods)
199
+ end
200
+
201
+ def validate_on_create(*methods, &block)
202
+ methods << block if block_given?
203
+ write_inheritable_set(:validate_on_create, methods)
204
+ end
205
+
206
+ def validate_on_update(*methods, &block)
207
+ methods << block if block_given?
208
+ write_inheritable_set(:validate_on_update, methods)
209
+ end
210
+
64
211
  # Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example:
65
212
  #
66
213
  # Model:
@@ -83,6 +230,7 @@ module ActiveRecord
83
230
  def validates_confirmation_of(*attr_names)
84
231
  configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save }
85
232
  configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
233
+ configuration[:message].gsub!(/\"/, '\\\\\"')
86
234
 
87
235
  for attr_name in attr_names
88
236
  attr_accessor "#{attr_name}_confirmation"
@@ -108,10 +256,11 @@ module ActiveRecord
108
256
  def validates_acceptance_of(*attr_names)
109
257
  configuration = { :message => ActiveRecord::Errors.default_error_messages[:accepted], :on => :save }
110
258
  configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
259
+ configuration[:message].gsub!(/\"/, '\\\\\"')
111
260
 
112
261
  for attr_name in attr_names
113
262
  attr_accessor(attr_name)
114
- class_eval(%(#{validation_method(configuration[:on])} %{errors.add('#{attr_name}', '#{configuration[:message]}') unless #{attr_name}.nil? or #{attr_name} == "1"}))
263
+ class_eval(%(#{validation_method(configuration[:on])} %{errors.add('#{attr_name}', "#{configuration[:message]}") unless #{attr_name}.nil? or #{attr_name} == "1"}))
115
264
  end
116
265
  end
117
266
 
@@ -123,17 +272,61 @@ module ActiveRecord
123
272
  def validates_presence_of(*attr_names)
124
273
  configuration = { :message => ActiveRecord::Errors.default_error_messages[:empty], :on => :save }
125
274
  configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
275
+ configuration[:message].gsub!(/\"/, '\\\\\"')
126
276
 
127
277
  for attr_name in attr_names
128
278
  class_eval(%(#{validation_method(configuration[:on])} %{errors.add_on_empty('#{attr_name}', "#{configuration[:message]}")}))
129
279
  end
130
280
  end
131
-
281
+
282
+
283
+ DEFAULT_VALIDATION_OPTIONS = {
284
+ :on => :save,
285
+ :allow_nil => false,
286
+ :message => nil
287
+ }.freeze
288
+
289
+ DEFAULT_SIZE_VALIDATION_OPTIONS = DEFAULT_VALIDATION_OPTIONS.merge(
290
+ :too_long => ActiveRecord::Errors.default_error_messages[:too_long],
291
+ :too_short => ActiveRecord::Errors.default_error_messages[:too_short],
292
+ :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length]
293
+ ).freeze
294
+
295
+ ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze
296
+
297
+
298
+ # Validates each attribute against a block.
299
+ #
300
+ # class Person < ActiveRecord::Base
301
+ # validates_each :first_name, :last_name do |record, attr|
302
+ # record.errors.add attr, 'starts with z.' if attr[0] == ?z
303
+ # end
304
+ # end
305
+ #
306
+ # Options:
307
+ # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
308
+ # * <tt>allow_nil</tt> - Skip validation if attribute is nil.
309
+ def validates_each(*attrs)
310
+ options = attrs.last.is_a?(Hash) ? attrs.pop.symbolize_keys : {}
311
+ attrs = attrs.flatten
312
+
313
+ # Declare the validation.
314
+ send(validation_method(options[:on] || :save)) do |record|
315
+ attrs.each do |attr|
316
+ value = record.send(attr)
317
+ next if value.nil? && options[:allow_nil]
318
+ yield record, attr, value
319
+ end
320
+ end
321
+ end
322
+
323
+
132
324
  # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time:
133
325
  #
134
326
  # class Person < ActiveRecord::Base
135
327
  # validates_length_of :first_name, :maximum=>30
136
328
  # validates_length_of :last_name, :maximum=>30, :message=>"less than %d if you don't mind"
329
+ # validates_length_of :fax, :in => 7..32, :allow_nil => true
137
330
  # validates_length_of :user_name, :within => 6..20, :too_long => "pick a shorter name", :too_short => "pick a longer name"
138
331
  # validates_length_of :fav_bra_size, :minimum=>1, :too_short=>"please enter at least %d character"
139
332
  # validates_length_of :smurf_leader, :is=>4, :message=>"papa is spelled with %d characters... don't play me."
@@ -145,55 +338,68 @@ module ActiveRecord
145
338
  # * <tt>is</tt> - The exact size of the attribute
146
339
  # * <tt>within</tt> - A range specifying the minimum and maximum size of the attribute
147
340
  # * <tt>in</tt> - A synonym(or alias) for :within
148
- #
341
+ # * <tt>allow_nil</tt> - Attribute may be nil; skip validation.
342
+ #
149
343
  # * <tt>too_long</tt> - The error message if the attribute goes over the maximum (default is: "is too long (max is %d characters)")
150
344
  # * <tt>too_short</tt> - The error message if the attribute goes under the minimum (default is: "is too short (min is %d characters)")
151
345
  # * <tt>wrong_length</tt> - The error message if using the :is method and the attribute is the wrong size (default is: "is the wrong length (should be %d characters)")
152
346
  # * <tt>message</tt> - The error message to use for a :minimum, :maximum, or :is violation. An alias of the appropriate too_long/too_short/wrong_length message
153
347
  # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
154
- def validates_length_of(*attr_names)
155
- configuration = { :too_long => ActiveRecord::Errors.default_error_messages[:too_long], :too_short => ActiveRecord::Errors.default_error_messages[:too_short], :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length], :on => :save }
156
- configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
348
+ def validates_length_of(*attrs)
349
+ # Merge given options with defaults.
350
+ options = DEFAULT_SIZE_VALIDATION_OPTIONS.dup
351
+ options.update(attrs.pop.symbolize_keys) if attrs.last.is_a?(Hash)
352
+
353
+ # Ensure that one and only one range option is specified.
354
+ range_options = ALL_RANGE_OPTIONS & options.keys
355
+ case range_options.size
356
+ when 0
357
+ raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.'
358
+ when 1
359
+ # Valid number of options; do nothing.
360
+ else
361
+ raise ArgumentError, 'Too many range options specified. Choose only one.'
362
+ end
157
363
 
158
- # you must use one of 4 options, :within, :maximum, :minimum, or :is
159
- within = configuration[:within] || configuration[:in]
160
- maximum = configuration[:maximum]
161
- minimum = configuration[:minimum]
162
- is = configuration[:is]
163
-
164
- raise(ArgumentError, "The :within, :maximum, :minimum, or :is options must be passed in the configuration hash") unless within or maximum or minimum or is
165
- # but not more than 1 of them at a time
166
- options_used = 0
167
- options_used += 1 if within
168
- options_used += 1 if maximum
169
- options_used += 1 if minimum
170
- options_used += 1 if is
171
- raise(ArgumentError, "The :within, :maximum, :minimum, and :is options are mutually exclusive") if options_used > 1
172
-
173
- option_to_use = within || maximum || minimum || is
174
- for attr_name in attr_names
175
- if within
176
- raise(ArgumentError, "The :within option must be a Range") unless within.kind_of?(Range)
177
- class_eval(%(#{validation_method(configuration[:on])} %{errors.add_on_boundary_breaking('#{attr_name}', #{within}, "#{configuration[:too_long]}", "#{configuration[:too_short]}")}))
178
- elsif maximum
179
- raise(ArgumentError, "The :maximum option must be a Fixnum") unless maximum.kind_of?(Fixnum)
180
- msg = configuration[:message] || configuration[:too_long]
181
- msg = (msg % maximum) rescue msg
182
- class_eval(%(#{validation_method(configuration[:on])} %{errors.add( '#{attr_name}', '#{msg}') if #{attr_name}.to_s.length > #{maximum} }))
183
- elsif minimum
184
- raise(ArgumentError, "The :minimum option must be a Fixnum") unless minimum.kind_of?(Fixnum)
185
- msg = configuration[:message] || configuration[:too_short]
186
- msg = (msg % minimum) rescue msg
187
- class_eval(%(#{validation_method(configuration[:on])} %{errors.add( '#{attr_name}', '#{msg}') if #{attr_name}.to_s.length < #{minimum} }))
188
- else
189
- raise(ArgumentError, "The :is option must be a Fixnum") unless is.kind_of?(Fixnum)
190
- msg = configuration[:message] || configuration[:wrong_length]
191
- msg = (msg % is) rescue msg
192
- class_eval(%(#{validation_method(configuration[:on])} %{errors.add( '#{attr_name}', '#{msg}') if #{attr_name}.to_s.length != #{is} }))
364
+ # Get range option and value.
365
+ option = range_options.first
366
+ option_value = options[range_options.first]
367
+
368
+ # Declare different validations per option.
369
+ case range_options.first
370
+ when :within, :in
371
+ raise ArgumentError, ':within must be a Range' unless option_value.is_a?(Range)
372
+ validates_each(attrs, options) do |record, attr|
373
+ next if record.send(attr).nil? and options[:allow_nil]
374
+ record.errors.add_on_boundary_breaking(attr, option_value, options[:too_long], options[:too_short])
375
+ end
376
+ when :is
377
+ raise ArgumentError, ':is must be a nonnegative Integer' unless option_value.is_a?(Integer) and option_value >= 0
378
+ message = options[:message] || options[:wrong_length]
379
+ message = (message % option_value) rescue message
380
+ validates_each(attrs, options) do |record, attr, value|
381
+ record.errors.add(attr, message) if value.nil? or value.size != option_value
193
382
  end
194
- end
383
+ when :minimum
384
+ raise ArgumentError, ':minimum must be a nonnegative Integer' unless option_value.is_a?(Integer) and option_value >= 0
385
+ message = options[:message] || options[:too_short]
386
+ message = (message % option_value) rescue message
387
+ validates_each(attrs, options) do |record, attr, value|
388
+ record.errors.add(attr, message) if value.nil? or value.size < option_value
389
+ end
390
+ when :maximum
391
+ raise ArgumentError, ':maximum must be a nonnegative Integer' unless option_value.is_a?(Integer) and option_value >= 0
392
+ message = options[:message] || options[:too_long]
393
+ message = (message % option_value) rescue message
394
+ validates_each(attrs, options) do |record, attr, value|
395
+ record.errors.add(attr, message) if value.nil? or value.size > option_value
396
+ end
397
+ end
195
398
  end
196
399
 
400
+ alias_method :validates_size_of, :validates_length_of
401
+
402
+
197
403
  # Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user
198
404
  # can be named "davidhh".
199
405
  #
@@ -210,12 +416,13 @@ module ActiveRecord
210
416
  def validates_uniqueness_of(*attr_names)
211
417
  configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken] }
212
418
  configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
419
+ configuration[:message].gsub!(/\"/, '\\\\\"')
213
420
 
214
421
  for attr_name in attr_names
215
422
  if scope = configuration[:scope]
216
- class_eval(%(validate %{errors.add('#{attr_name}', '#{configuration[:message]}') if self.class.find_first(new_record? ? ['#{attr_name} = ? AND #{scope} = ?', #{attr_name}, #{scope}] : ["#{attr_name} = ? AND \\\#{self.class.primary_key} <> ? AND #{scope} = ?", #{attr_name}, id, #{scope}])}))
423
+ class_eval(%(validate %{errors.add('#{attr_name}', "#{configuration[:message]}") if self.class.find_first(new_record? ? ['#{attr_name} = ? AND #{scope} = ?', #{attr_name}, #{scope}] : ["#{attr_name} = ? AND \\\#{self.class.primary_key} <> ? AND #{scope} = ?", #{attr_name}, id, #{scope}])}))
217
424
  else
218
- class_eval(%(validate %{errors.add('#{attr_name}', '#{configuration[:message]}') if self.class.find_first(new_record? ? ['#{attr_name} = ?', #{attr_name}] : ["#{attr_name} = ? AND \\\#{self.class.primary_key} <> ?", #{attr_name}, id])}))
425
+ class_eval(%(validate %{errors.add('#{attr_name}', "#{configuration[:message]}") if self.class.find_first(new_record? ? ['#{attr_name} = ?', #{attr_name}] : ["#{attr_name} = ? AND \\\#{self.class.primary_key} <> ?", #{attr_name}, id])}))
219
426
  end
220
427
  end
221
428
  end
@@ -236,6 +443,7 @@ module ActiveRecord
236
443
  def validates_format_of(*attr_names)
237
444
  configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil }
238
445
  configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
446
+ configuration[:message].gsub!(/\"/, '\\\\\"')
239
447
 
240
448
  raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp)
241
449
 
@@ -253,11 +461,13 @@ module ActiveRecord
253
461
  #
254
462
  # Configuration options:
255
463
  # * <tt>in</tt> - An enumerable object of available items
256
- # * <tt>message</tt> - Specifieds a customer error message (default is: "is not included in the list")
464
+ # * <tt>message</tt> - Specifies a customer error message (default is: "is not included in the list")
257
465
  # * <tt>allow_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false)
258
466
  def validates_inclusion_of(*attr_names)
259
467
  configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save }
260
468
  configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
469
+ configuration[:message].gsub!(/\"/, '\\\\\"')
470
+
261
471
  enum = configuration[:in] || configuration[:within]
262
472
  allow_nil = configuration[:allow_nil]
263
473
 
@@ -272,7 +482,7 @@ module ActiveRecord
272
482
  end
273
483
  end
274
484
 
275
- # Validates whether the associated object or objects are all themselves valid. Works with any kind of assocation.
485
+ # Validates whether the associated object or objects are all themselves valid. Works with any kind of association.
276
486
  #
277
487
  # class Book < ActiveRecord::Base
278
488
  # has_many :pages
@@ -296,17 +506,23 @@ module ActiveRecord
296
506
  def validates_associated(*attr_names)
297
507
  configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save }
298
508
  configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
509
+ configuration[:message].gsub!(/\"/, '\\\\\"')
299
510
 
300
511
  for attr_name in attr_names
301
512
  class_eval(%(#{validation_method(configuration[:on])} %{
302
513
  errors.add("#{attr_name}", "#{configuration[:message]}") unless
303
- (#{attr_name}.is_a?(Array) ? #{attr_name} : [#{attr_name}]).inject(true){ |memo, record| memo and (record.nil? or record.valid?) }
514
+ (#{attr_name}.is_a?(Array) ? #{attr_name} : [#{attr_name}]).inject(true){ |memo, record| (record.nil? or record.valid?) and memo }
304
515
  }))
305
516
  end
306
517
  end
307
518
 
308
519
 
309
520
  private
521
+ def write_inheritable_set(key, methods)
522
+ existing_methods = read_inheritable_attribute(key) || []
523
+ write_inheritable_attribute(key, methods | existing_methods)
524
+ end
525
+
310
526
  def validation_method(on)
311
527
  case on
312
528
  when :save then :validate
@@ -369,7 +585,7 @@ module ActiveRecord
369
585
 
370
586
  private
371
587
  def run_validations(validation_method)
372
- validations = self.class.read_inheritable_attribute(validation_method.to_s)
588
+ validations = self.class.read_inheritable_attribute(validation_method.to_sym)
373
589
  if validations.nil? then return end
374
590
  validations.each do |validation|
375
591
  if validation.is_a?(Symbol)
@@ -378,7 +594,7 @@ module ActiveRecord
378
594
  eval(validation, binding)
379
595
  elsif validation_block?(validation)
380
596
  validation.call(self)
381
- elsif filter_class?(validation, validation_method)
597
+ elsif validation_class?(validation, validation_method)
382
598
  validation.send(validation_method, self)
383
599
  else
384
600
  raise(
@@ -398,137 +614,4 @@ module ActiveRecord
398
614
  validation.respond_to?(validation_method)
399
615
  end
400
616
  end
401
-
402
- # Active Record validation is reported to and from this object, which is used by Base#save to
403
- # determine whether the object in a valid state to be saved. See usage example in Validations.
404
- class Errors
405
- def initialize(base) # :nodoc:
406
- @base, @errors = base, {}
407
- end
408
-
409
- @@default_error_messages = {
410
- :inclusion => "is not included in the list",
411
- :invalid => "is invalid",
412
- :confirmation => "doesn't match confirmation",
413
- :accepted => "must be accepted",
414
- :empty => "can't be empty",
415
- :too_long => "is too long (max is %d characters)",
416
- :too_short => "is too short (min is %d characters)",
417
- :wrong_length => "is the wrong length (should be %d characters)",
418
- :taken => "has already been taken",
419
- }
420
- cattr_accessor :default_error_messages
421
-
422
-
423
- # Adds an error to the base object instead of any particular attribute. This is used
424
- # to report errors that doesn't tie to any specific attribute, but rather to the object
425
- # as a whole. These error messages doesn't get prepended with any field name when iterating
426
- # with each_full, so they should be complete sentences.
427
- def add_to_base(msg)
428
- add(:base, msg)
429
- end
430
-
431
- # Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
432
- # for the same attribute and ensure that this error object returns false when asked if +empty?+. More than one
433
- # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
434
- # If no +msg+ is supplied, "invalid" is assumed.
435
- def add(attribute, msg = @@default_error_messages[:invalid])
436
- @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
437
- @errors[attribute.to_s] << msg
438
- end
439
-
440
- # Will add an error message to each of the attributes in +attributes+ that is empty (defined by <tt>attribute_present?</tt>).
441
- def add_on_empty(attributes, msg = @@default_error_messages[:empty])
442
- for attr in [attributes].flatten
443
- value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
444
- is_empty = value.respond_to?("empty?") ? value.empty? : false
445
- add(attr, msg) unless !value.nil? && !is_empty
446
- end
447
- end
448
-
449
- # Will add an error message to each of the attributes in +attributes+ that has a length outside of the passed boundary +range+.
450
- # If the length is above the boundary, the too_long_msg message will be used. If below, the too_short_msg.
451
- def add_on_boundary_breaking(attributes, range, too_long_msg = @@default_error_messages[:too_long], too_short_msg = @@default_error_messages[:too_short])
452
- for attr in [attributes].flatten
453
- value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
454
- add(attr, too_short_msg % range.begin) if value && value.length < range.begin
455
- add(attr, too_long_msg % range.end) if value && value.length > range.end
456
- end
457
- end
458
-
459
- alias :add_on_boundry_breaking :add_on_boundary_breaking
460
-
461
- # Returns true if the specified +attribute+ has errors associated with it.
462
- def invalid?(attribute)
463
- !@errors[attribute.to_s].nil?
464
- end
465
-
466
- # * Returns nil, if no errors are associated with the specified +attribute+.
467
- # * Returns the error message, if one error is associated with the specified +attribute+.
468
- # * Returns an array of error messages, if more than one error is associated with the specified +attribute+.
469
- def on(attribute)
470
- if @errors[attribute.to_s].nil?
471
- nil
472
- elsif @errors[attribute.to_s].length == 1
473
- @errors[attribute.to_s].first
474
- else
475
- @errors[attribute.to_s]
476
- end
477
- end
478
-
479
- alias :[] :on
480
-
481
- # Returns errors assigned to base object through add_to_base according to the normal rules of on(attribute).
482
- def on_base
483
- on(:base)
484
- end
485
-
486
- # Yields each attribute and associated message per error added.
487
- def each
488
- @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
489
- end
490
-
491
- # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
492
- # through iteration as "First name can't be empty".
493
- def each_full
494
- full_messages.each { |msg| yield msg }
495
- end
496
-
497
- # Returns all the full error messages in an array.
498
- def full_messages
499
- full_messages = []
500
-
501
- @errors.each_key do |attr|
502
- @errors[attr].each do |msg|
503
- next if msg.nil?
504
-
505
- if attr == "base"
506
- full_messages << msg
507
- else
508
- full_messages << @base.class.human_attribute_name(attr) + " " + msg
509
- end
510
- end
511
- end
512
-
513
- return full_messages
514
- end
515
-
516
- # Returns true if no errors have been added.
517
- def empty?
518
- return @errors.empty?
519
- end
520
-
521
- # Removes all the errors that have been added.
522
- def clear
523
- @errors = {}
524
- end
525
-
526
- # Returns the total number of errors added. Two errors added to the same attribute will be counted as such
527
- # with this as well.
528
- def count
529
- error_count = 0
530
- @errors.each_value { |attribute| error_count += attribute.length }
531
- error_count
532
- end
533
- end
534
617
  end