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,199 +1,284 @@
1
- require 'bureaucrat/utils'
2
- require 'bureaucrat/validation'
3
- require 'bureaucrat/widgets'
4
- require 'bureaucrat/fields'
5
-
6
- module Bureaucrat; module Forms
7
- class BoundField
8
- include Utils
9
-
10
- attr_accessor :label
11
-
12
- def initialize(form, field, name)
13
- @form = form
14
- @field = field
15
- @name = name
16
- @html_name = form.add_prefix(name).to_sym
17
- @html_initial_name = form.add_initial_prefix(name).to_sym
18
- @label = @field.label || pretty_name(name)
19
- @help_text = @field.help_text || ''
20
- end
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
21
23
 
22
- def to_s
23
- @field.show_hidden_initial ? as_widget + as_hidden(nil, true) : as_widget
24
- end
24
+ # Renders the field.
25
+ def to_s
26
+ @field.show_hidden_initial ? as_widget + as_hidden(nil, true) : as_widget
27
+ end
25
28
 
26
- def errors
27
- @form.errors.fetch(@name, @form.error_class.new)
28
- end
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)
29
41
 
30
- def as_widget(widget=nil, attrs=nil, only_initial=false)
31
- widget ||= @field.widget
32
- attrs ||= {}
33
- auto_id = self.auto_id
34
- attrs[:id] ||= auto_id if auto_id && !widget.attrs.key?(:id)
35
-
36
- if !@form.bound?
37
- data = @form.initial.fetch(@name.to_s, @field.initial)
38
- data = data.call if data.respond_to?(:call)
39
- else
40
- if @field.is_a?(Fields::FileField) && @data.nil?
42
+ if !@form.bound?
41
43
  data = @form.initial.fetch(@name, @field.initial)
44
+ data = data.call if data.respond_to?(:call)
42
45
  else
43
- data = self.data
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
44
51
  end
52
+
53
+ name = only_initial ? @html_initial_name : @html_name
54
+ widget.render(name.to_s, data, attrs)
45
55
  end
46
56
 
47
- name = only_initial ? @html_initial_name : @html_name
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
48
61
 
49
- widget.render(name.to_s, data, attrs)
50
- end
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
51
66
 
52
- def as_text(attrs=nil, only_initial=false)
53
- as_widget(Widgets::TextInput.new, attrs, only_initial)
54
- end
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
55
71
 
56
- def as_textarea(attrs=nil, only_initial=false)
57
- as_widget(Widgets::Textarea.new, attrs, only_initial)
58
- end
72
+ # The data associated to this field.
73
+ def data
74
+ @field.widget.value_from_formdata(@form.data, @html_name)
75
+ end
59
76
 
60
- def as_hidden(attrs=nil, only_initial=false)
61
- as_widget(@field.hidden_widget, attrs, only_initial)
62
- end
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.
63
80
 
64
- def data
65
- @field.widget.value_from_datahash(@form.data, @form.files, @html_name)
66
- end
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
67
98
 
68
- def label_tag(contents=nil, attrs=nil)
69
- contents ||= conditional_escape(@label)
70
- widget = @field.widget
71
- id_ = widget.attrs[:id] || self.auto_id
99
+ if id_
100
+ attrs = attrs ? flatatt(attrs) : ''
101
+ contents = "<label for=\"#{Widgets::Widget.id_for_label(id_)}\"#{attrs}>#{contents}</label>"
102
+ end
72
103
 
73
- if id_
74
- attrs = attrs ? flattatt(attrs) : ''
75
- contents = "<label for=\"#{Widgets::Widget.id_for_label(id_)}\"#{attrs}>#{contents}</label>"
104
+ mark_safe(contents)
76
105
  end
77
106
 
78
- mark_safe(contents)
79
- end
107
+ def css_classes(extra_classes = nil)
108
+ # Returns a string of space-separated CSS classes for this field.
80
109
 
