wizardly 0.1.8.9

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.
Files changed (37) hide show
  1. data/CHANGELOG.rdoc +33 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +645 -0
  4. data/init.rb +1 -0
  5. data/lib/jeffp-wizardly.rb +1 -0
  6. data/lib/validation_group.rb +147 -0
  7. data/lib/wizardly.rb +31 -0
  8. data/lib/wizardly/action_controller.rb +36 -0
  9. data/lib/wizardly/wizard.rb +16 -0
  10. data/lib/wizardly/wizard/button.rb +35 -0
  11. data/lib/wizardly/wizard/configuration.rb +194 -0
  12. data/lib/wizardly/wizard/configuration/methods.rb +422 -0
  13. data/lib/wizardly/wizard/dsl.rb +27 -0
  14. data/lib/wizardly/wizard/page.rb +62 -0
  15. data/lib/wizardly/wizard/text_helpers.rb +16 -0
  16. data/lib/wizardly/wizard/utils.rb +11 -0
  17. data/rails_generators/wizardly_app/USAGE +6 -0
  18. data/rails_generators/wizardly_app/templates/wizardly.rake +37 -0
  19. data/rails_generators/wizardly_app/wizardly_app_generator.rb +41 -0
  20. data/rails_generators/wizardly_controller/USAGE +3 -0
  21. data/rails_generators/wizardly_controller/templates/controller.rb.erb +34 -0
  22. data/rails_generators/wizardly_controller/templates/helper.rb.erb +14 -0
  23. data/rails_generators/wizardly_controller/wizardly_controller_generator.rb +57 -0
  24. data/rails_generators/wizardly_scaffold/USAGE +4 -0
  25. data/rails_generators/wizardly_scaffold/templates/form.html.erb +23 -0
  26. data/rails_generators/wizardly_scaffold/templates/form.html.haml.erb +22 -0
  27. data/rails_generators/wizardly_scaffold/templates/helper.rb.erb +30 -0
  28. data/rails_generators/wizardly_scaffold/templates/images/back.png +0 -0
  29. data/rails_generators/wizardly_scaffold/templates/images/cancel.png +0 -0
  30. data/rails_generators/wizardly_scaffold/templates/images/finish.png +0 -0
  31. data/rails_generators/wizardly_scaffold/templates/images/next.png +0 -0
  32. data/rails_generators/wizardly_scaffold/templates/images/skip.png +0 -0
  33. data/rails_generators/wizardly_scaffold/templates/layout.html.erb +15 -0
  34. data/rails_generators/wizardly_scaffold/templates/layout.html.haml.erb +10 -0
  35. data/rails_generators/wizardly_scaffold/templates/style.css +54 -0
  36. data/rails_generators/wizardly_scaffold/wizardly_scaffold_generator.rb +109 -0
  37. metadata +90 -0
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'wizardly'
@@ -0,0 +1 @@
1
+ require 'wizardly'
@@ -0,0 +1,147 @@
1
+ module ValidationGroup
2
+ module ActiveRecord
3
+ module ActsMethods # extends ActiveRecord::Base
4
+ def self.extended(base)
5
+ # Add class accessor which is shared between all models and stores
6
+ # validation groups defined for each model
7
+ base.class_eval do
8
+ cattr_accessor :validation_group_classes
9
+ self.validation_group_classes = {}
10
+
11
+ def self.validation_group_order; @validation_group_order; end
12
+ def self.validation_groups(all_classes = false)
13
+ return (self.validation_group_classes[self] || {}) unless all_classes
14
+ klasses = ValidationGroup::Util.current_and_ancestors(self).reverse
15
+ returning Hash.new do |hash|
16
+ klasses.each do |klass|
17
+ hash.merge! self.validation_group_classes[klass]
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ def validation_group(name, options={})
25
+ self_groups = (self.validation_group_classes[self] ||= {})
26
+ self_groups[name.to_sym] = case options[:fields]
27
+ when Array then options[:fields]
28
+ when Symbol, String then [options[:fields].to_sym]
29
+ else []
30
+ end
31
+ # jeffp: capture the declaration order for this class only (no
32
+ # superclasses)
33
+ (@validation_group_order ||= []) << name.to_sym
34
+
35
+ unless included_modules.include?(InstanceMethods)
36
+ # jeffp: added reader for current_validation_fields
37
+ attr_reader :current_validation_group, :current_validation_fields
38
+ include InstanceMethods
39
+ # jeffp: add valid?(group = nil), see definition below
40
+ alias_method_chain :valid?, :validation_group
41
+ end
42
+ end
43
+ end
44
+
45
+ module InstanceMethods # included in every model which calls validation_group
46
+ #needs testing
47
+ # def reset_fields_for_validation_group(group)
48
+ # group_classes = self.class.validation_group_classes
49
+ # found = ValidationGroup::Util.current_and_ancestors(self.class).find do |klass|
50
+ # group_classes[klass] && group_classes[klass].include?(group)
51
+ # end
52
+ # if found
53
+ # group_classes[found][group].each do |field|
54
+ # self[field] = nil
55
+ # end
56
+ # end
57
+ # end
58
+ def enable_validation_group(group)
59
+ # Check if given validation group is defined for current class or one of
60
+ # its ancestors
61
+ group_classes = self.class.validation_group_classes
62
+ found = ValidationGroup::Util.current_and_ancestors(self.class).
63
+ find do |klass|
64
+ group_classes[klass] && group_classes[klass].include?(group)
65
+ end
66
+ if found
67
+ @current_validation_group = group
68
+ # jeffp: capture current fields for performance optimization
69
+ @current_validation_fields = group_classes[found][group]
70
+ else
71
+ raise ArgumentError, "No validation group of name :#{group}"
72
+ end
73
+ end
74
+
75
+ def disable_validation_group
76
+ @current_validation_group = nil
77
+ # jeffp: delete fields
78
+ @current_validation_fields = nil
79
+ end
80
+
81
+ def reject_non_validation_group_errors
82
+ return unless validation_group_enabled?
83
+ self.errors.remove_on(@current_validation_fields)
84
+ end
85
+
86
+ # jeffp: optimizer for someone writing custom :validate method -- no need
87
+ # to validate fields outside the current validation group note: could also
88
+ # use in validation modules to improve performance
89
+ def should_validate?(attribute)
90
+ !self.validation_group_enabled? || (@current_validation_fields && @current_validation_fields.include?(attribute.to_sym))
91
+ end
92
+
93
+ def validation_group_enabled?
94
+ respond_to?(:current_validation_group) && !current_validation_group.nil?
95
+ end
96
+
97
+ # eliminates need to use :enable_validation_group before :valid? call --
98
+ # nice
99
+ def valid_with_validation_group?(group=nil)
100
+ self.enable_validation_group(group) if group
101
+ valid_without_validation_group?
102
+ end
103
+ end
104
+
105
+ module Errors # included in ActiveRecord::Errors
106
+ def add_with_validation_group(attribute,
107
+ msg = @@default_error_messages[:invalid], *args,
108
+ &block)
109
+ # jeffp: setting @current_validation_fields and use of should_validate? optimizes code
110
+ add_error = @base.respond_to?(:should_validate?) ? @base.should_validate?(attribute.to_sym) : true
111
+ add_without_validation_group(attribute, msg, *args, &block) if add_error
112
+ end
113
+
114
+ def remove_on(attributes)
115
+ return unless attributes
116
+ attributes = [attributes] unless attributes.is_a?(Array)
117
+ @errors.reject!{|k,v| !attributes.include?(k.to_sym)}
118
+ end
119
+
120
+ def self.included(base) #:nodoc:
121
+ base.class_eval do
122
+ alias_method_chain :add, :validation_group
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ module Util
129
+ # Return array consisting of current and its superclasses down to and
130
+ # including base_class.
131
+ def self.current_and_ancestors(current)
132
+ returning [] do |klasses|
133
+ klasses << current
134
+ root = current.base_class
135
+ until current == root
136
+ current = current.superclass
137
+ klasses << current
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ # jeffp: moved from init.rb for gemification purposes --
145
+ # require 'validation_group' loads everything now, init.rb requires 'validation_group' only
146
+ ActiveRecord::Base.send(:extend, ValidationGroup::ActiveRecord::ActsMethods)
147
+ ActiveRecord::Errors.send :include, ValidationGroup::ActiveRecord::Errors
@@ -0,0 +1,31 @@
1
+ require 'validation_group'
2
+ require 'wizardly/action_controller'
3
+
4
+ module Wizardly
5
+ module ActionController
6
+ module MacroMethods
7
+ def wizard_for_model(model, opts={}, &block)
8
+ include Wizardly::ActionController
9
+ #check for validation group gem
10
+ configure_wizard_for_model(model, opts, &block)
11
+ end
12
+ alias_method :act_wizardly_for, :wizard_for_model
13
+ end
14
+ end
15
+ end
16
+
17
+ begin
18
+ ActiveRecord::Base.class_eval do
19
+ class << self
20
+ alias_method :wizardly_page, :validation_group
21
+ end
22
+ end
23
+ rescue
24
+ end
25
+
26
+ ActionController::Base.send(:extend, Wizardly::ActionController::MacroMethods)
27
+
28
+
29
+
30
+
31
+
@@ -0,0 +1,36 @@
1
+ require 'wizardly/wizard'
2
+
3
+ module Wizardly
4
+ module ActionController
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+
8
+ base.class_eval do
9
+ before_filter :guard_entry
10
+ class << self
11
+ attr_reader :wizard_config #note: reader for @wizard_config on the class (not the instance)
12
+ end
13
+ hide_action :reset_wizard_session_vars, :wizard_config, :methodize_button_name
14
+ end
15
+ end
16
+
17
+ module ClassMethods
18
+ private
19
+ def configure_wizard_for_model(model, opts={}, &block)
20
+
21
+ # controller_name = self.name.sub(/Controller$/, '').underscore.to_sym
22
+ @wizard_config = Wizardly::Wizard::Configuration.create(controller_name, model, opts, &block)
23
+ # define methods
24
+ self.class_eval @wizard_config.print_page_action_methods
25
+ self.class_eval @wizard_config.print_callbacks
26
+ self.class_eval @wizard_config.print_helpers
27
+ self.class_eval @wizard_config.print_callback_macros
28
+ end
29
+ end
30
+
31
+ # instance methods for controller
32
+ public
33
+ def wizard_config; self.class.wizard_config; end
34
+
35
+ end
36
+ end
@@ -0,0 +1,16 @@
1
+ require 'wizardly/wizard/configuration'
2
+
3
+ module Wizardly
4
+ class WizardlyError < StandardError; end
5
+ class ModelNotFoundError < WizardlyError; end
6
+ class ValidationGroupError < WizardlyError; end
7
+ class CallbackError < WizardlyError; end
8
+ class MissingCallbackError < WizardlyError; end
9
+ class WizardConfigurationError < WizardlyError; end
10
+ class RedirectNotDefinedError < WizardlyError; end
11
+
12
+ class WizardlyGeneratorError < WizardlyError; end
13
+ class WizardlyScaffoldError < WizardlyGeneratorError; end
14
+ class WizardlyControllerGeneratorError < WizardlyGeneratorError; end
15
+
16
+ end
@@ -0,0 +1,35 @@
1
+ require 'wizardly/wizard/text_helpers'
2
+
3
+ module Wizardly
4
+ module Wizard
5
+ class Button
6
+ include TextHelpers
7
+ attr_reader :name
8
+ attr_reader :id
9
+
10
+ def initialize(id, name=nil)
11
+ @id = id
12
+ @name = name || symbol_to_button_name(id)
13
+ @user_defined = false
14
+ end
15
+
16
+ def user_defined?; @user_defined; end
17
+
18
+ #used in the dsl
19
+ def name_to(name, opts={})
20
+ case name
21
+ when String then @name = name.strip.squeeze(' ')
22
+ when Symbol then @name = symbol_to_button_name(name)
23
+ end
24
+ @id = opts[:id] if (opts[:id] && opts[:id].is_a?(Symbol))
25
+ end
26
+ end
27
+
28
+ class UserDefinedButton < Button
29
+ def initialize(id, name=nil)
30
+ super
31
+ @user_defined = true
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,194 @@
1
+ require 'wizardly/wizard/utils'
2
+ require 'wizardly/wizard/dsl'
3
+ require 'wizardly/wizard/button'
4
+ require 'wizardly/wizard/page'
5
+ require 'wizardly/wizard/configuration/methods'
6
+
7
+ module Wizardly
8
+ module Wizard
9
+ class Configuration
10
+ include TextHelpers
11
+ attr_reader :pages, :completed_redirect, :canceled_redirect, :controller_path, :controller_class_name, :controller_name, :page_order
12
+
13
+ #enum_attr :persistance, %w(sandbox session database)
14
+
15
+ def initialize(controller, opts) #completed_redirect = nil, canceled_redirect = nil)
16
+ @controller_class_name = controller.to_s.camelcase
17
+ @controller_class_name += 'Controller' unless @controller_class_name =~ /Controller$/
18
+ @controller_path = @controller_class_name.sub(/Controller$/,'').underscore
19
+ @controller_name = @controller_class_name.demodulize.sub(/Controller$/,'').underscore
20
+ @completed_redirect = opts[:completed] || opts[:when_completed] || opts[:redirect] #format_redirect(completed_redirect)
21
+ @canceled_redirect = opts[:canceled] || opts[:when_canceled] || opts[:redirect]
22
+ @include_skip_button = opts[:skip] || opts[:allow_skip] || opts[:allow_skipping] || false
23
+ @include_cancel_button = opts.key?(:cancel) ? opts[:cancel] : true
24
+ @guard_entry = opts.key?(:guard) ? opts[:guard] : true
25
+ @password_fields = opts[:mask_fields] || opts[:mask_passwords] || [:password, :password_confirmation]
26
+ @persist_model = opts[:persist_model] || :per_page
27
+ @form_data = opts[:form_data] || :session
28
+ raise(ArgumentError, ":persist_model option must be one of :once or :per_page", caller) unless [:once, :per_page].include?(@persist_model)
29
+ raise(ArgumentError, ":form_data option must be one of :sandbox or :session", caller) unless [:sandbox, :session].include?(@form_data)
30
+ @page_order = []
31
+ @pages = {}
32
+ @buttons = nil
33
+ @default_buttons = Hash[*[:next, :back, :cancel, :finish, :skip].collect {|default| [default, Button.new(default)] }.flatten]
34
+ end
35
+
36
+ def guard?; @guard_entry; end
37
+ def persist_model_per_page?; @persist_model == :per_page; end
38
+ def form_data_keep_in_session?; @form_data == :session; end
39
+ def model; @wizard_model_sym; end
40
+ def model_instance_variable; "@#{@wizard_model_sym.to_s}"; end
41
+ def model_class_name; @wizard_model_class_name; end
42
+ def model_const; @wizard_model_const; end
43
+
44
+ def first_page?(name); @page_order.first == name; end
45
+ def last_page?(name); @page_order.last == name; end
46
+ def next_page(name)
47
+ index = @page_order.index(name)
48
+ index += 1 unless self.last_page?(name)
49
+ @page_order[index]
50
+ end
51
+ def previous_page(name)
52
+ index = @page_order.index(name)
53
+ index -= 1 unless self.first_page?(name)
54
+ @page_order[index]
55
+ end
56
+ def button_for_function(name); @default_buttons[name]; end
57
+ def buttons
58
+ return @buttons if @buttons
59
+ # reduce buttons
60
+ @buttons = Hash[*@default_buttons.collect{|k,v|[v.id, v]}.flatten]
61
+ end
62
+
63
+ def self.create(controller_name, model_name, opts={}, &block)
64
+ # controller_name = controller_name.to_s.underscore.sub(/_controller$/, '').to_sym
65
+ model_name = model_name.to_s.underscore.to_sym
66
+ config = Wizardly::Wizard::Configuration.new(controller_name, opts)
67
+ config.inspect_model!(model_name)
68
+ Wizardly::Wizard::DSL.new(config).instance_eval(&block) if block_given?
69
+ config
70
+ end
71
+
72
+
73
+ def inspect_model!(model)
74
+ # first examine the model symbol, transform and see if the constant
75
+ # exists
76
+ begin
77
+ @wizard_model_sym = model.to_sym
78
+ @wizard_model_class_name = model.to_s.camelize
79
+ @wizard_model_const = @wizard_model_class_name.constantize
80
+ rescue Exception=>e
81
+ raise ModelNotFoundError, "Cannot convert :#{@wizard_model_sym} to model constant for #{@wizard_model_class_name}: " + e.message, caller
82
+ end
83
+
84
+ begin
85
+ @page_order = @wizard_model_const.validation_group_order
86
+ rescue Exception => e
87
+ raise ValidationGroupError, "Unable to read validation groups from #{@wizard_model_class_name}: " + e.message, caller
88
+ end
89
+ raise(ValidationGroupError, "No validation groups defined for model #{@wizard_model_class_name}", caller) unless (@page_order && !@page_order.empty?)
90
+
91
+ begin
92
+ groups = @wizard_model_const.validation_groups
93
+ enum_attrs = @wizard_model_const.respond_to?(:enumerated_attributes) ? @wizard_model_const.enumerated_attributes.collect {|k,v| k } : []
94
+ model_inst = @wizard_model_const.new
95
+ last_index = @page_order.size-1
96
+ @page_order.each_with_index do |p, index|
97
+ fields = groups[p].map do |f|
98
+ column = model_inst.column_for_attribute(f)
99
+ type = case
100
+ when enum_attrs.include?(f) then :enum
101
+ when (@password_fields && @password_fields.include?(f)) then :password
102
+ else
103
+ column ? column.type : :string
104
+ end
105
+ PageField.new(f, type)
106
+ end
107
+ page = Page.new(self, p, fields)
108
+
109
+ # default button settings based on order, can be altered by
110
+ # set_page(@id).buttons_to []
111
+ buttons = []
112
+ buttons << @default_buttons[:next] unless index >= last_index
113
+ buttons << @default_buttons[:finish] if index == last_index
114
+ buttons << @default_buttons[:back] unless index == 0
115
+ buttons << @default_buttons[:skip] if (@include_skip_button && index != last_index)
116
+ buttons << @default_buttons[:cancel] if (@include_cancel_button)
117
+ page.buttons = buttons
118
+ @pages[page.id] = page
119
+ end
120
+ rescue Exception => e
121
+ raise ValidationGroupError, "Failed to configure wizard from #{@wizard_model_class_name} validation groups: " + e.message, caller
122
+ end
123
+ end
124
+
125
+ public
126
+ # internal DSL method handlers
127
+ def _when_completed_redirect_to(redir); @completed_redirect = redir; end
128
+ def _when_canceled_redirect_to(redir); @canceled_redirect = redir; end
129
+ def _change_button(name)
130
+ raise(WizardConfigurationError, "Button :#{name} in _change_button() call does not exist", caller) unless self.buttons.key?(name)
131
+ _buttons = self.buttons
132
+ @buttons = nil # clear the buttons for regeneration after change in next line
133
+ _buttons[name]
134
+ end
135
+ def _create_button(name, opts)
136
+ id = opts[:id] || button_name_to_symbol(name)
137
+ raise(WizardConfigurationError, "Button '#{name}' with id :#{id} cannot be created. The ID already exists.", caller) if self.buttons.key?(id)
138
+ @buttons=nil
139
+ @default_buttons[id] = UserDefinedButton.new(id, name)
140
+ end
141
+ def _set_page(name); @pages[name]; end
142
+ def _mask_passwords(passwords)
143
+ case passwords
144
+ when String
145
+ passwords = [passwords.to_sym]
146
+ when Symbol
147
+ passwords = [passwords]
148
+ when Array
149
+ else
150
+ raise(WizardlyConfigurationError, "mask_passwords method only accepts string, symbol or array of password fields")
151
+ end
152
+ @password_fields.push(*passwords).uniq!
153
+ end
154
+
155
+ def print_config
156
+ io = StringIO.new
157
+ class_name = controller_name.to_s.camelize
158
+ class_name += 'Controller' unless class_name =~ /Controller$/
159
+ io.puts "#{class_name} wizard configuration"
160
+ io.puts
161
+ io.puts "model: #{model_class_name}"
162
+ io.puts "instance: @#{model}"
163
+ io.puts
164
+ io.puts "pages:"
165
+ self.page_order.each do |pid|
166
+ page = pages[pid]
167
+ # io.puts " #{index+1}. '#{page.title}' page (:#{page.id}) has"
168
+ io.puts " '#{page.title}' page (:#{page.id}) has"
169
+ io.puts " --fields: #{page.fields.inject([]){|a, f| a << '"'+f.name.to_s.titleize+'" [:'+f.column_type.to_s+']'}.join(', ')}"
170
+ io.puts " --buttons: #{page.buttons.inject([]){|a, b| a << b.name.to_s }.join(', ')}"
171
+ end
172
+ io.puts
173
+ io.puts "redirects:"
174
+ io.puts " when completed: #{completed_redirect ? completed_redirect.inspect : 'redirects to initial referer by default (specify :completed=>url to override)'}"
175
+ io.puts " when canceled: #{canceled_redirect ? canceled_redirect.inspect : 'redirects to initial referer by default (specify :canceled=>url to override)'}"
176
+ io.puts
177
+ io.puts "buttons:"
178
+ self.buttons.each do |k, b|
179
+ bs = StringIO.new
180
+ bs << " #{b.name} (:#{b.id}) "
181
+ if (b.user_defined?)
182
+ bs << "-- user defined button and function"
183
+ else
184
+ dk = @default_buttons.index(b)
185
+ bs << "-- used for internal <#{dk}> functionality"
186
+ end
187
+ io.puts bs.string
188
+ end
189
+ io.puts
190
+ io.string
191
+ end
192
+ end
193
+ end
194
+ end