hat-trick 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,8 +1,10 @@
1
1
  require 'hat_trick/model_methods'
2
+ require 'hat_trick/controller_helpers'
2
3
 
3
4
  module HatTrick
4
5
  module ControllerHooks
5
6
  extend ActiveSupport::Concern
7
+ include ControllerHelpers
6
8
 
7
9
  included do
8
10
  alias_method_chain :render, :hat_trick
@@ -10,12 +12,12 @@ module HatTrick
10
12
 
11
13
  def self.def_action_method_aliases(action_methods)
12
14
  action_methods.each do |meth|
13
- Rails.logger.info "Aliasing #{meth}"
15
+ # Rails.logger.info "Aliasing #{meth}"
14
16
  module_eval <<-RUBY_EVAL
15
17
  def #{meth}_with_hat_trick(*args)
16
18
  Rails.logger.info "#{meth}_with_hat_trick called"
17
19
  #{meth}_hook(*args) if respond_to?("#{meth}_hook", :include_private)
18
- common_hook(*args)
20
+ common_hook(*args) if respond_to?(:common_hook, :include_private)
19
21
  #{meth}_without_hat_trick(*args)
20
22
  end
21
23
  private "#{meth}_with_hat_trick"
@@ -26,53 +28,6 @@ module HatTrick
26
28
 
27
29
  private
28
30
 
29
- def params_model_name
30
- params.each do |k,v|
31
- return class_name(k) if v.is_a?(Hash) && is_model?(k)
32
- end
33
- nil
34
- end
35
-
36
- def is_model?(model_name)
37
- begin
38
- class_name(model_name).constantize
39
- rescue NameError
40
- return false
41
- end
42
- true
43
- end
44
-
45
- def class_name(hash_key)
46
- hash_key.to_s.camelize
47
- end
48
-
49
- def model_class
50
- model_name = params_model_name
51
- return nil if model_name.nil?
52
- begin
53
- model_class = params_model_name.constantize
54
- rescue NameError
55
- Rails.logger.error "Could not find model class #{params_model_name.camelize}"
56
- nil
57
- else
58
- model_class
59
- end
60
- end
61
-
62
- def setup_validation_group_for(wizard_step)
63
- klass = model_class
64
- return if klass.nil?
65
- step_name = wizard_step.name
66
- validation_groups = ::ActiveRecord::Base.validation_group_classes[klass] || []
67
- unless validation_groups.include?(step_name)
68
- klass.validation_group(step_name, :fields => params.keys)
69
- end
70
- HatTrick::ModelMethods.set_current_validation_group_for(model_class, step_name)
71
- unless klass.included_modules.include?(HatTrick::ModelMethods)
72
- klass.send(:include, HatTrick::ModelMethods)
73
- end
74
- end
75
-
76
31
  def create_hook(*args)
77
32
  setup_validation_group_for(ht_wizard.current_step)
78
33
  end
@@ -81,13 +36,10 @@ module HatTrick
81
36
  setup_validation_group_for(ht_wizard.current_step)
82
37
  end
83
38
 
84
- def common_hook(*args)
85
- # nothing here for now
86
- end
87
-
88
- def render_with_hat_trick(*args)
89
- if args.first.has_key?(:json)
90
- model = args[0][:json]
39
+ def render_with_hat_trick(*args, &block)
40
+ rendered = args.first
41
+ if rendered && rendered.has_key?(:json)
42
+ model = rendered[:json]
91
43
  ht_wizard.model = model
92
44
  end
93
45
 
@@ -102,17 +54,21 @@ module HatTrick
102
54
  :currentStep => ht_wizard.current_step,
103
55
  }
104
56
 
105
- # this sets the wizard_metadata for the initial page load
106
- gon.wizard_metadata = wizard_metadata
57
+ include_data = ht_wizard.include_data
58
+
59
+ # this sets the gon data (JS hatTrick object) for the initial page load
60
+ gon.metadata = wizard_metadata
61
+ gon.data = include_data
62
+ gon.model = ht_wizard.model
107
63
 