81
- def hidden?
82
- @field.widget.hidden?
83
- end
110
+ if extra_classes.respond_to?(:split)
111
+ extra_classes = extra_classes.split
112
+ end
84
113
 
85
- def auto_id
86
- fauto_id = @form.auto_id
87
- fauto_id ? fauto_id % @html_name : ''
88
- end
89
- end
114
+ extra_classes = Set.new(extra_classes)
90
115
 
91
- class Form
92
- include Utils
93
- include Validation
116
+ if !errors.empty? && !Utils.blank_value?(form.error_css_class)
117
+ extra_classes << form.error_css_class
118
+ end
94
119
 
95
- class << self
96
- attr_writer :base_fields
120
+ if field.required && !Utils.blank_value?(form.required_css_class)
121
+ extra_classes << form.required_css_class
122
+ end
97
123
 
98
- def base_fields
99
- @base_fields ||= Utils::OrderedHash.new
124
+ extra_classes.to_a.join(' ')
100
125
  end
101
126
 
102
- def field(name, field_obj)
103
- base_fields[name] = field_obj
127
+ # true if the widget for this field is of the hidden kind.
128
+ def hidden?
129
+ @field.widget.hidden?
104
130
  end
105
131
 
106
- # Copy data to the child class
107
- def inherited(c)
108
- super(c)
109
- c.base_fields = base_fields.dup
132
+ # Generates the id for this field.
133
+ def auto_id
134
+ fauto_id = @form.auto_id
135
+ fauto_id ? fauto_id % @html_name : ''
110
136
  end
111
137
  end
112
138
 
113
- attr_accessor :error_class, :auto_id, :initial, :data, :files, :cleaned_data
114
-
115
- def bound? ; @is_bound; end
116
-
117
- def initialize(data=nil, options={})
118
- @is_bound = !data.nil?
119
- @data = {}
120
- data.each {|k, v| @data[k.to_sym] = @data[k] = v} if data
121
- @files = options.fetch(:files, {})
122
- @auto_id = options.fetch(:auto_id, 'id_%s')
123
- @prefix = options[:prefix]
124
- @initial = options.fetch(:initial, {})
125
- @error_class = options.fetch(:error_class, Fields::ErrorList)
126
- @label_suffix = options.fetch(:label_suffix, ':')
127
- @empty_permitted = options.fetch(:empty_permitted, false)
128
- @errors = nil
129
- @changed_data = nil
130
-
131
- @fields = self.class.base_fields.dup
132
- @fields.each { |key, value| @fields[key] = value.dup }
133
- end
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.
134
147
 
135
- def to_s
136
- as_table
137
- end
148
+ class Form
149
+ include Utils
138
150
 
139
- def [](name)
140
- field = @fields[name] or return nil
141
- BoundField.new(self, field, name)
142
- end
151
+ # Fields associated to the form class
152
+ def self.base_fields
153
+ @base_fields ||= {}
154
+ end
143
155
 
144
- def errors
145
- full_clean if @errors.nil?
146
- @errors
147
- end
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
148
160
 
149
- def valid?
150
- @is_bound && (errors.nil? || errors.empty?)
151
- end
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
152
166
 
153
- def add_prefix(field_name)
154
- @prefix ? :"#{@prefix}-#{field_name}" : field_name
155
- end
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
156
222
 
157
- def add_initial_prefix(field_name)
158
- "initial-#{add_prefix(field_name)}"
159
- end
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
160
229
 
161
- def empty_permitted?
162
- @empty_permitted
163
- end
230
+ # Access a named field
231
+ def [](name)
232
+ field = @fields[name] or return nil
233
+ BoundField.new(self, field, name)
234
+ end
164
235
 
165
- def as_table
166
- html_output('<tr><th>%(label)s</th><td>%(errors)s%(field)s%(help_text)s</td></tr>',
167
- '<tr><td colspan="2">%s</td></tr>', '</td></tr>',
168
- '<br />%s', false)
169
- end
236
+ # Errors for this forms (runs validations)
237
+ def errors
238
+ full_clean if @errors.nil?
239
+ @errors
240
+ end
170
241
 
