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,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