108
- if ht_wizard.model && args[0].has_key?(:json)
109
- # this sets the wizard metadata for subsequent AJAX requests
110
- args[0][:json] = { :formModel => ht_wizard.model,
111
- :wizardMetadata => wizard_metadata }
112
- args[0][:json].merge! ht_wizard.include_data
64
+ # this sets the wizard metadata for subsequent AJAX requests
65
+ if ht_wizard.model && rendered.has_key?(:json)
66
+ args[0][:json] = { :model => ht_wizard.model,
67
+ :metadata => wizard_metadata }
68
+ args[0][:json].merge!( :data => include_data )
113
69
  end
114
70
 
115
- render_without_hat_trick(*args)
71
+ render_without_hat_trick(*args, &block)
116
72
  end
117
73
  end
118
74
  end
data/lib/hat_trick/dsl.rb CHANGED
@@ -1,75 +1,74 @@
1
1
  require 'hat_trick/wizard_definition'
2
2
  require 'hat_trick/controller_hooks'
3
+ require 'hat_trick/config'
3
4
 
4
5
  module HatTrick
5
6
  module DSL
6
7
  extend ActiveSupport::Concern
7
8
 
8
- module ClassMethods
9
- def wizard(&block)
10
- if block_given?
11
- include HatTrick::DSL::ControllerInstanceMethods
12
- include HatTrick::ControllerHooks
9
+ attr_accessor :ht_wizard, :configure_callback, :_ht_config
13
10
 
14
- @wizard_def = HatTrick::WizardDefinition.new
11
+ delegate :model, :previously_visited_step, :to => :ht_wizard
15
12
 
16
- @wizard_dsl = HatTrick::DSL::WizardContext.new(@wizard_def)
17
- @wizard_dsl.instance_eval &block
13
+ included do
14
+ alias_method_chain :initialize, :hat_trick
15
+ end
18
16
 
19
- else
20
- raise ArgumentError, "wizard called without a block"
21
- end
17
+ def initialize_with_hat_trick(*args)
18
+ @_ht_config = HatTrick::Config.new(self.class.wizard_def)
19
+ if configure_callback.is_a?(Proc)
20
+ ht_wizard.controller.instance_exec(@_ht_config, &configure_callback)
22
21
  end
22
+ initialize_without_hat_trick(*args)
23
23
  end
24
24
 
25
- module ControllerInstanceMethods
26
- extend ActiveSupport::Concern
27
-
28
- included do
29
- before_filter :setup_wizard
25
+ def next_step(name=nil)
26
+ if name.nil?
27
+ # getter
28
+ ht_wizard.next_step
29
+ else
30
+ # setter
31
+ step = ht_wizard.find_step(name)
32
+ # explicitly set steps should not be skipped
33
+ step.skipped = false
34
+ ht_wizard.current_step.next_step = step
30
35
  end
36
+ end
31
37
 
32
- private
38
+ def skip_this_step
39
+ ht_wizard.skip_step(ht_wizard.current_step)
40
+ end
33
41
 
34
- attr_reader :ht_wizard
42
+ module ClassMethods
43
+ attr_reader :wizard_def
35
44
 
36
- def setup_wizard
37
- wizard_def = self.class.instance_variable_get("@wizard_def")
38
- @ht_wizard = wizard_def.get_wizard(self)
45
+ def wizard(&block)
46
+ if block_given?
47
+ include HatTrick::DSL::ControllerInstanceMethods
48
+ include HatTrick::ControllerHooks
39
49
 
40
- if params.has_key?('_ht_meta')
41
- step_name = params['_ht_meta']['step']
42
- @ht_wizard.current_step = step_name if step_name
43
- end
44
- end
45
- end
50
+ @wizard_def = HatTrick::WizardDefinition.new
46
51
 
