rich-acts_as_revisable 0.6.0 → 0.9.8
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 +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
|