acts_as_approvable 0.1.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. data/.gitignore +4 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +5 -0
  4. data/Appraisals +22 -0
  5. data/CHANGELOG +76 -0
  6. data/Gemfile +3 -0
  7. data/Gemfile.lock +84 -0
  8. data/MIT-LICENSE +2 -2
  9. data/README.md +146 -0
  10. data/Rakefile +90 -7
  11. data/TODO.md +30 -0
  12. data/VERSION +1 -0
  13. data/acts_as_approvable.gemspec +40 -0
  14. data/features/create_approval.feature +36 -0
  15. data/features/destroy_approval.feature +19 -0
  16. data/features/reset_approval.feature +13 -0
  17. data/features/step_definitions/cucumber_steps.rb +132 -0
  18. data/features/support/env.rb +14 -0
  19. data/features/support/large.txt +29943 -0
  20. data/features/support/second_large.txt +31798 -0
  21. data/features/update_approval.feature +48 -0
  22. data/gemfiles/Gemfile.ci +14 -0
  23. data/gemfiles/Gemfile.ci.lock +98 -0
  24. data/gemfiles/mysql2.gemfile +7 -0
  25. data/gemfiles/mysql2.gemfile.lock +86 -0
  26. data/gemfiles/rails2.gemfile +8 -0
  27. data/gemfiles/rails2.gemfile.lock +86 -0
  28. data/gemfiles/rails30.gemfile +9 -0
  29. data/gemfiles/rails30.gemfile.lock +124 -0
  30. data/gemfiles/rails31.gemfile +9 -0
  31. data/gemfiles/rails31.gemfile.lock +135 -0
  32. data/gemfiles/sqlite.gemfile +7 -0
  33. data/generators/acts_as_approvable/USAGE +3 -0
  34. data/generators/acts_as_approvable/acts_as_approvable_generator.rb +81 -0
  35. data/generators/acts_as_approvable/templates/approvals.js +71 -0
  36. data/generators/acts_as_approvable/templates/approvals_controller.rb +91 -0
  37. data/generators/acts_as_approvable/templates/create_approvals.rb +27 -0
  38. data/generators/acts_as_approvable/templates/initializer.rb +3 -0
  39. data/generators/acts_as_approvable/templates/jquery.form.js +101 -0
  40. data/generators/acts_as_approvable/templates/views/erb/_owner_select.html.erb +4 -0
  41. data/generators/acts_as_approvable/templates/views/erb/_table.html.erb +26 -0
  42. data/generators/acts_as_approvable/templates/views/erb/index.html.erb +17 -0
  43. data/generators/acts_as_approvable/templates/views/haml/_owner_select.html.haml +3 -0
  44. data/generators/acts_as_approvable/templates/views/haml/_table.html.haml +19 -0
  45. data/generators/acts_as_approvable/templates/views/haml/index.html.haml +15 -0
  46. data/init.rb +1 -0
  47. data/lib/acts_as_approvable.rb +96 -2
  48. data/lib/acts_as_approvable/approval.rb +205 -11
  49. data/lib/acts_as_approvable/error.rb +34 -0
  50. data/lib/acts_as_approvable/model.rb +60 -0
  51. data/lib/acts_as_approvable/model/class_methods.rb +63 -0
  52. data/lib/acts_as_approvable/model/create_instance_methods.rb +88 -0
  53. data/lib/acts_as_approvable/model/destroy_instance_methods.rb +38 -0
  54. data/lib/acts_as_approvable/model/instance_methods.rb +107 -0
  55. data/lib/acts_as_approvable/model/update_instance_methods.rb +61 -0
  56. data/lib/acts_as_approvable/ownership.rb +141 -0
  57. data/lib/acts_as_approvable/railtie.rb +7 -0
  58. data/lib/acts_as_approvable/version.rb +1 -8
  59. data/lib/generators/acts_as_approvable/USAGE +1 -0
  60. data/lib/generators/acts_as_approvable/acts_as_approvable_generator.rb +68 -0
  61. data/lib/generators/acts_as_approvable/base.rb +30 -0
  62. data/lib/generators/acts_as_approvable/templates/approvals.js +71 -0
  63. data/lib/generators/acts_as_approvable/templates/approvals_controller.rb +91 -0
  64. data/lib/generators/acts_as_approvable/templates/create_approvals.rb +27 -0
  65. data/lib/generators/acts_as_approvable/templates/jquery.form.js +101 -0
  66. data/lib/generators/erb/acts_as_approvable_generator.rb +33 -0
  67. data/lib/generators/erb/templates/_owner_select.html.erb +4 -0
  68. data/lib/generators/erb/templates/_table.html.erb +26 -0
  69. data/lib/generators/erb/templates/index.html.erb +17 -0
  70. data/lib/generators/haml/acts_as_approvable_generator.rb +33 -0
  71. data/lib/generators/haml/templates/_owner_select.html.haml +3 -0
  72. data/lib/generators/haml/templates/_table.html.haml +19 -0
  73. data/lib/generators/haml/templates/index.html.haml +15 -0
  74. data/lib/tasks/acts_as_approvable.rake +4 -0
  75. data/rails/init.rb +1 -0
  76. data/spec/acts_as_approvable/approval_spec.rb +614 -0
  77. data/spec/acts_as_approvable/model/class_methods_spec.rb +219 -0
  78. data/spec/acts_as_approvable/model/create_instance_methods_spec.rb +169 -0
  79. data/spec/acts_as_approvable/model/destroy_instance_methods_spec.rb +71 -0
  80. data/spec/acts_as_approvable/model/instance_methods_spec.rb +328 -0
  81. data/spec/acts_as_approvable/model/update_instance_methods_spec.rb +111 -0
  82. data/spec/acts_as_approvable/model_spec.rb +113 -0
  83. data/spec/acts_as_approvable/ownership/class_methods_spec.rb +134 -0
  84. data/spec/acts_as_approvable/ownership/instance_methods_spec.rb +32 -0
  85. data/spec/acts_as_approvable/ownership_spec.rb +52 -0
  86. data/spec/acts_as_approvable_spec.rb +31 -0
  87. data/spec/spec_helper.rb +51 -0
  88. data/spec/support/database.rb +49 -0
  89. data/spec/support/database.yml +12 -0
  90. data/spec/support/matchers.rb +87 -0
  91. data/spec/support/models.rb +67 -0
  92. data/spec/support/schema.rb +54 -0
  93. metadata +375 -58
  94. data/README.rdoc +0 -38
  95. data/lib/acts_as_approvable/approver.rb +0 -76
  96. data/lib/generators/acts_as_approvable/install_generator.rb +0 -28
  97. data/lib/generators/acts_as_approvable/templates/install.rb +0 -16
