requires_approval 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +5 -0
- data/Gemfile +23 -0
- data/Gemfile.lock +96 -0
- data/Guardfile +42 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +121 -0
- data/Rakefile +42 -0
- data/VERSION +1 -0
- data/lib/errors.rb +11 -0
- data/lib/requires_approval.rb +331 -0
- data/requires_approval.gemspec +90 -0
- data/spec/lib/requires_approval_spec.rb +423 -0
- data/spec/spec_helper.rb +65 -0
- data/spec/support/BLANK +1 -0
- metadata +208 -0
data/.document
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
gem "activerecord", "~> 3.0.9"
|
7
|
+
gem "activesupport", "~> 3.0.9"
|
8
|
+
|
9
|
+
# Add dependencies to develop your gem here.
|
10
|
+
# Include everything needed to run rake, tests, features, etc.
|
11
|
+
group :development do
|
12
|
+
gem "bundler"
|
13
|
+
gem "guard-rspec"
|
14
|
+
gem "guard-bundler"
|
15
|
+
gem "guard-spork"
|
16
|
+
gem "jeweler", "~> 1.8.3"
|
17
|
+
gem "mocha"
|
18
|
+
gem "rdoc"
|
19
|
+
gem "rspec"
|
20
|
+
gem "ruby-debug19", :require => "ruby-debug"
|
21
|
+
gem "sqlite3"
|
22
|
+
gem "yard"
|
23
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activemodel (3.0.16)
|
5
|
+
activesupport (= 3.0.16)
|
6
|
+
builder (~> 2.1.2)
|
7
|
+
i18n (~> 0.5.0)
|
8
|
+
activerecord (3.0.16)
|
9
|
+
activemodel (= 3.0.16)
|
10
|
+
activesupport (= 3.0.16)
|
11
|
+
arel (~> 2.0.10)
|
12
|
+
tzinfo (~> 0.3.23)
|
13
|
+
activesupport (3.0.16)
|
14
|
+
archive-tar-minitar (0.5.2)
|
15
|
+
arel (2.0.10)
|
16
|
+
builder (2.1.2)
|
17
|
+
columnize (0.3.6)
|
18
|
+
diff-lcs (1.1.3)
|
19
|
+
ffi (1.1.0)
|
20
|
+
git (1.2.5)
|
21
|
+
guard (1.2.3)
|
22
|
+
listen (>= 0.4.2)
|
23
|
+
thor (>= 0.14.6)
|
24
|
+
guard-bundler (1.0.0)
|
25
|
+
bundler (~> 1.0)
|
26
|
+
guard (~> 1.1)
|
27
|
+
guard-rspec (1.2.0)
|
28
|
+
guard (>= 1.1)
|
29
|
+
guard-spork (1.1.0)
|
30
|
+
guard (>= 1.1)
|
31
|
+
spork (>= 0.8.4)
|
32
|
+
i18n (0.5.0)
|
33
|
+
jeweler (1.8.4)
|
34
|
+
bundler (~> 1.0)
|
35
|
+
git (>= 1.2.5)
|
36
|
+
rake
|
37
|
+
rdoc
|
38
|
+
json (1.7.4)
|
39
|
+
linecache19 (0.5.12)
|
40
|
+
ruby_core_source (>= 0.1.4)
|
41
|
+
listen (0.4.7)
|
42
|
+
rb-fchange (~> 0.0.5)
|
43
|
+
rb-fsevent (~> 0.9.1)
|
44
|
+
rb-inotify (~> 0.8.8)
|
45
|
+
metaclass (0.0.1)
|
46
|
+
mocha (0.12.1)
|
47
|
+
metaclass (~> 0.0.1)
|
48
|
+
rake (0.9.2.2)
|
49
|
+
rb-fchange (0.0.5)
|
50
|
+
ffi
|
51
|
+
rb-fsevent (0.9.1)
|
52
|
+
rb-inotify (0.8.8)
|
53
|
+
ffi (>= 0.5.0)
|
54
|
+
rdoc (3.12)
|
55
|
+
json (~> 1.4)
|
56
|
+
rspec (2.11.0)
|
57
|
+
rspec-core (~> 2.11.0)
|
58
|
+
rspec-expectations (~> 2.11.0)
|
59
|
+
rspec-mocks (~> 2.11.0)
|
60
|
+
rspec-core (2.11.1)
|
61
|
+
rspec-expectations (2.11.1)
|
62
|
+
diff-lcs (~> 1.1.3)
|
63
|
+
rspec-mocks (2.11.1)
|
64
|
+
ruby-debug-base19 (0.11.25)
|
65
|
+
columnize (>= 0.3.1)
|
66
|
+
linecache19 (>= 0.5.11)
|
67
|
+
ruby_core_source (>= 0.1.4)
|
68
|
+
ruby-debug19 (0.11.6)
|
69
|
+
columnize (>= 0.3.1)
|
70
|
+
linecache19 (>= 0.5.11)
|
71
|
+
ruby-debug-base19 (>= 0.11.19)
|
72
|
+
ruby_core_source (0.1.5)
|
73
|
+
archive-tar-minitar (>= 0.5.2)
|
74
|
+
spork (0.9.2)
|
75
|
+
sqlite3 (1.3.6)
|
76
|
+
thor (0.15.4)
|
77
|
+
tzinfo (0.3.33)
|
78
|
+
yard (0.8.2.1)
|
79
|
+
|
80
|
+
PLATFORMS
|
81
|
+
ruby
|
82
|
+
|
83
|
+
DEPENDENCIES
|
84
|
+
activerecord (~> 3.0.9)
|
85
|
+
activesupport (~> 3.0.9)
|
86
|
+
bundler
|
87
|
+
guard-bundler
|
88
|
+
guard-rspec
|
89
|
+
guard-spork
|
90
|
+
jeweler (~> 1.8.3)
|
91
|
+
mocha
|
92
|
+
rdoc
|
93
|
+
rspec
|
94
|
+
ruby-debug19
|
95
|
+
sqlite3
|
96
|
+
yard
|
data/Guardfile
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'bundler' do
|
5
|
+
watch('Gemfile')
|
6
|
+
# Uncomment next line if Gemfile contain `gemspec' command
|
7
|
+
# watch(/^.+\.gemspec/)
|
8
|
+
end
|
9
|
+
|
10
|
+
guard 'spork', :cucumber_env => { 'RAILS_ENV' => 'test' }, :rspec_env => { 'RAILS_ENV' => 'test' } do
|
11
|
+
watch('config/application.rb')
|
12
|
+
watch('config/environment.rb')
|
13
|
+
watch('config/environments/test.rb')
|
14
|
+
watch(%r{^config/initializers/.+\.rb$})
|
15
|
+
watch('Gemfile')
|
16
|
+
watch('Gemfile.lock')
|
17
|
+
watch('spec/spec_helper.rb') { :rspec }
|
18
|
+
watch('test/test_helper.rb') { :test_unit }
|
19
|
+
watch(%r{features/support/}) { :cucumber }
|
20
|
+
end
|
21
|
+
|
22
|
+
guard 'rspec', :version => 2 do
|
23
|
+
watch(%r{^spec/.+_spec\.rb$})
|
24
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
25
|
+
watch('spec/spec_helper.rb') { "spec" }
|
26
|
+
|
27
|
+
# Rails example
|
28
|
+
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
29
|
+
watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
|
30
|
+
watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
|
31
|
+
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
|
32
|
+
watch('config/routes.rb') { "spec/routing" }
|
33
|
+
watch('app/controllers/application_controller.rb') { "spec/controllers" }
|
34
|
+
|
35
|
+
# Capybara request specs
|
36
|
+
watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
|
37
|
+
|
38
|
+
# Turnip features and steps
|
39
|
+
watch(%r{^spec/acceptance/(.+)\.feature$})
|
40
|
+
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
|
41
|
+
end
|
42
|
+
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Dan Langevin
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
= requires_approval
|
2
|
+
|
3
|
+
===Installation
|
4
|
+
|
5
|
+
To install, just add requires_approval to your Gemfile
|
6
|
+
|
7
|
+
# Gemfile
|
8
|
+
gem "requires_approval"
|
9
|
+
|
10
|
+
=== Activation
|
11
|
+
|
12
|
+
To activate, use the requires_approval_for method in your class definition
|
13
|
+
|
14
|
+
# app/models/user.rb
|
15
|
+
#
|
16
|
+
# - first_name :string
|
17
|
+
# - last_name :string
|
18
|
+
# - birthday :date
|
19
|
+
# - created_at :datetime
|
20
|
+
# - updated_at :datetime
|
21
|
+
|
22
|
+
class User < ActiveRecord::Base
|
23
|
+
requires_approval_for(:first_name, :last_name)
|
24
|
+
end
|
25
|
+
|
26
|
+
And create a migration that adds the necessary table and fields for your
|
27
|
+
requires_approval model
|
28
|
+
|
29
|
+
# db/migrate/TIMESTAMP_add_user_versions.rb
|
30
|
+
def self.up
|
31
|
+
User.prepare_tables_for_requires_approval
|
32
|
+
end
|
33
|
+
|
34
|
+
This does the equivalent of the the following
|
35
|
+
|
36
|
+
# Generated migration
|
37
|
+
def self.up
|
38
|
+
create_table(:user_versions, :force => true) do |t|
|
39
|
+
t.string(:first_name)
|
40
|
+
t.string(:last_name)
|
41
|
+
t.integer(:user_id)
|
42
|
+
t.boolean(:is_approved)
|
43
|
+
t.timestamps
|
44
|
+
end
|
45
|
+
add_index(:user_versions, [:user_id, :is_approved])
|
46
|
+
|
47
|
+
# starts all new records out as frozen so they don't show up
|
48
|
+
add_column(:users, :is_frozen, :boolean, :default => true)
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
=== Usage
|
53
|
+
|
54
|
+
The first_name and last_name methods are now delegated to the user's
|
55
|
+
latest_unapproved_version
|
56
|
+
|
57
|
+
u = User.new
|
58
|
+
u.first_name = "Dan"
|
59
|
+
u.first_name => nil
|
60
|
+
u.latest_unapproved_version.first_name => "Dan"
|
61
|
+
u.pending_changes => {
|
62
|
+
"first_name" => {
|
63
|
+
"was" => nil,
|
64
|
+
"became" => "Dan"
|
65
|
+
}
|
66
|
+
}
|
67
|
+
u.save
|
68
|
+
|
69
|
+
u.approve_attributes(:first_name)
|
70
|
+
|
71
|
+
u.first_name => "Dan"
|
72
|
+
|
73
|
+
# it created one version and approved it
|
74
|
+
u.versions.count => 1
|
75
|
+
|
76
|
+
# so it won't show up as an unapproved version
|
77
|
+
u.latest_unapproved_version => nil
|
78
|
+
u.pending_changes => {}
|
79
|
+
|
80
|
+
==== Denying attributes
|
81
|
+
|
82
|
+
You can also deny changes. If you do so, they just disappear from
|
83
|
+
the latest_unapproved_version
|
84
|
+
|
85
|
+
u = User.create(:first_name => "X", :last_name => "Y")
|
86
|
+
u.approve_all_attributes
|
87
|
+
|
88
|
+
u.update_attribute(:first_name => "Dan")
|
89
|
+
u.pending_changes => {
|
90
|
+
"first_name" => {
|
91
|
+
"was" => "X",
|
92
|
+
"became" => "Dan"
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
u.deny_attributes(:first_name)
|
97
|
+
# no more changes
|
98
|
+
u.pending_changes => {}
|
99
|
+
|
100
|
+
FYI, this doesn't actually remove the latest_unapproved_version, it just
|
101
|
+
sets its values to be the same as the values in the parent record.
|
102
|
+
|
103
|
+
|
104
|
+
|
105
|
+
|
106
|
+
|
107
|
+
== Contributing to requires_approval
|
108
|
+
|
109
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
110
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
111
|
+
* Fork the project.
|
112
|
+
* Start a feature/bugfix branch.
|
113
|
+
* Commit and push until you are happy with your contribution.
|
114
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
115
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
116
|
+
|
117
|
+
== Copyright
|
118
|
+
|
119
|
+
Copyright (c) 2012 Dan Langevin. See LICENSE.txt for
|
120
|
+
further details.
|
121
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "requires_approval"
|
18
|
+
gem.homepage = "http://github.com/LifebookerInc/requires_approval"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{Gem to handle versioning and things that require approval}
|
21
|
+
gem.description = %Q{Gem to handle versioning and things that require approval}
|
22
|
+
gem.email = "dan.langevin@lifebooker.com"
|
23
|
+
gem.authors = ["Dan Langevin"]
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
end
|
26
|
+
Jeweler::RubygemsDotOrgTasks.new
|
27
|
+
|
28
|
+
require 'rspec/core'
|
29
|
+
require 'rspec/core/rake_task'
|
30
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
31
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
32
|
+
end
|
33
|
+
|
34
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
35
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
36
|
+
spec.rcov = true
|
37
|
+
end
|
38
|
+
|
39
|
+
task :default => :spec
|
40
|
+
|
41
|
+
require 'yard'
|
42
|
+
YARD::Rake::YardocTask.new
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
data/lib/errors.rb
ADDED
@@ -0,0 +1,331 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'errors'
|
3
|
+
|
4
|
+
|
5
|
+
module RequiresApproval
|
6
|
+
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do |klass|
|
10
|
+
klass.send(:extend, ClassMethods)
|
11
|
+
end
|
12
|
+
|
13
|
+
def approve_all_attributes
|
14
|
+
self.approve_attributes(self.fields_requiring_approval)
|
15
|
+
end
|
16
|
+
|
17
|
+
# # approve a list of attributes
|
18
|
+
def approve_attributes(*attributes)
|
19
|
+
|
20
|
+
# validate an normalize our attributes
|
21
|
+
attributes = self.check_attributes_for_approval(attributes)
|
22
|
+
|
23
|
+
# make sure that all attributes are provided if we have never
|
24
|
+
# been approved
|
25
|
+
fields_not_being_approved = (self.fields_requiring_approval - attributes)
|
26
|
+
|
27
|
+
if fields_not_being_approved.present? && self.never_approved?
|
28
|
+
raise PartialApprovalForNewObject.new(
|
29
|
+
"You must approve #{self.fields_requiring_approval.join(", ")} " +
|
30
|
+
"for a new #{self.class.name}"
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
attributes.flatten.each do |attr|
|
35
|
+
write_attribute(attr, self.latest_unapproved_version.send(attr))
|
36
|
+
end
|
37
|
+
|
38
|
+
# if we have approved all requested changes, make our latest
|
39
|
+
# unapproved version approved
|
40
|
+
unless self.has_pending_changes?
|
41
|
+
self.latest_unapproved_version.update_attribute(:is_approved, true)
|
42
|
+
else
|
43
|
+
# makes our latest_unapproved_version approved and
|
44
|
+
# creates another unapproved version with any remaining
|
45
|
+
# attributes
|
46
|
+
self.create_approval_version_record
|
47
|
+
end
|
48
|
+
|
49
|
+
self.is_frozen = false
|
50
|
+
|
51
|
+
self.save
|
52
|
+
self.reload
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
def deny_attributes(*attributes)
|
57
|
+
|
58
|
+
unless self.has_approved_version?
|
59
|
+
raise DenyingNeverApprovedError.new
|
60
|
+
end
|
61
|
+
|
62
|
+
attributes = self.check_attributes_for_approval(attributes)
|
63
|
+
|
64
|
+
attributes.flatten.each do |attr|
|
65
|
+
self.latest_unapproved_version.send("#{attr}=", self.send(attr))
|
66
|
+
true
|
67
|
+
end
|
68
|
+
|
69
|
+
# if we have denied all changes, remove the record
|
70
|
+
unless self.has_pending_changes?
|
71
|
+
self.latest_unapproved_version.destroy
|
72
|
+
else
|
73
|
+
self.latest_unapproved_version.save
|
74
|
+
end
|
75
|
+
|
76
|
+
self.reload
|
77
|
+
true
|
78
|
+
end
|
79
|
+
|
80
|
+
# have any of our versions ever been approved?
|
81
|
+
def has_approved_version?
|
82
|
+
self.versions.where(:is_approved => true).count > 0
|
83
|
+
end
|
84
|
+
|
85
|
+
# have we already approved all outstanding changes?
|
86
|
+
def has_pending_changes?
|
87
|
+
self.pending_changes.present?
|
88
|
+
end
|
89
|
+
|
90
|
+
# the changes users have requested since the last approval
|
91
|
+
def pending_changes
|
92
|
+
return {} if self.latest_unapproved_version.blank?
|
93
|
+
|
94
|
+
ret = {}
|
95
|
+
# check each field requiring approval
|
96
|
+
self.fields_requiring_approval.each do |field|
|
97
|
+
# if it is the same in the unapproved as in the parent table
|
98
|
+
# we skip it
|
99
|
+
unless self.send(field) == self.latest_unapproved_version.send(field)
|
100
|
+
# otherwise we get the change set
|
101
|
+
ret[field] = {
|
102
|
+
"was" => self.send(field),
|
103
|
+
"became" => self.latest_unapproved_version.send(field)
|
104
|
+
}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
ret
|
108
|
+
end
|
109
|
+
|
110
|
+
protected
|
111
|
+
|
112
|
+
# the attributes that require approval
|
113
|
+
def attributes_requiring_approval
|
114
|
+
self.attributes.select{|k,v| self.fields_requiring_approval.include?(k)}
|
115
|
+
end
|
116
|
+
|
117
|
+
# check if our attributes are valid for approval
|
118
|
+
def check_attributes_for_approval(attributes)
|
119
|
+
# normalize attributes
|
120
|
+
attributes = Array.wrap(attributes).flatten.collect(&:to_s)
|
121
|
+
|
122
|
+
# check for invalid attributes
|
123
|
+
invalid_fields = (attributes - self.fields_requiring_approval)
|
124
|
+
# if we have fields not requiring approval, raise an error
|
125
|
+
if invalid_fields.present?
|
126
|
+
raise InvalidFieldsError.new(
|
127
|
+
"fields_requiring_approval don't include #{invalid_fields.join(",")}"
|
128
|
+
)
|
129
|
+
end
|
130
|
+
attributes
|
131
|
+
end
|
132
|
+
|
133
|
+
# creates the record of an individual approval
|
134
|
+
def create_approval_version_record
|
135
|
+
outstanding_changes = self.pending_attributes
|
136
|
+
# update our old latest_unapproved_version to reflect our changes
|
137
|
+
self.latest_unapproved_version.update_attributes(
|
138
|
+
self.attributes_requiring_approval.merge(:is_approved => true)
|
139
|
+
)
|
140
|
+
# reload so this unapproved version is out of our cache and will not
|
141
|
+
# get its foreign key unassigned
|
142
|
+
self.latest_unapproved_version(true)
|
143
|
+
|
144
|
+
self.latest_unapproved_version = self.versions_class.new(
|
145
|
+
self.attributes_requiring_approval.merge(outstanding_changes)
|
146
|
+
)
|
147
|
+
end
|
148
|
+
|
149
|
+
# gets the latest unapproved version or creates a new one
|
150
|
+
def latest_unapproved_version_with_nil_check
|
151
|
+
self.latest_unapproved_version ||= begin
|
152
|
+
self.versions_class.new(self.attributes_requiring_approval)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
# has this record never been approved?
|
157
|
+
def never_approved?
|
158
|
+
!self.has_approved_version?
|
159
|
+
end
|
160
|
+
|
161
|
+
# ActiveRecord-style attribute hash for the
|
162
|
+
# requested changes
|
163
|
+
def pending_attributes
|
164
|
+
ret = {}
|
165
|
+
self.pending_changes.each_pair do |k, change|
|
166
|
+
ret[k] = change["became"]
|
167
|
+
end
|
168
|
+
ret
|
169
|
+
end
|
170
|
+
|
171
|
+
# the class which our versions are
|
172
|
+
def versions_class
|
173
|
+
self.class.versions_class
|
174
|
+
end
|
175
|
+
|
176
|
+
module ClassMethods
|
177
|
+
|
178
|
+
# adds the correct tables and columns for requires_approval
|
179
|
+
def prepare_tables_for_requires_approval
|
180
|
+
self.reset_column_information
|
181
|
+
|
182
|
+
# adds is_active to the parent table
|
183
|
+
self.add_requires_approval_fields
|
184
|
+
self.reset_column_information
|
185
|
+
|
186
|
+
# adds our versions table
|
187
|
+
self.drop_versions_table
|
188
|
+
self.create_versions_table
|
189
|
+
|
190
|
+
end
|
191
|
+
|
192
|
+
def requires_approval_for(*attrs)
|
193
|
+
self.set_options(attrs.extract_options!)
|
194
|
+
|
195
|
+
# set up our attributes that require approval
|
196
|
+
self.class_attribute :fields_requiring_approval
|
197
|
+
self.fields_requiring_approval = attrs.collect(&:to_s)
|
198
|
+
|
199
|
+
# set up delegates
|
200
|
+
self.set_up_version_delegates
|
201
|
+
|
202
|
+
# create a blank version before create to handle if no
|
203
|
+
# attributes were ever set
|
204
|
+
self.before_validation(
|
205
|
+
:latest_unapproved_version_with_nil_check,
|
206
|
+
:on => :create
|
207
|
+
)
|
208
|
+
|
209
|
+
# create the versions class
|
210
|
+
self.create_versions_class
|
211
|
+
self.has_many :versions,
|
212
|
+
:class_name => self.versions_class.name,
|
213
|
+
:foreign_key => self.versions_foreign_key
|
214
|
+
|
215
|
+
self.has_one :latest_unapproved_version,
|
216
|
+
:autosave => true,
|
217
|
+
:class_name => self.versions_class.name,
|
218
|
+
:foreign_key => self.versions_foreign_key,
|
219
|
+
:conditions => [
|
220
|
+
"#{self.versions_table_name}.is_approved = ?", false
|
221
|
+
]
|
222
|
+
end
|
223
|
+
|
224
|
+
def unapproved
|
225
|
+
includes(:latest_unapproved_version)
|
226
|
+
.where("#{self.versions_table_name}.id IS NOT NULL")
|
227
|
+
end
|
228
|
+
|
229
|
+
# the class which our versions are
|
230
|
+
def versions_class
|
231
|
+
"#{self.name}::#{self.versions_class_name}".constantize
|
232
|
+
end
|
233
|
+
|
234
|
+
protected
|
235
|
+
|
236
|
+
def add_requires_approval_fields
|
237
|
+
# add is_frozen
|
238
|
+
unless self.column_names.include?("is_frozen")
|
239
|
+
self.connection.add_column(
|
240
|
+
self.table_name, :is_frozen, :boolean, :default => true
|
241
|
+
)
|
242
|
+
end
|
243
|
+
# add is_deleted
|
244
|
+
unless self.column_names.include?("is_deleted")
|
245
|
+
self.connection.add_column(
|
246
|
+
self.table_name, :is_deleted, :boolean, :default => false
|
247
|
+
)
|
248
|
+
end
|
249
|
+
true
|
250
|
+
end
|
251
|
+
|
252
|
+
# create a class
|
253
|
+
def create_versions_class
|
254
|
+
versions_table_name = self.versions_table_name
|
255
|
+
|
256
|
+
self.const_set self.versions_class_name, Class.new(ActiveRecord::Base)
|
257
|
+
|
258
|
+
self.versions_class.class_eval do
|
259
|
+
self.table_name = versions_table_name
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def create_versions_table
|
264
|
+
self.connection.create_table(self.versions_table_name) do |t|
|
265
|
+
self.columns.each do |column|
|
266
|
+
t.send(column.type, column.name, {
|
267
|
+
:default => column.default,
|
268
|
+
:limit => column.limit,
|
269
|
+
:null => column.null,
|
270
|
+
:precision => column.precision,
|
271
|
+
:scale => column.scale
|
272
|
+
})
|
273
|
+
end
|
274
|
+
t.integer self.versions_foreign_key
|
275
|
+
t.boolean :is_approved, :default => false
|
276
|
+
end
|
277
|
+
self.connection.add_index(
|
278
|
+
self.versions_table_name,
|
279
|
+
[self.versions_foreign_key, :is_approved]
|
280
|
+
)
|
281
|
+
end
|
282
|
+
|
283
|
+
# drop the versions table if it exists
|
284
|
+
def drop_versions_table
|
285
|
+
if self.connection.tables.include?(self.versions_table_name)
|
286
|
+
self.connection.drop_table(self.versions_table_name)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def set_options(opts = {})
|
291
|
+
@versions_class_name = opts.delete(:versions_class_name)
|
292
|
+
@version_foreign_key = opts.delete(:versions_foreign_key)
|
293
|
+
@versions_table_name = opts.delete(:versions_table_name)
|
294
|
+
end
|
295
|
+
|
296
|
+
def set_up_version_delegates
|
297
|
+
self.fields_requiring_approval.each do |f|
|
298
|
+
define_method("#{f}=") do |val|
|
299
|
+
self.send("#{f}_will_change!")
|
300
|
+
self.latest_unapproved_version_with_nil_check.send("#{f}=", val)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def validates_approved_field(*args)
|
306
|
+
self.versions_class.class_eval do
|
307
|
+
validates(*args)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# class name for our versions
|
312
|
+
def versions_class_name
|
313
|
+
@versions_class_name ||= "Version"
|
314
|
+
end
|
315
|
+
|
316
|
+
# foreign key for our class on the version table
|
317
|
+
def versions_foreign_key
|
318
|
+
@version_foreign_key ||= "#{self.base_class.name.underscore}_id"
|
319
|
+
end
|
320
|
+
|
321
|
+
# table name for our versions
|
322
|
+
def versions_table_name
|
323
|
+
@versions_table_name ||= "#{self.base_class.name.underscore}_versions"
|
324
|
+
end
|
325
|
+
|
326
|
+
end
|
327
|
+
|
328
|
+
|
329
|
+
end
|
330
|
+
|
331
|
+
ActiveRecord::Base.send(:include, RequiresApproval)
|