bureaucrat 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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