acts_as_versioned 0.2.3 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,48 @@
1
+ *GIT* (version numbers are overrated)
2
+
3
+
4
+
5
+ * (16 Jun 2008) Backwards Compatibility is overrated (big updates for rails 2.1)
6
+
7
+ * Use ActiveRecord 2.1's dirty attribute checking instead [Asa Calow]
8
+ * Remove last traces of #non_versioned_fields
9
+ * Remove AR::Base.find_version and AR::Base.find_versions, rely on AR association proxies and named_scope
10
+ * Remove #versions_count, rely on AR association counter caching.
11
+ * Remove #versioned_attributes, basically the same as AR::Base.versioned_columns
12
+
13
+ * (5 Oct 2006) Allow customization of #versions association options [Dan Peterson]
14
+
15
+ *0.5.1*
16
+
17
+ * (8 Aug 2006) Versioned models now belong to the unversioned model. @article_version.article.class => Article [Aslak Hellesoy]
18
+
19
+ *0.5* # do versions even matter for plugins?
20
+
21
+ * (21 Apr 2006) Added without_locking and without_revision methods.
22
+
23
+ Foo.without_revision do
24
+ @foo.update_attributes ...
25
+ end
26
+
27
+ *0.4*
28
+
29
+ * (28 March 2006) Rename non_versioned_fields to non_versioned_columns (old one is kept for compatibility).
30
+ * (28 March 2006) Made explicit documentation note that string column names are required for non_versioned_columns.
31
+
32
+ *0.3.1*
33
+
34
+ * (7 Jan 2006) explicitly set :foreign_key option for the versioned model's belongs_to assocation for STI [Caged]
35
+ * (7 Jan 2006) added tests to prove has_many :through joins work
36
+
37
+ *0.3*
38
+
39
+ * (2 Jan 2006) added ability to share a mixin with versioned class
40
+ * (2 Jan 2006) changed the dynamic version model to MyModel::Version
41
+
42
+ *0.2.4*
43
+
44
+ * (27 Nov 2005) added note about possible destructive behavior of if_changed? [Michael Schuerig]
45
+
1
46
  *0.2.3*
2
47
 
