rich-acts_as_revisable 0.6.0 → 0.9.8
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +1 -1
- data/{README → README.rdoc} +54 -25
- data/Rakefile +44 -0
- data/lib/acts_as_revisable/acts/common.rb +170 -23
- data/lib/acts_as_revisable/acts/deletable.rb +29 -0
- data/lib/acts_as_revisable/acts/revisable.rb +362 -127
- data/lib/acts_as_revisable/acts/revision.rb +94 -21
- data/lib/acts_as_revisable/acts/scoped_model.rb +28 -4
- data/lib/acts_as_revisable/base.rb +6 -3
- data/lib/acts_as_revisable/clone_associations.rb +8 -0
- data/lib/acts_as_revisable/gem_spec_options.rb +18 -0
- data/lib/acts_as_revisable/quoted_columns.rb +14 -9
- data/lib/acts_as_revisable/version.rb +2 -2
- data/spec/associations_spec.rb +22 -0
- data/spec/branch_spec.rb +42 -0
- data/spec/find_spec.rb +38 -0
- data/spec/{acts_as_revisable_spec.rb → general_spec.rb} +21 -26
- data/spec/quoted_columns_spec.rb +19 -0
- data/spec/revert_spec.rb +42 -0
- data/spec/spec_helper.rb +41 -5
- metadata +29 -16
- /data/spec/{aar_options_spec.rb → options_spec.rb} +0 -0
data/LICENSE
CHANGED
data/{README → README.rdoc}
RENAMED
@@ -1,6 +1,6 @@
|
|
1
1
|
= acts_as_revisable
|
2
2
|
|
3
|
-
http://github.com/
|
3
|
+
http://github.com/fatjam/acts_as_revisable/tree/master
|
4
4
|
|
5
5
|
== DESCRIPTION:
|
6
6
|
|
@@ -37,7 +37,7 @@ This plugin wouldn't exist without Rick Olsen's acts_as_versioned. AAV has been
|
|
37
37
|
* Uses ActiveRecord's dirty attribute tracking.
|
38
38
|
* Several ways to find revisions including:
|
39
39
|
* revision number
|
40
|
-
* relative keywords (:
|
40
|
+
* relative keywords (:first, :previous and :last)
|
41
41
|
* timestamp
|
42
42
|
* Reverting
|
43
43
|
* Branching
|
@@ -85,40 +85,75 @@ Some example usage:
|
|
85
85
|
|
86
86
|
@project.name = "Stephen"
|
87
87
|
@project.save(:without_revision => true)
|
88
|
-
@project.name # => Stephen
|
88
|
+
@project.name # => "Stephen"
|
89
89
|
@project.revision_number # => 0
|
90
90
|
|
91
91
|
@project.name = "Sam"
|
92
92
|
@project.save(:revision_name => "Changed name")
|
93
93
|
@project.revision_number # => 1
|
94
|
-
|
94
|
+
|
95
|
+
@project.updated_attribute(:name, "Third")
|
96
|
+
@project.revision_number # => 2
|
97
|
+
|
95
98
|
Navigating revisions:
|
96
|
-
|
99
|
+
|
97
100
|
@previous = @project.find_revision(:previous)
|
98
101
|
# or
|
99
102
|
@previous = @project.revisions.first
|
100
103
|
|
101
|
-
@previous.name # =>
|
102
|
-
@previous.current_revision.name # =>
|
103
|
-
@previous.project.name # =>
|
104
|
-
@previous.revision_name # => Changed name
|
105
|
-
|
104
|
+
@previous.name # => "Sam"
|
105
|
+
@previous.current_revision.name # => "Third"
|
106
|
+
@previous.project.name # => "Third"
|
107
|
+
@previous.revision_name # => "Changed name"
|
108
|
+
|
109
|
+
@previous.previous.name # => "Rich"
|
110
|
+
|
111
|
+
# Forcing the creation of a new revision.
|
112
|
+
@project.updated_attribute("Rogelio")
|
113
|
+
@project.revision_number # => 3
|
114
|
+
|
115
|
+
@newest = @project.find_revision(:previous)
|
116
|
+
@newest.ancestors.map(&:name) # => ["Third", "Rich"]
|
117
|
+
|
118
|
+
@oldest = @project.find_revision(:first)
|
119
|
+
@oldest.descendants.map(&:name) # => ["Sam", "Third"]
|
120
|
+
|
106
121
|
Reverting:
|
107
122
|
|
108
123
|
@project.revert_to!(:previous)
|
109
124
|
@project.revision_number # => 2
|
110
|
-
@project.name # => Rich
|
125
|
+
@project.name # => "Rich"
|
111
126
|
|
112
127
|
@project.revert_to!(1, :without_revision => true)
|
113
128
|
@project.revision_number # => 2
|
114
|
-
@project.name # => Sam
|
129
|
+
@project.name # => "Sam"
|
115
130
|
|
116
|
-
Branching
|
131
|
+
Branching:
|
117
132
|
|
118
133
|
@branch = @project.branch(:name => "Bruno")
|
119
134
|
@branch.revision_number # => 0
|
120
|
-
@branch.branch_source.name # => Sam
|
135
|
+
@branch.branch_source.name # => "Sam"
|
121
136
|
|
137
|
+
Changesets:
|
138
|
+
|
139
|
+
@project.revision_number # => 2
|
140
|
+
|
141
|
+
@project.changeset! do
|
142
|
+
@project.name = "Josh"
|
143
|
+
|
144
|
+
# save would normally trigger a revision
|
145
|
+
@project.save
|
146
|
+
|
147
|
+
# update_attribute triggers a save triggering a revision (normally)
|
148
|
+
@project.updated_attribute(:name, "Chris")
|
149
|
+
|
150
|
+
# revise! normally forces a revision to be created
|
151
|
+
@project.revise!
|
152
|
+
end
|
153
|
+
|
154
|
+
# our revision number has only incremented by one
|
155
|
+
@project.revision_number # => 3
|
156
|
+
|
122
157
|
Associations have been cloned:
|
123
158
|
|
124
159
|
@project.owner === @previous.owner # => true
|
@@ -146,32 +181,26 @@ If the owner isn't set let's prevent reverting:
|
|
146
181
|
false unless self.owner?
|
147
182
|
end
|
148
183
|
end
|
149
|
-
|
184
|
+
|
150
185
|
== REQUIREMENTS:
|
151
186
|
|
152
187
|
This plugin currently depends on Edge Rails, ActiveRecord and ActiveSupport specifically, which will eventually become Rails 2.1.
|
153
188
|
|
154
189
|
== INSTALL:
|
155
190
|
|
156
|
-
acts_as_revisable uses Rails' new ability to use gems as plugins. Installing AAR is as simple as installing a gem
|
157
|
-
|
158
|
-
You may need to add GitHub as a Gem source:
|
159
|
-
|
160
|
-
sudo gem sources -a http://gems.github.com/
|
161
|
-
|
162
|
-
Then it can be installed as usual:
|
191
|
+
acts_as_revisable uses Rails' new ability to use gems as plugins. Installing AAR is as simple as installing a gem:
|
163
192
|
|
164
|
-
sudo gem install
|
193
|
+
sudo gem install fatjam-acts_as_revisable --source=http://gems.github.com
|
165
194
|
|
166
195
|
Once the gem is installed you'll want to activate it in your Rails app by adding the following line to config/environment.rb:
|
167
196
|
|
168
|
-
config.gem "
|
197
|
+
config.gem "fatjam-acts_as_revisable", :lib => "acts_as_revisable", :source => "http://gems.github.com"
|
169
198
|
|
170
199
|
== LICENSE:
|
171
200
|
|
172
201
|
(The MIT License)
|
173
202
|
|
174
|
-
Copyright (c) 2008
|
203
|
+
Copyright (c) 2008 JamLab, LLC.
|
175
204
|
|
176
205
|
Permission is hereby granted, free of charge, to any person obtaining
|
177
206
|
a copy of this software and associated documentation files (the
|
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
|
+
FatJam::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 = FatJam::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 = FatJam::ActsAsRevisable::GemSpecOptions::HASH.clone
|
43
|
+
sh %{sudo gem install pkg/#{options[:name]}-#{spec.version} --no-rdoc --no-ri}
|
44
|
+
end
|
@@ -1,29 +1,68 @@
|
|
1
1
|
module FatJam
|
2
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
|
3
11
|
module Common
|
4
|
-
def self.included(base)
|
12
|
+
def self.included(base) #:nodoc:
|
5
13
|
base.send(:extend, ClassMethods)
|
6
|
-
|
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
|
+
|
7
21
|
class << base
|
8
22
|
alias_method_chain :instantiate, :revisable
|
9
23
|
end
|
10
24
|
|
11
25
|
base.instance_eval do
|
12
26
|
define_callbacks :before_branch, :after_branch
|
13
|
-
has_many :branches, :class_name => base.class_name, :foreign_key => :revisable_branched_from_id
|
27
|
+
has_many :branches, (revisable_options.revision_association_options || {}).merge({:class_name => base.class_name, :foreign_key => :revisable_branched_from_id})
|
28
|
+
|
14
29
|
belongs_to :branch_source, :class_name => base.class_name, :foreign_key => :revisable_branched_from_id
|
15
|
-
|
16
|
-
|
17
|
-
|
30
|
+
after_save :execute_blocks_after_save
|
31
|
+
disable_revisable_scope :branch_source, :branches
|
32
|
+
end
|
18
33
|
end
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
34
|
+
|
35
|
+
# Executes the blocks stored in an accessor after a save.
|
36
|
+
def execute_blocks_after_save #:nodoc:
|
37
|
+
return unless revisable_after_callback_blocks[:save]
|
38
|
+
revisable_after_callback_blocks[:save].each do |block|
|
39
|
+
block.call
|
23
40
|
end
|
41
|
+
revisable_after_callback_blocks.delete(:save)
|
24
42
|
end
|
25
|
-
|
26
|
-
|
43
|
+
|
44
|
+
# Stores a block for later execution after a given callback.
|
45
|
+
# The parameter +key+ is the callback the block should be
|
46
|
+
# executed after.
|
47
|
+
def execute_after(key, &block) #:nodoc:
|
48
|
+
return unless block_given?
|
49
|
+
revisable_after_callback_blocks[key] ||= []
|
50
|
+
revisable_after_callback_blocks[key] << block
|
51
|
+
end
|
52
|
+
|
53
|
+
# Branch the +Revisable+ or +Revision+ and return the new
|
54
|
+
# +revisable+ instance. The instance has not been saved yet.
|
55
|
+
#
|
56
|
+
# ==== Callbacks
|
57
|
+
# * +before_branch+ is called on the +Revisable+ or +Revision+ that is
|
58
|
+
# being branched
|
59
|
+
# * +after_branch+ is called on the +Revisable+ or +Revision+ that is
|
60
|
+
# being branched
|
61
|
+
# * +after_branch_created+ is called on the newly created
|
62
|
+
# +Revisable+ instance.
|
63
|
+
def branch(*args, &block)
|
64
|
+
is_branching!
|
65
|
+
|
27
66
|
unless run_callbacks(:before_branch) { |r, o| r == false}
|
28
67
|
raise ActiveRecord::RecordNotSaved
|
29
68
|
end
|
@@ -32,30 +71,138 @@ module FatJam
|
|
32
71
|
options[:revisable_branched_from_id] = self.id
|
33
72
|
self.class.column_names.each do |col|
|
34
73
|
next unless self.class.revisable_should_clone_column? col
|
35
|
-
options[col.to_sym]
|
74
|
+
options[col.to_sym] = self[col] unless options.has_key?(col.to_sym)
|
36
75
|
end
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
76
|
+
|
77
|
+
br = self.class.revisable_class.new(options)
|
78
|
+
br.is_branching!
|
79
|
+
|
80
|
+
br.execute_after(:save) do
|
81
|
+
begin
|
82
|
+
run_callbacks(:after_branch)
|
83
|
+
br.run_callbacks(:after_branch_created)
|
84
|
+
ensure
|
85
|
+
br.is_branching!(false)
|
86
|
+
is_branching!(false)
|
87
|
+
end
|
41
88
|
end
|
89
|
+
|
90
|
+
block.call(br) if block_given?
|
91
|
+
|
92
|
+
br
|
42
93
|
end
|
43
|
-
|
94
|
+
|
95
|
+
# Same as #branch except it calls #save! on the new +Revisable+ instance.
|
96
|
+
def branch!(*args)
|
97
|
+
branch(*args) do |br|
|
98
|
+
br.save!
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Globally sets the reverting state of this record.
|
103
|
+
def is_branching!(value=true) #:nodoc:
|
104
|
+
set_revisable_state(:branching, value)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns true if the _record_ (not just this instance
|
108
|
+
# of the record) is currently being branched.
|
109
|
+
def is_branching?
|
110
|
+
get_revisable_state(:branching)
|
111
|
+
end
|
112
|
+
|
113
|
+
# When called on a +Revision+ it returns the original id. When
|
114
|
+
# called on a +Revisable+ it returns the id.
|
44
115
|
def original_id
|
45
116
|
self[:revisable_original_id] || self[:id]
|
46
117
|
end
|
47
118
|
|
48
|
-
|
49
|
-
|
119
|
+
# Globally sets the state for a given record. This is keyed
|
120
|
+
# on the primary_key of a saved record or the object_id
|
121
|
+
# on a new instance.
|
122
|
+
def set_revisable_state(type, value) #:nodoc:
|
123
|
+
key = self.read_attribute(self.class.primary_key)
|
124
|
+
key = object_id if key.nil?
|
125
|
+
revisable_current_states[type] ||= {}
|
126
|
+
revisable_current_states[type][key] = value
|
127
|
+
revisable_current_states[type].delete(key) unless value
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns the state of the given record.
|
131
|
+
def get_revisable_state(type) #:nodoc:
|
132
|
+
key = self.read_attribute(self.class.primary_key)
|
133
|
+
revisable_current_states[type] ||= {}
|
134
|
+
revisable_current_states[type][key] || revisable_current_states[type][object_id] || false
|
135
|
+
end
|
136
|
+
|
137
|
+
# Returns true if the instance is the first revision.
|
138
|
+
def first_revision?
|
139
|
+
self.revision_number == 1
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns true if the instance is the most recent revision.
|
143
|
+
def latest_revision?
|
144
|
+
self.revision_number == self.current_revision.revision_number
|
145
|
+
end
|
146
|
+
|
147
|
+
# Returns true if the instance is the current record and not a revision.
|
148
|
+
def current_revision?
|
149
|
+
self.is_a? self.class.revisable_class
|
150
|
+
end
|
151
|
+
|
152
|
+
# Accessor for revisable_number just to make external API more pleasant.
|
153
|
+
def revision_number
|
154
|
+
self[:revisable_number]
|
155
|
+
end
|
156
|
+
|
157
|
+
def diffs(what)
|
158
|
+
what = current_revision.find_revision(what)
|
159
|
+
returning({}) do |changes|
|
160
|
+
self.class.revisable_class.revisable_watch_columns.each do |c|
|
161
|
+
changes[c] = [self[c], what[c]] unless self[c] == what[c]
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
module ClassMethods
|
167
|
+
def disable_revisable_scope(*args)
|
168
|
+
args.each do |a|
|
169
|
+
class_eval <<-EOT
|
170
|
+
def #{a.to_s}_with_open_scope(*args, &block)
|
171
|
+
assoc = self.class.reflect_on_association(#{a.inspect})
|
172
|
+
models = [self.class]
|
173
|
+
|
174
|
+
if [:has_many, :has_one].member? assoc.macro
|
175
|
+
models << (assoc.options[:class_name] ? assoc.options[:class_name] : #{a.inspect}.to_s.singularize.camelize).constantize
|
176
|
+
end
|
177
|
+
|
178
|
+
begin
|
179
|
+
models.each {|m| m.scoped_model_enabled = false}
|
180
|
+
if associated = #{a.to_s}_without_open_scope(*args, &block)
|
181
|
+
associated.reload
|
182
|
+
end
|
183
|
+
ensure
|
184
|
+
models.each {|m| m.scoped_model_enabled = true}
|
185
|
+
end
|
186
|
+
end
|
187
|
+
EOT
|
188
|
+
alias_method_chain a, :open_scope
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Returns true if the revision should clone the given column.
|
193
|
+
def revisable_should_clone_column?(col) #:nodoc:
|
50
194
|
return false if (REVISABLE_SYSTEM_COLUMNS + REVISABLE_UNREVISABLE_COLUMNS).member? col
|
51
195
|
true
|
52
196
|
end
|
53
|
-
|
54
|
-
|
197
|
+
|
198
|
+
# acts_as_revisable's override for instantiate so we can
|
199
|
+
# return the appropriate type of model based on whether
|
200
|
+
# or not the record is the current record.
|
201
|
+
def instantiate_with_revisable(record) #:nodoc:
|
55
202
|
is_current = columns_hash["revisable_is_current"].type_cast(
|
56
203
|
record["revisable_is_current"])
|
57
204
|
|
58
|
-
if (is_current && self == self.revisable_class) || (is_current && self == self.revision_class)
|
205
|
+
if (is_current && self == self.revisable_class) || (!is_current && self == self.revision_class)
|
59
206
|
return instantiate_without_revisable(record)
|
60
207
|
end
|
61
208
|
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module FatJam
|
2
|
+
module ActsAsRevisable
|
3
|
+
module Deletable
|
4
|
+
def self.included(base)
|
5
|
+
base.instance_eval do
|
6
|
+
alias_method_chain :destroy, :revisable
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def destroy_with_revisable
|
11
|
+
now = Time.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
|
+
self.save(:without_revision => true)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|