bureaucrat 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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'