@@ -0,0 +1,4 @@
1
+ <%% form_for(approval, :url => assign_approval_path(approval.id), :html => {:class => 'assignment'}) do |f| %>
2
+ <%%= f.select(:owner_id, options_for_select(Approval.options_for_available_owners(true), approval.owner_id)) %>
3
+ <%%= f.submit('Set') %>
4
+ <%% end %>
@@ -0,0 +1,26 @@
1
+ <table>
2
+ <thead>
3
+ <tr>
4
+ <th>Type</th>
5
+ <th>Record</th>
6
+ <% if owner? %> <th>Owner</th>
7
+ <% end %> <th>State</th>
8
+ <th>&nbsp;</th>
9
+ </tr>
10
+ </thead>
11
+ <tbody>
12
+ <%% @approvals.each do |approval| %>
13
+ <tr>
14
+ <td><%%= approval.item_type.classify %></td>
15
+ <td><%%= approval.item.try(:to_s) || approval.item.id %></td>
16
+ <% if owner? %> <td><%%= render :partial => 'owner_select', :locals => {:approval => approval} %></td>
17
+ <% end %> <td><%%= approval.state %></td>
18
+ <td class='actions'>
19
+ <%%= link_to('Approve', approve_approval_path(approval), :class => 'approve') %>
20
+ <%%= link_to('Reject', reject_approval_path(approval), :class => 'reject') %>
21
+ </td>
22
+ </tr>
23
+ <%% end %>
24
+ </tbody>
25
+ </table>
26
+
@@ -0,0 +1,17 @@
1
+ <h1>Approval Queue</h1>
2
+
3
+ <%% form_for(:approval, :url => approvals_path, :html => {:method => :get}) do |f| %>
4
+ <%% unless @conditions[:state].is_a?(Array) # History page shouldn't allow selecting different states %>
5
+ <%%= label_tag(:state) %>
6
+ <%%= select_tag(:state, options_for_select(Approval.options_for_state, @conditions[:state])) %>
7
+ <%% end %>
8
+ <% if owner? %> <%%= label_tag(:owner_id) %>
9
+ <%%= select_tag(:owner_id, options_for_select(Approval.options_for_assigned_owners, @conditions[:owner_id]), :prompt => 'All Users') %>
10
+ <% end %> <%%= label_tag(:item_type) %>
11
+ <%%= select_tag(:item_type, options_for_select(Approval.options_for_type, @conditions[:item_type]), :prompt => 'All Types') %>
12
+ <%%= f.submit('Filter') %>
13
+ <%% end %>
14
+
15
+ <%%= render :partial => @table_partial %>
16
+ <% if scripts? %>
17
+ <%%= javascript_include_tag 'jquery.form.js', 'approvals.js' %><% end %>
@@ -0,0 +1,3 @@
1
+ - form_for(approval, :url => assign_approval_path(approval.id), :html => {:class => 'assignment'}) do |f|
2
+ = f.select(:owner_id, options_for_select(Approval.options_for_available_owners(true), approval.owner_id))
3
+ = f.submit('Set')
@@ -0,0 +1,19 @@
1
+ %table
2
+ %thead
3
+ %tr
4
+ %th Type
5
+ %th Record
6
+ <% if owner? %> %th Owner
7
+ <% end %> %th State
8
+ %th
9
+ %tbody
10
+ - @approvals.each do |approval|
11
+ %tr
12
+ %td= approval.item_type.classify
13
+ %td= approval.item.try(:to_s) || approval.item.id
14
+ <% if owner? %> %td= render :partial => 'owner_select', :locals => {:approval => approval}
15
+ <% end %> %td= approval.state
16
+ %td.actions
17
+ = link_to('Approve', approve_approval_path(approval), :class => 'approve')
18
+ = link_to('Reject', reject_approval_path(approval), :class => 'reject')
19
+
@@ -0,0 +1,15 @@
1
+ %h1 Approval Queue
2
+
3
+ - form_for(:approval, :url => approvals_path, :html => {:method => :get}) do |f|
4
+ - unless @conditions[:state].is_a?(Array) # History page shouldn't allow selecting different states
5
+ = label_tag('state', 'State')
6
+ = select_tag('state', options_for_select(Approval.options_for_state, @conditions[:state]))
7
+ <% if owner? %> = label_tag('owner_id', 'Owner')
8
+ = select_tag('owner_id', options_for_select(Approval.options_for_assigned_owners(true), @conditions[:owner_id]))
9
+ <% end %> = label_tag('item_type', 'Type')
10
+ = select_tag('item_type', options_for_select(Approval.options_for_type(true), @conditions[:item_type]))
11
+ %button{:type => 'Submit'} Filter
12
+
13
+ = render :partial => @table_partial
14
+ <% if scripts? %>
15
+ = javascript_include_tag 'jquery.form.js', 'approvals.js'<% end %>
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + '/rails/init'
@@ -1,4 +1,98 @@
1
1
  require 'active_record'
