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 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
+