1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
|
module ActiveRecord
# Active Records implement validation by overwriting Base#validate (or the variations, +validate_on_create+ and
# +validate_on_update+). Each of these methods can inspect the state of the object, which usually means ensuring
# that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression).
#
# Example:
#
# class Person < ActiveRecord::Base
# protected
# def validate
# errors.add_on_empty %w( first_name last_name )
# errors.add("phone_number", "has invalid format") unless phone_number =~ /[0-9]*/
# end
#
# def validate_on_create # is only run the first time a new object is saved
# unless valid_discount?(membership_discount)
# errors.add("membership_discount", "has expired")
# end
# end
#
# def validate_on_update
# errors.add_to_base("No changes have occured") if unchanged_attributes?
# end
# end
#
# person = Person.new("first_name" => "David", "phone_number" => "what?")
# person.save # => false (and doesn't do the save)
# person.errors.empty? # => false
# person.count # => 2
# person.errors.on "last_name" # => "can't be empty"
# person.errors.on "phone_number" # => "has invalid format"
# person.each_full { |msg| puts msg } # => "Last name can't be empty\n" +
# "Phone number has invalid format"
#
# person.attributes = { "last_name" => "Heinemeier", "phone_number" => "555-555" }
# person.save # => true (and person is now saved in the database)
#
# An +Errors+ object is automatically created for every Active Record.
module Validations
VALIDATIONS = %w( validate validate_on_create validate_on_create )
def self.append_features(base) # :nodoc:
super
base.class_eval do
alias_method :save_without_validation, :save
alias_method :save, :save_with_validation
alias_method :update_attribute_without_validation_skipping, :update_attribute
alias_method :update_attribute, :update_attribute_with_validation_skipping
VALIDATIONS.each { |vd| base.class_eval("def self.#{vd}(*methods) write_inheritable_array(\"#{vd}\", methods - (read_inheritable_attribute(\"#{vd}\") || [])) end") }
end
base.extend(ClassMethods)
end
module ClassMethods
# Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example:
#
# Model:
# class Person < ActiveRecord::Base
# validate_confirmation :user_name, :password
# validate_confirmation :email_address, "should match confirmation"
# end
#
# View:
# <%= password_field "person", "password" %>
# <%= password_field "person", "password_confirmation" %>
#
# The person has to already have a password attribute (a column in the people table), but the password_confirmation is virtual.
# It exists only as an in-memory variable for validating the password.
#
# NOTE: This validation is only happening on create. When you want to update the record, you'll have to decide and pursue your
# own course of action.
def validate_confirmation(*attr_names)
error_message = attr_names.last.is_a?(String) ? attr_names.pop : "doesn't match confirmation"
for attr_name in attr_names
attr_accessor "#{attr_name}_confirmation"
class_eval <<-EOM
validate_on_create %{errors.add('#{attr_name}', "#{error_message}") unless #{attr_name} == #{attr_name}_confirmation}
EOM
end
end
# Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example:
#
# Model:
# class Person < ActiveRecord::Base
# validate_acceptance :terms_of_service
# validate_acceptance :eula, "must be abided"
# end
#
# View:
# <%= check_box "person", "terms_of_service" %>
#
# The terms_of_service attribute is entirely virtual. It's only used for validation at the time of creation. No database column is needed.
#
# NOTE: The agreement is considered valid if it's set to the string "1". This makes it easy to relate it to an HTML checkbox.
def validate_acceptance(*attr_names)
error_message = attr_names.last.is_a?(String) ? attr_names.pop : "must be accepted"
for attr_name in attr_names
attr_accessor(attr_name)
class_eval <<-EOM
validate_on_create %{errors.add('#{attr_name}', '#{error_message}') unless #{attr_name} == "1"}
EOM
end
end
end
# The validation process on save can be skipped by passing false. The regular Base#save method is
# replaced with this when the validations module is mixed in, which it is by default.
def save_with_validation(perform_validation = true)
if perform_validation && valid? || !perform_validation then save_without_validation else false end
end
# Updates a single attribute and saves the record without going through the normal validation procedure.
# This is especially useful for boolean flags on existing records. The regular +update_attribute+ method
# in Base is replaced with this when the validations module is mixed in, which it is by default.
def update_attribute_with_validation_skipping(name, value)
@attributes[name] = value
save(false)
end
# Runs validate and validate_on_create or validate_on_update and returns true if no errors were added otherwise false.
def valid?
errors.clear
run_validations(:validate)
validate
if new_record?
run_validations(:validate_on_create)
validate_on_create
else
run_validations(:validate_on_update)
validate_on_update
end
errors.empty?
end
# Returns the Errors object that holds all information about attribute error messages.
def errors
@errors = Errors.new(self) if @errors.nil?
@errors
end
protected
# Overwrite this method for validation checks on all saves and use Errors.add(field, msg) for invalid attributes.
def validate #:doc:
end
# Overwrite this method for validation checks used only on creation.
def validate_on_create #:doc:
end
# Overwrite this method for validation checks used only on updates.
def validate_on_update # :doc:
end
private
def run_validations(validation_method)
validations = self.class.read_inheritable_attribute(validation_method.to_s)
if validations.nil? then return end
validations.each do |validation|
if Symbol === validation
self.send(validation)
elsif String === validation
eval(validation, binding)
elsif validation_block?(validation)
validation.call(self)
elsif filter_class?(validation, validation_method)
validation.send(validation_method, self)
else
raise(
ActiveRecordError,
"Validations need to be either a symbol, string (to be eval'ed), proc/method, or " +
"class implementing a static validation method"
)
end
end
end
def validation_block?(validation)
validation.respond_to?("call") && (validation.arity == 1 || validation.arity == -1)
end
def validation_class?(validation, validation_method)
validation.respond_to?(validation_method)
end
end
# Active Record validation is reported to and from this object, which is used by Base#save to
# determine whether the object in a valid state to be saved. See usage example in Validations.
class Errors
def initialize(base) # :nodoc:
@base, @errors = base, {}
end
# Adds an error to the base object instead of any particular attribute. This is used
# to report errors that doesn't tie to any specific attribute, but rather to the object
# as a whole. These error messages doesn't get prepended with any field name when iterating
# with each_full, so they should be complete sentences.
def add_to_base(msg)
add(:base, msg)
end
# Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt>
# for the same attribute and ensure that this error object returns false when asked if +empty?+. More than one
# error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>.
# If no +msg+ is supplied, "invalid" is assumed.
def add(attribute, msg = "invalid")
@errors[attribute.to_s] = [] if @errors[attribute.to_s].nil?
@errors[attribute.to_s] << msg
end
# Will add an error message to each of the attributes in +attributes+ that is empty (defined by <tt>attribute_present?</tt>).
def add_on_empty(attributes, msg = "can't be empty")
[attributes].flatten.each { |attr| add(attr, msg) unless @base.attribute_present?(attr.to_s) }
end
# Will add an error message to each of the attributes in +attributes+ that has a length outside of the passed boundary +range+.
# If the length is above the boundary, the too_long_msg message will be used. If below, the too_short_msg.
def add_on_boundary_breaking(attributes, range, too_long_msg = "is too long (max is %d characters)", too_short_msg = "is too short (min is %d characters)")
for attr in [attributes].flatten
add(attr, too_short_msg % range.begin) if @base.attribute_present?(attr.to_s) && @base.send(attr.to_s).length < range.begin
add(attr, too_long_msg % range.end) if @base.attribute_present?(attr.to_s) && @base.send(attr.to_s).length > range.end
end
end
alias :add_on_boundry_breaking :add_on_boundary_breaking
# Returns true if the specified +attribute+ has errors associated with it.
def invalid?(attribute)
!@errors[attribute.to_s].nil?
end
# * Returns nil, if no errors are associated with the specified +attribute+.
# * Returns the error message, if one error is associated with the specified +attribute+.
# * Returns an array of error messages, if more than one error is associated with the specified +attribute+.
def on(attribute)
if @errors[attribute.to_s].nil?
nil
elsif @errors[attribute.to_s].length == 1
@errors[attribute.to_s].first
else
@errors[attribute.to_s]
end
end
alias :[] :on
# Returns errors assigned to base object through add_to_base according to the normal rules of on(attribute).
def on_base
on(:base)
end
# Yields each attribute and associated message per error added.
def each
@errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } }
end
# Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned
# through iteration as "First name can't be empty".
def each_full
full_messages.each { |msg| yield msg }
end
# Returns all the full error messages in an array.
def full_messages
full_messages = []
@errors.each_key do |attr|
@errors[attr].each do |msg|
if attr == "base"
full_messages << msg
else
full_messages << @base.class.human_attribute_name(attr) + " " + msg
end
end
end
return full_messages
end
# Returns true if no errors have been added.
def empty?
return @errors.empty?
end
# Removes all the errors that have been added.
def clear
@errors = {}
end
# Returns the total number of errors added. Two errors added to the same attribute will be counted as such
# with this as well.
def count
error_count = 0
@errors.each_value { |attribute| error_count += attribute.length }
error_count
end
end
end
|