acts_as_versioned 0.2.3 → 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/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