formvalidator 0.1.3 → 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +1 -1
- data/CHANGELOG +14 -2
- data/README +4 -1
- data/formvalidator.gemspec +3 -3
- data/formvalidator.rb +128 -59
- data/tests/regress.rb +10 -7
- metadata +62 -50
- data/TODO +0 -5
data/AUTHORS
CHANGED
@@ -1 +1 @@
|
|
1
|
-
Travis Whitton <
|
1
|
+
Travis Whitton <tinymountain@gmail.com>
|
data/CHANGELOG
CHANGED
@@ -22,7 +22,19 @@ o Wrote additional unit test.
|
|
22
22
|
--------------------------------------------------------------------------------
|
23
23
|
8/14/2003 version 0.1.3
|
24
24
|
|
25
|
-
o Fixed credit card type detection bugs
|
26
|
-
o Fixed a bug regarding invalid field accounting
|
25
|
+
o Fixed credit card type detection bugs.
|
26
|
+
o Fixed a bug regarding invalid field accounting.
|
27
27
|
|
28
28
|
--------------------------------------------------------------------------------
|
29
|
+
10/26/2004 version 0.1.4
|
30
|
+
|
31
|
+
o Changed obsolete to_a() calls to use Array().
|
32
|
+
o Changed type() calls to use class().
|
33
|
+
o Fixed apply_string_constraint() to handle multiple elements.
|
34
|
+
o Fixed apply_regexp_constraint() to handle multiple elements.
|
35
|
+
o Remembered to increment version number.
|
36
|
+
|
37
|
+
--------------------------------------------------------------------------------
|
38
|
+
8/28/2010 version 0.1.5
|
39
|
+
|
40
|
+
o Bugfix for passing hash as profile to constructor
|
data/README
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
|
2
2
|
FormValidator Library
|
3
3
|
|
4
|
-
|
4
|
+
Aug. 28, 2010 Travis Whitton <tinymountain@gmail.com>
|
5
5
|
|
6
6
|
FormValidator is a full featured form validation library written in pure ruby.
|
7
7
|
See README.rdoc for more details.
|
@@ -12,6 +12,9 @@ See README.rdoc for more details.
|
|
12
12
|
build the docs if you want to
|
13
13
|
3. rdoc --main README.rdoc formvalidator.rb README.rdoc
|
14
14
|
|
15
|
+
[Alternate installation]
|
16
|
+
1. gem install formvalidator
|
17
|
+
|
15
18
|
[Copying]
|
16
19
|
FormValidator extension library is copywrited free software by Travis Whitton
|
17
20
|
<whitton@atlantic.net>. You can redistribute it under the terms specified in
|
data/formvalidator.gemspec
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = %q{formvalidator}
|
3
|
-
s.version = "0.1.
|
3
|
+
s.version = "0.1.5"
|
4
4
|
s.date = Time.now
|
5
5
|
s.summary = %q{FormValidator is a Ruby port of Perl's Data::FormValidator library.}
|
6
6
|
s.author = %q{Travis Whitton}
|
7
|
-
s.email = %q{
|
8
|
-
s.homepage = %q{http://
|
7
|
+
s.email = %q{tinymountain@gmail.com}
|
8
|
+
s.homepage = %q{http://github.com/tmountain/FormValidator}
|
9
9
|
s.require_path = %q{.}
|
10
10
|
s.autorequire = %q{formvalidator}
|
11
11
|
s.files = Dir.glob('**/*')
|
data/formvalidator.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
class FormValidator
|
2
|
-
VERSION = "0.1.
|
2
|
+
VERSION = "0.1.5"
|
3
3
|
|
4
4
|
# Constructor.
|
5
5
|
def initialize(profile=nil)
|
@@ -76,7 +76,7 @@ class FormValidator
|
|
76
76
|
if Hash === profile
|
77
77
|
@profile = profile
|
78
78
|
else
|
79
|
-
load_profiles
|
79
|
+
load_profiles if @profile_file
|
80
80
|
@profile = @profiles[profile]
|
81
81
|
end
|
82
82
|
check_profile_syntax(@profile)
|
@@ -103,7 +103,7 @@ class FormValidator
|
|
103
103
|
|
104
104
|
# [:a, :b, :c, [:d, :e, [:f, :g]]] -> ["a", "b", "c", ["d", "e", ["f", "g"]]]
|
105
105
|
def strify_array(array)
|
106
|
-
array.
|
106
|
+
Array(array).map do |m|
|
107
107
|
m = (Array === m) ? strify_array(m) : m
|
108
108
|
m = (Hash === m) ? strify_hash(m) : m
|
109
109
|
Symbol === m ? m.to_s : m
|
@@ -175,7 +175,7 @@ class FormValidator
|
|
175
175
|
#
|
176
176
|
# :required => [:name, :age, :phone]
|
177
177
|
def required
|
178
|
-
@profile[:required].
|
178
|
+
Array(@profile[:required]).each do |field|
|
179
179
|
@required_fields << field
|
180
180
|
@missing_fields.push(field) if @form[field].to_s.empty?
|
181
181
|
end
|
@@ -191,7 +191,7 @@ class FormValidator
|
|
191
191
|
#
|
192
192
|
# :optional => [:name, :age, :phone]
|
193
193
|
def optional
|
194
|
-
@profile[:optional].
|
194
|
+
Array(@profile[:optional]).each do |field|
|
195
195
|
@optional_fields << field unless @optional_fields.include?(field)
|
196
196
|
end
|
197
197
|
@optional_fields
|
@@ -206,7 +206,7 @@ class FormValidator
|
|
206
206
|
# :required_regexp => /name/
|
207
207
|
def required_regexp
|
208
208
|
@form.keys.each do |elem|
|
209
|
-
@profile[:required_regexp].
|
209
|
+
Array(@profile[:required_regexp]).each do |regexp|
|
210
210
|
regexp = Regexp.new(regexp)
|
211
211
|
if elem =~ regexp
|
212
212
|
@required_fields << elem unless @required_fields.include?(elem)
|
@@ -225,7 +225,7 @@ class FormValidator
|
|
225
225
|
# :required_regexp => /name/
|
226
226
|
def optional_regexp
|
227
227
|
@form.keys.each do |elem|
|
228
|
-
@profile[:optional_regexp].
|
228
|
+
Array(@profile[:optional_regexp]).each do |regexp|
|
229
229
|
regexp = Regexp.new(regexp)
|
230
230
|
if elem =~ regexp
|
231
231
|
@optional_fields << elem unless @optional_fields.include?(elem)
|
@@ -297,14 +297,14 @@ class FormValidator
|
|
297
297
|
if Hash === deps
|
298
298
|
deps.keys.each do |key|
|
299
299
|
if @form[field].to_s == key
|
300
|
-
deps[key].
|
300
|
+
Array(deps[key]).each do |dep|
|
301
301
|
@missing_fields.push(dep) if @form[dep].to_s.empty?
|
302
302
|
end
|
303
303
|
end
|
304
304
|
end
|
305
305
|
else
|
306
306
|
if not @form[field].to_s.empty?
|
307
|
-
deps.
|
307
|
+
Array(deps).each do |dep|
|
308
308
|
@missing_fields.push(dep) if @form[dep].to_s.empty?
|
309
309
|
end
|
310
310
|
end
|
@@ -341,11 +341,11 @@ class FormValidator
|
|
341
341
|
#
|
342
342
|
# :filters => :strip
|
343
343
|
def filters
|
344
|
-
@profile[:filters].
|
344
|
+
Array(@profile[:filters]).each do |filter|
|
345
345
|
if respond_to?("filter_#{filter}".intern)
|
346
346
|
@form.keys.each do |field|
|
347
347
|
# If a key has multiple elements, apply filter to each element
|
348
|
-
if @form[field].
|
348
|
+
if Array(@form[field]).length > 1
|
349
349
|
@form[field].each_index do |i|
|
350
350
|
elem = @form[field][i]
|
351
351
|
@form[field][i] = self.send("filter_#{filter}".intern, elem)
|
@@ -369,11 +369,11 @@ class FormValidator
|
|
369
369
|
#
|
370
370
|
# :field_filters => { :home_phone => :phone }
|
371
371
|
def field_filters
|
372
|
-
@profile[:field_filters].
|
373
|
-
filters.
|
372
|
+
Array(@profile[:field_filters]).each do |field,filters|
|
373
|
+
Array(filters).each do |filter|
|
374
374
|
if respond_to?("filter_#{filter}".intern)
|
375
375
|
# If a key has multiple elements, apply filter to each element
|
376
|
-
if @form[field].
|
376
|
+
if Array(@form[field]).length > 1
|
377
377
|
@form[field].each_index do |i|
|
378
378
|
elem = @form[field][i]
|
379
379
|
@form[field][i] = self.send("filter_#{filter}".intern, elem)
|
@@ -394,12 +394,12 @@ class FormValidator
|
|
394
394
|
#
|
395
395
|
# :field_filter_regexp_map => { /name/ => :capitalize }
|
396
396
|
def field_filter_regexp_map
|
397
|
-
@profile[:field_filter_regexp_map].
|
398
|
-
filters.
|
397
|
+
Array(@profile[:field_filter_regexp_map]).each do |re,filters|
|
398
|
+
Array(filters).each do |filter|
|
399
399
|
if respond_to?("filter_#{filter}".intern)
|
400
400
|
@form.keys.select {|key| key =~ re}.each do |match|
|
401
401
|
# If a key has multiple elements, apply filter to each element
|
402
|
-
if @form[match].
|
402
|
+
if Array(@form[match]).length > 1
|
403
403
|
@form[match].each_index do |i|
|
404
404
|
elem = @form[match][i]
|
405
405
|
@form[match][i] = self.send("filter_#{filter}".intern, elem)
|
@@ -435,7 +435,7 @@ class FormValidator
|
|
435
435
|
#
|
436
436
|
# :untaint_constraint_fields => %w{ name age }
|
437
437
|
def untaint_constraint_fields
|
438
|
-
@profile[:untaint_constraint_fields].
|
438
|
+
Array(@profile[:untaint_constraint_fields]).each do |field|
|
439
439
|
@untaint_fields.push(field)
|
440
440
|
end
|
441
441
|
end
|
@@ -505,7 +505,7 @@ class FormValidator
|
|
505
505
|
# Valid constraint objects are String, Hash, Array, Proc, and Regexp.
|
506
506
|
def do_constraint(key, constraints)
|
507
507
|
constraints.each do |constraint|
|
508
|
-
type = constraint.
|
508
|
+
type = constraint.class.to_s.intern
|
509
509
|
case type
|
510
510
|
when :String
|
511
511
|
apply_string_constraint(key, constraint)
|
@@ -540,39 +540,83 @@ class FormValidator
|
|
540
540
|
|
541
541
|
# Applies a builtin constraint to form[key]
|
542
542
|
def apply_string_constraint(key, constraint)
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
@form[key]
|
548
|
-
|
543
|
+
### New code to handle multiple elements (beware!)
|
544
|
+
if Array(@form[key]).length > 1
|
545
|
+
index = 0
|
546
|
+
Array(@form[key]).each do |value|
|
547
|
+
res = self.send("match_#{constraint}".intern, @form[key][index].to_s)
|
548
|
+
if res
|
549
|
+
if untaint?(key)
|
550
|
+
@form[key][index] = res
|
551
|
+
@form[key][index].untaint
|
552
|
+
end
|
553
|
+
else
|
554
|
+
@form[key].delete_at(index)
|
555
|
+
@invalid_fields[key] ||= []
|
556
|
+
unless @invalid_fields[key].include?(constraint)
|
557
|
+
@invalid_fields[key].push(constraint)
|
558
|
+
end
|
559
|
+
nil
|
560
|
+
end
|
561
|
+
index += 1
|
549
562
|
end
|
563
|
+
### End new code
|
550
564
|
else
|
551
|
-
@form.
|
552
|
-
|
553
|
-
|
554
|
-
|
565
|
+
res = self.send("match_#{constraint}".intern, @form[key].to_s)
|
566
|
+
if res
|
567
|
+
if untaint?(key)
|
568
|
+
@form[key] = res
|
569
|
+
@form[key].untaint
|
570
|
+
end
|
571
|
+
else
|
572
|
+
@form.delete(key)
|
573
|
+
@invalid_fields[key] ||= []
|
574
|
+
unless @invalid_fields[key].include?(constraint)
|
575
|
+
@invalid_fields[key].push(constraint)
|
576
|
+
end
|
577
|
+
nil
|
555
578
|
end
|
556
|
-
nil
|
557
579
|
end
|
558
580
|
end
|
559
581
|
|
560
582
|
# Applies regexp constraint to form[key]
|
561
583
|
def apply_regexp_constraint(key, constraint)
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
@form[key]
|
567
|
-
|
584
|
+
### New code to handle multiple elements (beware!)
|
585
|
+
if Array(@form[key]).length > 1
|
586
|
+
index = 0
|
587
|
+
Array(@form[key]).each do |value|
|
588
|
+
m = constraint.match(@form[key][index].to_s)
|
589
|
+
if m
|
590
|
+
if untaint?(key)
|
591
|
+
@form[key][index] = m[0]
|
592
|
+
@form[key][index].untaint
|
593
|
+
end
|
594
|
+
else
|
595
|
+
@form[key].delete_at(index)
|
596
|
+
@invalid_fields[key] ||= []
|
597
|
+
unless @invalid_fields[key].include?(constraint.inspect)
|
598
|
+
@invalid_fields[key].push(constraint.inspect)
|
599
|
+
end
|
600
|
+
nil
|
601
|
+
end
|
602
|
+
index += 1
|
568
603
|
end
|
604
|
+
### End new code
|
569
605
|
else
|
570
|
-
@form.
|
571
|
-
|
572
|
-
|
573
|
-
|
606
|
+
m = constraint.match(@form[key].to_s)
|
607
|
+
if m
|
608
|
+
if untaint?(key)
|
609
|
+
@form[key] = m[0]
|
610
|
+
@form[key].untaint
|
611
|
+
end
|
612
|
+
else
|
613
|
+
@form.delete(key)
|
614
|
+
@invalid_fields[key] ||= []
|
615
|
+
unless @invalid_fields[key].include?(constraint.inspect)
|
616
|
+
@invalid_fields[key].push(constraint.inspect)
|
617
|
+
end
|
618
|
+
nil
|
574
619
|
end
|
575
|
-
nil
|
576
620
|
end
|
577
621
|
end
|
578
622
|
|
@@ -599,10 +643,11 @@ class FormValidator
|
|
599
643
|
# If an optional name field is specified then it will be listed as
|
600
644
|
# the failed constraint in the invalid_fields hash.
|
601
645
|
def apply_hash_constraint(key, constraint)
|
602
|
-
name
|
603
|
-
action
|
604
|
-
params
|
605
|
-
res
|
646
|
+
name = constraint["name"]
|
647
|
+
action = constraint["constraint"]
|
648
|
+
params = constraint["params"]
|
649
|
+
res = false
|
650
|
+
skip_end = false
|
606
651
|
|
607
652
|
# In order to call a builtin or proc, params and action must be present.
|
608
653
|
if action and params
|
@@ -615,21 +660,45 @@ class FormValidator
|
|
615
660
|
end
|
616
661
|
|
617
662
|
if Regexp === action
|
618
|
-
|
619
|
-
|
620
|
-
|
663
|
+
### New code to handle multiple elements (beware!)
|
664
|
+
if Array(@form[key]).length > 1
|
665
|
+
index = 0
|
666
|
+
skip_end = true
|
667
|
+
Array(@form[key]).each do |value|
|
668
|
+
m = action.match(value)
|
669
|
+
res = m[0] if m
|
670
|
+
if res
|
671
|
+
@form[key][index] = res if untaint?(key)
|
672
|
+
else
|
673
|
+
@form[key].delete_at(index)
|
674
|
+
constraint = (name) ? name : constraint
|
675
|
+
@invalid_fields[key] ||= []
|
676
|
+
unless @invalid_fields[key].include?(constraint)
|
677
|
+
@invalid_fields[key].push(constraint)
|
678
|
+
end
|
679
|
+
nil
|
680
|
+
end
|
681
|
+
index += 1
|
682
|
+
end
|
683
|
+
### End new code
|
684
|
+
else
|
685
|
+
m = action.match(@form[key].to_s)
|
686
|
+
res = m[0] if m
|
687
|
+
end
|
621
688
|
end
|
622
689
|
|
623
|
-
if
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
@invalid_fields[key].
|
690
|
+
if not skip_end
|
691
|
+
if res
|
692
|
+
@form[key] = res if untaint?(key)
|
693
|
+
else
|
694
|
+
@form.delete(key)
|
695
|
+
constraint = (name) ? name : constraint
|
696
|
+
@invalid_fields[key] ||= []
|
697
|
+
unless @invalid_fields[key].include?(constraint)
|
698
|
+
@invalid_fields[key].push(constraint)
|
699
|
+
end
|
700
|
+
nil
|
631
701
|
end
|
632
|
-
nil
|
633
702
|
end
|
634
703
|
end
|
635
704
|
end # module ConstraintHelpers
|
@@ -758,14 +827,14 @@ class FormValidator
|
|
758
827
|
|
759
828
|
# Matches a US state.
|
760
829
|
def match_state(state)
|
761
|
-
state = (state.
|
830
|
+
state = (state.class == String) ? state.intern : state
|
762
831
|
index = STATES.index(state)
|
763
832
|
(index) ? STATES[index].to_s : nil
|
764
833
|
end
|
765
834
|
|
766
835
|
# Matches a Canadian province.
|
767
836
|
def match_province(prov)
|
768
|
-
prov = (prov.
|
837
|
+
prov = (prov.class == String) ? prov.intern : prov
|
769
838
|
index = PROVINCES.index(prov)
|
770
839
|
(index) ? PROVINCES[index].to_s : nil
|
771
840
|
end
|
data/tests/regress.rb
CHANGED
@@ -294,6 +294,16 @@ class TestValidator < Test::Unit::TestCase
|
|
294
294
|
assert_equal(false, form["ip"].tainted?)
|
295
295
|
end
|
296
296
|
|
297
|
+
def test_hash_constructor
|
298
|
+
profile = {
|
299
|
+
:test => {
|
300
|
+
:required => [ :foo ]
|
301
|
+
}
|
302
|
+
}
|
303
|
+
fv = FormValidator.new(profile)
|
304
|
+
assert(fv.validate({'foo' => 'bar'}, :test))
|
305
|
+
end
|
306
|
+
|
297
307
|
def test_constraint_regexp_map
|
298
308
|
form = {
|
299
309
|
"zipcode" => "32608"
|
@@ -492,13 +502,6 @@ class TestValidator < Test::Unit::TestCase
|
|
492
502
|
assert_nil(@fv.match_american_phone("(abc) abc-defg"))
|
493
503
|
end
|
494
504
|
|
495
|
-
def test_match_cc_number()
|
496
|
-
# Not a real credit card number! It simply matches the checksum.
|
497
|
-
assert_equal("378282246310005",
|
498
|
-
@fv.match_cc_number("378282246310005", :AMEX))
|
499
|
-
assert_nil(@fv.match_cc_number("378282256310005", :AMEX))
|
500
|
-
end
|
501
|
-
|
502
505
|
def test_match_cc_exp()
|
503
506
|
year = Time.new.year
|
504
507
|
month = Time.new.month
|
metadata
CHANGED
@@ -1,59 +1,71 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
|
-
rubygems_version: "0.8"
|
3
|
-
specification_version: 1
|
4
2
|
name: formvalidator
|
5
3
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.1.
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
- "."
|
11
|
-
author: Travis Whitton
|
12
|
-
email: whitton@atlantic.net
|
13
|
-
homepage: http://grub.ath.cx/formvalidator/
|
14
|
-
rubyforge_project:
|
15
|
-
description:
|
4
|
+
version: 0.1.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Travis Whitton
|
16
8
|
autorequire: formvalidator
|
17
|
-
default_executable:
|
18
9
|
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-08-28 00:00:00 -04:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: tinymountain@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.rdoc
|
24
|
+
files:
|
25
|
+
- install.rb
|
26
|
+
- README
|
27
|
+
- CHANGELOG
|
28
|
+
- README.rdoc
|
29
|
+
- tests/testprofile.rb
|
30
|
+
- tests/regress.rb
|
31
|
+
- examples/simple.rb
|
32
|
+
- examples/README
|
33
|
+
- examples/profiles/my_profile.rb
|
34
|
+
- examples/profiles/extension.rb
|
35
|
+
- examples/extend.rb
|
36
|
+
- examples/standard.rb
|
37
|
+
- examples/file.rb
|
38
|
+
- formvalidator.rb
|
39
|
+
- formvalidator.gemspec
|
40
|
+
- AUTHORS
|
19
41
|
has_rdoc: true
|
20
|
-
|
42
|
+
homepage: http://github.com/tmountain/FormValidator
|
43
|
+
licenses: []
|
44
|
+
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options:
|
47
|
+
- --main
|
48
|
+
- README.rdoc
|
49
|
+
require_paths:
|
50
|
+
- .
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
21
52
|
requirements:
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: "0"
|
56
|
+
version:
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: "0"
|
26
62
|
version:
|
27
|
-
platform: ruby
|
28
|
-
files:
|
29
|
-
- tests
|
30
|
-
- README
|
31
|
-
- examples
|
32
|
-
- CHANGELOG
|
33
|
-
- TODO
|
34
|
-
- formvalidator.rb
|
35
|
-
- README.rdoc
|
36
|
-
- install.rb
|
37
|
-
- AUTHORS
|
38
|
-
- formvalidator.gemspec
|
39
|
-
- tests/testprofile.rb
|
40
|
-
- tests/regress.rb
|
41
|
-
- examples/file.rb
|
42
|
-
- examples/standard.rb
|
43
|
-
- examples/profiles
|
44
|
-
- examples/extend.rb
|
45
|
-
- examples/simple.rb
|
46
|
-
- examples/README
|
47
|
-
- examples/profiles/my_profile.rb
|
48
|
-
- examples/profiles/extension.rb
|
49
|
-
test_files:
|
50
|
-
- tests/regress.rb
|
51
|
-
rdoc_options:
|
52
|
-
- "--main"
|
53
|
-
- README.rdoc
|
54
|
-
extra_rdoc_files:
|
55
|
-
- README.rdoc
|
56
|
-
executables: []
|
57
|
-
extensions: []
|
58
63
|
requirements: []
|
59
|
-
|
64
|
+
|
65
|
+
rubyforge_project:
|
66
|
+
rubygems_version: 1.3.5
|
67
|
+
signing_key:
|
68
|
+
specification_version: 3
|
69
|
+
summary: FormValidator is a Ruby port of Perl's Data::FormValidator library.
|
70
|
+
test_files:
|
71
|
+
- tests/regress.rb
|
data/TODO
DELETED
@@ -1,5 +0,0 @@
|
|
1
|
-
Patches are Welcome!
|
2
|
-
--------------------------------------------------------------------------------
|
3
|
-
o Make constraints handle form elements with multiple fields.
|
4
|
-
o Make field filters accept proc objects.
|
5
|
-
o Make a tutorial on using FormValidator on the Ruby Wiki.
|