47
- class WizardContext
48
- attr_reader :wizard_def
49
- attr_accessor :wizard
52
+ yield
50
53
 
51
- delegate :model, :previously_visited_step, :to => :wizard
54
+ else
55
+ raise ArgumentError, "wizard called without a block"
56
+ end
57
+ end
52
58
 
53
- def initialize(wizard_def)
54
- @wizard_def = wizard_def
59
+ def configure(&block)
60
+ raise "Must pass a block to configure" unless block_given?
61
+ @config_callback = block
55
62
  end
56
63
 
57
64
  def step(name, args={}, &block)
65
+ raise "step must be called from within a wizard block" unless wizard_def
58
66
  wizard_def.add_step(name, args)
59
67
  instance_eval &block if block_given?
60
68
  end
61
69
 
62
- def next_step(name=nil)
63
- if name.nil?
64
- wizard.next_step
65
- else
66
- raise "next_step should only be called from an after block" if wizard.nil?
67
- step = wizard.find_step(name)
68
- wizard.current_step.next_step = step
69
- end
70
- end
71
-
72
70
  def repeat_step(name)
71
+ raise "repeat_step must be called from within a wizard block" unless wizard_def
73
72
  repeated_step = wizard_def.find_step(name)
74
73
  raise ArgumentError, "Couldn't find step named #{name}" unless repeated_step
75
74
  new_step = repeated_step.dup
@@ -77,47 +76,72 @@ module HatTrick
77
76
  new_step.fieldset = repeated_step.fieldset
78
77
  # but use the current step's name
79
78
  new_step.name = wizard_def.last_step.name
80
- if wizard
81
- # TODO: Might turn all these into run-time methods; which would get
82
- # rid of this wizard / wizard_def distinction
83
- else
84
- # replace the step we're in the middle of defining w/ new_step
85
- wizard_def.replace_step(wizard_def.last_step, new_step)
86
- end
79
+ # replace the step we're in the middle of defining w/ new_step
80
+ wizard_def.replace_step(wizard_def.last_step, new_step)
87
81
  end
88
82
 
89
83
  def skip_this_step
90
- if wizard
91
- wizard.skip_step(wizard.current_step)
92
- else
93
- # skip_this_step in wizard definition context means the step
94
- # can be explicitly jumped to, but won't be in the normal flow
95
- wizard_def.last_step.skipped = true
96
- end
84
+ # skip_this_step in wizard definition (class) context means the step
85
+ # can be explicitly jumped to, but won't be visited in the normal flow
86
+ raise "skip_this_step must be called from within a wizard block" unless wizard_def
87
+ wizard_def.last_step.skipped = true
97
88
  end
98
89
 
99
90
  def button_to(name, options=nil)
100
- if wizard
101
- raise "button_to not yet supported in before/after blocks"
102
- end
91
+ raise "button_to must be called from within a wizard block" unless wizard_def
103
92
  label = options[:label] if options
104
93
  label ||= name.to_s.humanize
94
+
95
+ id = options[:id] if options
96
+
105
97
  step = wizard_def.last_step
106
- step.buttons = step.buttons.merge(name => label)
98
+ button = { :label => label }
99
+ button[:id] = id unless id.nil?
100
+ step.buttons = step.buttons.merge(name => button)
107
101
  end
108
102
 
109
103
  def before(&block)
104
+ raise "before must be called from within a wizard block" unless wizard_def
110
105
  wizard_def.last_step.before_callback = block
111
106
  end
112
107
 
113
108
  def after(&block)
109
+ raise "after must be called from within a wizard block" unless wizard_def
114
110
  wizard_def.last_step.after_callback = block
115
111
  end
116
112
 
117
113
  def include_data(key, &block)
114
+ raise "include_data must be called from within a wizard block" unless wizard_def
118
115
  wizard_def.last_step.include_data = { key.to_sym => block }
119
116
  end
