pjb3-version_fu 1.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/MIT-LICENSE +24 -0
- data/README.rdoc +131 -0
- data/lib/version_fu.rb +128 -0
- data/rails/init.rb +1 -0
- data/version_fu.gemspec +24 -0
- metadata +64 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
== version_fu
|
2
|
+
Copyright (c) 2008 Jordan McKible
|
3
|
+
|
4
|
+
== acts_as_versioned
|
5
|
+
Copyright (c) 2005 Rick Olson
|
6
|
+
====================================================================
|
7
|
+
|
8
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
9
|
+
this software and associated documentation files (the "Software"), to deal in
|
10
|
+
the Software without restriction, including without limitation the rights to
|
11
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
12
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
13
|
+
so, subject to the following conditions:
|
14
|
+
|
15
|
+
The above copyright notice and this permission notice shall be included in all
|
16
|
+
copies or substantial portions of the Software.
|
17
|
+
|
18
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
19
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
20
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
21
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
22
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
23
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
24
|
+
SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
= version_fu
|
2
|
+
|
3
|
+
version_fu is a ActveRecord versioning plugin that takes advantage of the new dirty attribute checking available in Rails 2.1. Previous solutions like Rick Olson's acts_as_versioned are no long compatible with Rails.
|
4
|
+
|
5
|
+
|
6
|
+
== Installation
|
7
|
+
|
8
|
+
./script/plugin install git://github.com/jmckible/version_fu.git
|
9
|
+
|
10
|
+
|
11
|
+
== Usage
|
12
|
+
|
13
|
+
Let's say I have a pages table:
|
14
|
+
|
15
|
+
class Page < ActiveRecord::Base
|
16
|
+
# attributes: id, type, title, body, created_at, updated_at, creator_id, author_id
|
17
|
+
end
|
18
|
+
|
19
|
+
I want to track any changes made. First step will be to make a new page_versions table:
|
20
|
+
|
21
|
+
class CreatePageVersions < ActiveRecord::Migration
|
22
|
+
def self.up
|
23
|
+
create_table :page_versions do |t|
|
24
|
+
t.integer :page_id, :version, :author_id
|
25
|
+
t.string :title
|
26
|
+
t.text :body
|
27
|
+
t.timestamps
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.down
|
32
|
+
drop_table :page_versions
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
In this case, the author_id column represents the last person to edit the page. We want to track this attribute with every version. However, the creator_id is the person who created the page. The will never change, so it's not part of the versioned table.
|
37
|
+
|
38
|
+
To look at previous versions:
|
39
|
+
|
40
|
+
Page.first.versions
|
41
|
+
|
42
|
+
Which will give you all of the previous versions, which are Page::Version objects. If you want a specific version:
|
43
|
+
|
44
|
+
Page.first.find_version(2)
|
45
|
+
|
46
|
+
Which will give you the Page::Version object that represents the page as of version 2. If you need a read-only Page object that reflects the state of the object at the given version:
|
47
|
+
|
48
|
+
Page.first.as_of_version(2)
|
49
|
+
|
50
|
+
If you need to revert to an older version of the object:
|
51
|
+
|
52
|
+
Page.first.revert
|
53
|
+
|
54
|
+
Which reverts to the previous version, or:
|
55
|
+
|
56
|
+
Page.first.revert_to(2)
|
57
|
+
|
58
|
+
Which reverts to a specific version.
|
59
|
+
|
60
|
+
Don't forget to add a version column to your pages table. Have it default to 1 just to be safe (although the plugin should account for this):
|
61
|
+
|
62
|
+
class AddVersionToPages < ActiveRecord::Migration
|
63
|
+
def self.up
|
64
|
+
add_column :pages, :version, :integer, :default=>1
|
65
|
+
end
|
66
|
+
def self.down
|
67
|
+
remove_column :pages, :version
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
Of course if you're adding this plugin to a table with existing data, you'll probably want to instantiate some initial versions to start with.
|
72
|
+
|
73
|
+
Alright, so now that the database tables are in place, we can fire up version_fu. It's quite simple:
|
74
|
+
|
75
|
+
class Page < ActiveRecord::Base
|
76
|
+
version_fu
|
77
|
+
end
|
78
|
+
|
79
|
+
Thats it.
|
80
|
+
|
81
|
+
|
82
|
+
== Configuration
|
83
|
+
|
84
|
+
You can pass a few configuration options if need be. If you stick with the defaults above, you can skip all this.
|
85
|
+
|
86
|
+
class Page < ActiveRecord::Base
|
87
|
+
version_fu :class_name=>'Version', :foreign_key=>'page_id', :table_name=>'page_versions', :version_column=>'version'
|
88
|
+
end
|
89
|
+
|
90
|
+
* :class_name - The name of the versioned class. It will be a submodule of the versioning class - e.g. Page::Version
|
91
|
+
|
92
|
+
* :foreign_key - The column in the versioned table associated with the versioning class
|
93
|
+
|
94
|
+
* :table_name - The name of the versioned table
|
95
|
+
|
96
|
+
* :version_column - The name of the version column
|
97
|
+
|
98
|
+
|
99
|
+
== Extensions
|
100
|
+
|
101
|
+
Now that you've got some versions, it would be nice to use ActiveRecord associations on it. For example, Page.first.versions.latest.author wouldn't currently work because the Page::Version class doesn't know about the author method. The version_fu call does all you to pass a block which is executed by the versioned class. There is just one gotcha for associations:
|
102
|
+
|
103
|
+
class Page < ActiveRecord::Base
|
104
|
+
version_fu do
|
105
|
+
belongs_to :author, :class_name=>'::Author'
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
Don't forget the class name, or you'll get a warning
|
110
|
+
|
111
|
+
== When to Version
|
112
|
+
|
113
|
+
By default a new version will be saved whenever a versioned column is changed. However, you can control this at a more fine grained level. Just override the create_new_version? method. For example, let's say you only want to save a new version if both the page title and body changed. Taking advantage of the dirty attribute methods, you could do something like this:
|
114
|
+
|
115
|
+
class Page < ActiveRecord::Base
|
116
|
+
version_fu do
|
117
|
+
belongs_to :author, :class_name=>'::Author'
|
118
|
+
end
|
119
|
+
def create_new_version?
|
120
|
+
title_changed? && body_changed?
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
== Author
|
126
|
+
|
127
|
+
* version_fu was created by Jordan McKible http://jordan.mckible.com
|
128
|
+
|
129
|
+
* Available on GitHub at http://github.com/jmckible/version_fu/tree/master
|
130
|
+
|
131
|
+
* acts_as_versioned by Rick Olson http://github.com/technoweenie/acts_as_versioned/tree/master
|
data/lib/version_fu.rb
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
module VersionFu
|
2
|
+
def self.included(base)
|
3
|
+
base.extend ClassMethods
|
4
|
+
end
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def version_fu(options={}, &block)
|
8
|
+
return if self.included_modules.include? VersionFu::InstanceMethods
|
9
|
+
__send__ :include, VersionFu::InstanceMethods
|
10
|
+
|
11
|
+
cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name,
|
12
|
+
:version_column, :versioned_columns
|
13
|
+
|
14
|
+
self.versioned_class_name = options[:class_name] || 'Version'
|
15
|
+
self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
|
16
|
+
self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
|
17
|
+
self.version_column = options[:version_column] || 'version'
|
18
|
+
|
19
|
+
# Setup versions association
|
20
|
+
class_eval do
|
21
|
+
has_many :versions, :class_name => "#{self.to_s}::#{versioned_class_name}",
|
22
|
+
:foreign_key => versioned_foreign_key,
|
23
|
+
:dependent => :destroy do
|
24
|
+
def latest
|
25
|
+
find :first, :order=>'version desc'
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
before_save :check_for_new_version
|
30
|
+
end
|
31
|
+
|
32
|
+
# Versioned Model
|
33
|
+
const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
|
34
|
+
# find first version before the given version
|
35
|
+
def self.before(version)
|
36
|
+
find :first, :order => 'version desc',
|
37
|
+
:conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]
|
38
|
+
end
|
39
|
+
|
40
|
+
# find first version after the given version.
|
41
|
+
def self.after(version)
|
42
|
+
find :first, :order => 'version',
|
43
|
+
:conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]
|
44
|
+
end
|
45
|
+
|
46
|
+
def previous
|
47
|
+
self.class.before(self)
|
48
|
+
end
|
49
|
+
|
50
|
+
def next
|
51
|
+
self.class.after(self)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Housekeeping on versioned class
|
56
|
+
versioned_class.cattr_accessor :original_class
|
57
|
+
versioned_class.original_class = self
|
58
|
+
versioned_class.set_table_name versioned_table_name
|
59
|
+
|
60
|
+
# Version parent association
|
61
|
+
versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
|
62
|
+
:class_name => "::#{self.to_s}",
|
63
|
+
:foreign_key => versioned_foreign_key
|
64
|
+
|
65
|
+
# Block extension
|
66
|
+
versioned_class.class_eval &block if block_given?
|
67
|
+
|
68
|
+
# Finally setup which columns to version
|
69
|
+
self.versioned_columns = versioned_class.new.attributes.keys -
|
70
|
+
[versioned_class.primary_key, versioned_foreign_key, version_column, 'created_at', 'updated_at']
|
71
|
+
end
|
72
|
+
|
73
|
+
def versioned_class
|
74
|
+
const_get versioned_class_name
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
module InstanceMethods
|
80
|
+
def find_version(number)
|
81
|
+
versions.find :first, :conditions=>{:version=>number}
|
82
|
+
end
|
83
|
+
|
84
|
+
def check_for_new_version
|
85
|
+
instatiate_revision if create_new_version?
|
86
|
+
true # Never halt save
|
87
|
+
end
|
88
|
+
|
89
|
+
# This the method to override if you want to have more control over when to version
|
90
|
+
def create_new_version?
|
91
|
+
# Any versioned column changed?
|
92
|
+
self.class.versioned_columns.detect {|a| __send__ "#{a}_changed?"}
|
93
|
+
end
|
94
|
+
|
95
|
+
def instatiate_revision
|
96
|
+
new_version = versions.build
|
97
|
+
versioned_columns.each do |attribute|
|
98
|
+
new_version.__send__ "#{attribute}=", __send__(attribute)
|
99
|
+
end
|
100
|
+
version_number = new_record? ? 1 : version + 1
|
101
|
+
new_version.version = version_number
|
102
|
+
self.version = version_number
|
103
|
+
end
|
104
|
+
|
105
|
+
def revert
|
106
|
+
revert_to(version-1) unless version == 1
|
107
|
+
end
|
108
|
+
|
109
|
+
def revert_to(version)
|
110
|
+
revert_to_version = find_version(version)
|
111
|
+
versioned_columns.each do |a|
|
112
|
+
send("#{a}=", revert_to_version.send(a))
|
113
|
+
end
|
114
|
+
save
|
115
|
+
end
|
116
|
+
|
117
|
+
def as_of_version(version)
|
118
|
+
v = find_version(version)
|
119
|
+
obj = self.class.new
|
120
|
+
(versioned_columns + [:version, :updated_at]).each do |a|
|
121
|
+
obj.send("#{a}=", v.send(a))
|
122
|
+
end
|
123
|
+
obj.id = id
|
124
|
+
obj.freeze
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ActiveRecord::Base.send(:include, VersionFu)
|
data/version_fu.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = %q{version_fu}
|
3
|
+
s.version = "1.0"
|
4
|
+
|
5
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
6
|
+
s.authors = ["Jordan McKible", "Paul Barry"]
|
7
|
+
s.date = %q{2008-09-12}
|
8
|
+
s.description = %q{Dirty ActiveRecord Versioning (update of acts_as_versioned)}
|
9
|
+
s.email = %q{mail (at) paulbarry (dot) com}
|
10
|
+
s.extra_rdoc_files = ["lib/version_fu.rb", "README.rdoc"]
|
11
|
+
s.files = ["version_fu.gemspec", "lib/version_fu.rb", "rails/init.rb", "README.rdoc", "MIT-LICENSE"]
|
12
|
+
s.has_rdoc = true
|
13
|
+
s.homepage = %q{http://tuples.us/2008/05/03/lazily-announcing-version_fu/}
|
14
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Version Fu", "--main", "README.rdoc"]
|
15
|
+
s.require_paths = ["lib"]
|
16
|
+
s.rubyforge_project = %q{version_du}
|
17
|
+
s.rubygems_version = %q{1.2.0}
|
18
|
+
s.summary = %q{Gem version of version_fu Rails plugin.}
|
19
|
+
|
20
|
+
if s.respond_to? :specification_version then
|
21
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
22
|
+
s.specification_version = 2
|
23
|
+
end
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pjb3-version_fu
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: "1.0"
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jordan McKible
|
8
|
+
- Paul Barry
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2008-09-12 00:00:00 -07:00
|
14
|
+
default_executable:
|
15
|
+
dependencies: []
|
16
|
+
|
17
|
+
description: Dirty ActiveRecord Versioning (update of acts_as_versioned)
|
18
|
+
email: mail (at) paulbarry (dot) com
|
19
|
+
executables: []
|
20
|
+
|
21
|
+
extensions: []
|
22
|
+
|
23
|
+
extra_rdoc_files:
|
24
|
+
- lib/version_fu.rb
|
25
|
+
- README.rdoc
|
26
|
+
files:
|
27
|
+
- version_fu.gemspec
|
28
|
+
- lib/version_fu.rb
|
29
|
+
- rails/init.rb
|
30
|
+
- README.rdoc
|
31
|
+
- MIT-LICENSE
|
32
|
+
has_rdoc: true
|
33
|
+
homepage: http://tuples.us/2008/05/03/lazily-announcing-version_fu/
|
34
|
+
post_install_message:
|
35
|
+
rdoc_options:
|
36
|
+
- --line-numbers
|
37
|
+
- --inline-source
|
38
|
+
- --title
|
39
|
+
- Version Fu
|
40
|
+
- --main
|
41
|
+
- README.rdoc
|
42
|
+
require_paths:
|
43
|
+
- lib
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: "0"
|
49
|
+
version:
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: "0"
|
55
|
+
version:
|
56
|
+
requirements: []
|
57
|
+
|
58
|
+
rubyforge_project: version_du
|
59
|
+
rubygems_version: 1.2.0
|
60
|
+
signing_key:
|
61
|
+
specification_version: 2
|
62
|
+
summary: Gem version of version_fu Rails plugin.
|
63
|
+
test_files: []
|
64
|
+
|