bureaucrat 0.0.3 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,397 +1,553 @@
1
1
  require 'uri'
2
2
 
3
- require 'bureaucrat/utils'
4
-
5
3
  module Bureaucrat
6
- module Widgets
7
- class Media
8
- include Utils
4
+ module Widgets
5
+ # Base class for widgets
6
+ class Widget
7
+ include Utils
9
8
 
10
- MEDIA_TYPES = [:css, :js]
11
- DEFAULT_MEDIA_PATH = 'http://localhost'
9
+ attr_accessor :is_required
10
+ attr_reader :attrs
12
11
 
13
- attr_accessor :media_path
12
+ def initialize(attrs = nil)
13
+ @attrs = attrs.nil? ? {} : attrs.dup
14
+ end
14
15
 
15
- def initialize(media_attrs={})
16
- self.media_path = media_attrs.delete(:media_path) || DEFAULT_MEDIA_PATH
17
- media_attrs = media_attrs.to_hash if media_attrs.is_a?(Media)
18
- @css = {}
19
- @js = []
16
+ def initialize_copy(original)
17
+ super(original)
18
+ @attrs = original.attrs.dup
19
+ end
20
20
 
21
- MEDIA_TYPES.each do |name|
22
- data = media_attrs[name]
23
- add_type(name, data) if data
24
- end
25
- end
21
+ def render(name, value, attrs = nil)
22
+ raise NotImplementedError
23
+ end
26
24
 
27
- def to_s
28
- render
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
29
52
  end
30
53
 
31
- def to_hash
32
- hash = {}
33
- MEDIA_TYPES.each {|name| hash[name] = instance_variable_get("@#{name}")}
34
- hash
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("<input#{flatatt(final_attrs)} />")
63
+ end
64
+
65
+ def input_type
66
+ nil
67
+ end
35
68
  end
36
69
 
37
- def render
38
- mark_safe(MEDIA_TYPES.map do |name|
39
- render_type(name)
40
- end.inject(&:+).join("\n"))
70
+ # Class for text inputs
71
+ class TextInput < Input
72
+ def input_type
73
+ 'text'
74
+ end
41
75
  end
42
76
 
43
- def render_type(type)
44
- send("render_#{type}")
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
45
92
  end
46
93
 
47
- def render_js
48
- @js.map do |path|
49
- "<script type=\"text/javascript\" src=\"#{absolute_path(path)}\"></script>"
50
- end
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
51
103
  end
52
104
 
53
- def render_css
54
- fragments = @css.keys.sort.map do |medium|
55
- @css[medium].map do |path|
56
- "<link href=\"#{absolute_path(path)}\" type=\"text/css\" media=\"#{medium}\" rel=\"stylesheet\" />"
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}"
57
128
  end
129
+
130
+ inputs << "<input#{flatatt(input_attrs)} />"
58
131
  end
59
- fragments.empty? ? fragments : fragments.inject(&:+)
60
- end
61
132
 
62
- def absolute_path(path)
63
- path =~ /^(\/|https?:\/\/)/ ? path : URI.join(media_path, path)
133
+ mark_safe(inputs.join("\n"))
134
+ end
64
135
  end
65
136
 
66
- def [](name)
67
- raise IndexError("Unknown media type '#{name}'") unless
68
- MEDIA_TYPES.include?(name)
69
- Media.new(name => instance_variable_get("@{name}"))
70
- end
137
+ class FileInput < Input
138
+ def render(name, value, attrs=nil)
139
+ super(name, nil, attrs)
140
+ end
71
141
 
72
- def add_type(type, data)
73
- send("add_#{type}", data)
74
- end
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
75
149
 
76
- def add_js(data)
77
- @js += data.select {|path| !@js.include?(path)}
150
+ def input_type
151
+ 'file'
152
+ end
153
+
154
+ def needs_multipart?
155
+ true
156
+ end
78
157
  end
79
158
 
