bureaucrat 0.0.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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