171
- def as_ul
172
- html_output('<li>%(errors)s%(label)s %(field)s%(help_text)s</li>',
173
- '<li>%s</li>', '</li>', ' %s', false)
174
- end
242
+ # Perform validation and returns true if there are no errors
243
+ def valid?
244
+ @is_bound && (errors.nil? || errors.empty?)
245
+ end
175
246
 
176
- def as_p
177
- html_output('<p>%(label)s %(field)s%(help_text)s</p>',
178
- '%s', '</p>', ' %s', true)
179
- end
247
+ # Generates a prefix for field named +field_name+
248
+ def add_prefix(field_name)
249
+ @prefix ? "#{@prefix}-#{field_name}" : field_name
250
+ end
180
251
 
181
- def non_field_errors
182
- errors.fetch(:__NON_FIELD_ERRORS, @error_class.new)
183
- end
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
184
261
 
185
- def full_clean
186
- @errors = Fields::ErrorHash.new
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
187
272
 
188
- return unless bound?
273
+ return unless bound?
189
274
 
190
- @cleaned_data = {}
275
+ @cleaned_data = StringAccessHash.new
191
276
 
192
- return if empty_permitted? && !changed?
277
+ return if empty_permitted? && !changed?
193
278
 
194
- @fields.each do |name, field|
195
- value = field.widget.value_from_datahash(@data, @files,
196
- add_prefix(name))
279
+ @fields.each do |name, field|
280
+ value = field.widget.
281
+ value_from_formdata(@data, add_prefix(name))
197
282
 
198
283
  begin
199
284
  if field.is_a?(Fields::FileField)
@@ -205,142 +290,96 @@ module Bureaucrat; module Forms
205
290
 
206
291
  clean_method = 'clean_%s' % name
207
292
  @cleaned_data[name] = send(clean_method) if respond_to?(clean_method)
208
- rescue Fields::FieldValidationError => e
293
+ rescue ValidationError => e
209
294
  @errors[name] = e.messages
210
- @cleaned_data.clear
295
+ @cleaned_data.delete(name)
211
296
  end
212
297
  end
213
298
 
214
- begin
215
- @cleaned_data = clean
216
- rescue Fields::FieldValidationError => e
217
- @errors[:__NON_FIELD_ERRORS] = e.messages
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?
218
305
  end
219
- @cleaned_data = nil if @errors && !@errors.empty?
220
- end
221
306
 
222
- def clean
223
- @cleaned_data
224
- end
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
225
312
 
226
- def changed?
227
- changed_data && !changed_data.empty?
228
- end
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
229
317
 
230
- def changed_data
231
- if @changed_data.nil?
232
- @changed_data = []
318
+ # List names for fields that have changed data
319
+ def changed_data
320
+ if @changed_data.nil?
321
+ @changed_data = []
233
322
 
234
- @fields.each do |name, field|
323
+ @fields.each do |name, field|
235
324
  prefixed_name = add_prefix(name)
236
- data_value = field.widget.value_from_datahash(@data, @files,
237
- prefixed_name)
325
+ data_value = field.widget.
326
+ value_from_formdata(@data, prefixed_name)
327
+
238
328
  if !field.show_hidden_initial
239
329
  initial_value = @initial.fetch(name, field.initial)
240
330
  else
241
331
  initial_prefixed_name = add_initial_prefix(name)
242
332
  hidden_widget = field.hidden_widget.new
243
- initial_value = hidden_widget.value_from_datahash(@data, @files,
244
- initial_prefixed_name)
333
+ initial_value = hidden_widget.
334
+ value_from_formdata(@data, initial_prefixed_name)
245
335
  end
246
336
 
247
337
  @changed_data << name if