80
- def add_css(data)
81
- data.each do |medium, paths|
82
- @css[medium] ||= []
83
- css = @css[medium]
84
- css.concat(paths.select {|path| !css.include?(path)})
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<br />%(input_text)s: %(input)s'
176
+ end
177
+
178
+ def template_with_clear
179
+ '%(clear)s <label for="%(clear_checkbox_id)s">%(clear_checkbox_label)s</label>'
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] = '<a href="%s">%s</a>' % [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
85
214
  end
86
- end
87
215
 
88
- def +(other)
89
- combined = Media.new
90
- MEDIA_TYPES.each do |name|
91
- combined.add_type(name, instance_variable_get("@#{name}"))
92
- combined.add_type(name, other.instance_variable_get("@#{name}"))
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
93
236
  end
94
- combined
237
+ end
95
238
  end
96
- end
97
239
 
98
- class Widget
99
- include Utils
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
100
245
 
101
- class << self
102
- attr_accessor :needs_multipart_form, :is_hidden
246
+ super(default_attrs)
247
+ end
103
248
 
104
- def inherited(c)
105
- super(c)
106
- c.is_hidden = is_hidden
107
- c.needs_multipart_form = needs_multipart_form
249
+ def render(name, value, attrs=nil)
250
+ value ||= ''
251
+ final_attrs = build_attrs(attrs, name: name)
252
+ mark_safe("<textarea#{flatatt(final_attrs)}>#{conditional_escape(value.to_s)}</textarea>")
108
253
  end
109
254
  end
110
255
 
111
- self.needs_multipart_form = false
112
- self.is_hidden = false
256
+ # DateInput
257
+ # DateTimeInput
258
+ # TimeInput
113
259
 
114
- attr_reader :attrs
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
115
265
 
116
- def initialize(attrs=nil)
117
- @attrs = attrs.nil? ? {} : attrs.dup
118
- end
266
+ def render(name, value, attrs=nil)
267
+ final_attrs = build_attrs(attrs, type: 'checkbox', name: name.to_s)
119
268
 
120
- def initialize_copy(original)
121
- super(original)
122
- @attrs = original.attrs.dup
123
- end
269
+ # FIXME: this is horrible, shouldn't just rescue everything
270
+ result = @check_test.call(value) rescue false
124
271
 
125
- def render(name, value, attrs=nil)
126
- raise NotImplementedError
127
- end
272
+ if result
273
+ final_attrs[:checked] = 'checked'
274
+ end
128
275
 
129
- def build_attrs(extra_attrs=nil, options={})
130
- attrs = @attrs.merge(options)
131
- attrs.update(extra_attrs) if extra_attrs
132
- attrs
133
- end
276
+ unless ['', true, false, nil].include?(value)
277
+ final_attrs[:value] = value.to_s
278
+ end
134
279
 
135
- def value_from_datahash(data, files, name)
136
- data[name]
137
- end
280
+ mark_safe("<input#{flatatt(final_attrs)} />")
281
+ end
138
282
 
139
- def self.id_for_label(id_)
140
- id_
141
- end
283
+ def value_from_formdata(data, name)
284
+ if data.include?(name)
285
+ value = data[name]
142
286
 
143
- def has_changed?(initial, data)
144
- data_value = data || ''
145
- initial_value = initial || ''
146
- initial_value != data_value
147
- end
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
148
300
 
149
- def hidden?
150
- self.class.is_hidden
301
+ def has_changed(initial, data)
302
+ make_bool(initial) != make_bool(data)
303
+ end
151
304
  end
152
305
 
153
- def media
154
- Media.new
155
- end
156
- end
306
+ class Select < Widget
307
+ attr_accessor :choices
157
308
 
158
- class Input < Widget
159
- class << self
160
- attr_accessor :input_type
309
+ def initialize(attrs=nil, choices=[])
310
+ super(attrs)
311
+ @choices = choices.collect
312
+ end
161
313
 
162
- # Copy data to the child class
163
- def inherited(c)
164
- super(c)
165
- c.input_type = input_type.dup if input_type
314
+ def render(name, value, attrs=nil, choices=[])
315
+ value = '' if value.nil?
316
+ final_attrs = build_attrs(attrs, name: name)
317
+ output = ["<select#{flatatt(final_attrs)}>"]
318
+ options = render_options(choices, [value])
319
+ output << options if options && !options.empty?
320
+ output << '</select>'
321
+ mark_safe(output.join("\n"))
166
322
  end
167
- end
168
323
 
169
- self.is_hidden = false
170
- self.input_type = nil
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 << '<optgroup label="%s">' % escape(option_value.to_s)
331
+ option_label.each do |option|
332
+ val, label = option
333
+ output << render_option(val, label, selected_choices)
334
+ end
335
+ output << '</optgroup>'
336
+ else
337
+ output << render_option(option_value, option_label,
338
+ selected_choices)
339
+ end
340
+ end
341
+ output.join("\n")
342
+ end
171
343
 
172
- def render(name, value, attrs=nil)
173
- value ||= ''
174
- final_attrs = build_attrs(attrs,
175
- :type => self.class.input_type.to_s,
176
- :name => name.to_s)
177
- final_attrs[:value] = value.to_s unless value == ''
178
- mark_safe("<input#{flatatt(final_attrs)} />")
179
- end
180
- end
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
181
348
 
182
- class TextInput < Input
183
- self.input_type = 'text'
184
- end
349
+ if selected_choices.include?(option_attributes[:value])
350
+ option_attributes[:selected] = "selected"
351
+ end
185
352
 
186
- class PasswordInput < Input
187
- self.input_type = 'password'
353
+ attributes = []
188
354
 
189
- def initialize(attrs=nil, render_value=true)
190
- super(attrs)
191
- @render_value = render_value
192
- end
355
+ option_attributes.each_pair do |attr_name, attr_value|
356
+ attributes << %Q[#{attr_name.to_s}="#{escape(attr_value.to_s)}"]
357
+ end
193
358
 
194
- def render(name, value, attrs=nil)
195
- value = nil unless @render_value
196
- super(name, value, attrs)
359
+ "<option #{attributes.join(' ')}>#{conditional_escape(option_label.to_s)}</option>"
360
+ end
197
361
  end
198
- end
199
362
 
200
- class HiddenInput < Input
201
- self.input_type = 'hidden'
202
- self.is_hidden = true
203
- end
363
+ class NullBooleanSelect < Select
364
+ def initialize(attrs=nil)
365
+ choices = [['1', 'Unknown'], ['2', 'Yes'], ['3', 'No']]
366
+ super(attrs, choices)
367
+ end
204
368
 
205
- class MultipleHiddenInput < HiddenInput
206
- # Used by choice fields
207
- attr_accessor :choices
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
208
377
 
209
- def initialize(attrs=nil, choices=[])
210
- super(attrs)
211
- # choices can be any enumerable
212
- @choices = choices
213
- end
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
214
385
 
215
- def render(name, value, attrs=nil, choices=[])
216
- value ||= []
217
- final_attrs = build_attrs(attrs, :type => self.class.input_type,
218
- :name => name)
219
- mark_safe(value.map do |v|
220
- rattrs = {:value => v.to_s}.merge(final_attrs)
221
- "<input#{flatatt(rattrs)} />"
222
- end.join("\n"))
223
- end
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
224
394
 
225
- def value_from_datahash(data, files, name)
226
- #if data.is_a?(MultiValueDict) || data.is_a?(MergeDict)
227
- # data.getlist(name)
228
- #else
229
- # data[name]
230
- #end
231
- data[name]
395
+ initial != data
396
+ end
232
397
  end
233
- end
234
398
 
235
- class FileInput < Input
236
- self.input_type = 'file'
237
- self.needs_multipart_form = true
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 = ["<select multiple=\"multiple\"#{flatatt(final_attrs)}>"]
404
+ options = render_options(choices, value)
405
+ output << options if options && !options.empty?
406
+ output << '</select>'
407
+ mark_safe(output.join("\n"))
408
+ end
238
409
 
239
- def render(name, value, attrs=nil)
240
- super(name, nil, attrs)
241
- end
410
+ def has_changed?(initial, data)
411
+ initial = [] if initial.nil?
412
+ data = [] if data.nil?
242
413
 
243
- def value_from_datahash(data, files, name)
244
- files.fetch(name, nil)
245
- end
414
+ if initial.length != data.length
415
+ return true
416
+ end
246
417
 
247
- def has_changed?(initial, data)
248
- data.nil?
418
+ Set.new(initial.map(&:to_s)) != Set.new(data.map(&:to_s))
419
+ end
249
420
  end
250
- end
251
421
 
252
- class Textarea < Widget
253
- def initialize(attrs=nil)
254
- # The 'rows' and 'cols' attributes are required for HTML correctness.
255
- @attrs = {:cols => '40', :rows => '10'}
256
- @attrs.merge!(attrs) if attrs
257
- end
422
+ class RadioInput
423
+ include Utils
258
424
 
259
- def render(name, value, attrs=nil)
260
- value ||= ''
261
- final_attrs = build_attrs(attrs, :name => name)
262
- mark_safe("<textarea#{flatatt(final_attrs)}>#{conditional_escape(value.to_s)}</textarea>")
263
- end
264
- end
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
265
433
 
266
- # DateInput
267
- # DateTimeInput
268
- # TimeInput
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("<label#{label_for}>#{tag} #{choice_label}</label>")
438
+ end
269
439
 
270
- class CheckboxInput < Widget
271
- def initialize(attrs=nil, check_test=nil)
272
- super(attrs)
273
- @check_test = check_test || lambda {|v| make_bool(v)}
274
- end
440
+ def checked?
441
+ @value == @choice_value
442
+ end
275
443
 
276
- def render(name, value, attrs=nil)
277
- final_attrs = build_attrs(attrs, :type => 'checkbox', :name => name.to_s)
278
- result = @check_test.call(value) rescue false
279
- final_attrs[:checked] = 'checked' if result
280
- final_attrs[:value] = value.to_s unless
281
- ['', true, false, nil].include?(value)
282
- mark_safe("<input#{flatatt(final_attrs)} />")
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("<input#{flatatt(final_attrs)} />")
450
+ end
283
451
  end
284
452
 
285
- def value_from_datahash(data, files, name)
286
- data.include?(name) ? super(data, files, name) : false
287
- end
453
+ class RadioFieldRenderer
454
+ include Utils
288
455
 
289
- def has_changed(initial, data)
290
- make_bool(initial) != make_bool(data)
291
- end
292
- end
456
+ def initialize(name, value, attrs, choices)
457
+ @name = name
458
+ @value = value
459
+ @attrs = attrs
460
+ @choices = choices
461
+ end
293
462
 
294
- class Select < Widget
295
- attr_accessor :choices
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
296
468
 
297
- def initialize(attrs=nil, choices=[])
298
- super(attrs)
299
- @choices = choices.collect
300
- end
469
+ def [](idx)
470
+ choice = @choices[idx]
471
+ RadioInput.new(@name, @value, @attrs.dup, choice, idx)
472
+ end
301
473
 
302
- def render(name, value, attrs=nil, choices=[])
303
- value = '' if value.nil?
304
- final_attrs = build_attrs(attrs, :name => name)
305
- output = ["<select#{flatatt(final_attrs)}>"]
306
- options = render_options(choices, [value])
307
- output << options if options && !options.empty?
308
- output << '</select>'
309
- mark_safe(output.join("\n"))
310
- end
474
+ def to_s
475
+ render
476
+ end
311
477
 
312
- def render_options(choices, selected_choices)
313
- selected_choices = selected_choices.map(&:to_s).uniq
314
- output = []
315
- (@choices + choices).each do |option_value, option_label|
316
- if option_label.is_a?(Array)
317
- output << '<optgroup label="%s">' % escape(option_value.to_s)
318
- option_label.each do |option|
319
- val, label = option
320
- output << render_option(val, label, selected_choices)
321
- end
322
- output << '</optgroup>'
323
- else
324
- output << render_option(option_value, option_label,
325
- selected_choices)
326
- end
327
- end
328
- output.join("\n")
478
+ def render
479
+ list = []
480
+ each {|radio| list << "<li>#{radio}</li>"}
481
+ mark_safe("<ul>\n#{list.join("\n")}\n</ul>")
482
+ end
329
483
  end
330
484
 
331
- def render_option(option_value, option_label, selected_choices)
332
- option_value = option_value.to_s
333
- selected_html = selected_choices.include?(option_value) ? ' selected="selected"' : ''
334
- "<option value=\"#{escape(option_value)}\"#{selected_html}>#{conditional_escape(option_label.to_s)}</option>"
335
- end
336
- end
485
+ class RadioSelect < Select
486
+ def self.id_for_label(id_)
487
+ id_.empty? ? id_ : id_ + '_0'
488
+ end
337
489
 
338
- class NullBooleanSelect < Select
339
- def initialize(attrs=nil)
340
- choices = [['1', 'Unknown'], ['2', 'Yes'], ['3', 'No']]
341
- super(attrs, choices)
342
- end
490
+ def renderer
491
+ RadioFieldRenderer
492
+ end
343
493
 
344
- def render(name, value, attrs=nil, choices=[])
345
- value = case value
346
- when true, '2' then '2'
347
- when false, '3' then '3'
348
- else '1'
349
- end
350
- super(name, value, attrs, choices)
351
- end
494
+ def initialize(*args)
495
+ options = args.last.is_a?(Hash) ? args.last : {}
496
+ @renderer = options.fetch(:renderer, renderer)
497
+ super
498
+ end
352
499
 
353
- def value_from_datahash(data, files, name)
354
- value = data[name]
355
- case value
356
- when '2', true then true
357
- when '3', false then false
358
- else nil
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)
359
506
  end
360
- end
361
507
 
362
- def has_changed?(initial, data)
363
- make_bool(initial) != make_bool(data)
508
+ def render(name, value, attrs=nil, choices=[])
509
+ get_renderer(name, value, attrs, choices).render
510
+ end
364
511
  end
365
- end
366
512
 
367
- class SelectMultiple < Select
368
- def render(name, value, attrs=nil, choices=[])
369
- value = [] if value.nil?
370
- final_attrs = build_attrs(attrs, :name => name)
371
- output = ["<select multiple=\"multiple\"#{flatatt(final_attrs)}>"]
372
- options = render_options(choices, value)
373
- output << options if options && !options.empty?
374
- output << '</select>'
375
- mark_safe(output.join("\n"))
376
- end
513
+ class CheckboxSelectMultiple < SelectMultiple
514
+ def self.id_for_label(id_)
515
+ id_.empty? ? id_ : id_ + '_0'
516
+ end
377
517
 
378
- def value_from_datahash(data, files, name)
379
- #if data.is_a?(MultiValueDict) || data.is_a?(MergeDict)
380
- # data.getlist(name)
381
- #else
382
- # data[name]
383
- #end
384
- data[name]
385
- end
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 = ['<ul>']
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
386
535
 
387
- def has_changed?(initial, data)
388
- initial = [] if initial.nil?
389
- data = [] if data.nil?
390
- return true if initial.length != data.length
391
- initial.zip(data).each do |value1, value2|
392
- return true if value1.to_s != value2.to_s
393
- end
394
- false
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 << "<li><label#{label_for}>#{rendered_cb} #{opt_label}</label></li>"
542
+ end
543
+ output << '</ul>'
544
+ mark_safe(output.join("\n"))
545
+ end
395
546
  end
547
+
548
+ # TODO: MultiWidget < Widget
549
+ # TODO: SplitDateTimeWidget < MultiWidget
550
+ # TODO: SplitHiddenDateTimeWidget < SplitDateTimeWidget
551
+
396
552
  end
397
- end; end
553
+ end