rich-acts_as_revisable 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README +189 -0
- data/generators/revisable_migration/revisable_migration_generator.rb +9 -0
- data/generators/revisable_migration/templates/migration.rb +13 -0
- data/lib/acts_as_revisable/acts/common.rb +84 -0
- data/lib/acts_as_revisable/acts/revisable.rb +236 -0
- data/lib/acts_as_revisable/acts/revision.rb +78 -0
- data/lib/acts_as_revisable/acts/scoped_model.rb +51 -0
- data/lib/acts_as_revisable/base.rb +47 -0
- data/lib/acts_as_revisable/clone_associations.rb +29 -0
- data/lib/acts_as_revisable/options.rb +22 -0
- data/lib/acts_as_revisable/quoted_columns.rb +30 -0
- data/lib/acts_as_revisable/version.rb +11 -0
- data/lib/acts_as_revisable.rb +14 -0
- data/rails/init.rb +1 -0
- data/spec/aar_options_spec.rb +83 -0
- data/spec/acts_as_revisable_spec.rb +78 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +43 -0
- metadata +73 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Rich Cavanaugh
|
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
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
= acts_as_revisable
|
2
|
+
|
3
|
+
http://github.com/rich/acts_as_revisable/tree/master
|
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
|
+
* Numerous custom callbacks for both revisable and revision models.
|
17
|
+
* revisable models
|
18
|
+
* before_revise
|
19
|
+
* after_revise
|
20
|
+
* before_revert
|
21
|
+
* after_revert
|
22
|
+
* before_changeset
|
23
|
+
* after_changeset
|
24
|
+
* after_branch_created
|
25
|
+
* revision models
|
26
|
+
* before_restore
|
27
|
+
* after_restore
|
28
|
+
* both revisable and revision models
|
29
|
+
* before_branch
|
30
|
+
* after_branch
|
31
|
+
These work like any other ActiveRecord callbacks. The before_* callbacks can stop the the action. This uses the Callbacks module in ActiveSupport.
|
32
|
+
* Works with a single table.
|
33
|
+
* Provides migration generators to add the revisable columns.
|
34
|
+
* Grouping several revisable actions into a single revision (changeset).
|
35
|
+
* Monitor all or just specified columns to trigger a revision.
|
36
|
+
* Clone all or specified associations to the revision model.
|
37
|
+
* Uses ActiveRecord's dirty attribute tracking.
|
38
|
+
* Several ways to find revisions including:
|
39
|
+
* revision number
|
40
|
+
* relative keywords (:previous, :last)
|
41
|
+
* timestamp
|
42
|
+
* Reverting
|
43
|
+
* Branching
|
44
|
+
* Selectively disable revision tracking
|
45
|
+
* Naming revisions
|
46
|
+
|
47
|
+
== SYNOPSIS:
|
48
|
+
|
49
|
+
Given a simple model:
|
50
|
+
|
51
|
+
class Project < ActiveRecord::Base
|
52
|
+
# columns: id, name, unimportant, created_at
|
53
|
+
end
|
54
|
+
|
55
|
+
Let's make the projects table revisable:
|
56
|
+
|
57
|
+
ruby script/generate revisable_migration Project
|
58
|
+
rake db:migrate
|
59
|
+
|
60
|
+
Now Project itself:
|
61
|
+
|
62
|
+
class Project < ActiveRecord::Base
|
63
|
+
has_one :owner
|
64
|
+
|
65
|
+
acts_as_revisable do
|
66
|
+
revision_class_name "Session"
|
67
|
+
except :unimportant
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
Create the revision class:
|
72
|
+
|
73
|
+
class Session < ActiveRecord::Base
|
74
|
+
# we can accept the more standard hash syntax
|
75
|
+
acts_as_revision :revisable_class_name => "Project", :clone_associations => :all
|
76
|
+
end
|
77
|
+
|
78
|
+
Some example usage:
|
79
|
+
|
80
|
+
@project = Project.create(:name => "Rich", :unimportant => "some text")
|
81
|
+
@project.revision_number # => 0
|
82
|
+
|
83
|
+
@project.update_attribute(:unimportant, "more text")
|
84
|
+
@project.revision_number # => 0
|
85
|
+
|
86
|
+
@project.name = "Stephen"
|
87
|
+
@project.save(:without_revision => true)
|
88
|
+
@project.name # => Stephen
|
89
|
+
@project.revision_number # => 0
|
90
|
+
|
91
|
+
@project.name = "Sam"
|
92
|
+
@project.save(:revision_name => "Changed name")
|
93
|
+
@project.revision_number # => 1
|
94
|
+
|
95
|
+
Navigating revisions:
|
96
|
+
|
97
|
+
@previous = @project.find_revision(:previous)
|
98
|
+
# or
|
99
|
+
@previous = @project.revisions.first
|
100
|
+
|
101
|
+
@previous.name # => Rich
|
102
|
+
@previous.current_revision.name # => Sam
|
103
|
+
@previous.project.name # => Sam
|
104
|
+
@previous.revision_name # => Changed name
|
105
|
+
|
106
|
+
Reverting:
|
107
|
+
|
108
|
+
@project.revert_to!(:previous)
|
109
|
+
@project.revision_number # => 2
|
110
|
+
@project.name # => Rich
|
111
|
+
|
112
|
+
@project.revert_to!(1, :without_revision => true)
|
113
|
+
@project.revision_number # => 2
|
114
|
+
@project.name # => Sam
|
115
|
+
|
116
|
+
Branching
|
117
|
+
|
118
|
+
@branch = @project.branch(:name => "Bruno")
|
119
|
+
@branch.revision_number # => 0
|
120
|
+
@branch.branch_source.name # => Sam
|
121
|
+
|
122
|
+
Associations have been cloned:
|
123
|
+
|
124
|
+
@project.owner === @previous.owner # => true
|
125
|
+
|
126
|
+
Maybe we don't want to be able to branch from revisions:
|
127
|
+
|
128
|
+
class Session < ActiveRecord::Base
|
129
|
+
# assuming we still have the other code from Session above
|
130
|
+
|
131
|
+
before_branch do
|
132
|
+
false
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
@project.revisions.first.branch # Raises an exception
|
137
|
+
@project.branch # works as expected
|
138
|
+
|
139
|
+
If the owner isn't set let's prevent reverting:
|
140
|
+
|
141
|
+
class Project < ActiveRecord::Base
|
142
|
+
# assuming we still have the other code from Project above
|
143
|
+
|
144
|
+
before_revert :check_owner_befor_reverting
|
145
|
+
def check_owner_befor_reverting
|
146
|
+
false unless self.owner?
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
== REQUIREMENTS:
|
151
|
+
|
152
|
+
This plugin currently depends on Edge Rails, ActiveRecord and ActiveSupport specifically, which will eventually become Rails 2.1.
|
153
|
+
|
154
|
+
== INSTALL:
|
155
|
+
|
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:
|
163
|
+
|
164
|
+
sudo gem install rich-acts_as_revisable
|
165
|
+
|
166
|
+
== LICENSE:
|
167
|
+
|
168
|
+
(The MIT License)
|
169
|
+
|
170
|
+
Copyright (c) 2008
|
171
|
+
|
172
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
173
|
+
a copy of this software and associated documentation files (the
|
174
|
+
'Software'), to deal in the Software without restriction, including
|
175
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
176
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
177
|
+
permit persons to whom the Software is furnished to do so, subject to
|
178
|
+
the following conditions:
|
179
|
+
|
180
|
+
The above copyright notice and this permission notice shall be
|
181
|
+
included in all copies or substantial portions of the Software.
|
182
|
+
|
183
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
184
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
185
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
186
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
187
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
188
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
189
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -0,0 +1,9 @@
|
|
1
|
+
class RevisableMigrationGenerator < Rails::Generator::NamedBase
|
2
|
+
def manifest
|
3
|
+
record do |m|
|
4
|
+
revisable_columns = [["revisable_original_id", "integer"], ["revisable_branched_from_id", "integer"], ["revisable_number", "integer"], ["revisable_name", "string"], ["revisable_type", "string"], ["revisable_current_at", "datetime"], ["revisable_revised_at", "datetime"], ["revisable_deleted_at", "datetime"], ["revisable_is_current", "boolean"]]
|
5
|
+
|
6
|
+
m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => "make_#{class_name.downcase}_revisable", :assigns => {:cols => revisable_columns}
|
7
|
+
end
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class Make<%= class_name.underscore.camelize %>Revisable < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
<% cols.each do |c| -%>
|
4
|
+
add_column :<%= class_name.downcase.pluralize %>, :<%= c.first %>, :<%= c.last %>
|
5
|
+
<% end -%>
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.down
|
9
|
+
<% cols.each do |c| -%>
|
10
|
+
remove_column :<%= class_name.downcase.pluralize %>, :<%= c.first %>
|
11
|
+
<% end -%>
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module FatJam
|
2
|
+
module ActsAsRevisable
|
3
|
+
module Common
|
4
|
+
def self.included(base)
|
5
|
+
base.send(:extend, ClassMethods)
|
6
|
+
|
7
|
+
class << base
|
8
|
+
alias_method_chain :instantiate, :revisable
|
9
|
+
end
|
10
|
+
|
11
|
+
base.instance_eval do
|
12
|
+
define_callbacks :before_branch, :after_branch
|
13
|
+
has_many :branches, :class_name => base.class_name, :foreign_key => :revisable_branched_from_id
|
14
|
+
belongs_to :branch_source, :class_name => base.class_name, :foreign_key => :revisable_branched_from_id
|
15
|
+
|
16
|
+
end
|
17
|
+
base.alias_method_chain :branch_source, :open_scope
|
18
|
+
end
|
19
|
+
|
20
|
+
def branch_source_with_open_scope(*args, &block)
|
21
|
+
self.class.without_model_scope do
|
22
|
+
branch_source_without_open_scope(*args, &block)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def branch(*args)
|
27
|
+
unless run_callbacks(:before_branch) { |r, o| r == false}
|
28
|
+
raise ActiveRecord::RecordNotSaved
|
29
|
+
end
|
30
|
+
|
31
|
+
options = args.extract_options!
|
32
|
+
options[:revisable_branched_from_id] = self.id
|
33
|
+
self.class.column_names.each do |col|
|
34
|
+
next unless self.class.revisable_should_clone_column? col
|
35
|
+
options[col.to_sym] ||= self[col]
|
36
|
+
end
|
37
|
+
|
38
|
+
returning(self.class.revisable_class.create!(options)) do |br|
|
39
|
+
run_callbacks(:after_branch)
|
40
|
+
br.run_callbacks(:after_branch_created)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def original_id
|
45
|
+
self[:revisable_original_id] || self[:id]
|
46
|
+
end
|
47
|
+
|
48
|
+
module ClassMethods
|
49
|
+
def revisable_should_clone_column?(col)
|
50
|
+
return false if (REVISABLE_SYSTEM_COLUMNS + REVISABLE_UNREVISABLE_COLUMNS).member? col
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
def instantiate_with_revisable(record)
|
55
|
+
is_current = columns_hash["revisable_is_current"].type_cast(
|
56
|
+
record["revisable_is_current"])
|
57
|
+
|
58
|
+
if (is_current && self == self.revisable_class) || (is_current && self == self.revision_class)
|
59
|
+
return instantiate_without_revisable(record)
|
60
|
+
end
|
61
|
+
|
62
|
+
object = if is_current
|
63
|
+
self.revisable_class
|
64
|
+
else
|
65
|
+
self.revision_class
|
66
|
+
end.allocate
|
67
|
+
|
68
|
+
object.instance_variable_set("@attributes", record)
|
69
|
+
object.instance_variable_set("@attributes_cache", Hash.new)
|
70
|
+
|
71
|
+
if object.respond_to_without_attributes?(:after_find)
|
72
|
+
object.send(:callback, :after_find)
|
73
|
+
end
|
74
|
+
|
75
|
+
if object.respond_to_without_attributes?(:after_initialize)
|
76
|
+
object.send(:callback, :after_initialize)
|
77
|
+
end
|
78
|
+
|
79
|
+
object
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
module FatJam
|
2
|
+
module ActsAsRevisable
|
3
|
+
module Revisable
|
4
|
+
def self.included(base)
|
5
|
+
base.send(:extend, ClassMethods)
|
6
|
+
|
7
|
+
base.class_inheritable_hash :aa_revisable_current_revisions
|
8
|
+
base.aa_revisable_current_revisions = {}
|
9
|
+
|
10
|
+
class << base
|
11
|
+
alias_method_chain :find, :revisable
|
12
|
+
alias_method_chain :with_scope, :revisable
|
13
|
+
end
|
14
|
+
|
15
|
+
base.instance_eval do
|
16
|
+
define_callbacks :before_revise, :after_revise, :before_revert, :after_revert, :before_changeset, :after_changeset, :after_branch_created
|
17
|
+
|
18
|
+
alias_method_chain :save, :revisable
|
19
|
+
alias_method_chain :save!, :revisable
|
20
|
+
|
21
|
+
acts_as_scoped_model :find => {:conditions => {:revisable_is_current => true}}
|
22
|
+
|
23
|
+
has_many :revisions, :class_name => revision_class_name, :foreign_key => :revisable_original_id, :order => "revisable_number DESC", :dependent => :destroy
|
24
|
+
has_many revision_class_name.pluralize.downcase.to_sym, :class_name => revision_class_name, :foreign_key => :revisable_original_id, :order => "revisable_number DESC", :dependent => :destroy
|
25
|
+
|
26
|
+
before_create :before_revisable_create
|
27
|
+
before_update :before_revisable_update
|
28
|
+
after_update :after_revisable_update
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def before_revisable_create
|
33
|
+
self[:revisable_is_current] = true
|
34
|
+
end
|
35
|
+
|
36
|
+
def before_revisable_update
|
37
|
+
return unless @aa_revisable_force_revision == true || (self.changed? && !(@aa_revisable_no_revision === true) && !(self.changed.map(&:downcase) & self.class.revisable_columns).blank?)
|
38
|
+
|
39
|
+
return false unless run_callbacks(:before_revise) { |r, o| r == false}
|
40
|
+
|
41
|
+
@revisable_revision = self.to_revision
|
42
|
+
end
|
43
|
+
|
44
|
+
def after_revisable_update
|
45
|
+
if @revisable_revision
|
46
|
+
@revisable_revision.save
|
47
|
+
@aa_revisable_was_revised = true
|
48
|
+
revisions.reload
|
49
|
+
run_callbacks(:after_revise)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_revision
|
54
|
+
rev = self.class.revision_class.new(@aa_revisable_new_params)
|
55
|
+
|
56
|
+
rev.revisable_original_id = self.id
|
57
|
+
|
58
|
+
self.class.column_names.each do |col|
|
59
|
+
next unless self.class.revisable_should_clone_column? col
|
60
|
+
val = self.send("#{col}_changed?") ? self.send("#{col}_was") : self.send(col)
|
61
|
+
rev.send("#{col}=", val)
|
62
|
+
end
|
63
|
+
|
64
|
+
@aa_revisable_new_params = nil
|
65
|
+
|
66
|
+
rev
|
67
|
+
end
|
68
|
+
|
69
|
+
def save_with_revisable!(*args)
|
70
|
+
@aa_revisable_new_params ||= args.extract_options!
|
71
|
+
@aa_revisable_no_revision = true if @aa_revisable_new_params.delete :without_revision
|
72
|
+
save_without_revisable!(*args)
|
73
|
+
end
|
74
|
+
|
75
|
+
def save_with_revisable(*args)
|
76
|
+
@aa_revisable_new_params ||= args.extract_options!
|
77
|
+
@aa_revisable_no_revision = true if @aa_revisable_new_params.delete :without_revision
|
78
|
+
save_without_revisable(*args)
|
79
|
+
end
|
80
|
+
|
81
|
+
def find_revision(number)
|
82
|
+
revisions.find_by_revisable_number(number)
|
83
|
+
end
|
84
|
+
|
85
|
+
def revert_to!(*args)
|
86
|
+
unless run_callbacks(:before_revert) { |r, o| r == false}
|
87
|
+
raise ActiveRecord::RecordNotSaved
|
88
|
+
end
|
89
|
+
|
90
|
+
options = args.extract_options!
|
91
|
+
|
92
|
+
rev = case args.first
|
93
|
+
when self.class.revision_class
|
94
|
+
args.first
|
95
|
+
when :first
|
96
|
+
revisions.last
|
97
|
+
when :previous
|
98
|
+
revisions.first
|
99
|
+
when Fixnum
|
100
|
+
revisions.find_by_revisable_number(args.first)
|
101
|
+
when Time
|
102
|
+
revisions.find(:first, :conditions => ["? >= ? and ? <= ?", :revisable_revised_at, args.first, :revisable_current_at, args.first])
|
103
|
+
end
|
104
|
+
|
105
|
+
unless rev.run_callbacks(:before_restore) { |r, o| r == false}
|
106
|
+
raise ActiveRecord::RecordNotSaved
|
107
|
+
end
|
108
|
+
|
109
|
+
self.class.column_names.each do |col|
|
110
|
+
next unless self.class.revisable_should_clone_column? col
|
111
|
+
self[col] = rev[col]
|
112
|
+
end
|
113
|
+
|
114
|
+
@aa_revisable_no_revision = true if options.delete :without_revision
|
115
|
+
@aa_revisable_new_params = options
|
116
|
+
|
117
|
+
returning(@aa_revisable_no_revision ? save! : revise!) do
|
118
|
+
rev.run_callbacks(:after_restore)
|
119
|
+
run_callbacks(:after_revert)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def revert_to_without_revision!(*args)
|
124
|
+
options = args.extract_options!
|
125
|
+
options.update({:without_revision => true})
|
126
|
+
revert_to!(*(args << options))
|
127
|
+
end
|
128
|
+
|
129
|
+
def revise!
|
130
|
+
return if in_revision?
|
131
|
+
|
132
|
+
@aa_revisable_force_revision = true
|
133
|
+
in_revision!
|
134
|
+
returning(save!) do
|
135
|
+
in_revision!(false)
|
136
|
+
@aa_revisable_force_revision = false
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def revised?
|
141
|
+
@aa_revisable_was_revised || false
|
142
|
+
end
|
143
|
+
|
144
|
+
def in_revision?
|
145
|
+
key = self.read_attribute(self.class.primary_key)
|
146
|
+
aa_revisable_current_revisions[key] || false
|
147
|
+
end
|
148
|
+
|
149
|
+
def in_revision!(val=true)
|
150
|
+
key = self.read_attribute(self.class.primary_key)
|
151
|
+
aa_revisable_current_revisions[key] = val
|
152
|
+
aa_revisable_current_revisions.delete(key) unless val
|
153
|
+
end
|
154
|
+
|
155
|
+
def changeset(&block)
|
156
|
+
return unless block_given?
|
157
|
+
|
158
|
+
return yield(self) if in_revision?
|
159
|
+
|
160
|
+
unless run_callbacks(:before_changeset) { |r, o| r == false}
|
161
|
+
raise ActiveRecord::RecordNotSaved
|
162
|
+
end
|
163
|
+
|
164
|
+
in_revision!
|
165
|
+
|
166
|
+
returning(yield(self)) do
|
167
|
+
in_revision!(false)
|
168
|
+
run_callbacks(:after_changeset)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def revision_number
|
173
|
+
revisions.first.revisable_number
|
174
|
+
rescue NoMethodError
|
175
|
+
0
|
176
|
+
end
|
177
|
+
|
178
|
+
module ClassMethods
|
179
|
+
def with_scope_with_revisable(*args, &block)
|
180
|
+
options = args.extract_options![:find]
|
181
|
+
|
182
|
+
if options && options.delete(:with_revisions)
|
183
|
+
without_model_scope do
|
184
|
+
with_scope_without_revisable(*args, &block)
|
185
|
+
end
|
186
|
+
else
|
187
|
+
with_scope_without_revisable(*args, &block)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def find_with_revisable(*args)
|
192
|
+
options = args.extract_options!
|
193
|
+
|
194
|
+
if options && options.delete(:with_revisions)
|
195
|
+
without_model_scope do
|
196
|
+
find_without_revisable(*args)
|
197
|
+
end
|
198
|
+
else
|
199
|
+
find_without_revisable(*args)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def find_with_revisions(*args)
|
204
|
+
options = args.extract_options!
|
205
|
+
options.update({:with_revisions => true})
|
206
|
+
find_with_revisable(*(args << options))
|
207
|
+
end
|
208
|
+
|
209
|
+
def revision_class_name
|
210
|
+
self.revisable_options.revision_class_name || "#{self.class_name}Revision"
|
211
|
+
end
|
212
|
+
|
213
|
+
def revision_class
|
214
|
+
@aa_revision_class ||= revision_class_name.constantize
|
215
|
+
end
|
216
|
+
|
217
|
+
def revisable_class
|
218
|
+
self
|
219
|
+
end
|
220
|
+
|
221
|
+
def revisable_columns
|
222
|
+
return @aa_revisable_columns unless @aa_revisable_columns.blank?
|
223
|
+
return @aa_revisable_columns ||= [] if self.revisable_options.except == :all
|
224
|
+
return @aa_revisable_columns ||= [self.revisable_options.only].flatten.map(&:to_s).map(&:downcase) unless self.revisable_options.only.blank?
|
225
|
+
|
226
|
+
except = [self.revisable_options.except].flatten || []
|
227
|
+
except += REVISABLE_SYSTEM_COLUMNS
|
228
|
+
except += REVISABLE_UNREVISABLE_COLUMNS
|
229
|
+
except.uniq!
|
230
|
+
|
231
|
+
@aa_revisable_columns ||= (column_names - except.map(&:to_s)).flatten.map(&:downcase)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'acts_as_revisable/clone_associations'
|
2
|
+
|
3
|
+
module FatJam
|
4
|
+
module ActsAsRevisable
|
5
|
+
module Revision
|
6
|
+
def self.included(base)
|
7
|
+
base.send(:extend, ClassMethods)
|
8
|
+
|
9
|
+
base.instance_eval do
|
10
|
+
set_table_name(revisable_class.table_name)
|
11
|
+
acts_as_scoped_model :find => {:conditions => {:revisable_is_current => false}}
|
12
|
+
|
13
|
+
CloneAssociations.clone(revisable_class, self)
|
14
|
+
|
15
|
+
define_callbacks :before_restore, :after_restore
|
16
|
+
|
17
|
+
belongs_to :current_revision, :class_name => revisable_class_name, :foreign_key => :revisable_original_id
|
18
|
+
belongs_to revisable_class_name.downcase.to_sym, :class_name => revisable_class_name, :foreign_key => :revisable_original_id
|
19
|
+
|
20
|
+
before_create :revision_setup
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def revision_name=(val)
|
25
|
+
self[:revisable_name] = val
|
26
|
+
end
|
27
|
+
|
28
|
+
def revision_name
|
29
|
+
self[:revisable_name]
|
30
|
+
end
|
31
|
+
|
32
|
+
def revision_number
|
33
|
+
self[:revisable_number]
|
34
|
+
end
|
35
|
+
|
36
|
+
def revision_setup
|
37
|
+
now = Time.now
|
38
|
+
prev = current_revision.revisions.first
|
39
|
+
prev.update_attribute(:revisable_revised_at, now) if prev
|
40
|
+
self[:revisable_current_at] = now + 1.second
|
41
|
+
self[:revisable_is_current] = false
|
42
|
+
self[:revisable_branched_from_id] = current_revision[:revisable_branched_from_id]
|
43
|
+
self[:revisable_type] = current_revision[:type]
|
44
|
+
self[:revisable_number] = (self.class.maximum(:revisable_number, :conditions => {:revisable_original_id => self[:revisable_original_id]}) || 0) + 1
|
45
|
+
end
|
46
|
+
|
47
|
+
module ClassMethods
|
48
|
+
def revisable_class_name
|
49
|
+
self.revisable_options.revisable_class_name || self.class_name.gsub(/Revision/, '')
|
50
|
+
end
|
51
|
+
|
52
|
+
def revisable_class
|
53
|
+
@revisable_class ||= revisable_class_name.constantize
|
54
|
+
end
|
55
|
+
|
56
|
+
def revision_class
|
57
|
+
self
|
58
|
+
end
|
59
|
+
|
60
|
+
def revision_cloned_associations
|
61
|
+
clone_associations = self.revisable_options.clone_associations
|
62
|
+
|
63
|
+
@aa_revisable_cloned_associations ||= if clone_associations.blank?
|
64
|
+
[]
|
65
|
+
elsif clone_associations.eql? :all
|
66
|
+
revisable_class.reflect_on_all_associations.map(&:name)
|
67
|
+
elsif clone_associations.is_a? [].class
|
68
|
+
clone_associations
|
69
|
+
elsif clone_associations[:only]
|
70
|
+
[clone_associations[:only]].flatten
|
71
|
+
elsif clone_associations[:except]
|
72
|
+
revisable_class.reflect_on_all_associations.map(&:name) - [clone_associations[:except]].flatten
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module FatJam
|
2
|
+
module ActsAsScopedModel
|
3
|
+
def self.included(base)
|
4
|
+
base.send(:extend, ClassMethods)
|
5
|
+
end
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
SCOPED_METHODS = %w(construct_calculation_sql construct_finder_sql update_all delete_all destroy_all).freeze
|
9
|
+
|
10
|
+
def call_method_with_static_scope(meth, args)
|
11
|
+
return send(meth, *args) unless self.scoped_model_enabled
|
12
|
+
|
13
|
+
with_scope(self.scoped_model_static_scope) do
|
14
|
+
send(meth, *args)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
SCOPED_METHODS.each do |m|
|
19
|
+
module_eval <<-EVAL
|
20
|
+
def #{m}_with_static_scope(*args)
|
21
|
+
call_method_with_static_scope(:#{m}_without_static_scope, args)
|
22
|
+
end
|
23
|
+
EVAL
|
24
|
+
end
|
25
|
+
|
26
|
+
def without_model_scope
|
27
|
+
return unless block_given?
|
28
|
+
|
29
|
+
begin
|
30
|
+
self.scoped_model_enabled = false
|
31
|
+
rv = yield
|
32
|
+
ensure
|
33
|
+
self.scoped_model_enabled = true
|
34
|
+
end
|
35
|
+
|
36
|
+
rv
|
37
|
+
end
|
38
|
+
|
39
|
+
def acts_as_scoped_model(*args)
|
40
|
+
class << self
|
41
|
+
attr_accessor :scoped_model_static_scope, :scoped_model_enabled
|
42
|
+
SCOPED_METHODS.each do |m|
|
43
|
+
alias_method_chain m.to_sym, :static_scope
|
44
|
+
end
|
45
|
+
end
|
46
|
+
self.scoped_model_enabled = true
|
47
|
+
self.scoped_model_static_scope = args.extract_options!
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'acts_as_revisable/options'
|
2
|
+
require 'acts_as_revisable/acts/common'
|
3
|
+
require 'acts_as_revisable/acts/revision'
|
4
|
+
require 'acts_as_revisable/acts/revisable'
|
5
|
+
|
6
|
+
module FatJam
|
7
|
+
# define the columns used internall by AAR
|
8
|
+
REVISABLE_SYSTEM_COLUMNS = %w(revisable_original_id revisable_branched_from_id revisable_number revisable_name revisable_type revisable_current_at revisable_revised_at revisable_deleted_at revisable_is_current)
|
9
|
+
|
10
|
+
# define the ActiveRecord magic columns that should not be monitored
|
11
|
+
REVISABLE_UNREVISABLE_COLUMNS = %w(id type created_at updated_at)
|
12
|
+
|
13
|
+
module ActsAsRevisable
|
14
|
+
def self.included(base)
|
15
|
+
base.send(:extend, ClassMethods)
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
|
20
|
+
# This +acts_as+ extension provides for making a model the
|
21
|
+
# revisable model in an acts_as_revisable pair.
|
22
|
+
def acts_as_revisable(*args, &block)
|
23
|
+
revisable_shared_setup(args, block)
|
24
|
+
self.send(:include, Revisable)
|
25
|
+
end
|
26
|
+
|
27
|
+
# This +acts_as+ extension provides for making a model the
|
28
|
+
# revision model in an acts_as_revisable pair.
|
29
|
+
def acts_as_revision(*args, &block)
|
30
|
+
revisable_shared_setup(args, block)
|
31
|
+
self.send(:include, Revision)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
# Performs the setup needed for both kinds of acts_as_revisable
|
36
|
+
# models.
|
37
|
+
def revisable_shared_setup(args, block)
|
38
|
+
self.send(:include, Common)
|
39
|
+
class << self
|
40
|
+
attr_accessor :revisable_options
|
41
|
+
end
|
42
|
+
options = args.extract_options!
|
43
|
+
@revisable_options = Options.new(options, &block)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module FatJam
|
2
|
+
module ActsAsRevisable
|
3
|
+
module CloneAssociations
|
4
|
+
class << self
|
5
|
+
def clone(from, to)
|
6
|
+
return unless from.is_a?(ActiveRecord::Base) && to.is_a?(ActiveRecord::Base)
|
7
|
+
|
8
|
+
from.revision_cloned_associations.each do |key|
|
9
|
+
assoc = from.reflect_on_association(key)
|
10
|
+
meth = "clone_#{assoc.macro.to_s}_association"
|
11
|
+
meth = "clone_association" unless respond_to? meth
|
12
|
+
send(meth, assoc, to)
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def clone_association(association, to)
|
18
|
+
options = association.options.clone
|
19
|
+
options[:foreign_key] ||= "revisable_original_id"
|
20
|
+
to.send(association.macro, association.name, options)
|
21
|
+
end
|
22
|
+
|
23
|
+
def clone_belongs_to_association(association, to)
|
24
|
+
to.send(association.macro, association.name, association.options.clone)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module FatJam
|
2
|
+
module ActsAsRevisable
|
3
|
+
# This class provides for a flexible method of setting
|
4
|
+
# options and querying them. This is especially useful
|
5
|
+
# for giving users flexibility when using your plugin.
|
6
|
+
class Options
|
7
|
+
def initialize(*options, &block)
|
8
|
+
@options = options.extract_options!
|
9
|
+
instance_eval(&block) if block_given?
|
10
|
+
end
|
11
|
+
|
12
|
+
def method_missing(key, *args)
|
13
|
+
return (@options[key.to_s.gsub(/\?$/, '').to_sym].eql?(true)) if key.to_s.match(/\?$/)
|
14
|
+
if args.blank?
|
15
|
+
@options[key.to_sym]
|
16
|
+
else
|
17
|
+
@options[key.to_sym] = args.size == 1 ? args.first : args
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module FatJam::QuotedColumnConditions
|
2
|
+
def self.included(base)
|
3
|
+
base.send(:extend, ClassMethods)
|
4
|
+
|
5
|
+
class << base
|
6
|
+
alias_method_chain :quote_bound_value, :quoted_column
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def quote_bound_value_with_quoted_column(value)
|
12
|
+
if value.is_a?(Symbol) && column_names.member?(value.to_s)
|
13
|
+
# code borrowed from sanitize_sql_hash_for_conditions
|
14
|
+
attr = value.to_s
|
15
|
+
|
16
|
+
# Extract table name from qualified attribute names.
|
17
|
+
if attr.include?('.')
|
18
|
+
table_name, attr = attr.split('.', 2)
|
19
|
+
table_name = connection.quote_table_name(table_name)
|
20
|
+
else
|
21
|
+
table_name = quoted_table_name
|
22
|
+
end
|
23
|
+
|
24
|
+
return "#{table_name}.#{connection.quote_column_name(attr)}"
|
25
|
+
end
|
26
|
+
|
27
|
+
quote_bound_value_without_quoted_column(value)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
require 'activesupport' unless defined? ActiveSupport
|
5
|
+
require 'activerecord' unless defined? ActiveRecord
|
6
|
+
|
7
|
+
require 'acts_as_revisable/version.rb'
|
8
|
+
require 'acts_as_revisable/acts/scoped_model'
|
9
|
+
require 'acts_as_revisable/quoted_columns'
|
10
|
+
require 'acts_as_revisable/base'
|
11
|
+
|
12
|
+
ActiveRecord::Base.send(:include, FatJam::ActsAsScopedModel)
|
13
|
+
ActiveRecord::Base.send(:include, FatJam::QuotedColumnConditions)
|
14
|
+
ActiveRecord::Base.send(:include, FatJam::ActsAsRevisable)
|
data/rails/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'acts_as_revisable'
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
shared_examples_for "common Options usage" do
|
4
|
+
it "should return a set value" do
|
5
|
+
@options.one.should == 1
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should return nil for an unset value" do
|
9
|
+
@options.two.should be_nil
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should return false for unset query option" do
|
13
|
+
@options.should_not be_unset_value
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should return true for a query option set to true" do
|
17
|
+
@options.should be_yes
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should return false for a query option set to false" do
|
21
|
+
@options.should_not be_no
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should return false for a query on a non-boolean value" do
|
25
|
+
@options.should_not be_one
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should return an array when passed one" do
|
29
|
+
@options.arr.should be_a_kind_of(Array)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should not return an array when not passed one" do
|
33
|
+
@options.one.should_not be_a_kind_of(Array)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should have the right number of elements in an array" do
|
37
|
+
@options.arr.size.should == 3
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe FatJam::ActsAsRevisable::Options do
|
42
|
+
describe "with hash options" do
|
43
|
+
before(:each) do
|
44
|
+
@options = FatJam::ActsAsRevisable::Options.new :one => 1, :yes => true, :no => false, :arr => [1,2,3]
|
45
|
+
end
|
46
|
+
|
47
|
+
it_should_behave_like "common Options usage"
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "with block options" do
|
51
|
+
before(:each) do
|
52
|
+
@options = FatJam::ActsAsRevisable::Options.new do
|
53
|
+
one 1
|
54
|
+
yes true
|
55
|
+
arr [1,2,3]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
it_should_behave_like "common Options usage"
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "with both block and hash options" do
|
63
|
+
before(:each) do
|
64
|
+
@options = FatJam::ActsAsRevisable::Options.new(:yes => true, :arr => [1,2,3]) do
|
65
|
+
one 1
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
it_should_behave_like "common Options usage"
|
70
|
+
|
71
|
+
describe "the block should override the hash" do
|
72
|
+
before(:each) do
|
73
|
+
@options = FatJam::ActsAsRevisable::Options.new(:yes => false, :one => 10, :arr => [1,2,3,4,5]) do
|
74
|
+
one 1
|
75
|
+
yes true
|
76
|
+
arr [1,2,3]
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
it_should_behave_like "common Options usage"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper.rb'
|
2
|
+
|
3
|
+
class Project < ActiveRecord::Base
|
4
|
+
acts_as_revisable do
|
5
|
+
revision_class_name "Session"
|
6
|
+
except :unimportant
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Session < ActiveRecord::Base
|
11
|
+
acts_as_revision do
|
12
|
+
revisable_class_name "Project"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe FatJam::ActsAsRevisable do
|
17
|
+
before(:all) do
|
18
|
+
setup_db
|
19
|
+
end
|
20
|
+
|
21
|
+
after(:each) do
|
22
|
+
cleanup_db
|
23
|
+
end
|
24
|
+
|
25
|
+
after(:all) do
|
26
|
+
teardown_db
|
27
|
+
end
|
28
|
+
|
29
|
+
before(:each) do
|
30
|
+
@project = Project.create(:name => "Rich", :notes => "this plugin's author")
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "without revisions" do
|
34
|
+
it "should have a revision_number of zero" do
|
35
|
+
@project.revision_number.should == 0
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should have no revisions" do
|
39
|
+
@project.revisions.should be_empty
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "with revisions" do
|
44
|
+
before(:each) do
|
45
|
+
@project.update_attribute(:name, "Stephen")
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should have a revision_number of one" do
|
49
|
+
@project.revision_number.should == 1
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should have a single revision" do
|
53
|
+
@project.revisions.size.should == 1
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should return an instance of the revision class" do
|
57
|
+
@project.revisions.first.should be_an_instance_of(Session)
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should have the original revision's data" do
|
61
|
+
@project.revisions.first.name.should == "Rich"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "with excluded columns modified" do
|
66
|
+
before(:each) do
|
67
|
+
@project.update_attribute(:unimportant, "a new value")
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should maintain the revision_number at zero" do
|
71
|
+
@project.revision_number.should be_zero
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should not have any revisions" do
|
75
|
+
@project.revisions.should be_empty
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
begin
|
2
|
+
require 'spec'
|
3
|
+
rescue LoadError
|
4
|
+
require 'rubygems'
|
5
|
+
gem 'rspec'
|
6
|
+
require 'spec'
|
7
|
+
end
|
8
|
+
|
9
|
+
if ENV['EDGE_RAILS_PATH']
|
10
|
+
edge_path = File.expand_path(ENV['EDGE_RAILS_PATH'])
|
11
|
+
require File.join(edge_path, 'activesupport', 'lib', 'active_support')
|
12
|
+
require File.join(edge_path, 'activerecord', 'lib', 'active_record')
|
13
|
+
end
|
14
|
+
|
15
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib')
|
16
|
+
require 'acts_as_revisable'
|
17
|
+
|
18
|
+
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
|
19
|
+
|
20
|
+
def setup_db
|
21
|
+
ActiveRecord::Schema.define(:version => 1) do
|
22
|
+
create_table :projects do |t|
|
23
|
+
t.string :name, :unimportant, :revisable_name, :revisable_type
|
24
|
+
t.text :notes
|
25
|
+
t.boolean :revisable_is_current
|
26
|
+
t.integer :revisable_original_id, :revisable_branched_from_id, :revisable_number
|
27
|
+
t.datetime :revisable_current_at, :revisable_revised_at, :revisable_deleted_at
|
28
|
+
t.timestamps
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def teardown_db
|
34
|
+
ActiveRecord::Base.connection.tables.each do |table|
|
35
|
+
ActiveRecord::Base.connection.drop_table(table)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def cleanup_db
|
40
|
+
ActiveRecord::Base.connection.tables.each do |table|
|
41
|
+
ActiveRecord::Base.connection.execute("delete from #{table}")
|
42
|
+
end
|
43
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rich-acts_as_revisable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rich Cavanaugh of FatJam, LLC.
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-05-03 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: cavanaugh@fatjam.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README
|
24
|
+
- LICENSE
|
25
|
+
files:
|
26
|
+
- LICENSE
|
27
|
+
- README
|
28
|
+
- generators/revisable_migration/revisable_migration_generator.rb
|
29
|
+
- generators/revisable_migration/templates/migration.rb
|
30
|
+
- lib/acts_as_revisable.rb
|
31
|
+
- lib/acts_as_revisable/acts/common.rb
|
32
|
+
- lib/acts_as_revisable/acts/revisable.rb
|
33
|
+
- lib/acts_as_revisable/acts/revision.rb
|
34
|
+
- lib/acts_as_revisable/acts/scoped_model.rb
|
35
|
+
- lib/acts_as_revisable/base.rb
|
36
|
+
- lib/acts_as_revisable/options.rb
|
37
|
+
- lib/acts_as_revisable/quoted_columns.rb
|
38
|
+
- lib/acts_as_revisable/version.rb
|
39
|
+
- lib/acts_as_revisable/clone_associations.rb
|
40
|
+
- rails/init.rb
|
41
|
+
- spec/acts_as_revisable_spec.rb
|
42
|
+
- spec/spec.opts
|
43
|
+
- spec/spec_helper.rb
|
44
|
+
- spec/aar_options_spec.rb
|
45
|
+
has_rdoc: true
|
46
|
+
homepage: http://github.com/rich/acts_as_revisable/tree/master
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options:
|
49
|
+
- --main
|
50
|
+
- README
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
version:
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
requirements:
|
61
|
+
- - ">="
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: "0"
|
64
|
+
version:
|
65
|
+
requirements: []
|
66
|
+
|
67
|
+
rubyforge_project:
|
68
|
+
rubygems_version: 1.0.1
|
69
|
+
signing_key:
|
70
|
+
specification_version: 2
|
71
|
+
summary: acts_as_revisable enables revision tracking, querying, reverting and branching of ActiveRecord models. Inspired by acts_as_versioned.
|
72
|
+
test_files: []
|
73
|
+
|