hat-trick 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,35 @@
1
+ require 'hat_trick/step_definition'
2
+
3
+ module HatTrick
4
+ class Step
5
+ attr_reader :step_def, :wizard
6
+ attr_writer :skipped
7
+ attr_accessor :next_step
8
+
9
+ delegate :name, :fieldset, :buttons, :repeat_of, :to_sym, :to_s, :as_json,
10
+ :run_after_callback, :run_before_callback, :repeat?,
11
+ :run_include_data_callback, :include_data_key, :to => :step_def
12
+
13
+ def initialize(step_def, wizard)
14
+ @step_def = step_def
15
+ @wizard = wizard
16
+ @skipped = step_def.skipped?
17
+ end
18
+
19
+ def session
20
+ wizard.session
21
+ end
22
+
23
+ def skipped?
24
+ not visited? and @skipped
25
+ end
26
+
27
+ def visited?
28
+ session["hat-trick.steps_visited"].include? self.to_sym
29
+ end
30
+
31
+ def visited=(_visited)
32
+ session["hat-trick.steps_visited"] << self.to_sym
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,104 @@
1
+ module HatTrick
2
+ class StepDefinition
3
+ attr_reader :callbacks, :include_data_key
4
+ attr_accessor :name, :fieldset, :buttons, :repeat_of
5
+ attr_writer :skipped
6
+
7
+ def initialize(args={})
8
+ args.each_pair do |k,v|
9
+ setter = "#{k}="
10
+ if respond_to?(setter)
11
+ send(setter,v)
12
+ end
13
+ end
14
+ @callbacks = {}
15
+ @buttons = {}
16
+ @skipped = false
17
+ end
18
+
19
+ def initialize_copy(source)
20
+ @callbacks = source.callbacks.dup
21
+ @buttons = source.buttons.dup
22
+ @skipped = false
23
+ @repeat_of = source
24
+ end
25
+
26
+ def name=(name)
27
+ @name = name.to_sym
28
+ end
29
+
30
+ def fieldset=(fieldset)
31
+ @fieldset = fieldset.to_sym
32
+ end
33
+
34
+ def fieldset
35
+ @fieldset or name
36
+ end
37
+
38
+ def repeat?
39
+ !repeat_of.nil?
40
+ end
41
+
42
+ def skipped?
43
+ @skipped
44
+ end
45
+
46
+ def to_s
47
+ str = "<HatTrick::Step:0x%08x :#{name}" % (object_id * 2)
48
+ str += " fieldset: #{fieldset}" if fieldset != name
49
+ str += ">"
50
+ str
51
+ end
52
+
53
+ def to_sym
54
+ name.to_sym
55
+ end
56
+
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
66
+ end
67
+
68
+ def after_callback=(blk)
69
+ callbacks[:after] = blk
70
+ end
71
+
72
+ def include_data=(hash)
73
+ callbacks[:include_data] = hash.values.first
74
+ @include_data_key = hash.keys.first
75
+ end
76
+
77
+ def run_before_callback(context, wizard_dsl, model)
78
+ run_callback(:before, context, wizard_dsl, model)
79
+ end
80
+
81
+ def run_include_data_callback(context, wizard_dsl, model)
82
+ run_callback(:include_data, context, wizard_dsl, model)
83
+ end
84
+
85
+ def run_after_callback(context, wizard_dsl, model)
86
+ run_callback(:after, context, wizard_dsl, model)
87
+ end
88
+
89
+ protected
90
+
91
+ def run_callback(type, context, *args)
92
+ callback = callbacks[type.to_sym]
93
+ if callback && callback.is_a?(Proc)
94
+ if callback.arity > 1
95
+ context.instance_exec args[0], args[1], &callback
96
+ elsif callback.arity == 1
97
+ context.instance_exec args[0], &callback
98
+ else
99
+ context.instance_eval &callback
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,3 @@
1
+ module HatTrick
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,176 @@
1
+ require 'hat_trick/controller_hooks'
2
+ require 'hat_trick/wizard_steps'
3
+
4
+ module HatTrick
5
+ class Wizard
6
+ include WizardSteps
7
+
8
+ attr_accessor :controller, :model
9
+ attr_reader :current_step, :wizard_def, :wizard_dsl_context, :steps
10
+
11
+ def initialize(wizard_def)
12
+ @wizard_def = wizard_def
13
+ @wizard_dsl_context = DSL::WizardContext.new(@wizard_def)
14
+ @wizard_dsl_context.wizard = self
15
+ @steps = @wizard_def.steps.map { |s| HatTrick::Step.new(s, self) }
16
+ end
17
+
18
+ def controller=(new_controller)
19
+ @controller = new_controller
20
+ end
21
+
22
+ def current_step=(_step)
23
+ raise "Don't set current_step to nil" if _step.nil?
24
+ step = find_step(_step)
25
+ raise "#{step} is not a member of this wizard" unless step
26
+ @current_step = step
27
+ end
28
+
29
+ def session
30
+ controller.session unless controller.nil?
31
+ end
32
+
33
+ def model_created?
34
+ !(model.nil? || (model.respond_to?(:new_record?) && model.new_record?))
35
+ end
36
+
37
+ def current_form_url
38
+ model_created? ? update_url : create_url
39
+ end
40
+
41
+ def current_form_method
42
+ model_created? ? 'put' : 'post'
43
+ end
44
+
45
+ def create_url
46
+ controller.url_for(:controller => controller.controller_name,
47
+ :action => 'create', :only_path => true)
48
+ end
49
+
50
+ def update_url
51
+ if model_created?
52
+ controller.url_for(:controller => controller.controller_name,
53
+ :action => 'update', :id => model,
54
+ :only_path => true)
55
+ else
56
+ nil
57
+ end
58
+ end
59
+
60
+ def next_step
61
+ step = find_next_step
62
+ while step.skipped? do
63
+ step = find_step_after(step)
64
+ end
65
+ step
66
+ end
67
+
68
+ def previously_visited_step
69
+ steps_before(current_step).select { |s| s.visited? }.last
70
+ end
71
+
72
+ def skip_step(_step)
73
+ step = find_step(_step)
74
+ step.skipped = true
75
+ end
76
+
77
+ def started?
78
+ !current_step.nil?
79
+ end
80
+
81
+ def start
82
+ session["hat-trick.steps_visited"] = []
83
+ self.current_step = first_step
84
+ run_before_callback
85
+ end
86
+
87
+ def finish
88
+ # Do something here
89
+ # Such as: Force the wizard to display the "done" page
90
+ Rails.logger.info "WIZARD FINISHED!"
91
+ end
92
+
93
+ def run_before_callback(step=current_step)
94
+ step.run_before_callback(controller, wizard_dsl_context, model)
95
+ end
96
+
97
+ def run_after_callback(step=current_step)
98
+ step.run_after_callback(controller, wizard_dsl_context, model)
99
+ end
100
+
101
+ def advance_step_with_debugger
102
+ require 'ruby-debug'
103
+ debugger
104
+ advance_step
105
+ end
106
+
107
+ def advance_step(next_step_name=nil)
108
+ # clean up current step
109
+ current_step.visited = true
110
+ run_after_callback
111
+
112
+ # see if there is a requested next step
113
+ requested_next_step = find_step(next_step_name) unless next_step_name.nil?
114
+
115
+ # finish if we're on the last step
116
+ if current_step == last_step && !requested_next_step
117
+ finish
118
+ else # we're not on the last step
119
+ if requested_next_step
120
+ Rails.logger.info "Force advancing to step: #{requested_next_step}"
121
+ self.current_step = requested_next_step
122
+ run_before_callback
123
+ # if the step was explicitly requested, we ignore #skipped?
124
+ else
125
+ Rails.logger.info "Advancing to step: #{next_step}"
126
+ self.current_step = next_step
127
+ run_before_callback
128
+ # Running the before callback may have marked current_step as skipped
129
+ while current_step.skipped?
130
+ self.current_step = next_step
131
+ run_before_callback
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ def include_data
138
+ return {} unless model
139
+ inc_data = current_step.run_include_data_callback(controller, wizard_dsl_context, model)
140
+ data_key = current_step.include_data_key
141
+ { data_key.to_s.camelize(:lower) => camelize_hash_keys(inc_data) }
142
+ end
143
+
144
+ def alias_action_methods
145
+ action_methods = controller.action_methods.reject do |m|
146
+ /^render/ =~ m.to_s ||
147
+ controller.respond_to?("#{m}_with_hat_trick", :include_private)
148
+ end
149
+ HatTrick::ControllerHooks.def_action_method_aliases(action_methods)
150
+ action_methods.each do |meth|
151
+ controller.class.send(:alias_method_chain, meth, :hat_trick)
152
+ controller.class.send(:private, "#{meth}_without_hat_trick")
153
+ end
154
+ end
155
+
156
+ private
157
+
158
+ def find_next_step
159
+ find_step(current_step.next_step) or find_step_after(current_step)
160
+ end
161
+
162
+ def find_step_after(step)
163
+ next_path_step = step_after step
164
+ next_path_step or find_next_active_step(step)
165
+ end
166
+
167
+ def camelize_hash_keys(_hash)
168
+ return nil if _hash.nil?
169
+ hash = {}
170
+ _hash.each do |k,v|
171
+ hash[k.to_s.camelize(:lower)] = v
172
+ end
173
+ hash
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,22 @@
1
+ require 'hat_trick/step'
2
+ require 'hat_trick/wizard_steps'
3
+ require 'hat_trick/wizard'
4
+
5
+ module HatTrick
6
+ class WizardDefinition
7
+ include WizardSteps
8
+
9
+ def initialize
10
+ @steps = []
11
+ end
12
+
13
+ def get_wizard(controller)
14
+ controller.send(:ht_wizard) or (
15
+ wizard = HatTrick::Wizard.new(self)
16
+ wizard.controller = controller
17
+ wizard.alias_action_methods
18
+ wizard
19
+ )
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,95 @@
1
+ module HatTrick
2
+ module WizardSteps
3
+ include Enumerable
4
+ attr_reader :steps
5
+
6
+ delegate :each, :empty?, :first, :last, :to => :steps
7
+
8
+ alias_method :each_step, :each
9
+ alias_method :to_ary, :steps
10
+ alias_method :first_step, :first
11
+ alias_method :last_step, :last
12
+
13
+ def find_step(step)
14
+ return nil if step.nil?
15
+ if step.is_a?(HatTrick::Step) || step.is_a?(HatTrick::StepDefinition)
16
+ find { |s| s.equal? step }
17
+ else
18
+ find { |s| s.name == step.to_sym }
19
+ end
20
+ end
21
+
22
+ def step_after(_step)
23
+ steps_after(_step).first
24
+ end
25
+
26
+ def steps_after(_step)
27
+ step = find_step(_step)
28
+ return [] unless step
29
+ after_index = steps.index(step) + 1
30
+ steps[after_index .. -1]
31
+ end
32
+
33
+ def step_before(_step)
34
+ steps_before(_step).last
35
+ end
36
+
37
+ def steps_before(_step)
38
+ step = find_step(_step)
39
+ return [] unless step
40
+ before_index = steps.index(step) - 1
41
+ steps[0 .. before_index]
42
+ end
43
+
44
+ def add_step(step, args={})
45
+ if step.is_a?(HatTrick::Step) || step.is_a?(HatTrick::StepDefinition)
46
+ new_step = step
47
+ else
48
+ step_args = args.merge(:name => step)
49
+ new_step = HatTrick::StepDefinition.new(step_args)
50
+ end
51
+
52
+ steps << new_step
53
+
54
+ new_step
55
+ end
56
+
57
+ def delete_step(_step)
58
+ step = find_step(_step)
59
+ steps.delete(step)
60
+ end
61
+
62
+ def replace_step(_old_step, _new_step)
63
+ old_step = find_step(_old_step)
64
+ old_index = steps.index(old_step)
65
+ raise ArgumentError, "Couldn't find step #{_old_step}" unless old_step
66
+ new_step_in_wizard = find_step(_new_step)
67
+ if new_step_in_wizard
68
+ # new_step is already in the wizard
69
+ return move_step(new_step_in_wizard, old_index)
70
+ end
71
+
72
+ if _new_step.is_a?(HatTrick::Step) || _new_step.is_a?(HatTrick::StepDefinition)
73
+ new_step = _new_step
74
+ else
75
+ new_step = HatTrick::StepDefinition.new(:name => _new_step)
76
+ end
77
+
78
+ steps.delete_at(old_index)
79
+ steps.insert(old_index, new_step)
80
+ new_step
81
+ end
82
+
83
+ def move_step(step, index)
84
+ raise ArgumentError, "#{step} isn't in this wizard" unless steps.include?(step)
85
+ current_index = steps.index(step)
86
+ unless index < current_index
87
+ raise ArgumentError, "#{step} has index #{current_index}; must be >= #{index}"
88
+ end
89
+
90
+ while steps.index(step) > index
91
+ steps.delete_at(index)
92
+ end
93
+ end
94
+ end
95
+ end
data/lib/hat_trick.rb ADDED
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__)
2
+ require 'hat-trick'
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ describe HatTrick::Step do
4
+ subject { HatTrick::Step.new(name: "test_step") }
5
+
6
+ let(:wizard_def) { HatTrick::WizardDefinition.new }
7
+
8
+ it { should be_a(HatTrick::Step) }
9
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe HatTrick::WizardDefinition do
4
+ subject { HatTrick::WizardDefinition.new }
5
+
6
+ it { should be_empty }
7
+
8
+ describe "#add_step" do
9
+ it "should accept a symbol for the step name" do
10
+ step = subject.add_step(:step1)
11
+ step.name.should == :step1
12
+ end
13
+
14
+ it "should add a step to the wizard def" do
15
+ subject.add_step(:step1)
16
+ subject.should have(1).step
17
+ end
18
+
19
+ it "should update the last_step attr" do
20
+ step = subject.add_step(:step1)
21
+ subject.last_step.should == step
22
+ end
23
+ end
24
+
25
+ describe "#delete_step" do
26
+ it "should remove the step from the wizard def" do
27
+ step = subject.add_step(:goner)
28
+ subject.delete_step(step)
29
+ subject.steps.should_not include(step)
30
+ end
31
+
32
+ it "should update the the links between surrounding steps" do
33
+ end
34
+ end
35
+
36
+ describe "#replace_step" do
37
+ end
38
+
39
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+
3
+ describe HatTrick::Wizard do
4
+ subject {
5
+ wiz_def = HatTrick::WizardDefinition.new
6
+ wiz_def.add_step(:step1)
7
+ wiz_def.add_step(:step2)
8
+ wiz_def.add_step(:step3)
9
+ wiz_def.add_step(:step4)
10
+ wiz_def.add_step(:step5)
11
+
12
+ HatTrick::Wizard.new(wiz_def)
13
+ }
14
+
15
+ before :each do
16
+ subject.start
17
+ end
18
+
19
+ describe "advancing steps" do
20
+ it "should go from step1 to step2" do
21
+ subject.current_step.to_sym.should == :step1
22
+ subject.advance_step
23
+ subject.current_step.to_sym.should == :step2
24
+ end
25
+ end
26
+
27
+ describe "skipping steps" do
28
+ it "should skip step2 when requested" do
29
+ subject.skip_step :step2
30
+ subject.advance_step
31
+ subject.current_step.to_sym.should == :step3
32
+ end
33
+
34
+ it "should skip steps 2 & 3 when requested" do
35
+ subject.skip_step :step2
36
+ subject.skip_step :step3
37
+ subject.advance_step
38
+ subject.current_step.to_sym.should == :step4
39
+ end
40
+ end
41
+
42
+ describe "repeating steps" do
43
+ it "should repeat step 2 when requested" do
44
+ end
45
+ end
46
+
47
+ describe "setting explicit next steps" do
48
+ it "should advance to the requested next step when one is set" do
49
+ subject.steps.first.next_step = :step4
50
+ subject.advance_step
51
+ subject.current_step.to_sym.should == :step4
52
+ end
53
+ end
54
+
55
+ describe "#previously_visited_step" do
56
+ it "should return the most recently visited step" do
57
+ subject.steps[0].visited = true
58
+ subject.steps[1].visited = true
59
+ subject.steps[3].visited = true
60
+ subject.current_step = :step5
61
+ subject.previously_visited_step.to_sym.should == :step4
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,33 @@
1
+ ENV["RAILS_ENV"] ||= 'test'
2
+ require 'bundler/setup'
3
+ require 'logger'
4
+
5
+ module Rails
6
+ def self.root
7
+ Pathname.new(File.expand_path('../..', __FILE__))
8
+ end
9
+
10
+ def self.logger
11
+ @logger ||= ::Logger.new(STDOUT).tap { |l| l.level = ::Logger::ERROR }
12
+ end
13
+
14
+ class Engine
15
+ def self.initializer(*args, &block); end
16
+ end
17
+ end
18
+
19
+ module ActionController
20
+ class Base
21
+ end
22
+ end
23
+
24
+ require File.expand_path('../../lib/hat-trick', __FILE__)
25
+ require 'rspec/autorun'
26
+
27
+ # Requires supporting ruby files with custom matchers and macros, etc,
28
+ # in spec/support/ and its subdirectories.
29
+ Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
30
+
31
+ RSpec.configure do |config|
32
+ config.mock_with :mocha
33
+ end