db_acts_as_versioned 3.3.11 → 3.3.12
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.
- checksums.yaml +4 -4
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +120 -0
- data/Rakefile +1 -0
- data/db_acts_as_versioned.gemspec +23 -0
- data/lib/db_acts_as_versioned.rb +507 -0
- data/lib/db_acts_as_versioned/version.rb +3 -0
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e8a609d29fc09e002abc5b348dd1dd462f46d92d
|
4
|
+
data.tar.gz: d022d7238d222bcd4a79d73298880376a36f77b6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d81fedfde69c5d2fa9346b6f82188ef3328c5574c30900094b2c8ca9d4dd91da02514b8fce3764cd33e2bc7bbfaa0de6eb6aea9f5414a5a3f228729ff7d8b3c0
|
7
|
+
data.tar.gz: bd38c435ccd1bff949d51bbc9f017e9bbdc02066b049162bd72a76f45ffdb3fe0ca3911f552a9a493770f956b1f4e51fc7e3145396d4f0638b6b79ebce3a1a0d
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Martin Sommer
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
## DbActsAsVersioned ##
|
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 'db_acts_as_versioned', '~> 3.3.9'
|
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
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'db_acts_as_versioned/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "db_acts_as_versioned"
|
8
|
+
spec.version = DbActsAsVersioned::VERSION
|
9
|
+
spec.authors = ["Sommer Systems, LLC"]
|
10
|
+
spec.email = ["sommerm@vmware.com"]
|
11
|
+
spec.summary = %q{Active Record model versioning}
|
12
|
+
spec.description = %q{Active Record model versioning, with adjustments made for Rails 4 and PostgreSQL.}
|
13
|
+
spec.homepage = "https://github.com/mjsommer/acts_as_versioned"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
end
|
@@ -0,0 +1,507 @@
|
|
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 "db_acts_as_versioned/version"
|
22
|
+
require 'active_support/concern'
|
23
|
+
|
24
|
+
module ActiveRecord #:nodoc:
|
25
|
+
module Acts #:nodoc:
|
26
|
+
# Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
|
27
|
+
# versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
|
28
|
+
# column is present as well.
|
29
|
+
#
|
30
|
+
# 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
|
31
|
+
# your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
|
32
|
+
#
|
33
|
+
# class Page < ActiveRecord::Base
|
34
|
+
# # assumes pages_versions table
|
35
|
+
# acts_as_versioned
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# Example:
|
39
|
+
#
|
40
|
+
# page = Page.create(:title => 'hello world!')
|
41
|
+
# page.version # => 1
|
42
|
+
#
|
43
|
+
# page.title = 'hello world'
|
44
|
+
# page.save
|
45
|
+
# page.version # => 2
|
46
|
+
# page.versions.size # => 2
|
47
|
+
#
|
48
|
+
# page.revert_to(1) # using version number
|
49
|
+
# page.title # => 'hello world!'
|
50
|
+
#
|
51
|
+
# page.revert_to(page.versions.last) # using versioned instance
|
52
|
+
# page.title # => 'hello world'
|
53
|
+
#
|
54
|
+
# page.versions.earliest # efficient query to find the first version
|
55
|
+
# page.versions.latest # efficient query to find the most recently created version
|
56
|
+
#
|
57
|
+
#
|
58
|
+
# Simple Queries to page between versions
|
59
|
+
#
|
60
|
+
# page.versions.before(version)
|
61
|
+
# page.versions.after(version)
|
62
|
+
#
|
63
|
+
# Access the previous/next versions from the versioned model itself
|
64
|
+
#
|
65
|
+
# version = page.versions.latest
|
66
|
+
# version.previous # go back one version
|
67
|
+
# version.next # go forward one version
|
68
|
+
#
|
69
|
+
# See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
|
70
|
+
module Versioned
|
71
|
+
VERSION = "0.6.0"
|
72
|
+
CALLBACKS = [:set_new_version, :save_version, :save_version?]
|
73
|
+
|
74
|
+
# == Configuration options
|
75
|
+
#
|
76
|
+
# * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
|
77
|
+
# * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
|
78
|
+
# * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
|
79
|
+
# * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
|
80
|
+
# * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
|
81
|
+
# * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
|
82
|
+
# * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
|
83
|
+
# * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
|
84
|
+
# For finer control, pass either a Proc or modify Model#version_condition_met?
|
85
|
+
#
|
86
|
+
# acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
|
87
|
+
#
|
88
|
+
# or...
|
89
|
+
#
|
90
|
+
# class Auction
|
91
|
+
# def version_condition_met? # totally bypasses the <tt>:if</tt> option
|
92
|
+
# !expired?
|
93
|
+
# end
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
|
97
|
+
# either a symbol or array of symbols.
|
98
|
+
#
|
99
|
+
# * <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
|
100
|
+
# to create an anonymous mixin:
|
101
|
+
#
|
102
|
+
# class Auction
|
103
|
+
# acts_as_versioned do
|
104
|
+
# def started?
|
105
|
+
# !started_at.nil?
|
106
|
+
# end
|
107
|
+
# end
|
108
|
+
# end
|
109
|
+
#
|
110
|
+
# or...
|
111
|
+
#
|
112
|
+
# module AuctionExtension
|
113
|
+
# def started?
|
114
|
+
# !started_at.nil?
|
115
|
+
# end
|
116
|
+
# end
|
117
|
+
# class Auction
|
118
|
+
# acts_as_versioned :extend => AuctionExtension
|
119
|
+
# end
|
120
|
+
#
|
121
|
+
# Example code:
|
122
|
+
#
|
123
|
+
# @auction = Auction.find(1)
|
124
|
+
# @auction.started?
|
125
|
+
# @auction.versions.first.started?
|
126
|
+
#
|
127
|
+
# == Database Schema
|
128
|
+
#
|
129
|
+
# The model that you're versioning needs to have a 'version' attribute. The model is versioned
|
130
|
+
# into a table called #{model}_versions where the model name is singlular. The _versions table should
|
131
|
+
# contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
|
132
|
+
#
|
133
|
+
# A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
|
134
|
+
# then that field is reflected in the versioned model as 'versioned_type' by default.
|
135
|
+
#
|
136
|
+
# Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
|
137
|
+
# method, perfect for a migration. It will also create the version column if the main model does not already have it.
|
138
|
+
#
|
139
|
+
# class AddVersions < ActiveRecord::Migration
|
140
|
+
# def self.up
|
141
|
+
# # create_versioned_table takes the same options hash
|
142
|
+
# # that create_table does
|
143
|
+
# Post.create_versioned_table
|
144
|
+
# end
|
145
|
+
#
|
146
|
+
# def self.down
|
147
|
+
# Post.drop_versioned_table
|
148
|
+
# end
|
149
|
+
# end
|
150
|
+
#
|
151
|
+
# == Changing What Fields Are Versioned
|
152
|
+
#
|
153
|
+
# By default, acts_as_versioned will version all but these fields:
|
154
|
+
#
|
155
|
+
# [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
|
156
|
+
#
|
157
|
+
# You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
|
158
|
+
#
|
159
|
+
# class Post < ActiveRecord::Base
|
160
|
+
# acts_as_versioned
|
161
|
+
# self.non_versioned_columns << 'comments_count'
|
162
|
+
# end
|
163
|
+
#
|
164
|
+
def db_acts_as_versioned(options = {}, &extension)
|
165
|
+
# don't allow multiple calls
|
166
|
+
return if self.included_modules.include?(ActiveRecord::Acts::Versioned::Behaviors)
|
167
|
+
|
168
|
+
cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
|
169
|
+
:version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
|
170
|
+
:version_association_options, :version_if_changed, :version_except_columns
|
171
|
+
|
172
|
+
self.versioned_class_name = options[:class_name] || "Version"
|
173
|
+
self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
|
174
|
+
self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
|
175
|
+
self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
|
176
|
+
self.version_column = options[:version_column] || 'version'
|
177
|
+
self.version_sequence_name = options[:sequence_name]
|
178
|
+
self.max_version_limit = options[:limit].to_i
|
179
|
+
self.version_condition = options[:if] || true
|
180
|
+
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
|
181
|
+
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
|
182
|
+
self.version_association_options = {
|
183
|
+
:class_name => "#{self.to_s}::#{versioned_class_name}",
|
184
|
+
:foreign_key => versioned_foreign_key,
|
185
|
+
:dependent => :delete_all
|
186
|
+
}.merge(options[:association_options] || {})
|
187
|
+
|
188
|
+
if block_given?
|
189
|
+
extension_module_name = "#{versioned_class_name}Extension"
|
190
|
+
silence_warnings do
|
191
|
+
self.const_set(extension_module_name, Module.new(&extension))
|
192
|
+
end
|
193
|
+
|
194
|
+
options[:extend] = self.const_get(extension_module_name)
|
195
|
+
end
|
196
|
+
|
197
|
+
unless options[:if_changed].nil?
|
198
|
+
self.track_altered_attributes = true
|
199
|
+
options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
|
200
|
+
self.version_if_changed = options[:if_changed].map(&:to_s)
|
201
|
+
end
|
202
|
+
|
203
|
+
include options[:extend] if options[:extend].is_a?(Module)
|
204
|
+
|
205
|
+
include ActiveRecord::Acts::Versioned::Behaviors
|
206
|
+
|
207
|
+
#
|
208
|
+
# Create the dynamic versioned model
|
209
|
+
#
|
210
|
+
const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
|
211
|
+
def self.reloadable?;
|
212
|
+
false;
|
213
|
+
end
|
214
|
+
|
215
|
+
# find first version before the given version
|
216
|
+
def self.before(version)
|
217
|
+
where(["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]).
|
218
|
+
order('version DESC').
|
219
|
+
first
|
220
|
+
end
|
221
|
+
|
222
|
+
# find first version after the given version.
|
223
|
+
def self.after(version)
|
224
|
+
where(["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]).
|
225
|
+
order('version ASC').
|
226
|
+
first
|
227
|
+
end
|
228
|
+
|
229
|
+
# finds earliest version of this record
|
230
|
+
def self.earliest
|
231
|
+
order("#{original_class.version_column}").first
|
232
|
+
end
|
233
|
+
|
234
|
+
# find latest version of this record
|
235
|
+
def self.latest
|
236
|
+
order("#{original_class.version_column} desc").first
|
237
|
+
end
|
238
|
+
|
239
|
+
def previous
|
240
|
+
self.class.before(self)
|
241
|
+
end
|
242
|
+
|
243
|
+
def next
|
244
|
+
self.class.after(self)
|
245
|
+
end
|
246
|
+
|
247
|
+
def versions_count
|
248
|
+
page.version
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
versioned_class.cattr_accessor :original_class
|
253
|
+
versioned_class.original_class = self
|
254
|
+
versioned_class.table_name = versioned_table_name
|
255
|
+
versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
|
256
|
+
:class_name => "::#{self.to_s}",
|
257
|
+
:foreign_key => versioned_foreign_key
|
258
|
+
versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
|
259
|
+
versioned_class.set_sequence_name version_sequence_name if version_sequence_name
|
260
|
+
end
|
261
|
+
|
262
|
+
module Behaviors
|
263
|
+
extend ActiveSupport::Concern
|
264
|
+
|
265
|
+
included do
|
266
|
+
has_many :versions, self.version_association_options
|
267
|
+
|
268
|
+
before_save :set_new_version
|
269
|
+
after_save :save_version
|
270
|
+
after_save :clear_old_versions
|
271
|
+
end
|
272
|
+
|
273
|
+
# Saves a version of the model in the versioned table. This is called in the after_save callback by default
|
274
|
+
def save_version
|
275
|
+
if @saving_version
|
276
|
+
@saving_version = nil
|
277
|
+
rev = self.class.versioned_class.create
|
278
|
+
clone_versioned_model(self, rev)
|
279
|
+
rev.send("#{self.class.version_column}=", send(self.class.version_column))
|
280
|
+
rev.send("#{self.class.versioned_foreign_key}=", id)
|
281
|
+
rev.save
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
# Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
|
286
|
+
# Override this method to set your own criteria for clearing old versions.
|
287
|
+
def clear_old_versions
|
288
|
+
return if self.class.max_version_limit == 0
|
289
|
+
excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
|
290
|
+
if excess_baggage > 0
|
291
|
+
self.class.versioned_class.delete_all ["#{self.class.version_column} <= ? and #{self.class.versioned_foreign_key} = ?", excess_baggage, id]
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Reverts a model to a given version. Takes either a version number or an instance of the versioned model
|
296
|
+
def revert_to(version)
|
297
|
+
if version.is_a?(self.class.versioned_class)
|
298
|
+
return false unless version.send(self.class.versioned_foreign_key) == id and !version.new_record?
|
299
|
+
else
|
300
|
+
return false unless version = versions.where(self.class.version_column => version).first
|
301
|
+
end
|
302
|
+
self.clone_versioned_model(version, self)
|
303
|
+
send("#{self.class.version_column}=", version.send(self.class.version_column))
|
304
|
+
true
|
305
|
+
end
|
306
|
+
|
307
|
+
# Reverts a model to a given version and saves the model.
|
308
|
+
# Takes either a version number or an instance of the versioned model
|
309
|
+
def revert_to!(version)
|
310
|
+
revert_to(version) ? save_without_revision : false
|
311
|
+
end
|
312
|
+
|
313
|
+
# Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
|
314
|
+
def save_without_revision
|
315
|
+
save_without_revision!
|
316
|
+
true
|
317
|
+
rescue
|
318
|
+
false
|
319
|
+
end
|
320
|
+
|
321
|
+
def save_without_revision!
|
322
|
+
without_locking do
|
323
|
+
without_revision do
|
324
|
+
save!
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
329
|
+
def altered?
|
330
|
+
changed.map { |c| self.class.versioned_columns.map(&:name).include?(c) & !self.class.version_except_columns.include?(c) }.any?
|
331
|
+
end
|
332
|
+
|
333
|
+
# Clones a model. Used when saving a new version or reverting a model's version.
|
334
|
+
def clone_versioned_model(orig_model, new_model)
|
335
|
+
self.class.versioned_columns.each do |col|
|
336
|
+
next unless orig_model.has_attribute?(col.name)
|
337
|
+
define_method(new_model, col.name.to_sym)
|
338
|
+
new_model.send("#{col.name.to_sym}=", orig_model.send(col.name))
|
339
|
+
end
|
340
|
+
|
341
|
+
if orig_model.is_a?(self.class.versioned_class)
|
342
|
+
new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
|
343
|
+
elsif new_model.is_a?(self.class.versioned_class)
|
344
|
+
sym = self.class.versioned_inheritance_column.to_sym
|
345
|
+
define_method new_model, sym
|
346
|
+
new_model.send("#{sym}=", orig_model[orig_model.class.inheritance_column]) if orig_model[orig_model.class.inheritance_column]
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
def define_method(object, method)
|
351
|
+
return if object.methods.include? method
|
352
|
+
metaclass = class << object; self; end
|
353
|
+
metaclass.send :attr_accessor, method
|
354
|
+
end
|
355
|
+
|
356
|
+
# Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
|
357
|
+
def save_version?
|
358
|
+
version_condition_met? && altered?
|
359
|
+
end
|
360
|
+
|
361
|
+
# Checks condition set in the :if option to check whether a revision should be created or not. Override this for
|
362
|
+
# custom version condition checking.
|
363
|
+
def version_condition_met?
|
364
|
+
case
|
365
|
+
when version_condition.is_a?(Symbol)
|
366
|
+
send(version_condition)
|
367
|
+
when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
|
368
|
+
version_condition.call(self)
|
369
|
+
else
|
370
|
+
version_condition
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
# Executes the block with the versioning callbacks disabled.
|
375
|
+
#
|
376
|
+
# @foo.without_revision do
|
377
|
+
# @foo.save
|
378
|
+
# end
|
379
|
+
#
|
380
|
+
def without_revision(&block)
|
381
|
+
self.class.without_revision(&block)
|
382
|
+
end
|
383
|
+
|
384
|
+
# Turns off optimistic locking for the duration of the block
|
385
|
+
#
|
386
|
+
# @foo.without_locking do
|
387
|
+
# @foo.save
|
388
|
+
# end
|
389
|
+
#
|
390
|
+
def without_locking(&block)
|
391
|
+
self.class.without_locking(&block)
|
392
|
+
end
|
393
|
+
|
394
|
+
def empty_callback()
|
395
|
+
end
|
396
|
+
|
397
|
+
#:nodoc:
|
398
|
+
|
399
|
+
protected
|
400
|
+
# sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
|
401
|
+
def set_new_version
|
402
|
+
@saving_version = new_record? || save_version?
|
403
|
+
self.send("#{self.class.version_column}=", next_version) if new_record? || (!locking_enabled? && save_version?)
|
404
|
+
end
|
405
|
+
|
406
|
+
# Gets the next available version for the current record, or 1 for a new record
|
407
|
+
def next_version
|
408
|
+
(new_record? ? 0 : versions.calculate(:maximum, version_column).to_i) + 1
|
409
|
+
end
|
410
|
+
|
411
|
+
|
412
|
+
module ClassMethods
|
413
|
+
# Returns an array of columns that are versioned. See non_versioned_columns
|
414
|
+
def versioned_columns
|
415
|
+
@versioned_columns ||= columns.select { |c| !non_versioned_columns.include?(c.name) }
|
416
|
+
end
|
417
|
+
|
418
|
+
# Returns an instance of the dynamic versioned model
|
419
|
+
def versioned_class
|
420
|
+
const_get versioned_class_name
|
421
|
+
end
|
422
|
+
|
423
|
+
# Rake migration task to create the versioned table using options passed to acts_as_versioned
|
424
|
+
def create_versioned_table(create_table_options = {})
|
425
|
+
# create version column in main table if it does not exist
|
426
|
+
if !self.content_columns.find { |c| [version_column.to_s, 'lock_version'].include? c.name }
|
427
|
+
self.connection.add_column table_name, version_column, :integer
|
428
|
+
self.reset_column_information
|
429
|
+
end
|
430
|
+
|
431
|
+
return if connection.table_exists?(versioned_table_name)
|
432
|
+
|
433
|
+
self.connection.create_table(versioned_table_name, create_table_options) do |t|
|
434
|
+
t.column versioned_foreign_key, :integer
|
435
|
+
t.column version_column, :integer
|
436
|
+
end
|
437
|
+
|
438
|
+
self.versioned_columns.each do |col|
|
439
|
+
self.connection.add_column versioned_table_name, col.name, col.type,
|
440
|
+
:limit => col.limit,
|
441
|
+
:default => col.default,
|
442
|
+
:scale => col.scale,
|
443
|
+
:precision => col.precision
|
444
|
+
end
|
445
|
+
|
446
|
+
if type_col = self.columns_hash[inheritance_column]
|
447
|
+
self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
|
448
|
+
:limit => type_col.limit,
|
449
|
+
:default => type_col.default,
|
450
|
+
:scale => type_col.scale,
|
451
|
+
:precision => type_col.precision
|
452
|
+
end
|
453
|
+
|
454
|
+
# Limit index name length to 63, the Postgresql limit of NAMEDATALEN-1.
|
455
|
+
name = 'index_' + versioned_table_name + '_on_' + versioned_foreign_key
|
456
|
+
self.connection.add_index versioned_table_name, versioned_foreign_key, :name => name[0,63]
|
457
|
+
end
|
458
|
+
|
459
|
+
# Rake migration task to drop the versioned table
|
460
|
+
def drop_versioned_table
|
461
|
+
self.connection.drop_table versioned_table_name
|
462
|
+
end
|
463
|
+
|
464
|
+
# Executes the block with the versioning callbacks disabled.
|
465
|
+
#
|
466
|
+
# Foo.without_revision do
|
467
|
+
# @foo.save
|
468
|
+
# end
|
469
|
+
#
|
470
|
+
def without_revision(&block)
|
471
|
+
class_eval do
|
472
|
+
CALLBACKS.each do |attr_name|
|
473
|
+
alias_method "orig_#{attr_name}".to_sym, attr_name
|
474
|
+
alias_method attr_name, :empty_callback
|
475
|
+
end
|
476
|
+
end
|
477
|
+
block.call
|
478
|
+
ensure
|
479
|
+
class_eval do
|
480
|
+
CALLBACKS.each do |attr_name|
|
481
|
+
alias_method attr_name, "orig_#{attr_name}".to_sym
|
482
|
+
end
|
483
|
+
end
|
484
|
+
end
|
485
|
+
|
486
|
+
# Turns off optimistic locking for the duration of the block
|
487
|
+
#
|
488
|
+
# Foo.without_locking do
|
489
|
+
# @foo.save
|
490
|
+
# end
|
491
|
+
#
|
492
|
+
def without_locking(&block)
|
493
|
+
current = ActiveRecord::Base.lock_optimistically
|
494
|
+
ActiveRecord::Base.lock_optimistically = false if current
|
495
|
+
begin
|
496
|
+
block.call
|
497
|
+
ensure
|
498
|
+
ActiveRecord::Base.lock_optimistically = true if current
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|
502
|
+
end
|
503
|
+
end
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
ActiveRecord::Base.extend ActiveRecord::Acts::Versioned
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: db_acts_as_versioned
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.3.
|
4
|
+
version: 3.3.12
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sommer Systems, LLC
|
@@ -45,7 +45,15 @@ email:
|
|
45
45
|
executables: []
|
46
46
|
extensions: []
|
47
47
|
extra_rdoc_files: []
|
48
|
-
files:
|
48
|
+
files:
|
49
|
+
- .gitignore
|
50
|
+
- Gemfile
|
51
|
+
- LICENSE.txt
|
52
|
+
- README.md
|
53
|
+
- Rakefile
|
54
|
+
- db_acts_as_versioned.gemspec
|
55
|
+
- lib/db_acts_as_versioned.rb
|
56
|
+
- lib/db_acts_as_versioned/version.rb
|
49
57
|
homepage: https://github.com/mjsommer/acts_as_versioned
|
50
58
|
licenses:
|
51
59
|
- MIT
|