├── .gitignore
├── LICENSE
├── README.md
├── Rakefile
├── bureaucrat.gemspec
├── examples
└── fields_test
│ ├── config.ru
│ ├── rackapp1.html
│ └── style.css
├── lib
├── bureaucrat.rb
└── bureaucrat
│ ├── fields.rb
│ ├── forms.rb
│ ├── formsets.rb
│ ├── quickfields.rb
│ ├── temporary_uploaded_file.rb
│ ├── utils.rb
│ ├── validators.rb
│ └── widgets.rb
└── test
├── fields_test.rb
├── forms_test.rb
├── formsets_test.rb
├── test_helper.rb
└── widgets_test.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | pkg/
2 | tmp/
3 | doc/
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
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.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Bureaucrat
2 | ==========
3 |
4 | Form handling for Ruby inspired by [Django forms](https://docs.djangoproject.com/en/dev/#forms).
5 |
6 | Description
7 | -----------
8 |
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
15 | ______|________
16 | / | \
17 | Field Field Field ----> clean
18 | | | |
19 | Widget Widget Widget ----> render
20 |
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.
33 |
34 | Usage examples
35 | --------------
36 |
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 | unbound_form[:nickname].to_s # => ''
64 | unbound_form[:nickname].errors # => []
65 | unbound_form[:nickname].label_tag # => ''
66 |
67 | # Bound form with validation errors
68 | invalid_bound_form = MyForm.new(nickname: 'bureaucrat', email: 'badformat', age: '30')
69 | invalid_bound_form.valid? # => false
70 | invalid_bound_form.errors # {email: ["Enter a valid e-mail address."]}
71 | invalid_bound_form.cleaned_data # => nil
72 | invalid_bound_form[:email].to_s # => ''
73 | invalid_bound_form[:email].errors # => ["Enter a valid e-mail address."]
74 | invalid_bound_form[:email].label_tag # => ''
75 |
76 | # Bound form without validation errors
77 | valid_bound_form = MyForm.new(nickname: 'bureaucrat', email: 'valid@email.com', age: '30')
78 | valid_bound_form.valid? # => true
79 | valid_bound_form.errors # {}
80 | valid_bound_form.cleaned_data # => {age: 30, newsletter: false, nickname: "bureaucrat", realname: "", :email = >"valid@email.com"}
81 |
82 | valid_bound_form.save # A new User is created and a confirmation mail is delivered
83 | ```
84 |
85 | Examples of different ways of defining forms
86 | --------------
87 |
88 | ```ruby
89 | require 'bureaucrat'
90 | require 'bureaucrat/quickfields'
91 |
92 | class MyForm < Bureaucrat::Forms::Form
93 | include Bureaucrat::Fields
94 |
95 | field :nickname, CharField.new(max_length: 50)
96 | field :realname, CharField.new(required: false)
97 | field :email, EmailField.new
98 | field :age, IntegerField.new(min_value: 0)
99 | field :newsletter, BooleanField.new(required: false)
100 | end
101 |
102 | class MyFormQuick < Bureaucrat::Forms::Form
103 | extend Bureaucrat::Quickfields
104 |
105 | string :nickname, max_length: 50
106 | string :realname, required: false
107 | email :email
108 | integer :age, min_value: 0
109 | boolean :newsletter, required: false
110 | end
111 |
112 | def inline_form
113 | f = Class.new(Bureaucrat::Forms::Form)
114 | f.extend(Bureaucrat::Quickfields)
115 | yield f
116 | f
117 | end
118 |
119 | form_maker = inline_form do |f|
120 | f.string :nickname, max_length: 50
121 | f.string :realname, required: false
122 | f.email :email
123 | f.integer :age, min_value: 0
124 | f.boolean :newsletter, required: false
125 | end
126 | ```
127 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
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
10 |
--------------------------------------------------------------------------------
/bureaucrat.gemspec:
--------------------------------------------------------------------------------
1 | Gem::Specification.new do |s|
2 | s.name = 'bureaucrat'
3 | s.version = '0.10.2'
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
33 |
--------------------------------------------------------------------------------
/examples/fields_test/config.ru:
--------------------------------------------------------------------------------
1 | require 'erb'
2 | require 'bigdecimal'
3 | require 'prettyprint'
4 | require 'rack'
5 | require 'bureaucrat'
6 | require 'bureaucrat/quickfields'
7 |
8 | class TestForm1 < Bureaucrat::Forms::Form
9 | extend Bureaucrat::Quickfields
10 |
11 | string :string_field
12 | string :string_field2, :required => false,\
13 | :label => 'Another String'
14 | text :text_field
15 | password :password, :required => false
16 | password :password_confirmation, :required => false
17 | integer :integer
18 | decimal :decimal
19 | regex :regex, /\w\d\d\w+/, :help_text => '\w\d\d\w+'
20 | email :email
21 | #file :file, :required => false
22 | boolean :boolean
23 | null_boolean :null_boolean
24 | choice :choice, [['', 'Select a letter'],
25 | ['a', 'Letter A'],
26 | ['b', 'Letter B']]
27 | multiple_choice :multiple_choice, [['', 'Select some letters'],
28 | ['a', 'Letter A'],
29 | ['b', 'Letter B'],
30 | ['c', 'Letter C']]
31 | radio_choice :radio_choice, [['a', 'Letter A'],
32 | ['b', 'Letter B'],
33 | ['c', 'Letter C']]
34 | checkbox_multiple_choice :checkbox_multiple_choice,\
35 | [['a', 'Letter A'],
36 | ['b', 'Letter B'],
37 | ['c', 'Letter C']]
38 |
39 | def initialize(*args)
40 | super(*args)
41 | @error_css_class = 'with-errors'
42 | @required_css_class = 'required'
43 | end
44 |
45 | def save
46 | puts cleaned_data
47 | end
48 | end
49 |
50 | template_path = File.join(File.dirname(__FILE__), 'rackapp1.html')
51 | template = File.open(template_path) do |f|
52 | ERB.new(f.read)
53 | end
54 |
55 | stylesheet_path = File.join(File.dirname(__FILE__), 'style.css')
56 | styles = File.open(stylesheet_path) do |f|
57 | f.read
58 | end
59 |
60 | fields_test_app = lambda do |env|
61 | req = Rack::Request.new(env)
62 |
63 | unless req.path == '/'
64 | return [404, {'Content-Type' => 'text', 'Content-Length' => '0'}, '']
65 | end
66 |
67 | if req.post?
68 | form = TestForm1.new(req.POST)
69 | else
70 | form = TestForm1.new
71 | end
72 |
73 | result = template.result(binding)
74 |
75 | [200, {'Content-Type' => 'text', 'Content-Length' => result.length.to_s}, [result]]
76 | end
77 |
78 | run fields_test_app
79 |
--------------------------------------------------------------------------------
/examples/fields_test/rackapp1.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Test 1
5 |
8 |
9 |
10 |
31 |
32 | <% if req.post? && form.valid? %>
33 | Result:
34 |
35 |
36 | <% form.cleaned_data.each do |label, data| %>
37 | <%= "#{label}: #{ERB::Util.html_escape(data.inspect)}" %>
38 | <% end %>
39 |
40 |
41 | <% end %>
42 |
43 |
44 |
--------------------------------------------------------------------------------
/examples/fields_test/style.css:
--------------------------------------------------------------------------------
1 | body{
2 | font-family:"Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif;
3 | font-size:12px;
4 | }
5 |
6 | p, h1, form, button {
7 | border:0;
8 | margin:0;
9 | padding:0;
10 | }
11 |
12 | .form {
13 | border:solid 2px #b7ddf2;
14 | background:#ebf4fb;
15 | }
16 |
17 | .form form {
18 | margin:0 auto;
19 | width:960px;
20 | padding:14px;
21 | }
22 |
23 | .form h1 {
24 | font-size:14px;
25 | font-weight:bold;
26 | margin-bottom:8px;
27 | }
28 | .form ul {
29 | list-style-type: none;
30 | }
31 | .form .field {
32 | padding: 10px 0;
33 | width: 470px;
34 | float: left;
35 | }
36 | .form .field.with-errors label {
37 | color: #dd0000;
38 | }
39 | .form .required label {
40 | color: #000000;
41 | }
42 | .form label {
43 | color: #999999;
44 | display:block;
45 | font-weight:bold;
46 | text-align:right;
47 | padding: 6px 2px;
48 | width:140px;
49 | float:left;
50 | }
51 | .form ul label {
52 | width: auto;
53 | }
54 | .form .help-text {
55 | color:#ffffff;
56 | background-color: #222244;
57 | border: solid 2px #ffffff;
58 | display:none;
59 | font-size:11px;
60 | font-weight:normal;
61 | text-align:right;
62 | padding: 6px 2px;
63 | position: absolute;
64 | top: -30px;
65 | right: -30px;
66 | }
67 | .form .field-errors {
68 | color:#ffffff;
69 | background-color: #bb2222;
70 | border: solid 2px #ffffff;
71 | display:none;
72 | font-size:11px;
73 | font-weight:normal;
74 | text-align:right;
75 | padding: 6px 2px;
76 | position: absolute;
77 | top: -30px;
78 | right: -30px;
79 | }
80 | .form .help, .form .error {
81 | position: relative;
82 | }
83 | .form .help:hover .help-text {
84 | display: block;
85 | }
86 | .form .error:hover .field-errors {
87 | display: block;
88 | }
89 | .form input, .form textarea, .form select {
90 | float:left;
91 | font-size:12px;
92 | padding:4px 2px;
93 | border:solid 1px #aacfe4;
94 | width:220px;
95 | margin:2px 0 20px 10px;
96 | }
97 | .form ul input {
98 | width: auto;
99 | }
100 | .form button {
101 | clear:both;
102 | margin-left:450px;
103 | width:125px;
104 | height:31px;
105 | background:#000000;
106 | text-align:center;
107 | line-height:31px;
108 | color:#FFFFFF;
109 | font-size:11px;
110 | font-weight:bold;
111 | }
112 |
--------------------------------------------------------------------------------
/lib/bureaucrat.rb:
--------------------------------------------------------------------------------
1 | module Bureaucrat
2 | VERSION = '0.10.0'
3 |
4 | class ValidationError < Exception
5 | attr_reader :code, :params, :messages
6 |
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
29 |
--------------------------------------------------------------------------------
/lib/bureaucrat/fields.rb:
--------------------------------------------------------------------------------
1 | require 'set'
2 |
3 | module Bureaucrat
4 | module Fields
5 |
6 | class ErrorList < Array
7 | include Utils
8 |
9 | def to_s
10 | as_ul
11 | end
12 |
13 | def as_ul
14 | if empty?
15 | ''
16 | else
17 | ul = ''
18 | li = '%s'
19 |
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
28 | end
29 |
30 | class ErrorHash < Hash
31 | include Utils
32 |
33 | def to_s
34 | as_ul
35 | end
36 |
37 | def as_ul
38 | ul = ''
39 | li = '%s%s'
40 | empty? ? '' : mark_safe(ul % map {|k, v| li % [k, v]}.join)
41 | end
42 |
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
48 | end
49 |
50 | class Field
51 | attr_accessor :required, :label, :initial, :error_messages, :widget, :hidden_widget, :show_hidden_initial, :help_text, :validators
52 |
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)
60 |
61 | @widget = @widget.new if @widget.is_a?(Class)
62 | @widget.attrs.update(widget_attrs(@widget))
63 | @widget.is_required = @required
64 |
65 | @hidden_widget = options.fetch(:hidden_widget, default_hidden_widget)
66 | @hidden_widget = @hidden_widget.new if @hidden_widget.is_a?(Class)
67 |
68 | @error_messages = default_error_messages.
69 | merge(options.fetch(:error_messages, {}))
70 |
71 | @validators = default_validators + options.fetch(:validators, [])
72 | end
73 |
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 | }
80 | end
81 |
82 | # Default validators for this kind of field.
83 | def default_validators
84 | []
85 | end
86 |
87 | # Default widget for this kind of field. Override on subclasses to customize.
88 | def default_widget
89 | Widgets::TextInput
90 | end
91 |
92 | # Default hidden widget for this kind of field. Override on subclasses to customize.
93 | def default_hidden_widget
94 | Widgets::HiddenInput
95 | end
96 |
97 | def prepare_value(value)
98 | value
99 | end
100 |
101 | def to_object(value)
102 | value
103 | end
104 |
105 | def validate(value)
106 | if required && Validators.empty_value?(value)
107 | raise ValidationError.new(error_messages[:required])
108 | end
109 | end
110 |
111 | def run_validators(value)
112 | if Validators.empty_value?(value)
113 | return
114 | end
115 |
116 | errors = []
117 |
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
135 |
136 | unless errors.empty?
137 | raise ValidationError.new(errors)
138 | end
139 | end
140 |
141 | def clean(value)
142 | value = to_object(value)
143 | validate(value)
144 | run_validators(value)
145 | value
146 | end
147 |
148 | # The data to be displayed when rendering for a bound form
149 | def bound_data(data, initial)
150 | data
151 | end
152 |
153 | # List of attributes to add on the widget. Override to add field specific attributes
154 | def widget_attrs(widget)
155 | {}
156 | end
157 |
158 | # Populates object.name if posible
159 | def populate_object(object, name, value)
160 | setter = :"#{name}="
161 |
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
180 |
181 | end
182 |
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)
190 |
191 | if @min_length
192 | validators << Validators::MinLengthValidator.new(@min_length)
193 | end
194 |
195 | if @max_length
196 | validators << Validators::MaxLengthValidator.new(@max_length)
197 | end
198 | end
199 |
200 | def to_object(value)
201 | if Validators.empty_value?(value)
202 | ''
203 | else
204 | value
205 | end
206 | end
207 |
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
216 | end
217 |
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)
223 |
224 | if @min_value
225 | validators << Validators::MinValueValidator.new(@min_value)
226 | end
227 |
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)
241 |
242 | if Validators.empty_value?(value)
243 | return nil
244 | end
245 |
246 | begin
247 | Integer(value.to_s)
248 | rescue ArgumentError
249 | raise ValidationError.new(error_messages[:invalid])
250 | end
251 | end
252 | end
253 |
254 | class FloatField < IntegerField
255 | def default_error_messages
256 | super.merge(invalid: 'Enter a number.')
257 | end
258 |
259 | def to_object(value)
260 | if Validators.empty_value?(value)
261 | return nil
262 | end
263 |
264 | begin
265 | Utils.make_float(value.to_s)
266 | rescue ArgumentError
267 | raise ValidationError.new(error_messages[:invalid])
268 | end
269 | end
270 | end
271 |
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
282 |
283 | super(options)
284 |
285 | if @min_value
286 | validators << Validators::MinValueValidator.new(@min_value)
287 | end
288 |
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
302 |
303 | def to_object(value)
304 | if Validators.empty_value?(value)
305 | return nil
306 | end
307 |
308 | begin
309 | Utils.make_float(value)
310 | BigDecimal.new(value)
311 | rescue ArgumentError
312 | raise ValidationError.new(error_messages[:invalid])
313 | end
314 | end
315 |
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
351 | end
352 |
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)
367 |
368 | @regex = regex
369 |
370 | validators << Validators::RegexValidator.new(regex: regex)
371 | end
372 | end
373 |
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
387 | end
388 |
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
396 |
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.')
403 | end
404 |
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
451 | end
452 |
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
460 |
461 | if !data && initial
462 | initial
463 | else
464 | super(data)
465 | end
466 | end
467 |
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
475 | end
476 |
477 | #class ImageField < FileField
478 | #end
479 |
480 | # URLField
481 |
482 | class BooleanField < Field
483 | def default_widget
484 | Widgets::CheckboxInput
485 | end
486 |
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
493 |
494 | value = super(value)
495 |
496 | if !value && required
497 | raise ValidationError.new(error_messages[:required])
498 | end
499 |
500 | value
501 | end
502 | end
503 |
504 | class NullBooleanField < BooleanField
505 | def default_widget
506 | Widgets::NullBooleanSelect
507 | end
508 |
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
516 |
517 | def validate(value)
518 | end
519 | end
520 |
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
532 |
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
536 |
537 | def default_widget
538 | Widgets::Select
539 | end
540 |
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
554 | end
555 | end
556 |
557 | def validate(value)
558 | super(value)
559 |
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|
569 | if v.is_a?(Array)
570 | # This is an optgroup, so look inside the group for options
571 | v.each do |k2, v2|
572 | return true if value == k2.to_s
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
577 | else
578 | return true if value == k.to_s
579 | end
580 | end
581 |
582 | false
583 | end
584 | end
585 |
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)
597 |
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)
614 | end
615 | end
616 |
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)
651 | end
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
691 |
692 | def default_validators
693 | [Validators::ValidateSlug]
694 | end
695 | end
696 | end
697 | end
698 |
--------------------------------------------------------------------------------
/lib/bureaucrat/forms.rb:
--------------------------------------------------------------------------------
1 | module Bureaucrat
2 | module Forms
3 |
4 | # Instances of +BoundField+ represent a fields with associated data.
5 | # +BoundField+s are used internally by the +Form+ class.
6 | class BoundField
7 | include Utils
8 |
9 | # Field label text
10 | attr_accessor :label, :form, :field, :name, :html_name, :html_initial_name, :help_text
11 |
12 | # Instantiates a new +BoundField+ associated to +form+'s field +field+
13 | # named +name+.
14 | def initialize(form, field, name)
15 | @form = form
16 | @field = field
17 | @name = name
18 | @html_name = form.add_prefix(name)
19 | @html_initial_name = form.add_initial_prefix(name)
20 | @label = @field.label || pretty_name(name)
21 | @help_text = @field.help_text || ''
22 | end
23 |
24 | # Renders the field.
25 | def to_s
26 | @field.show_hidden_initial ? as_widget + as_hidden(nil, true) : as_widget
27 | end
28 |
29 | # Errors for this field.
30 | def errors
31 | @form.errors.fetch(@name, @form.error_class.new)
32 | end
33 |
34 | # Renders this field with the option of using alternate widgets
35 | # and attributes.
36 | def as_widget(widget=nil, attrs=nil, only_initial=false)
37 | widget ||= @field.widget
38 | attrs ||= {}
39 | auto_id = self.auto_id
40 | attrs[:id] ||= auto_id if auto_id && !widget.attrs.key?(:id)
41 |
42 | if !@form.bound?
43 | data = @form.initial.fetch(@name, @field.initial)
44 | data = data.call if data.respond_to?(:call)
45 | else
46 | if @field.is_a?(Fields::FileField) && @data.nil?
47 | data = @form.initial.fetch(@name, @field.initial)
48 | else
49 | data = self.data
50 | end
51 | end
52 |
53 | name = only_initial ? @html_initial_name : @html_name
54 | widget.render(name.to_s, data, attrs)
55 | end
56 |
57 | # Renders this field as a text input.
58 | def as_text(attrs=nil, only_initial=false)
59 | as_widget(Widgets::TextInput.new, attrs, only_initial)
60 | end
61 |
62 | # Renders this field as a text area.
63 | def as_textarea(attrs=nil, only_initial=false)
64 | as_widget(Widgets::Textarea.new, attrs, only_initial)
65 | end
66 |
67 | # Renders this field as hidden.
68 | def as_hidden(attrs=nil, only_initial=false)
69 | as_widget(@field.hidden_widget, attrs, only_initial)
70 | end
71 |
72 | # The data associated to this field.
73 | def data
74 | @field.widget.value_from_formdata(@form.data, @html_name)
75 | end
76 |
77 | def value
78 | # Returns the value for this BoundField, using the initial value if
79 | # the form is not bound or the data otherwise.
80 |
81 | if form.bound?
82 | val = field.bound_data(data, form.initial.fetch(name, field.initial))
83 | else
84 | val = form.initial.fetch(name, field.initial)
85 | if val.respond_to?(:call)
86 | val = val.call
87 | end
88 | end
89 |
90 | field.prepare_value(val)
91 | end
92 |
93 | # Renders the label tag for this field.
94 | def label_tag(contents=nil, attrs=nil)
95 | contents ||= conditional_escape(@label)
96 | widget = @field.widget
97 | id_ = widget.attrs[:id] || self.auto_id
98 |
99 | if id_
100 | attrs = attrs ? flatatt(attrs) : ''
101 | contents = ""
102 | end
103 |
104 | mark_safe(contents)
105 | end
106 |
107 | def css_classes(extra_classes = nil)
108 | # Returns a string of space-separated CSS classes for this field.
109 |
110 | if extra_classes.respond_to?(:split)
111 | extra_classes = extra_classes.split
112 | end
113 |
114 | extra_classes = Set.new(extra_classes)
115 |
116 | if !errors.empty? && !Utils.blank_value?(form.error_css_class)
117 | extra_classes << form.error_css_class
118 | end
119 |
120 | if field.required && !Utils.blank_value?(form.required_css_class)
121 | extra_classes << form.required_css_class
122 | end
123 |
124 | extra_classes.to_a.join(' ')
125 | end
126 |
127 | # true if the widget for this field is of the hidden kind.
128 | def hidden?
129 | @field.widget.hidden?
130 | end
131 |
132 | # Generates the id for this field.
133 | def auto_id
134 | fauto_id = @form.auto_id
135 | fauto_id ? fauto_id % @html_name : ''
136 | end
137 | end
138 |
139 | # Base class for forms. Forms are a collection of fields with data that
140 | # knows how to render and validate itself.
141 | #
142 | # === Bound vs Unbound forms
143 | # A form is 'bound' if it was initialized with a set of data for its fields,
144 | # otherwise it is 'unbound'. Only bound forms can be validated. Unbound
145 | # forms always respond with false to +valid?+ and return an empty
146 | # list of errors.
147 |
148 | class Form
149 | include Utils
150 |
151 | # Fields associated to the form class
152 | def self.base_fields
153 | @base_fields ||= {}
154 | end
155 |
156 | # Declares a named field to be used on this form.
157 | def self.field(name, field_obj)
158 | base_fields[name] = field_obj
159 | end
160 |
161 | # Copy data to the child class
162 | def self.inherited(c)
163 | super(c)
164 | c.instance_variable_set(:@base_fields, base_fields.dup)
165 | end
166 |
167 | # Error object class for this form
168 | attr_accessor :error_class
169 | # Required class for this form
170 | attr_accessor :required_css_class
171 | # Required class for this form
172 | attr_accessor :error_css_class
173 | # Format string for field id generator
174 | attr_accessor :auto_id
175 | # Hash of {field_name => initial_value}
176 | attr_accessor :initial
177 | # Data associated to this form {field_name => value}
178 | attr_accessor :data
179 | # TODO: complete implementation
180 | attr_accessor :files
181 | # Validated and cleaned data
182 | attr_accessor :cleaned_data
183 | # Fields belonging to this form
184 | attr_accessor :fields
185 |
186 | # Checks if this form was initialized with data.
187 | def bound? ; @is_bound; end
188 |
189 | # Instantiates a new form bound to the passed data (or unbound if data is nil)
190 | #
191 | # +data+ is a hash of {field_name => value} for this form to be bound
192 | # (will be unbound if nil)
193 | #
194 | # Possible options are:
195 | # :prefix prefix that will be used for fields when rendered
196 | # :auto_id format string that will be used when generating
197 | # field ids (default: 'id_%s')
198 | # :initial hash of {field_name => default_value}
199 | # (doesn't make a form bound)
200 | # :error_class class used to represent errors (default: ErrorList)
201 | # :label_suffix suffix string that will be appended to labels' text
202 | # (default: ':')
203 | # :empty_permitted boolean value that specifies if this form is valid
204 | # when empty
205 |
206 | def initialize(data=nil, options={})
207 | @is_bound = !data.nil?
208 | @data = StringAccessHash.new(data || {})
209 | @files = options.fetch(:files, {})
210 | @auto_id = options.fetch(:auto_id, 'id_%s')
211 | @prefix = options[:prefix]
212 | @initial = StringAccessHash.new(options.fetch(:initial, {}))
213 | @error_class = options.fetch(:error_class, Fields::ErrorList)
214 | @label_suffix = options.fetch(:label_suffix, ':')
215 | @empty_permitted = options.fetch(:empty_permitted, false)
216 | @errors = nil
217 | @changed_data = nil
218 |
219 | @fields = self.class.base_fields.dup
220 | @fields.each { |key, value| @fields[key] = value.dup }
221 | end
222 |
223 | # Iterates over the fields
224 | def each
225 | @fields.each do |name, field|
226 | yield BoundField.new(self, field, name)
227 | end
228 | end
229 |
230 | # Access a named field
231 | def [](name)
232 | field = @fields[name] or return nil
233 | BoundField.new(self, field, name)
234 | end
235 |
236 | # Errors for this forms (runs validations)
237 | def errors
238 | full_clean if @errors.nil?
239 | @errors
240 | end
241 |
242 | # Perform validation and returns true if there are no errors
243 | def valid?
244 | @is_bound && (errors.nil? || errors.empty?)
245 | end
246 |
247 | # Generates a prefix for field named +field_name+
248 | def add_prefix(field_name)
249 | @prefix ? "#{@prefix}-#{field_name}" : field_name
250 | end
251 |
252 | # Generates an initial-prefix for field named +field_name+
253 | def add_initial_prefix(field_name)
254 | "initial-#{add_prefix(field_name)}"
255 | end
256 |
257 | # true if the form is valid when empty
258 | def empty_permitted?
259 | @empty_permitted
260 | end
261 |
262 | # Returns the list of errors that aren't associated to a specific field
263 | def non_field_errors
264 | errors.fetch(:__NON_FIELD_ERRORS, @error_class.new)
265 | end
266 |
267 | # Runs all the validations for this form. If the form is invalid
268 | # the list of errors is populated, if it is valid, cleaned_data is
269 | # populated
270 | def full_clean
271 | @errors = Fields::ErrorHash.new
272 |
273 | return unless bound?
274 |
275 | @cleaned_data = StringAccessHash.new
276 |
277 | return if empty_permitted? && !changed?
278 |
279 | @fields.each do |name, field|
280 | value = field.widget.
281 | value_from_formdata(@data, add_prefix(name))
282 |
283 | begin
284 | if field.is_a?(Fields::FileField)
285 | initial = @initial.fetch(name, field.initial)
286 | @cleaned_data[name] = field.clean(value, initial)
287 | else
288 | @cleaned_data[name] = field.clean(value)
289 | end
290 |
291 | clean_method = 'clean_%s' % name
292 | @cleaned_data[name] = send(clean_method) if respond_to?(clean_method)
293 | rescue ValidationError => e
294 | @errors[name] = e.messages
295 | @cleaned_data.delete(name)
296 | end
297 | end
298 |
299 | begin
300 | @cleaned_data = clean
301 | rescue ValidationError => e
302 | @errors[:__NON_FIELD_ERRORS] = e.messages
303 | end
304 | @cleaned_data = nil if @errors && !@errors.empty?
305 | end
306 |
307 | # Performs the last step of validations on the form, override in subclasses
308 | # to customize behaviour.
309 | def clean
310 | @cleaned_data
311 | end
312 |
313 | # true if the form has data that isn't equal to its initial data
314 | def changed?
315 | changed_data && !changed_data.empty?
316 | end
317 |
318 | # List names for fields that have changed data
319 | def changed_data
320 | if @changed_data.nil?
321 | @changed_data = []
322 |
323 | @fields.each do |name, field|
324 | prefixed_name = add_prefix(name)
325 | data_value = field.widget.
326 | value_from_formdata(@data, prefixed_name)
327 |
328 | if !field.show_hidden_initial
329 | initial_value = @initial.fetch(name, field.initial)
330 | else
331 | initial_prefixed_name = add_initial_prefix(name)
332 | hidden_widget = field.hidden_widget.new
333 | initial_value = hidden_widget.
334 | value_from_formdata(@data, initial_prefixed_name)
335 | end
336 |
337 | @changed_data << name if
338 | field.widget.has_changed?(initial_value, data_value)
339 | end
340 | end
341 |
342 | @changed_data
343 | end
344 |
345 | # true if this form contains fields that require the form to be
346 | # multipart
347 | def multipart?
348 | @fields.any? {|f| f.widget.multipart_form?}
349 | end
350 |
351 | # List of hidden fields.
352 | def hidden_fields
353 | @fields.select {|f| f.hidden?}
354 | end
355 |
356 | # List of visible fields
357 | def visible_fields
358 | @fields.select {|f| !f.hidden?}
359 | end
360 |
361 | # Attributes for labels, override in subclasses to customize behaviour
362 | def label_attributes(name, field)
363 | {}
364 | end
365 |
366 | # Populates the passed object's attributes with data from the fields
367 | def populate_object(object)
368 | @fields.each do |name, field|
369 | field.populate_object(object, name, @cleaned_data[name])
370 | end
371 | end
372 |
373 | private
374 |
375 | # Returns the value for the field name +field_name+ from the associated
376 | # data
377 | def raw_value(fieldname)
378 | field = @fields.fetch(fieldname)
379 | prefix = add_prefix(fieldname)
380 | field.widget.value_from_formdata(@data, prefix)
381 | end
382 |
383 | end
384 | end
385 | end
386 |
--------------------------------------------------------------------------------
/lib/bureaucrat/formsets.rb:
--------------------------------------------------------------------------------
1 | module Bureaucrat
2 | module Formsets
3 | TOTAL_FORM_COUNT = :'TOTAL_FORMS'
4 | INITIAL_FORM_COUNT = :'INITIAL_FORMS'
5 | MAX_NUM_FORM_COUNT = :'MAX_NUM_FORMS'
6 | ORDERING_FIELD_NAME = :'ORDER'
7 | DELETION_FIELD_NAME = :'DELETE'
8 |
9 | class ManagementForm < Forms::Form
10 | include Fields
11 | include Widgets
12 |
13 | field TOTAL_FORM_COUNT, IntegerField.new(widget: HiddenInput)
14 | field INITIAL_FORM_COUNT, IntegerField.new(widget: HiddenInput)
15 | field MAX_NUM_FORM_COUNT, IntegerField.new(widget: HiddenInput,
16 | required: false)
17 | end
18 |
19 | class BaseFormSet
20 | include Utils
21 | include Fields
22 | include Forms
23 |
24 | def self.default_prefix
25 | 'form'
26 | end
27 |
28 | # All forms in this formset
29 | attr_accessor :forms
30 | # Form class used for this formset forms
31 | attr_accessor :form
32 | # Amount of extra (empty) forms on this formset
33 | attr_accessor :extra
34 | # Boolean that determines is this formset supports reordering
35 | attr_accessor :can_order
36 | # Boolean that determines is this formset supports deletion
37 | attr_accessor :can_delete
38 | # Maximum count of forms allowed on this formset
39 | attr_accessor :max_num
40 | # Prefix that will be used when rendering form fields
41 | attr_accessor :prefix
42 | # Error object class for this formset
43 | attr_accessor :error_class
44 | # Initial data for the forms in this formset. Array of Hashes of {field_name => initial_value}
45 | attr_accessor :initial
46 | # Data associated to this formset. Hash of {prefixed_field_name => value}
47 | attr_accessor :data
48 | # Format string for field id generator
49 | attr_accessor :auto_id
50 |
51 | def initialize(data=nil, options={})
52 | set_defaults
53 | @is_bound = !data.nil?
54 | @prefix = options.fetch(:prefix, self.class.default_prefix)
55 | @auto_id = options.fetch(:auto_id, 'id_%s')
56 | @data = data || {}
57 | @initial = options[:initial]
58 | @error_class = options.fetch(:error_class, ErrorList)
59 | @errors = nil
60 | @non_form_errors = nil
61 |
62 | construct_forms
63 | end
64 |
65 | def each(&block)
66 | forms.each(&block)
67 | end
68 |
69 | def [](index)
70 | forms[index]
71 | end
72 |
73 | def length
74 | forms.length
75 | end
76 |
77 | def management_form
78 | if @is_bound
79 | form = ManagementForm.new(@data, auto_id: @auto_id,
80 | prefix: @prefix)
81 | unless form.valid?
82 | msg = 'ManagementForm data is missing or has been tampered with'
83 | raise ValidationError.new(msg)
84 | end
85 | else
86 | form = ManagementForm.new(nil, auto_id: @auto_id,
87 | prefix: @prefix,
88 | initial: {
89 | TOTAL_FORM_COUNT => total_form_count,
90 | INITIAL_FORM_COUNT => initial_form_count,
91 | MAX_NUM_FORM_COUNT => self.max_num
92 | })
93 | end
94 | form
95 | end
96 |
97 | def total_form_count
98 | if @is_bound
99 | management_form.cleaned_data[TOTAL_FORM_COUNT]
100 | else
101 | initial_forms = initial_form_count
102 | total_forms = initial_form_count + self.extra
103 |
104 | # Allow all existing related objects/inlines to be displayed,
105 | # but don't allow extra beyond max_num.
106 | if self.max_num > 0 && initial_forms > self.max_num
107 | initial_forms
108 | elsif self.max_num > 0 && total_forms > self.max_num
109 | max_num
110 | else
111 | total_forms
112 | end
113 | end
114 | end
115 |
116 | def initial_form_count
117 | if @is_bound
118 | management_form.cleaned_data[INITIAL_FORM_COUNT]
119 | else
120 | n = @initial ? @initial.length : 0
121 |
122 | (self.max_num > 0 && n > self.max_num) ? self.max_num : n
123 | end
124 | end
125 |
126 | def construct_forms
127 | @forms = (0...total_form_count).map { |i| construct_form(i) }
128 | end
129 |
130 | def construct_form(i, options={})
131 | defaults = {auto_id: @auto_id, prefix: add_prefix(i)}
132 | defaults[:initial] = @initial[i] if @initial && @initial[i]
133 |
134 | # Allow extra forms to be empty.
135 | defaults[:empty_permitted] = true if i >= initial_form_count
136 | defaults.merge!(options)
137 | form_data = @is_bound ? @data : nil
138 | form = self.form.new(form_data, defaults)
139 | add_fields(form, i)
140 | form
141 | end
142 |
143 | def initial_forms
144 | @forms[0, initial_form_count]
145 | end
146 |
147 | def extra_forms
148 | n = initial_form_count
149 | @forms[n, @forms.length - n]
150 | end
151 |
152 | # Maybe this should just go away?
153 | def cleaned_data
154 | unless valid?
155 | raise NoMethodError.new("'#{self.class.name}' object has no method 'cleaned_data'")
156 | end
157 | @forms.collect(&:cleaned_data)
158 | end
159 |
160 | def deleted_forms
161 | unless valid? && self.can_delete
162 | raise NoMethodError.new("'#{self.class.name}' object has no method 'deleted_forms'")
163 | end
164 |
165 | if @deleted_form_indexes.nil?
166 | @deleted_form_indexes = (0...total_form_count).select do |i|
167 | form = @forms[i]
168 |
169 | if i >= initial_form_count && !form.changed?
170 | false
171 | else
172 | should_delete_form?(form)
173 | end
174 | end
175 | end
176 |
177 | @deleted_form_indexes.map {|i| @forms[i]}
178 | end
179 |
180 | def ordered_forms
181 | unless valid? && self.can_order
182 | raise NoMethodError.new("'#{self.class.name}' object has no method 'ordered_forms'")
183 | end
184 |
185 | if @ordering.nil?
186 | @ordering = (0...total_form_count).map do |i|
187 | form = @forms[i]
188 | next if i >= initial_form_count && !form.changed?
189 | next if self.can_delete && should_delete_form?(form)
190 | [i, form.cleaned_data[ORDERING_FIELD_NAME]]
191 | end.compact
192 | @ordering.sort! do |a, b|
193 | if x[1].nil? then 1
194 | elsif y[1].nil? then -1
195 | else x[1] - y[1]
196 | end
197 | end
198 | end
199 |
200 | @ordering.map {|i| @forms[i.first]}
201 | end
202 |
203 | def non_form_errors
204 | @non_form_errors || @error_class.new
205 | end
206 |
207 | def errors
208 | full_clean if @errors.nil?
209 | @errors
210 | end
211 |
212 | def should_delete_form?(form)
213 | field = form.fields[DELETION_FIELD_NAME]
214 | raw_value = form.send(:raw_value, DELETION_FIELD_NAME)
215 | field.clean(raw_value)
216 | end
217 |
218 | def valid?
219 | return false unless @is_bound
220 |
221 | forms_valid = true
222 |
223 | (0...total_form_count).each do |i|
224 | form = @forms[i]
225 | next if self.can_delete && should_delete_form?(form)
226 |
227 | forms_valid = false unless errors[i].empty?
228 | end
229 |
230 | forms_valid && non_form_errors.empty?
231 | end
232 |
233 | def full_clean
234 | if @is_bound
235 | @errors = @forms.collect(&:errors)
236 |
237 | begin
238 | self.clean
239 | rescue ValidationError => e
240 | @non_form_errors = @error_class.new(e.messages)
241 | end
242 | else
243 | @errors = []
244 | end
245 | end
246 |
247 | def clean
248 | end
249 |
250 | def add_fields(form, index)
251 | if can_order
252 | attrs = {label: 'Order', required: false}
253 | attrs[:initial] = index + 1 if index && index < initial_form_count
254 | form.fields[ORDERING_FIELD_NAME] = IntegerField.new(attrs)
255 | end
256 | if can_delete
257 | field = BooleanField.new(label: 'Delete', required: false)
258 | form.fields[DELETION_FIELD_NAME] = field
259 | end
260 | end
261 |
262 | def add_prefix(index)
263 | '%s-%s' % [@prefix, index]
264 | end
265 |
266 | def multipart?
267 | @forms && @forms.first.multipart?
268 | end
269 | end
270 |
271 | module_function
272 |
273 | def make_formset_class(form, options={})
274 | formset = options.fetch(:formset, BaseFormSet)
275 |
276 | Class.new(formset) do
277 | define_method :set_defaults do
278 | @form = form
279 | @extra = options.fetch(:extra, 1)
280 | @can_order = options.fetch(:can_order, false)
281 | @can_delete = options.fetch(:can_delete, false)
282 | @max_num = options.fetch(:max_num, 0)
283 | end
284 | private :set_defaults
285 | end
286 | end
287 |
288 | def all_valid?(formsets)
289 | valid = true
290 | formsets.each do |formset|
291 | valid = false unless formset.valid?
292 | end
293 | valid
294 | end
295 |
296 | end
297 | end
298 |
--------------------------------------------------------------------------------
/lib/bureaucrat/quickfields.rb:
--------------------------------------------------------------------------------
1 | module Bureaucrat
2 | # Shortcuts for declaring form fields
3 | module Quickfields
4 | include Fields
5 |
6 | # Hide field named +name+
7 | def hide(name)
8 | base_fields[name] = base_fields[name].dup
9 | base_fields[name].widget = Widgets::HiddenInput.new
10 | end
11 |
12 | # Delete field named +name+
13 | def delete(name)
14 | base_fields.delete name
15 | end
16 |
17 | # Declare a +CharField+ with text input widget
18 | def string(name, options = {})
19 | field name, CharField.new(options)
20 | end
21 |
22 | # Declare a +CharField+ with text area widget
23 | def text(name, options = {})
24 | field name, CharField.new(options.merge(widget: Widgets::Textarea.new))
25 | end
26 |
27 | # Declare a +CharField+ with password widget
28 | def password(name, options = {})
29 | field name, CharField.new(options.merge(widget: Widgets::PasswordInput.new))
30 | end
31 |
32 | # Declare an +IntegerField+
33 | def integer(name, options = {})
34 | field name, IntegerField.new(options)
35 | end
36 |
37 | # Declare a +BigDecimalField+
38 | def decimal(name, options = {})
39 | field name, BigDecimalField.new(options)
40 | end
41 |
42 | # Declare a +RegexField+
43 | def regex(name, regexp, options = {})
44 | field name, RegexField.new(regexp, options)
45 | end
46 |
47 | # Declare an +EmailField+
48 | def email(name, options = {})
49 | field name, EmailField.new(options)
50 | end
51 |
52 | # Declare a +FileField+
53 | def file(name, options = {})
54 | field name, FileField.new(options)
55 | end
56 |
57 | # Declare a +BooleanField+
58 | def boolean(name, options = {})
59 | field name, BooleanField.new(options)
60 | end
61 |
62 | # Declare a +NullBooleanField+
63 | def null_boolean(name, options = {})
64 | field name, NullBooleanField.new(options)
65 | end
66 |
67 | # Declare a +ChoiceField+ with +choices+
68 | def choice(name, choices = [], options = {})
69 | field name, ChoiceField.new(choices, options)
70 | end
71 |
72 | # Declare a +TypedChoiceField+ with +choices+
73 | def typed_choice(name, choices = [], options = {})
74 | field name, TypedChoiceField.new(choices, options)
75 | end
76 |
77 | # Declare a +MultipleChoiceField+ with +choices+
78 | def multiple_choice(name, choices = [], options = {})
79 | field name, MultipleChoiceField.new(choices, options)
80 | end
81 |
82 | # Declare a +ChoiceField+ using the +RadioSelect+ widget
83 | def radio_choice(name, choices = [], options = {})
84 | field name, ChoiceField.new(choices, options.merge(widget: Widgets::RadioSelect.new))
85 | end
86 |
87 | # Declare a +MultipleChoiceField+ with the +CheckboxSelectMultiple+ widget
88 | def checkbox_multiple_choice(name, choices = [], options = {})
89 | field name, MultipleChoiceField.new(choices, options.merge(widget: Widgets::CheckboxSelectMultiple.new))
90 | end
91 | end
92 | end
93 |
--------------------------------------------------------------------------------
/lib/bureaucrat/temporary_uploaded_file.rb:
--------------------------------------------------------------------------------
1 | module Bureaucrat
2 | class TemporaryUploadedFile
3 | attr_accessor :filename, :content_type, :name, :tempfile, :head, :size
4 |
5 | def initialize(data)
6 | @filename = data[:filename]
7 | @content_type = data[:content_type]
8 | @name = data[:name]
9 | @tempfile = data[:tempfile]
10 | @size = @tempfile.size
11 | @head = data[:head]
12 | end
13 | end
14 | end
15 |
--------------------------------------------------------------------------------
/lib/bureaucrat/utils.rb:
--------------------------------------------------------------------------------
1 | module Bureaucrat
2 | module Utils
3 | extend self
4 |
5 | module SafeData
6 | end
7 |
8 | class SafeString < String
9 | include SafeData
10 |
11 | def +(rhs)
12 | rhs.is_a?(SafeString) ? SafeString.new(super(rhs)) : super(rhs)
13 | end
14 | end
15 |
16 | class StringAccessHash < Hash
17 | def initialize(other = {})
18 | super()
19 | update(other)
20 | end
21 |
22 | def []=(key, value)
23 | super(key.to_s, value)
24 | end
25 |
26 | def [](key)
27 | super(key.to_s)
28 | end
29 |
30 | def fetch(key, *args)
31 | super(key.to_s, *args)
32 | end
33 |
34 | def include?(key)
35 | super(key.to_s)
36 | end
37 |
38 | def update(other)
39 | other.each_pair{|k, v| self[k] = v}
40 | self
41 | end
42 |
43 | def merge(other)
44 | dup.update(other)
45 | end
46 |
47 | def delete(key)
48 | super(key.to_s)
49 | end
50 | end
51 |
52 | def blank_value?(value)
53 | !value || value == ''
54 | end
55 |
56 | def mark_safe(s)
57 | s.is_a?(SafeData) ? s : SafeString.new(s.to_s)
58 | end
59 |
60 | ESCAPES = {
61 | '&' => '&',
62 | '<' => '<',
63 | '>' => '>',
64 | '"' => '"',
65 | "'" => '''
66 | }
67 | def escape(html)
68 | mark_safe(html.gsub(/[&<>"']/) {|match| ESCAPES[match]})
69 | end
70 |
71 | def conditional_escape(html)
72 | html.is_a?(SafeData) ? html : escape(html)
73 | end
74 |
75 | def flatatt(attrs)
76 | attrs.map {|k, v| " #{k}=\"#{conditional_escape(v)}\""}.join('')
77 | end
78 |
79 | def format_string(string, values)
80 | output = string.dup
81 | values.each_pair do |variable, value|
82 | output.gsub!(/%\(#{variable}\)s/, value.to_s)
83 | end
84 | output
85 | end
86 |
87 | def pretty_name(name)
88 | name.to_s.capitalize.gsub(/_/, ' ')
89 | end
90 |
91 | def make_float(value)
92 | value += '0' if value.is_a?(String) && value != '.' && value[-1,1] == '.'
93 | Float(value)
94 | end
95 |
96 | def make_bool(value)
97 | !(value.respond_to?(:empty?) ? value.empty? : [0, nil, false].include?(value))
98 | end
99 |
100 | end
101 | end
102 |
--------------------------------------------------------------------------------
/lib/bureaucrat/validators.rb:
--------------------------------------------------------------------------------
1 | module Bureaucrat
2 | module Validators
3 | def empty_value?(value)
4 | value.nil? || value == '' || value == [] || value == {}
5 | end
6 | module_function :empty_value?
7 |
8 | class RegexValidator
9 | attr_accessor :regex, :message, :code
10 |
11 | def initialize(options = {})
12 | @regex = Regexp.new(options.fetch(:regex, ''))
13 | @message = options.fetch(:message, 'Enter a valid value.')
14 | @code = options.fetch(:code, :invalid)
15 | end
16 |
17 | # Validates that the input validates the regular expression
18 | def call(value)
19 | if regex !~ value
20 | raise ValidationError.new(@message, code, regex: regex)
21 | end
22 | end
23 | end
24 |
25 | ValidateInteger = lambda do |value|
26 | begin
27 | Integer(value)
28 | rescue ArgumentError
29 | raise ValidationError.new('')
30 | end
31 | end
32 |
33 | # Original from Django's EmailField:
34 | # email_re = re.compile(
35 | # r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom
36 | # r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"' # quoted-string
37 | # r')@(?:[A-Z0-9]+(?:-*[A-Z0-9]+)*\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain
38 | EMAIL_RE = /
39 | (^[-!#\$%&'*+\/=?^_`{}|~0-9A-Z]+(\.[-!#\$%&'*+\/=?^_`{}|~0-9A-Z]+)*
40 | |^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-\011\013\014\016-\177])*"
41 | )@(?:[A-Z0-9]+(?:-*[A-Z0-9]+)*\.)+[A-Z]{2,6}$
42 | /xi
43 |
44 | ValidateEmail =
45 | RegexValidator.new(regex: EMAIL_RE,
46 | message: 'Enter a valid e-mail address.')
47 |
48 | SLUG_RE = /^[-\w]+$/
49 |
50 | ValidateSlug =
51 | RegexValidator.new(regex: SLUG_RE,
52 | message: "Enter a valid 'slug' consisting of letters, numbers, underscores or hyphens.")
53 |
54 | IPV4_RE = /^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$/
55 |
56 | IPV4Validator =
57 | RegexValidator.new(regex: IPV4_RE,
58 | message: 'Enter a valid IPv4 address.')
59 |
60 | COMMA_SEPARATED_INT_LIST_RE = /^[\d,]+$/
61 |
62 | ValidateCommaSeparatedIntegerList =
63 | RegexValidator.new(regex: COMMA_SEPARATED_INT_LIST_RE,
64 | message: 'Enter only digits separated by commas.',
65 | code: :invalid)
66 |
67 | class BaseValidator
68 | def initialize(limit_value)
69 | @limit_value = limit_value
70 | end
71 |
72 | def message
73 | 'Ensure this value is %(limit_value)s (it is %(show_value)s).'
74 | end
75 |
76 | def code
77 | :limit_value
78 | end
79 |
80 | def compare(a, b)
81 | a.object_id != b.object_id
82 | end
83 |
84 | def clean(x)
85 | x
86 | end
87 |
88 | def call(value)
89 | cleaned = clean(value)
90 | params = { limit_value: @limit_value, show_value: cleaned }
91 |
92 | if compare(cleaned, @limit_value)
93 | msg = Utils.format_string(message, params)
94 | raise ValidationError.new(msg, code, params)
95 | end
96 | end
97 | end
98 |
99 | class MaxValueValidator < BaseValidator
100 | def message
101 | 'Ensure this value is less than or equal to %(limit_value)s.'
102 | end
103 |
104 | def code
105 | :max_value
106 | end
107 |
108 | def compare(a, b)
109 | a > b
110 | end
111 | end
112 |
113 | class MinValueValidator < BaseValidator
114 | def message
115 | 'Ensure this value is greater than or equal to %(limit_value)s.'
116 | end
117 |
118 | def code
119 | :min_value
120 | end
121 |
122 | def compare(a, b)
123 | a < b
124 | end
125 | end
126 |
127 | class MinLengthValidator < BaseValidator
128 | def message
129 | 'Ensure this value has at least %(limit_value)d characters (it has %(show_value)d).'
130 | end
131 |
132 | def code
133 | :min_length
134 | end
135 |
136 | def compare(a, b)
137 | a < b
138 | end
139 |
140 | def clean(x)
141 | x.length
142 | end
143 | end
144 |
145 | class MaxLengthValidator < BaseValidator
146 | def message
147 | 'Ensure this value has at most %(limit_value)d characters (it has %(show_value)d).'
148 | end
149 |
150 | def code
151 | :max_length
152 | end
153 |
154 | def compare(a, b)
155 | a > b
156 | end
157 |
158 | def clean(x)
159 | x.length
160 | end
161 | end
162 | end
163 | end
164 |
--------------------------------------------------------------------------------
/lib/bureaucrat/widgets.rb:
--------------------------------------------------------------------------------
1 | require 'uri'
2 |
3 | module Bureaucrat
4 | module Widgets
5 | # Base class for widgets
6 | class Widget
7 | include Utils
8 |
9 | attr_accessor :is_required
10 | attr_reader :attrs
11 |
12 | def initialize(attrs = nil)
13 | @attrs = attrs.nil? ? {} : attrs.dup
14 | end
15 |
16 | def initialize_copy(original)
17 | super(original)
18 | @attrs = original.attrs.dup
19 | end
20 |
21 | def render(name, value, attrs = nil)
22 | raise NotImplementedError
23 | end
24 |
25 | def build_attrs(extra_attrs = nil, options = {})
26 | attrs = @attrs.merge(options)
27 | attrs.update(extra_attrs) if extra_attrs
28 | attrs
29 | end
30 |
31 | def value_from_formdata(data, name)
32 | data[name]
33 | end
34 |
35 | def self.id_for_label(id_)
36 | id_
37 | end
38 |
39 | def has_changed?(initial, data)
40 | data_value = data || ''
41 | initial_value = initial || ''
42 | initial_value != data_value
43 | end
44 |
45 | def needs_multipart?
46 | false
47 | end
48 |
49 | def hidden?
50 | false
51 | end
52 | end
53 |
54 | # Base class for input widgets
55 | class Input < Widget
56 | def render(name, value, attrs=nil)
57 | value ||= ''
58 | final_attrs = build_attrs(attrs,
59 | type: input_type.to_s,
60 | name: name.to_s)
61 | final_attrs[:value] = value.to_s unless value == ''
62 | mark_safe("")
63 | end
64 |
65 | def input_type
66 | nil
67 | end
68 | end
69 |
70 | # Class for text inputs
71 | class TextInput < Input
72 | def input_type
73 | 'text'
74 | end
75 | end
76 |
77 | # Class for password inputs
78 | class PasswordInput < Input
79 | def initialize(attrs = nil, render_value = false)
80 | super(attrs)
81 | @render_value = render_value
82 | end
83 |
84 | def input_type
85 | 'password'
86 | end
87 |
88 | def render(name, value, attrs=nil)
89 | value = nil unless @render_value
90 | super(name, value, attrs)
91 | end
92 | end
93 |
94 | # Class for hidden inputs
95 | class HiddenInput < Input
96 | def input_type
97 | 'hidden'
98 | end
99 |
100 | def hidden?
101 | true
102 | end
103 | end
104 |
105 | class MultipleHiddenInput < HiddenInput
106 | # Used by choice fields
107 | attr_accessor :choices
108 |
109 | def initialize(attrs=nil, choices=[])
110 | super(attrs)
111 | # choices can be any enumerable
112 | @choices = choices
113 | end
114 |
115 | def render(name, value, attrs=nil, choices=[])
116 | value ||= []
117 | final_attrs = build_attrs(attrs, type: input_type.to_s,
118 | name: "#{name}[]")
119 |
120 | id = final_attrs[:id]
121 | inputs = []
122 |
123 | value.each.with_index do |v, i|
124 | input_attrs = final_attrs.merge(value: v.to_s)
125 |
126 | if id
127 | input_attrs[:id] = "#{id}_#{i}"
128 | end
129 |
130 | inputs << ""
131 | end
132 |
133 | mark_safe(inputs.join("\n"))
134 | end
135 | end
136 |
137 | class FileInput < Input
138 | def render(name, value, attrs=nil)
139 | super(name, nil, attrs)
140 | end
141 |
142 | def value_from_formdata(data, name)
143 | data[name] && TemporaryUploadedFile.new(data[name])
144 | end
145 |
146 | def has_changed?(initial, data)
147 | data.nil?
148 | end
149 |
150 | def input_type
151 | 'file'
152 | end
153 |
154 | def needs_multipart?
155 | true
156 | end
157 | end
158 |
159 | class ClearableFileInput < FileInput
160 | FILE_INPUT_CONTRADICTION = Object.new
161 |
162 | def initial_text
163 | 'Currently'
164 | end
165 |
166 | def input_text
167 | 'Change'
168 | end
169 |
170 | def clear_checkbox_label
171 | 'Clear'
172 | end
173 |
174 | def template_with_initial
175 | '%(initial_text)s: %(initial)s %(clear_template)s
%(input_text)s: %(input)s'
176 | end
177 |
178 | def template_with_clear
179 | '%(clear)s '
180 | end
181 |
182 | def clear_checkbox_name(name)
183 | "#{name}-clear"
184 | end
185 |
186 | def clear_checkbox_id(checkbox_name)
187 | "#{checkbox_name}_id"
188 | end
189 |
190 | def render(name, value, attrs = nil)
191 | substitutions = {
192 | initial_text: initial_text,
193 | input_text: input_text,
194 | clear_template: '',
195 | clear_checkbox_label: clear_checkbox_label
196 | }
197 | template = '%(input)s'
198 | substitutions[:input] = super(name, value, attrs)
199 |
200 | if value && value.respond_to?(:url) && value.url
201 | template = template_with_initial
202 | substitutions[:initial] = '%s' % [escape(value.url),
203 | escape(value.to_s)]
204 | unless is_required
205 | checkbox_name = clear_checkbox_name(name)
206 | checkbox_id = clear_checkbox_id(checkbox_name)
207 | substitutions[:clear_checkbox_name] = conditional_escape(checkbox_name)
208 | substitutions[:clear_checkbox_id] = conditional_escape(checkbox_id)
209 | substitutions[:clear] = CheckboxInput.new.
210 | render(checkbox_name, false, {id: checkbox_id})
211 | substitutions[:clear_template] =
212 | Utils.format_string(template_with_clear, substitutions)
213 | end
214 | end
215 |
216 | mark_safe(Utils.format_string(template, substitutions))
217 | end
218 |
219 | def value_from_formdata(data, name)
220 | upload = super(data, name)
221 | checked = CheckboxInput.new.
222 | value_from_formdata(data, clear_checkbox_name(name))
223 |
224 | if !is_required && checked
225 | if upload
226 | # If the user contradicts themselves (uploads a new file AND
227 | # checks the "clear" checkbox), we return a unique marker
228 | # object that FileField will turn into a ValidationError.
229 | FILE_INPUT_CONTRADICTION
230 | else
231 | # False signals to clear any existing value, as opposed to just None
232 | false
233 | end
234 | else
235 | upload
236 | end
237 | end
238 | end
239 |
240 | class Textarea < Widget
241 | def initialize(attrs=nil)
242 | # The 'rows' and 'cols' attributes are required for HTML correctness.
243 | default_attrs = {cols: '40', rows: '10'}
244 | default_attrs.merge!(attrs) if attrs
245 |
246 | super(default_attrs)
247 | end
248 |
249 | def render(name, value, attrs=nil)
250 | value ||= ''
251 | final_attrs = build_attrs(attrs, name: name)
252 | mark_safe("")
253 | end
254 | end
255 |
256 | # DateInput
257 | # DateTimeInput
258 | # TimeInput
259 |
260 | class CheckboxInput < Widget
261 | def initialize(attrs=nil, check_test=nil)
262 | super(attrs)
263 | @check_test = check_test || lambda {|v| make_bool(v)}
264 | end
265 |
266 | def render(name, value, attrs=nil)
267 | final_attrs = build_attrs(attrs, type: 'checkbox', name: name.to_s)
268 |
269 | # FIXME: this is horrible, shouldn't just rescue everything
270 | result = @check_test.call(value) rescue false
271 |
272 | if result
273 | final_attrs[:checked] = 'checked'
274 | end
275 |
276 | unless ['', true, false, nil].include?(value)
277 | final_attrs[:value] = value.to_s
278 | end
279 |
280 | mark_safe("")
281 | end
282 |
283 | def value_from_formdata(data, name)
284 | if data.include?(name)
285 | value = data[name]
286 |
287 | if value.is_a?(String)
288 | case value.downcase
289 | when 'true' then true
290 | when 'false' then false
291 | else value
292 | end
293 | else
294 | value
295 | end
296 | else
297 | false
298 | end
299 | end
300 |
301 | def has_changed(initial, data)
302 | make_bool(initial) != make_bool(data)
303 | end
304 | end
305 |
306 | class Select < Widget
307 | attr_accessor :choices
308 |
309 | def initialize(attrs=nil, choices=[])
310 | super(attrs)
311 | @choices = choices.collect
312 | end
313 |
314 | def render(name, value, attrs=nil, choices=[])
315 | value = '' if value.nil?
316 | final_attrs = build_attrs(attrs, name: name)
317 | output = ["'
321 | mark_safe(output.join("\n"))
322 | end
323 |
324 | def render_options(choices, selected_choices)
325 | selected_choices = selected_choices.map(&:to_s).uniq
326 | output = []
327 | (@choices.to_a + choices.to_a).each do |option_value, option_label|
328 | option_label ||= option_value
329 | if option_label.is_a?(Array)
330 | output << ''
336 | else
337 | output << render_option(option_value, option_label,
338 | selected_choices)
339 | end
340 | end
341 | output.join("\n")
342 | end
343 |
344 | def render_option(option_attributes, option_label, selected_choices)
345 | unless option_attributes.is_a?(Hash)
346 | option_attributes = { value: option_attributes.to_s }
347 | end
348 |
349 | if selected_choices.include?(option_attributes[:value])
350 | option_attributes[:selected] = "selected"
351 | end
352 |
353 | attributes = []
354 |
355 | option_attributes.each_pair do |attr_name, attr_value|
356 | attributes << %Q[#{attr_name.to_s}="#{escape(attr_value.to_s)}"]
357 | end
358 |
359 | ""
360 | end
361 | end
362 |
363 | class NullBooleanSelect < Select
364 | def initialize(attrs=nil)
365 | choices = [['1', 'Unknown'], ['2', 'Yes'], ['3', 'No']]
366 | super(attrs, choices)
367 | end
368 |
369 | def render(name, value, attrs=nil, choices=[])
370 | value = case value
371 | when true, '2' then '2'
372 | when false, '3' then '3'
373 | else '1'
374 | end
375 | super(name, value, attrs, choices)
376 | end
377 |
378 | def value_from_formdata(data, name)
379 | case data[name]
380 | when '2', true, 'true' then true
381 | when '3', false, 'false' then false
382 | else nil
383 | end
384 | end
385 |
386 | def has_changed?(initial, data)
387 | unless initial.nil?
388 | initial = make_bool(initial)
389 | end
390 |
391 | unless data.nil?
392 | data = make_bool(data)
393 | end
394 |
395 | initial != data
396 | end
397 | end
398 |
399 | class SelectMultiple < Select
400 | def render(name, value, attrs=nil, choices=[])
401 | value = [] if value.nil?
402 | final_attrs = build_attrs(attrs, name: "#{name}[]")
403 | output = ["'
407 | mark_safe(output.join("\n"))
408 | end
409 |
410 | def has_changed?(initial, data)
411 | initial = [] if initial.nil?
412 | data = [] if data.nil?
413 |
414 | if initial.length != data.length
415 | return true
416 | end
417 |
418 | Set.new(initial.map(&:to_s)) != Set.new(data.map(&:to_s))
419 | end
420 | end
421 |
422 | class RadioInput
423 | include Utils
424 |
425 | def initialize(name, value, attrs, choice, index)
426 | @name = name
427 | @value = value
428 | @attrs = attrs
429 | @choice_value = choice[0].to_s
430 | @choice_label = choice[1].to_s
431 | @index = index
432 | end
433 |
434 | def to_s
435 | label_for = @attrs.include?(:id) ? " for=\"#{@attrs[:id]}_#{@index}\"" : ''
436 | choice_label = conditional_escape(@choice_label.to_s)
437 | mark_safe("")
438 | end
439 |
440 | def checked?
441 | @value == @choice_value
442 | end
443 |
444 | def tag
445 | @attrs[:id] = "#{@attrs[:id]}_#{@index}" if @attrs.include?(:id)
446 | final_attrs = @attrs.merge(type: 'radio', name: @name,
447 | value: @choice_value)
448 | final_attrs[:checked] = 'checked' if checked?
449 | mark_safe("")
450 | end
451 | end
452 |
453 | class RadioFieldRenderer
454 | include Utils
455 |
456 | def initialize(name, value, attrs, choices)
457 | @name = name
458 | @value = value
459 | @attrs = attrs
460 | @choices = choices
461 | end
462 |
463 | def each
464 | @choices.each_with_index do |choice, i|
465 | yield RadioInput.new(@name, @value, @attrs.dup, choice, i)
466 | end
467 | end
468 |
469 | def [](idx)
470 | choice = @choices[idx]
471 | RadioInput.new(@name, @value, @attrs.dup, choice, idx)
472 | end
473 |
474 | def to_s
475 | render
476 | end
477 |
478 | def render
479 | list = []
480 | each {|radio| list << "#{radio}"}
481 | mark_safe("")
482 | end
483 | end
484 |
485 | class RadioSelect < Select
486 | def self.id_for_label(id_)
487 | id_.empty? ? id_ : id_ + '_0'
488 | end
489 |
490 | def renderer
491 | RadioFieldRenderer
492 | end
493 |
494 | def initialize(*args)
495 | options = args.last.is_a?(Hash) ? args.last : {}
496 | @renderer = options.fetch(:renderer, renderer)
497 | super
498 | end
499 |
500 | def get_renderer(name, value, attrs=nil, choices=[])
501 | value ||= ''
502 | str_value = value.to_s
503 | final_attrs = build_attrs(attrs)
504 | choices = @choices.to_a + choices.to_a
505 | @renderer.new(name, str_value, final_attrs, choices)
506 | end
507 |
508 | def render(name, value, attrs=nil, choices=[])
509 | get_renderer(name, value, attrs, choices).render
510 | end
511 | end
512 |
513 | class CheckboxSelectMultiple < SelectMultiple
514 | def self.id_for_label(id_)
515 | id_.empty? ? id_ : id_ + '_0'
516 | end
517 |
518 | def render(name, values, attrs=nil, choices=[])
519 | values ||= []
520 | multi_name = "#{name}[]"
521 | has_id = attrs && attrs.include?(:id)
522 | final_attrs = build_attrs(attrs, name: multi_name)
523 | output = ['']
524 | str_values = {}
525 | values.each {|val| str_values[(val.to_s)] = true}
526 |
527 | (@choices.to_a + choices.to_a).each_with_index do |opt_pair, i|
528 | opt_val, opt_label = opt_pair
529 | if has_id
530 | final_attrs[:id] = "#{attrs[:id]}_#{i}"
531 | label_for = " for=\"#{final_attrs[:id]}\""
532 | else
533 | label_for = ''
534 | end
535 |
536 | check_test = lambda{|value| str_values[value]}
537 | cb = CheckboxInput.new(final_attrs, check_test)
538 | opt_val = opt_val.to_s
539 | rendered_cb = cb.render(multi_name, opt_val)
540 | opt_label = conditional_escape(opt_label.to_s)
541 | output << ""
542 | end
543 | output << '
'
544 | mark_safe(output.join("\n"))
545 | end
546 | end
547 |
548 | # TODO: MultiWidget < Widget
549 | # TODO: SplitDateTimeWidget < MultiWidget
550 | # TODO: SplitHiddenDateTimeWidget < SplitDateTimeWidget
551 |
552 | end
553 | end
554 |
--------------------------------------------------------------------------------
/test/fields_test.rb:
--------------------------------------------------------------------------------
1 | require_relative 'test_helper'
2 |
3 | module FieldTests
4 | class Test_with_empty_options < BureaucratTestCase
5 | def setup
6 | @field = Fields::Field.new
7 | end
8 |
9 | def test_be_required
10 | blank_value = ''
11 | assert_raises(ValidationError) do
12 | @field.clean(blank_value)
13 | end
14 | end
15 | end
16 |
17 | class Test_with_required_as_false < BureaucratTestCase
18 | def setup
19 | @field = Fields::Field.new(required: false)
20 | end
21 |
22 | def test_not_be_required
23 | blank_value = ''
24 | assert_nothing_raised do
25 | @field.clean(blank_value)
26 | end
27 | end
28 | end
29 |
30 | class Test_on_clean < BureaucratTestCase
31 | def setup
32 | @field = Fields::Field.new
33 | end
34 |
35 | def test_return_the_original_value_if_valid
36 | value = 'test'
37 | assert_equal(value, @field.clean(value))
38 | end
39 | end
40 |
41 | class Test_when_copied < BureaucratTestCase
42 | def setup
43 | @field = Fields::Field.new(initial: 'initial',
44 | label: 'label')
45 | @field_copy = @field.dup
46 | end
47 |
48 | def test_have_its_own_copy_of_initial_value
49 | assert_not_equal(@field.initial.object_id, @field_copy.initial.object_id)
50 | end
51 |
52 | def test_have_its_own_copy_of_the_label
53 | assert_not_equal(@field.label.object_id, @field_copy.label.object_id)
54 | end
55 |
56 | def test_have_its_own_copy_of_the_widget
57 | assert_not_equal(@field.widget.object_id, @field_copy.widget.object_id)
58 | end
59 |
60 | def test_have_its_own_copy_of_validators
61 | assert_not_equal(@field.validators.object_id, @field_copy.validators.object_id)
62 | end
63 |
64 | def test_have_its_own_copy_of_the_error_messaes
65 | assert_not_equal(@field.error_messages.object_id, @field_copy.error_messages.object_id)
66 | end
67 | end
68 | end
69 |
70 | module CharFieldTests
71 | class Test_with_empty_options < BureaucratTestCase
72 | def setup
73 | @field = Fields::CharField.new
74 | end
75 |
76 | def test_not_validate_max_length
77 | assert_nothing_raised do
78 | @field.clean("string" * 1000)
79 | end
80 | end
81 |
82 | def test_not_validate_min_length
83 | assert_nothing_raised do
84 | @field.clean("1")
85 | end
86 | end
87 | end
88 |
89 | class Test_with_max_length < BureaucratTestCase
90 | def setup
91 | @field = Fields::CharField.new(max_length: 10)
92 | end
93 |
94 | def test_allow_values_with_length_less_than_or_equal_to_max_length
95 | assert_nothing_raised do
96 | @field.clean('a' * 10)
97 | @field.clean('a' * 5)
98 | end
99 | end
100 |
101 | def test_not_allow_values_with_length_greater_than_max_length
102 | assert_raises(ValidationError) do
103 | @field.clean('a' * 11)
104 | end
105 | end
106 | end
107 |
108 | class Test_with_min_length < BureaucratTestCase
109 | def setup
110 | @field = Fields::CharField.new(min_length: 10)
111 | end
112 |
113 | def test_allow_values_with_length_greater_or_equal_to_min_length
114 | assert_nothing_raised do
115 | @field.clean('a' * 10)
116 | @field.clean('a' * 20)
117 | end
118 | end
119 |
120 | def test_not_allow_values_with_length_less_than_min_length
121 | assert_raises(ValidationError) do
122 | @field.clean('a' * 9)
123 | end
124 | end
125 | end
126 |
127 | class Test_on_clean < BureaucratTestCase
128 | def setup
129 | @field = Fields::CharField.new
130 | end
131 |
132 | def test_return_the_original_value_if_valid
133 | valid_value = 'test'
134 | assert_equal(valid_value, @field.clean(valid_value))
135 | end
136 |
137 | def test_return_a_blank_string_if_value_is_nil_and_required_is_false
138 | @field.required = false
139 | nil_value = nil
140 | assert_equal('', @field.clean(nil_value))
141 | end
142 |
143 | def test_return_a_blank_string_if_value_is_empty_and_required_is_false
144 | @field.required = false
145 | empty_value = ''
146 | assert_equal('', @field.clean(empty_value))
147 | end
148 | end
149 |
150 | end
151 |
152 | module IntegerFieldTests
153 | class Test_with_max_value < BureaucratTestCase
154 | def setup
155 | @field = Fields::IntegerField.new(max_value: 10)
156 | end
157 |
158 | def test_allow_values_less_or_equal_to_max_value
159 | assert_nothing_raised do
160 | @field.clean('10')
161 | @field.clean('4')
162 | end
163 | end
164 |
165 | def test_not_allow_values_greater_than_max_value
166 | assert_raises(ValidationError) do
167 | @field.clean('11')
168 | end
169 | end
170 | end
171 |
172 | class Test_with_min_value < BureaucratTestCase
173 | def setup
174 | @field = Fields::IntegerField.new(min_value: 10)
175 | end
176 |
177 | def test_allow_values_greater_or_equal_to_min_value
178 | assert_nothing_raised do
179 | @field.clean('10')
180 | @field.clean('20')
181 | end
182 | end
183 |
184 | def test_not_allow_values_less_than_min_value
185 | assert_raises(ValidationError) do
186 | @field.clean('9')
187 | end
188 | end
189 |
190 | end
191 |
192 | class Test_on_clean < BureaucratTestCase
193 | def setup
194 | @field = Fields::IntegerField.new
195 | end
196 |
197 | def test_return_an_integer_if_valid
198 | valid_value = '123'
199 | assert_equal(123, @field.clean(valid_value))
200 | end
201 |
202 | def test_return_nil_if_value_is_nil_and_required_is_false
203 | @field.required = false
204 | assert_nil(@field.clean(nil))
205 | end
206 |
207 | def test_return_nil_if_value_is_empty_and_required_is_false
208 | @field.required = false
209 | empty_value = ''
210 | assert_nil(@field.clean(empty_value))
211 | end
212 |
213 | def test_not_validate_invalid_formats
214 | invalid_formats = ['a', 'hello', '23eeee', '.', 'hi323',
215 | 'joe@example.com', '___3232___323',
216 | '123.0', '123..4']
217 |
218 | invalid_formats.each do |invalid|
219 | assert_raises(ValidationError) do
220 | @field.clean(invalid)
221 | end
222 | end
223 | end
224 |
225 | def test_validate_valid_formats
226 | valid_formats = ['3', '100', '-100', '0', '-0']
227 |
228 | assert_nothing_raised do
229 | valid_formats.each do |valid|
230 | @field.clean(valid)
231 | end
232 | end
233 | end
234 |
235 | def test_return_an_instance_of_Integer_if_valid
236 | result = @field.clean('7')
237 | assert_kind_of(Integer, result)
238 | end
239 | end
240 |
241 | end
242 |
243 | module FloatFieldTests
244 | class Test_with_max_value < BureaucratTestCase
245 | def setup
246 | @field = Fields::FloatField.new(max_value: 10.5)
247 | end
248 |
249 | def test_allow_values_less_or_equal_to_max_value
250 | assert_nothing_raised do
251 | @field.clean('10.5')
252 | @field.clean('5')
253 | end
254 | end
255 |
256 | def test_not_allow_values_greater_than_max_value
257 | assert_raises(ValidationError) do
258 | @field.clean('10.55')
259 | end
260 | end
261 | end
262 |
263 | class Test_with_min_value < BureaucratTestCase
264 | def setup
265 | @field = Fields::FloatField.new(min_value: 10.5)
266 | end
267 |
268 | def test_allow_values_greater_or_equal_than_min_value
269 | assert_nothing_raised do
270 | @field.clean('10.5')
271 | @field.clean('20.5')
272 | end
273 | end
274 |
275 | def test_not_allow_values_less_than_min_value
276 | assert_raises(ValidationError) do
277 | @field.clean('10.49')
278 | end
279 | end
280 | end
281 |
282 | class Test_on_clean < BureaucratTestCase
283 | def setup
284 | @field = Fields::FloatField.new
285 | end
286 |
287 | def test_return_nil_if_value_is_nil_and_required_is_false
288 | @field.required = false
289 | assert_nil(@field.clean(nil))
290 | end
291 |
292 | def test_return_nil_if_value_is_empty_and_required_is_false
293 | @field.required = false
294 | empty_value = ''
295 | assert_nil(@field.clean(empty_value))
296 | end
297 |
298 | def test_not_validate_invalid_formats
299 | invalid_formats = ['a', 'hello', '23eeee', '.', 'hi323',
300 | 'joe@example.com', '___3232___323',
301 | '123..', '123..4']
302 |
303 | invalid_formats.each do |invalid|
304 | assert_raises(ValidationError) do
305 | @field.clean(invalid)
306 | end
307 | end
308 | end
309 |
310 | def test_validate_valid_formats
311 | valid_formats = ['3.14', "100", "1233.", ".3333", "0.434", "0.0"]
312 |
313 | assert_nothing_raised do
314 | valid_formats.each do |valid|
315 | @field.clean(valid)
316 | end
317 | end
318 | end
319 |
320 | def test_return_an_instance_of_Float_if_valid
321 | result = @field.clean('3.14')
322 | assert_instance_of(Float, result)
323 | end
324 | end
325 | end
326 |
327 | module BigDecimalFieldTests
328 | class Test_with_max_value < BureaucratTestCase
329 | def setup
330 | @field = Fields::BigDecimalField.new(max_value: 10.5)
331 | end
332 |
333 | def test_allow_values_less_or_equal_to_max_value
334 | assert_nothing_raised do
335 | @field.clean('10.5')
336 | @field.clean('5')
337 | end
338 | end
339 |
340 | def test_not_allow_values_greater_than_max_value
341 | assert_raises(ValidationError) do
342 | @field.clean('10.55')
343 | end
344 | end
345 | end
346 |
347 | class Test_with_min_value < BureaucratTestCase
348 | def setup
349 | @field = Fields::BigDecimalField.new(min_value: 10.5)
350 | end
351 |
352 | def test_allow_values_greater_or_equal_to_min_value
353 | assert_nothing_raised do
354 | @field.clean('10.5')
355 | @field.clean('20.5')
356 | end
357 | end
358 |
359 | def test_not_allow_values_less_than_min_value
360 | assert_raises(ValidationError) do
361 | @field.clean('10.49')
362 | end
363 | end
364 | end
365 |
366 | class Test_on_clean < BureaucratTestCase
367 | def setup
368 | @field = Fields::BigDecimalField.new
369 | end
370 |
371 | def test_return_nil_if_value_is_nil_and_required_is_false
372 | @field.required = false
373 | assert_nil(@field.clean(nil))
374 | end
375 |
376 | def test_return_nil_if_value_is_empty_and_required_is_false
377 | @field.required = false
378 | empty_value = ''
379 | assert_nil(@field.clean(empty_value))
380 | end
381 |
382 | def test_not_validate_invalid_formats
383 | invalid_formats = ['a', 'hello', '23eeee', '.', 'hi323',
384 | 'joe@example.com', '___3232___323',
385 | '123..', '123..4']
386 |
387 | invalid_formats.each do |invalid|
388 | assert_raises(ValidationError) do
389 | @field.clean(invalid)
390 | end
391 | end
392 | end
393 |
394 | def test_validate_valid_formats
395 | valid_formats = ['3.14', "100", "1233.", ".3333", "0.434", "0.0"]
396 |
397 | assert_nothing_raised do
398 | valid_formats.each do |valid|
399 | @field.clean(valid)
400 | end
401 | end
402 | end
403 |
404 | def test_return_an_instance_of_BigDecimal_if_valid
405 | result = @field.clean('3.14')
406 | assert_instance_of(BigDecimal, result)
407 | end
408 | end
409 | end
410 |
411 | module RegexFieldTests
412 | class Test_on_clean < BureaucratTestCase
413 | def setup
414 | @field = Fields::RegexField.new(/ba(na){2,}/)
415 | end
416 |
417 | def test_validate_matching_values
418 | valid_values = ['banana', 'bananananana']
419 | valid_values.each do |valid|
420 | assert_nothing_raised do
421 | @field.clean(valid)
422 | end
423 | end
424 | end
425 |
426 | def test_not_validate_non_matching_values
427 | invalid_values = ['bana', 'spoon']
428 | assert_raises(ValidationError) do
429 | invalid_values.each do |invalid|
430 | @field.clean(invalid)
431 | end
432 | end
433 | end
434 |
435 | def test_return_a_blank_string_if_value_is_empty_and_required_is_false
436 | @field.required = false
437 | empty_value = ''
438 | assert_equal('', @field.clean(empty_value))
439 | end
440 | end
441 | end
442 |
443 | module EmailFieldTests
444 | class Test_on_clean < BureaucratTestCase
445 | def setup
446 | @field = Fields::EmailField.new
447 | end
448 |
449 | def test_validate_email_matching_values
450 | valid_values = ['email@domain.com', 'email+extra@domain.com',
451 | 'email@domain.fm', 'email@domain.co.uk']
452 | valid_values.each do |valid|
453 | assert_nothing_raised do
454 | @field.clean(valid)
455 | end
456 | end
457 | end
458 |
459 | def test_not_validate_non_email_matching_values
460 | invalid_values = ['banana', 'spoon', 'invalid@dom#ain.com',
461 | 'invalid@@domain.com', 'invalid@domain',
462 | 'invalid@.com']
463 | invalid_values.each do |invalid|
464 | assert_raises(ValidationError) do
465 | @field.clean(invalid)
466 | end
467 | end
468 | end
469 | end
470 | end
471 |
472 | module BooleanFieldTests
473 | class Test_on_clean < BureaucratTestCase
474 | def setup
475 | @true_values = [1, true, 'true', '1']
476 | @false_values = [nil, 0, false, 'false', '0']
477 | @field = Fields::BooleanField.new
478 | end
479 |
480 | def test_return_true_for_true_values
481 | @true_values.each do |true_value|
482 | assert_equal(true, @field.clean(true_value))
483 | end
484 | end
485 |
486 | def test_return_false_for_false_values
487 | @field.required = false
488 | @false_values.each do |false_value|
489 | assert_equal(false, @field.clean(false_value))
490 | end
491 | end
492 |
493 | def test_validate_on_true_values_when_required
494 | assert_nothing_raised do
495 | @true_values.each do |true_value|
496 | @field.clean(true_value)
497 | end
498 | end
499 | end
500 |
501 | def test_not_validate_on_false_values_when_required
502 | @false_values.each do |false_value|
503 | assert_raises(ValidationError) do
504 | @field.clean(false_value)
505 | end
506 | end
507 | end
508 |
509 | def test_validate_on_false_values_when_not_required
510 | @field.required = false
511 | assert_nothing_raised do
512 | @false_values.each do |false_value|
513 | @field.clean(false_value)
514 | end
515 | end
516 | end
517 | end
518 | end
519 |
520 | module NullBooleanFieldTests
521 | class Test_on_clean < BureaucratTestCase
522 | def setup
523 | @true_values = [true, 'true', '1']
524 | @false_values = [false, 'false', '0']
525 | @null_values = [nil, '', 'banana']
526 | @field = Fields::NullBooleanField.new
527 | end
528 |
529 | def test_return_true_for_true_values
530 | @true_values.each do |true_value|
531 | assert_equal(true, @field.clean(true_value))
532 | end
533 | end
534 |
535 | def test_return_false_for_false_values
536 | @false_values.each do |false_value|
537 | assert_equal(false, @field.clean(false_value))
538 | end
539 | end
540 |
541 | def test_return_nil_for_null_values
542 | @null_values.each do |null_value|
543 | assert_equal(nil, @field.clean(null_value))
544 | end
545 | end
546 |
547 | def test_validate_on_all_values
548 | all_values = @true_values + @false_values + @null_values
549 | assert_nothing_raised do
550 | all_values.each do |value|
551 | @field.clean(value)
552 | end
553 | end
554 | end
555 | end
556 | end
557 |
558 | module ChoiceFieldTests
559 | class Test_when_copied < BureaucratTestCase
560 | def setup
561 | @choices = [['tea', 'Tea'], ['milk', 'Milk']]
562 | @field = Fields::ChoiceField.new(@choices)
563 | @field_copy = @field.dup
564 | end
565 |
566 | def test_have_its_own_copy_of_choices
567 | assert_not_equal(@field.choices.object_id, @field_copy.choices.object_id)
568 | end
569 | end
570 |
571 | class Test_on_clean < BureaucratTestCase
572 | def setup
573 | @choices = [['tea', 'Tea'], ['milk', 'Milk']]
574 | @choices_hash = [[{ value: "able" }, "able"], [{ value: "baker" }, "Baker"]]
575 | @field = Fields::ChoiceField.new(@choices)
576 | @field_hash = Fields::ChoiceField.new(@choices_hash)
577 | end
578 |
579 | def test_validate_all_values_in_choices_list
580 | assert_nothing_raised do
581 | @choices.collect(&:first).each do |valid|
582 | @field.clean(valid)
583 | end
584 | end
585 | end
586 |
587 | def test_validate_all_values_in_a_hash_choices_list
588 | assert_nothing_raised do
589 | @choices_hash.collect(&:first).each do |valid|
590 | @field_hash.clean(valid[:value])
591 | end
592 | end
593 | end
594 |
595 | def test_not_validate_a_value_not_in_choices_list
596 | assert_raises(ValidationError) do
597 | @field.clean('not_in_choices')
598 | end
599 | end
600 |
601 | def test_not_validate_a_value_not_in_a_hash_choices_list
602 | assert_raises(ValidationError) do
603 | @field_hash.clean('not_in_choices')
604 | end
605 | end
606 |
607 | def test_return_the_original_value_if_valid
608 | value = 'tea'
609 | result = @field.clean(value)
610 | assert_equal(value, result)
611 | end
612 |
613 | def test_return_the_original_value_if_valid_from_a_hash_choices_list
614 | value = 'baker'
615 | result = @field_hash.clean(value)
616 | assert_equal(value, result)
617 | end
618 |
619 | def test_return_an_empty_string_if_value_is_empty_and_not_required
620 | @field.required = false
621 | result = @field.clean('')
622 | assert_equal('', result)
623 | end
624 |
625 | def test_return_an_empty_string_if_value_is_empty_and_not_required_from_a_hash_choices_list
626 | @field_hash.required = false
627 | result = @field_hash.clean('')
628 | assert_equal('', result)
629 | end
630 | end
631 | end
632 |
633 | module TypedChoiceFieldTests
634 | class Test_on_clean < BureaucratTestCase
635 | def setup
636 | @choices = [[1, 'One'], [2, 'Two'], ['3', 'Three']]
637 | to_int = lambda{|val| Integer(val)}
638 | @field = Fields::TypedChoiceField.new(@choices,
639 | coerce: to_int)
640 | end
641 |
642 | def test_validate_all_values_in_choices_list
643 | assert_nothing_raised do
644 | @choices.collect(&:first).each do |valid|
645 | @field.clean(valid)
646 | end
647 | end
648 | end
649 |
650 | def test_not_validate_a_value_not_in_choices_list
651 | assert_raises(ValidationError) do
652 | @field.clean('four')
653 | end
654 | end
655 |
656 | def test_return_the_original_value_if_valid
657 | value = 1
658 | result = @field.clean(value)
659 | assert_equal(value, result)
660 | end
661 |
662 | def test_return_a_coerced_version_of_the_original_value_if_valid_but_of_different_type
663 | value = 2
664 | result = @field.clean(value.to_s)
665 | assert_equal(value, result)
666 | end
667 |
668 | def test_return_an_empty_string_if_value_is_empty_and_not_required
669 | @field.required = false
670 | result = @field.clean('')
671 | assert_equal('', result)
672 | end
673 | end
674 | end
675 |
676 | module MultipleChoiceFieldTests
677 | class Test_on_clean < BureaucratTestCase
678 | def setup
679 | @choices = [['tea', 'Tea'], ['milk', 'Milk'], ['coffee', 'Coffee']]
680 | @field = Fields::MultipleChoiceField.new(@choices)
681 | end
682 |
683 | def test_validate_all_single_values_in_choices_list
684 | assert_nothing_raised do
685 | @choices.collect(&:first).each do |valid|
686 | @field.clean([valid])
687 | end
688 | end
689 | end
690 |
691 | def test_validate_multiple_values
692 | values = ['tea', 'coffee']
693 | assert_nothing_raised do
694 | @field.clean(values)
695 | end
696 | end
697 |
698 | def test_not_validate_a_value_not_in_choices_list
699 | assert_raises(ValidationError) do
700 | @field.clean(['tea', 'not_in_choices'])
701 | end
702 | end
703 |
704 | def test_return_the_original_value_if_valid
705 | value = 'tea'
706 | result = @field.clean([value])
707 | assert_equal([value], result)
708 | end
709 |
710 | def test_return_an_empty_list_if_value_is_empty_and_not_required
711 | @field.required = false
712 | result = @field.clean([])
713 | assert_equal([], result)
714 | end
715 | end
716 | end
717 |
--------------------------------------------------------------------------------
/test/forms_test.rb:
--------------------------------------------------------------------------------
1 | require_relative 'test_helper'
2 |
3 | module FormTests
4 | class Test_inherited_form_with_a_CharField < BureaucratTestCase
5 | class OneForm < Forms::Form
6 | include Bureaucrat::Fields
7 |
8 | field :name, CharField.new
9 | end
10 |
11 | def test_have_a_BoundField
12 | form = OneForm.new
13 | assert_kind_of(Forms::BoundField, form[:name])
14 | end
15 |
16 | def test_be_bound_when_data_is_provided
17 | form = OneForm.new(name: 'name')
18 | assert_equal(true, form.bound?)
19 | end
20 |
21 | class Test_when_calling_valid < BureaucratTestCase
22 | def test_return_false_when_data_isnt_valid
23 | form = OneForm.new(name: nil)
24 | assert_equal(false, form.valid?)
25 | end
26 |
27 | def test_return_true_when_data_is_valid
28 | form = OneForm.new(name: 'valid')
29 | assert_equal(true, form.valid?)
30 | end
31 | end
32 |
33 | class Test_when_calling_errors < BureaucratTestCase
34 | def test_have_errors_when_invalid
35 | form = OneForm.new(name: nil)
36 | assert_operator(form.errors.size, :>, 0)
37 | end
38 |
39 | def test_not_have_errors_when_valid
40 | form = OneForm.new(name: 'valid')
41 | assert_equal(form.errors.size, 0)
42 | end
43 | end
44 |
45 | class Test_when_calling_changed_data < BureaucratTestCase
46 | def test_return_an_empty_list_if_no_field_was_changed
47 | form = OneForm.new
48 | assert_equal([], form.changed_data)
49 | end
50 |
51 | def test_return_a_list_of_changed_fields_when_modified
52 | form = OneForm.new(name: 'changed')
53 | assert_equal([:name], form.changed_data)
54 | end
55 | end
56 | end
57 |
58 | class Test_form_with_custom_clean_proc_on_field < BureaucratTestCase
59 | class CustomCleanForm < Forms::Form
60 | include Bureaucrat::Fields
61 |
62 | field :name, CharField.new
63 |
64 | def clean_name
65 | value = cleaned_data[:name]
66 | unless value == 'valid_name'
67 | raise Bureaucrat::ValidationError.new("Invalid name")
68 | end
69 | value.upcase
70 | end
71 | end
72 |
73 | def test_not_be_valid_if_clean_method_fails
74 | form = CustomCleanForm.new(name: 'other')
75 | assert_equal(false, form.valid?)
76 | end
77 |
78 | def test_be_valid_if_clean_method_passes
79 | form = CustomCleanForm.new(name: 'valid_name')
80 | assert_equal(true, form.valid?)
81 | end
82 |
83 | def test_set_the_value_to_the_one_returned_by_the_custom_clean_method
84 | form = CustomCleanForm.new(name: 'valid_name')
85 | form.valid?
86 | assert_equal('VALID_NAME', form.cleaned_data[:name])
87 | end
88 |
89 | end
90 |
91 | class Test_populating_objects < BureaucratTestCase
92 | class PopulatorForm < Forms::Form
93 | include Bureaucrat::Fields
94 |
95 | field :name, CharField.new(required: false)
96 | field :color, CharField.new(required: false)
97 | field :number, IntegerField.new(required: false)
98 | end
99 |
100 | def test_correctly_populate_an_object_with_all_fields
101 | obj = Struct.new(:name, :color, :number).new
102 | name_value = 'The Name'
103 | color_value = 'Black'
104 | number_value = 10
105 |
106 | form = PopulatorForm.new(name: name_value,
107 | color: color_value,
108 | number: number_value.to_s)
109 |
110 | assert form.valid?
111 |
112 | form.populate_object(obj)
113 |
114 | assert_equal(name_value, obj.name)
115 | assert_equal(color_value, obj.color)
116 | assert_equal(number_value, obj.number)
117 | end
118 |
119 | def test_correctly_populate_an_object_without_all_fields
120 | obj = Struct.new(:name, :number).new
121 | name_value = 'The Name'
122 | color_value = 'Black'
123 | number_value = 10
124 |
125 | form = PopulatorForm.new(name: name_value,
126 | color: color_value,
127 | number: number_value.to_s)
128 |
129 | assert form.valid?
130 |
131 | form.populate_object(obj)
132 |
133 | assert_equal(name_value, obj.name)
134 | assert_equal(number_value, obj.number)
135 | end
136 |
137 | def test_correctly_populate_an_object_with_all_fields_with_some_missing_values
138 | obj = Struct.new(:name, :color, :number).new('a', 'b', 2)
139 |
140 | form = PopulatorForm.new({})
141 |
142 | assert form.valid?
143 |
144 | form.populate_object(obj)
145 |
146 | assert_equal('', obj.name)
147 | assert_equal('', obj.color)
148 | assert_equal(nil, obj.number)
149 | end
150 | end
151 | end
152 |
--------------------------------------------------------------------------------
/test/formsets_test.rb:
--------------------------------------------------------------------------------
1 | require_relative 'test_helper'
2 |
3 | module FormsetTests
4 | class SimpleForm < Bureaucrat::Forms::Form
5 | include Bureaucrat::Fields
6 |
7 | field :name, CharField.new
8 | end
9 |
10 | SimpleFormFormSet =
11 | Bureaucrat::Formsets.make_formset_class(SimpleForm, extra: 2)
12 |
13 | class CustomFormSet < Bureaucrat::Formsets::BaseFormSet
14 | def clean
15 | raise Bureaucrat::ValidationError.new('This is wrong!')
16 | end
17 | end
18 |
19 | SimpleFormCustomFormSet =
20 | Bureaucrat::Formsets.make_formset_class(SimpleForm,
21 | extra: 2,
22 | formset: CustomFormSet)
23 |
24 | class Test_formset_with_empty_data < BureaucratTestCase
25 | def setup
26 | management_form_data = {
27 | :'form-TOTAL_FORMS' => '2',
28 | :'form-INITIAL_FORMS' => '2'
29 | }
30 | valid_data = {:'form-0-name' => 'Lynch', :'form-1-name' => 'Tio'}
31 | invalid_data = {:'form-0-name' => 'Lynch', :'form-1-name' => ''}
32 | @set = SimpleFormFormSet.new
33 | @valid_bound_set = SimpleFormFormSet.new(management_form_data.merge(valid_data))
34 | @invalid_bound_set = SimpleFormFormSet.new(management_form_data.merge(invalid_data))
35 | end
36 |
37 | def test_#valid?_returns_true_if_all_forms_are_valid
38 | assert(@valid_bound_set.valid?)
39 | end
40 |
41 | def test_#valid?_returns_false_if_there_is_an_invalid_form
42 | assert(!@invalid_bound_set.valid?)
43 | end
44 |
45 | def test_correctly_return_the_list_of_errors
46 | assert_equal([{}, {name: ["This field is required"]}],
47 | @invalid_bound_set.errors)
48 | end
49 |
50 | def test_correctly_return_the_list_of_cleaned_data
51 | expected = [{'name' => 'Lynch'}, {'name' => 'Tio'}]
52 | result = @valid_bound_set.cleaned_data
53 | assert_equal(expected, result)
54 | end
55 | end
56 |
57 | class Test_Formset_with_clean_method_raising_a_ValidationError_exception < BureaucratTestCase
58 | def setup
59 | management_form_data = {
60 | :'form-TOTAL_FORMS' => '2',
61 | :'form-INITIAL_FORMS' => '2'
62 | }
63 | valid_data = {:'form-0-name' => 'Lynch', :'form-1-name' => 'Tio'}
64 | @bound_set =
65 | SimpleFormCustomFormSet.new(management_form_data.merge(valid_data))
66 | end
67 |
68 | def test_not_be_valid
69 | assert_equal(false, @bound_set.valid?)
70 | end
71 |
72 | def test_add_clean_errors_to_nonfield_errors
73 | @bound_set.valid?
74 | assert_equal(["This is wrong!"], @bound_set.non_form_errors)
75 | end
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | require 'bigdecimal'
2 | require 'minitest/autorun'
3 |
4 | require_relative '../lib/bureaucrat'
5 | require_relative '../lib/bureaucrat/formsets'
6 |
7 | # Used to compare rendered htmls
8 | require 'rexml/document'
9 |
10 | class BureaucratTestCase < MiniTest::Unit::TestCase
11 | include Bureaucrat
12 |
13 | def assert_nothing_raised(&block)
14 | block.call
15 | assert true
16 | end
17 |
18 | def assert_not_equal(value, other)
19 | assert value != other, "should be different from #{value}"
20 | end
21 |
22 | def normalize_html(html)
23 | begin
24 | node = REXML::Document.new("#{html.strip}")
25 | node.to_s.gsub!(/<\/?DUMMYROOT>/, '')
26 | rescue
27 | html
28 | end
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/test/widgets_test.rb:
--------------------------------------------------------------------------------
1 | require_relative 'test_helper'
2 |
3 | module WidgetTests
4 | class Test_TextInput_widget < BureaucratTestCase
5 | class Test_with_empty_attributes < BureaucratTestCase
6 | def test_correctly_render
7 | input = Widgets::TextInput.new
8 | expected = normalize_html('')
9 | rendered = normalize_html(input.render('test', 'hi'))
10 | assert_equal(expected, rendered)
11 | end
12 | end
13 |
14 | class Test_with_attributes < BureaucratTestCase
15 | def test_correctly_render
16 | input = Widgets::TextInput.new(attribute: 'value')
17 | expected = normalize_html('')
18 | rendered = normalize_html(input.render('test', 'hi'))
19 | assert_equal(expected, rendered)
20 | end
21 | end
22 |
23 | class Test_without_value < BureaucratTestCase
24 | def test_not_render_a_value
25 | input = Widgets::TextInput.new
26 | expected = normalize_html('')
27 | rendered = normalize_html(input.render('test', nil))
28 | assert_equal(expected, rendered)
29 | end
30 | end
31 |
32 | class Test_when_copied < BureaucratTestCase
33 | def test_have_a_copy_of_the_attributes
34 | input1 = Widgets::TextInput.new(attribute: 2)
35 | input2 = input1.dup
36 | assert_not_equal(input1.attrs.object_id, input2.attrs.object_id)
37 | end
38 | end
39 | end
40 |
41 | class Test_PasswordInput_widget < BureaucratTestCase
42 | class Test_with_render_value_true < BureaucratTestCase
43 | def test_render_correctly_including_value
44 | input = Widgets::PasswordInput.new(nil, true)
45 | expected = normalize_html("")
46 | rendered = normalize_html(input.render('test', 'secret'))
47 | assert_equal(expected, rendered)
48 | end
49 | end
50 |
51 | class Test_with_render_value_false < BureaucratTestCase
52 | def test_render_correctly_not_including_value
53 | input = Widgets::PasswordInput.new(nil, false)
54 | expected = normalize_html("")
55 | rendered = normalize_html(input.render('test', 'secret'))
56 | assert_equal(expected, rendered)
57 | end
58 | end
59 | end
60 |
61 | class Test_HiddenInput_widget < BureaucratTestCase
62 | def test_correctly_render
63 | input = Widgets::HiddenInput.new
64 | expected = normalize_html("")
65 | rendered = normalize_html(input.render('test', 'secret'))
66 | assert_equal(expected, rendered)
67 | end
68 | end
69 |
70 | class Test_MultipleHiddenInput_widget < BureaucratTestCase
71 | def test_correctly_render
72 | input = Widgets::MultipleHiddenInput.new
73 | expected = normalize_html("\n")
74 | rendered = normalize_html(input.render('test', ['v1', 'v2']))
75 | assert_equal(expected, rendered)
76 | end
77 | # TODO: value_from_datahash
78 | end
79 |
80 | class Test_FileInput_widget < BureaucratTestCase
81 | def test_correctly_render
82 | input = Widgets::FileInput.new
83 | expected = normalize_html("")
84 | rendered = normalize_html(input.render('test', "anything"))
85 | assert_equal(expected, rendered)
86 | end
87 | # TODO: value_from_datahash, has_changed?
88 | end
89 |
90 | class Test_Textarea_widget < BureaucratTestCase
91 | def test_correctly_render
92 | input = Widgets::Textarea.new(cols: '50', rows: '15')
93 | expected = normalize_html("")
94 | rendered = normalize_html(input.render('test', "hello"))
95 | assert_equal(expected, rendered)
96 | end
97 |
98 | def test_correctly_render_multiline
99 | input = Widgets::Textarea.new(cols: '50', rows: '15')
100 | expected = normalize_html("")
101 | rendered = normalize_html(input.render('test', "hello\n\ntest"))
102 | assert_equal(expected, rendered)
103 | end
104 | end
105 |
106 | class Test_CheckboxInput_widget < BureaucratTestCase
107 | def test_correctly_render_with_a_false_value
108 | input = Widgets::CheckboxInput.new
109 | expected = normalize_html("")
110 | rendered = normalize_html(input.render('test', false))
111 | assert_equal(expected, rendered)
112 | end
113 |
114 | def test_correctly_render_with_a_true_value
115 | input = Widgets::CheckboxInput.new
116 | expected =""
117 | rendered = normalize_html(input.render('test', true))
118 | assert_equal(expected, rendered)
119 | end
120 |
121 | def test_correctly_render_with_a_non_boolean_value
122 | input = Widgets::CheckboxInput.new
123 | expected = ""
124 | rendered = normalize_html(input.render('test', 'anything'))
125 | assert_equal(expected, rendered)
126 | end
127 | # TODO: value_from_datahash, has_changed?
128 | end
129 |
130 | module SelectWidgetTests
131 | class Test_with_empty_choices < BureaucratTestCase
132 | def test_correctly_render
133 | input = Widgets::Select.new
134 | expected = normalize_html("")
135 | rendered = normalize_html(input.render('test', 'hello'))
136 | assert_equal(expected, rendered)
137 | end
138 | end
139 |
140 | class Test_with_flat_choices < BureaucratTestCase
141 | def setup
142 | @choices = [['1', 'One'], ['2', 'Two']]
143 | end
144 |
145 | def test_correctly_render_none_selected
146 | input = Widgets::Select.new(nil, @choices)
147 | expected = normalize_html("")
148 | rendered = normalize_html(input.render('test', 'hello'))
149 | assert_equal(expected, rendered)
150 | end
151 |
152 | def test_correctly_render_with_selected
153 | input = Widgets::Select.new(nil, @choices)
154 | expected = normalize_html("")
155 | rendered = normalize_html(input.render('test', '2'))
156 | assert_equal(expected, rendered)
157 | end
158 | end
159 |
160 | class Test_with_group_choices < BureaucratTestCase
161 | def setup
162 | @groupchoices = [['numbers', ['1', 'One'], ['2', 'Two']],
163 | ['words', [['spoon', 'Spoon'], ['banana', 'Banana']]]]
164 | end
165 |
166 | def test_correctly_render_none_selected
167 | input = Widgets::Select.new(nil, @groupchoices)
168 | expected = normalize_html("")
169 | rendered = normalize_html(input.render('test', 'hello'))
170 | assert_equal(expected, rendered)
171 | end
172 |
173 | def test_correctly_render_with_selected
174 | input = Widgets::Select.new(nil, @groupchoices)
175 | expected = normalize_html("")
176 | rendered = normalize_html(input.render('test', 'banana'))
177 | assert_equal(expected, rendered)
178 | end
179 | end
180 |
181 | class Test_with_simple_choices < BureaucratTestCase
182 | def setup
183 | @simplechoices = [ "able", "baker", "charlie" ]
184 | end
185 |
186 | def test_correctly_render_none_selected
187 | input = Widgets::Select.new(nil, @simplechoices)
188 | expected = normalize_html("")
189 | rendered = normalize_html(input.render('test', 'hello'))
190 | assert_equal(expected, rendered)
191 | end
192 |
193 | def test_correctly_render_with_selected
194 | input = Widgets::Select.new(nil, @simplechoices)
195 | expected = normalize_html("")
196 | rendered = normalize_html(input.render('test', 'baker'))
197 | assert_equal(expected, rendered)
198 | end
199 | end
200 |
201 | class Test_with_option_choices < BureaucratTestCase
202 | def setup
203 | @optionchoices = [[{ value: "foo", disabled: "disabled", onSelect: "doSomething();" }, "Foo"],
204 | [{ value: "bar" }, "Bar"]]
205 | @optionchoicesselected = [[{ value: "foo", disabled: "disabled" }, "Foo"],
206 | [{ value: "bar", selected: "selected" }, "Bar"]]
207 | end
208 |
209 | def test_correctly_render_none_selected
210 | input = Widgets::Select.new(nil, @optionchoices)
211 | expected = normalize_html("")
212 | rendered = normalize_html(input.render('test', 'hello'))
213 | assert_equal(expected, rendered)
214 | end
215 |
216 | def test_correctly_render_traditional_selected
217 | input = Widgets::Select.new(nil, @optionchoices)
218 | expected = normalize_html("")
219 | rendered = normalize_html(input.render('test', 'bar'))
220 | assert_equal(expected, rendered)
221 | end
222 |
223 | def test_correctly_render_option_selected
224 | input = Widgets::Select.new(nil, @optionchoicesselected)
225 | expected = normalize_html("")
226 | rendered = normalize_html(input.render('test', 'hello'))
227 | assert_equal(expected, rendered)
228 | end
229 | end
230 | end
231 |
232 | class Test_NullBooleanSelect_widget < BureaucratTestCase
233 | def test_correctly_render_with_Unknown_as_the_default_value_when_none_is_selected
234 | input = Widgets::NullBooleanSelect.new
235 | expected = normalize_html("")
236 | rendered = normalize_html(input.render('test', nil))
237 | assert_equal(expected, rendered)
238 | end
239 |
240 | def test_correctly_render_with_selected
241 | input = Widgets::NullBooleanSelect.new
242 | expected = normalize_html("")
243 | rendered = normalize_html(input.render('test', '2'))
244 | assert_equal(expected, rendered)
245 | end
246 | end
247 |
248 | module SelectMultipleTests
249 | class Test_with_empty_choices < BureaucratTestCase
250 | def test_correctly_render
251 | input = Widgets::SelectMultiple.new
252 | expected = normalize_html("")
253 | rendered = normalize_html(input.render('test', ['hello']))
254 | assert_equal(expected, rendered)
255 | end
256 | end
257 |
258 | class Test_with_flat_choices < BureaucratTestCase
259 | def setup
260 | @choices = [['1', 'One'], ['2', 'Two']]
261 | end
262 |
263 | def test_correctly_render_none_selected
264 | input = Widgets::SelectMultiple.new(nil, @choices)
265 | expected = normalize_html("")
266 | rendered = normalize_html(input.render('test', ['hello']))
267 | assert_equal(expected, rendered)
268 | end
269 |
270 | def test_correctly_render_with_selected
271 | input = Widgets::SelectMultiple.new(nil, @choices)
272 | expected = normalize_html("")
273 | rendered = normalize_html(input.render('test', ['1', '2']))
274 | assert_equal(expected, rendered)
275 | end
276 | end
277 |
278 | class Test_with_group_choices < BureaucratTestCase
279 | def setup
280 | @groupchoices = [['numbers', ['1', 'One'], ['2', 'Two']],
281 | ['words', [['spoon', 'Spoon'], ['banana', 'Banana']]]]
282 | end
283 |
284 | def test_correctly_render_none_selected
285 | input = Widgets::SelectMultiple.new(nil, @groupchoices)
286 | expected = normalize_html("")
287 | rendered = normalize_html(input.render('test', ['hello']))
288 | assert_equal(expected, rendered)
289 | end
290 |
291 | def test_correctly_render_with_selected
292 | input = Widgets::SelectMultiple.new(nil, @groupchoices)
293 | expected = normalize_html("")
294 | rendered = normalize_html(input.render('test', ['banana', 'spoon']))
295 | assert_equal(expected, rendered)
296 | end
297 | end
298 | end
299 |
300 | class Test_RadioSelect_widget < BureaucratTestCase
301 | def test_correctly_render_none_selected
302 | input = Widgets::RadioSelect.new(nil, [['1', 'One'], ['2', 'Two']])
303 | expected = normalize_html("")
304 | rendered = normalize_html(input.render('radio', '', id: 'id_radio'))
305 | assert_equal(expected, rendered)
306 | end
307 |
308 | def test_correctly_render_with_selected
309 | input = Widgets::RadioSelect.new(nil, [['1', 'One'], ['2', 'Two']])
310 | expected = normalize_html("")
311 | rendered = normalize_html(input.render('radio', '1', id: 'id_radio'))
312 | assert_equal(expected, rendered)
313 | end
314 | end
315 |
316 | module CheckboxSelectMultipleTests
317 | class Test_with_empty_choices < BureaucratTestCase
318 | def test_render_an_empty_ul
319 | input = Widgets::CheckboxSelectMultiple.new
320 | expected = normalize_html("")
321 | rendered = normalize_html(input.render('test', ['hello'], id: 'id_checkboxes'))
322 | assert_equal(expected, rendered)
323 | end
324 | end
325 |
326 | class Test_with_choices < BureaucratTestCase
327 | def setup
328 | @choices = [['1', 'One'], ['2', 'Two'], ['3', 'Three']]
329 | end
330 |
331 | def test_correctly_renders_none_selected
332 | input = Widgets::CheckboxSelectMultiple.new(nil, @choices)
333 | expected = normalize_html("")
334 | rendered = normalize_html(input.render('test', ['hello'], id: 'id_checkboxes'))
335 | assert_equal(expected, rendered)
336 | end
337 |
338 | def test_correctly_renders_with_selected
339 | input = Widgets::CheckboxSelectMultiple.new(nil, @choices)
340 | expected = normalize_html("")
341 | rendered = normalize_html(input.render('test', ['1', '2'],
342 | id: 'id_checkboxes'))
343 | assert_equal(expected, rendered)
344 | end
345 | end
346 | end
347 |
348 | end
349 |
--------------------------------------------------------------------------------