kb-acts_as_revisable 1.0.3
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.
- data/LICENSE +20 -0
- data/README.rdoc +221 -0
- data/Rakefile +44 -0
- data/generators/revisable_migration/revisable_migration_generator.rb +21 -0
- data/generators/revisable_migration/templates/migration.rb +14 -0
- data/lib/acts_as_revisable/acts/common.rb +209 -0
- data/lib/acts_as_revisable/acts/deletable.rb +33 -0
- data/lib/acts_as_revisable/acts/revisable.rb +491 -0
- data/lib/acts_as_revisable/acts/revision.rb +149 -0
- data/lib/acts_as_revisable/base.rb +52 -0
- data/lib/acts_as_revisable/gem_spec_options.rb +18 -0
- data/lib/acts_as_revisable/options.rb +22 -0
- data/lib/acts_as_revisable/quoted_columns.rb +31 -0
- data/lib/acts_as_revisable/version.rb +11 -0
- data/lib/acts_as_revisable.rb +10 -0
- data/rails/init.rb +1 -0
- data/spec/associations_spec.rb +22 -0
- data/spec/branch_spec.rb +42 -0
- data/spec/deletable_spec.rb +16 -0
- data/spec/find_spec.rb +30 -0
- data/spec/general_spec.rb +79 -0
- data/spec/options_spec.rb +83 -0
- data/spec/quoted_columns_spec.rb +19 -0
- data/spec/revert_spec.rb +42 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +93 -0
- metadata +85 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 JamLab, LLC.
|
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,221 @@
|
|
1
|
+
= acts_as_revisable
|
2
|
+
|
3
|
+
http://github.com/rich/acts_as_revisable
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
acts_as_revisable enables revision tracking, querying, reverting and branching of ActiveRecord models. It does this while providing more Rails-like API than similar plugins. This includes extensions of standard ActiveRecord methods and numerous custom callbacks for the entire AAR life-cycle.
|
8
|
+
|
9
|
+
This plugin wouldn't exist without Rick Olsen's acts_as_versioned. AAV has been a critical part of practically every Rails project I've developed. It's only through extensive usage of AAV that the concepts for AAR came about.
|
10
|
+
|
11
|
+
== FEATURES:
|
12
|
+
|
13
|
+
* Both the revisable and revision models must be explicitly defined.
|
14
|
+
Yes, this is a feature. The less magic needed the better. This allows you to build up your revision models just as you would any other.
|
15
|
+
|
16
|
+
If you absolutely need a generated revision model, you may pass ":generate_revision_class => true" to acts_as_revisable and it will generate the class at runtime for you. Think of this like scaffolding and not to be kept around for a real application.
|
17
|
+
|
18
|
+
* Numerous custom callbacks for both revisable and revision models.
|
19
|
+
* revisable models
|
20
|
+
* before_revise
|
21
|
+
* after_revise
|
22
|
+
* before_revert
|
23
|
+
* after_revert
|
24
|
+
* before_changeset
|
25
|
+
* after_changeset
|
26
|
+
* after_branch_created
|
27
|
+
* before_revise_on_destroy (when :on_destroy => :revise is set)
|
28
|
+
* after_revise_on_destroy (when :on_destroy => :revise is set)
|
29
|
+
* revision models
|
30
|
+
* before_restore
|
31
|
+
* after_restore
|
32
|
+
* both revisable and revision models
|
33
|
+
* before_branch
|
34
|
+
* after_branch
|
35
|
+
These work like any other ActiveRecord callbacks. The before_* callbacks can stop the the action. This uses the Callbacks module in ActiveSupport.
|
36
|
+
* Works with a single table.
|
37
|
+
* Provides migration generators to add the revisable columns.
|
38
|
+
* Grouping several revisable actions into a single revision (changeset).
|
39
|
+
* Monitor all or just specified columns to trigger a revision.
|
40
|
+
* Uses ActiveRecord's dirty attribute tracking.
|
41
|
+
* Several ways to find revisions including:
|
42
|
+
* revision number
|
43
|
+
* relative keywords (:first, :previous and :last)
|
44
|
+
* timestamp
|
45
|
+
* Reverting
|
46
|
+
* Branching
|
47
|
+
* Selectively disable revision tracking
|
48
|
+
* Naming revisions
|
49
|
+
|
50
|
+
== SYNOPSIS:
|
51
|
+
|
52
|
+
Given a simple model:
|
53
|
+
|
54
|
+
class Project < ActiveRecord::Base
|
55
|
+
# columns: id, name, unimportant, created_at
|
56
|
+
end
|
57
|
+
|
58
|
+
Let's make the projects table revisable:
|
59
|
+
|
60
|
+
ruby script/generate revisable_migration Project
|
61
|
+
rake db:migrate
|
62
|
+
|
63
|
+
Now Project itself:
|
64
|
+
|
65
|
+
class Project < ActiveRecord::Base
|
66
|
+
has_one :owner
|
67
|
+
|
68
|
+
acts_as_revisable do
|
69
|
+
revision_class_name "Session"
|
70
|
+
except :unimportant
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
Create the revision class:
|
75
|
+
|
76
|
+
class Session < ActiveRecord::Base
|
77
|
+
# we can accept the more standard hash syntax
|
78
|
+
acts_as_revision :revisable_class_name => "Project"
|
79
|
+
end
|
80
|
+
|
81
|
+
Some example usage:
|
82
|
+
|
83
|
+
@project = Project.create(:name => "Rich", :unimportant => "some text")
|
84
|
+
@project.revision_number # => 0
|
85
|
+
|
86
|
+
@project.update_attribute(:unimportant, "more text")
|
87
|
+
@project.revision_number # => 0
|
88
|
+
|
89
|
+
@project.name = "Stephen"
|
90
|
+
@project.save(:without_revision => true)
|
91
|
+
@project.name # => "Stephen"
|
92
|
+
@project.revision_number # => 0
|
93
|
+
|
94
|
+
@project.name = "Sam"
|
95
|
+
@project.save(:revision_name => "Changed name")
|
96
|
+
@project.revision_number # => 1
|
97
|
+
|
98
|
+
@project.updated_attribute(:name, "Third")
|
99
|
+
@project.revision_number # => 2
|
100
|
+
|
101
|
+
Navigating revisions:
|
102
|
+
|
103
|
+
@previous = @project.find_revision(:previous)
|
104
|
+
# or
|
105
|
+
@previous = @project.revisions.first
|
106
|
+
|
107
|
+
@previous.name # => "Sam"
|
108
|
+
@previous.current_revision.name # => "Third"
|
109
|
+
@previous.project.name # => "Third"
|
110
|
+
@previous.revision_name # => "Changed name"
|
111
|
+
|
112
|
+
@previous.previous.name # => "Rich"
|
113
|
+
|
114
|
+
# Forcing the creation of a new revision.
|
115
|
+
@project.updated_attribute("Rogelio")
|
116
|
+
@project.revision_number # => 3
|
117
|
+
|
118
|
+
@newest = @project.find_revision(:previous)
|
119
|
+
@newest.ancestors.map(&:name) # => ["Third", "Rich"]
|
120
|
+
|
121
|
+
@oldest = @project.find_revision(:first)
|
122
|
+
@oldest.descendants.map(&:name) # => ["Sam", "Third"]
|
123
|
+
|
124
|
+
Reverting:
|
125
|
+
|
126
|
+
@project.revert_to!(:previous)
|
127
|
+
@project.revision_number # => 2
|
128
|
+
@project.name # => "Rich"
|
129
|
+
|
130
|
+
@project.revert_to!(1, :without_revision => true)
|
131
|
+
@project.revision_number # => 2
|
132
|
+
@project.name # => "Sam"
|
133
|
+
|
134
|
+
Branching:
|
135
|
+
|
136
|
+
@branch = @project.branch(:name => "Bruno")
|
137
|
+
@branch.revision_number # => 0
|
138
|
+
@branch.branch_source.name # => "Sam"
|
139
|
+
|
140
|
+
Changesets:
|
141
|
+
|
142
|
+
@project.revision_number # => 2
|
143
|
+
|
144
|
+
@project.changeset! do
|
145
|
+
@project.name = "Josh"
|
146
|
+
|
147
|
+
# save would normally trigger a revision
|
148
|
+
@project.save
|
149
|
+
|
150
|
+
# update_attribute triggers a save triggering a revision (normally)
|
151
|
+
@project.updated_attribute(:name, "Chris")
|
152
|
+
|
153
|
+
# revise! normally forces a revision to be created
|
154
|
+
@project.revise!
|
155
|
+
end
|
156
|
+
|
157
|
+
# our revision number has only incremented by one
|
158
|
+
@project.revision_number # => 3
|
159
|
+
|
160
|
+
Maybe we don't want to be able to branch from revisions:
|
161
|
+
|
162
|
+
class Session < ActiveRecord::Base
|
163
|
+
# assuming we still have the other code from Session above
|
164
|
+
|
165
|
+
before_branch do
|
166
|
+
false
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
@project.revisions.first.branch # Raises an exception
|
171
|
+
@project.branch # works as expected
|
172
|
+
|
173
|
+
If the owner isn't set let's prevent reverting:
|
174
|
+
|
175
|
+
class Project < ActiveRecord::Base
|
176
|
+
# assuming we still have the other code from Project above
|
177
|
+
|
178
|
+
before_revert :check_owner_befor_reverting
|
179
|
+
def check_owner_befor_reverting
|
180
|
+
false unless self.owner?
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
== REQUIREMENTS:
|
185
|
+
|
186
|
+
This plugin requires Rails 2.3. Use version 0.9.8 of this plugin for Rails 2.1 and 2.2.
|
187
|
+
|
188
|
+
== INSTALL:
|
189
|
+
|
190
|
+
acts_as_revisable uses Rails' new ability to use gems as plugins. Installing AAR is as simple as installing a gem:
|
191
|
+
|
192
|
+
sudo gem install rich-acts_as_revisable --source=http://gems.github.com
|
193
|
+
|
194
|
+
Once the gem is installed you'll want to activate it in your Rails app by adding the following line to config/environment.rb:
|
195
|
+
|
196
|
+
config.gem "rich-acts_as_revisable", :lib => "acts_as_revisable", :source => "http://gems.github.com"
|
197
|
+
|
198
|
+
== LICENSE:
|
199
|
+
|
200
|
+
(The MIT License)
|
201
|
+
|
202
|
+
Copyright (c) 2009 Rich Cavanaugh
|
203
|
+
|
204
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
205
|
+
a copy of this software and associated documentation files (the
|
206
|
+
'Software'), to deal in the Software without restriction, including
|
207
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
208
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
209
|
+
permit persons to whom the Software is furnished to do so, subject to
|
210
|
+
the following conditions:
|
211
|
+
|
212
|
+
The above copyright notice and this permission notice shall be
|
213
|
+
included in all copies or substantial portions of the Software.
|
214
|
+
|
215
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
216
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
217
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
218
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
219
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
220
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
221
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rake/rdoctask'
|
2
|
+
require 'rake/gempackagetask'
|
3
|
+
require 'fileutils'
|
4
|
+
require 'lib/acts_as_revisable/version'
|
5
|
+
require 'lib/acts_as_revisable/gem_spec_options'
|
6
|
+
|
7
|
+
Rake::RDocTask.new do |rdoc|
|
8
|
+
files = ['README.rdoc','LICENSE','lib/**/*.rb','doc/**/*.rdoc','spec/*.rb']
|
9
|
+
rdoc.rdoc_files.add(files)
|
10
|
+
rdoc.main = 'README.rdoc'
|
11
|
+
rdoc.title = 'acts_as_revisable RDoc'
|
12
|
+
rdoc.rdoc_dir = 'doc'
|
13
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
14
|
+
end
|
15
|
+
|
16
|
+
spec = Gem::Specification.new do |s|
|
17
|
+
WithoutScope::ActsAsRevisable::GemSpecOptions::HASH.each do |key, value|
|
18
|
+
s.send("#{key.to_s}=",value)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
Rake::GemPackageTask.new(spec) do |package|
|
23
|
+
package.gem_spec = spec
|
24
|
+
end
|
25
|
+
|
26
|
+
desc "Generate the static gemspec required for github."
|
27
|
+
task :generate_gemspec do
|
28
|
+
options = WithoutScope::ActsAsRevisable::GemSpecOptions::HASH.clone
|
29
|
+
options[:name] = "acts_as_revisable"
|
30
|
+
|
31
|
+
spec = ["Gem::Specification.new do |s|"]
|
32
|
+
options.each do |key, value|
|
33
|
+
spec << " s.#{key.to_s} = #{value.inspect}"
|
34
|
+
end
|
35
|
+
spec << "end"
|
36
|
+
|
37
|
+
open("acts_as_revisable.gemspec", "w").write(spec.join("\n"))
|
38
|
+
end
|
39
|
+
|
40
|
+
desc "Install acts_as_revisable"
|
41
|
+
task :install => :repackage do
|
42
|
+
options = WithoutScope::ActsAsRevisable::GemSpecOptions::HASH.clone
|
43
|
+
sh %{sudo gem install pkg/#{options[:name]}-#{spec.version} --no-rdoc --no-ri}
|
44
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class RevisableMigrationGenerator < Rails::Generator::NamedBase
|
2
|
+
def manifest
|
3
|
+
record do |m|
|
4
|
+
revisable_columns = [
|
5
|
+
["revisable_original_id", "integer"],
|
6
|
+
["revisable_branched_from_id", "integer"],
|
7
|
+
["revisable_number", "integer", 0],
|
8
|
+
["revisable_name", "string"],
|
9
|
+
["revisable_type", "string"],
|
10
|
+
["revisable_current_at", "datetime"],
|
11
|
+
["revisable_revised_at", "datetime"],
|
12
|
+
["revisable_deleted_at", "datetime"],
|
13
|
+
["revisable_is_current", "boolean", 1]
|
14
|
+
]
|
15
|
+
|
16
|
+
m.migration_template 'migration.rb', 'db/migrate',
|
17
|
+
:migration_file_name => "make_#{class_name.underscore.pluralize}_revisable",
|
18
|
+
:assigns => { :cols => revisable_columns }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<% table_name = class_name.underscore.pluralize -%>
|
2
|
+
class Make<%= class_name.underscore.pluralize.camelize %>Revisable < ActiveRecord::Migration
|
3
|
+
def self.up
|
4
|
+
<% cols.each do |column_name,column_type,default| -%>
|
5
|
+
add_column :<%= table_name %>, :<%= column_name %>, :<%= column_type %><%= ", :default => #{default}" unless default.blank? %>
|
6
|
+
<% end -%>
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.down
|
10
|
+
<% cols.each do |column_name,_| -%>
|
11
|
+
remove_column :<%= table_name %>, :<%= column_name %>
|
12
|
+
<% end -%>
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
module WithoutScope
|
2
|
+
module ActsAsRevisable
|
3
|
+
# This module is mixed into the revision and revisable classes.
|
4
|
+
#
|
5
|
+
# ==== Callbacks
|
6
|
+
#
|
7
|
+
# * +before_branch+ is called on the +Revisable+ or +Revision+ that is
|
8
|
+
# being branched
|
9
|
+
# * +after_branch+ is called on the +Revisable+ or +Revision+ that is
|
10
|
+
# being branched
|
11
|
+
module Common
|
12
|
+
def self.included(base) #:nodoc:
|
13
|
+
base.send(:extend, ClassMethods)
|
14
|
+
|
15
|
+
base.class_inheritable_hash :revisable_after_callback_blocks
|
16
|
+
base.revisable_after_callback_blocks = {}
|
17
|
+
|
18
|
+
base.class_inheritable_hash :revisable_current_states
|
19
|
+
base.revisable_current_states = {}
|
20
|
+
|
21
|
+
base.instance_eval do
|
22
|
+
define_callbacks :before_branch, :after_branch
|
23
|
+
has_many :branches, (revisable_options.revision_association_options || {}).merge({:class_name => base.class_name, :foreign_key => :revisable_branched_from_id})
|
24
|
+
|
25
|
+
belongs_to :branch_source, :class_name => base.class_name, :foreign_key => :revisable_branched_from_id
|
26
|
+
after_save :execute_blocks_after_save
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Executes the blocks stored in an accessor after a save.
|
31
|
+
def execute_blocks_after_save #:nodoc:
|
32
|
+
return unless revisable_after_callback_blocks[:save]
|
33
|
+
revisable_after_callback_blocks[:save].each do |block|
|
34
|
+
block.call
|
35
|
+
end
|
36
|
+
revisable_after_callback_blocks.delete(:save)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Stores a block for later execution after a given callback.
|
40
|
+
# The parameter +key+ is the callback the block should be
|
41
|
+
# executed after.
|
42
|
+
def execute_after(key, &block) #:nodoc:
|
43
|
+
return unless block_given?
|
44
|
+
revisable_after_callback_blocks[key] ||= []
|
45
|
+
revisable_after_callback_blocks[key] << block
|
46
|
+
end
|
47
|
+
|
48
|
+
# Branch the +Revisable+ or +Revision+ and return the new
|
49
|
+
# +revisable+ instance. The instance has not been saved yet.
|
50
|
+
#
|
51
|
+
# ==== Callbacks
|
52
|
+
# * +before_branch+ is called on the +Revisable+ or +Revision+ that is
|
53
|
+
# being branched
|
54
|
+
# * +after_branch+ is called on the +Revisable+ or +Revision+ that is
|
55
|
+
# being branched
|
56
|
+
# * +after_branch_created+ is called on the newly created
|
57
|
+
# +Revisable+ instance.
|
58
|
+
def branch(*args, &block)
|
59
|
+
is_branching!
|
60
|
+
|
61
|
+
unless run_callbacks(:before_branch) { |r, o| r == false}
|
62
|
+
raise ActiveRecord::RecordNotSaved
|
63
|
+
end
|
64
|
+
|
65
|
+
options = args.extract_options!
|
66
|
+
options[:revisable_branched_from_id] = self.id
|
67
|
+
self.class.column_names.each do |col|
|
68
|
+
next unless self.class.revisable_should_clone_column? col
|
69
|
+
options[col.to_sym] = self[col] unless options.has_key?(col.to_sym)
|
70
|
+
end
|
71
|
+
|
72
|
+
br = self.class.revisable_class.new(options)
|
73
|
+
br.is_branching!
|
74
|
+
|
75
|
+
br.execute_after(:save) do
|
76
|
+
begin
|
77
|
+
run_callbacks(:after_branch)
|
78
|
+
br.run_callbacks(:after_branch_created)
|
79
|
+
ensure
|
80
|
+
br.is_branching!(false)
|
81
|
+
is_branching!(false)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
block.call(br) if block_given?
|
86
|
+
|
87
|
+
br
|
88
|
+
end
|
89
|
+
|
90
|
+
# Same as #branch except it calls #save! on the new +Revisable+ instance.
|
91
|
+
def branch!(*args)
|
92
|
+
branch(*args) do |br|
|
93
|
+
br.save!
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Globally sets the reverting state of this record.
|
98
|
+
def is_branching!(value=true) #:nodoc:
|
99
|
+
set_revisable_state(:branching, value)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Returns true if the _record_ (not just this instance
|
103
|
+
# of the record) is currently being branched.
|
104
|
+
def is_branching?
|
105
|
+
get_revisable_state(:branching)
|
106
|
+
end
|
107
|
+
|
108
|
+
# When called on a +Revision+ it returns the original id. When
|
109
|
+
# called on a +Revisable+ it returns the id.
|
110
|
+
def original_id
|
111
|
+
self[:revisable_original_id] || self[:id]
|
112
|
+
end
|
113
|
+
|
114
|
+
# Globally sets the state for a given record. This is keyed
|
115
|
+
# on the primary_key of a saved record or the object_id
|
116
|
+
# on a new instance.
|
117
|
+
def set_revisable_state(type, value) #:nodoc:
|
118
|
+
key = self.read_attribute(self.class.primary_key)
|
119
|
+
key = object_id if key.nil?
|
120
|
+
revisable_current_states[type] ||= {}
|
121
|
+
revisable_current_states[type][key] = value
|
122
|
+
revisable_current_states[type].delete(key) unless value
|
123
|
+
end
|
124
|
+
|
125
|
+
# Returns the state of the given record.
|
126
|
+
def get_revisable_state(type) #:nodoc:
|
127
|
+
key = self.read_attribute(self.class.primary_key)
|
128
|
+
revisable_current_states[type] ||= {}
|
129
|
+
revisable_current_states[type][key] || revisable_current_states[type][object_id] || false
|
130
|
+
end
|
131
|
+
|
132
|
+
# Returns true if the instance is the first revision.
|
133
|
+
def first_revision?
|
134
|
+
self.revision_number == 1
|
135
|
+
end
|
136
|
+
|
137
|
+
# Returns true if the instance is the most recent revision.
|
138
|
+
def latest_revision?
|
139
|
+
self.revision_number == self.current_revision.revision_number
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns true if the instance is the current record and not a revision.
|
143
|
+
def current_revision?
|
144
|
+
self.is_a? self.class.revisable_class
|
145
|
+
end
|
146
|
+
|
147
|
+
# Accessor for revisable_number just to make external API more pleasant.
|
148
|
+
def revision_number
|
149
|
+
self[:revisable_number] ||= 0
|
150
|
+
end
|
151
|
+
|
152
|
+
def revision_number=(value)
|
153
|
+
self[:revisable_number] = value
|
154
|
+
end
|
155
|
+
|
156
|
+
def diffs(what)
|
157
|
+
what = current_revision.find_revision(what)
|
158
|
+
returning({}) do |changes|
|
159
|
+
self.class.revisable_class.revisable_watch_columns.each do |c|
|
160
|
+
changes[c] = [self[c], what[c]] unless self[c] == what[c]
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def deleted?
|
166
|
+
self.revisable_deleted_at.present?
|
167
|
+
end
|
168
|
+
|
169
|
+
module ClassMethods
|
170
|
+
# Returns true if the revision should clone the given column.
|
171
|
+
def revisable_should_clone_column?(col) #:nodoc:
|
172
|
+
return false if (REVISABLE_SYSTEM_COLUMNS + REVISABLE_UNREVISABLE_COLUMNS).member? col
|
173
|
+
true
|
174
|
+
end
|
175
|
+
|
176
|
+
# acts_as_revisable's override for instantiate so we can
|
177
|
+
# return the appropriate type of model based on whether
|
178
|
+
# or not the record is the current record.
|
179
|
+
def instantiate(record) #:nodoc:
|
180
|
+
is_current = columns_hash["revisable_is_current"].type_cast(
|
181
|
+
record["revisable_is_current"])
|
182
|
+
|
183
|
+
if (is_current && self == self.revisable_class) || (!is_current && self == self.revision_class)
|
184
|
+
return super(record)
|
185
|
+
end
|
186
|
+
|
187
|
+
object = if is_current
|
188
|
+
self.revisable_class
|
189
|
+
else
|
190
|
+
self.revision_class
|
191
|
+
end.allocate
|
192
|
+
|
193
|
+
object.instance_variable_set("@attributes", record)
|
194
|
+
object.instance_variable_set("@attributes_cache", Hash.new)
|
195
|
+
|
196
|
+
if object.respond_to_without_attributes?(:after_find)
|
197
|
+
object.send(:callback, :after_find)
|
198
|
+
end
|
199
|
+
|
200
|
+
if object.respond_to_without_attributes?(:after_initialize)
|
201
|
+
object.send(:callback, :after_initialize)
|
202
|
+
end
|
203
|
+
|
204
|
+
object
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module WithoutScope
|
2
|
+
module ActsAsRevisable
|
3
|
+
module Deletable
|
4
|
+
def self.included(base)
|
5
|
+
base.instance_eval do
|
6
|
+
define_callbacks :before_revise_on_destroy, :after_revise_on_destroy
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def destroy
|
11
|
+
now = Time.zone.now
|
12
|
+
|
13
|
+
prev = self.revisions.first
|
14
|
+
self.revisable_deleted_at = now
|
15
|
+
self.revisable_is_current = false
|
16
|
+
|
17
|
+
self.revisable_current_at = if prev
|
18
|
+
prev.update_attribute(:revisable_revised_at, now)
|
19
|
+
prev.revisable_revised_at + 1.second
|
20
|
+
else
|
21
|
+
self.created_at
|
22
|
+
end
|
23
|
+
|
24
|
+
self.revisable_revised_at = self.revisable_deleted_at
|
25
|
+
|
26
|
+
return false unless run_callbacks(:before_revise_on_destroy) { |r, o| r == false}
|
27
|
+
returning(self.save(:without_revision => true)) do
|
28
|
+
run_callbacks(:after_revise_on_destroy)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|