rich-rich-acts_as_revisable 0.6.0
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 +193 -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.rb +14 -0
- data/lib/acts_as_revisable/acts/common.rb +84 -0
- data/lib/acts_as_revisable/acts/revisable.rb +247 -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 +28 -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/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,193 @@
|
|
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
|
+
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
|
+
|
168
|
+
config.gem "rich-acts_as_revisable", :lib => "acts_as_revisable", :source => "http://gems.github.com"
|
169
|
+
|
170
|
+
== LICENSE:
|
171
|
+
|
172
|
+
(The MIT License)
|
173
|
+
|
174
|
+
Copyright (c) 2008
|
175
|
+
|
176
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
177
|
+
a copy of this software and associated documentation files (the
|
178
|
+
'Software'), to deal in the Software without restriction, including
|
179
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
180
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
181
|
+
permit persons to whom the Software is furnished to do so, subject to
|
182
|
+
the following conditions:
|
183
|
+
|
184
|
+
The above copyright notice and this permission notice shall be
|
185
|
+
included in all copies or substantial portions of the Software.
|
186
|
+
|
187
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
188
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
189
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
190
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
191
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
192
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
193
|
+
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,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)
|
@@ -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,247 @@
|
|
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 should_revise?
|
37
|
+
return true if @aa_revisable_force_revision == true
|
38
|
+
return false if @aa_revisable_no_revision == true
|
39
|
+
return false unless self.changed?
|
40
|
+
!(self.changed.map(&:downcase) & self.class.revisable_columns).blank?
|
41
|
+
end
|
42
|
+
|
43
|
+
def before_revisable_update
|
44
|
+
return unless should_revise?
|
45
|
+
return false unless run_callbacks(:before_revise) { |r, o| r == false}
|
46
|
+
|
47
|
+
@revisable_revision = self.to_revision
|
48
|
+
end
|
49
|
+
|
50
|
+
def after_revisable_update
|
51
|
+
if @revisable_revision
|
52
|
+
@revisable_revision.save
|
53
|
+
@aa_revisable_was_revised = true
|
54
|
+
revisions.reload
|
55
|
+
run_callbacks(:after_revise)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_revision
|
60
|
+
rev = self.class.revision_class.new(@aa_revisable_new_params)
|
61
|
+
|
62
|
+
rev.revisable_original_id = self.id
|
63
|
+
|
64
|
+
self.class.column_names.each do |col|
|
65
|
+
next unless self.class.revisable_should_clone_column? col
|
66
|
+
val = self.send("#{col}_changed?") ? self.send("#{col}_was") : self.send(col)
|
67
|
+
rev.send("#{col}=", val)
|
68
|
+
end
|
69
|
+
|
70
|
+
@aa_revisable_new_params = nil
|
71
|
+
|
72
|
+
rev
|
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 save_with_revisable(*args)
|
82
|
+
@aa_revisable_new_params ||= args.extract_options!
|
83
|
+
@aa_revisable_no_revision = true if @aa_revisable_new_params.delete :without_revision
|
84
|
+
save_without_revisable(*args)
|
85
|
+
end
|
86
|
+
|
87
|
+
def find_revision(number)
|
88
|
+
revisions.find_by_revisable_number(number)
|
89
|
+
end
|
90
|
+
|
91
|
+
def revert_to!(*args)
|
92
|
+
unless run_callbacks(:before_revert) { |r, o| r == false}
|
93
|
+
raise ActiveRecord::RecordNotSaved
|
94
|
+
end
|
95
|
+
|
96
|
+
options = args.extract_options!
|
97
|
+
|
98
|
+
rev = case args.first
|
99
|
+
when self.class.revision_class
|
100
|
+
args.first
|
101
|
+
when :first
|
102
|
+
revisions.last
|
103
|
+
when :previous
|
104
|
+
revisions.first
|
105
|
+
when Fixnum
|
106
|
+
revisions.find_by_revisable_number(args.first)
|
107
|
+
when Time
|
108
|
+
revisions.find(:first, :conditions => ["? >= ? and ? <= ?", :revisable_revised_at, args.first, :revisable_current_at, args.first])
|
109
|
+
end
|
110
|
+
|
111
|
+
unless rev.run_callbacks(:before_restore) { |r, o| r == false}
|
112
|
+
raise ActiveRecord::RecordNotSaved
|
113
|
+
end
|
114
|
+
|
115
|
+
self.class.column_names.each do |col|
|
116
|
+
next unless self.class.revisable_should_clone_column? col
|
117
|
+
self[col] = rev[col]
|
118
|
+
end
|
119
|
+
|
120
|
+
@aa_revisable_no_revision = true if options.delete :without_revision
|
121
|
+
@aa_revisable_new_params = options
|
122
|
+
|
123
|
+
returning(@aa_revisable_no_revision ? save! : revise!) do
|
124
|
+
rev.run_callbacks(:after_restore)
|
125
|
+
run_callbacks(:after_revert)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def revert_to_without_revision!(*args)
|
130
|
+
options = args.extract_options!
|
131
|
+
options.update({:without_revision => true})
|
132
|
+
revert_to!(*(args << options))
|
133
|
+
end
|
134
|
+
|
135
|
+
def revise!
|
136
|
+
return if in_revision?
|
137
|
+
|
138
|
+
begin
|
139
|
+
@aa_revisable_force_revision = true
|
140
|
+
in_revision!
|
141
|
+
save!
|
142
|
+
ensure
|
143
|
+
in_revision!(false)
|
144
|
+
@aa_revisable_force_revision = false
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def revised?
|
149
|
+
@aa_revisable_was_revised || false
|
150
|
+
end
|
151
|
+
|
152
|
+
def in_revision?
|
153
|
+
key = self.read_attribute(self.class.primary_key)
|
154
|
+
aa_revisable_current_revisions[key] || false
|
155
|
+
end
|
156
|
+
|
157
|
+
def in_revision!(val=true)
|
158
|
+
key = self.read_attribute(self.class.primary_key)
|
159
|
+
aa_revisable_current_revisions[key] = val
|
160
|
+
aa_revisable_current_revisions.delete(key) unless val
|
161
|
+
end
|
162
|
+
|
163
|
+
def changeset(&block)
|
164
|
+
return unless block_given?
|
165
|
+
|
166
|
+
return yield(self) if in_revision?
|
167
|
+
|
168
|
+
unless run_callbacks(:before_changeset) { |r, o| r == false}
|
169
|
+
raise ActiveRecord::RecordNotSaved
|
170
|
+
end
|
171
|
+
|
172
|
+
begin
|
173
|
+
in_revision!
|
174
|
+
|
175
|
+
returning(yield(self)) do
|
176
|
+
run_callbacks(:after_changeset)
|
177
|
+
end
|
178
|
+
ensure
|
179
|
+
in_revision!(false)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def revision_number
|
184
|
+
revisions.first.revisable_number
|
185
|
+
rescue NoMethodError
|
186
|
+
0
|
187
|
+
end
|
188
|
+
|
189
|
+
module ClassMethods
|
190
|
+
def with_scope_with_revisable(*args, &block)
|
191
|
+
options = (args.grep(Hash).first || {})[:find]
|
192
|
+
|
193
|
+
if options && options.delete(:with_revisions)
|
194
|
+
without_model_scope do
|
195
|
+
with_scope_without_revisable(*args, &block)
|
196
|
+
end
|
197
|
+
else
|
198
|
+
with_scope_without_revisable(*args, &block)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def find_with_revisable(*args)
|
203
|
+
options = args.grep(Hash).first
|
204
|
+
|
205
|
+
if options && options.delete(:with_revisions)
|
206
|
+
without_model_scope do
|
207
|
+
find_without_revisable(*args)
|
208
|
+
end
|
209
|
+
else
|
210
|
+
find_without_revisable(*args)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def find_with_revisions(*args)
|
215
|
+
args << {} if args.grep(Hash).blank?
|
216
|
+
args.grep(Hash).first.update({:with_revisions => true})
|
217
|
+
find_with_revisable(*args)
|
218
|
+
end
|
219
|
+
|
220
|
+
def revision_class_name
|
221
|
+
self.revisable_options.revision_class_name || "#{self.class_name}Revision"
|
222
|
+
end
|
223
|
+
|
224
|
+
def revision_class
|
225
|
+
@aa_revision_class ||= revision_class_name.constantize
|
226
|
+
end
|
227
|
+
|
228
|
+
def revisable_class
|
229
|
+
self
|
230
|
+
end
|
231
|
+
|
232
|
+
def revisable_columns
|
233
|
+
return @aa_revisable_columns unless @aa_revisable_columns.blank?
|
234
|
+
return @aa_revisable_columns ||= [] if self.revisable_options.except == :all
|
235
|
+
return @aa_revisable_columns ||= [self.revisable_options.only].flatten.map(&:to_s).map(&:downcase) unless self.revisable_options.only.blank?
|
236
|
+
|
237
|
+
except = [self.revisable_options.except].flatten || []
|
238
|
+
except += REVISABLE_SYSTEM_COLUMNS
|
239
|
+
except += REVISABLE_UNREVISABLE_COLUMNS
|
240
|
+
except.uniq!
|
241
|
+
|
242
|
+
@aa_revisable_columns ||= (column_names - except.map(&:to_s)).flatten.map(&:downcase)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
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_associations(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,28 @@
|
|
1
|
+
module FatJam
|
2
|
+
module ActsAsRevisable
|
3
|
+
module CloneAssociations
|
4
|
+
class << self
|
5
|
+
def clone_associations(from, to)
|
6
|
+
return unless from.descends_from_active_record? && to.descends_from_active_record?
|
7
|
+
|
8
|
+
to.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
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def clone_association(association, to)
|
17
|
+
options = association.options.clone
|
18
|
+
options[:foreign_key] ||= "revisable_original_id"
|
19
|
+
to.send(association.macro, association.name, options)
|
20
|
+
end
|
21
|
+
|
22
|
+
def clone_belongs_to_association(association, to)
|
23
|
+
to.send(association.macro, association.name, association.options.clone)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
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
|
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-rich-acts_as_revisable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.6.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
|
+
|