acts_as_versioned_jw 3.2.3

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'http://rubygems.org'
2
+
3
+ group :development do
4
+ gem 'rails', '~> 3.1.0'
5
+ gem 'sqlite3'
6
+ gem 'echoe'
7
+ end
@@ -0,0 +1,101 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ actionmailer (3.1.4)
5
+ actionpack (= 3.1.4)
6
+ mail (~> 2.3.0)
7
+ actionpack (3.1.4)
8
+ activemodel (= 3.1.4)
9
+ activesupport (= 3.1.4)
10
+ builder (~> 3.0.0)
11
+ erubis (~> 2.7.0)
12
+ i18n (~> 0.6)
13
+ rack (~> 1.3.6)
14
+ rack-cache (~> 1.1)
15
+ rack-mount (~> 0.8.2)
16
+ rack-test (~> 0.6.1)
17
+ sprockets (~> 2.0.3)
18
+ activemodel (3.1.4)
19
+ activesupport (= 3.1.4)
20
+ builder (~> 3.0.0)
21
+ i18n (~> 0.6)
22
+ activerecord (3.1.4)
23
+ activemodel (= 3.1.4)
24
+ activesupport (= 3.1.4)
25
+ arel (~> 2.2.3)
26
+ tzinfo (~> 0.3.29)
27
+ activeresource (3.1.4)
28
+ activemodel (= 3.1.4)
29
+ activesupport (= 3.1.4)
30
+ activesupport (3.1.4)
31
+ multi_json (~> 1.0)
32
+ allison (2.0.3)
33
+ arel (2.2.3)
34
+ builder (3.0.0)
35
+ echoe (4.6.3)
36
+ allison (>= 2.0.3)
37
+ gemcutter (>= 0.7.0)
38
+ rake (>= 0.9.2)
39
+ rdoc (>= 3.6.1)
40
+ rubyforge (>= 2.0.4)
41
+ erubis (2.7.0)
42
+ gemcutter (0.7.1)
43
+ hike (1.2.1)
44
+ i18n (0.6.0)
45
+ json (1.6.6)
46
+ json_pure (1.7.7)
47
+ mail (2.3.3)
48
+ i18n (>= 0.4.0)
49
+ mime-types (~> 1.16)
50
+ treetop (~> 1.4.8)
51
+ mime-types (1.18)
52
+ multi_json (1.2.0)
53
+ polyglot (0.3.3)
54
+ rack (1.3.6)
55
+ rack-cache (1.2)
56
+ rack (>= 0.4)
57
+ rack-mount (0.8.3)
58
+ rack (>= 1.0.0)
59
+ rack-ssl (1.3.2)
60
+ rack
61
+ rack-test (0.6.1)
62
+ rack (>= 1.0)
63
+ rails (3.1.4)
64
+ actionmailer (= 3.1.4)
65
+ actionpack (= 3.1.4)
66
+ activerecord (= 3.1.4)
67
+ activeresource (= 3.1.4)
68
+ activesupport (= 3.1.4)
69
+ bundler (~> 1.0)
70
+ railties (= 3.1.4)
71
+ railties (3.1.4)
72
+ actionpack (= 3.1.4)
73
+ activesupport (= 3.1.4)
74
+ rack-ssl (~> 1.3.2)
75
+ rake (>= 0.8.7)
76
+ rdoc (~> 3.4)
77
+ thor (~> 0.14.6)
78
+ rake (0.9.2.2)
79
+ rdoc (3.12)
80
+ json (~> 1.4)
81
+ rubyforge (2.0.4)
82
+ json_pure (>= 1.1.7)
83
+ sprockets (2.0.3)
84
+ hike (~> 1.2)
85
+ rack (~> 1.0)
86
+ tilt (~> 1.1, != 1.3.0)
87
+ sqlite3 (1.3.5)
88
+ thor (0.14.6)
89
+ tilt (1.3.3)
90
+ treetop (1.4.10)
91
+ polyglot
92
+ polyglot (>= 0.3.1)
93
+ tzinfo (0.3.33)
94
+
95
+ PLATFORMS
96
+ ruby
97
+
98
+ DEPENDENCIES
99
+ echoe
100
+ rails (~> 3.1.0)
101
+ sqlite3
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2005 Rick Olson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,10 @@
1
+ Gemfile
2
+ Gemfile.lock
3
+ MIT-LICENSE
4
+ Manifest
5
+ README.md
6
+ RUNNING_UNIT_TESTS
7
+ Rakefile
8
+ acts_as_versioned_jw.gemspec
9
+ init.rb
10
+ lib/acts_as_versioned.rb
@@ -0,0 +1,120 @@
1
+ ## About ##
2
+ =====
3
+
4
+ acts_as_versioned is a gem for Rails 3.1, 3.2 & 4 to enable easy versioning of models. As a versioned model is updated revisions are kept in a seperate table, providing a record of what changed.
5
+
6
+ ## Getting Started ##
7
+ =====
8
+
9
+ In your Gemfile simply include:
10
+
11
+ gem 'acts_as_versioned_jw', '~> 3.2.2'
12
+
13
+ The next time you run `bundle install` you'll be all set to start using acts_as_versioned.
14
+
15
+ ## Usage ##
16
+ =====
17
+
18
+ #### Versioning a Model ####
19
+ By default acts_as_versioned is unobtrusive. You will need to explicitly state which models to version. To do so, add the line `acts_as_versioned` to your model, like so:
20
+
21
+ class MyModel < ActiveRecord::Base
22
+ acts_as_versioned
23
+ #...
24
+ end
25
+
26
+ Next we need to create a migration to setup our versioning tables:
27
+
28
+ bundle exec rails generate migration AddVersioningToMyModel
29
+
30
+ Once that is completed, edit the generated migration. acts_as_versioned patches your model to add a `create_versioned_table` and `drop_versioned_table` method. A migration for `MyModel` (assuming MyModel already existed) might look like:
31
+
32
+ class AddVersioningToMyModel < ActiveRecord::Migration
33
+ def self.up
34
+ MyModel.create_versioned_table
35
+ end
36
+
37
+ def self.down
38
+ MyModel.drop_versioned_table
39
+ end
40
+ end
41
+
42
+ Execute your migration:
43
+
44
+ bundle exec rake db:migrate
45
+
46
+ And you're finished! Without any addition work, `MyModel` is being versioned.
47
+
48
+ #### Excluding attributes from versioning ####
49
+
50
+ Sometime you want to exclude an attribute of a model from being versioned. That can be accomplished with the `:except` paramter to `acts_as_versioned`:
51
+
52
+ class MyMode < ActiveRecord::Base
53
+ acts_as_versioned :except => :some_attr_i_dont_want_versioned
54
+
55
+ end
56
+
57
+
58
+ #### Revisions ####
59
+
60
+ Recording a history of changes to a model is only useful if you can do something with that data. With acts_as_versioned there are several ways you can interact with a model's revisions.
61
+
62
+ ##### Version Number #####
63
+ To determine what the current version number for a model is:
64
+
65
+ model.version
66
+
67
+ The `version` attribute is available for both the actual model, and also any revisions of a model. Thusly, the following is valid:
68
+
69
+ model.versions.last.version
70
+
71
+ ##### Revisions List #####
72
+ As alluded to above, you can get an array of revisions of a model via the `versions` attribute:
73
+
74
+ model.versions
75
+
76
+ The returned objects are of a type `MyModel::Version` where `MyModel` is the model you are working with. These objects have identical fields to `MyModel`. So, if `MyModel` had a `name` attribute, you could also say:
77
+
78
+ model.versions.last.name
79
+
80
+ ##### Reverting to a Revision #####
81
+ To revert a model to an older revision, simply call `revert_to` with the version number you desire to rever to:
82
+
83
+ model.revert_to(version_number)
84
+
85
+ ##### Saving Without Revisions #####
86
+ Occasionally you might need to save a model without necessary creating revisions. To do so, use the `save_without_revision` method:
87
+
88
+ model.save_without_revision
89
+
90
+
91
+ #### Migrations ####
92
+ Adding a field to your model does not automatically add it to the versioning table. So, when you add new fields, be sure to add them to both:
93
+
94
+ class AddNewFieldToMyModel < ActiveRecord::Migration
95
+ def change
96
+ add_column :my_models, :new_field_, :string
97
+ add_column :my_model_versions, :new_field_, :string
98
+ end
99
+ end
100
+
101
+ #### Version Class ####
102
+ As has been stated, the versioned data is stored seperately from the main class. This also implies that `model.versions` returns an area of object of a class _other_ than `MyModel` (where `model` is an instance of `MyModel`, keeping with our working example). The instances returned are actually of type `MyModel::Version`. With this, comes the fact that any methods, associations, etc. defined on `MyModel` are not present on `MyModel::Version`.
103
+
104
+ While this sounds obvious, it can some times be unexpected. Especially when acts_as_versioned make it so easy to grab historical records from a live record. A common scenario where this can come up is associations.
105
+
106
+ Say `MyModel` belongs to `TheMan`. Also, assume that you want to find out where (in the past) a particular instance of `MyModel` was updated in regards to it's association to `TheMan`. You could write that as:
107
+
108
+ model.versions.keep_if { |m| m.the_man != current_man }.last
109
+
110
+ However, this will not work. This is because `MyModel::Version` does _not_ belong to `TheMan`. You could compare ids here, or you could patch `MyModel::Version` to belong to `TheMan` like:
111
+
112
+ class MyModel
113
+ acts_as_versioned
114
+ belongs_to :the_men
115
+ #some stuff
116
+
117
+ class Version
118
+ belongs_to :the_men
119
+ end
120
+ end
@@ -0,0 +1,41 @@
1
+ == Creating the test database
2
+
3
+ The default name for the test databases is "activerecord_versioned". If you
4
+ want to use another database name then be sure to update the connection
5
+ adapter setups you want to test with in test/connections/<your database>/connection.rb.
6
+ When you have the database online, you can import the fixture tables with
7
+ the test/fixtures/db_definitions/*.sql files.
8
+
9
+ Make sure that you create database objects with the same user that you specified in i
10
+ connection.rb otherwise (on Postgres, at least) tests for default values will fail.
11
+
12
+ == Running with Rake
13
+
14
+ The easiest way to run the unit tests is through Rake. The default task runs
15
+ the entire test suite for all the adapters. You can also run the suite on just
16
+ one adapter by using the tasks test_mysql_ruby, test_ruby_mysql, test_sqlite,
17
+ or test_postresql. For more information, checkout the full array of rake tasks with "rake -T"
18
+
19
+ Rake can be found at http://rake.rubyforge.org
20
+
21
+ == Running by hand
22
+
23
+ Unit tests are located in test directory. If you only want to run a single test suite,
24
+ or don't want to bother with Rake, you can do so with something like:
25
+
26
+ cd test; ruby -I "connections/native_mysql" base_test.rb
27
+
28
+ That'll run the base suite using the MySQL-Ruby adapter. Change the adapter
29
+ and test suite name as needed.
30
+
31
+ == Faster tests
32
+
33
+ If you are using a database that supports transactions, you can set the
34
+ "AR_TX_FIXTURES" environment variable to "yes" to use transactional fixtures.
35
+ This gives a very large speed boost. With rake:
36
+
37
+ rake AR_TX_FIXTURES=yes
38
+
39
+ Or, by hand:
40
+
41
+ AR_TX_FIXTURES=yes ruby -I connections/native_sqlite3 base_test.rb
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'echoe'
4
+
5
+ Echoe.new('acts_as_versioned_jw', '3.2.3') do |p|
6
+ p.description = "Active Record model versioning"
7
+ p.url = "http://github.com/jwhitehorn/acts_as_versioned"
8
+ p.author = "Jason Whitehorn"
9
+ p.email = "jason.whitehorn@gmail.com"
10
+ p.dependencies = ['activerecord']
11
+ end
12
+
13
+ Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "acts_as_versioned_jw"
5
+ s.version = "3.2.3"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Jason Whitehorn"]
9
+ s.date = "2014-04-25"
10
+ s.description = "Active Record model versioning"
11
+ s.email = "jason.whitehorn@gmail.com"
12
+ s.extra_rdoc_files = ["README.md", "lib/acts_as_versioned.rb"]
13
+ s.files = ["Gemfile", "Gemfile.lock", "MIT-LICENSE", "Manifest", "README.md", "RUNNING_UNIT_TESTS", "Rakefile", "acts_as_versioned_jw.gemspec", "init.rb", "lib/acts_as_versioned.rb"]
14
+ s.homepage = "http://github.com/jwhitehorn/acts_as_versioned"
15
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Acts_as_versioned_jw", "--main", "README.md"]
16
+ s.require_paths = ["lib"]
17
+ s.rubyforge_project = "acts_as_versioned_jw"
18
+ s.rubygems_version = "1.8.25"
19
+ s.summary = "Active Record model versioning"
20
+
21
+ if s.respond_to? :specification_version then
22
+ s.specification_version = 3
23
+
24
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
25
+ s.add_runtime_dependency(%q<activerecord>, [">= 0"])
26
+ else
27
+ s.add_dependency(%q<activerecord>, [">= 0"])
28
+ end
29
+ else
30
+ s.add_dependency(%q<activerecord>, [">= 0"])
31
+ end
32
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'acts_as_versioned'
@@ -0,0 +1,506 @@
1
+ # Copyright (c) 2005 Rick Olson
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.
21
+ require 'active_support/concern'
22
+
23
+ module ActiveRecord #:nodoc:
24
+ module Acts #:nodoc:
25
+ # Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
26
+ # versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
27
+ # column is present as well.
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
30
+ # your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
31
+ #
32
+ # class Page < ActiveRecord::Base
33
+ # # assumes pages_versions table
34
+ # acts_as_versioned
35
+ # end
36
+ #
37
+ # Example:
38
+ #
39
+ # page = Page.create(:title => 'hello world!')
40
+ # page.version # => 1
41
+ #
42
+ # page.title = 'hello world'
43
+ # page.save
44
+ # page.version # => 2
45
+ # page.versions.size # => 2
46
+ #
47
+ # page.revert_to(1) # using version number
48
+ # page.title # => 'hello world!'
49
+ #
50
+ # page.revert_to(page.versions.last) # using versioned instance
51
+ # page.title # => 'hello world'
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
+ #
68
+ # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
69
+ module Versioned
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, :version_except_columns
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.version_except_columns = [options[:except]].flatten.map(&:to_s) #these columns are kept in _versioned, but changing them does not excplitly cause a version change
180
+ self.non_versioned_columns = [self.primary_key, inheritance_column, self.version_column, 'lock_version', versioned_inheritance_column] #these columns are excluded from _versions, and changing them does not cause a version change
181
+ self.version_association_options = {
182
+ :class_name => "#{self.to_s}::#{versioned_class_name}",
183
+ :foreign_key => versioned_foreign_key,
184
+ :dependent => :delete_all
185
+ }.merge(options[:association_options] || {})
186
+
187
+ if block_given?
188
+ extension_module_name = "#{versioned_class_name}Extension"
189
+ silence_warnings do
190
+ self.const_set(extension_module_name, Module.new(&extension))
191
+ end
192
+
193
+ options[:extend] = self.const_get(extension_module_name)
194
+ end
195
+
196
+ unless options[:if_changed].nil?
197
+ self.track_altered_attributes = true
198
+ options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
199
+ self.version_if_changed = options[:if_changed].map(&:to_s)
200
+ end
201
+
202
+ include options[:extend] if options[:extend].is_a?(Module)
203
+
204
+ include ActiveRecord::Acts::Versioned::Behaviors
205
+
206
+ #
207
+ # Create the dynamic versioned model
208
+ #
209
+ const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
210
+ def self.reloadable?;
211
+ false;
212
+ end
213
+
214
+ # find first version before the given version
215
+ def self.before(version)
216
+ where(["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]).
217
+ order('version DESC').
218
+ first
219
+ end
220
+
221
+ # find first version after the given version.
222
+ def self.after(version)
223
+ where(["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]).
224
+ order('version ASC').
225
+ first
226
+ end
227
+
228
+ # finds earliest version of this record
229
+ def self.earliest
230
+ order("#{original_class.version_column}").first
231
+ end
232
+
233
+ # find latest version of this record
234
+ def self.latest
235
+ order("#{original_class.version_column} desc").first
236
+ end
237
+
238
+ def previous
239
+ self.class.before(self)
240
+ end
241
+
242
+ def next
243
+ self.class.after(self)
244
+ end
245
+
246
+ def versions_count
247
+ page.version
248
+ end
249
+ end
250
+
251
+ versioned_class.cattr_accessor :original_class
252
+ versioned_class.original_class = self
253
+ versioned_class.table_name = versioned_table_name
254
+ versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
255
+ :class_name => "::#{self.to_s}",
256
+ :foreign_key => versioned_foreign_key
257
+ versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
258
+ versioned_class.set_sequence_name version_sequence_name if version_sequence_name
259
+ end
260
+
261
+ module Behaviors
262
+ extend ActiveSupport::Concern
263
+
264
+ included do
265
+ has_many :versions, self.version_association_options
266
+
267
+ before_save :set_new_version
268
+ after_save :save_version
269
+ after_save :clear_old_versions
270
+ end
271
+
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
281
+ end
282
+ end
283
+
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]
291
+ end
292
+ end
293
+
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
+ changed.map { |c| self.class.versioned_columns.map(&:name).include?(c) & !self.class.version_except_columns.include?(c) }.any?
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
+ next unless orig_model.has_attribute?(col.name)
336
+ define_method(new_model, col.name.to_sym)
337
+ new_model.send("#{col.name.to_sym}=", orig_model.send(col.name))
338
+ end
339
+
340
+ if orig_model.is_a?(self.class.versioned_class)
341
+ new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
342
+ elsif new_model.is_a?(self.class.versioned_class)
343
+ sym = self.class.versioned_inheritance_column.to_sym
344
+ define_method new_model, sym
345
+ new_model.send("#{sym}=", orig_model[orig_model.class.inheritance_column]) if orig_model[orig_model.class.inheritance_column]
346
+ end
347
+ end
348
+
349
+ def define_method(object, method)
350
+ return if object.methods.include? method
351
+ metaclass = class << object; self; end
352
+ metaclass.send :attr_accessor, method
353
+ end
354
+
355
+ # Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
356
+ def save_version?
357
+ version_condition_met? && altered?
358
+ end
359
+
360
+ # Checks condition set in the :if option to check whether a revision should be created or not. Override this for
361
+ # custom version condition checking.
362
+ def version_condition_met?
363
+ case
364
+ when version_condition.is_a?(Symbol)
365
+ send(version_condition)
366
+ when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
367
+ version_condition.call(self)
368
+ else
369
+ version_condition
370
+ end
371
+ end
372
+
373
+ # Executes the block with the versioning callbacks disabled.
374
+ #
375
+ # @foo.without_revision do
376
+ # @foo.save
377
+ # end
378
+ #
379
+ def without_revision(&block)
380
+ self.class.without_revision(&block)
381
+ end
382
+
383
+ # Turns off optimistic locking for the duration of the block
384
+ #
385
+ # @foo.without_locking do
386
+ # @foo.save
387
+ # end
388
+ #
389
+ def without_locking(&block)
390
+ self.class.without_locking(&block)
391
+ end
392
+
393
+ def empty_callback()
394
+ end
395
+
396
+ #:nodoc:
397
+
398
+ protected
399
+ # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
400
+ def set_new_version
401
+ @saving_version = new_record? || save_version?
402
+ self.send("#{self.class.version_column}=", next_version) if new_record? || (!locking_enabled? && save_version?)
403
+ end
404
+
405
+ # Gets the next available version for the current record, or 1 for a new record
406
+ def next_version
407
+ (new_record? ? 0 : versions.calculate(:maximum, version_column).to_i) + 1
408
+ end
409
+
410
+
411
+ module ClassMethods
412
+ # Returns an array of columns that are versioned. See non_versioned_columns
413
+ def versioned_columns
414
+ @versioned_columns ||= columns.select { |c| !non_versioned_columns.include?(c.name) }
415
+ end
416
+
417
+ # Returns an instance of the dynamic versioned model
418
+ def versioned_class
419
+ const_get versioned_class_name
420
+ end
421
+
422
+ # Rake migration task to create the versioned table using options passed to acts_as_versioned
423
+ def create_versioned_table(create_table_options = {})
424
+ # create version column in main table if it does not exist
425
+ if !self.content_columns.find { |c| [version_column.to_s, 'lock_version'].include? c.name }
426
+ self.connection.add_column table_name, version_column, :integer
427
+ self.reset_column_information
428
+ end
429
+
430
+ return if connection.table_exists?(versioned_table_name)
431
+
432
+ self.connection.create_table(versioned_table_name, create_table_options) do |t|
433
+ t.column versioned_foreign_key, :integer
434
+ t.column version_column, :integer
435
+ end
436
+
437
+ self.versioned_columns.each do |col|
438
+ self.connection.add_column versioned_table_name, col.name, col.type,
439
+ :limit => col.limit,
440
+ :default => col.default,
441
+ :scale => col.scale,
442
+ :precision => col.precision
443
+ end
444
+
445
+ if type_col = self.columns_hash[inheritance_column]
446
+ self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
447
+ :limit => type_col.limit,
448
+ :default => type_col.default,
449
+ :scale => type_col.scale,
450
+ :precision => type_col.precision
451
+ end
452
+
453
+ # Limit index name length to 63, the Postgresql limit of NAMEDATALEN-1.
454
+ name = 'index_' + versioned_table_name + '_on_' + versioned_foreign_key
455
+ self.connection.add_index versioned_table_name, versioned_foreign_key, :name => name[0,63]
456
+ end
457
+
458
+ # Rake migration task to drop the versioned table
459
+ def drop_versioned_table
460
+ self.connection.drop_table versioned_table_name
461
+ end
462
+
463
+ # Executes the block with the versioning callbacks disabled.
464
+ #
465
+ # Foo.without_revision do
466
+ # @foo.save
467
+ # end
468
+ #
469
+ def without_revision(&block)
470
+ class_eval do
471
+ CALLBACKS.each do |attr_name|
472
+ alias_method "orig_#{attr_name}".to_sym, attr_name
473
+ alias_method attr_name, :empty_callback
474
+ end
475
+ end
476
+ block.call
477
+ ensure
478
+ class_eval do
479
+ CALLBACKS.each do |attr_name|
480
+ alias_method attr_name, "orig_#{attr_name}".to_sym
481
+ end
482
+ end
483
+ end
484
+
485
+ # Turns off optimistic locking for the duration of the block
486
+ #
487
+ # Foo.without_locking do
488
+ # @foo.save
489
+ # end
490
+ #
491
+ def without_locking(&block)
492
+ current = ActiveRecord::Base.lock_optimistically
493
+ ActiveRecord::Base.lock_optimistically = false if current
494
+ begin
495
+ block.call
496
+ ensure
497
+ ActiveRecord::Base.lock_optimistically = true if current
498
+ end
499
+ end
500
+ end
501
+ end
502
+ end
503
+ end
504
+ end
505
+
506
+ ActiveRecord::Base.extend ActiveRecord::Acts::Versioned
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acts_as_versioned_jw
3
+ version: !ruby/object:Gem::Version
4
+ version: 3.2.3
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jason Whitehorn
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-04-25 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ description: Active Record model versioning
31
+ email: jason.whitehorn@gmail.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files:
35
+ - README.md
36
+ - lib/acts_as_versioned.rb
37
+ files:
38
+ - Gemfile
39
+ - Gemfile.lock
40
+ - MIT-LICENSE
41
+ - Manifest
42
+ - README.md
43
+ - RUNNING_UNIT_TESTS
44
+ - Rakefile
45
+ - acts_as_versioned_jw.gemspec
46
+ - init.rb
47
+ - lib/acts_as_versioned.rb
48
+ homepage: http://github.com/jwhitehorn/acts_as_versioned
49
+ licenses: []
50
+ post_install_message:
51
+ rdoc_options:
52
+ - --line-numbers
53
+ - --inline-source
54
+ - --title
55
+ - Acts_as_versioned_jw
56
+ - --main
57
+ - README.md
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ! '>='
70
+ - !ruby/object:Gem::Version
71
+ version: '1.2'
72
+ requirements: []
73
+ rubyforge_project: acts_as_versioned_jw
74
+ rubygems_version: 1.8.25
75
+ signing_key:
76
+ specification_version: 3
77
+ summary: Active Record model versioning
78
+ test_files: []