kkorach-acts_as_revisable 0.9.7

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.
data/README.rdoc ADDED
@@ -0,0 +1,222 @@
1
+ = acts_as_revisable
2
+
3
+ http://github.com/fatjam/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 (:first, :previous and :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
+ @project.updated_attribute(:name, "Third")
96
+ @project.revision_number # => 2
97
+
98
+ Navigating revisions:
99
+
100
+ @previous = @project.find_revision(:previous)
101
+ # or
102
+ @previous = @project.revisions.first
103
+
104
+ @previous.name # => "Sam"
105
+ @previous.current_revision.name # => "Third"
106
+ @previous.project.name # => "Third"
107
+ @previous.revision_name # => "Changed name"
108
+
109
+ @previous.previous.name # => "Rich"
110
+
111
+ # Forcing the creation of a new revision.
112
+ @project.updated_attribute("Rogelio")
113
+ @project.revision_number # => 3
114
+
115
+ @newest = @project.find_revision(:previous)
116
+ @newest.ancestors.map(&:name) # => ["Third", "Rich"]
117
+
118
+ @oldest = @project.find_revision(:first)
119
+ @oldest.descendants.map(&:name) # => ["Sam", "Third"]
120
+
121
+ Reverting:
122
+
123
+ @project.revert_to!(:previous)
124
+ @project.revision_number # => 2
125
+ @project.name # => "Rich"
126
+
127
+ @project.revert_to!(1, :without_revision => true)
128
+ @project.revision_number # => 2
129
+ @project.name # => "Sam"
130
+
131
+ Branching:
132
+
133
+ @branch = @project.branch(:name => "Bruno")
134
+ @branch.revision_number # => 0
135
+ @branch.branch_source.name # => "Sam"
136
+
137
+ Changesets:
138
+
139
+ @project.revision_number # => 2
140
+
141
+ @project.changeset! do
142
+ @project.name = "Josh"
143
+
144
+ # save would normally trigger a revision
145
+ @project.save
146
+
147
+ # update_attribute triggers a save triggering a revision (normally)
148
+ @project.updated_attribute(:name, "Chris")
149
+
150
+ # revise! normally forces a revision to be created
151
+ @project.revise!
152
+ end
153
+
154
+ # our revision number has only incremented by one
155
+ @project.revision_number # => 3
156
+
157
+ Associations have been cloned:
158
+
159
+ @project.owner === @previous.owner # => true
160
+
161
+ Maybe we don't want to be able to branch from revisions:
162
+
163
+ class Session < ActiveRecord::Base
164
+ # assuming we still have the other code from Session above
165
+
166
+ before_branch do
167
+ false
168
+ end
169
+ end
170
+
171
+ @project.revisions.first.branch # Raises an exception
172
+ @project.branch # works as expected
173
+
174
+ If the owner isn't set let's prevent reverting:
175
+
176
+ class Project < ActiveRecord::Base
177
+ # assuming we still have the other code from Project above
178
+
179
+ before_revert :check_owner_befor_reverting
180
+ def check_owner_befor_reverting
181
+ false unless self.owner?
182
+ end
183
+ end
184
+
185
+ == REQUIREMENTS:
186
+
187
+ This plugin currently depends on Edge Rails, ActiveRecord and ActiveSupport specifically, which will eventually become Rails 2.1.
188
+
189
+ == INSTALL:
190
+
191
+ acts_as_revisable uses Rails' new ability to use gems as plugins. Installing AAR is as simple as installing a gem:
192
+
193
+ sudo gem install fatjam-acts_as_revisable --source=http://gems.github.com
194
+
195
+ Once the gem is installed you'll want to activate it in your Rails app by adding the following line to config/environment.rb:
196
+
197
+ config.gem "fatjam-acts_as_revisable", :lib => "acts_as_revisable", :source => "http://gems.github.com"
198
+
199
+ == LICENSE:
200
+
201
+ (The MIT License)
202
+
203
+ Copyright (c) 2008 JamLab, LLC.
204
+
205
+ Permission is hereby granted, free of charge, to any person obtaining
206
+ a copy of this software and associated documentation files (the
207
+ 'Software'), to deal in the Software without restriction, including
208
+ without limitation the rights to use, copy, modify, merge, publish,
209
+ distribute, sublicense, and/or sell copies of the Software, and to
210
+ permit persons to whom the Software is furnished to do so, subject to
211
+ the following conditions:
212
+
213
+ The above copyright notice and this permission notice shall be
214
+ included in all copies or substantial portions of the Software.
215
+
216
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
217
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
218
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
219
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
220
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
221
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
222
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,44 @@
1
+ require 'rake/rdoctask'
2
+ require 'rake/gempackagetask'
3
+ require 'fileutils'
4
+ require 'lib/acts_as_revisable/version'
5
+ require '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
+ FatJam::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 = FatJam::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 = FatJam::ActsAsRevisable::GemSpecOptions::HASH.clone
43
+ sh %{sudo gem install pkg/#{options[:name]}-#{spec.version} --no-rdoc --no-ri}
44
+ end
@@ -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,231 @@
1
+ module FatJam
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
+ class << base
22
+ alias_method_chain :instantiate, :revisable
23
+ end
24
+
25
+ base.instance_eval do
26
+ define_callbacks :before_branch, :after_branch
27
+ has_many :branches, (revisable_options.revision_association_options || {}).merge({:class_name => base.class_name, :foreign_key => :revisable_branched_from_id})
28
+
29
+ belongs_to :branch_source, :class_name => base.class_name, :foreign_key => :revisable_branched_from_id
30
+ after_save :execute_blocks_after_save
31
+ disable_revisable_scope :branch_source, :branches
32
+ end
33
+ end
34
+
35
+ # Executes the blocks stored in an accessor after a save.
36
+ def execute_blocks_after_save #:nodoc:
37
+ return unless revisable_after_callback_blocks[:save]
38
+ revisable_after_callback_blocks[:save].each do |block|
39
+ block.call
40
+ end
41
+ revisable_after_callback_blocks.delete(:save)
42
+ end
43
+
44
+ # Stores a block for later execution after a given callback.
45
+ # The parameter +key+ is the callback the block should be
46
+ # executed after.
47
+ def execute_after(key, &block) #:nodoc:
48
+ return unless block_given?
49
+ revisable_after_callback_blocks[key] ||= []
50
+ revisable_after_callback_blocks[key] << block
51
+ end
52
+
53
+ # Branch the +Revisable+ or +Revision+ and return the new
54
+ # +revisable+ instance. The instance has not been saved yet.
55
+ #
56
+ # ==== Callbacks
57
+ # * +before_branch+ is called on the +Revisable+ or +Revision+ that is
58
+ # being branched
59
+ # * +after_branch+ is called on the +Revisable+ or +Revision+ that is
60
+ # being branched
61
+ # * +after_branch_created+ is called on the newly created
62
+ # +Revisable+ instance.
63
+ def branch(*args, &block)
64
+ is_branching!
65
+
66
+ unless run_callbacks(:before_branch) { |r, o| r == false}
67
+ raise ActiveRecord::RecordNotSaved
68
+ end
69
+
70
+ options = args.extract_options!
71
+ options[:revisable_branched_from_id] = self.id
72
+ self.class.column_names.each do |col|
73
+ next unless self.class.revisable_should_clone_column? col
74
+ options[col.to_sym] = self[col] unless options.has_key?(col.to_sym)
75
+ end
76
+
77
+ br = self.class.revisable_class.new(options)
78
+ br.is_branching!
79
+
80
+ br.execute_after(:save) do
81
+ begin
82
+ run_callbacks(:after_branch)
83
+ br.run_callbacks(:after_branch_created)
84
+ ensure
85
+ br.is_branching!(false)
86
+ is_branching!(false)
87
+ end
88
+ end
89
+
90
+ block.call(br) if block_given?
91
+
92
+ br
93
+ end
94
+
95
+ # Same as #branch except it calls #save! on the new +Revisable+ instance.
96
+ def branch!(*args)
97
+ branch(*args) do |br|
98
+ br.save!
99
+ end
100
+ end
101
+
102
+ # Globally sets the reverting state of this record.
103
+ def is_branching!(value=true) #:nodoc:
104
+ set_revisable_state(:branching, value)
105
+ end
106
+
107
+ # Returns true if the _record_ (not just this instance
108
+ # of the record) is currently being branched.
109
+ def is_branching?
110
+ get_revisable_state(:branching)
111
+ end
112
+
113
+ # When called on a +Revision+ it returns the original id. When
114
+ # called on a +Revisable+ it returns the id.
115
+ def original_id
116
+ self[:revisable_original_id] || self[:id]
117
+ end
118
+
119
+ # Globally sets the state for a given record. This is keyed
120
+ # on the primary_key of a saved record or the object_id
121
+ # on a new instance.
122
+ def set_revisable_state(type, value) #:nodoc:
123
+ key = self.read_attribute(self.class.primary_key)
124
+ key = object_id if key.nil?
125
+ revisable_current_states[type] ||= {}
126
+ revisable_current_states[type][key] = value
127
+ revisable_current_states[type].delete(key) unless value
128
+ end
129
+
130
+ # Returns the state of the given record.
131
+ def get_revisable_state(type) #:nodoc:
132
+ key = self.read_attribute(self.class.primary_key)
133
+ revisable_current_states[type] ||= {}
134
+ revisable_current_states[type][key] || revisable_current_states[type][object_id] || false
135
+ end
136
+
137
+ # Returns true if the instance is the first revision.
138
+ def first_revision?
139
+ self.revision_number == 1
140
+ end
141
+
142
+ # Returns true if the instance is the most recent revision.
143
+ def latest_revision?
144
+ self.revision_number == self.current_revision.revision_number
145
+ end
146
+
147
+ # Returns true if the instance is the current record and not a revision.
148
+ def current_revision?
149
+ self.is_a? self.class.revisable_class
150
+ end
151
+
152
+ # Accessor for revisable_number just to make external API more pleasant.
153
+ def revision_number
154
+ self[:revisable_number]
155
+ end
156
+
157
+ def diffs(what)
158
+ what = current_revision.find_revision(what)
159
+ returning({}) do |changes|
160
+ self.class.revisable_class.revisable_watch_columns.each do |c|
161
+ changes[c] = [self[c], what[c]] unless self[c] == what[c]
162
+ end
163
+ end
164
+ end
165
+
166
+ module ClassMethods
167
+ def disable_revisable_scope(*args)
168
+ args.each do |a|
169
+ class_eval <<-EOT
170
+ def #{a.to_s}_with_open_scope(*args, &block)
171
+ assoc = self.class.reflect_on_association(#{a.inspect})
172
+ models = [self.class]
173
+
174
+ if [:has_many, :has_one].member? assoc.macro
175
+ models << (assoc.options[:class_name] ? assoc.options[:class_name] : #{a.inspect}.to_s.singularize.camelize).constantize
176
+ end
177
+
178
+ begin
179
+ models.each {|m| m.scoped_model_enabled = false}
180
+ if associated = #{a.to_s}_without_open_scope(*args, &block)
181
+ associated.reload
182
+ end
183
+ ensure
184
+ models.each {|m| m.scoped_model_enabled = true}
185
+ end
186
+ end
187
+ EOT
188
+ alias_method_chain a, :open_scope
189
+ end
190
+ end
191
+
192
+ # Returns true if the revision should clone the given column.
193
+ def revisable_should_clone_column?(col) #:nodoc:
194
+ return false if (REVISABLE_SYSTEM_COLUMNS + REVISABLE_UNREVISABLE_COLUMNS).member? col
195
+ true
196
+ end
197
+
198
+ # acts_as_revisable's override for instantiate so we can
199
+ # return the appropriate type of model based on whether
200
+ # or not the record is the current record.
201
+ def instantiate_with_revisable(record) #:nodoc:
202
+ is_current = columns_hash["revisable_is_current"].type_cast(
203
+ record["revisable_is_current"])
204
+
205
+ if (is_current && self == self.revisable_class) || (!is_current && self == self.revision_class)
206
+ return instantiate_without_revisable(record)
207
+ end
208
+
209
+ object = if is_current
210
+ self.revisable_class
211
+ else
212
+ self.revision_class
213
+ end.allocate
214
+
215
+ object.instance_variable_set("@attributes", record)
216
+ object.instance_variable_set("@attributes_cache", Hash.new)
217
+
218
+ if object.respond_to_without_attributes?(:after_find)
219
+ object.send(:callback, :after_find)
220
+ end
221
+
222
+ if object.respond_to_without_attributes?(:after_initialize)
223
+ object.send(:callback, :after_initialize)
224
+ end
225
+
226
+ object
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end