bureaucrat 0.0.1

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.
@@ -0,0 +1,346 @@
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
21
+
22
+ def to_s
23
+ @field.show_hidden_initial ? as_widget + as_hidden(nil, true) : as_widget
24
+ end
25
+
26
+ def errors
27
+ @form.errors.fetch(@name, @form.error_class.new)
28
+ end
29
+
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, @field.initial)
38
+ data = data.call if data.respond_to?(:call)
39
+ else
40
+ if @field.is_a?(Fields::FileField) && @data.nil?
41
+ data = @form.initial.fetch(@name, @field.initial)
42
+ else
43
+ data = self.data
44
+ end
45
+ end
46
+
47
+ name = only_initial ? @html_initial_name : @html_name
48
+
49
+ widget.render(name.to_s, data, attrs)
50
+ end
51
+
52
+ def as_text(attrs=nil, only_initial=false)
53
+ as_widget(Widgets::TextInput.new, attrs, only_initial)
54
+ end
55
+
56
+ def as_textarea(attrs=nil, only_initial=false)
57
+ as_widget(Widgets::Textarea.new, attrs, only_initial)
58
+ end
59
+
60
+ def as_hidden(attrs=nil, only_initial=false)
61
+ as_widget(@field.hidden_widget, attrs, only_initial)
62
+ end
63
+
64
+ def data
65
+ @field.widget.value_from_datahash(@form.data, @form.files, @html_name)
66
+ end
67
+
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
72
+
73
+ if id_
74
+ attrs = attrs ? flattatt(attrs) : ''
75
+ contents = "<label for=\"#{Widgets::Widget.id_for_label(id_)}\"#{attrs}>#{contents}</label>"
76
+ end
77
+
78
+ mark_safe(contents)
79
+ end
80
+
81
+ def hidden?
82
+ @field.widget.hidden?
83
+ end
84
+
85
+ def auto_id
86
+ fauto_id = @form.auto_id
87
+ fauto_id ? fauto_id % @html_name : ''
88
+ end
89
+ end
90
+
91
+ class Form
92
+ include Utils
93
+ include Validation
94
+
95
+ class << self
96
+ attr_writer :base_fields
97
+
98
+ def base_fields
99
+ @base_fields ||= Utils::OrderedHash.new
100
+ end
101
+
102
+ def field(name, field_obj)
103
+ base_fields[name] = field_obj
104
+ end
105
+
106
+ # Copy data to the child class
107
+ def inherited(c)
108
+ super(c)
109
+ c.base_fields = base_fields.dup
110
+ end
111
+ end
112
+
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 = data ? data.dup : {}
120
+ @data.each {|k, v| @data[k.to_sym] ||= v}
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
134
+
135
+ def to_s
136
+ as_table
137
+ end
138
+
139
+ def [](name)
140
+ field = @fields[name] or return nil
141
+ BoundField.new(self, field, name)
142
+ end
143
+
144
+ def errors
145
+ full_clean if @errors.nil?
146
+ @errors
147
+ end
148
+
149
+ def valid?
150
+ @is_bound && (errors.nil? || errors.empty?)
151
+ end
152
+
153
+ def add_prefix(field_name)
154
+ @prefix ? :"#{@prefix}-#{field_name}" : field_name
155
+ end
156
+
157
+ def add_initial_prefix(field_name)
158
+ "initial-#{add_prefix(field_name)}"
159
+ end
160
+
161
+ def empty_permitted?
162
+ @empty_permitted
163
+ end
164
+
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
170
+
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
175
+
176
+ def as_p
177
+ html_output('<p>%(label)s %(field)s%(help_text)s</p>',
178
+ '%s', '</p>', ' %s', true)
179
+ end
180
+
181
+ def non_field_errors
182
+ errors.fetch(:__NON_FIELD_ERRORS, @error_class.new)
183
+ end
184
+
185
+ def full_clean
186
+ @errors = Fields::ErrorHash.new
187
+
188
+ return unless bound?
189
+
190
+ @cleaned_data = {}
191
+
192
+ return if empty_permitted? && !changed?
193
+
194
+ @fields.each do |name, field|
195
+ value = field.widget.value_from_datahash(@data, @files,
196
+ add_prefix(name))
197
+
198
+ begin
199
+ if field.is_a?(Fields::FileField)
200
+ initial = @initial.fetch(name, field.initial)
201
+ @cleaned_data[name] = field.clean(value, initial)
202
+ else
203
+ @cleaned_data[name] = field.clean(value)
204
+ end
205
+
206
+ clean_method = 'clean_%s' % name
207
+ @cleaned_data[name] = send(clean_method) if respond_to?(clean_method)
208
+ rescue Fields::FieldValidationError => e
209
+ @errors[name] = e.messages
210
+ @cleaned_data.clear
211
+ end
212
+ end
213
+
214
+ begin
215
+ @cleaned_data = clean
216
+ rescue Fields::FieldValidationError => e
217
+ @errors[:__NON_FIELD_ERRORS] = e.messages
218
+ end
219
+ @cleaned_data = nil if @errors && !@errors.empty?
220
+ end
221
+
222
+ def clean
223
+ @cleaned_data
224
+ end
225
+
226
+ def changed?
227
+ changed_data && !changed_data.empty?
228
+ end
229
+
230
+ def changed_data
231
+ if @changed_data.nil?
232
+ @changed_data = []
233
+
234
+ @fields.each do |name, field|
235
+ prefixed_name = add_prefix(name)
236
+ data_value = field.widget.value_from_datahash(@data, @files,
237
+ prefixed_name)
238
+ if !field.show_hidden_initial
239
+ initial_value = @initial.fetch(name, field.initial)
240
+ else
241
+ initial_prefixed_name = add_initial_prefix(name)
242
+ hidden_widget = field.hidden_widget.new
243
+ initial_value = hidden_widget.value_from_datahash(@data, @files,
244
+ initial_prefixed_name)
245
+ end
246
+
247
+ @changed_data << name if
248
+ field.widget.has_changed?(initial_value, data_value)
249
+ 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
+ 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
+
269
+ def visible_fields
270
+ @fields.select {|f| !f.hidden?}
271
+ end
272
+
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 = [], []
278
+
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)
283
+
284
+ mark_safe(output.join("\n"))
285
+ end
286
+
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
308
+
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
316
+ end
317
+ end
318
+
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
336
+ end
337
+ end
338
+
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
+ end
344
+
345
+ end
346
+ end; end
@@ -0,0 +1,256 @@
1
+ require 'bureaucrat/utils'
2
+ require 'bureaucrat/validation'
3
+ require 'bureaucrat/widgets'
4
+ require 'bureaucrat/forms'
5
+
6
+ # TODO: needs more testing
7
+ module Bureaucrat
8
+ module Formsets
9
+ TOTAL_FORM_COUNT = :'TOTAL_FORMS'
10
+ INITIAL_FORM_COUNT = :'INITIAL_FORMS'
11
+ ORDERING_FIELD_NAME = :'ORDER'
12
+ DELETION_FIELD_NAME = :'DELETE'
13
+
14
+ class ManagementForm < Forms::Form
15
+ include Fields
16
+ include Widgets
17
+
18
+ field TOTAL_FORM_COUNT, IntegerField.new(:widget => HiddenInput)
19
+ field INITIAL_FORM_COUNT, IntegerField.new(:widget => HiddenInput)
20
+ end
21
+
22
+ class BaseFormSet
23
+ include Utils
24
+ include Validation
25
+ include Fields
26
+ include Forms
27
+
28
+ def self.default_prefix
29
+ 'form'
30
+ end
31
+
32
+ attr_accessor :form, :extra, :can_order, :can_delete, :max_num
33
+
34
+ def initialize(data=nil, options={})
35
+ @is_bound = !data.nil?
36
+ @prefix = options.fetch(:prefix, self.class.default_prefix)
37
+ @auto_id = options.fetch(:auto_id, 'id_%s')
38
+ @data = data
39
+ @files = options[:files]
40
+ @initial = options[:initial]
41
+ @error_class = options.fetch(:error_class, ErrorList)
42
+ @errors = nil
43
+ @non_form_errors = nil
44
+
45
+ construct_forms
46
+ end
47
+
48
+ def to_s
49
+ as_table
50
+ end
51
+
52
+ def management_form
53
+ if @data || @files
54
+ form = ManagementForm.new(@data, :auto_id => @auto_id,
55
+ :prefix => @prefix)
56
+ raise ValidationError.new('ManagementForm data is missing or has been tampered with') unless
57
+ form.valid?
58
+ else
59
+ form = ManagementForm.new(nil, :auto_id => @auto_id,
60
+ :prefix => @prefix,
61
+ :initial => {
62
+ TOTAL_FORM_COUNT => total_form_count,
63
+ INITIAL_FORM_COUNT => initial_form_count
64
+ })
65
+ end
66
+ form
67
+ end
68
+
69
+ def total_form_count
70
+ if @data || @files
71
+ management_form.cleaned_data[TOTAL_FORM_COUNT]
72
+ else
73
+ n = initial_form_count + self.extra
74
+ (n > self.max_num && self.max_num > 0) ? self.max_num : n
75
+ end
76
+ end
77
+
78
+ def initial_form_count
79
+ if @data || @files
80
+ management_form.cleaned_data[INITIAL_FORM_COUNT]
81
+ else
82
+ n = @initial ? @initial.length : 0
83
+ (n > self.max_num && self.max_num > 0) ? self.max_num : n
84
+ end
85
+ end
86
+
87
+ def construct_forms
88
+ @forms = (0...total_form_count).map { |i| construct_form(i) }
89
+ end
90
+
91
+ def construct_form(i, options={})
92
+ defaults = {:auto_id => @auto_id, :prefix => add_prefix(i)}
93
+ defaults[:files] = @files if @files
94
+ defaults[:initial] = @initial[i] if @initial && @initial[i]
95
+
96
+ # Allow extra forms to be empty.
97
+ defaults[:empty_permitted] = true if i >= initial_form_count
98
+ defaults.merge!(options)
99
+ form = self.form.new(@data, defaults)
100
+ add_fields(form, i)
101
+ form
102
+ end
103
+
104
+ def initial_forms
105
+ @forms[0, initial_form_count]
106
+ end
107
+
108
+ def extra_forms
109
+ n = initial_form_count
110
+ @forms[n, @forms.length - n]
111
+ end
112
+
113
+ # Maybe this should just go away?
114
+ def cleaned_data
115
+ unless valid?
116
+ raise NoMethodError.new("'#{self.class.name}' object has no method 'cleaned_data'")
117
+ end
118
+ @forms.collect(&:cleaned_data)
119
+ end
120
+
121
+ def deleted_forms
122
+ unless valid? && self.can_delete
123
+ raise NoMethodError.new("'#{self.class.name}' object has no method 'deleted_forms'")
124
+ end
125
+
126
+ if @deleted_form_indexes.nil?
127
+ @deleted_form_indexes = (0...total_form_count).select do |i|
128
+ form = @forms[i]
129
+ (i < initial_form_count || form.changed?) && form.cleaned_data[DELETION_FIELD_NAME]
130
+ end
131
+ end
132
+ @deleted_form_indexes.map {|i| @forms[i]}
133
+ end
134
+
135
+ def ordered_forms
136
+ unless valid? && self.can_order
137
+ raise NoMethodError.new("'#{self.class.name}' object has no method 'ordered_forms'")
138
+ end
139
+
140
+ if @ordering.nil?
141
+ @ordering = (0...total_form_count).map do |i|
142
+ form = @forms[i]
143
+ next if i >= initial_form_count && !form.changed?
144
+ next if self.can_delete && form.cleaned_data[DELETION_FIELD_NAME]
145
+ [i, form.cleaned_data[ORDERING_FIELD_NAME]]
146
+ end.compact
147
+ @ordering.sort! do |a, b|
148
+ if x[1].nil? then 1
149
+ elsif y[1].nil? then -1
150
+ else x[1] - y[1]
151
+ end
152
+ end
153
+ end
154
+
155
+ @ordering.map {|i| @forms[i.first]}
156
+ end
157
+
158
+ def non_form_errors
159
+ @non_form_errors || @error_class.new
160
+ end
161
+
162
+ def errors
163
+ full_clean if @errors.nil?
164
+ @errors
165
+ end
166
+
167
+ def valid?
168
+ return false unless @is_bound
169
+ forms_valid = true
170
+ (0...total_form_count).each do |i|
171
+ form = @forms[i]
172
+ if self.can_delete
173
+ field = form.fields[DELETION_FIELD_NAME]
174
+ raw_value = form.send(:raw_value, DELETION_FIELD_NAME)
175
+ should_delete = field.clean(raw_value)
176
+ next if should_delete
177
+ end
178
+ forms_valid = false unless errors[i].empty?
179
+ end
180
+ forms_valid && non_form_errors.empty?
181
+ end
182
+
183
+ def full_clean
184
+ if @is_bound
185
+ @errors = @forms.collect(&:errors)
186
+
187
+ begin
188
+ self.clean
189
+ rescue ValidationError => e
190
+ @non_form_errors = e.messages
191
+ end
192
+ else
193
+ @errors = []
194
+ end
195
+ end
196
+
197
+ def clean
198
+ end
199
+
200
+ def add_fields(form, index)
201
+ if self.can_order
202
+ attrs = {:label => 'Order', :required => false}
203
+ attrs[:initial] = index + 1 if index < initial_form_count
204
+ form.fields[ORDERING_FIELD_NAME] = IntegerField.new(attrs)
205
+ end
206
+ if self.can_delete
207
+ field = BooleanField.new(:label => 'Delete', :required => false)
208
+ form.fields[DELETION_FIELD_NAME] = field
209
+ end
210
+ end
211
+
212
+ def add_prefix(index)
213
+ '%s-%s' % [@prefix, index]
214
+ end
215
+
216
+ def multipart?
217
+ @forms && @forms.first.multipart?
218
+ end
219
+
220
+ def media
221
+ @forms ? @forms.first.media : Media.new
222
+ end
223
+
224
+ def as_table
225
+ forms = @forms.map(&:as_table).join(' ')
226
+ mark_safe([management_form.to_s, forms].join("\n"))
227
+ end
228
+ end
229
+
230
+ module_function
231
+
232
+ def make_formset_class(form, options={})
233
+ formset = options.fetch(:formset, BaseFormSet)
234
+ attrs = {
235
+ :form => form,
236
+ :extra => options.fetch(:extra, 1),
237
+ :can_order => options.fetch(:can_order, false),
238
+ :can_delete => options.fetch(:can_delete, false),
239
+ :max_num => options.fetch(:max_num, 0)
240
+ }
241
+ Class.new(formset) do
242
+ attrs.each do |name, value|
243
+ define_method(name) { value }
244
+ end
245
+ end
246
+ end
247
+
248
+ def all_valid?(formsets)
249
+ valid = true
250
+ formsets.each do |formset|
251
+ valid = false unless formset.valid?
252
+ end
253
+ valid
254
+ end
255
+
256
+ end; end
@@ -0,0 +1,59 @@
1
+ require 'bureaucrat/fields'
2
+
3
+ module Bureaucrat
4
+ module Quickfields
5
+ include Fields
6
+
7
+ def hide(name)
8
+ base_fields[name].widget = Widgets::HiddenInput.new
9
+ end
10
+
11
+ def string(name, options={})
12
+ field name, CharField.new(options)
13
+ end
14
+
15
+ def password(name, options={})
16
+ field name, CharField.new(options.merge(:widget => Widgets::PasswordInput.new))
17
+ end
18
+
19
+ def integer(name, options={})
20
+ field name, IntegerField.new(options)
21
+ end
22
+
23
+ def decimal(name, options={})
24
+ field name, BigDecimalField.new(options)
25
+ end
26
+
27
+ def regex(name, regexp, options={})
28
+ field name, RegexField.new(regexp, options)
29
+ end
30
+
31
+ def email(name, options={})
32
+ field name, EmailField.new(options)
33
+ end
34
+
35
+ def file(name, options={})
36
+ field name, FileField.new(options)
37
+ end
38
+
39
+ def boolean(name, options={})
40
+ field name, BooleanField.new(options)
41
+ end
42
+
43
+ def null_boolean(name, options={})
44
+ field name, NullBooleanField.new(options)
45
+ end
46
+
47
+ def choice(name, choices=[], options={})
48
+ field name, ChoiceField.new(choices, options)
49
+ end
50
+
51
+ def typed_choice(name, choices=[], options={})
52
+ field name, TypedChoiceField.new(choices, options)
53
+ end
54
+
55
+ def multiple_choice(name, choices=[], options={})
56
+ field name, MultipleChoiceField.new(choices, options)
57
+ end
58
+
59
+ end; end