hat-trick 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,8 +1,10 @@
1
1
  module HatTrick
2
2
  class StepDefinition
3
3
  attr_reader :callbacks, :include_data_key
4
- attr_accessor :name, :fieldset, :buttons, :repeat_of
5
- attr_writer :skipped
4
+ attr_accessor :name, :fieldset, :wizard
5
+ attr_writer :skipped, :first
6
+
7
+ delegate :config, :to => :wizard
6
8
 
7
9
  def initialize(args={})
8
10
  args.each_pair do |k,v|
@@ -12,15 +14,53 @@ module HatTrick
12
14
  end
13
15
  end
14
16
  @callbacks = {}
15
- @buttons = {}
16
- @skipped = false
17
+ @buttons = [
18
+ { next: default_button(:next) },
19
+ { back: default_button(:back) }
20
+ ]
21
+ @skipped ||= false
22
+ @last ||= false
23
+ @first ||= false
24
+ end
25
+
26
+ def buttons
27
+ # We should check for a new translation every time because the user may
28
+ # have changed their desired localization.
29
+ @buttons.map do |b|
30
+ button_type = b.keys.first
31
+ if b[button_type][:default]
32
+ b[button_type][:label] = button_label(button_type)
33
+ end
34
+ b
35
+ end
17
36
  end
18
37
 
19
- def initialize_copy(source)
20
- @callbacks = {}
21
- @buttons = source.buttons.dup
22
- @skipped = false
23
- @repeat_of = source
38
+ def next_button
39
+ get_button(:next)
40
+ end
41
+
42
+ def back_button
43
+ get_button(:back)
44
+ end
45
+
46
+ def get_button(type)
47
+ button = buttons.detect { |b| b.keys.first == type.to_sym }
48
+ button[type.to_sym] unless button.nil?
49
+ end
50
+
51
+ def default_button(type)
52
+ { :label => button_label(type), :default => true }
53
+ end
54
+
55
+ def add_button(button)
56
+ @buttons.delete_if do |b|
57
+ b.keys.first == button.keys.first && b[b.keys.first][:default]
58
+ end
59
+ @buttons << button
60
+ end
61
+
62
+ def delete_button(type)
63
+ @buttons.delete_if { |b| b.keys.first == type }
24
64
  end
25
65
 
26
66
  def name=(name)
@@ -35,14 +75,27 @@ module HatTrick
35
75
  @fieldset or name
36
76
  end
37
77
 
38
- def repeat?
39
- !repeat_of.nil?
40
- end
41
-
42
78
  def skipped?
43
79
  @skipped
44
80
  end
45
81
 
82
+ def last=(_last)
83
+ @last = _last
84
+ if @last
85
+ @buttons.delete_if { |b| b.keys.include?(:next) }
86
+ else
87
+ @buttons << { :next => default_next_button }
88
+ end
89
+ end
90
+
91
+ def last?
92
+ @last
93
+ end
94
+
95
+ def first?
96
+ @first
97
+ end
98
+
46
99
  def to_s
47
100
  str = "<HatTrick::Step:0x%08x :#{name}" % (object_id * 2)
48
101
  str += " fieldset: #{fieldset}" if fieldset != name
@@ -54,19 +107,12 @@ module HatTrick
54
107
  name.to_sym
55
108
  end
56
109
 
57
- def as_json(options = nil)
58
- json = { :name => name, :fieldset => fieldset }
59
- json[:repeatOf] = repeat_of.as_json if repeat?
60
- json[:buttons] = buttons unless buttons.empty?
61
- json
62
- end
63
-
64
- def before_callback=(blk)
65
- callbacks[:before] = blk
110
+ def before_callback=(block)
111
+ callbacks[:before] = block
66
112
  end
67
113
 
68
- def after_callback=(blk)
69
- callbacks[:after] = blk
114
+ def after_callback=(block)
115
+ callbacks[:after] = block
70
116
  end
71
117
 
72
118
  def include_data=(hash)
@@ -74,31 +120,111 @@ module HatTrick
74
120
  @include_data_key = hash.keys.first
75
121
  end
76
122
 