2
- require 'acts_as_approvable/approver'
2
+
3
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
4
+
5
+ require 'acts_as_approvable/model'
6
+ require 'acts_as_approvable/approval'
7
+ require 'acts_as_approvable/error'
8
+ require 'acts_as_approvable/ownership'
3
9
  require 'acts_as_approvable/version'
4
- ActiveRecord::Base.send :include, ActsAsApprovable::Approver
10
+
11
+ if defined?(Rails) && Rails.version =~ /^3\./
12
+ require 'acts_as_approvable/railtie'
13
+ elsif defined?(ActiveRecord)
14
+ ActiveRecord::Base.send :extend, ActsAsApprovable::Model
15
+ end
16
+
17
+ $LOAD_PATH.shift
18
+
19
+ module ActsAsApprovable
20
+ ##
21
+ # Enable the approval queue at a global level.
22
+ def self.enable
23
+ @enabled = true
24
+ end
25
+
26
+ ##
27
+ # Disable the approval queue at a global level.
28
+ def self.disable
29
+ @enabled = false
30
+ end
31
+
32
+ ##
33
+ # Returns true if the approval queue is enabled globally.
34
+ def self.enabled?
35
+ @enabled = true if @enabled.nil?
36
+ @enabled
37
+ end
38
+
39
+ ##
40
+ # Set the referenced Owner class to be used by generic finders.
41
+ #
42
+ # @see Ownership
43
+ def self.owner_class=(klass)
44
+ @owner_class = klass
45
+ end
46
+
47
+ ##
48
+ # Get the referenced Owner class to be used by generic finders.
49
+ #
50
+ # @see Ownership
51
+ def self.owner_class
52
+ @owner_class
53
+ end
54
+
55
+ ##
56
+ # Set the class used for overriding Ownership retrieval
57
+ #
58
+ # @see Ownership
59
+ def self.owner_source=(source)
60
+ @owner_source = source
61
+ end
62
+
63
+ ##
64
+ # Get the class used for overriding Ownership retrieval
65
+ #
66
+ # @see Ownership
67
+ def self.owner_source
68
+ @owner_source
69
+ end
70
+
71
+ ##
72
+ # Set the engine used for rendering view files.
73
+ def self.view_language=(lang)
74
+ @lang = lang
75
+ end
76
+
77
+ ##
78
+ # Get the engine used for rendering view files. Defaults to 'erb'
79
+ def self.view_language
80
+ if Rails.version =~ /^3\./
81
+ Rails.configuration.generators.rails[:template_engine].try(:to_s) || 'erb'
82
+ else
83
+ @lang || 'erb'
84
+ end
85
+ end
86
+
87
+ ##
88
+ # Enable or disable the stale record check when approving updates.
89
+ def self.stale_check=(bool)
90
+ @stale_check = !!bool
91
+ end
92
+
93
+ ##
94
+ # Get the state of the stale check.
95
+ def self.stale_check?
96
+ @stale_check || true
97
+ end
98
+ end
@@ -1,12 +1,206 @@
1
1
  class Approval < ActiveRecord::Base