117
+
118
+ def set_contents(&block)
119
+ raise "set_contents must be called from within a wizard block" unless wizard_def
120
+ current_step_name = wizard_def.last_step.to_sym
121
+ include_data "hat_trick_step_contents" do |wiz, model|
122
+ { current_step_name => instance_exec(wiz, model, &block) }
123
+ end
124
+ end
125
+ end
126
+
127
+ module ControllerInstanceMethods
128
+ extend ActiveSupport::Concern
129
+
130
+ included do
131
+ before_filter :setup_wizard
132
+ end
133
+
134
+ private
135
+
136
+ def setup_wizard
137
+ wizard_def = self.class.instance_variable_get("@wizard_def")
138
+ @ht_wizard = wizard_def.get_wizard(self)
139
+
140
+ if params.has_key?('_ht_meta')
141
+ step_name = params['_ht_meta']['step']
142
+ @ht_wizard.current_step = step_name if step_name
143
+ end
144
+ end
120
145
  end
121
146
  end
122
147
  end
123
-
@@ -1,7 +1,7 @@
1
1
  require 'hat_trick/form_helper'
2
2
 
3
3
  module HatTrick
4
- class RailsEngine < ::Rails::Engine
4
+ class Engine < ::Rails::Engine
5
5
  # just defining this causes Rails to look for assets inside this gem
6
6
 
7
7
  initializer 'hat-trick.form_helpers' do
@@ -7,10 +7,6 @@ module HatTrick
7
7
  wizard = controller.send(:ht_wizard)
8
8
  wizard.start unless wizard.started?
9
9
 
10
- # Do we still need these 2 lines?
11
- wizard.model = record
12
- controller.gon.form_model = record
13
-
14
10
  options[:url] = wizard.current_form_url
15
11
  options[:method] = wizard.current_form_method.to_sym
16
12
 
@@ -1,24 +1,46 @@
1
1
  module HatTrick
2
2
  module ModelMethods
3
3
  extend ActiveSupport::Concern
4
- mattr_accessor :current_validation_group
4
+ mattr_accessor :validation_groups
5
+
6
+ attr_accessor :_dummy # so the dummy field will have something to set
5
7
 
6
8
  included do
7
- alias_method_chain :save, :hat_trick
9
+ alias_method_chain :perform_validations, :hat_trick if instance_methods.include?(:perform_validations)
10
+ alias_method_chain :as_json, :model_name if instance_methods.include?(:as_json)
8
11
  end
9
12
 
10
13
  def self.set_current_validation_group_for(klass, validation_group_name)
11
- self.current_validation_group ||= {}
12
- current_validation_group[klass.to_s.underscore] = validation_group_name
14
+ self.validation_groups ||= {}
15
+ validation_groups[klass.to_s.underscore] = validation_group_name
13
16
  end
14
17
 
15
18
  def self.current_validation_group_for(klass)
16
- current_validation_group[klass.to_s.underscore]
19
+ return nil unless validation_groups
20
+ validation_groups[klass.to_s.underscore]
21
+ end
22
+
23
+ def perform_validations_with_hat_trick(*args, &block)
24
+ enable_current_validation_group
25
+ perform_validations_without_hat_trick(*args, &block)
26
+ end
27
+
28
+ def as_json_with_model_name(*args, &block)
29
+ json = as_json_without_model_name(*args, &block)
30
+ json.merge! :__name__ => self.class.to_s.underscore if json.respond_to?(:merge!)
31
+ end
32
+
33
+ private
34
+
35
+ # don't call this method 'current_validation_group', it conflicts with
36
+ # the gem
37
+ def current_step_validation_group
38
+ HatTrick::ModelMethods.current_validation_group_for(self.class)
17
39
  end
18
40
 
19
- def save_with_hat_trick(*args)
20
- enable_validation_group HatTrick::ModelMethods.current_validation_group_for(self.class)
21
- save_without_hat_trick(*args)
41
+ def enable_current_validation_group
42
+ validation_group = current_step_validation_group
43
+ enable_validation_group validation_group if validation_group
22
44
  end
