rich-rich-acts_as_revisable 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,11 @@
1
+ module FatJam #:nodoc:
2
+ module ActsAsRevisable
3
+ module VERSION #:nodoc:
4
+ MAJOR = 0
5
+ MINOR = 6
6
+ TINY = 0
7
+
8
+ STRING = [MAJOR, MINOR, TINY].join('.')
9
+ end
10
+ end
11
+ 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
@@ -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
+