2
- belongs_to :approvable, :polymorphic => true
3
- belongs_to :approver, :polymorphic => true
4
-
5
- validates :approvable_id, :approvable_type, :presence => true
6
- validates :approvable_id, :uniqueness => {:scope => :approvable_type}
7
-
8
- # Scoped wrapped in lambdas because ActiveRecord's connection hasn't been established at the
9
- # time of this classes' load.
10
- scope :pending, lambda{ where(:approved => false) }
11
- scope :approved_today, lambda{ where(:approved => true).where(arel_table[:updated_at].eq(Date.today)) }
12
- end
2
+ ##
3
+ # Enumeration of available states.
4
+ STATES = %w(pending approved rejected)
5
+
6
+ belongs_to :item, :polymorphic => true
7
+
8
+ validates_presence_of :item
9
+ validates_inclusion_of :event, :in => %w(create update destroy)
10
+ validates_numericality_of :state, :greater_than_or_equal_to => 0, :less_than => STATES.length
11
+
12
+ serialize :object
13
+ serialize :original
14
+
15
+ before_save :able_to_save?
16
+
17
+ ##
18
+ # Find the enumerated value for a given state.
19
+ #
20
+ # @return [Integer]
21
+ def self.enumerate_state(state)
22
+ enumerate_states(state).first
23
+ end
24
+
25
+ ##
26
+ # Find the enumerated values for a list of states.
27
+ #
28
+ # @return [Array]
29
+ def self.enumerate_states(*states)
30
+ states.map { |name| STATES.index(name) }.compact
31
+ end
32
+
33
+ ##
34
+ # Build an array of states usable by Rails' `#options_for_select`.
35
+ def self.options_for_state
36
+ options = [['All', -1]]
37
+ STATES.each_index { |x| options << [STATES[x].capitalize, x] }
38
+ options
39
+ end
40
+
41
+ ##
42
+ # Build an array of types usable by Rails' `#options_for_select`.
43
+ def self.options_for_type(with_prompt = false)
44
+ types = all(:select => 'DISTINCT(item_type)').map { |row| row.item_type }
45
+ types.unshift(['All Types', nil]) if with_prompt
46
+ types
47
+ end
48
+
49
+ ##
50
+ # Get the current state of the approval. Converts from integer via {STATES} constant.
51
+ def state
52
+ STATES[(read_attribute(:state) || 0)]
53
+ end
54
+
55
+ ##
56
+ # Get the previous state of the approval. Converts from integer via {STATES} constant.
57
+ def state_was
58
+ STATES[(changed_attributes[:state] || 0)]
59
+ end
60
+
61
+ ##
62
+ # Set the state of the approval. Converts from string to integer via {STATES} constant.
63
+ def state=(state)
64
+ state = self.class.enumerate_state(state) if state.is_a?(String)
65
+ write_attribute(:state, state)
66
+ end
67
+
68
+ ##
69
+ # Returns true if the approval is still pending.
70
+ def pending?
71
+ state == 'pending'
72
+ end
73
+
74
+ ##
75
+ # Returns true if the approval has been approved.
76
+ def approved?
77
+ state == 'approved'
78
+ end
79
+
80
+ ##
81
+ # Returns true if the approval has been rejected.
82
+ def rejected?
83
+ state == 'rejected'
84
+ end
85
+
86
+ ##
87
+ # Returns true if the approval has been approved or rejected.
88
+ def locked?
89
+ approved? or rejected?
90
+ end
91
+
92
+ ##
93
+ # Returns true if the approval has not been approved or rejected.
94
+ def unlocked?
95
+ not locked?
96
+ end
97
+
98
+ ##
99
+ # Returns true if the approval able to be saved. This requires an unlocked
100
+ # approval, or an approval just leaving the 'pending' state.
101
+ def able_to_save?
102
+ unlocked? or state_was == 'pending'
103
+ end
104
+
105
+ ##
106
+ # Returns true if the affected item has been updated since this approval was
107
+ # last updated.
108
+ def stale?
109
+ unlocked? and item.has_attribute?(:updated_at) and updated_at < item.updated_at
110
+ end
111
+
112
+ ##
113
+ # Returns true if the affected item has not been updated since this approval
114
+ # was created.
115
+ def fresh?
116
+ not stale?
117
+ end
118
+
119
+ ##
120
+ # Returns true if the approval is stale and the stale check is enabled.
121
+ def stale_approval?
122
+ ActsAsApprovable.stale_check? and update? and stale?
123
+ end
124
+
125
+ ##
126
+ # Returns true if this is an `:update` approval event.
127
+ def update?
128
+ event == 'update'
129
+ end
130
+
131
+ ##
132
+ # Returns true if this is a `:create` approval event.
133
+ def create?
134
+ event == 'create'
135
+ end
136
+
137
+ ##
138
+ # Returns true if this is a `:destroy` approval event.
139
+ def destroy?
140
+ event == 'destroy'
141
+ end
142
+
143
+ ##
144
+ # Attempt to approve the record change.
145
+ #
146
+ # @param [Boolean] force if the approval record is stale force the acceptance.
147
+ # @raise [ActsAsApprovable::Error::Locked] raised if the record is {#locked? locked}.
148
+ # @raise [ActsAsApprovable::Error::Stale] raised if the record is {#stale? stale} and `force` is false.
149
+ def approve!(force = false)
150
+ raise ActsAsApprovable::Error::Locked if locked?
151
+ raise ActsAsApprovable::Error::Stale if !force and stale_approval?
152
+ return unless run_item_callback(:before_approve)
153
+
154
+ if update?
155
+ data = {}
156
+ object.each do |attr, value|
157
+ data[attr] = value if item.attribute_names.include?(attr)
158
+ end
159
+
160
+ item.attributes = data
161
+ elsif create?
162
+ item.set_approval_state('approved')
163
+ elsif destroy?
164
+ item.destroy_without_approval
165
+ end
166
+
167
+ item.save_without_approval! unless destroy?
168
+ update_attributes!(:state => 'approved')
169
+ run_item_callback(:after_approve)
170
+ end
171
+
172
+ ##
173
+ # Attempt to reject the record change.
174
+ #
175
+ # @param [String] reason a reason for rejecting the change.
176
+ # @raise [ActsAsApprovable::Error::Locked] raised if the record is {#locked? locked}.
177
+ def reject!(reason = nil)
178
+ raise ActsAsApprovable::Error::Locked if locked?
179
+ return unless run_item_callback(:before_reject)
180
+
181
+ item.set_approval_state('rejected') if create?
182
+
183
+ item.save_without_approval!
184
+ update_attributes!(:state => 'rejected', :reason => reason)
185
+ run_item_callback(:after_reject)
186
+ end
187
+
188
+ ##
189
+ # Force the approval back into a 'pending' state. Only valid for :create events.
190
+ #
191
+ # @raise [ActsAsApprovable::Error::InvalidTransition] raised if the event is not {#create? :create}.
192
+ def reset!
193
+ raise ActsAsApprovable::Error::InvalidTransition.new(state, 'pending', self) unless create?
194
+
195
+ item.set_approval_state('pending')
196
+ item.save_without_approval!
197
+
198
+ state_will_change! # Force an update to the record
199
+ update_attributes!(:state => 'rejected')
200
+ end
201
+
202
+ private
203
+ def run_item_callback(callback)
204
+ item.send(callback, self) != false
205
+ end
206
+ end
@@ -0,0 +1,34 @@
1
+ module ActsAsApprovable
2
+ class Error < RuntimeError
3
+ ##
4
+ # Raised when a locked approval is accepted or rejected.
5
+ class Locked < ActsAsApprovable::Error
6
+ def initialize(*args)
7
+ super('this approval is locked')
8
+ end
9
+ end
10
+
11
+ ##
12
+ # Raised when a stale approval is accepted.
13
+ class Stale < ActsAsApprovable::Error
14
+ def initialize(*args)
15
+ super('this approval is stale and should not be approved')
16
+ end
17
+ end
18
+
19
+ ##
20
+ # Raised when a record is assigned as owner that is not found in
21
+ # {ActsAsApprovable::Ownership::ClassMethods#available_owners}.
22
+ class InvalidOwner < ActsAsApprovable::Error
23
+ def initialize(*args)
24
+ super('this record cannot be assigned as an owner')
25
+ end
26
+ end
27
+
28
+ class InvalidTransition < ActsAsApprovable::Error
29
+ def initialize(from, to, approval)
30
+ super("you may not transition from #{from} to #{to} in a #{approval.event} approval")
31
+ end
32
+ end
33
+ end
34
+ end