248
338
  field.widget.has_changed?(initial_value, data_value)
249
339
  end
250
- end
251
-
252
- @changed_data
253
- end
254
-
255
- def media
256
- @fields.values.inject(Widgets::Media.new) do |media, field|
257
- media + field.widget.media
258
340
  end
259
- end
260
-
261
- def multipart?
262
- @fields.any? {|f| f.widgetneeds_multipart_form?}
263
- end
264
-
265
- def hidden_fields
266
- @fields.select {|f| f.hidden?}
267
- end
268
341
 
269
- def visible_fields
270
- @fields.select {|f| !f.hidden?}
271
- end
342
+ @changed_data
343
+ end
272
344
 
273
- private
274
- def html_output(normal_row, error_row, row_ender, help_text_html,
275
- errors_on_separate_row)
276
- top_errors = non_field_errors
277
- output, hidden_fields = [], []
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
278
350
 
279
- add_fields_output(output, hidden_fields, normal_row, error_row,
280
- help_text_html, errors_on_separate_row, top_errors)
281
- output = [error_row % top_errors] + output unless top_errors.empty?
282
- add_hidden_fields_output(output, hidden_fields, row_ender)
351
+ # List of hidden fields.
352
+ def hidden_fields
353
+ @fields.select {|f| f.hidden?}
354
+ end
283
355
 
284
- mark_safe(output.join("\n"))
285
- end
356
+ # List of visible fields
357
+ def visible_fields
358
+ @fields.select {|f| !f.hidden?}
359
+ end
286
360
 
287
- def add_fields_output(output, hidden_fields, normal_row, error_row,
288
- help_text_html, errors_on_separate_row,
289
- top_errors)
290
- @fields.each do |name, field|
291
- bf = BoundField.new(self, field, name)
292
- bf_errors = @error_class.new(bf.errors.map {|e| conditional_escape(e)})
293
- if bf.hidden?
294
- top_errors += bf_errors.map do |e|
295
- "(Hidden field #{name}) #{e.to_s}"
296
- end unless bf_errors.empty?
297
- hidden_fields << bf.to_s
298
- else
299
- output << error_row % bf_errors if
300
- errors_on_separate_row && !bf_errors.empty?
301
-
302
- label = ''
303
- unless bf.label.nil? || bf.label.empty?
304
- label = conditional_escape(bf.label)
305
- label += @label_suffix if @label_suffix && label[-1,1] !~ /[:?.!]/
306
- label = bf.label_tag(label)
307
- end
361
+ # Attributes for labels, override in subclasses to customize behaviour
362
+ def label_attributes(name, field)
363
+ {}
364
+ end
308
365
 
309
- help_text = field.help_text.empty? ? '' : help_text_html % field.help_text
310
- vars = {
311
- :errors => bf_errors, :label => label,
312
- :field => bf, :help_text => help_text
313
- }
314
- output << format_string(normal_row, vars)
315
- end
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])
316
370
  end
317
- end
371
+ end
318
372
 
319
- def add_hidden_fields_output(output, hidden_fields, row_ender)
320
- unless hidden_fields.empty?
321
- str_hidden = hidden_fields.join('')
322
-
323
- unless output.empty?
324
- last_row = output[-1]
325
- if last_row !~ /#{row_ender}$/
326
- vars = {
327
- :errors => '', :label => '', :field => '', :help_text => ''
328
- }
329
- last_row = format_string(normal_row, vars)
330
- output << last_row
331
- end
332
- output[-1] = last_row[0...-row_ender.length] + str_hidden + row_ender
333
- else
334
- output << str_hidden
335
- end
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)
336
381
  end
337
- end
338
382
 
339
- def raw_value(fieldname)
340
- field = @fields.fetch(fieldname)
341
- prefix = add_prefix(fieldname)
342
- field.widget.value_from_datahash(@data, @files, prefix)
343
383
  end
344
-
345
384
  end
346
- end; end
385
+ end