acts_as_revisable 1.1.1

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 JamLab, LLC.
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.
@@ -0,0 +1,221 @@
1
+ = acts_as_revisable
2
+
3
+ http://github.com/rich/acts_as_revisable
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
+ If you absolutely need a generated revision model, you may pass ":generate_revision_class => true" to acts_as_revisable and it will generate the class at runtime for you. Think of this like scaffolding and not to be kept around for a real application.
17
+
18
+ * Numerous custom callbacks for both revisable and revision models.
19
+ * revisable models
20
+ * before_revise
21
+ * after_revise
22
+ * before_revert
23
+ * after_revert
24
+ * before_changeset
25
+ * after_changeset
26
+ * after_branch_created
27
+ * before_revise_on_destroy (when :on_destroy => :revise is set)
28
+ * after_revise_on_destroy (when :on_destroy => :revise is set)
29
+ * revision models
30
+ * before_restore
31
+ * after_restore
32
+ * both revisable and revision models
33
+ * before_branch
34
+ * after_branch
35
+ These work like any other ActiveRecord callbacks. The before_* callbacks can stop the the action. This uses the Callbacks module in ActiveSupport.
36
+ * Works with a single table.
37
+ * Provides migration generators to add the revisable columns.
38
+ * Grouping several revisable actions into a single revision (changeset).
39
+ * Monitor all or just specified columns to trigger a revision.
40
+ * Uses ActiveRecord's dirty attribute tracking.
41
+ * Several ways to find revisions including:
42
+ * revision number
43
+ * relative keywords (:first, :previous and :last)
44
+ * timestamp
45
+ * Reverting
46
+ * Branching
47
+ * Selectively disable revision tracking
48
+ * Naming revisions
49
+
50
+ == SYNOPSIS:
51
+
52
+ Given a simple model:
53
+
54
+ class Project < ActiveRecord::Base
55
+ # columns: id, name, unimportant, created_at
56
+ end
57
+
58
+ Let's make the projects table revisable:
59
+
60
+ ruby script/generate revisable_migration Project
61
+ rake db:migrate
62
+
63
+ Now Project itself:
64
+
65
+ class Project < ActiveRecord::Base
66
+ has_one :owner
67
+
68
+ acts_as_revisable do
69
+ revision_class_name "Session"
70
+ except :unimportant
71
+ end
72
+ end
73
+
74
+ Create the revision class:
75
+
76
+ class Session < ActiveRecord::Base
77
+ # we can accept the more standard hash syntax
78
+ acts_as_revision :revisable_class_name => "Project"
79
+ end
80
+
81
+ Some example usage:
82
+
83
+ @project = Project.create(:name => "Rich", :unimportant => "some text")
84
+ @project.revision_number # => 0
85
+
86
+ @project.update_attribute(:unimportant, "more text")
87
+ @project.revision_number # => 0
88
+
89
+ @project.name = "Stephen"
90
+ @project.save(:without_revision => true)
91
+ @project.name # => "Stephen"
92
+ @project.revision_number # => 0
93
+
94
+ @project.name = "Sam"
95
+ @project.save(:revision_name => "Changed name")
96
+ @project.revision_number # => 1
97
+
98
+ @project.updated_attribute(:name, "Third")
99
+ @project.revision_number # => 2
100
+
101
+ Navigating revisions:
102
+
103
+ @previous = @project.find_revision(:previous)
104
+ # or
105
+ @previous = @project.revisions.first
106
+
107
+ @previous.name # => "Sam"
108
+ @previous.current_revision.name # => "Third"
109
+ @previous.project.name # => "Third"
110
+ @previous.revision_name # => "Changed name"
111
+
112
+ @previous.previous.name # => "Rich"
113
+
114
+ # Forcing the creation of a new revision.
115
+ @project.updated_attribute("Rogelio")
116
+ @project.revision_number # => 3
117
+
118
+ @newest = @project.find_revision(:previous)
119
+ @newest.ancestors.map(&:name) # => ["Third", "Rich"]
120
+
121
+ @oldest = @project.find_revision(:first)
122
+ @oldest.descendants.map(&:name) # => ["Sam", "Third"]
123
+
124
+ Reverting:
125
+
126
+ @project.revert_to!(:previous)
127
+ @project.revision_number # => 2
128
+ @project.name # => "Rich"
129
+
130
+ @project.revert_to!(1, :without_revision => true)
131
+ @project.revision_number # => 2
132
+ @project.name # => "Sam"
133
+
134
+ Branching:
135
+
136
+ @branch = @project.branch(:name => "Bruno")
137
+ @branch.revision_number # => 0
138
+ @branch.branch_source.name # => "Sam"
139
+
140
+ Changesets:
141
+
142
+ @project.revision_number # => 2
143
+
144
+ @project.changeset! do
145
+ @project.name = "Josh"
146
+
147
+ # save would normally trigger a revision
148
+ @project.save
149
+
150
+ # update_attribute triggers a save triggering a revision (normally)
151
+ @project.updated_attribute(:name, "Chris")
152
+
153
+ # revise! normally forces a revision to be created
154
+ @project.revise!
155
+ end
156
+
157
+ # our revision number has only incremented by one
158
+ @project.revision_number # => 3
159
+
160
+ Maybe we don't want to be able to branch from revisions:
161
+
162
+ class Session < ActiveRecord::Base
163
+ # assuming we still have the other code from Session above
164
+
165
+ before_branch do
166
+ false
167
+ end
168
+ end
169
+
170
+ @project.revisions.first.branch # Raises an exception
171
+ @project.branch # works as expected
172
+
173
+ If the owner isn't set let's prevent reverting:
174
+
175
+ class Project < ActiveRecord::Base
176
+ # assuming we still have the other code from Project above
177
+
178
+ before_revert :check_owner_befor_reverting
179
+ def check_owner_befor_reverting
180
+ false unless self.owner?
181
+ end
182
+ end
183
+
184
+ == REQUIREMENTS:
185
+
186
+ This plugin requires Rails 2.3. Use version 0.9.8 of this plugin for Rails 2.1 and 2.2.
187
+
188
+ == INSTALL:
189
+
190
+ acts_as_revisable uses Rails' new ability to use gems as plugins. Installing AAR is as simple as installing a gem:
191
+
192
+ sudo gem install rich-acts_as_revisable --source=http://gems.github.com
193
+
194
+ Once the gem is installed you'll want to activate it in your Rails app by adding the following line to config/environment.rb:
195
+
196
+ config.gem "rich-acts_as_revisable", :lib => "acts_as_revisable", :source => "http://gems.github.com"
197
+
198
+ == LICENSE:
199
+
200
+ (The MIT License)
201
+
202
+ Copyright (c) 2009 Rich Cavanaugh
203
+
204
+ Permission is hereby granted, free of charge, to any person obtaining
205
+ a copy of this software and associated documentation files (the
206
+ 'Software'), to deal in the Software without restriction, including
207
+ without limitation the rights to use, copy, modify, merge, publish,
208
+ distribute, sublicense, and/or sell copies of the Software, and to
209
+ permit persons to whom the Software is furnished to do so, subject to
210
+ the following conditions:
211
+
212
+ The above copyright notice and this permission notice shall be
213
+ included in all copies or substantial portions of the Software.
214
+
215
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
216
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
217
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
218
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
219
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
220
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
221
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,44 @@
1
+ require 'rake/rdoctask'
2
+ require 'rake/gempackagetask'
3
+ require 'fileutils'
4
+ require "#{File.dirname(__FILE__)}/lib/acts_as_revisable/version"
5
+ require "#{File.dirname(__FILE__)}/lib/acts_as_revisable/gem_spec_options"
6
+
7
+ Rake::RDocTask.new do |rdoc|
8
+ files = ['README.rdoc','LICENSE','lib/**/*.rb','doc/**/*.rdoc','spec/*.rb']
9
+ rdoc.rdoc_files.add(files)
10
+ rdoc.main = 'README.rdoc'
11
+ rdoc.title = 'acts_as_revisable RDoc'
12
+ rdoc.rdoc_dir = 'doc'
13
+ rdoc.options << '--line-numbers' << '--inline-source'
14
+ end
15
+
16
+ spec = Gem::Specification.new do |s|
17
+ WithoutScope::ActsAsRevisable::GemSpecOptions::HASH.each do |key, value|
18
+ s.send("#{key.to_s}=",value)
19
+ end
20
+ end
21
+
22
+ Rake::GemPackageTask.new(spec) do |package|
23
+ package.gem_spec = spec
24
+ end
25
+
26
+ desc "Generate the static gemspec required for github."
27
+ task :generate_gemspec do
28
+ options = WithoutScope::ActsAsRevisable::GemSpecOptions::HASH.clone
29
+ options[:name] = "acts_as_revisable"
30
+
31
+ spec = ["Gem::Specification.new do |s|"]
32
+ options.each do |key, value|
33
+ spec << " s.#{key.to_s} = #{value.inspect}"
34
+ end
35
+ spec << "end"
36
+
37
+ open("acts_as_revisable.gemspec", "w").write(spec.join("\n"))
38
+ end
39
+
40
+ desc "Install acts_as_revisable"
41
+ task :install => :repackage do
42
+ options = WithoutScope::ActsAsRevisable::GemSpecOptions::HASH.clone
43
+ sh %{sudo gem install pkg/#{options[:name]}-#{spec.version} --no-rdoc --no-ri}
44
+ end
@@ -0,0 +1,21 @@
1
+ class RevisableMigrationGenerator < Rails::Generator::NamedBase
2
+ def manifest
3
+ record do |m|
4
+ revisable_columns = [
5
+ ["revisable_original_id", "integer"],
6
+ ["revisable_branched_from_id", "integer"],
7
+ ["revisable_number", "integer", 0],
8
+ ["revisable_name", "string"],
9
+ ["revisable_type", "string"],
10
+ ["revisable_current_at", "datetime"],
11
+ ["revisable_revised_at", "datetime"],
12
+ ["revisable_deleted_at", "datetime"],
13
+ ["revisable_is_current", "boolean", 1]
14
+ ]
15
+
16
+ m.migration_template 'migration.rb', 'db/migrate',
17
+ :migration_file_name => "make_#{class_name.underscore.pluralize}_revisable",
18
+ :assigns => { :cols => revisable_columns }
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ <% table_name = class_name.underscore.pluralize -%>
2
+ class Make<%= class_name.underscore.pluralize.camelize %>Revisable < ActiveRecord::Migration
3
+ def self.up
4
+ <% cols.each do |column_name,column_type,default| -%>
5
+ add_column :<%= table_name %>, :<%= column_name %>, :<%= column_type %><%= ", :default => #{default}" unless default.blank? %>
6
+ <% end -%>
7
+ end
8
+
9
+ def self.down
10
+ <% cols.each do |column_name,_| -%>
11
+ remove_column :<%= table_name %>, :<%= column_name %>
12
+ <% end -%>
13
+ end
14
+ end
@@ -0,0 +1,10 @@
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/base'
9
+
10
+ ActiveRecord::Base.send(:include, WithoutScope::ActsAsRevisable)
@@ -0,0 +1,209 @@
1
+ module WithoutScope
2
+ module ActsAsRevisable
3
+ # This module is mixed into the revision and revisable classes.
4
+ #
5
+ # ==== Callbacks
6
+ #
7
+ # * +before_branch+ is called on the +Revisable+ or +Revision+ that is
8
+ # being branched
9
+ # * +after_branch+ is called on the +Revisable+ or +Revision+ that is
10
+ # being branched
11
+ module Common
12
+ def self.included(base) #:nodoc:
13
+ base.send(:extend, ClassMethods)
14
+
15
+ base.class_inheritable_hash :revisable_after_callback_blocks
16
+ base.revisable_after_callback_blocks = {}
17
+
18
+ base.class_inheritable_hash :revisable_current_states
19
+ base.revisable_current_states = {}
20
+
21
+ base.instance_eval do
22
+ define_callbacks :before_branch, :after_branch
23
+ has_many :branches, (revisable_options.revision_association_options || {}).merge({:class_name => base.name, :foreign_key => :revisable_branched_from_id})
24
+
25
+ belongs_to :branch_source, :class_name => base.name, :foreign_key => :revisable_branched_from_id
26
+ after_save :execute_blocks_after_save
27
+ end
28
+ end
29
+
30
+ # Executes the blocks stored in an accessor after a save.
31
+ def execute_blocks_after_save #:nodoc:
32
+ return unless revisable_after_callback_blocks[:save]
33
+ revisable_after_callback_blocks[:save].each do |block|
34
+ block.call
35
+ end
36
+ revisable_after_callback_blocks.delete(:save)
37
+ end
38
+
39
+ # Stores a block for later execution after a given callback.
40
+ # The parameter +key+ is the callback the block should be
41
+ # executed after.
42
+ def execute_after(key, &block) #:nodoc:
43
+ return unless block_given?
44
+ revisable_after_callback_blocks[key] ||= []
45
+ revisable_after_callback_blocks[key] << block
46
+ end
47
+
48
+ # Branch the +Revisable+ or +Revision+ and return the new
49
+ # +revisable+ instance. The instance has not been saved yet.
50
+ #
51
+ # ==== Callbacks
52
+ # * +before_branch+ is called on the +Revisable+ or +Revision+ that is
53
+ # being branched
54
+ # * +after_branch+ is called on the +Revisable+ or +Revision+ that is
55
+ # being branched
56
+ # * +after_branch_created+ is called on the newly created
57
+ # +Revisable+ instance.
58
+ def branch(*args, &block)
59
+ is_branching!
60
+
61
+ unless run_callbacks(:before_branch) { |r, o| r == false}
62
+ raise ActiveRecord::RecordNotSaved
63
+ end
64
+
65
+ options = args.extract_options!
66
+ options[:revisable_branched_from_id] = self.id
67
+ self.class.column_names.each do |col|
68
+ next unless self.class.revisable_should_clone_column? col
69
+ options[col.to_sym] = self[col] unless options.has_key?(col.to_sym)
70
+ end
71
+
72
+ br = self.class.revisable_class.new(options)
73
+ br.is_branching!
74
+
75
+ br.execute_after(:save) do
76
+ begin
77
+ run_callbacks(:after_branch)
78
+ br.run_callbacks(:after_branch_created)
79
+ ensure
80
+ br.is_branching!(false)
81
+ is_branching!(false)
82
+ end
83
+ end
84
+
85
+ block.call(br) if block_given?
86
+
87
+ br
88
+ end
89
+
90
+ # Same as #branch except it calls #save! on the new +Revisable+ instance.
91
+ def branch!(*args)
92
+ branch(*args) do |br|
93
+ br.save!
94
+ end
95
+ end
96
+
97
+ # Globally sets the reverting state of this record.
98
+ def is_branching!(value=true) #:nodoc:
99
+ set_revisable_state(:branching, value)
100
+ end
101
+
102
+ # Returns true if the _record_ (not just this instance
103
+ # of the record) is currently being branched.
104
+ def is_branching?
105
+ get_revisable_state(:branching)
106
+ end
107
+
108
+ # When called on a +Revision+ it returns the original id. When
109
+ # called on a +Revisable+ it returns the id.
110
+ def original_id
111
+ self[:revisable_original_id] || self[:id]
112
+ end
113
+
114
+ # Globally sets the state for a given record. This is keyed
115
+ # on the primary_key of a saved record or the object_id
116
+ # on a new instance.
117
+ def set_revisable_state(type, value) #:nodoc:
118
+ key = self.read_attribute(self.class.primary_key)
119
+ key = object_id if key.nil?
120
+ revisable_current_states[type] ||= {}
121
+ revisable_current_states[type][key] = value
122
+ revisable_current_states[type].delete(key) unless value
123
+ end
124
+
125
+ # Returns the state of the given record.
126
+ def get_revisable_state(type) #:nodoc:
127
+ key = self.read_attribute(self.class.primary_key)
128
+ revisable_current_states[type] ||= {}
129
+ revisable_current_states[type][key] || revisable_current_states[type][object_id] || false
130
+ end
131
+
132
+ # Returns true if the instance is the first revision.
133
+ def first_revision?
134
+ self.revision_number == 1
135
+ end
136
+
137
+ # Returns true if the instance is the most recent revision.
138
+ def latest_revision?
139
+ self.revision_number == self.current_revision.revision_number
140
+ end
141
+
142
+ # Returns true if the instance is the current record and not a revision.
143
+ def current_revision?
144
+ self.is_a? self.class.revisable_class
145
+ end
146
+
147
+ # Accessor for revisable_number just to make external API more pleasant.
148
+ def revision_number
149
+ self[:revisable_number] ||= 0
150
+ end
151
+
152
+ def revision_number=(value)
153
+ self[:revisable_number] = value
154
+ end
155
+
156
+ def diffs(what)
157
+ what = current_revision.find_revision(what)
158
+ returning({}) do |changes|
159
+ self.class.revisable_class.revisable_watch_columns.each do |c|
160
+ changes[c] = [self[c], what[c]] unless self[c] == what[c]
161
+ end
162
+ end
163
+ end
164
+
165
+ def deleted?
166
+ self.revisable_deleted_at.present?
167
+ end
168
+
169
+ module ClassMethods
170
+ # Returns true if the revision should clone the given column.
171
+ def revisable_should_clone_column?(col) #:nodoc:
172
+ return false if (REVISABLE_SYSTEM_COLUMNS + REVISABLE_UNREVISABLE_COLUMNS).member? col
173
+ true
174
+ end
175
+
176
+ # acts_as_revisable's override for instantiate so we can
177
+ # return the appropriate type of model based on whether
178
+ # or not the record is the current record.
179
+ def instantiate(record) #:nodoc:
180
+ is_current = columns_hash["revisable_is_current"].type_cast(
181
+ record["revisable_is_current"])
182
+
183
+ if (is_current && self == self.revisable_class) || (!is_current && self == self.revision_class)
184
+ return super(record)
185
+ end
186
+
187
+ object = if is_current
188
+ self.revisable_class
189
+ else
190
+ self.revision_class
191
+ end.allocate
192
+
193
+ object.instance_variable_set("@attributes", record)
194
+ object.instance_variable_set("@attributes_cache", Hash.new)
195
+
196
+ if object.respond_to_without_attributes?(:after_find)
197
+ object.send(:callback, :after_find)
198
+ end
199
+
200
+ if object.respond_to_without_attributes?(:after_initialize)
201
+ object.send(:callback, :after_initialize)
202
+ end
203
+
204
+ object
205
+ end
206
+ end
207
+ end
208
+ end
209
+ end