Browse files

added route helper

  • Loading branch information...
1 parent 99bf674 commit 3b72d6153bed48b942d2752f4fdcd1c919e8dc27 @ssendev committed Aug 26, 2012
View
24 README.md
@@ -85,6 +85,30 @@ The wizard is set to call steps in order in the show action, you can specify cus
**Note:** Wicked uses the `:id` parameter to control the flow of steps, if you need to have an id parameter, please use nested routes see [building objects with wicked](https://github.com/schneems/wicked/wiki/Partial-Validation-of-Active-Record-Objects) for an example. It will need to be prefixed, for example a Product's `:id` would be `:product_id`
+### Routing helpers
+
+The routing helper which was loaned from Wizardry(https://raw.github.com/lexmag/wizardry) allows for nicer routes
+
+```ruby
+resource :after_signup do
+ is_wicked
+end
+# same as
+wicked_resource :after_signup
+# or
+wicked_resources :products
+```
+
+will replace default *:id/edit* path with *:id/edit/:step* and update with *:id/:step*.
+`is_wicked` and wicked_resource also accepts a hash to further configure the behavior
+
+* `:step_constraint => /first/second/` Set to true to generate a regexp which allows only the steps which are assigned in the controller or pass a regexp.
+* `:step_optional => true` The route will be `*:id/edit(/:step)*` but be carefull not to shadow other routes.
+* `:step_parameter => :pets` Defaults to :step can but can be changed set to :id for backwards compatibility with the old routes.
+* `:show_step_action => :show` the action used to display the steps, defaults to :edit but can be set back to :show.
+wicked_resource(s) or the parent resource from is_wicked can take:
+* `:path_names => { :edit => 'amend' }` Changes the route to `*:id/amend/:step*` can be set to nil for `*:id/:step*`.
+
You'll need to call `render_wizard` at the end of your action to get the correct views to show up.
By default the wizard will render a view with the same name as the step. So for our controller `AfterSignupController` with a view path of `/views/after_signup/` if call the :confirm_password step, our wizard will render `/views/after_signup/confirm_password.html.erb`
View
4 lib/wicked.rb
@@ -1,3 +1,4 @@
+
module Wicked
module Controller
module Concerns
@@ -11,4 +12,5 @@ module Wizard
require 'wicked/controller/concerns/steps'
require 'wicked/controller/concerns/path'
require 'wicked/wizard'
-require 'wicked/engine'
+require 'wicked/engine'
+require 'wicked/routes'
View
4 lib/wicked/controller/concerns/path.rb
@@ -21,8 +21,8 @@ def wicked_action
def wizard_path(goto_step = nil, options = {})
options = { :controller => wicked_controller,
- :action => 'show',
- :id => goto_step || params[:id],
+ :action => wicked_step_action,
+ wicked_step_parameter.to_sym => goto_step || params[wicked_step_parameter],
:only_path => true
}.merge options
url_for(options)
View
2 lib/wicked/controller/concerns/steps.rb
@@ -47,6 +47,8 @@ module ClassMethods
def steps(*args)
options = args.extract_options!
steps = args
+ class_attribute :wicked_steps, instance_writer: false
+ self.wicked_steps = steps
prepend_before_filter(options) do
self.steps = steps
end
View
69 lib/wicked/routes.rb
@@ -0,0 +1,69 @@
+# inspired by https://github.com/lexmag/wizardry/blob/master/lib/wizardry/routes.rb
+
+class ActionDispatch::Routing::Mapper
+ def is_wicked(opts = {})
+ unless resource_scope?
+ raise ArgumentError, "can't use is_wicked outside resource(s) scope"
+ end
+
+ draw = { :update => true, :edit => true }
+
+ options = @scope[:scope_level_resource].options
+
+ except = Array.wrap(options.delete(:except)).map(&:to_sym)
+ only = Array.wrap(options.delete(:only)).map(&:to_sym)
+
+ only_was_present = only.present?
+ if only_was_present = only.present?
+ only.each { |action| except.delete(action) if except.include?(action) } # discard contradicting options
+ draw.each { |k,v| draw[k] = only.delete(k) == k }
+ end
+ draw.each { |k,v| draw[k] = v && ! except.include?(k) } if except.present?
+
+ options.merge!(only: only) if only_was_present
+ except += draw.keys
+ options.merge!(except: except.uniq)
+
+ controller = @scope[:controller].to_s.dup.concat('_controller').classify.constantize rescue nil
+ controller = @scope[:controller].to_s.singularize.concat('_controller').classify.constantize unless controller.present?
+
+ step_parameter = opts.delete(:step_parameter).try(:to_s) || 'step'
+ controller.class_attribute :wicked_step_parameter, instance_writer: false unless defined? controller.wicked_step_parameter
+ controller.wicked_step_parameter = step_parameter
+ step_action = opts.delete(:show_step_action).try(:to_s) || 'edit'
+ controller.class_attribute :wicked_step_action, instance_writer: false unless defined? controller.wicked_step_action
+ controller.wicked_step_action = step_action
+
+ path_name = 'edit'
+ path_name = @scope[:path_names][:edit] if @scope[:path_names].key? :edit
+ step = opts.delete(:step_optional) ? "(/:#{step_parameter})" : "/:#{step_parameter}"
+ step = step.sub(/\//, '') unless path_name
+ edit_params = {"#{path_name}#{step}" => step_action.to_sym, as: step_action.to_sym, on: :member}
+ update_params = {"#{step}" => :update, as: :update, on: :member}
+
+ step_constraint = opts.delete(:step_constraint)
+ step_constraint = Regexp.new controller.wicked_steps.join('|') if step_constraint == true
+ if step_constraint
+ edit_params.merge! step_parameter.to_sym => step_constraint
+ update_params.merge! step_parameter.to_sym => step_constraint
+ end
+
+ get edit_params if draw[:edit]
+ put update_params if draw[:update]
+
+ end
+
+ [:resources, :resource].each do |method|
+ class_eval <<-EOT, __FILE__, __LINE__ + 1
+ def wicked_#{method}(*res) # def wicked_resources(*res)
+ wicked_options = res.extract_options! # wicked_options = res.extract_options!
+ options = wicked_options.slice! :step_constraint, :step_optional, :step_parameter, :show_step_action # options = wicked_options.slice :step_constraint, :step_optional, :step_parameter, :show_step_action
+ res.push options if options.present? # res.push options if options.present?
+ #{method} *res do # resources *res do
+ is_wicked wicked_options # is_wicked wicked_options
+ yield if block_given? # yield if block_given?
+ end # end
+ end # end
+ EOT
+ end
+end
View
11 lib/wicked/wizard.rb
@@ -15,6 +15,11 @@ module Wizard
:next_step?
# Set @step and @next_step variables
before_filter :setup_wizard
+
+ self.class_attribute :wicked_step_parameter, instance_writer: false unless defined? wicked_step_parameter
+ self.wicked_step_parameter ||= :id
+ self.class_attribute :wicked_step_action, instance_writer: false unless defined? wicked_step_action
+ self.wicked_step_action ||= 'show'
end
def index
@@ -23,10 +28,10 @@ def index
private
def setup_wizard
- redirect_to wizard_path(steps.first) if params[:id].try(:to_sym) == :wizard_first
- redirect_to wizard_path(steps.last) if params[:id].try(:to_sym) == :wizard_last
+ redirect_to wizard_path(steps.first) if params[wicked_step_parameter].try(:to_sym) == :wizard_first
+ redirect_to wizard_path(steps.last) if params[wicked_step_parameter].try(:to_sym) == :wizard_last
- @step = params[:id].try(:to_sym) || steps.first
+ @step = params[wicked_step_parameter].try(:to_sym) || steps.first
@previous_step = previous_step(@step)
@next_step = next_step(@step)
end
View
14 test/dummy/app/controllers/bar2_controller.rb
@@ -0,0 +1,14 @@
+## This controller uses includes
+
+class Bar2Controller < ApplicationController
+ include Wicked::Wizard
+ steps :first, :second, :last_step
+
+ def edit
+ skip_step if params[:skip_step]
+ render_wizard
+ end
+
+ def update
+ end
+end
View
13 test/dummy/app/controllers/foo2_controller.rb
@@ -0,0 +1,13 @@
+## This controller uses inheritance
+
+class Foo2Controller < Wicked::WizardController
+ steps :first, :second, :last_step
+
+ def edit
+ skip_step if params[:skip_step]
+ render_wizard
+ end
+
+ def update
+ end
+end
View
22 test/dummy/app/controllers/jump2_controller.rb
@@ -0,0 +1,22 @@
+## This controller uses includes
+
+class Jump2Controller < ApplicationController
+ include Wicked::Wizard
+ steps :first, :second, :last_step
+
+ def edit
+ skip_step if params[:skip_step]
+ jump_to :last_step if params[:jump_to]
+ if params[:resource]
+ value = params[:resource][:save] == 'true'
+ @bar = Bar.new(value)
+ render_wizard(@bar)
+ else
+ render_wizard
+ end
+ end
+
+ def update
+ end
+
+end
View
6 test/dummy/app/controllers/routes_controller.rb
@@ -0,0 +1,6 @@
+## This controller uses includes
+
+class ProductsController < ApplicationController
+ include Wicked::Wizard
+ steps :first, :second, :last_step
+end
View
13 test/dummy/app/controllers/steps2_controller.rb
@@ -0,0 +1,13 @@
+## This controller uses includes
+
+class StepPositions2Controller < ApplicationController
+ include Wicked::Wizard
+ steps :first, :second, :last_step
+
+ def edit
+ render_wizard
+ end
+
+ def update
+ end
+end
View
5 test/dummy/app/views/bar2/first.html.erb
@@ -0,0 +1,5 @@
+first
+
+<%= link_to 'last', wizard_path(:last_step) %>
+<%= link_to 'current', wizard_path %>
+<%= link_to 'skip', next_wizard_path %>
View
3 test/dummy/app/views/bar2/last_step.html.erb
@@ -0,0 +1,3 @@
+last_step
+
+<%= "step #{wizard_steps.index(step) + 1 } of #{wizard_steps.count}" %>
View
3 test/dummy/app/views/bar2/second.html.erb
@@ -0,0 +1,3 @@
+second
+
+<%= link_to 'previous', previous_wizard_path %>
View
1 test/dummy/app/views/foo2/first.html.erb
@@ -0,0 +1 @@
+first
View
1 test/dummy/app/views/foo2/last_step.html.erb
@@ -0,0 +1 @@
+last_step
View
1 test/dummy/app/views/foo2/second.html.erb
@@ -0,0 +1 @@
+second
View
1 test/dummy/app/views/jump2/first.html.erb
@@ -0,0 +1 @@
+first
View
1 test/dummy/app/views/jump2/last_step.html.erb
@@ -0,0 +1 @@
+last_step
View
1 test/dummy/app/views/jump2/second.html.erb
@@ -0,0 +1 @@
+second
View
9 test/dummy/app/views/step_positions2/_step_position.html.erb
@@ -0,0 +1,9 @@
+<% wizard_steps.each do |s| %>
+ <p>
+ <%= "#{s} step is the current step" if current_step?(s) %><br />
+ <%= "#{s} step is a past step" if past_step?(s) %><br />
+ <%= "#{s} step is a future step" if future_step?(s) %><br />
+ <%= "#{s} step was the previous step" if previous_step?(s) %><br />
+ <%= "#{s} step is the next step" if next_step?(s) %><br />
+ </p>
+<% end %>
View
1 test/dummy/app/views/step_positions2/first.html.erb
@@ -0,0 +1 @@
+<%= render 'step_position' %>
View
1 test/dummy/app/views/step_positions2/last_step.html.erb
@@ -0,0 +1 @@
+<%= render 'step_position' %>
View
1 test/dummy/app/views/step_positions2/second.html.erb
@@ -0,0 +1 @@
+<%= render 'step_position' %>
View
8 test/dummy/config/routes.rb
@@ -1,8 +1,16 @@
Dummy::Application.routes.draw do
resources :foo
+ wicked_resource :foo2, controller: :foo2
resources :bar
+ wicked_resource :bar2, controller: :bar2
resources :jump
+ wicked_resource :jump2, controller: :jump2
resources :step_positions
+ wicked_resource :step_positions2, controller: :step_positions2
+ resources :products, except: :destroy, path_names: { new: :make } do
+ get :commit, on: :collection
+ is_wicked step_constraint: true
+ end
# The priority is based upon order of creation:
# first created -> highest priority.
View
15 test/integration/jump_test.rb
@@ -13,4 +13,19 @@ class JumpNavigationTest < ActiveSupport::IntegrationCase
assert has_content?('first')
assert !has_content?('last_step')
end
+end
+
+class JumpNavigationEditTest < ActiveSupport::IntegrationCase
+ test 'consider jump_to when calling render_wizard with resource' do
+ step = :first
+ visit(edit_jump2_path(nil, step: step, :resource => {:save => true}, :jump_to => :last_step))
+ assert has_content?('last_step')
+ end
+
+ test 'disregard jump_to when saving the resource fails' do
+ step = :first
+ visit(edit_jump2_path(nil, step: step, :resource => {:save => false}, :jump_to => :last_step))
+ assert has_content?('first')
+ assert !has_content?('last_step')
+ end
end
View
77 test/integration/navigation_test.rb
@@ -40,6 +40,39 @@ class InheritNavigationTest < ActiveSupport::IntegrationCase
end
+class InheritNavigationEditTest < ActiveSupport::IntegrationCase
+ test 'edit first' do
+ step = :first
+ visit(edit_foo2_path(nil, step: step))
+ assert has_content?(step.to_s)
+ end
+
+ test 'edit second' do
+ step = :second
+ visit(edit_foo2_path(nil, step: step))
+ assert has_content?(step.to_s)
+ end
+
+ test 'skip first edit' do
+ step = :first
+ visit(edit_foo2_path(nil, skip_step: 'true', step: step))
+ assert has_content?('second')
+ end
+
+ test 'invalid edit step' do
+ step = :notastep
+ assert_raise(ActionView::MissingTemplate) do
+ visit(edit_foo2_path(nil, step: step))
+ end
+ end
+
+ test 'finish edit' do
+ step = :finish
+ visit(edit_foo2_path(nil, step: step))
+ assert has_content?('home')
+ end
+
+end
class IncludeNavigationTest < ActiveSupport::IntegrationCase
@@ -83,6 +116,50 @@ class IncludeNavigationTest < ActiveSupport::IntegrationCase
visit(bar_path(step))
assert has_content?('home')
end
+
end
+class IncludeNavigationEditTest < ActiveSupport::IntegrationCase
+ test 'edit first' do
+ step = :first
+ visit(edit_bar2_path(step))
+ assert has_content?(step.to_s)
+ end
+
+ test 'edit second' do
+ step = :second
+ visit(edit_bar2_path(step))
+ assert has_content?(step.to_s)
+ end
+
+ test 'skip first edit' do
+ step = :first
+ visit(edit_bar2_path(step, :skip_step => 'true'))
+ assert has_content?(:second.to_s)
+ end
+
+ test 'pointer to first edit' do
+ visit(edit_bar2_path(:wizard_first))
+ assert has_content?('first')
+ end
+
+ test 'pointer to last edit' do
+ visit(edit_bar2_path(:wizard_last))
+ assert has_content?('last_step')
+ end
+
+ test 'invalid edit step' do
+ step = :notastep
+ assert_raise(ActionView::MissingTemplate) do
+ visit(edit_bar2_path(step))
+ end
+ end
+
+ test 'finish edit' do
+ step = :finish
+ visit(edit_bar2_path(step))
+ assert has_content?('home')
+ end
+
+end
View
151 test/integration/routes_test.rb
@@ -0,0 +1,151 @@
+# mostly from https://github.com/lexmag/wizardry/blob/master/spec/wizardry/routes_spec.rb
+
+require 'test_helper'
+
+class RouteHelperTest < ActionController::TestCase
+
+ include Rails.application.routes.url_helpers
+
+
+ test 'must accept wizard `edit` routes' do
+ assert_recognizes({ controller: 'products', action: 'edit', id: '1', step: 'first'}, '/products/1/edit/first')
+ assert_recognizes({ controller: 'products', action: 'edit', id: '1', step: 'second'}, '/products/1/edit/second')
+ assert_recognizes({ controller: 'products', action: 'edit', id: '1', step: 'last_step'}, '/products/1/edit/last_step')
+ assert '/products/1/first', edit_product_path(id: 1, step: 'first')
+ end
+
+ test 'must accept only valid steps' do
+ assert_raises ActionController::RoutingError do
+ assert_recognizes({ controller: 'products', action: 'edit', id: '1', step: 'fictional'}, '/products/1/edit/fictional')
+ end
+ end
+
+ test 'must not accept default `edit` route' do
+ assert_raises ActionController::RoutingError do
+ assert_recognizes({ controller: 'products', action: 'edit', id: '1'}, '/products/1/edit')
+ end
+ end
+
+ test 'must not accept `destroy` route' do
+ assert_raises ActionController::RoutingError do
+ assert_recognizes({ controller: 'products', action: 'destroy', id: '1'}, { path: '/products/1', method: :delete })
+ end
+ end
+
+ test 'must accept routes defined in block' do
+ assert_recognizes({ controller: 'products', action: 'commit' }, '/products/commit')
+ assert '/products/commit', commit_products_path
+ end
+
+ test 'must have overriden `new` path name' do
+ assert_recognizes({ controller: 'products', action: 'new' }, '/products/make')
+ assert '/products/make', new_product_path
+ end
+
+ test 'must have `wicked_resources` helper' do
+ with_routing do |set|
+ set.draw{ wicked_resources :products }
+
+ assert_recognizes({ controller: 'products', action: 'edit', id: '1', step: 'initial'}, '/products/1/edit/initial')
+ end
+ end
+
+ test 'must have `wicked_resource` helper' do
+ with_routing do |set|
+ set.draw{ wicked_resource :products }
+
+ assert_recognizes({ controller: 'products', action: 'edit', step: 'initial'}, '/products/edit/initial')
+ end
+ end
+
+ test 'must not let to use `is_wicked` outside resource(s) scope' do
+ with_routing do |set|
+ assert_raises ArgumentError do
+ set.draw{ is_wicked }
+ end
+ end
+ end
+
+ test 'must not accept default `edit` route with `only` option' do
+ with_routing do |set|
+ set.draw{ wicked_resources :products, only: [:edit, :update] }
+
+ assert_raises ActionController::RoutingError do
+ assert_recognizes({ controller: 'products', action: 'edit', id: '1'}, '/products/1/edit')
+ end
+ end
+ end
+
+ test 'must have only wicked routes' do
+ with_routing do |set|
+ set.draw{ wicked_resource :products, only: [:edit, :update] }
+
+ assert_recognizes({ controller: 'products', action: 'edit', step: 'first'}, '/products/edit/first')
+ assert_recognizes({ controller: 'products', action: 'update', step: 'initial'}, { path: '/products/initial', method: :put })
+ assert_equal 2, set.routes.count
+ end
+ end
+
+ test 'must not have edit, update routes' do
+ with_routing do |set|
+ set.draw{ wicked_resource :products, except: [:edit, :update], }
+
+ assert_raises ActionController::RoutingError do
+ assert_recognizes({ controller: 'products', action: 'edit', step: 'first'}, '/products/edit/first')
+ end
+ assert_raises ActionController::RoutingError do
+ assert_recognizes({ controller: 'products', action: 'edit'}, '/products/edit')
+ end
+ assert_raises ActionController::RoutingError do
+ assert_recognizes({ controller: 'products', action: 'update', step: 'first'}, { path: '/products/first', method: :put })
+ end
+ assert_raises ActionController::RoutingError do
+ assert_recognizes({ controller: 'products', action: 'update'}, { path: '/products', method: :put })
+ end
+ assert_equal 4, set.routes.count
+ end
+ end
+
+ test 'must only have update route' do
+ with_routing do |set|
+ set.draw{ wicked_resource :products, except: [:edit, :update], only: :update }
+
+ assert_recognizes({ controller: 'products', action: 'update', step: 'first'}, { path: '/products/first', method: :put })
+ assert_equal 1, set.routes.count
+ end
+ end
+
+ test 'must only have edit route' do
+ with_routing do |set|
+ set.draw{ wicked_resource :products, only: :edit }
+
+ assert_recognizes({ controller: 'products', action: 'edit', step: 'first'}, '/products/edit/first')
+ assert_equal 1, set.routes.count
+ end
+ end
+
+ test 'must rename edit route' do
+ with_routing do |set|
+ set.draw{ wicked_resource :products, path_names: { edit: :amend } }
+
+ assert_recognizes({ controller: 'products', action: 'edit', step: 'first'}, '/products/amend/first')
+ end
+ end
+
+ test 'must change edit route to omit /edit' do
+ with_routing do |set|
+ set.draw{ wicked_resource :products, path_names: { edit: nil } }
+
+ assert_recognizes({ controller: 'products', action: 'edit', step: 'first'}, '/products/first')
+ end
+ end
+
+ test 'step must be optional' do
+ with_routing do |set|
+ set.draw{ wicked_resource :products, step_optional: true }
+ assert_recognizes({ controller: 'products', action: 'edit', step: 'first'}, '/products/edit/first')
+ assert_recognizes({ controller: 'products', action: 'edit'}, '/products/edit')
+ end
+ end
+
+end
View
25 test/integration/steps_test.rb
@@ -25,6 +25,31 @@ class StepPositionsTest < ActiveSupport::IntegrationCase
end
+class StepPositionsEditTest < ActiveSupport::IntegrationCase
+
+ test 'on first' do
+ step = :first
+ visit(edit_step_positions2_path(step))
+ assert has_content?('first step is the current step') # current_step?
+ assert true # past_step?
+ assert has_content?('last_step step is a future step') # future_step?
+ assert has_content?('second step is a future step') # future_step?
+ assert true # previous_step?
+ assert has_content?('second step is the next step') # next_step?
+ end
+
+ test 'on second' do
+ step = :second
+ visit(edit_step_positions2_path(step))
+ assert has_content?('second step is the current step') # current_step?
+ assert has_content?('first step is a past step') # past_step?
+ assert has_content?('last_step step is a future step') # future_step?
+ assert has_content?('first step was the previous step') # previous_step?
+ assert has_content?('last_step step is the next step') # next_step?
+ end
+
+end
+
# current_step?
# past_step?
# future_step?

0 comments on commit 3b72d61

Please sign in to comment.