23
45
  end
24
46
  end
@@ -17,7 +17,7 @@ module HatTrick
17
17
  end
18
18
 
19
19
  def initialize_copy(source)
20
- @callbacks = source.callbacks.dup
20
+ @callbacks = {}
21
21
  @buttons = source.buttons.dup
22
22
  @skipped = false
23
23
  @repeat_of = source
@@ -74,27 +74,27 @@ module HatTrick
74
74
  @include_data_key = hash.keys.first
75
75
  end
76
76
 
77
- def run_before_callback(context, wizard_dsl, model)
78
- run_callback(:before, context, wizard_dsl, model)
77
+ def run_before_callback(context, model)
78
+ run_callback(:before, context, model)
79
79
  end
80
80
 
81
- def run_include_data_callback(context, wizard_dsl, model)
82
- run_callback(:include_data, context, wizard_dsl, model)
81
+ def run_include_data_callback(context, model)
82
+ run_callback(:include_data, context, model)
83
83
  end
84
84
 
85
- def run_after_callback(context, wizard_dsl, model)
86
- run_callback(:after, context, wizard_dsl, model)
85
+ def run_after_callback(context, model)
86
+ run_callback(:after, context, model)
87
87
  end
88
88
 
89
89
  protected
90
90
 
91
- def run_callback(type, context, *args)
91
+ def run_callback(type, context, model)
92
92
  callback = callbacks[type.to_sym]
93
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
94
+ if callback.arity > 0
95
+ unless model.is_a?(ActiveModel::Errors)
96
+ context.instance_exec model, &callback
97
+ end
98
98
  else
99
99
  context.instance_eval &callback
100
100
  end
@@ -1,3 +1,3 @@
1
1
  module HatTrick
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -1,17 +1,16 @@
1
1
  require 'hat_trick/controller_hooks'
2
2
  require 'hat_trick/wizard_steps'
3
+ require 'hat_trick/dsl'
3
4
 
4
5
  module HatTrick
5
6
  class Wizard
6
7
  include WizardSteps
7
8
 
8
9
  attr_accessor :controller, :model
9
- attr_reader :current_step, :wizard_def, :wizard_dsl_context, :steps
10
+ attr_reader :current_step, :wizard_def, :steps
10
11
 
11
12
  def initialize(wizard_def)
12
13
  @wizard_def = wizard_def
13
- @wizard_dsl_context = DSL::WizardContext.new(@wizard_def)
14
- @wizard_dsl_context.wizard = self
15
14
  @steps = @wizard_def.steps.map { |s| HatTrick::Step.new(s, self) }
16
15
  end
17
16
 
@@ -19,6 +18,13 @@ module HatTrick
19
18
  @controller = new_controller
20
19
  end
21
20
 
21
+ def model=(new_model)
22
+ @model = new_model
23
+ unless @model.class.included_modules.include?(HatTrick::ModelMethods)
24
+ @model.class.send(:include, HatTrick::ModelMethods)
25
+ end
26
+ end
27
+
22
28
  def current_step=(_step)
23
29
  raise "Don't set current_step to nil" if _step.nil?
24
30
  step = find_step(_step)
@@ -27,7 +33,11 @@ module HatTrick
27
33
  end
28
34
 
29
35
  def session
30
- controller.session unless controller.nil?
36
+ if controller.nil?
37
+ fake_session
38
+ else
39
+ controller.session
40
+ end
31
41
  end
32
42
 
33
43
  def model_created?
@@ -43,11 +53,13 @@ module HatTrick
43
53
  end
44
54
 
45
55
  def create_url
56
+ wizard_def.configured_create_url or
46
57
  controller.url_for(:controller => controller.controller_name,
47
58
  :action => 'create', :only_path => true)
48
59
  end
49
60
 
50
61
  def update_url
62
+ wizard_def.configured_update_url or
51
63
  if model_created?
