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,397 @@
1
+ require 'uri'
2
+
3
+ require 'bureaucrat/utils'
4
+
5
+ module Bureaucrat
6
+ module Widgets
7
+ class Media
8
+ include Utils
9
+
10
+ MEDIA_TYPES = [:css, :js]
11
+ DEFAULT_MEDIA_PATH = 'http://localhost'
12
+
13
+ attr_accessor :media_path
14
+
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 = []
20
+
21
+ MEDIA_TYPES.each do |name|
22
+ data = media_attrs[name]
23
+ add_type(name, data) if data
24
+ end
25
+ end
26
+
27
+ def to_s
28
+ render
29
+ end
30
+
31
+ def to_hash
32
+ hash = {}
33
+ MEDIA_TYPES.each {|name| hash[name] = instance_variable_get("@#{name}")}
34
+ hash
35
+ end
36
+
37
+ def render
38
+ mark_safe(MEDIA_TYPES.map do |name|
39
+ render_type(name)
40
+ end.inject(&:+).join("\n"))
41
+ end
42
+
43
+ def render_type(type)
44
+ send("render_#{type}")
45
+ end
46
+
47
+ def render_js
48
+ @js.map do |path|
49
+ "<script type=\"text/javascript\" src=\"#{absolute_path(path)}\"></script>"
50
+ end
51
+ end
52
+
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\" />"
57
+ end
58
+ end
59
+ fragments.empty? ? fragments : fragments.inject(&:+)
60
+ end
61
+
62
+ def absolute_path(path)
63
+ path =~ /^(\/|https?:\/\/)/ ? path : URI.join(media_path, path)
64
+ end
65
+
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
71
+
72
+ def add_type(type, data)
73
+ send("add_#{type}", data)
74
+ end
75
+
76
+ def add_js(data)
77
+ @js += data.select {|path| !@js.include?(path)}
78
+ end
79
+
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)})
85
+ end
86
+ end
87
+
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}"))
93
+ end
94
+ combined
95
+ end
96
+ end
97
+
98
+ class Widget
99
+ include Utils
100
+
101
+ class << self
102
+ attr_accessor :needs_multipart_form, :is_hidden
103
+
104
+ def inherited(c)
105
+ super(c)
106
+ c.is_hidden = is_hidden
107
+ c.needs_multipart_form = needs_multipart_form
108
+ end
109
+ end
110
+
111
+ self.needs_multipart_form = false
112
+ self.is_hidden = false
113
+
114
+ attr_reader :attrs
115
+
116
+ def initialize(attrs=nil)
117
+ @attrs = attrs.nil? ? {} : attrs.dup
118
+ end
119
+
120
+ def initialize_copy(original)
121
+ super(original)
122
+ @attrs = original.attrs.dup
123
+ end
124
+
125
+ def render(name, value, attrs=nil)
126
+ raise NotImplementedError
127
+ end
128
+
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
134
+
135
+ def value_from_datahash(data, files, name)
136
+ return data[name]
137
+ end
138
+
139
+ def self.id_for_label(id_)
140
+ id_
141
+ end
142
+
143
+ def has_changed?(initial, data)
144
+ data_value = data || ''
145
+ initial_value = initial || ''
146
+ initial_value != data_value
147
+ end
148
+
149
+ def hidden?
150
+ self.class.is_hidden
151
+ end
152
+
153
+ def media
154
+ Media.new
155
+ end
156
+ end
157
+
158
+ class Input < Widget
159
+ class << self
160
+ attr_accessor :input_type
161
+
162
+ # Copy data to the child class
163
+ def inherited(c)
164
+ super(c)
165
+ c.input_type = input_type.dup if input_type
166
+ end
167
+ end
168
+
169
+ self.is_hidden = false
170
+ self.input_type = nil
171
+
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
181
+
182
+ class TextInput < Input
183
+ self.input_type = 'text'
184
+ end
185
+
186
+ class PasswordInput < Input
187
+ self.input_type = 'password'
188
+
189
+ def initialize(attrs=nil, render_value=true)
190
+ super(attrs)
191
+ @render_value = render_value
192
+ end
193
+
194
+ def render(name, value, attrs=nil)
195
+ value = nil unless @render_value
196
+ super(name, value, attrs)
197
+ end
198
+ end
199
+
200
+ class HiddenInput < Input
201
+ self.input_type = 'hidden'
202
+ self.is_hidden = true
203
+ end
204
+
205
+ class MultipleHiddenInput < HiddenInput
206
+ # Used by choice fields
207
+ attr_accessor :choices
208
+
209
+ def initialize(attrs=nil, choices=[])
210
+ super(attrs)
211
+ # choices can be any enumerable
212
+ @choices = choices
213
+ end
214
+
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
224
+
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]
232
+ end
233
+ end
234
+
235
+ class FileInput < Input
236
+ self.input_type = 'file'
237
+ self.needs_multipart_form = true
238
+
239
+ def render(name, value, attrs=nil)
240
+ super(name, nil, attrs)
241
+ end
242
+
243
+ def value_from_datahash(data, files, name)
244
+ files.fetch(name, nil)
245
+ end
246
+
247
+ def has_changed?(initial, data)
248
+ data.nil?
249
+ end
250
+ end
251
+
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
258
+
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
265
+
266
+ # DateInput
267
+ # DateTimeInput
268
+ # TimeInput
269
+
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
275
+
276
+ def render(name, value, attrs=nil)
277
+ final_attrs = build_attrs(attrs, :type => 'checkbox', :name => name.to_s)
278
+ result = self.check_test(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)} />")
283
+ end
284
+
285
+ def value_from_datahash(data, files, name)
286
+ data.include?(name) ? super(data, files, name) : false
287
+ end
288
+
289
+ def has_changed(initial, data)
290
+ make_bool(initial) != make_bool(data)
291
+ end
292
+ end
293
+
294
+ class Select < Widget
295
+ attr_accessor :choices
296
+
297
+ def initialize(attrs=nil, choices=[])
298
+ super(attrs)
299
+ @choices = choices.collect
300
+ end
301
+
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
311
+
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")
329
+ end
330
+
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
337
+
338
+ class NullBooleanSelect < Select
339
+ def initialize(attrs=nil)
340
+ choices = [['1', 'Unknown'], ['2', 'Yes'], ['3', 'No']]
341
+ super(attrs, choices)
342
+ end
343
+
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
352
+
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
359
+ end
360
+ end
361
+
362
+ def has_changed?(initial, data)
363
+ make_bool(initial) != make_bool(data)
364
+ end
365
+ end
366
+
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
377
+
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
386
+
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
395
+ end
396
+ end
397
+ end; end
@@ -0,0 +1,220 @@
1
+ require 'digest'
2
+
3
+ require 'bureaucrat/forms'
4
+ # import cPickle as pickle
5
+
6
+ # from django.conf import settings
7
+ # from django.http import Http404
8
+ # from django.shortcuts import render_to_response
9
+ # from django.template.context import RequestContext
10
+ # from django.utils.hashcompat import md5_constructor
11
+ # from django.utils.translation import ugettext_lazy as _
12
+
13
+ module Bureaucrat; Wizard
14
+ module Utils
15
+ def security_hash(request, form, *args)
16
+ data = []
17
+ form.each do |bf|
18
+ value = if form.empty_permitted? && ! form.changed? then bf.data
19
+ else bf.field.clean(bf.data)
20
+ end
21
+ value ||= ''
22
+ value = value.strip if value.respond_to? :strip
23
+ data.append([bf.name, value])
24
+ end
25
+
26
+ data += args
27
+ data << settings.SECRET_KEY # FIXME
28
+
29
+ Digest::MD5.hexdigest(Marshal.dump(data))
30
+ end
31
+
32
+ module_function :security_hash
33
+ end
34
+
35
+ class FormWizard
36
+ # FIXME
37
+ # The HTML (and POST data) field name for the "step" variable.
38
+
39
+ class << self
40
+ attr_accessor :step_field_name
41
+ end
42
+
43
+ self.step_field_name = "wizard_step"
44
+
45
+ def initialize(form_list, initial=Hash.new)
46
+ @form_list = form_list.dup
47
+ @initial = initial
48
+ @step = 0
49
+ end
50
+
51
+ def get_form(step, data=nil)
52
+ @form_list[step].new(data,
53
+ :prefix => prefix_for_step(step),
54
+ :initial => @initial.fetch(step, nil))
55
+ end
56
+
57
+ def num_steps
58
+ @form_list.length
59
+ end
60
+
61
+ def new(request, *args)
62
+ current_step = determine_step(request, *args)
63
+ parse_params(request, *args)
64
+
65
+ raise ArgumentError("Step #{current_step} does not exist") if
66
+ current_step >= num_steps
67
+
68
+ (0...current_step).each do |i|
69
+ form = get_form(i, request.POST)
70
+
71
+ return render_hash_failure(request, i) if
72
+ request.POST.fetch("hash_#{i}", '') != security_hash(request, form)
73
+
74
+ process_step(request, form, i)
75
+ end
76
+
77
+ form = get_form(current_step, request.post? ? request.POST : nil)
78
+
79
+ if form.valid?
80
+ process_step(request, form, current_step)
81
+ next_step = current_step + 1
82
+
83
+ if next_step == num_steps
84
+ final_form_list = (0...num_steps).map {|i| get_form(i, request.POST)}
85
+
86
+ final_form_list.each_with_index do |f, i|
87
+ return render_revalidation_failure(request, i, f) unless f.valid?
88
+ end
89
+
90
+ return done(request, final_form_list)
91
+ else
92
+ form = get_form(next_step)
93
+ @step = current_step = next_step
94
+ end
95
+ end
96
+
97
+ render(form, request, current_step)
98
+ end
99
+
100
+ def render(form, request, step, context=nil)
101
+ "Renders the given Form object, returning an HttpResponse." # FIXME
102
+ old_data = request.POST
103
+ prev_fields = []
104
+
105
+ if old_data
106
+ hidden = Widgets::HiddenInput.new
107
+ # Collect all data from previous steps and render it as HTML hidden fields.
108
+ (0...step).each do |i|
109
+ old_form = get_form(i, old_data)
110
+ hash_name = "hash_#{i}"
111
+ hash = old_data[hash_name] || security_hash(request, old_form)
112
+ prev_fields += old_form.map {|bf| bf.as_hidden}
113
+ prev_fields << hidden.render(hash_name, hash)
114
+ end
115
+ end
116
+
117
+ render_template(request, form, prev_fields.join(''), step, context)
118
+ end
119
+
120
+ # METHODS SUBCLASSES MIGHT OVERRIDE IF APPROPRIATE
121
+
122
+ def prefix_for_step(step)
123
+ step.to_s
124
+ end
125
+
126
+ def render_hash_failure(request, step)
127
+ # Hook for rendering a template if a hash check failed.
128
+
129
+ # step is the step that failed. Any previous step is guaranteed to be
130
+ # valid.
131
+
132
+ # This default implementation simply renders the form for the given step,
133
+ # but subclasses may want to display an error message, etc.
134
+ render(get_form(step), request, step, :context => {:wizard_error => _('We apologize, but your form has expired. Please continue filling out the form from this page.')})
135
+ end
136
+
137
+ def render_revalidation_failure(request, step, form)
138
+ # Hook for rendering a template if final revalidation failed.
139
+
140
+ # It is highly unlikely that this point would ever be reached, but See
141
+ # the comment in __call__() for an explanation.
142
+ render(form, request, step)
143
+ end
144
+
145
+ def security_hash(request, form)
146
+ Utils::security_hash(request, form)
147
+ end
148
+
149
+ def determine_step(request, *args)
150
+ if request.post?
151
+ begin
152
+ Integer(request.POST.fetch(step_field_name, 0))
153
+ rescue ArgumentError
154
+ 0
155
+ end
156
+ else
157
+ 0
158
+ end
159
+ end
160
+
161
+ def parse_params(request, *args)
162
+ end
163
+
164
+ def get_template(step)
165
+ 'forms/wizard.erb'
166
+ end
167
+
168
+ def render_template(request, form, previous_fields, step, context=nil)
169
+
170
+ # Renders the template for the given step, returning an HttpResponse object.
171
+
172
+ # Override this method if you want to add a custom context, return a
173
+ # different MIME type, etc. If you only need to override the template
174
+ # name, use get_template() instead.
175
+
176
+ # The template will be rendered with the following context:
177
+ # step_field -- The name of the hidden field containing the step.
178
+ # step0 -- The current step (zero-based).
179
+ # step -- The current step (one-based).
180
+ # step_count -- The total number of steps.
181
+ # form -- The Form instance for the current step (either empty
182
+ # or with errors).
183
+ # previous_fields -- A string representing every previous data field,
184
+ # plus hashes for completed forms, all in the form of
185
+ # hidden fields. Note that you'll need to run this
186
+ # through the 'safe' template filter, to prevent
187
+ # auto-escaping, because it's raw HTML.
188
+
189
+ context ||= {}
190
+ # FIXME
191
+ render_to_response(get_template(step),
192
+ context.merge(:step_field => step_field_name,
193
+ :step0 => step,
194
+ :step => step + 1,
195
+ :step_count => num_steps,
196
+ :form => form,
197
+ :previous_fields => previous_fields),
198
+ :context_instance => RequestContext(request))
199
+ end
200
+
201
+ def process_step(request, form, step)
202
+ # Hook for modifying the FormWizard's internal state, given a fully
203
+ # validated Form object. The Form is guaranteed to have clean, valid
204
+ # data.
205
+
206
+ # This method should *not* modify any of that data. Rather, it might want
207
+ # to set self.extra_context or dynamically alter self.form_list, based on
208
+ # previously submitted forms.
209
+
210
+ # Note that this method is called every time a page is rendered for *all*
211
+ # submitted steps.
212
+ end
213
+
214
+ # METHODS SUBCLASSES MUST OVERRIDE ########################################
215
+
216
+ def done(request, form_list)
217
+ raise NotImplementedError.new("#{self.class.name} doesn't define the required 'done' method.")
218
+ end
219
+ end
220
+ end
data/lib/bureaucrat.rb ADDED
@@ -0,0 +1,10 @@
1
+ libdir = File.dirname(__FILE__)
2
+ $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
3
+
4
+ module Bureaucrat
5
+ VERSION = '0.0.1'
6
+ end
7
+
8
+ require 'bureaucrat/widgets'
9
+ require 'bureaucrat/fields'
10
+ require 'bureaucrat/forms'