wizardly 0.1.8.9

Sign up to get free protection for your applications and to get access to all the features.
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