52
64
  controller.url_for(:controller => controller.controller_name,
53
65
  :action => 'update', :id => model,
@@ -80,28 +92,16 @@ module HatTrick
80
92
 
81
93
  def start
82
94
  session["hat-trick.steps_visited"] = []
83
- self.current_step = first_step
95
+ self.current_step ||= first_step
84
96
  run_before_callback
85
97
  end
86
98
 
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
99
  def run_before_callback(step=current_step)
94
- step.run_before_callback(controller, wizard_dsl_context, model)
100
+ step.run_before_callback(controller, model)
95
101
  end
96
102
 
97
103
  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
104
+ step.run_after_callback(controller, model)
105
105
  end
106
106
 
107
107
  def advance_step(next_step_name=nil)
@@ -114,7 +114,7 @@ module HatTrick
114
114
 
115
115
  # finish if we're on the last step
116
116
  if current_step == last_step && !requested_next_step
117
- finish
117
+ raise "Tried to advance beyond the last step of the wizard"
118
118
  else # we're not on the last step
119
119
  if requested_next_step
120
120
  Rails.logger.info "Force advancing to step: #{requested_next_step}"
@@ -135,10 +135,20 @@ module HatTrick
135
135
  end
136
136
 
137
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) }
138
+ return {} if model.nil?
139
+ inc_data = {}
140
+ include_data_steps = steps_before(current_step) << 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
151
+ inc_data
142
152
  end
143
153
 
144
154
  def alias_action_methods
@@ -155,6 +165,10 @@ module HatTrick
155
165
 
156
166
  private
157
167
 
168
+ def fake_session
169
+ @fake_session ||= {}
170
+ end
171
+
158
172
  def find_next_step
159
173
  find_step(current_step.next_step) or find_step_after(current_step)
160
174
  end
@@ -165,10 +179,13 @@ module HatTrick
165
179
  end
166
180
 
167
181
  def camelize_hash_keys(_hash)
168
- return nil if _hash.nil?
169
182
  hash = {}
170
- _hash.each do |k,v|
171
- hash[k.to_s.camelize(:lower)] = v
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
172
189
  end
173
190
  hash
174
191
  end
@@ -6,6 +6,8 @@ module HatTrick
6
6
  class WizardDefinition
7
7
  include WizardSteps
8
8
 
9
+ attr_accessor :configured_create_url, :configured_update_url
10
+
9
11
  def initialize
10
12
  @steps = []
11
13
  end
@@ -26,7 +26,9 @@ module HatTrick
26
26
  def steps_after(_step)
27
27
  step = find_step(_step)
28
28
  return [] unless step
29
- after_index = steps.index(step) + 1
29
+ step_index = steps.index(step)
30
+ max_index = steps.count - 1
31
+ after_index = step_index < max_index ? step_index + 1 : step_index
30
32
  steps[after_index .. -1]
31
33
  end
32
34
 
@@ -37,7 +39,8 @@ module HatTrick
37
39
  def steps_before(_step)
38
40
  step = find_step(_step)
39
41
  return [] unless step
40
- before_index = steps.index(step) - 1
42
+ step_index = steps.index(step)
43
+ before_index = step_index > 0 ? step_index - 1 : step_index
41
44
  steps[0 .. before_index]
42
45
  end
43
46
 
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ # will automatically get HatTrick::DSL included
4
+ class FakeController < ActionController::Base; end
5
+
6
+ describe HatTrick::DSL do
7
+ let(:controller_class) { FakeController }
8
+
9
+ # save some typing
10
+ def dsl(&block)
11
+ controller_class.instance_eval &block
12
+ end
13
+
14
+ describe HatTrick::DSL::ClassMethods do
15
+ describe "#step" do
16
+ it "should call Wizard#add_step" do
17
+ HatTrick::WizardDefinition.any_instance.expects(:add_step).with(:foo, {})
18
+ dsl do
19
+ wizard do
20
+ step :foo
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end