123
+ def step_contents_callback=(block)
124
+ callbacks[:step_contents] = block
125
+ end
126
+
127
+ def step_contents(context, model)
128
+ contents = run_step_contents_callback(context, model)
129
+ {
130
+ :hatTrickStepContents => {
131
+ name.to_s.camelize(:lower).to_sym => contents
132
+ }
133
+ }
134
+ end
135
+
136
+ def include_data(context, model)
137
+ inc_data = run_include_data_callback(context, model)
138
+ return {} unless inc_data.respond_to?(:as_json)
139
+ key = include_data_key.to_s.camelize(:lower)
140
+ { key => camelize_hash_keys(inc_data) }
141
+ end
142
+
143
+ def before_callbacks
144
+ before_callbacks = [callbacks[:before]]
145
+ if wizard.before_callback_for_all_steps
146
+ before_callbacks << wizard.before_callback_for_all_steps
147
+ end
148
+ before_callbacks
149
+ end
150
+
77
151
  def run_before_callback(context, model)
78
- run_callback(:before, context, model)
152
+ Rails.logger.info "Running before callback for #{name}"
153
+ run_callbacks(before_callbacks, context, model)
79
154
  end
80
155
 
81
156
  def run_include_data_callback(context, model)
82
- run_callback(:include_data, context, model)
157
+ run_callbacks([callbacks[:include_data]], context, model)
158
+ end
159
+
160
+ def run_step_contents_callback(context, model)
161
+ run_callbacks([callbacks[:step_contents]], context, model)
162
+ end
163
+
164
+ def after_callbacks
165
+ after_callbacks = [callbacks[:after]]
166
+ if wizard.after_callback_for_all_steps
167
+ after_callbacks << wizard.after_callback_for_all_steps
168
+ end
169
+ after_callbacks
83
170
  end
84
171
 
85
172
  def run_after_callback(context, model)
86
- run_callback(:after, context, model)
173
+ run_callbacks(after_callbacks, context, model)
174
+ end
175
+
176
+ private
177
+
178
+ def button_label(type)
179
+ config.send("#{type}_button_label") or
180
+ if i18n_key = config.send("#{type}_button_label_i18n_key")
181
+ I18n.t i18n_key
182
+ end or
183
+ default_button_label(type)
87
184
  end
88
185
 
89
- protected
186
+ def default_button_label(type)
187
+ translate_button_label(type)
188
+ end
90
189
 
91
- def run_callback(type, context, model)
92
- callback = callbacks[type.to_sym]
93
- if callback && callback.is_a?(Proc)
94
- if callback.arity > 0
95
- unless model.is_a?(ActiveModel::Errors)
96
- context.instance_exec model, &callback
190
+ def translate_button_label(type)
191
+ default_label = type.to_s.humanize
192
+ begin
193
+ label = I18n.t("wizard.buttons.#{type}", :default => default_label)
194
+ rescue NameError
195
+ label = default_label
196
+ end
197
+ label
198
+ end
199
+
200
+ def run_callbacks(callbacks, context, model)
201
+ result = nil
202
+ callbacks.each do |callback|
203
+ if callback && callback.is_a?(Proc)
204
+ if callback.arity > 0
205
+ unless model.nil? || model.is_a?(ActiveModel::Errors)
206
+ result = context.instance_exec model, &callback
207
+ end
208
+ else
209
+ result = context.instance_eval &callback
97
210
  end
98
- else
99
- context.instance_eval &callback
100
211
  end
101
212
  end
213
+
214
+ # return the last result; mainly for include_data
215
+ result
216
+ end
217
+
218
+ def camelize_hash_keys(_hash)
219
+ hash = {}
220
+ if _hash.respond_to?(:each)
221
+ _hash.each do |k,v|
222
+ hash[k.to_s.camelize(:lower)] = v
223
+ end
224
+ else
225
+ hash = _hash
226
+ end
227
+ hash
102
228
  end
103
229
  end
104
230
  end
@@ -1,3 +1,3 @@
1
1
  module HatTrick
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3"
3
3
  end
@@ -6,12 +6,15 @@ module HatTrick
6
6
  class Wizard
7
7
  include WizardSteps
8
8
 
