bureaucrat 0.0.3 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,124 +1,139 @@
1
1
  Bureaucrat
2
2
  ==========
3
3
 
4
- Form handling for Ruby inspired by Django forms.
4
+ Form handling for Ruby inspired by [Django forms](https://docs.djangoproject.com/en/dev/#forms).
5
5
 
6
- Structure
7
- ---------
6
+ Description
7
+ -----------
8
8
 
9
- Form ----> as_<render_mode>, valid?, errors/cleaned_data
9
+ Bureaucrat is a library for handling the processing, validation and rendering of HTML forms.
10
+
11
+ Structure of a Form
12
+ -------------------
13
+
14
+ Form ----> valid?, errors/cleaned_data
10
15
  ______|________
11
16
  / | \
12
17
  Field Field Field ----> clean
13
18
  | | |
14
19
  Widget Widget Widget ----> render
15
20
 
16
- - A Form has a list of Fields.
17
- - A Field has a Widget.
18
- - A Widget knows how to render itself.
19
- - A Field knows how to validate an input value and convert it from a string to the required type.
20
- - A Form knows how to render all its fields along with all the required error messages.
21
- - After validation, a valid Form responds to 'cleaned_data' by returning a hash of validated values.
22
- - After validation an invalid Form responds to 'errors' by returning a hash of field_name => error_messages
21
+ **Form**:
22
+ Collection of named Fields, handles global validation and the last pass of
23
+ data conversion.
24
+ After validation, a valid Form responds to `cleaned_data` by returning a
25
+ hash of validated values and an invalid Form responds to `errors` by
26
+ returning a hash of field_name => error_messages.
27
+
28
+ **Field**:
29
+ Handles the validation and data conversion of each field belonging to the Form. Each Field is associated to a name on the parent Form.
30
+
31
+ **Widget**:
32
+ Handles the rendering of a Form field. Each Field has two widgets associated, one for normal rendering, and another for hidden inputs rendering. Every type of Field has default Widgets defined, but they can be overriden on a per-Form basis.
23
33
 
24
34
  Usage examples
25
35
  --------------
26
36
 
27
- require 'bureaucrat'
28
- require 'bureaucrat/quickfields'
29
-
30
- class MyForm < Bureaucrat::Forms::Form
31
- extend Bureaucrat::Quickfields
32
-
33
- string :nickname, :max_length => 50
34
- string :realname, :require => false
35
- email :email
36
- integer :age, :min_value => 0
37
- boolean :newsletter, :required => false
38
-
39
- def save
40
- user = User.create!(cleaned_data)
41
- Mailer.deliver_confirmation_mail(user)
42
- user
43
- end
44
- end
45
-
46
- # A Form initialized without parameters is an unbound Form.
47
- unbound_form = MyForm.new
48
- unbound_form.valid? # => false
49
- unbound_form.errors # => {}
50
- unbound_form.cleaned_data # => nil
51
- puts unbound_form.as_p
52
- # Prints:
53
- # <p><label for="id_nickname">Nickname:</label> <input type="text" name="nickname" id="id_nickname" maxlength="50" /></p>
54
- # <p><label for="id_realname">Realname:</label> <input type="text" name="realname" id="id_realname" /></p>
55
- # <p><label for="id_email">Email:</label> <input type="text" name="email" id="id_email" /></p>
56
- # <p><label for="id_age">Age:</label> <input type="text" name="age" id="id_age" /></p>
57
- # <p><label for="id_newsletter">Newsletter:</label> <input type="checkbox" name="newsletter" id="id_newsletter" /></p>
58
-
59
- invalid_bound_form = MyForm.new(:nickname => 'bureaucrat', :email => 'badformat', :age => '30')
60
- invalid_bound_form.valid? # => false
61
- invalid_bound_form.errors # {:email => ["Enter a valid e-mail address."]}
62
- invalid_bound_form.cleaned_data # => nil
63
- puts invalid_bound_form.as_table
64
- # Prints:
65
- # <tr><th><label for="id_nickname">Nickname:</label></th><td><input type="text" value="bureaucrat" name="nickname" id="id_nickname" maxlength="50" /></td></tr>
66
- # <tr><th><label for="id_realname">Realname:</label></th><td><ul class="errorlist"><li>This field is required</li></ul><input type="text" name="realname" id="id_realname" /></td></tr>
67
- # <tr><th><label for="id_email">Email:</label></th><td><ul class="errorlist"><li>Enter a valid e-mail address.</li></ul><input type="text" value="badformat" name="email" id="id_email" /></td></tr>
68
- # <tr><th><label for="id_age">Age:</label></th><td><input type="text" value="30" name="age" id="id_age" /></td></tr>
69
- # <tr><th><label for="id_newsletter">Newsletter:</label></th><td><input type="checkbox" name="newsletter" id="id_newsletter" /></td></tr>
70
-
71
- valid_bound_form = MyForm.new(:nickname => 'bureaucrat', :email => 'valid@email.com', :age => '30')
72
- valid_bound_form.valid? # => true
73
- valid_bound_form.errors # {}
74
- valid_bound_form.cleaned_data # => {:age => 30, :newsletter => false, :nickname => "bureaucrat", :realname => "", :email = >"valid@email.com"}
75
- puts valid_bound_form.as_ul
76
- # Prints:
77
- # <li><label for="id_nickname">Nickname:</label> <input type="text" value="bureaucrat" name="nickname" id="id_nickname" maxlength="50" /></li>
78
- # <li><ul class="errorlist"><li>This field is required</li></ul><label for="id_realname">Realname:</label> <input type="text" name="realname" id="id_realname" /></li>
79
- # <li><label for="id_email">Email:</label> <input type="text" value="valid@email.com" name="email" id="id_email" /></li>
80
- # <li><label for="id_age">Age:</label> <input type="text" value="30" name="age" id="id_age" /></li>
81
- # <li><label for="id_newsletter">Newsletter:</label> <input type="checkbox" name="newsletter" id="id_newsletter" /></li>
82
-
83
- valid_bound_form.save # A new User is created and a confirmation mail is delivered
37
+ ```ruby
38
+ require 'bureaucrat'
39
+ require 'bureaucrat/quickfields'
40
+
41
+ class MyForm < Bureaucrat::Forms::Form
42
+ extend Bureaucrat::Quickfields
43
+
44
+ string :nickname, max_length: 50
45
+ string :realname, required: false
46
+ email :email
47
+ integer :age, min_value: 0
48
+ boolean :newsletter, required: false
49
+
50
+ # Note: Bureaucrat doesn't define save
51
+ def save
52
+ user = User.create!(cleaned_data)
53
+ Mailer.deliver_confirmation_mail(user)
54
+ user
55
+ end
56
+ end
57
+
58
+ # A Form initialized without parameters is an unbound Form.
59
+ unbound_form = MyForm.new
60
+ unbound_form.valid? # => false
61
+ unbound_form.errors # => {}
62
+ unbound_form.cleaned_data # => nil
63
+ puts unbound_form.as_p
64
+ # Prints:
65
+ # <p><label for="id_nickname">Nickname:</label> <input type="text" name="nickname" id="id_nickname" maxlength="50" /></p>
66
+ # <p><label for="id_realname">Realname:</label> <input type="text" name="realname" id="id_realname" /></p>
67
+ # <p><label for="id_email">Email:</label> <input type="text" name="email" id="id_email" /></p>
68
+ # <p><label for="id_age">Age:</label> <input type="text" name="age" id="id_age" /></p>
69
+ # <p><label for="id_newsletter">Newsletter:</label> <input type="checkbox" name="newsletter" id="id_newsletter" /></p>
70
+
71
+ invalid_bound_form = MyForm.new(nickname: 'bureaucrat', email: 'badformat', age: '30')
72
+ invalid_bound_form.valid? # => false
73
+ invalid_bound_form.errors # {email: ["Enter a valid e-mail address."]}
74
+ invalid_bound_form.cleaned_data # => nil
75
+ puts invalid_bound_form.as_table
76
+ # Prints:
77
+ # <tr><th><label for="id_nickname">Nickname:</label></th><td><input type="text" value="bureaucrat" name="nickname" id="id_nickname" maxlength="50" /></td></tr>
78
+ # <tr><th><label for="id_realname">Realname:</label></th><td><ul class="errorlist"><li>This field is required</li></ul><input type="text" name="realname" id="id_realname" /></td></tr>
79
+ # <tr><th><label for="id_email">Email:</label></th><td><ul class="errorlist"><li>Enter a valid e-mail address.</li></ul><input type="text" value="badformat" name="email" id="id_email" /></td></tr>
80
+ # <tr><th><label for="id_age">Age:</label></th><td><input type="text" value="30" name="age" id="id_age" /></td></tr>
81
+ # <tr><th><label for="id_newsletter">Newsletter:</label></th><td><input type="checkbox" name="newsletter" id="id_newsletter" /></td></tr>
82
+
83
+ valid_bound_form = MyForm.new(nickname: 'bureaucrat', email: 'valid@email.com', age: '30')
84
+ valid_bound_form.valid? # => true
85
+ valid_bound_form.errors # {}
86
+ valid_bound_form.cleaned_data # => {age: 30, newsletter: false, nickname: "bureaucrat", realname: "", :email = >"valid@email.com"}
87
+ puts valid_bound_form.as_ul
88
+ # Prints:
89
+ # <li><label for="id_nickname">Nickname:</label> <input type="text" value="bureaucrat" name="nickname" id="id_nickname" maxlength="50" /></li>
90
+ # <li><ul class="errorlist"><li>This field is required</li></ul><label for="id_realname">Realname:</label> <input type="text" name="realname" id="id_realname" /></li>
91
+ # <li><label for="id_email">Email:</label> <input type="text" value="valid@email.com" name="email" id="id_email" /></li>
92
+ # <li><label for="id_age">Age:</label> <input type="text" value="30" name="age" id="id_age" /></li>
93
+ # <li><label for="id_newsletter">Newsletter:</label> <input type="checkbox" name="newsletter" id="id_newsletter" /></li>
94
+
95
+ valid_bound_form.save # A new User is created and a confirmation mail is delivered
96
+ ```
84
97
 
85
98
  Examples of different ways of defining forms
86
99
  --------------
87
100
 
88
- require 'bureaucrat'
89
- require 'bureaucrat/quickfields'
90
-
91
- class MyForm < Bureaucrat::Forms::Form
92
- include Bureaucrat::Fields
93
-
94
- field :nickname, CharField.new(:max_length => 50)
95
- field :realname, CharField.new(:required => false)
96
- field :email, EmailField.new
97
- field :age, IntegerField.new(:min_value => 0)
98
- field :newsletter, BooleanField.new(:required => false)
99
- end
100
-
101
- class MyFormQuick < Bureaucrat::Forms::Form
102
- extend Bureaucrat::Quickfields
103
-
104
- string :nickname, :max_length => 50
105
- string :realname, :required => false
106
- email :email
107
- integer :age, :min_value => 0
108
- boolean :newsletter, :required => false
109
- end
110
-
111
- def quicker_form
112
- f = Class.new(Bureaucrat::Forms::Form)
113
- f.extend(Bureaucrat::Quickfields)
114
- yield f
115
- f
116
- end
117
-
118
- MyFormQuicker = quicker_form do |f|
119
- f.string :nickname, :max_length => 50
120
- f.string :realname, :required => false
121
- f.email :email
122
- f.integer :age, :min_value => 0
123
- f.boolean :newsletter, :required => false
124
- end
101
+ ```ruby
102
+ require 'bureaucrat'
103
+ require 'bureaucrat/quickfields'
104
+
105
+ class MyForm < Bureaucrat::Forms::Form
106
+ include Bureaucrat::Fields
107
+
108
+ field :nickname, CharField.new(max_length: 50)
109
+ field :realname, CharField.new(required: false)
110
+ field :email, EmailField.new
111
+ field :age, IntegerField.new(min_value: 0)
112
+ field :newsletter, BooleanField.new(required: false)
113
+ end
114
+
115
+ class MyFormQuick < Bureaucrat::Forms::Form
116
+ extend Bureaucrat::Quickfields
117
+
118
+ string :nickname, max_length: 50
119
+ string :realname, required: false
120
+ email :email
121
+ integer :age, min_value: 0
122
+ boolean :newsletter, required: false
123
+ end
124
+
125
+ def inline_form
126
+ f = Class.new(Bureaucrat::Forms::Form)
127
+ f.extend(Bureaucrat::Quickfields)
128
+ yield f
129
+ f
130
+ end
131
+
132
+ form_maker = inline_form do |f|
133
+ f.string :nickname, max_length: 50
134
+ f.string :realname, required: false
135
+ f.email :email
136
+ f.integer :age, min_value: 0
137
+ f.boolean :newsletter, required: false
138
+ end
139
+ ```
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "rake/testtask"
2
+
3
+ task :default => :test
4
+
5
+ desc 'Run all tests'
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.pattern = './test/**/*_test.rb'
8
+ t.verbose = false
9
+ end
@@ -0,0 +1,32 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'bureaucrat'
3
+ s.version = '0.10.0'
4
+ s.summary = "Form handling for Ruby inspired by Django forms."
5
+ s.description = "Bureaucrat is a form handling library for Ruby."
6
+ s.author = "Bruno Deferrari"
7
+ s.email = "utizoc@gmail.com"
8
+ s.homepage = "http://github.com/tizoc/bureaucrat"
9
+
10
+ s.files = [
11
+ "lib/bureaucrat/fields.rb",
12
+ "lib/bureaucrat/forms.rb",
13
+ "lib/bureaucrat/formsets.rb",
14
+ "lib/bureaucrat/quickfields.rb",
15
+ "lib/bureaucrat/temporary_uploaded_file.rb",
16
+ "lib/bureaucrat/utils.rb",
17
+ "lib/bureaucrat/validators.rb",
18
+ "lib/bureaucrat/widgets.rb",
19
+ "lib/bureaucrat.rb",
20
+ "README.md",
21
+ "LICENSE",
22
+ "test/fields_test.rb",
23
+ "test/forms_test.rb",
24
+ "test/formsets_test.rb",
25
+ "test/test_helper.rb",
26
+ "test/widgets_test.rb",
27
+ "Rakefile",
28
+ "bureaucrat.gemspec"
29
+ ]
30
+
31
+ s.require_paths = ['lib']
32
+ end
data/lib/bureaucrat.rb CHANGED
@@ -1,10 +1,28 @@
1
- libdir = File.dirname(__FILE__)
2
- $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
3
-
4
1
  module Bureaucrat
5
- VERSION = '0.0.1'
6
- end
2
+ VERSION = '0.10.0'
3
+
4
+ class ValidationError < Exception
5
+ attr_reader :code, :params, :messages
7
6
 
8
- require 'bureaucrat/widgets'
9
- require 'bureaucrat/fields'
10
- require 'bureaucrat/forms'
7
+ def initialize(message, code = nil, params = nil)
8
+ if message.is_a? Array
9
+ @messages = message
10
+ else
11
+ @code = code
12
+ @params = params
13
+ @messages = [message]
14
+ end
15
+ end
16
+
17
+ def to_s
18
+ "ValidationError(#{@messages.inspect})"
19
+ end
20
+ end
21
+
22
+ require_relative 'bureaucrat/utils'
23
+ require_relative 'bureaucrat/validators'
24
+ require_relative 'bureaucrat/widgets'
25
+ require_relative 'bureaucrat/fields'
26
+ require_relative 'bureaucrat/forms'
27
+ require_relative 'bureaucrat/temporary_uploaded_file'
28
+ end
@@ -1,461 +1,697 @@
1
- require 'bureaucrat/utils'
2
- require 'bureaucrat/validation'
3
- require 'bureaucrat/widgets'
1
+ require 'set'
4
2
 
5
3
  module Bureaucrat
6
- module Fields
4
+ module Fields
7
5
 
8
- class ErrorList < Array
9
- include Utils
6
+ class ErrorList < Array
7
+ include Utils
10
8
 
11
- def to_s
12
- as_ul
13
- end
9
+ def to_s
10
+ as_ul
11
+ end
14
12
 
15
- def as_ul
16
- ul = '<ul class="errorlist">%s</ul>'
17
- li = '<li>%s</li>'
18
- empty? ? '' : mark_safe(ul % map {|e| li % conditional_escape(e)}.join("\n"))
19
- end
13
+ def as_ul
14
+ if empty?
15
+ ''
16
+ else
17
+ ul = '<ul class="errorlist">%s</ul>'
18
+ li = '<li>%s</li>'
20
19
 
21
- def as_text
22
- empty? ? '' : map{|e| '* %s' % e}.join("\n")
20
+ result = ul % map{|e| li % conditional_escape(e)}.join("\n")
21
+ mark_safe(result)
22
+ end
23
+ end
24
+
25
+ def as_text
26
+ empty? ? '' : map{|e| '* %s' % e}.join("\n")
27
+ end
23
28
  end
24
- end
25
29
 
26
- class ErrorHash < Hash
27
- include Utils
30
+ class ErrorHash < Hash
31
+ include Utils
28
32
 
29
- def to_s
30
- as_ul
31
- end
33
+ def to_s
34
+ as_ul
35
+ end
32
36
 
33
- def as_ul
34
- ul = '<ul class="errorlist">%s</ul>'
35
- li = '<li>%s%s</li>'
36
- empty? ? '' : mark_safe(ul % map {|k, v| li % [k, v]}.join)
37
- end
37
+ def as_ul
38
+ ul = '<ul class="errorlist">%s</ul>'
39
+ li = '<li>%s%s</li>'
40
+ empty? ? '' : mark_safe(ul % map {|k, v| li % [k, v]}.join)
41
+ end
38
42
 
39
- def as_text
40
- map do |k, v|
41
- '* %s\n%s' % [k, v.map{|i| ' * %s'}.join("\n")]
42
- end.join("\n")
43
+ def as_text
44
+ map do |k, v|
45
+ "* %s\n%s" % [k, v.map{|i| ' * %s'}.join("\n")]
46
+ end.join("\n")
47
+ end
43
48
  end
44
- end
45
49
 
46
- class FieldValidationError < Exception
47
- attr_reader :messages
50
+ class Field
51
+ attr_accessor :required, :label, :initial, :error_messages, :widget, :hidden_widget, :show_hidden_initial, :help_text, :validators
48
52
 
49
- def initialize(message)
50
- message = [message] unless message.is_a?(Array)
51
- @messages = ErrorList.new(message)
52
- end
53
+ def initialize(options={})
54
+ @required = options.fetch(:required, true)
55
+ @show_hidden_initial = options.fetch(:show_hidden_initial, false)
56
+ @label = options[:label]
57
+ @initial = options[:initial]
58
+ @help_text = options.fetch(:help_text, '')
59
+ @widget = options.fetch(:widget, default_widget)
53
60
 
54
- def to_s
55
- @messages.inspect
56
- end
57
- end
61
+ @widget = @widget.new if @widget.is_a?(Class)
62
+ @widget.attrs.update(widget_attrs(@widget))
63
+ @widget.is_required = @required
58
64
 
59
- class Field
60
- include Validation::Validators
61
- include Validation::Converters
65
+ @hidden_widget = options.fetch(:hidden_widget, default_hidden_widget)
66
+ @hidden_widget = @hidden_widget.new if @hidden_widget.is_a?(Class)
62
67
 
63
- class << self
64
- attr_reader :default_error_messages
68
+ @error_messages = default_error_messages.
69
+ merge(options.fetch(:error_messages, {}))
65
70
 
66
- def set_error(key, template)
67
- @default_error_messages ||= {}
68
- @default_error_messages[key] = template
71
+ @validators = default_validators + options.fetch(:validators, [])
69
72
  end
70
73
 
71
- def widget(widget=nil)
72
- @widget = widget unless widget.nil?
73
- @widget
74
+ # Default error messages for this kind of field. Override on subclasses to add or replace messages
75
+ def default_error_messages
76
+ {
77
+ required: 'This field is required',
78
+ invalid: 'Enter a valid value'
79
+ }
74
80
  end
75
81
 
76
- def hidden_widget(hidden_widget=nil)
77
- @hidden_widget = hidden_widget unless hidden_widget.nil?
78
- @hidden_widget
82
+ # Default validators for this kind of field.
83
+ def default_validators
84
+ []
79
85
  end
80
86
 
81
- # Copy field properties to the child class
82
- def inherited(c)
83
- c.widget widget
84
- c.hidden_widget hidden_widget
85
- default_error_messages.each {|k, v| c.set_error k, v}
87
+ # Default widget for this kind of field. Override on subclasses to customize.
88
+ def default_widget
89
+ Widgets::TextInput
86
90
  end
87
- end
88
91
 
89
- # Field properties
90
- widget Widgets::TextInput
91
- hidden_widget Widgets::HiddenInput
92
- set_error :required, 'This field is required'
93
- set_error :invalid, 'Enter a valid value'
92
+ # Default hidden widget for this kind of field. Override on subclasses to customize.
93
+ def default_hidden_widget
94
+ Widgets::HiddenInput
95
+ end
94
96
 
95
- attr_accessor :required, :label, :initial, :error_messages, :widget, :hidden_widget, :show_hidden_initial, :help_text
97
+ def prepare_value(value)
98
+ value
99
+ end
96
100
 
97
- def initialize(options={})
98
- @required = options.fetch(:required, true)
99
- @show_hidden_initial = options.fetch(:show_hidden_initial, false)
100
- @label = options[:label]
101
- @initial = options[:initial]
102
- @help_text = options.fetch(:help_text, '')
103
- @widget = options.fetch(:widget, self.class.widget)
101
+ def to_object(value)
102
+ value
103
+ end
104
104
 
105
- @widget = @widget.new if @widget.is_a?(Class)
106
- extra_attrs = widget_attrs(@widget)
107
- @widget.attrs.update(extra_attrs) if extra_attrs
105
+ def validate(value)
106
+ if required && Validators.empty_value?(value)
107
+ raise ValidationError.new(error_messages[:required])
108
+ end
109
+ end
108
110
 
109
- @error_messages = self.class.default_error_messages.
110
- merge(options.fetch(:error_messages, {}))
111
- end
111
+ def run_validators(value)
112
+ if Validators.empty_value?(value)
113
+ return
114
+ end
112
115
 
113
- def validating
114
- yield
115
- rescue Validation::ValidationError => error
116
- tpl = error_messages.fetch(error.error_code, error.error_code.to_s)
117
- msg = Utils.format_string(tpl, error.parameters)
118
- raise FieldValidationError.new(msg)
119
- end
116
+ errors = []
120
117
 
121
- def clean(value)
122
- validating { is_present(value) if @required }
123
- value
124
- end
118
+ validators.each do |v|
119
+ begin
120
+ v.call(value)
121
+ rescue ValidationError => e
122
+ if e.code && error_messages.has_key?(e.code)
123
+ message = error_messages[e.code]
124
+
125
+ if e.params
126
+ message = Utils.format_string(message, e.params)
127
+ end
128
+
129
+ errors << message
130
+ else
131
+ errors += e.messages
132
+ end
133
+ end
134
+ end
125
135
 
126
- def widget_attrs(widget)
127
- # Override to add field specific attributes
128
- end
136
+ unless errors.empty?
137
+ raise ValidationError.new(errors)
138
+ end
139
+ end
129
140
 
130
- def initialize_copy(original)
131
- super(original)
132
- @initial = original.initial ? original.initial.dup : original.initial
133
- @label = original.label ? original.label.dup : original.label
134
- @error_messages = original.error_messages.dup
135
- end
141
+ def clean(value)
142
+ value = to_object(value)
143
+ validate(value)
144
+ run_validators(value)
145
+ value
146
+ end
136
147
 
137
- end
148
+ # The data to be displayed when rendering for a bound form
149
+ def bound_data(data, initial)
150
+ data
151
+ end
138
152
 
139
- class CharField < Field
140
- set_error :max_length, 'Ensure this value has at most %(max)s characters (it has %(length)s).'
141
- set_error :min_length, 'Ensure this value has at least %(min)s characters (it has %(length)s).'
153
+ # List of attributes to add on the widget. Override to add field specific attributes
154
+ def widget_attrs(widget)
155
+ {}
156
+ end
142
157
 
143
- attr_accessor :max_length, :min_length
158
+ # Populates object.name if posible
159
+ def populate_object(object, name, value)
160
+ setter = :"#{name}="
144
161
 
145
- def initialize(options={})
146
- @max_length = options.delete(:max_length)
147
- @min_length = options.delete(:min_length)
148
- super(options)
149
- end
162
+ if object.respond_to?(setter)
163
+ object.send(setter, value)
164
+ end
165
+ end
166
+
167
+ def initialize_copy(original)
168
+ super(original)
169
+ @initial = original.initial
170
+ begin
171
+ @initial = @initial.dup
172
+ rescue TypeError
173
+ # non-clonable
174
+ end
175
+ @label = original.label && original.label.dup
176
+ @widget = original.widget && original.widget.dup
177
+ @validators = original.validators.dup
178
+ @error_messages = original.error_messages.dup
179
+ end
150
180
 
151
- def widget_attrs(widget)
152
- return if @max_length.nil?
153
- return {:maxlength => @max_length.to_s} if
154
- widget.kind_of?(Widgets::TextInput) || widget.kind_of?(Widgets::PasswordInput)
155
181
  end
156
182
 
157
- def clean(value)
158
- super(value)
159
- return '' if empty_value?(value)
183
+ class CharField < Field
184
+ attr_accessor :max_length, :min_length
185
+
186
+ def initialize(options = {})
187
+ @max_length = options.delete(:max_length)
188
+ @min_length = options.delete(:min_length)
189
+ super(options)
160
190
 
161
- validating do
162
- has_max_length(value, @max_length) if @max_length
163
- has_min_length(value, @min_length) if @min_length
191
+ if @min_length
192
+ validators << Validators::MinLengthValidator.new(@min_length)
164
193
  end
165
194
 
166
- value
167
- end
168
- end
195
+ if @max_length
196
+ validators << Validators::MaxLengthValidator.new(@max_length)
197
+ end
198
+ end
169
199
 
170
- class IntegerField < Field
171
- set_error :invalid, 'Enter a whole number.'
172
- set_error :max_value, 'Ensure this value is less than or equal to %(max)s.'
173
- set_error :min_value, 'Ensure this value is greater than or equal to %(min)s.'
200
+ def to_object(value)
201
+ if Validators.empty_value?(value)
202
+ ''
203
+ else
204
+ value
205
+ end
206
+ end
174
207
 
175
- def initialize(options={})
176
- @max_value = options.delete(:max_value)
177
- @min_value = options.delete(:min_value)
178
- super(options)
208
+ def widget_attrs(widget)
209
+ super(widget).tap do |attrs|
210
+ if @max_length && (widget.kind_of?(Widgets::TextInput) ||
211
+ widget.kind_of?(Widgets::PasswordInput))
212
+ attrs.merge(maxlength: @max_length.to_s)
213
+ end
214
+ end
215
+ end
179
216
  end
180
217
 
181
- def clean(value)
182
- super(value)
183
- return nil if empty_value?(value)
218
+ class IntegerField < Field
219
+ def initialize(options={})
220
+ @max_value = options.delete(:max_value)
221
+ @min_value = options.delete(:min_value)
222
+ super(options)
184
223
 
185
- validating do
186
- value = to_integer(value)
187
- is_not_greater_than(value, @max_value) if @max_value
188
- is_not_lesser_than(value, @min_value) if @min_value
224
+ if @min_value
225
+ validators << Validators::MinValueValidator.new(@min_value)
189
226
  end
190
227
 
191
- value
192
- end
193
- end
228
+ if @max_value
229
+ validators << Validators::MaxValueValidator.new(@max_value)
230
+ end
231
+ end
232
+
233
+ def default_error_messages
234
+ super.merge(invalid: 'Enter a whole number.',
235
+ max_value: 'Ensure this value is less than or equal to %(max)s.',
236
+ min_value: 'Ensure this value is greater than or equal to %(min)s.')
237
+ end
238
+
239
+ def to_object(value)
240
+ value = super(value)
194
241
 
195
- class FloatField < Field
196
- set_error :invalid, 'Enter a number.'
197
- set_error :max_value, 'Ensure this value is less than or equal to %(max)s.'
198
- set_error :min_value, 'Ensure this value is greater than or equal to %(min)s.'
242
+ if Validators.empty_value?(value)
243
+ return nil
244
+ end
199
245
 
200
- def initialize(options={})
201
- @max_value = options.delete(:max_value)
202
- @min_value = options.delete(:min_value)
203
- super(options)
246
+ begin
247
+ Integer(value.to_s)
248
+ rescue ArgumentError
249
+ raise ValidationError.new(error_messages[:invalid])
250
+ end
251
+ end
204
252
  end
205
253
 
206
- def clean(value)
207
- super(value)
208
- return nil if empty_value?(value)
254
+ class FloatField < IntegerField
255
+ def default_error_messages
256
+ super.merge(invalid: 'Enter a number.')
257
+ end
209
258
 
210
- validating do
211
- value = to_float(value)
212
- is_not_greater_than(value, @max_value) if @max_value
213
- is_not_lesser_than(value, @min_value) if @min_value
259
+ def to_object(value)
260
+ if Validators.empty_value?(value)
261
+ return nil
214
262
  end
215
263
 
216
- value
264
+ begin
265
+ Utils.make_float(value.to_s)
266
+ rescue ArgumentError
267
+ raise ValidationError.new(error_messages[:invalid])
268
+ end
269
+ end
217
270
  end
218
- end
219
271
 
220
- class BigDecimalField < Field
221
- set_error :invalid, 'Enter a number.'
222
- set_error :max_value, 'Ensure this value is less than or equal to %(max)s.'
223
- set_error :min_value, 'Ensure this value is greater than or equal to %(min)s.'
224
- set_error :max_digits, 'Ensure that there are no more than %(max)s digits in total.'
225
- set_error :max_decimal_places, 'Ensure that there are no more than %(max)s decimal places.'
226
- set_error :max_whole_digits, 'Ensure that there are no more than %(max)s digits before the decimal point.'
227
-
228
- def initialize(options={})
229
- @max_value = options.delete(:max_value)
230
- @min_value = options.delete(:min_value)
231
- @max_digits = options.delete(:max_digits)
232
- @max_decimal_places = options.delete(:max_decimal_places)
233
- @whole_digits = @max_digits - @decimal_places if @max_digits && @decimal_places
234
- super(options)
235
- end
272
+ class BigDecimalField < Field
273
+ def initialize(options={})
274
+ @max_value = options.delete(:max_value)
275
+ @min_value = options.delete(:min_value)
276
+ @max_digits = options.delete(:max_digits)
277
+ @max_decimal_places = options.delete(:max_decimal_places)
278
+
279
+ if @max_digits && @max_decimal_places
280
+ @max_whole_digits = @max_digits - @decimal_places
281
+ end
236
282
 
237
- def clean(value)
238
- super(value)
239
- return nil if !@required && empty_value?(value)
283
+ super(options)
240
284
 
241
- validating do
242
- value = to_big_decimal(value.to_s.strip)
243
- is_not_greater_than(value, @max_value) if @max_value
244
- is_not_lesser_than(value, @min_value) if @min_value
245
- has_max_digits(value, @max_digits) if @max_digits
246
- has_max_decimal_places(value, @max_decimal_places) if @max_decimal_places
247
- has_max_whole_digits(value, @max_whole_digits) if @max_whole_digits
285
+ if @min_value
286
+ validators << Validators::MinValueValidator.new(@min_value)
248
287
  end
249
288
 
250
- value
251
- end
252
- end
289
+ if @max_value
290
+ validators << Validators::MaxValueValidator.new(@max_value)
291
+ end
292
+ end
293
+
294
+ def default_error_messages
295
+ super.merge(invalid: 'Enter a number.',
296
+ max_value: 'Ensure this value is less than or equal to %(max)s.',
297
+ min_value: 'Ensure this value is greater than or equal to %(min)s.',
298
+ max_digits: 'Ensure that there are no more than %(max)s digits in total.',
299
+ max_decimal_places: 'Ensure that there are no more than %(max)s decimal places.',
300
+ max_whole_digits: 'Ensure that there are no more than %(max)s digits before the decimal point.')
301
+ end
253
302
 
254
- # DateField
255
- # TimeField
256
- # DateTimeField
303
+ def to_object(value)
304
+ if Validators.empty_value?(value)
305
+ return nil
306
+ end
257
307
 
258
- class RegexField < CharField
259
- def initialize(regex, options={})
260
- error_message = options.delete(:error_message)
261
- if error_message
262
- options[:error_messages] ||= {}
263
- options[:error_messages][:invalid] = error_messages
308
+ begin
309
+ Utils.make_float(value)
310
+ BigDecimal.new(value)
311
+ rescue ArgumentError
312
+ raise ValidationError.new(error_messages[:invalid])
313
+ end
264
314
  end
265
- super(options)
266
- @regex = regex
267
- end
268
315
 
269
- def clean(value)
270
- value = super(value)
271
- return value if value.empty?
272
- validating { matches_regex(value, @regex) }
273
- value
316
+ def validate(value)
317
+ super(value)
318
+
319
+ if Validators.empty_value?(value)
320
+ return nil
321
+ end
322
+
323
+ if value.nan? || value.infinite?
324
+ raise ValidationError.new(error_messages[:invalid])
325
+ end
326
+
327
+ sign, alldigits, _, whole_digits = value.split
328
+
329
+ if @max_digits && alldigits.length > @max_digits
330
+ msg = Utils.format_string(error_messages[:max_digits],
331
+ max: @max_digits)
332
+ raise ValidationError.new(msg)
333
+ end
334
+
335
+ decimals = alldigits.length - whole_digits
336
+
337
+ if @max_decimal_places && decimals > @max_decimal_places
338
+ msg = Utils.format_string(error_messages[:max_decimal_places],
339
+ max: @max_decimal_places)
340
+ raise ValidationError.new(msg)
341
+ end
342
+
343
+ if @max_whole_digits && whole_digits > @max_whole_digits
344
+ msg = Utils.format_string(error_messages[:max_whole_digits],
345
+ max: @max_whole_digits)
346
+ raise ValidationError.new(msg)
347
+ end
348
+
349
+ value
350
+ end
274
351
  end
275
- end
276
352
 
277
- class EmailField < RegexField
278
- # Original from Django's EmailField:
279
- # email_re = re.compile(
280
- # r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom
281
- # r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string
282
- # r')@(?:[A-Z0-9]+(?:-*[A-Z0-9]+)*\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain
283
- EMAIL_RE = /(^[-!#\$%&'*+\/=?^_`{}|~0-9A-Z]+(\.[-!#\$%&'*+\/=?^_`{}|~0-9A-Z]+)*|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*")@(?:[A-Z0-9]+(?:-*[A-Z0-9]+)*\.)+[A-Z]{2,6}$/i
353
+ # DateField
354
+ # TimeField
355
+ # DateTimeField
356
+
357
+ class RegexField < CharField
358
+ def initialize(regex, options={})
359
+ error_message = options.delete(:error_message)
360
+
361
+ if error_message
362
+ options[:error_messages] ||= {}
363
+ options[:error_messages][:invalid] = error_message
364
+ end
365
+
366
+ super(options)
284
367
 
285
- set_error :invalid, 'Enter a valid e-mail address.'
368
+ @regex = regex
286
369
 
287
- def initialize(options={})
288
- super(EMAIL_RE, options)
370
+ validators << Validators::RegexValidator.new(regex: regex)
371
+ end
289
372
  end
290
- end
291
373
 
292
- # TODO: add tests
293
- class FileField < Field
294
- widget Widgets::FileInput
295
- set_error :invalid, 'No file was submitted. Check the encoding type on the form.'
296
- set_error :missing, 'No file was submitted.'
297
- set_error :empty, 'The submitted file is empty.'
298
- set_error :max_length, 'Ensure this filename has at most %(max)d characters (it has %(length)d).'
299
-
300
- def initialize(options)
301
- @max_length = options.delete(:max_length)
302
- super(options)
374
+ class EmailField < CharField
375
+ def default_error_messages
376
+ super.merge(invalid: 'Enter a valid e-mail address.')
377
+ end
378
+
379
+ def default_validators
380
+ [Validators::ValidateEmail]
381
+ end
382
+
383
+ def clean(value)
384
+ value = to_object(value).strip
385
+ super(value)
386
+ end
303
387
  end
304
388
 
305
- def clean(data, initial=nil)
306
- super(initial || data)
389
+ # TODO: rewrite
390
+ class FileField < Field
391
+ def initialize(options)
392
+ @max_length = options.delete(:max_length)
393
+ @allow_empty_file = options.delete(:allow_empty_file)
394
+ super(options)
395
+ end
307
396
 
308
- if !required && empty_value?(data)
309
- return nil
310
- elsif !data && initial
311
- return initial
397
+ def default_error_messages
398
+ super.merge(invalid: 'No file was submitted. Check the encoding type on the form.',
399
+ missing: 'No file was submitted.',
400
+ empty: 'The submitted file is empty.',
401
+ max_length: 'Ensure this filename has at most %(max)d characters (it has %(length)d).',
402
+ contradiction: 'Please either submit a file or check the clear checkbox, not both.')
312
403
  end
313
404
 
314
- # TODO: file validators?
315
- validating do
316
- # UploadedFile objects should have name and size attributes.
317
- begin
318
- file_name = data.name
319
- file_size = data.size
320
- rescue NoMethodError
321
- fail_with(:invalid)
405
+ def default_widget
406
+ Widgets::ClearableFileInput
407
+ end
408
+
409
+ def to_object(data)
410
+ if Validators.empty_value?(data)
411
+ return nil
412
+ end
413
+
414
+ # UploadedFile objects should have name and size attributes.
415
+ begin
416
+ file_name = data.name
417
+ file_size = data.size
418
+ rescue NoMethodError
419
+ raise ValidationError.new(error_messages[:invalid])
420
+ end
421
+
422
+ if @max_length && file_name.length > @max_length
423
+ msg = Utils.format_string(error_messages[:max_length],
424
+ max: @max_length,
425
+ length: file_name.length)
426
+ raise ValidationError.new(msg)
427
+ end
428
+
429
+ if Utils.blank_value?(file_name)
430
+ raise ValidationError.new(error_messages[:invalid])
431
+ end
432
+
433
+ if !@allow_empty_file && !file_size
434
+ raise ValidationError.new(error_messages[:empty])
435
+ end
436
+
437
+ data
438
+ end
439
+
440
+ def clean(data, initial = nil)
441
+ # If the widget got contradictory inputs, we raise a validation error
442
+ if data.object_id == Widgets::ClearableFileInput::FILE_INPUT_CONTRADICTION.object_id
443
+ raise ValidationError.new(error_messages[:contradiction])
444
+ end
445
+
446
+ # False means the field value should be cleared; further validation is
447
+ # not needed.
448
+ if data == false
449
+ unless @required
450
+ return false
322
451
  end
323
452
 
324
- fail_with(:max_length, :max => @max_length, :length => file_name.length) if
325
- @max_length && file_name.length > @max_length
453
+ # If the field is required, clearing is not possible (the widget
454
+ # shouldn't return false data in that case anyway). false is not
455
+ # an 'empty_value'; if a false value makes it this far
456
+ # it should be validated from here on out as nil (so it will be
457
+ # caught by the required check).
458
+ data = nil
459
+ end
326
460
 
327
- fail_with(:invalid) unless file_name
328
- fail_with(:empty) unless file_size || file_size == 0
461
+ if !data && initial
462
+ initial
463
+ else
464
+ super(data)
329
465
  end
466
+ end
330
467
 
331
- data
468
+ def bound_data(data, initial)
469
+ if data.nil? || data.object_id == Widgets::ClearableFileInput::FILE_INPUT_CONTRADICTION.object_id
470
+ initial
471
+ else
472
+ data
473
+ end
474
+ end
332
475
  end
333
- end
334
476
 
335
- #class ImageField < FileField
336
- #end
477
+ #class ImageField < FileField
478
+ #end
337
479
 
338
- # URLField
480
+ # URLField
339
481
 
340
- class BooleanField < Field
341
- widget Widgets::CheckboxInput
482
+ class BooleanField < Field
483
+ def default_widget
484
+ Widgets::CheckboxInput
485
+ end
342
486
 
343
- def clean(value)
344
- value = to_bool(value)
345
- super(value)
346
- validating { is_true(value) if @required }
347
- value
348
- end
349
- end
487
+ def to_object(value)
488
+ if value.kind_of?(String) && ['false', '0'].include?(value.downcase)
489
+ value = false
490
+ else
491
+ value = Utils.make_bool(value)
492
+ end
350
493
 
351
- class NullBooleanField < BooleanField
352
- widget Widgets::NullBooleanSelect
494
+ value = super(value)
353
495
 
354
- def clean(value)
355
- case value
356
- when true, 'true', '1', 'on' then true
357
- when false, 'false', '0' then false
358
- else nil
496
+ if !value && required
497
+ raise ValidationError.new(error_messages[:required])
498
+ end
499
+
500
+ value
359
501
  end
360
502
  end
361
- end
362
503
 
363
- class ChoiceField < Field
364
- widget Widgets::Select
365
- set_error :invalid_choice, 'Select a valid choice. %(value)s is not one of the available choices.'
504
+ class NullBooleanField < BooleanField
505
+ def default_widget
506
+ Widgets::NullBooleanSelect
507
+ end
366
508
 
367
- def initialize(choices=[], options={})
368
- options[:required] = options.fetch(:required, true)
369
- super(options)
370
- self.choices = choices
371
- end
509
+ def to_object(value)
510
+ case value
511
+ when true, 'true', '1', 'on' then true
512
+ when false, 'false', '0' then false
513
+ else nil
514
+ end
515
+ end
372
516
 
373
- def choices
374
- @choices
517
+ def validate(value)
518
+ end
375
519
  end
376
520
 
377
- def choices=(value)
378
- @choices = @widget.choices = value.to_a
379
- end
521
+ class ChoiceField < Field
522
+ def initialize(choices=[], options={})
523
+ options[:required] = options.fetch(:required, true)
524
+ super(options)
525
+ self.choices = choices
526
+ end
527
+
528
+ def initialize_copy(original)
529
+ super(original)
530
+ self.choices = original.choices.dup
531
+ end
380
532
 
381
- def clean(value)
382
- value = super(value)
383
- value = '' if empty_value?(value)
384
- value = value.to_s
533
+ def default_error_messages
534
+ super.merge(invalid_choice: 'Select a valid choice. %(value)s is not one of the available choices.')
535
+ end
385
536
 
386
- return value if value.empty?
537
+ def default_widget
538
+ Widgets::Select
539
+ end
387
540
 
388
- validating do
389
- fail_with(:invalid_choice, :value => value) unless valid_value?(value)
541
+ def choices
542
+ @choices
543
+ end
544
+
545
+ def choices=(value)
546
+ @choices = @widget.choices = value
547
+ end
548
+
549
+ def to_object(value)
550
+ if Validators.empty_value?(value)
551
+ ''
552
+ else
553
+ value.to_s
390
554
  end
555
+ end
391
556
 
392
- value
393
- end
557
+ def validate(value)
558
+ super(value)
394
559
 
395
- def valid_value?(value)
396
- @choices.each do |k, v|
560
+ unless !value || Validators.empty_value?(value) || valid_value?(value)
561
+ msg = Utils.format_string(error_messages[:invalid_choice],
562
+ value: value)
563
+ raise ValidationError.new(msg)
564
+ end
565
+ end
566
+
567
+ def valid_value?(value)
568
+ @choices.each do |k, v|
397
569
  if v.is_a?(Array)
398
570
  # This is an optgroup, so look inside the group for options
399
571
  v.each do |k2, v2|
400
572
  return true if value == k2.to_s
401
573
  end
574
+ elsif k.is_a?(Hash)
575
+ # this is a hash valued choice list
576
+ return true if value == k[:value].to_s
402
577
  else
403
578
  return true if value == k.to_s
404
579
  end
405
580
  end
406
- false
407
- end
408
- end
409
581
 
410
- # TODO: tests
411
- class TypedChoiceField < ChoiceField
412
- def initialize(choices=[], options={})
413
- @coerce = options.delete(:coerce) || lambda{|val| val}
414
- @empty_value = options.fetch(:empty_value, '')
415
- options.delete(:empty_value)
416
- super(choices, options)
582
+ false
583
+ end
417
584
  end
418
585
 
419
- def clean(value)
420
- value = super(value)
421
- return @empty_value if value == @empty_value || empty_value?(value)
586
+ class TypedChoiceField < ChoiceField
587
+ def initialize(choices=[], options={})
588
+ @coerce = options.delete(:coerce) || lambda{|val| val}
589
+ @empty_value = options.fetch(:empty_value, '')
590
+ options.delete(:empty_value)
591
+ super(choices, options)
592
+ end
593
+
594
+ def to_object(value)
595
+ value = super(value)
596
+ original_validate(value)
422
597
 
423
- begin
424
- @coerce.call(value)
425
- rescue
426
- validating { fail_with(:invalid_choice, :value => value) }
598
+ if value == @empty_value || Validators.empty_value?(value)
599
+ return @empty_value
600
+ end
601
+
602
+ begin
603
+ @coerce.call(value)
604
+ rescue TypeError, ValidationError
605
+ msg = Utils.format_string(error_messages[:invalid_choice],
606
+ value: value)
607
+ raise ValidationError.new(msg)
608
+ end
609
+ end
610
+
611
+ alias_method :original_validate, :validate
612
+
613
+ def validate(value)
427
614
  end
428
615
  end
429
- end
430
616
 
431
- # TODO: tests
432
- class MultipleChoiceField < ChoiceField
433
- widget Widgets::SelectMultiple
434
- hidden_widget Widgets::MultipleHiddenInput
435
- set_error :invalid_choice, 'Select a valid choice. %(value)s is not one of the available choices.'
436
- set_error :invalid_list, 'Enter a list of values.'
437
-
438
- def clean(value)
439
- validating do
440
- is_present(value) if @required
441
- return [] if ! @required && ! value || value.empty?
442
- is_array(value)
443
- not_empty(value) if @required
444
-
445
- new_value = value.map(&:to_s)
446
- new_value.each do |val|
447
- fail_with(:invalid_choice, :value => val) unless valid_value?(val)
617
+ class MultipleChoiceField < ChoiceField
618
+ def default_error_messages
619
+ super.merge(invalid_choice: 'Select a valid choice. %(value)s is not one of the available choices.',
620
+ invalid_list: 'Enter a list of values.')
621
+ end
622
+
623
+ def default_widget
624
+ Widgets::SelectMultiple
625
+ end
626
+
627
+ def default_hidden_widget
628
+ Widgets::MultipleHiddenInput
629
+ end
630
+
631
+ def to_object(value)
632
+ if !value || Validators.empty_value?(value)
633
+ []
634
+ elsif !value.is_a?(Array)
635
+ raise ValidationError.new(error_messages[:invalid_list])
636
+ else
637
+ value.map(&:to_s)
638
+ end
639
+ end
640
+
641
+ def validate(value)
642
+ if required && (!value || Validators.empty_value?(value))
643
+ raise ValidationError.new(error_messages[:required])
644
+ end
645
+
646
+ value.each do |val|
647
+ unless valid_value?(val)
648
+ msg = Utils.format_string(error_messages[:invalid_choice],
649
+ value: val)
650
+ raise ValidationError.new(msg)
448
651
  end
449
652
  end
653
+ end
654
+ end
655
+
656
+ # TypedMultipleChoiceField < MultipleChoiceField
657
+
658
+ # TODO: tests
659
+ class ComboField < Field
660
+ def initialize(fields=[], *args)
661
+ super(*args)
662
+ fields.each {|f| f.required = false}
663
+ @fields = fields
664
+ end
665
+
666
+ def clean(value)
667
+ super(value)
668
+ @fields.each {|f| value = f.clean(value)}
669
+ value
670
+ end
671
+ end
672
+
673
+ # MultiValueField
674
+ # FilePathField
675
+ # SplitDateTimeField
676
+
677
+ class IPAddressField < CharField
678
+ def default_error_messages
679
+ super.merge(invalid: 'Enter a valid IPv4 address.')
680
+ end
681
+
682
+ def default_validators
683
+ [Validators::ValidateIPV4Address]
684
+ end
685
+ end
686
+
687
+ class SlugField < CharField
688
+ def default_error_messages
689
+ super.merge(invalid: "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.")
690
+ end
450
691
 
451
- new_value
692
+ def default_validators
693
+ [Validators::ValidateSlug]
694
+ end
452
695
  end
453
696
  end
454
-
455
- # ComboField
456
- # MultiValueField
457
- # FilePathField
458
- # SplitDateTimeField
459
- # IPAddressField
460
- # SlugField
461
- end; end
697
+ end