bureaucrat 0.0.1
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/LICENSE +22 -0
- data/README.md +124 -0
- data/lib/bureaucrat/fields.rb +461 -0
- data/lib/bureaucrat/forms.rb +346 -0
- data/lib/bureaucrat/formsets.rb +256 -0
- data/lib/bureaucrat/quickfields.rb +59 -0
- data/lib/bureaucrat/utils.rb +84 -0
- data/lib/bureaucrat/validation.rb +130 -0
- data/lib/bureaucrat/validation_old.rb +148 -0
- data/lib/bureaucrat/widgets.rb +397 -0
- data/lib/bureaucrat/wizard.rb +220 -0
- data/lib/bureaucrat.rb +10 -0
- data/test/fields_test.rb +577 -0
- data/test/forms_test.rb +131 -0
- data/test/formsets_test.rb +51 -0
- data/test/test_helper.rb +22 -0
- data/test/widgets_test.rb +328 -0
- metadata +71 -0
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
(The MIT License)
|
2
|
+
|
3
|
+
Copyright (c) 2009 Bruno Deferrari
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
'Software'), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
19
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
20
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
21
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
22
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
Bureaucrat
|
2
|
+
==========
|
3
|
+
|
4
|
+
Form handling for Ruby inspired by Django forms.
|
5
|
+
|
6
|
+
Structure
|
7
|
+
---------
|
8
|
+
|
9
|
+
Form ----> as_<render_mode>, valid?, errors/cleaned_data
|
10
|
+
______|________
|
11
|
+
/ | \
|
12
|
+
Field Field Field ----> clean
|
13
|
+
| | |
|
14
|
+
Widget Widget Widget ----> render
|
15
|
+
|
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
|
23
|
+
|
24
|
+
Usage examples
|
25
|
+
--------------
|
26
|
+
|
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
|
84
|
+
|
85
|
+
Examples of different ways of defining forms
|
86
|
+
--------------
|
87
|
+
|
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
|
@@ -0,0 +1,461 @@
|
|
1
|
+
require 'bureaucrat/utils'
|
2
|
+
require 'bureaucrat/validation'
|
3
|
+
require 'bureaucrat/widgets'
|
4
|
+
|
5
|
+
module Bureaucrat
|
6
|
+
module Fields
|
7
|
+
|
8
|
+
class ErrorList < Array
|
9
|
+
include Utils
|
10
|
+
|
11
|
+
def to_s
|
12
|
+
as_ul
|
13
|
+
end
|
14
|
+
|
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
|
20
|
+
|
21
|
+
def as_text
|
22
|
+
empty? ? '' : map{|e| '* %s' % e}.join("\n")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class ErrorHash < Hash
|
27
|
+
include Utils
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
as_ul
|
31
|
+
end
|
32
|
+
|
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
|
38
|
+
|
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
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class FieldValidationError < Exception
|
47
|
+
attr_reader :messages
|
48
|
+
|
49
|
+
def initialize(message)
|
50
|
+
message = [message] unless message.is_a?(Array)
|
51
|
+
@messages = ErrorList.new(message)
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_s
|
55
|
+
@messages.inspect
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Field
|
60
|
+
include Validation::Validators
|
61
|
+
include Validation::Converters
|
62
|
+
|
63
|
+
class << self
|
64
|
+
attr_reader :default_error_messages
|
65
|
+
|
66
|
+
def set_error(key, template)
|
67
|
+
@default_error_messages ||= {}
|
68
|
+
@default_error_messages[key] = template
|
69
|
+
end
|
70
|
+
|
71
|
+
def widget(widget=nil)
|
72
|
+
@widget = widget unless widget.nil?
|
73
|
+
@widget
|
74
|
+
end
|
75
|
+
|
76
|
+
def hidden_widget(hidden_widget=nil)
|
77
|
+
@hidden_widget = hidden_widget unless hidden_widget.nil?
|
78
|
+
@hidden_widget
|
79
|
+
end
|
80
|
+
|
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}
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
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'
|
94
|
+
|
95
|
+
attr_accessor :required, :label, :initial, :error_messages, :widget, :hidden_widget, :show_hidden_initial, :help_text
|
96
|
+
|
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)
|
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
|
108
|
+
|
109
|
+
@error_messages = self.class.default_error_messages.
|
110
|
+
merge(options.fetch(:error_messages, {}))
|
111
|
+
end
|
112
|
+
|
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
|
120
|
+
|
121
|
+
def clean(value)
|
122
|
+
validating { is_present(value) if @required }
|
123
|
+
value
|
124
|
+
end
|
125
|
+
|
126
|
+
def widget_attrs(widget)
|
127
|
+
# Override to add field specific attributes
|
128
|
+
end
|
129
|
+
|
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
|
136
|
+
|
137
|
+
end
|
138
|
+
|
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).'
|
142
|
+
|
143
|
+
attr_accessor :max_length, :min_length
|
144
|
+
|
145
|
+
def initialize(options={})
|
146
|
+
@max_length = options.delete(:max_length)
|
147
|
+
@min_length = options.delete(:min_length)
|
148
|
+
super(options)
|
149
|
+
end
|
150
|
+
|
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
|
+
end
|
156
|
+
|
157
|
+
def clean(value)
|
158
|
+
super(value)
|
159
|
+
return '' if empty_value?(value)
|
160
|
+
|
161
|
+
validating do
|
162
|
+
has_max_length(value, @max_length) if @max_length
|
163
|
+
has_min_length(value, @min_length) if @min_length
|
164
|
+
end
|
165
|
+
|
166
|
+
value
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
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.'
|
174
|
+
|
175
|
+
def initialize(options={})
|
176
|
+
@max_value = options.delete(:max_value)
|
177
|
+
@min_value = options.delete(:min_value)
|
178
|
+
super(options)
|
179
|
+
end
|
180
|
+
|
181
|
+
def clean(value)
|
182
|
+
super(value)
|
183
|
+
return nil if empty_value?(value)
|
184
|
+
|
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
|
189
|
+
end
|
190
|
+
|
191
|
+
value
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
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.'
|
199
|
+
|
200
|
+
def initialize(options={})
|
201
|
+
@max_value = options.delete(:max_value)
|
202
|
+
@min_value = options.delete(:min_value)
|
203
|
+
super(options)
|
204
|
+
end
|
205
|
+
|
206
|
+
def clean(value)
|
207
|
+
super(value)
|
208
|
+
return nil if empty_value?(value)
|
209
|
+
|
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
|
214
|
+
end
|
215
|
+
|
216
|
+
value
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
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
|
236
|
+
|
237
|
+
def clean(value)
|
238
|
+
super(value)
|
239
|
+
return nil if !@required && empty_value?(value)
|
240
|
+
|
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
|
248
|
+
end
|
249
|
+
|
250
|
+
value
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# DateField
|
255
|
+
# TimeField
|
256
|
+
# DateTimeField
|
257
|
+
|
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
|
264
|
+
end
|
265
|
+
super(options)
|
266
|
+
@regex = regex
|
267
|
+
end
|
268
|
+
|
269
|
+
def clean(value)
|
270
|
+
value = super(value)
|
271
|
+
return value if value.empty?
|
272
|
+
validating { matches_regex(value, @regex) }
|
273
|
+
value
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
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
|
284
|
+
|
285
|
+
set_error :invalid, 'Enter a valid e-mail address.'
|
286
|
+
|
287
|
+
def initialize(options={})
|
288
|
+
super(EMAIL_RE, options)
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
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)
|
303
|
+
end
|
304
|
+
|
305
|
+
def clean(data, initial=nil)
|
306
|
+
super(initial || data)
|
307
|
+
|
308
|
+
if !required && empty_value?(data)
|
309
|
+
return nil
|
310
|
+
elsif !data && initial
|
311
|
+
return initial
|
312
|
+
end
|
313
|
+
|
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)
|
322
|
+
end
|
323
|
+
|
324
|
+
fail_with(:max_length, :max => @max_length, :length => file_name.length) if
|
325
|
+
@max_length && file_name.length > @max_length
|
326
|
+
|
327
|
+
fail_with(:invalid) unless file_name
|
328
|
+
fail_with(:empty) unless file_size || file_size == 0
|
329
|
+
end
|
330
|
+
|
331
|
+
data
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
#class ImageField < FileField
|
336
|
+
#end
|
337
|
+
|
338
|
+
# URLField
|
339
|
+
|
340
|
+
class BooleanField < Field
|
341
|
+
widget Widgets::CheckboxInput
|
342
|
+
|
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
|
350
|
+
|
351
|
+
class NullBooleanField < BooleanField
|
352
|
+
widget Widgets::NullBooleanSelect
|
353
|
+
|
354
|
+
def clean(value)
|
355
|
+
case value
|
356
|
+
when true, 'true', '1' then true
|
357
|
+
when false, 'false', '0' then false
|
358
|
+
else nil
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
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.'
|
366
|
+
|
367
|
+
def initialize(choices=[], options={})
|
368
|
+
options[:required] = options.fetch(:required, true)
|
369
|
+
super(options)
|
370
|
+
self.choices = choices
|
371
|
+
end
|
372
|
+
|
373
|
+
def choices
|
374
|
+
@choices
|
375
|
+
end
|
376
|
+
|
377
|
+
def choices=(value)
|
378
|
+
@choices = @widget.choices = value.to_a
|
379
|
+
end
|
380
|
+
|
381
|
+
def clean(value)
|
382
|
+
value = super(value)
|
383
|
+
value = '' if empty_value?(value)
|
384
|
+
value = value.to_s
|
385
|
+
|
386
|
+
return value if value.empty?
|
387
|
+
|
388
|
+
validating do
|
389
|
+
fail_with(:invalid_choice, :value => value) unless valid_value?(value)
|
390
|
+
end
|
391
|
+
|
392
|
+
value
|
393
|
+
end
|
394
|
+
|
395
|
+
def valid_value?(value)
|
396
|
+
@choices.each do |k, v|
|
397
|
+
if v.is_a?(Array)
|
398
|
+
# This is an optgroup, so look inside the group for options
|
399
|
+
v.each do |k2, v2|
|
400
|
+
return true if value == k2.to_s
|
401
|
+
end
|
402
|
+
else
|
403
|
+
return true if value == k.to_s
|
404
|
+
end
|
405
|
+
end
|
406
|
+
false
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
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)
|
417
|
+
end
|
418
|
+
|
419
|
+
def clean(value)
|
420
|
+
value = super(value)
|
421
|
+
return @empty_value if value == @empty_value || empty_value?(value)
|
422
|
+
|
423
|
+
begin
|
424
|
+
@coerce.call(value)
|
425
|
+
rescue
|
426
|
+
validating { fail_with(:invalid_choice, :value => value) }
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
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)
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
new_value
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
# ComboField
|
456
|
+
# MultiValueField
|
457
|
+
# FilePathField
|
458
|
+
# SplitDateTimeField
|
459
|
+
# IPAddressField
|
460
|
+
# SlugField
|
461
|
+
end; end
|