9
- attr_accessor :controller, :model
9
+ attr_accessor :controller, :model, :external_redirect_url
10
10
  attr_reader :current_step, :wizard_def, :steps
11
11
 
12
+ delegate :config, :to => :wizard_def
13
+
12
14
  def initialize(wizard_def)
13
15
  @wizard_def = wizard_def
14
16
  @steps = @wizard_def.steps.map { |s| HatTrick::Step.new(s, self) }
17
+ @current_step = first_step
15
18
  end
16
19
 
17
20
  def controller=(new_controller)
@@ -20,15 +23,12 @@ module HatTrick
20
23
 
21
24
  def model=(new_model)
22
25
  @model = new_model
23
- unless @model.class.included_modules.include?(HatTrick::ModelMethods)
24
- @model.class.send(:include, HatTrick::ModelMethods)
25
- end
26
26
  end
27
27
 
28
28
  def current_step=(_step)
29
29
  raise "Don't set current_step to nil" if _step.nil?
30
30
  step = find_step(_step)
31
- raise "#{step} is not a member of this wizard" unless step
31
+ raise StepNotFound, "#{_step} is not a member of this wizard" unless step
32
32
  @current_step = step
33
33
  end
34
34
 
@@ -72,7 +72,7 @@ module HatTrick
72
72
  def next_step
73
73
  step = find_next_step
74
74
  while step.skipped? do
75
- step = find_step_after(step)
75
+ step = step_after(step)
76
76
  end
77
77
  step
78
78
  end
@@ -91,11 +91,64 @@ module HatTrick
91
91
  end
92
92
 
93
93
  def start
94
- session["hat-trick.steps_visited"] = []
94
+ reset_step_session_data
95
95
  self.current_step ||= first_step
96
96
  run_before_callback
97
97
  end
98
98
 
99
+ def steps_before_current
100
+ steps_before(current_step)
101
+ end
102
+
103
+ def steps_after_current
104
+ steps_after(current_step)
105
+ end
106
+
107
+ def visited_steps
108
+ session["hat-trick.visited_steps"] ||= []
109
+ end
110
+
111
+ def skipped_steps
112
+ session["hat-trick.skipped_steps"] ||= []
113
+ end
114
+
115
+ def percent_complete(step=current_step)
116
+ percent = (steps_before_current.count.to_f / total_step_count) * 100
117
+ if percent > 100.0
118
+ 100
119
+ elsif percent <= 0
120
+ 0
121
+ elsif percent < 5.0
122
+ 5
123
+ else
124
+ percent
125
+ end
126
+ end
127
+
128
+ def steps_remaining=(count)
129
+ self.override_step_count = steps_before_current.count + count
130
+ end
131
+
132
+ def steps_remaining
133
+ total_step_count - steps_before_current.count
134
+ end
135
+
136
+ def override_step_count=(count)
137
+ if count.nil?
138
+ session.delete('hat-trick.override_step_count')
139
+ else
140
+ session['hat-trick.override_step_count'] = count
141
+ end
142
+ end
143
+
144
+ def override_step_count
145
+ session['hat-trick.override_step_count']
146
+ end
147
+
148
+ def total_step_count
149
+ override_step_count or steps.count
150
+ end
151
+
99
152
  def run_before_callback(step=current_step)
100
153
  step.run_before_callback(controller, model)
101
154
  end
@@ -104,13 +157,29 @@ module HatTrick
104
157
  step.run_after_callback(controller, model)
105
158
  end
106
159
 
160
+ def redirect_to_step(step)
161
+ redirect_from = current_step.fieldset
162
+ self.current_step = step
163
+ current_step.redirect_from = redirect_from
164
+ end
165
+
107
166
  def advance_step(next_step_name=nil)
108
167
  # clean up current step
109
- current_step.visited = true
168
+ current_step.mark_as_visited
169
+ before_callback_next_step = current_step.next_step
110
170
  run_after_callback
171
+ after_callback_next_step = current_step.next_step
111
172
 