3
48
  * (12 Nov 2005) fixed bug with old behavior of #blank? [Michael Schuerig]
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'http://rubygems.org'
2
+
3
+ group :development do
4
+ gem 'rails', '3.0.0.beta4'
5
+ gem 'sqlite3-ruby', '1.3.1'
6
+ gem 'mysql', '2.8.1'
7
+ end
@@ -0,0 +1,146 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'date'
4
+
5
+ #############################################################################
6
+ #
7
+ # Helper functions
8
+ #
9
+ #############################################################################
10
+
11
+ def name
12
+ @name ||= Dir['*.gemspec'].first.split('.').first
13
+ end
14
+
15
+ def version
16
+ line = File.read("lib/#{name}.rb")[/^\s*VERSION\s*=\s*.*/]
17
+ line.match(/.*VERSION\s*=\s*['"](.*)['"]/)[1]
18
+ end
19
+
20
+ def date
21
+ Date.today.to_s
22
+ end
23
+
24
+ def rubyforge_project
25
+ name
26
+ end
27
+
28
+ def gemspec_file
29
+ "#{name}.gemspec"
30
+ end
31
+
32
+ def gem_file
33
+ "#{name}-#{version}.gem"
34
+ end
35
+
36
+ def replace_header(head, header_name)
37
+ head.sub!(/(\.#{header_name}\s*= ').*'/) { "#{$1}#{send(header_name)}'"}
38
+ end
39
+
40
+ #############################################################################
41
+ #
42
+ # Standard tasks
43
+ #
44
+ #############################################################################
45
+
46
+ task :default => :test
47
+
48
+ require 'rake/testtask'
49
+ Rake::TestTask.new(:test) do |test|
50
+ test.libs << 'lib' << 'test'
51
+ test.pattern = 'test/**/*_test.rb'
52
+ test.verbose = true
53
+ end
54
+
55
+ desc "Generate RCov test coverage and open in your browser"
56
+ task :coverage do
57
+ require 'rcov'
58
+ sh "rm -fr coverage"
59
+ sh "rcov test/test_*.rb"
60
+ sh "open coverage/index.html"
61
+ end
62
+
63
+ require 'rake/rdoctask'
64
+ Rake::RDocTask.new do |rdoc|
65
+ rdoc.rdoc_dir = 'rdoc'
66
+ rdoc.title = "#{name} #{version}"
67
+ rdoc.rdoc_files.include('README*')
68
+ rdoc.rdoc_files.include('lib/**/*.rb')
69
+ end
70
+
71
+ desc "Open an irb session preloaded with this library"
72
+ task :console do
73
+ sh "irb -rubygems -r ./lib/#{name}.rb"
74
+ end
75
+
76
+ #############################################################################
77
+ #
78
+ # Custom tasks (add your own tasks here)
79
+ #
80
+ #############################################################################
81
+
82
+
83
+
84
+ #############################################################################
85
+ #
86
+ # Packaging tasks
87
+ #
88
+ #############################################################################
89
+
90
+ task :release => :build do
91
+ unless `git branch` =~ /^\* master$/
92
+ puts "You must be on the master branch to release!"
93
+ exit!
94
+ end
95
+ sh "git commit --allow-empty -a -m 'Release #{version}'"
96
+ sh "git tag v#{version}"
97
+ sh "git push origin master"
98
+ sh "git push v#{version}"
99
+ sh "gem push pkg/#{name}-#{version}.gem"
100
+ end
101
+
102
+ task :build => :gemspec do
103
+ sh "mkdir -p pkg"
104
+ sh "gem build #{gemspec_file}"
105
+ sh "mv #{gem_file} pkg"
106
+ end
107
+
108
+ task :gemspec => :validate do
109
+ # read spec file and split out manifest section
110
+ spec = File.read(gemspec_file)
111
+ head, manifest, tail = spec.split(" # = MANIFEST =\n")
112
+
113
+ # replace name version and date
114
+ replace_header(head, :name)
115
+ replace_header(head, :version)
116
+ replace_header(head, :date)
117
+ #comment this out if your rubyforge_project has a different name
118
+ replace_header(head, :rubyforge_project)
119
+
120
+ # determine file list from git ls-files
121
+ files = `git ls-files`.
122
+ split("\n").
123
+ sort.
124
+ reject { |file| file =~ /^\./ }.
125
+ reject { |file| file =~ /^(rdoc|pkg)/ }.
126
+ map { |file| " #{file}" }.
127
+ join("\n")
128
+
129
+ # piece file back together and write
130
+ manifest = " s.files = %w[\n#{files}\n ]\n"
131
+ spec = [head, manifest, tail].join(" # = MANIFEST =\n")
132
+ File.open(gemspec_file, 'w') { |io| io.write(spec) }
133
+ puts "Updated #{gemspec_file}"
134
+ end
135
+
136
+ task :validate do
137
+ libfiles = Dir['lib/*'] - ["lib/#{name}.rb", "lib/#{name}"]
138
+ unless libfiles.empty?
139
+ puts "Directory `lib` should only contain a `#{name}.rb` file and `#{name}` dir."
140
+ exit!
141
+ end
142
+ unless Dir['VERSION*'].empty?
143
+ puts "A `VERSION` file at root level violates Gem best practices."
144
+ exit!
145
+ end
146
+ end
@@ -0,0 +1,85 @@
1
+ ## This is the rakegem gemspec template. Make sure you read and understand
2
+ ## all of the comments. Some sections require modification, and others can
3
+ ## be deleted if you don't need them. Once you understand the contents of
4
+ ## this file, feel free to delete any comments that begin with two hash marks.
5
+ ## You can find comprehensive Gem::Specification documentation, at
6
+ ## http://docs.rubygems.org/read/chapter/20
7
+ Gem::Specification.new do |s|
8
+ s.specification_version = 2 if s.respond_to? :specification_version=
9
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
10
+ s.rubygems_version = '1.3.5'
11
+
12
+ ## Leave these as is they will be modified for you by the rake gemspec task.
13
+ ## If your rubyforge_project name is different, then edit it and comment out
14
+ ## the sub! line in the Rakefile
15
+ s.name = 'acts_as_versioned'
16
+ s.version = '0.6.0'
17
+ s.date = '2010-07-19'
18
+ s.rubyforge_project = 'acts_as_versioned'
19
+
20
+ ## Make sure your summary is short. The description may be as long
21
+ ## as you like.
22
+ s.summary = "Add simple versioning to ActiveRecord models."
23
+ s.description = "Add simple versioning to ActiveRecord models."
24
+
25
+ ## List the primary authors. If there are a bunch of authors, it's probably
26
+ ## better to set the email to an email list or something. If you don't have
27
+ ## a custom homepage, consider using your GitHub URL or the like.
28
+ s.authors = ["Rick Olson"]
29
+ s.email = 'technoweenie@gmail.com'
30
+ s.homepage = 'http://github.com/technoweenie/acts_as_versioned'
31
+
32
+ ## This gets added to the $LOAD_PATH so that 'lib/NAME.rb' can be required as
33
+ ## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
34
+ s.require_paths = %w[lib]
35
+
36
+ ## Specify any RDoc options here. You'll want to add your README and
37
+ ## LICENSE files to the extra_rdoc_files list.
38
+ s.rdoc_options = ["--charset=UTF-8"]
39
+ s.extra_rdoc_files = %w[README MIT-LICENSE CHANGELOG]
40
+
41
+ ## List your runtime dependencies here. Runtime dependencies are those
42
+ ## that are needed for an end user to actually USE your code.
43
+ s.add_dependency('activerecord', ["~> 3.0.0.beta4"])
44
+
45
+ ## List your development dependencies here. Development dependencies are
46
+ ## those that are only needed during development
47
+ s.add_development_dependency('sqlite3-ruby', ["~> 1.3.1"])
48
+
49
+ ## Leave this section as-is. It will be automatically generated from the
50
+ ## contents of your Git repository via the gemspec task. DO NOT REMOVE
51
+ ## THE MANIFEST COMMENTS, they are used as delimiters by the task.
52
+ # = MANIFEST =
53
+ s.files = %w[
54
+ CHANGELOG
55
+ Gemfile
56
+ MIT-LICENSE
57
+ README
58
+ RUNNING_UNIT_TESTS
59
+ Rakefile
60
+ acts_as_versioned.gemspec
61
+ init.rb
62
+ lib/acts_as_versioned.rb
63
+ test/abstract_unit.rb
64
+ test/database.yml
65
+ test/fixtures/authors.yml
66
+ test/fixtures/landmark.rb
67
+ test/fixtures/landmark_versions.yml
68
+ test/fixtures/landmarks.yml
69
+ test/fixtures/locked_pages.yml
70
+ test/fixtures/locked_pages_revisions.yml
71
+ test/fixtures/migrations/1_add_versioned_tables.rb
72
+ test/fixtures/page.rb
73
+ test/fixtures/page_versions.yml
74
+ test/fixtures/pages.yml
75
+ test/fixtures/widget.rb
76
+ test/migration_test.rb
77
+ test/schema.rb
78
+ test/versioned_test.rb
79
+ ]
80
+ # = MANIFEST =
81
+
82
+ ## Test files will be grabbed from the file list. Make sure the path glob
83
+ ## matches what you actually use.
84
+ s.test_files = s.files.select { |path| path =~ /^test\/test_.*\.rb/ }
85
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'acts_as_versioned'
@@ -18,11 +18,12 @@
18
18
  # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
19
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
20
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+ require 'active_support/concern'
21
22
 
22
23
  module ActiveRecord #:nodoc:
23
24
  module Acts #:nodoc:
24
25
  # Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
25
- # versioned table ready and that your model has a version field. This works with optimisic locking if the lock_version
26
+ # versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
26
27
  # column is present as well.
27
28
  #
28
29
  # The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
@@ -49,349 +50,445 @@ module ActiveRecord #:nodoc:
49
50
  # page.revert_to(page.versions.last) # using versioned instance
50
51
  # page.title # => 'hello world'
51
52
  #
53
+ # page.versions.earliest # efficient query to find the first version
54
+ # page.versions.latest # efficient query to find the most recently created version
55
+ #
56
+ #
57
+ # Simple Queries to page between versions
58
+ #
59
+ # page.versions.before(version)
60
+ # page.versions.after(version)
61
+ #
62
+ # Access the previous/next versions from the versioned model itself
63
+ #
64
+ # version = page.versions.latest
65
+ # version.previous # go back one version
66
+ # version.next # go forward one version
67
+ #
52
68
  # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
53
69
  module Versioned
54
- def self.included(base) # :nodoc:
55
- base.extend ClassMethods
56
- end
70
+ VERSION = "0.6.0"
71
+ CALLBACKS = [:set_new_version, :save_version, :save_version?]
72
+
73
+ # == Configuration options
74
+ #
75
+ # * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
76
+ # * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
77
+ # * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
78
+ # * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
79
+ # * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
80
+ # * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
81
+ # * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
82
+ # * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
83
+ # For finer control, pass either a Proc or modify Model#version_condition_met?
84
+ #
85
+ # acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
86
+ #
87
+ # or...
88
+ #
89
+ # class Auction
90
+ # def version_condition_met? # totally bypasses the <tt>:if</tt> option
91
+ # !expired?
92
+ # end
93
+ # end
94
+ #
95
+ # * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
96
+ # either a symbol or array of symbols.
97
+ #
98
+ # * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block
99
+ # to create an anonymous mixin:
100
+ #
101
+ # class Auction
102
+ # acts_as_versioned do
103
+ # def started?
104
+ # !started_at.nil?
105
+ # end
106
+ # end
107
+ # end
108
+ #
109
+ # or...
110
+ #
111
+ # module AuctionExtension
112
+ # def started?
113
+ # !started_at.nil?
114
+ # end
115
+ # end
116
+ # class Auction
117
+ # acts_as_versioned :extend => AuctionExtension
118
+ # end
119
+ #
120
+ # Example code:
121
+ #
122
+ # @auction = Auction.find(1)
123
+ # @auction.started?
124
+ # @auction.versions.first.started?
125
+ #
126
+ # == Database Schema
127
+ #
128
+ # The model that you're versioning needs to have a 'version' attribute. The model is versioned
129
+ # into a table called #{model}_versions where the model name is singlular. The _versions table should
130
+ # contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
131
+ #
132
+ # A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
133
+ # then that field is reflected in the versioned model as 'versioned_type' by default.
134
+ #
135
+ # Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
136
+ # method, perfect for a migration. It will also create the version column if the main model does not already have it.
137
+ #
138
+ # class AddVersions < ActiveRecord::Migration
139
+ # def self.up
140
+ # # create_versioned_table takes the same options hash
141
+ # # that create_table does
142
+ # Post.create_versioned_table
143
+ # end
144
+ #
145
+ # def self.down
146
+ # Post.drop_versioned_table
147
+ # end
148
+ # end
149
+ #
150
+ # == Changing What Fields Are Versioned
151
+ #
152
+ # By default, acts_as_versioned will version all but these fields:
153
+ #
154
+ # [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
155
+ #
156
+ # You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
157
+ #
158
+ # class Post < ActiveRecord::Base
159
+ # acts_as_versioned
160
+ # self.non_versioned_columns << 'comments_count'
161
+ # end
162
+ #
163
+ def acts_as_versioned(options = {}, &extension)
164
+ # don't allow multiple calls
165
+ return if self.included_modules.include?(ActiveRecord::Acts::Versioned::Behaviors)
166
+
167
+ cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
168
+ :version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
169
+ :version_association_options, :version_if_changed
170
+
171
+ self.versioned_class_name = options[:class_name] || "Version"
172
+ self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
173
+ self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
174
+ self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
175
+ self.version_column = options[:version_column] || 'version'
176
+ self.version_sequence_name = options[:sequence_name]
177
+ self.max_version_limit = options[:limit].to_i
178
+ self.version_condition = options[:if] || true
179
+ self.non_versioned_columns = [self.primary_key, inheritance_column, self.version_column, 'lock_version', versioned_inheritance_column] + options[:non_versioned_columns].to_a.map(&:to_s)
180
+ self.version_association_options = {
181
+ :class_name => "#{self.to_s}::#{versioned_class_name}",
182
+ :foreign_key => versioned_foreign_key,
183
+ :dependent => :delete_all
184
+ }.merge(options[:association_options] || {})
185
+
186
+ if block_given?
187
+ extension_module_name = "#{versioned_class_name}Extension"
188
+ silence_warnings do
189
+ self.const_set(extension_module_name, Module.new(&extension))
190
+ end
191
+
192
+ options[:extend] = self.const_get(extension_module_name)
193
+ end
194
+
195
+ unless options[:if_changed].nil?
196
+ self.track_altered_attributes = true
197
+ options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
198
+ self.version_if_changed = options[:if_changed].map(&:to_s)
199
+ end
200
+
201
+ include options[:extend] if options[:extend].is_a?(Module)
202
+
203
+ include ActiveRecord::Acts::Versioned::Behaviors
57
204
 
58
- module ClassMethods
59
- # == Configuration options
60
- #
61
- # * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
62
- # * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
63
- # * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
64
- # * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
65
- # * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
66
- # * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
67
- # * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
68
- # * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
69
- # For finer control, pass either a Proc or modify Model#version_condition_met?
70
- #
71
- # acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
72
- #
73
- # or...
74
- #
75
- # class Auction
76
- # def version_condition_met? # totally bypasses the <tt>:if</tt> option
77
- # !expired?
78
- # end
79
- # end
80
- #
81
- # * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
82
- # either a symbol or array of symbols.
83
- #
84
- # == Database Schema
85
- #
86
- # The model that you're versioning needs to have a 'version' attribute. The model is versioned
87
- # into a table called #{model}_versions where the model name is singlular. The _versions table should
88
- # contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
89
- #
90
- # A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
91
- # then that field is reflected in the versioned model as 'versioned_type' by default.
92
205
  #
93
- # Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
94
- # method, perfect for a migration. It will also create the version column if the main model does not already have it.
206
+ # Create the dynamic versioned model
95
207
  #
96
- # class AddVersions < ActiveRecord::Migration
97
- # def self.up
98
- # # create_versioned_table takes the same options hash
99
- # # that create_table does
100
- # Post.create_versioned_table
101
- # end
102
- #
103
- # def self.down
104
- # Post.drop_versioned_table
105
- # end
106
- # end
107
- def acts_as_versioned(options = {})
108
- # don't allow multiple calls
109
- return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
110
-
111
- class_eval do
112
- include ActiveRecord::Acts::Versioned::ActMethods
113
- cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
114
- :version_column, :max_version_limit, :track_changed_attributes, :version_condition, :version_sequence_name
115
- attr_accessor :changed_attributes
208
+ const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
209
+ def self.reloadable?;
210
+ false;
116
211
  end
117
-
118
- self.versioned_class_name = options[:class_name] || "#{self.to_s.demodulize}Version"
119
- self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
120
- self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{Inflector.underscore(Inflector.demodulize(class_name_of_active_record_descendant(self)))}_versions#{table_name_suffix}"
121
- self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
122
- self.version_column = options[:version_column] || 'version'
123
- self.version_sequence_name = options[:sequence_name]
124
- self.max_version_limit = options[:limit].to_i
125
- self.version_condition = options[:if] || true
126
-
127
- class_eval do
128
- has_many :versions,
129
- :class_name => "ActiveRecord::Acts::Versioned::#{versioned_class_name}",
130
- :foreign_key => "#{versioned_foreign_key}",
131
- :order => 'version'
132
- before_save :set_new_version
133
- after_create :save_version_on_create
134
- after_update :save_version
135
- after_save :clear_old_versions
136
- after_save :clear_changed_attributes
137
-
138
- unless options[:if_changed].nil?
139
- self.track_changed_attributes = true
140
- options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
141
- options[:if_changed].each do |attr_name|
142
- define_method("#{attr_name}=") do |value|
143
- (self.changed_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) or self.send(attr_name) == value
144
- write_attribute(attr_name.to_s, value)
145
- end
146
- end
147
- end
212
+
213
+ # find first version before the given version
214
+ def self.before(version)
215
+ where(["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]).
216
+ order('version DESC').
217
+ first
148
218
  end
149
-
150
- # create the dynamic versioned model
151
- # maybe if i sit down long enough i can think up a better way to do this.
152
- dynamic_model = <<-EOV
153
- class ActiveRecord::Acts::Versioned::#{versioned_class_name} < ActiveRecord::Base
154
- set_table_name "#{versioned_table_name}"
155
- belongs_to :#{self.to_s.demodulize.underscore}, :class_name => "#{self.to_s}"
156
- EOV
157
-
158
- dynamic_model += %Q{set_sequence_name "#{version_sequence_name}"\n} if version_sequence_name
159
-
160
- eval dynamic_model + 'end'
161
- end
162
- end
163
-
164
- module ActMethods
165
- def self.included(base) # :nodoc:
166
- base.extend ClassMethods
167
- end
168
-
169
- # Saves a version of the model if applicable
170
- def save_version
171
- save_version_on_create if save_version?
172
- end
173
-
174
- # Saves a version of the model in the versioned table. This is called in the after_save callback by default
175
- def save_version_on_create
176
- rev = self.class.versioned_class.new
177
- self.clone_versioned_model(self, rev)
178
- rev.version = send(self.class.version_column)
179
- rev.send("#{self.class.versioned_foreign_key}=", self.id)
180
- rev.save
181
- end
182
219
 
183
- # Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
184
- # Override this method to set your own criteria for clearing old versions.
185
- def clear_old_versions
186
- return if self.class.max_version_limit == 0
187
- excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
188
- if excess_baggage > 0
189
- sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
190
- self.class.versioned_class.connection.execute sql
220
+ # find first version after the given version.
221
+ def self.after(version)
222
+ where(["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]).
223
+ order('version ASC').
224
+ first
191
225
  end
192
- end
193
226
 
194
- # Finds a specific version of this model.
195
- def find_version(version)
196
- return version if version.is_a?(self.class.versioned_class)
197
- return nil if version.is_a?(ActiveRecord::Base)
198
- find_versions(:conditions => ['version = ?', version], :limit => 1).first
199
- end
200
-
201
- # Finds versions of this model. Takes an options hash like <tt>find</tt>
202
- def find_versions(options = {})
203
- versions.find(:all, options)
204
- end
227
+ # finds earliest version of this record
228
+ def self.earliest
229
+ order("#{original_class.version_column}").first
230
+ end
205
231
 
206
- # Reverts a model to a given version. Takes either a version number or an instance of the versioned model
207
- def revert_to(version)
208
- if version.is_a?(self.class.versioned_class)
209
- return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
210
- else
211
- return false unless version = find_version(version)
232
+ # find latest version of this record
233
+ def self.latest
234
+ order("#{original_class.version_column} desc").first
212
235
  end
213
- self.clone_versioned_model(version, self)
214
- self.send("#{self.class.version_column}=", version.version)
215
- true
216
- end
217
236
 
218
- # Reverts a model to a given version and saves the model.
219
- # Takes either a version number or an instance of the versioned model
220
- def revert_to!(version)
221
- revert_to(version) ? save_without_revision : false
222
- end
237
+ def previous
238
+ self.class.before(self)
239
+ end
223
240
 
224
- # Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
225
- def save_without_revision
226
- old_lock_value = ActiveRecord::Base.lock_optimistically
227
- ActiveRecord::Base.lock_optimistically = false if old_lock_value
228
- disable_acts_as_versioned_callbacks
229
- save_result = self.save
230
- enable_acts_as_versioned_callbacks
231
- ActiveRecord::Base.lock_optimistically = true if old_lock_value
232
- save_result
233
- end
234
-
235
- # Returns an array of attribute keys that are versioned. See non_versioned_fields
236
- def versioned_attributes
237
- self.attributes.keys.select { |k| !self.class.non_versioned_fields.include?(k) }
238
- end
239
-
240
- # If called with no parameters, gets whether the current model has changed and needs to be versioned.
241
- # If called with a single parameter, gets whether the parameter has changed.
242
- def changed?(attr_name = nil)
243
- attr_name.nil? ?
244
- (!self.class.track_changed_attributes or (changed_attributes and changed_attributes.length > 0)) :
245
- (changed_attributes and changed_attributes.include?(attr_name.to_s))
246
- end
247
-
248
- # keep old dirty? method
249
- alias_method :dirty?, :changed?
250
-
251
- # Clones a model. Used when saving a new version or reverting a model's version.
252
- def clone_versioned_model(orig_model, new_model)
253
- self.versioned_attributes.each do |key|
254
- new_model.send("#{key}=", orig_model.attributes[key]) if orig_model.attribute_present?(key)
241
+ def next
242
+ self.class.after(self)
255
243
  end
256
-
257
- if orig_model.is_a?(self.class.versioned_class)
258
- new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
259
- elsif new_model.is_a?(self.class.versioned_class)
260
- new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
244
+
245
+ def versions_count
246
+ page.version
261
247
  end
262
248
  end
263
-
264
- # Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
265
- def save_version?
266
- version_condition_met? and changed?
267
- end
268
-
269
- # Checks condition set in the :if option to check whether a revision should be created or not. Override this for
270
- # custom version condition checking.
271
- def version_condition_met?
272
- case
273
- when version_condition.is_a?(Symbol)
274
- send(version_condition)
275
- when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
276
- version_condition.call(self)
277
- else
278
- version_condition
279
- end
280
- end
281
249
 
282
- protected
283
- # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
284
- def set_new_version
285
- self.send("#{self.class.version_column}=", self.next_version) if new_record? or (!locking_enabled? and save_version?)
286
- end
287
-
288
- # Gets the next available version for the current record, or 1 for a new record
289
- def next_version
290
- return 1 if new_record?
291
- connection.select_one("SELECT MAX(version)+1 AS next_version FROM #{self.class.versioned_table_name} WHERE #{self.class.versioned_foreign_key} = #{self.id}")['next_version'] || 1
292
- end
293
-
294
- # clears current changed attributes. Called after save.
295
- def clear_changed_attributes
296
- self.changed_attributes = []
297
- end
250
+ versioned_class.cattr_accessor :original_class
251
+ versioned_class.original_class = self
252
+ versioned_class.set_table_name versioned_table_name
253
+ versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
254
+ :class_name => "::#{self.to_s}",
255
+ :foreign_key => versioned_foreign_key
256
+ versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
257
+ versioned_class.set_sequence_name version_sequence_name if version_sequence_name
258
+ end
298
259
 
299
- private
300
- unless defined?(ACTS_AS_VERSIONED_CALLBACKS)
301
- ACTS_AS_VERSIONED_CALLBACKS = [:set_new_version, :save_version_on_create, :save_version, :clear_changed_attributes]
302
- end
260
+ module Behaviors
261
+ extend ActiveSupport::Concern
303
262
 
304
- ACTS_AS_VERSIONED_CALLBACKS.each do |attr_name|
305
- alias_method "orig_#{attr_name}".to_sym, attr_name
263
+ included do
264
+ has_many :versions, self.version_association_options
265
+
266
+ before_save :set_new_version
267
+ after_save :save_version
268
+ after_save :clear_old_versions
306
269
  end
307
-
308
- def empty_callback() end #:nodoc:
309
270
 
310
- def enable_acts_as_versioned_callbacks
311
- self.class.class_eval do
312
- ACTS_AS_VERSIONED_CALLBACKS.each do |attr_name|
313
- alias_method attr_name, "orig_#{attr_name}".to_sym
271
+ module InstanceMethods
272
+ # Saves a version of the model in the versioned table. This is called in the after_save callback by default
273
+ def save_version
274
+ if @saving_version
275
+ @saving_version = nil
276
+ rev = self.class.versioned_class.new
277
+ clone_versioned_model(self, rev)
278
+ rev.send("#{self.class.version_column}=", send(self.class.version_column))
279
+ rev.send("#{self.class.versioned_foreign_key}=", id)
280
+ rev.save
314
281
  end
315
282
  end
316
- end
317
283
 
318
- def disable_acts_as_versioned_callbacks
319
- self.class.class_eval do
320
- ACTS_AS_VERSIONED_CALLBACKS.each do |attr_name|
321
- alias_method attr_name, :empty_callback
284
+ # Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
285
+ # Override this method to set your own criteria for clearing old versions.
286
+ def clear_old_versions
287
+ return if self.class.max_version_limit == 0
288
+ excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
289
+ if excess_baggage > 0
290
+ self.class.versioned_class.delete_all ["#{self.class.version_column} <= ? and #{self.class.versioned_foreign_key} = ?", excess_baggage, id]
322
291
  end
323
292
  end
324
- end
325
293
 
326
- module ClassMethods
327
- # Finds a specific version of a specific row of this model
328
- def find_version(id, version)
329
- find_versions(id,
330
- :conditions => ["#{versioned_foreign_key} = ? AND version = ?", id, version],
331
- :limit => 1).first
294
+ # Reverts a model to a given version. Takes either a version number or an instance of the versioned model
295
+ def revert_to(version)
296
+ if version.is_a?(self.class.versioned_class)
297
+ return false unless version.send(self.class.versioned_foreign_key) == id and !version.new_record?
298
+ else
299
+ return false unless version = versions.where(self.class.version_column => version).first
300
+ end
301
+ self.clone_versioned_model(version, self)
302
+ send("#{self.class.version_column}=", version.send(self.class.version_column))
303
+ true
304
+ end
305
+
306
+ # Reverts a model to a given version and saves the model.
307
+ # Takes either a version number or an instance of the versioned model
308
+ def revert_to!(version)
309
+ revert_to(version) ? save_without_revision : false
310
+ end
311
+
312
+ # Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
313
+ def save_without_revision
314
+ save_without_revision!
315
+ true
316
+ rescue
317
+ false
318
+ end
319
+
320
+ def save_without_revision!
321
+ without_locking do
322
+ without_revision do
323
+ save!
324
+ end
325
+ end
326
+ end
327
+
328
+ def altered?
329
+ track_altered_attributes ? (version_if_changed - changed).length < version_if_changed.length : changed?
330
+ end
331
+
332
+ # Clones a model. Used when saving a new version or reverting a model's version.
333
+ def clone_versioned_model(orig_model, new_model)
334
+ self.class.versioned_columns.each do |col|
335
+ new_model.send("#{col.name}=", orig_model.send(col.name)) if orig_model.has_attribute?(col.name)
336
+ end
337
+
338
+ if orig_model.is_a?(self.class.versioned_class)
339
+ new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
340
+ elsif new_model.is_a?(self.class.versioned_class)
341
+ new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
342
+ end
343
+ end
344
+
345
+ # Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
346
+ def save_version?
347
+ version_condition_met? && altered?
348
+ end
349
+
350
+ # Checks condition set in the :if option to check whether a revision should be created or not. Override this for
351
+ # custom version condition checking.
352
+ def version_condition_met?
353
+ case
354
+ when version_condition.is_a?(Symbol)
355
+ send(version_condition)
356
+ when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
357
+ version_condition.call(self)
358
+ else
359
+ version_condition
360
+ end
361
+ end
362
+
363
+ # Executes the block with the versioning callbacks disabled.
364
+ #
365
+ # @foo.without_revision do
366
+ # @foo.save
367
+ # end
368
+ #
369
+ def without_revision(&block)
370
+ self.class.without_revision(&block)
371
+ end
372
+
373
+ # Turns off optimistic locking for the duration of the block
374
+ #
375
+ # @foo.without_locking do
376
+ # @foo.save
377
+ # end
378
+ #
379
+ def without_locking(&block)
380
+ self.class.without_locking(&block)
381
+ end
382
+
383
+ def empty_callback()
332
384
  end
333
-
334
- # Finds versions of a specific model. Takes an options hash like <tt>find</tt>
335
- def find_versions(id, options = {})
336
- versioned_class.find :all, {
337
- :conditions => ["#{versioned_foreign_key} = ?", id],
338
- :order => 'version' }.merge(options)
385
+
386
+ #:nodoc:
387
+
388
+ protected
389
+ # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
390
+ def set_new_version
391
+ @saving_version = new_record? || save_version?
392
+ self.send("#{self.class.version_column}=", next_version) if new_record? || (!locking_enabled? && save_version?)
339
393
  end
340
-
341
- # Returns an array of columns that are versioned. See non_versioned_fields
394
+
395
+ # Gets the next available version for the current record, or 1 for a new record
396
+ def next_version
397
+ (new_record? ? 0 : versions.calculate(:maximum, version_column).to_i) + 1
398
+ end
399
+ end
400
+
401
+ module ClassMethods
402
+ # Returns an array of columns that are versioned. See non_versioned_columns
342
403
  def versioned_columns
343
- self.columns.select { |c| !non_versioned_fields.include?(c.name) }
404
+ @versioned_columns ||= columns.select { |c| !non_versioned_columns.include?(c.name) }
344
405
  end
345
-
406
+
346
407
  # Returns an instance of the dynamic versioned model
347
408
  def versioned_class
348
- "ActiveRecord::Acts::Versioned::#{versioned_class_name}".constantize
409
+ const_get versioned_class_name
349
410
  end
350
-
351
- # An array of fields that are not saved in the versioned table
352
- def non_versioned_fields
353
- [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
354
- end
355
-
411
+
356
412
  # Rake migration task to create the versioned table using options passed to acts_as_versioned
357
413
  def create_versioned_table(create_table_options = {})
358
414
  # create version column in main table if it does not exist
359
- if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
360
- self.connection.add_column table_name, :version, :integer
415
+ if !self.content_columns.find { |c| [version_column.to_s, 'lock_version'].include? c.name }
416
+ self.connection.add_column table_name, version_column, :integer
417
+ self.reset_column_information
361
418
  end
362
-
419
+
420
+ return if connection.table_exists?(versioned_table_name)
421
+
363
422
  self.connection.create_table(versioned_table_name, create_table_options) do |t|
364
423
  t.column versioned_foreign_key, :integer
365
- t.column :version, :integer
424
+ t.column version_column, :integer
366
425
  end
367
-
368
- updated_col = nil
369
- self.versioned_columns.each do |col|
370
- updated_col = col if !updated_col and %(updated_at updated_on).include?(col.name)
371
- self.connection.add_column versioned_table_name, col.name, col.type,
372
- :limit => col.limit,
373
- :default => col.default
426
+
427
+ self.versioned_columns.each do |col|
428
+ self.connection.add_column versioned_table_name, col.name, col.type,
429
+ :limit => col.limit,
430
+ :default => col.default,
431
+ :scale => col.scale,
432
+ :precision => col.precision
374
433
  end
375
-
434
+
376
435
  if type_col = self.columns_hash[inheritance_column]
377
- self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
378
- :limit => type_col.limit,
379
- :default => type_col.default
380
- end
381
-
382
- if updated_col.nil?
383
- self.connection.add_column versioned_table_name, :updated_at, :timestamp
436
+ self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
437
+ :limit => type_col.limit,
438
+ :default => type_col.default,
439
+ :scale => type_col.scale,
440
+ :precision => type_col.precision
384
441
  end
442
+
443
+ self.connection.add_index versioned_table_name, versioned_foreign_key
385
444
  end
386
-
445
+
387
446
  # Rake migration task to drop the versioned table
388
447
  def drop_versioned_table
389
448
  self.connection.drop_table versioned_table_name
390
449
  end
450
+
451
+ # Executes the block with the versioning callbacks disabled.
452
+ #
453
+ # Foo.without_revision do
454
+ # @foo.save
455
+ # end
456
+ #
457
+ def without_revision(&block)
458
+ class_eval do
459
+ CALLBACKS.each do |attr_name|
460
+ alias_method "orig_#{attr_name}".to_sym, attr_name
461
+ alias_method attr_name, :empty_callback
462
+ end
463
+ end
464
+ block.call
465
+ ensure
466
+ class_eval do
467
+ CALLBACKS.each do |attr_name|
468
+ alias_method attr_name, "orig_#{attr_name}".to_sym
469
+ end
470
+ end
471
+ end
472
+
473
+ # Turns off optimistic locking for the duration of the block
474
+ #
475
+ # Foo.without_locking do
476
+ # @foo.save
477
+ # end
478
+ #
479
+ def without_locking(&block)
480
+ current = ActiveRecord::Base.lock_optimistically
481
+ ActiveRecord::Base.lock_optimistically = false if current
482
+ begin
483
+ block.call
484
+ ensure
485
+ ActiveRecord::Base.lock_optimistically = true if current
486
+ end
487
+ end
391
488
  end
392
489
  end
393
490
  end
394
491
  end
395
492
  end
396
493
 
397
- ActiveRecord::Base.class_eval { include ActiveRecord::Acts::Versioned }
494
+ ActiveRecord::Base.extend ActiveRecord::Acts::Versioned