hat-trick 0.1.2 → 0.1.3

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