112
- # see if there is a requested next step
113
- requested_next_step = find_step(next_step_name) unless next_step_name.nil?
173
+ # return if any redirects were requested
174
+ return if external_redirect_url.present?
175
+
176
+ # if after callback changed the next step, go to that one
177
+ requested_next_step = if after_callback_next_step != before_callback_next_step
178
+ after_callback_next_step
179
+ else
180
+ # use argument, if there was one
181
+ find_step(next_step_name) unless next_step_name.nil?
182
+ end
114
183
 
115
184
  # finish if we're on the last step
116
185
  if current_step == last_step && !requested_next_step
@@ -122,72 +191,62 @@ module HatTrick
122
191
  run_before_callback
123
192
  # if the step was explicitly requested, we ignore #skipped?
124
193
  else
125
- Rails.logger.info "Advancing to step: #{next_step}"
126
194
  self.current_step = next_step
127
195
  run_before_callback
128
196
  # Running the before callback may have marked current_step as skipped
129
197
  while current_step.skipped?
130
198
  self.current_step = next_step
131
199
  run_before_callback
200
+ # make sure we don't loop forever
201
+ break if current_step == last_step
132
202
  end
203
+ Rails.logger.info "Advancing to step: #{current_step}"
133
204
  end
134
205
  end
135
206
  end
136
207
 
137
208
  def include_data
209
+ include_data_for_step(current_step)
210
+ end
211
+
212
+ def include_data_for_step(step)
138
213
  return {} if model.nil?
139
- inc_data = {}
140
- include_data_steps = steps_before(current_step).reject(&:skipped?) << current_step
141
- include_data_steps.each do |step|
142
- step_data = step.run_include_data_callback(controller, model)
143
- next if step_data.nil? || !step_data.respond_to?(:as_json)
144
- step_key = step.include_data_key.to_s.camelize(:lower)
145
- begin
146
- inc_data[step_key] = camelize_hash_keys(step_data)
147
- rescue NoMethodError => e
148
- Rails.logger.error "Unable to serialize data for step #{step}: #{e}"
149
- end
150
- end
214
+ inc_data = step.include_data(controller, model)
215
+ contents = step.step_contents(controller, model)
216
+ inc_data.merge! contents
217
+ inc_data.delete_if { |k,v| v.nil? }
151
218
  inc_data
152
219
  end
153
220
 
154
221
  def alias_action_methods
155
222
  action_methods = controller.action_methods.reject do |m|
156
- /^render/ =~ m.to_s ||
157
- controller.respond_to?("#{m}_with_hat_trick", :include_private)
223
+ /^render/ =~ m.to_s or
224
+ m.to_s.include?('!') or
225
+ controller.respond_to?("#{m}_without_hat_trick", :include_private)
158
226
  end
159
227
  HatTrick::ControllerHooks.def_action_method_aliases(action_methods)
160
- action_methods.each do |meth|
161
- controller.class.send(:alias_method_chain, meth, :hat_trick)
162
- controller.class.send(:private, "#{meth}_without_hat_trick")
228
+ action_methods.each do |m|
229
+ Rails.logger.info "Aliasing #{m}"
230
+ controller.class.send(:alias_method_chain, m, :hat_trick)
231
+ controller.class.send(:private, "#{m}_without_hat_trick")
163
232
  end
164
233
  end
165
234
 
166
235
  private
167
236
 
237
+ def reset_step_session_data
238
+ # TODO: Move this into a StepCollection class (maybe subclass Set)
239
+ visited_steps = []
240
+ skipped_steps = []
241
+ end
242
+
168
243
  def fake_session
244
+ Rails.logger.warn "Using a fake session object!"
169
245
  @fake_session ||= {}
170
246
  end
171
247
 
172
248
  def find_next_step
173
- find_step(current_step.next_step) or find_step_after(current_step)
174
- end
175
-
176
- def find_step_after(step)
177
- next_path_step = step_after step
178
- next_path_step or find_next_active_step(step)
179
- end
180
-
181
- def camelize_hash_keys(_hash)
182
- hash = {}
183
- if _hash.respond_to?(:each)
184
- _hash.each do |k,v|
185
- hash[k.to_s.camelize(:lower)] = v
186
- end
187
- else
188
- hash = _hash
189
- end
190
- hash
249
+ find_step(current_step.next_step) || step_after(current_step)
191
250
  end
192
251
  end
193
252
  end
