client_side_validations 0.8.0 → 2.2.0

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.
data/README.markdown ADDED
@@ -0,0 +1,109 @@
1
+ # Client Side Validations
2
+ Now you can easily drop in client side validations in any Rails app. It will use validations defined in a given ActiveRecord (or ActiveModel) class for use with a Javascript form validator. (currently only [jquery.validate](http://bassistance.de/jquery-plugins/jquery-plugin-validation/) is supported)
3
+
4
+ For Rails 2 and Rails 3 example apps please see [client_side_validations_example](http://github.com/dnclabs/client_side_validations_examples)
5
+
6
+ The concept is simple:
7
+
8
+ 1. Include the middleware
9
+ 2. Define validations in the model as you normally would
10
+ 3. The validations are sent to the client in JSON
11
+ 4. client_side_validations.js converts the JSON for a given validation plugin and binds the validator to the form
12
+
13
+ Currently the following validations are supported:
14
+
15
+ * validates_presence_of
16
+ * validates_format_of
17
+ * validates_numericality_of
18
+ * validates_length_of
19
+ * validates_uniqueness_of
20
+
21
+ The uniqueness validation works for both ActiveRecord and Mongoid.
22
+
23
+ ## Installation
24
+ > gem install client_side_validations
25
+
26
+ ### Rails 2
27
+ Add "config.gem :client_side_validations" to the "config/environment.rb" file
28
+
29
+ Then run the generator:
30
+ > script/generate client_side_validations
31
+
32
+ This will copy client_side_validations.js to "public/javascripts"
33
+
34
+ ### Rails 3
35
+ Add "gem 'client_side_validations" to the Gemfile
36
+
37
+ Then run the generator:
38
+ > rails g client_side_validations
39
+
40
+ This will copy client_side_validations.js to "public/javascripts"
41
+
42
+ ## Configuration
43
+ Currently only [jquery.validate](http://bassistance.de/jquery-plugins/jquery-plugin-validation/) is supported so you will need to download [jQuery](http://docs.jquery.com/Downloading_jQuery) and the jQuery Validate plugin to "public/javascripts"
44
+
45
+ ### Rack
46
+ The following routes will be reserved for client side validations:
47
+
48
+ /validations.json
49
+ /validations/uniqueness.json
50
+
51
+ Add the middleware to your stack:
52
+
53
+ config/environment.rb for Rails 2.x
54
+
55
+ config/application.rb for Rails 3.x
56
+
57
+ ...
58
+ config.middleware.use 'ClientSideValidations'
59
+ ...
60
+
61
+ ### Model
62
+ Validate your models as you normally would
63
+
64
+ class Book < ActiveRecord::Base
65
+ validates_presence_of :author
66
+ end
67
+
68
+ ### Layout
69
+ You currently need both jQuery and the jQuery Validate plugin loaded before you load Client Side Validations
70
+
71
+ ...
72
+ <%= javascript_include_tag 'jquery', 'jquery.validate', 'client_side_validations' %>
73
+ ...
74
+
75
+ ### View
76
+ Have a form ask for client side validations by passing :validate => true
77
+
78
+ ...
79
+
80
+ <% form_for @book, :validations => true do |b| %>
81
+ <%= b.label :author %></br>
82
+ <%= b.text_field :author %></br>
83
+ <%= submit_tag 'Create' %>
84
+ <% end %>
85
+
86
+ ...
87
+
88
+ That should be it!
89
+
90
+ ## Advanced Options
91
+
92
+ ### Model
93
+ If you want to define only specific fields for client side validations just override the validation_fields method on each model
94
+
95
+ class Book < ActiveRecord::Base
96
+ validatese_presence_of :author
97
+ validates_presence_of :body
98
+
99
+ private
100
+
101
+ def validation_fields
102
+ [:author]
103
+ end
104
+ end
105
+
106
+
107
+ Written by Brian Cardarella
108
+
109
+ Copyright (c) 2010 Democratic National Committee. See LICENSE for details.
data/TODO CHANGED
@@ -1,2 +1 @@
1
- - Add Rails Generators
2
1
  - Accept options in helper to extend the given validation plugin
@@ -1,33 +1,36 @@
1
- /* Additional jQueryValidator methods */
2
-
3
1
  if (typeof(jQuery) != "undefined") {
4
- if (typeof($('').validate) != "undefined") {
5
- jQuery.validator.addMethod("format", function(value, element, params) {
6
- var pattern = new RegExp(params, "i");
7
- return this.optional(element) || pattern.test(value);
8
- }, jQuery.validator.format("Invalid format."));
9
- }
10
- }
11
-
12
- $.extend($.fn, {
13
- clientSideValidations: function(url, adapter) {
14
- var form = this;
15
- var id = form[0].id;
16
- if (/new/.test(id)) {
17
- id = /new_(\w+)/.exec(id)[1]
18
- } else if (/edit/.test(id)) {
19
- id = /edit_(\w+)_\d+/.exec(id)[1]
20
- }
21
- var client = new ClientSideValidations(id, adapter)
22
- $.getJSON(url, function(json) {
23
- var validations = client.adaptValidations(json);
24
- form.validate({
25
- rules: validations.rules,
26
- messages: validations.messages
2
+ jQuery.validator.addMethod("format", function(value, element, params) {
3
+ var pattern = new RegExp(params, "i");
4
+ return this.optional(element) || pattern.test(value);
5
+ }, jQuery.validator.format("Invalid format."));
6
+
7
+ $.extend($.fn, {
8
+ clientSideValidations: function() {
9
+ var form = this;
10
+ var object = form.attr('object-csv');
11
+ var url = '/validations.json?model=' + object;
12
+ var id = form[0].id;
13
+ var adapter = 'jquery.validate';
14
+ if (/new/.test(id)) {
15
+ id = /new_(\w+)/.exec(id)[1]
16
+ } else if (/edit/.test(id)) {
17
+ id = /edit_(\w+)_\d+/.exec(id)[1]
18
+ }
19
+ var client = new ClientSideValidations(id, adapter)
20
+ $.getJSON(url, function(json) {
21
+ var validations = client.adaptValidations(json);
22
+ form.validate({
23
+ rules: validations.rules,
24
+ messages: validations.messages
25
+ });
27
26
  });
28
- });
29
- }
30
- });
27
+ }
28
+ });
29
+
30
+ $(document).ready(function() {
31
+ $('form[object-csv]').clientSideValidations();
32
+ });
33
+ }
31
34
 
32
35
  ClientSideValidations = function(id, adapter) {
33
36
  this.id = id;
@@ -64,6 +67,13 @@ ClientSideValidations = function(id, adapter) {
64
67
  value = this.validations[attr][validation]['maximum'];
65
68
  }
66
69
  break;
70
+ case 'uniqueness':
71
+ rule = 'remote';
72
+ value = {
73
+ url: '/validations/uniqueness.json'
74
+ }
75
+ break;
76
+
67
77
  default:
68
78
  }
69
79
  if(rule == null) {
@@ -1,47 +1,44 @@
1
- require 'rubygems' unless defined?(Gem)
1
+ require 'rubygems'
2
+ require 'json'
3
+ require 'cgi'
2
4
 
3
- module DNCLabs
4
- module ClientSideValidations
5
- def validations_to_json(*attrs)
6
- hash = Hash.new { |h, attribute| h[attribute] = {} }
7
- attrs.each do |attr|
8
- hash[attr].merge!(validation_to_hash(attr))
9
- end
10
- hash.to_json
11
- end
12
-
13
- def validation_to_hash(_attr, _options = {})
14
- @dnc_csv_adapter ||= Adapter.new(self)
15
- @dnc_csv_adapter.validation_to_hash(_attr, _options)
5
+ class ClientSideValidations
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ params = CGI::parse(env['QUERY_STRING'])
12
+ case env['PATH_INFO']
13
+ when %r{^/validations.json}
14
+ body = get_validations(params['model'][0])
15
+ [200, {'Content-Type' => 'application/json', 'Content-Length' => "#{body.length}"}, body]
16
+ when %r{^/validations/uniqueness.json}
17
+ field = params.keys.first
18
+ resource, attribute = field.split(/[^\w]/)
19
+ value = params[field][0]
20
+ body = is_unique?(resource, attribute, value).to_s
21
+ [200, {'Content-Type' => 'application/json', 'Content-Length' => "#{body.length}"}, body]
22
+ else
23
+ @app.call(env)
16
24
  end
17
25
  end
18
- end
19
-
20
-
21
- # ORM
22
-
23
- if defined?(ActiveModel)
24
- require 'adapters/active_model'
25
- unless Object.respond_to?(:to_json)
26
- require 'active_support/json/encoding'
26
+
27
+ private
28
+
29
+ def get_validations(resource)
30
+ constantize_resource(resource).new.validations_to_json
27
31
  end
28
- DNCLabs::ClientSideValidations::Adapter = DNCLabs::ClientSideValidations::Adapters::ActiveModel
29
- klass = ActiveModel::Validations
30
- elsif defined?(ActiveRecord)
31
- if ActiveRecord::VERSION::MAJOR == 2
32
- require 'adapters/active_record_2'
33
- DNCLabs::ClientSideValidations::Adapter = DNCLabs::ClientSideValidations::Adapters::ActiveRecord2
34
- klass = ActiveRecord::Base
32
+
33
+ def is_unique?(resource, attribute, value)
34
+ constantize_resource(resource).send("find_by_#{attribute}", value) == nil
35
35
  end
36
+
37
+ def constantize_resource(resource)
38
+ eval(resource.split('_').map{ |word| word.capitalize}.join)
39
+ end
40
+
36
41
  end
37
42
 
38
- klass.class_eval do
39
- include DNCLabs::ClientSideValidations
40
- end
41
-
42
-
43
- # Template
44
-
45
- if defined?(ActionView)
46
- require 'adapters/action_view'
47
- end
43
+ require 'client_side_validations/orm'
44
+ require 'client_side_validations/template'
@@ -0,0 +1,57 @@
1
+ module DNCLabs
2
+ module ClientSideValidations
3
+ module Adapters
4
+ module ActionView
5
+ module BaseMethods
6
+
7
+ def self.included(base)
8
+ form_method = base.instance_method(:form_for)
9
+ base.class_eval do
10
+ define_method(:form_for) do |record_or_name_or_array, *args, &proc|
11
+ options = args.extract_options!
12
+ options.symbolize_keys!
13
+ if validations = options.delete(:validations)
14
+ unless options.key?(:html)
15
+ options[:html] = {}
16
+ end
17
+
18
+ unless validations == true
19
+ object = validations.to_s.underscore
20
+ else
21
+ case record_or_name_or_array
22
+ when String, Symbol
23
+ object = record_or_name_or_array.to_s
24
+ else
25
+ object = record_or_name_or_array.class.to_s.underscore
26
+ end
27
+ end
28
+
29
+ options[:html]['object-csv'] = object
30
+ end
31
+ args << options
32
+ form_method.bind(self).call(record_or_name_or_array, *args, &proc)
33
+ end
34
+ end
35
+ end
36
+
37
+ end # BaseMethods
38
+
39
+ module FormBuilderMethods
40
+
41
+ def client_side_validations(options = {})
42
+ @template.send(:client_side_validations, @object_name, objectify_options(options))
43
+ end
44
+
45
+ end # FormBuilderMethods
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ ActionView::Base.class_eval do
52
+ include DNCLabs::ClientSideValidations::Adapters::ActionView::BaseMethods
53
+ end
54
+
55
+ ActionView::Helpers::FormBuilder.class_eval do
56
+ include DNCLabs::ClientSideValidations::Adapters::ActionView::FormBuilderMethods
57
+ end
@@ -35,6 +35,10 @@ module DNCLabs
35
35
  validation_hash
36
36
  end
37
37
 
38
+ def validation_fields
39
+ base._validators.keys
40
+ end
41
+
38
42
  private
39
43
 
40
44
  def get_validation_message(validation, locale)
@@ -45,12 +49,18 @@ module DNCLabs
45
49
  I18n.translate('errors.messages.invalid', :locale => locale)
46
50
  when :length
47
51
  if count = validation.options[:minimum]
48
- I18n.translate('errors.messages.too_short', :locale => locale).sub('{{count}}', count.to_s)
52
+ I18n.translate('errors.messages.too_short', :locale => locale).sub('%{count}', count.to_s)
49
53
  elsif count = validation.options[:maximum]
50
- I18n.translate('errors.messages.too_long', :locale => locale).sub('{{count}}', count.to_s)
54
+ I18n.translate('errors.messages.too_long', :locale => locale).sub('%{count}', count.to_s)
51
55
  end
52
56
  when :numericality
53
57
  I18n.translate('errors.messages.not_a_number', :locale => locale)
58
+ when :uniqueness
59
+ if defined?(ActiveRecord) && base.kind_of?(ActiveRecord::Base)
60
+ I18n.translate('activerecord.errors.messages.taken', :locale => locale)
61
+ elsif defined?(Mongoid) && base.class.included_modules.include?(Mongoid::Document)
62
+ I18n.translate('errors.messages.taken', :locale => locale)
63
+ end
54
64
  end
55
65
 
56
66
  message = validation.options[:message]
@@ -65,10 +75,11 @@ module DNCLabs
65
75
 
66
76
  def get_validation_options(options)
67
77
  options = options.stringify_keys
68
- options.delete('on')
78
+ options.delete('on') == :create
69
79
  options.delete('tokenizer')
70
80
  options.delete('only_integer')
71
81
  options.delete('allow_nil')
82
+ options.delete('case_sensitive')
72
83
  if options['with'].kind_of?(Regexp)
73
84
  options['with'] = options['with'].inspect.to_s.sub("\\A","^").sub("\\Z","$").sub(%r{^/},"").sub(%r{/i?$}, "")
74
85
  end
@@ -82,6 +93,7 @@ module DNCLabs
82
93
  def remove_reserved_conditionals(*conditionals)
83
94
  conditionals.flatten!
84
95
  conditionals.delete_if { |conditional| conditional =~ /@_/ }
96
+ conditionals.delete_if { |conditional| conditional =~ /validation_context/ }
85
97
  conditionals.first
86
98
  end
87
99
  end
@@ -35,6 +35,10 @@ module DNCLabs
35
35
  validation_hash
36
36
  end
37
37
 
38
+ def validation_fields
39
+ base.class.reflect_on_all_validations.map { |v| v.name }.uniq
40
+ end
41
+
38
42
  private
39
43
 
40
44
  def get_validation_message(validation, locale)
@@ -51,6 +55,8 @@ module DNCLabs
51
55
  end
52
56
  when 'validates_numericality_of'
53
57
  I18n.translate('activerecord.errors.messages.not_a_number', :locale => locale)
58
+ when 'validates_uniqueness_of'
59
+ I18n.translate('activerecord.errors.messages.taken', :locale => locale)
54
60
  end
55
61
 
56
62
  message = validation.options[:message]
@@ -82,6 +88,8 @@ module DNCLabs
82
88
  'numericality'
83
89
  when 'validates_length_of'
84
90
  'length'
91
+ when 'validates_uniqueness_of'
92
+ 'uniqueness'
85
93
  end
86
94
  end
87
95
  end
@@ -0,0 +1,45 @@
1
+ module DNCLabs
2
+ module ClientSideValidations
3
+ def validations_to_json
4
+ hash = Hash.new { |h, field| h[field] = {} }
5
+ validation_fields.each do |field|
6
+ hash[field].merge!(validation_to_hash(field))
7
+ end
8
+ hash.to_json
9
+ end
10
+
11
+ def validation_to_hash(field, options = {})
12
+ dnc_csv_adapter.validation_to_hash(field, options)
13
+ end
14
+
15
+ def validation_fields
16
+ dnc_csv_adapter.validation_fields
17
+ end
18
+
19
+ def dnc_csv_adapter
20
+ unless @dnc_csv_adapter
21
+ @dnc_csv_adapter = Adapter.new(self)
22
+ end
23
+ @dnc_csv_adapter
24
+ end
25
+ end
26
+ end
27
+
28
+ if defined?(ActiveModel)
29
+ require 'client_side_validations/adapters/active_model'
30
+ DNCLabs::ClientSideValidations::Adapter = DNCLabs::ClientSideValidations::Adapters::ActiveModel
31
+ klass = ActiveModel::Validations
32
+
33
+ elsif defined?(ActiveRecord)
34
+ if ActiveRecord::VERSION::MAJOR == 2
35
+ require 'client_side_validations/adapters/active_record_2'
36
+ DNCLabs::ClientSideValidations::Adapter = DNCLabs::ClientSideValidations::Adapters::ActiveRecord2
37
+ klass = ActiveRecord::Base
38
+ end
39
+ end
40
+
41
+ if klass
42
+ klass.class_eval do
43
+ include DNCLabs::ClientSideValidations
44
+ end
45
+ end