@@ -6,19 +6,28 @@ module HatTrick
6
6
  class WizardDefinition
7
7
  include WizardSteps
8
8
 
9
- attr_accessor :configured_create_url, :configured_update_url
9
+ attr_reader :config
10
+ attr_accessor :before_callback_for_all_steps, :after_callback_for_all_steps
10
11
 
11
- def initialize
12
+ def initialize(config)
13
+ @config = config
12
14
  @steps = []
13
15
  end
14
16
 
15
- def get_wizard(controller)
16
- controller.send(:ht_wizard) or (
17
- wizard = HatTrick::Wizard.new(self)
18
- wizard.controller = controller
19
- wizard.alias_action_methods
20
- wizard
21
- )
17
+ def make_wizard_for(controller)
18
+ Rails.logger.info "Making new wizard instance"
19
+ wizard = HatTrick::Wizard.new(self)
20
+ wizard.controller = controller
21
+ wizard.alias_action_methods
22
+ wizard
23
+ end
24
+
25
+ def configured_create_url
26
+ config.create_url
27
+ end
28
+
29
+ def configured_update_url
30
+ config.update_url
22
31
  end
23
32
  end
24
33
  end
@@ -1,4 +1,6 @@
1
1
  module HatTrick
2
+ class StepNotFound < StandardError; end
3
+
2
4
  module WizardSteps
3
5
  include Enumerable
4
6
  attr_reader :steps
@@ -26,9 +28,11 @@ module HatTrick
26
28
  def steps_after(_step)
27
29
  step = find_step(_step)
28
30
  return [] unless step
31
+ return [] if step.last? && !step.skipped?
29
32
  step_index = steps.index(step)
30
33
  max_index = steps.count - 1
31
- after_index = step_index < max_index ? step_index + 1 : step_index
34
+ return [] if step_index >= max_index
35
+ after_index = step_index + 1
32
36
  steps[after_index .. -1]
33
37
  end
34
38
 
@@ -40,7 +44,8 @@ module HatTrick
40
44
  step = find_step(_step)
41
45
  return [] unless step
42
46
  step_index = steps.index(step)
43
- before_index = step_index > 0 ? step_index - 1 : step_index
47
+ return [] if step_index <= 0
48
+ before_index = step_index - 1
44
49
  steps[0 .. before_index]
45
50
  end
46
51
 
@@ -48,10 +53,14 @@ module HatTrick
48
53
  if step.is_a?(HatTrick::Step) || step.is_a?(HatTrick::StepDefinition)
49
54
  new_step = step
50
55
  else
51
- step_args = args.merge(:name => step)
56
+ step_args = args.merge(:name => step, :wizard => self)
52
57
  new_step = HatTrick::StepDefinition.new(step_args)
53
58
  end
54
59
 
60
+ if steps.count == 0
61
+ new_step.first = true
62
+ end
63
+
55
64
  steps << new_step
56
65
 
57
66
  new_step
@@ -1,26 +1,27 @@
1
1
  require 'spec_helper'
2
2
 
3
- # will automatically get HatTrick::DSL included
4
- class FakeController < ActionController::Base; end
5
-
6
3
  describe HatTrick::DSL do
7
- let(:controller_class) { FakeController }
4
+ subject(:controller) {
5
+ Class.new.send(:include, HatTrick::DSL).tap do |c|
6
+ c.stubs(:before_filter)
7
+ c.any_instance.stubs(:render)
8
+ end
9
+ }
8
10
 
9
- # save some typing
10
- def dsl(&block)
11
- controller_class.instance_eval &block
12
- end
13
-
14
11
  describe HatTrick::DSL::ClassMethods do
15
12
  describe "#step" do
16
13
  it "should call Wizard#add_step" do
17
14
  HatTrick::WizardDefinition.any_instance.expects(:add_step).with(:foo, {})
18
- dsl do
15
+ controller.instance_eval do
19
16
  wizard do
20
17
  step :foo
21
18
  end
22
19
  end
23
20
  end
21
+
22
+ it "raises an error if called outside a wizard block" do
23
+ expect { controller.step :foo }.to raise_error
24
+ end
24
25
  end
25
26
  end
26
27
  end