versions 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -17
- data/History.txt +12 -0
- data/README.rdoc +27 -6
- data/Rakefile +3 -3
- data/lib/versions/auto.rb +14 -1
- data/lib/versions/destroy.rb +63 -0
- data/lib/versions/multi.rb +115 -133
- data/lib/versions/version.rb +3 -0
- data/lib/versions.rb +1 -1
- data/test/fixtures.rb +16 -0
- data/test/unit/auto_test.rb +17 -1
- data/test/unit/multi_test.rb +137 -4
- data/test/unit/property_test.rb +66 -0
- data/versions.gemspec +78 -0
- metadata +10 -15
- data/lib/versions/property.rb +0 -98
data/.gitignore
CHANGED
data/History.txt
ADDED
data/README.rdoc
CHANGED
@@ -6,19 +6,40 @@ website: http://zenadmin.org/650
|
|
6
6
|
|
7
7
|
license: MIT
|
8
8
|
|
9
|
-
== Auto
|
9
|
+
== Auto (status: beta)
|
10
10
|
|
11
11
|
Duplicate on save if should_clone? returns true.
|
12
12
|
|
13
|
-
== Multi
|
13
|
+
== Multi (status: beta)
|
14
14
|
|
15
15
|
Hide many versions behind a single current one.
|
16
16
|
|
17
|
-
== Transparent
|
17
|
+
== Transparent (status: alpha)
|
18
18
|
|
19
|
-
Hide versions from outside world
|
19
|
+
Hide versions from outside world (simulate read/writes on the node but
|
20
|
+
the actions are done on the version). Uses method_missing to forward
|
21
|
+
method calls.
|
20
22
|
|
21
|
-
|
23
|
+
=== Properties integration (status: beta)
|
22
24
|
|
23
|
-
|
25
|
+
You can get the same functionality as 'Transparent' by using the Property gem and
|
26
|
+
storing properties in the version:
|
27
|
+
|
28
|
+
class Contact < ActiveRecord::Base
|
29
|
+
include Versions::Multi
|
30
|
+
has_multiple :versions
|
31
|
+
|
32
|
+
include Property
|
33
|
+
store_properties_in :version
|
34
|
+
|
35
|
+
property do |p|
|
36
|
+
p.string 'first_name', 'name'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
== SharedAttachment (status: alpha)
|
42
|
+
|
43
|
+
Enable file attachments linked to versions. The attachments are shared between versions
|
44
|
+
and deleted when no more versions are using them.
|
24
45
|
|
data/Rakefile
CHANGED
@@ -15,7 +15,7 @@ begin
|
|
15
15
|
require 'rcov/rcovtask'
|
16
16
|
Rcov::RcovTask.new do |test|
|
17
17
|
test.libs << 'test' << 'lib'
|
18
|
-
test.pattern = 'test
|
18
|
+
test.pattern = 'test/**/**_test.rb'
|
19
19
|
test.verbose = true
|
20
20
|
test.rcov_opts = ['-T', '--exclude-only', '"test\/,^\/"']
|
21
21
|
end
|
@@ -38,11 +38,11 @@ begin
|
|
38
38
|
gemspec.email = "gaspard@teti.ch"
|
39
39
|
gemspec.homepage = "http://zenadmin.org/650"
|
40
40
|
gemspec.authors = ["Gaspard Bucher"]
|
41
|
-
gemspec.add_development_dependency "shoulda", ">= 0"
|
42
41
|
|
43
42
|
gemspec.add_development_dependency('shoulda')
|
43
|
+
gemspec.add_development_dependency('property', '>= 0.8.1')
|
44
|
+
|
44
45
|
gemspec.add_dependency('activerecord')
|
45
|
-
gemspec.add_dependency('property', '>= 0.8.0')
|
46
46
|
end
|
47
47
|
Jeweler::GemcutterTasks.new
|
48
48
|
rescue LoadError
|
data/lib/versions/auto.rb
CHANGED
@@ -5,7 +5,10 @@ module Versions
|
|
5
5
|
attr_reader :previous_id
|
6
6
|
|
7
7
|
def self.included(base)
|
8
|
+
raise TypeError.new("Missing 'number' field in table #{base.table_name}.") unless base.column_names.include?('number')
|
8
9
|
base.before_save :prepare_save_or_clone
|
10
|
+
base.after_save :clear_number_counter
|
11
|
+
base.attr_protected :number
|
9
12
|
end
|
10
13
|
|
11
14
|
def should_clone?
|
@@ -22,8 +25,13 @@ module Versions
|
|
22
25
|
end
|
23
26
|
|
24
27
|
def prepare_save_or_clone
|
25
|
-
if
|
28
|
+
if new_record?
|
29
|
+
self[:number] = 1
|
30
|
+
elsif should_clone?
|
26
31
|
@previous_id = self[:id]
|
32
|
+
@previous_number ||= self[:number]
|
33
|
+
self[:number] = @previous_number + 1
|
34
|
+
|
27
35
|
self[:id] = nil
|
28
36
|
self[:created_at] = nil
|
29
37
|
self[:updated_at] = nil
|
@@ -34,5 +42,10 @@ module Versions
|
|
34
42
|
end
|
35
43
|
true
|
36
44
|
end
|
45
|
+
|
46
|
+
def clear_number_counter
|
47
|
+
@previous_number = nil
|
48
|
+
true
|
49
|
+
end
|
37
50
|
end # Auto
|
38
51
|
end # Versions
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Versions
|
2
|
+
# If you include this module in your model (which should also include Versions::Multi), deleting
|
3
|
+
# the last version will destroy the model.
|
4
|
+
module Destroy
|
5
|
+
|
6
|
+
# This module should be included in the model that serves as version.
|
7
|
+
module Version
|
8
|
+
def self.included(base)
|
9
|
+
|
10
|
+
base.class_eval do
|
11
|
+
attr_accessor :__destroy
|
12
|
+
belongs_to :node
|
13
|
+
before_create :setup_version_on_create
|
14
|
+
attr_protected :number, :user_id
|
15
|
+
|
16
|
+
alias_method_chain :save, :destroy
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def save_with_destroy(*args)
|
21
|
+
if @__destroy
|
22
|
+
node = self.node
|
23
|
+
if destroy
|
24
|
+
# reset @version
|
25
|
+
node.send(:version_destroyed)
|
26
|
+
true
|
27
|
+
end
|
28
|
+
else
|
29
|
+
save_without_destroy(*args)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def setup_version_on_create
|
35
|
+
raise "You should define 'setup_version_on_create' method in '#{self.class}' class."
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.included(base)
|
40
|
+
base.alias_method_chain :save, :destroy
|
41
|
+
end
|
42
|
+
|
43
|
+
def save_with_destroy(*args)
|
44
|
+
version = self.version
|
45
|
+
# TODO: we could use 'version.mark_for_destruction' instead of __destroy...
|
46
|
+
if version.__destroy && versions.count == 1
|
47
|
+
destroy # will destroy last version
|
48
|
+
else
|
49
|
+
save_without_destroy(*args)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
# This is called after a version is destroyed
|
55
|
+
def version_destroyed
|
56
|
+
# remove from versions list
|
57
|
+
if versions.loaded?
|
58
|
+
node.versions -= [@version]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
data/lib/versions/multi.rb
CHANGED
@@ -1,142 +1,124 @@
|
|
1
|
-
module
|
2
|
-
module
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
def self.included(base)
|
9
|
-
|
10
|
-
base.class_eval do
|
11
|
-
attr_accessor :__destroy
|
12
|
-
belongs_to :node
|
13
|
-
before_create :setup_version_on_create
|
14
|
-
attr_protected :number, :user_id
|
15
|
-
|
16
|
-
alias_method_chain :node, :secure
|
17
|
-
alias_method_chain :save, :destroy
|
18
|
-
end
|
19
|
-
end
|
1
|
+
module Versions
|
2
|
+
# This module hides 'has_many' versions as if there was only a 'belongs_to' version,
|
3
|
+
# automatically registering the latest version's id.
|
4
|
+
module Multi
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
end
|
20
8
|
|
21
|
-
|
22
|
-
@node ||= begin
|
23
|
-
if n = node_without_secure
|
24
|
-
visitor.visit(n)
|
25
|
-
n.version = self
|
26
|
-
end
|
27
|
-
n
|
28
|
-
end
|
29
|
-
end
|
9
|
+
module ClassMethods
|
30
10
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
# reset @version
|
36
|
-
node.send(:version_destroyed)
|
37
|
-
true
|
38
|
-
end
|
39
|
-
else
|
40
|
-
save_without_destroy(*args)
|
41
|
-
end
|
42
|
-
end
|
11
|
+
def has_multiple(versions, options = {})
|
12
|
+
name = versions.to_s.singularize
|
13
|
+
klass = (options[:class_name] || name.capitalize).constantize
|
14
|
+
owner_name = options[:as] || 'owner'
|
43
15
|
|
44
|
-
|
45
|
-
|
46
|
-
raise "You should define 'setup_version_on_create' method in '#{self.class}' class."
|
47
|
-
end
|
48
|
-
end
|
16
|
+
raise TypeError.new("Missing 'number' field in table #{klass.table_name}.") unless klass.column_names.include?('number')
|
17
|
+
raise TypeError.new("Missing '#{owner_name}_id' in table #{klass.table_name}.") unless klass.column_names.include?("#{owner_name}_id")
|
49
18
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
base.after_create :save_version_after_create
|
55
|
-
#base.accepts_nested_attributes_for :version
|
19
|
+
has_many versions, :order => 'number DESC', :dependent => :destroy
|
20
|
+
validate :"validate_#{name}"
|
21
|
+
after_create :"save_#{name}_after_create"
|
22
|
+
before_update :"save_#{name}_before_update"
|
56
23
|
|
57
|
-
|
24
|
+
include module_for_multiple(name, klass, owner_name)
|
25
|
+
klass.belongs_to owner_name, :class_name => self.to_s
|
58
26
|
end
|
59
27
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
28
|
+
protected
|
29
|
+
def module_for_multiple(name, klass, owner_name)
|
30
|
+
|
31
|
+
# Eval is ugly, but it's the fastest solution I know of
|
32
|
+
line = __LINE__
|
33
|
+
definitions = <<-EOF
|
34
|
+
def #{name} # def version
|
35
|
+
@#{name} ||= begin # @version ||= begin
|
36
|
+
if v_id = #{name}_id # if v_id = version_id
|
37
|
+
version = ::#{klass}.find(v_id) # version = ::Version.find(v_id)
|
38
|
+
else # else
|
39
|
+
version = ::#{klass}.new # version = ::Version.new
|
40
|
+
end # end
|
41
|
+
version.#{owner_name} = self # version.owner = self
|
42
|
+
version # version
|
43
|
+
end # end
|
44
|
+
end # end
|
45
|
+
|
46
|
+
def #{name}=(v) # def version=(v)
|
47
|
+
@#{name} = v # @version = v
|
48
|
+
end # end
|
49
|
+
|
50
|
+
def #{name}_attributes=(attributes) # def version_attributes=(attributes)
|
51
|
+
#{name}.attributes = attributes # version.attributes = attributes
|
52
|
+
end # end
|
53
|
+
|
54
|
+
private
|
55
|
+
def validate_#{name} # def validate_version
|
56
|
+
unless #{name}.valid? # unless version.valid?
|
57
|
+
merge_multi_errors('#{name}', @#{name}) # merge_multi_errors('version', version)
|
58
|
+
end # end
|
59
|
+
end # end
|
60
|
+
|
61
|
+
def save_#{name}_before_update # def save_version_before_update
|
62
|
+
return true if !@#{name}.changed? # return true if !@version.changed?
|
63
|
+
if !@#{name}.save(false) # if !@version.save_with_validation(false)
|
64
|
+
merge_multi_errors('#{name}', @#{name}) # merge_multi_errors('version', @version)
|
65
|
+
false # false
|
66
|
+
else # else
|
67
|
+
set_current_#{name}_before_update # set_current_version_before_update
|
68
|
+
true # true
|
69
|
+
end # end
|
70
|
+
end # end
|
71
|
+
#
|
72
|
+
def save_#{name}_after_create # def save_version_after_create
|
73
|
+
@#{name}.#{owner_name}_id = self[:id] # version.owner_id = self[:id]
|
74
|
+
if !@#{name}.save(false) # if !@version.save_with_validation(false)
|
75
|
+
merge_multi_errors('#{name}', @#{name}) # merge_multi_errors('version', @version)
|
76
|
+
raise ActiveRecord::RecordInvalid.new(self) # raise ActiveRecord::RecordInvalid.new(self)
|
77
|
+
else # else
|
78
|
+
set_current_#{name}_after_create # set_current_version_after_create
|
79
|
+
end # end
|
80
|
+
true # true
|
81
|
+
end # end
|
82
|
+
|
83
|
+
|
84
|
+
# This method is triggered when the version is saved, but before the
|
85
|
+
# master record is updated. This method is usually overwritten
|
86
|
+
# in the class.
|
87
|
+
def set_current_#{name}_before_update # def set_current_version_before_update
|
88
|
+
self[:#{name}_id] = @#{name}.id # self[:version_id] = @version.id
|
89
|
+
end # end
|
90
|
+
|
91
|
+
# This method is triggered when the version is saved, after the
|
92
|
+
# master record has been created. This method is usually overwriten
|
93
|
+
# in the class.
|
94
|
+
def set_current_#{name}_after_create # def set_current_version_after_create
|
95
|
+
# raw SQL to skip callbacks and validtions #
|
96
|
+
conn = self.class.connection # conn = self.class.connection
|
97
|
+
|
98
|
+
# conn.execute("UPDATE pages SET \#{conn.quote_column_name("version_id")} = \#{conn.quote(@version.id)} WHERE id = \#{conn.quote(self.id)}")
|
99
|
+
conn.execute(
|
100
|
+
"UPDATE #{table_name} " +
|
101
|
+
"SET \#{conn.quote_column_name("#{name}_id")} = \#{conn.quote(@#{name}.id)} " +
|
102
|
+
"WHERE id = \#{conn.quote(self.id)}"
|
103
|
+
)
|
104
|
+
self[:#{name}_id] = @#{name}.id # self[:version_id] = @version.id
|
105
|
+
changed_attributes.clear # changed_attributes.clear
|
106
|
+
end # end
|
107
|
+
EOF
|
108
|
+
|
109
|
+
methods_module = Module.new
|
110
|
+
methods_module.class_eval(definitions, __FILE__, line + 1)
|
111
|
+
methods_module
|
112
|
+
end # module_for_multiple
|
113
|
+
end # ClassMethods
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def merge_multi_errors(name, model)
|
118
|
+
model.errors.each_error do |attribute, message|
|
119
|
+
attribute = "#{name}_#{attribute}"
|
120
|
+
errors.add(attribute, message) unless errors[attribute] # FIXME: rails 3: if errors[attribute].empty?
|
67
121
|
end
|
68
122
|
end
|
69
|
-
|
70
|
-
|
71
|
-
version.attributes = attributes
|
72
|
-
end
|
73
|
-
|
74
|
-
# The logic to get the 'current' version should be
|
75
|
-
# rewritten in class including MultiVersion.
|
76
|
-
def version
|
77
|
-
raise "You should define 'version' method in '#{self.class}' class."
|
78
|
-
end
|
79
|
-
|
80
|
-
def version=(v)
|
81
|
-
@version = v
|
82
|
-
end
|
83
|
-
|
84
|
-
private
|
85
|
-
|
86
|
-
def validate_version
|
87
|
-
# We force the existence of at least one version with this code
|
88
|
-
unless version.valid?
|
89
|
-
merge_version_errors
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
def save_version_before_update
|
94
|
-
if !@version.save #_with_validation(false)
|
95
|
-
merge_version_errors
|
96
|
-
else
|
97
|
-
current_version_before_update
|
98
|
-
end
|
99
|
-
end
|
100
|
-
|
101
|
-
def save_version_after_create
|
102
|
-
version.node_id = self[:id]
|
103
|
-
if !@version.save #_with_validation(false)
|
104
|
-
merge_version_errors
|
105
|
-
rollback!
|
106
|
-
else
|
107
|
-
current_version_after_create
|
108
|
-
end
|
109
|
-
true
|
110
|
-
end
|
111
|
-
|
112
|
-
# This method is triggered when the version is saved, but before the
|
113
|
-
# master record is updated.
|
114
|
-
# The role of this method is typically to do things like:
|
115
|
-
# self[:version_id] = version.id
|
116
|
-
def current_version_before_update
|
117
|
-
end
|
118
|
-
|
119
|
-
# This method is triggered when the version is saved, after the
|
120
|
-
# master record has been created.
|
121
|
-
# The role of this method is typically to do things like:
|
122
|
-
# update_attribute(:version_id, version.id)
|
123
|
-
def current_version_after_create
|
124
|
-
end
|
125
|
-
|
126
|
-
# This is called after a version is destroyed
|
127
|
-
def version_destroyed
|
128
|
-
# remove from versions list
|
129
|
-
if versions.loaded?
|
130
|
-
node.versions -= [@version]
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
def merge_version_errors
|
135
|
-
@version.errors.each_error do |attribute, message|
|
136
|
-
attribute = "version_#{attribute}"
|
137
|
-
errors.add(attribute, message) unless errors[attribute] # FIXME: rails 3: if errors[attribute].empty?
|
138
|
-
end
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|
123
|
+
end # Multi
|
124
|
+
end # Versions
|
data/lib/versions.rb
CHANGED
data/test/fixtures.rb
CHANGED
@@ -1,3 +1,16 @@
|
|
1
|
+
{:default=>[:"models.multi_test/simple_version.should not contain letter x", :"messages.should not contain letter x", "should not contain letter x"], :value=>"Fox", :scope=>[:activerecord, :errors], :model=>"Multitest::simpleversion", :attribute=>"Title"}
|
2
|
+
|
3
|
+
module I18n
|
4
|
+
# I hate I18n in rails (soooo many bad surprises)
|
5
|
+
def self.translate(key, options)
|
6
|
+
if options[:default].first.to_s =~ /\A.*\.(.*)\Z/
|
7
|
+
$1
|
8
|
+
else
|
9
|
+
options[:default].last
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
1
14
|
begin
|
2
15
|
class VersionsMigration < ActiveRecord::Migration
|
3
16
|
def self.down
|
@@ -7,6 +20,7 @@ begin
|
|
7
20
|
def self.up
|
8
21
|
create_table 'pages' do |t|
|
9
22
|
t.integer 'version_id'
|
23
|
+
t.integer 'foo_id'
|
10
24
|
t.string 'name'
|
11
25
|
t.timestamps
|
12
26
|
end
|
@@ -16,6 +30,8 @@ begin
|
|
16
30
|
t.text 'text'
|
17
31
|
t.string 'properties'
|
18
32
|
t.integer 'attachment_id'
|
33
|
+
t.integer 'number'
|
34
|
+
t.integer 'owner_id'
|
19
35
|
t.timestamps
|
20
36
|
end
|
21
37
|
|
data/test/unit/auto_test.rb
CHANGED
@@ -3,8 +3,8 @@ require 'helper'
|
|
3
3
|
class AutoTest < Test::Unit::TestCase
|
4
4
|
|
5
5
|
class BadVersion < ActiveRecord::Base
|
6
|
-
include Versions::Auto
|
7
6
|
set_table_name :versions
|
7
|
+
include Versions::Auto
|
8
8
|
end
|
9
9
|
|
10
10
|
class Version < ActiveRecord::Base
|
@@ -39,6 +39,10 @@ class AutoTest < Test::Unit::TestCase
|
|
39
39
|
@version = Version.create('title' => 'Socrate')
|
40
40
|
end
|
41
41
|
|
42
|
+
should 'start number at 1' do
|
43
|
+
assert_equal 1, subject.number
|
44
|
+
end
|
45
|
+
|
42
46
|
context 'returning false' do
|
43
47
|
should 'update record if should_clone is false' do
|
44
48
|
assert_difference('Version.count', 0) do
|
@@ -56,6 +60,10 @@ class AutoTest < Test::Unit::TestCase
|
|
56
60
|
subject.update_attributes('title' => 'Aristotle')
|
57
61
|
assert !subject.cloned?
|
58
62
|
end
|
63
|
+
|
64
|
+
should 'not increase version number' do
|
65
|
+
assert_equal 1, subject.number
|
66
|
+
end
|
59
67
|
end
|
60
68
|
|
61
69
|
context 'returning true' do
|
@@ -79,6 +87,14 @@ class AutoTest < Test::Unit::TestCase
|
|
79
87
|
subject.update_attributes('title' => 'Aristotle')
|
80
88
|
assert subject.cloned?
|
81
89
|
end
|
90
|
+
|
91
|
+
should 'increase number on each clone' do
|
92
|
+
subject.update_attributes('title' => 'Aristotle')
|
93
|
+
assert_equal 2, subject.number
|
94
|
+
|
95
|
+
subject.update_attributes('title' => 'Aristotle')
|
96
|
+
assert_equal 3, subject.number
|
97
|
+
end
|
82
98
|
end
|
83
99
|
end
|
84
100
|
end
|
data/test/unit/multi_test.rb
CHANGED
@@ -1,7 +1,140 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
|
-
class
|
4
|
-
|
5
|
-
|
3
|
+
class MultiTest < Test::Unit::TestCase
|
4
|
+
class SimpleVersion < ActiveRecord::Base
|
5
|
+
set_table_name :versions
|
6
|
+
validate :title_does_not_contain_letter_x
|
7
|
+
before_save :fail_if_title_contains_y
|
8
|
+
|
9
|
+
def title_does_not_contain_letter_x
|
10
|
+
errors.add('title', 'should not contain letter x') if self[:title].to_s =~ /x/
|
11
|
+
end
|
12
|
+
|
13
|
+
def fail_if_title_contains_y
|
14
|
+
if self[:title].to_s =~ /y/
|
15
|
+
errors.add('title', 'should not contain letter y')
|
16
|
+
false
|
17
|
+
else
|
18
|
+
true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
class SimplePage < ActiveRecord::Base
|
23
|
+
include Versions::Multi
|
24
|
+
|
25
|
+
set_table_name :pages
|
26
|
+
has_multiple :foos, :class_name => 'MultiTest::SimpleVersion'
|
27
|
+
end
|
28
|
+
|
29
|
+
class Version < ActiveRecord::Base
|
30
|
+
set_table_name :versions
|
31
|
+
include Versions::Auto
|
32
|
+
def should_clone?
|
33
|
+
changed?
|
34
|
+
end
|
35
|
+
end
|
36
|
+
class Page < ActiveRecord::Base
|
37
|
+
set_table_name :pages
|
38
|
+
include Versions::Multi
|
39
|
+
has_multiple :versions, :class_name => 'MultiTest::Version'
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'A class with multiple foos' do
|
43
|
+
|
44
|
+
should 'accept foo nested attributes' do
|
45
|
+
assert_nothing_raised { SimplePage.create('name' => 'one', 'foo_attributes' => {'title' => 'First'}) }
|
46
|
+
end
|
47
|
+
|
48
|
+
should 'create a foo instance of the given type' do
|
49
|
+
page = SimplePage.create
|
50
|
+
assert page.valid?
|
51
|
+
assert_kind_of MultiTest::SimpleVersion, page.foo
|
52
|
+
end
|
53
|
+
|
54
|
+
should 'set foo_id after_create' do
|
55
|
+
page = SimplePage.create
|
56
|
+
foo_id = page.foo_id
|
57
|
+
assert foo_id
|
58
|
+
page = SimplePage.find(page)
|
59
|
+
assert_equal foo_id, page.foo_id
|
60
|
+
end
|
61
|
+
|
62
|
+
should 'replace current instance on new foos' do
|
63
|
+
page = SimplePage.create
|
64
|
+
first_foo = page.foo.id
|
65
|
+
page.foo = SimpleVersion.new('title' => 'hello')
|
66
|
+
assert page.save
|
67
|
+
page = SimplePage.find(page.id)
|
68
|
+
assert_not_equal first_foo, page.foo.id
|
69
|
+
end
|
70
|
+
|
71
|
+
should 'merge foo errors in model on create' do
|
72
|
+
page = SimplePage.create('foo_attributes' => {'title' => 'Fox'})
|
73
|
+
assert !page.valid?
|
74
|
+
assert_equal 'should not contain letter x', page.errors['foo_title']
|
75
|
+
end
|
76
|
+
|
77
|
+
should 'merge foo errors in model on update' do
|
78
|
+
page = SimplePage.create('foo_attributes' => {'title' => 'phone'})
|
79
|
+
assert page.valid?
|
80
|
+
assert !page.update_attributes('foo_attributes' => {'title' => 'fax'})
|
81
|
+
assert_equal 'should not contain letter x', page.errors['foo_title']
|
82
|
+
end
|
83
|
+
|
84
|
+
should 'rollback if foo save fails on create' do
|
85
|
+
assert_difference('MultiTest::SimpleVersion.count', 0) do
|
86
|
+
assert_difference('MultiTest::SimplePage.count', 0) do
|
87
|
+
assert_raise(ActiveRecord::RecordInvalid) do
|
88
|
+
begin
|
89
|
+
page = SimplePage.create('foo_attributes' => {'title' => 'Fly'})
|
90
|
+
rescue ActiveRecord::RecordInvalid => err
|
91
|
+
assert_equal 'Validation failed: Foo title should not contain letter y', err.message
|
92
|
+
raise
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
should 'abort if foo save fails on update' do
|
100
|
+
page = SimplePage.create('foo_attributes' => {'title' => 'mosquito'})
|
101
|
+
assert page.valid?
|
102
|
+
assert !page.update_attributes('foo_attributes' => {'title' => 'fly'})
|
103
|
+
assert_equal 'should not contain letter y', page.errors['foo_title']
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
context 'A class with multiple auto versions' do
|
108
|
+
should 'create new versions on update' do
|
109
|
+
page = Page.create
|
110
|
+
assert_difference('Version.count', 1) do
|
111
|
+
assert page.update_attributes('version_attributes' => {'title' => 'newTitle'})
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
should 'mark new version as not dirty after create' do
|
116
|
+
page = Page.create
|
117
|
+
assert !page.version.changed?
|
118
|
+
end
|
119
|
+
|
120
|
+
should 'mark new version as not dirty after update' do
|
121
|
+
page = Page.create
|
122
|
+
assert page.update_attributes('version_attributes' => {'title' => 'Yodle'})
|
123
|
+
assert !page.version.changed?
|
124
|
+
end
|
125
|
+
|
126
|
+
should 'find latest version' do
|
127
|
+
page = Page.create
|
128
|
+
v_id = page.version.id
|
129
|
+
assert page.update_attributes('version_attributes' => {'title' => 'newTitle'})
|
130
|
+
assert_not_equal v_id, page.version.id
|
131
|
+
end
|
132
|
+
|
133
|
+
should 'not create new versions on update if content did not change' do
|
134
|
+
page = Page.create('version_attributes' => {'title' => 'One'})
|
135
|
+
assert_difference('Version.count', 0) do
|
136
|
+
assert page.update_attributes('version_attributes' => {'title' => 'One'})
|
137
|
+
end
|
138
|
+
end
|
6
139
|
end
|
7
|
-
end
|
140
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'property'
|
3
|
+
|
4
|
+
class PropertyTest < Test::Unit::TestCase
|
5
|
+
class Version < ActiveRecord::Base
|
6
|
+
set_table_name :versions
|
7
|
+
include Versions::Auto
|
8
|
+
|
9
|
+
def should_clone?
|
10
|
+
if changed?
|
11
|
+
true
|
12
|
+
else
|
13
|
+
false
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Page < ActiveRecord::Base
|
19
|
+
set_table_name :pages
|
20
|
+
include Versions::Multi
|
21
|
+
has_multiple :versions, :class_name => 'PropertyTest::Version'
|
22
|
+
|
23
|
+
include Property
|
24
|
+
store_properties_in :version
|
25
|
+
|
26
|
+
property do |p|
|
27
|
+
p.text 'history'
|
28
|
+
p.string 'author', :default => 'John Malkovitch'
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'Working with properties stored in version' do
|
33
|
+
|
34
|
+
should 'create an initial version' do
|
35
|
+
page = nil
|
36
|
+
assert_difference('PropertyTest::Version.count', 1) do
|
37
|
+
page = Page.create('history' => 'His Story')
|
38
|
+
end
|
39
|
+
assert_equal 'His Story', page.history
|
40
|
+
assert_equal 1, page.version.number
|
41
|
+
end
|
42
|
+
|
43
|
+
should 'create new versions on property update' do
|
44
|
+
page = Page.create('history' => 'His Story')
|
45
|
+
assert_difference('PropertyTest::Version.count', 1) do
|
46
|
+
assert page.update_attributes('history' => 'Her Story')
|
47
|
+
end
|
48
|
+
assert_equal 'Her Story', page.history
|
49
|
+
assert_equal 2, page.version.number
|
50
|
+
end
|
51
|
+
|
52
|
+
should 'mark as dirty on property update' do
|
53
|
+
page = Page.create('history' => 'His Story')
|
54
|
+
page.prop['history'] = 'Her Story'
|
55
|
+
assert page.changed?
|
56
|
+
end
|
57
|
+
|
58
|
+
should 'not create new versions on property update with same values' do
|
59
|
+
page = Page.create('history' => 'His Story')
|
60
|
+
assert_difference('PropertyTest::Version.count', 0) do
|
61
|
+
assert page.update_attributes('history' => 'His Story')
|
62
|
+
end
|
63
|
+
assert_equal 1, page.version.number
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/versions.gemspec
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{versions}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Gaspard Bucher"]
|
12
|
+
s.date = %q{2010-02-14}
|
13
|
+
s.description = %q{A list of libraries to work with ActiveRecord model versioning: Auto (duplicate on save), Multi (hide many versions behind a single one), Transparent (hide versions from outside world), Property (define properties on model, store them in versions)}
|
14
|
+
s.email = %q{gaspard@teti.ch}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
"History.txt",
|
23
|
+
"LICENSE",
|
24
|
+
"README.rdoc",
|
25
|
+
"Rakefile",
|
26
|
+
"lib/versions.rb",
|
27
|
+
"lib/versions/auto.rb",
|
28
|
+
"lib/versions/destroy.rb",
|
29
|
+
"lib/versions/multi.rb",
|
30
|
+
"lib/versions/shared_attachment.rb",
|
31
|
+
"lib/versions/shared_attachment/attachment.rb",
|
32
|
+
"lib/versions/shared_attachment/owner.rb",
|
33
|
+
"lib/versions/transparent.rb",
|
34
|
+
"lib/versions/version.rb",
|
35
|
+
"test/fixtures.rb",
|
36
|
+
"test/helper.rb",
|
37
|
+
"test/unit/attachment_test.rb",
|
38
|
+
"test/unit/auto_test.rb",
|
39
|
+
"test/unit/multi_test.rb",
|
40
|
+
"test/unit/property_test.rb",
|
41
|
+
"test/unit/transparent_test.rb",
|
42
|
+
"versions.gemspec"
|
43
|
+
]
|
44
|
+
s.homepage = %q{http://zenadmin.org/650}
|
45
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
46
|
+
s.require_paths = ["lib"]
|
47
|
+
s.rubygems_version = %q{1.3.5}
|
48
|
+
s.summary = %q{A list of libraries to work with ActiveRecord model versioning}
|
49
|
+
s.test_files = [
|
50
|
+
"test/fixtures.rb",
|
51
|
+
"test/helper.rb",
|
52
|
+
"test/unit/attachment_test.rb",
|
53
|
+
"test/unit/auto_test.rb",
|
54
|
+
"test/unit/multi_test.rb",
|
55
|
+
"test/unit/property_test.rb",
|
56
|
+
"test/unit/transparent_test.rb"
|
57
|
+
]
|
58
|
+
|
59
|
+
if s.respond_to? :specification_version then
|
60
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
61
|
+
s.specification_version = 3
|
62
|
+
|
63
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
64
|
+
s.add_development_dependency(%q<shoulda>, [">= 0"])
|
65
|
+
s.add_development_dependency(%q<property>, [">= 0.8.1"])
|
66
|
+
s.add_runtime_dependency(%q<activerecord>, [">= 0"])
|
67
|
+
else
|
68
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
69
|
+
s.add_dependency(%q<property>, [">= 0.8.1"])
|
70
|
+
s.add_dependency(%q<activerecord>, [">= 0"])
|
71
|
+
end
|
72
|
+
else
|
73
|
+
s.add_dependency(%q<shoulda>, [">= 0"])
|
74
|
+
s.add_dependency(%q<property>, [">= 0.8.1"])
|
75
|
+
s.add_dependency(%q<activerecord>, [">= 0"])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: versions
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gaspard Bucher
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2010-02-
|
12
|
+
date: 2010-02-14 00:00:00 +01:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -23,14 +23,14 @@ dependencies:
|
|
23
23
|
version: "0"
|
24
24
|
version:
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
|
-
name:
|
26
|
+
name: property
|
27
27
|
type: :development
|
28
28
|
version_requirement:
|
29
29
|
version_requirements: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
33
|
+
version: 0.8.1
|
34
34
|
version:
|
35
35
|
- !ruby/object:Gem::Dependency
|
36
36
|
name: activerecord
|
@@ -42,16 +42,6 @@ dependencies:
|
|
42
42
|
- !ruby/object:Gem::Version
|
43
43
|
version: "0"
|
44
44
|
version:
|
45
|
-
- !ruby/object:Gem::Dependency
|
46
|
-
name: property
|
47
|
-
type: :runtime
|
48
|
-
version_requirement:
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
50
|
-
requirements:
|
51
|
-
- - ">="
|
52
|
-
- !ruby/object:Gem::Version
|
53
|
-
version: 0.8.0
|
54
|
-
version:
|
55
45
|
description: "A list of libraries to work with ActiveRecord model versioning: Auto (duplicate on save), Multi (hide many versions behind a single one), Transparent (hide versions from outside world), Property (define properties on model, store them in versions)"
|
56
46
|
email: gaspard@teti.ch
|
57
47
|
executables: []
|
@@ -64,23 +54,27 @@ extra_rdoc_files:
|
|
64
54
|
files:
|
65
55
|
- .document
|
66
56
|
- .gitignore
|
57
|
+
- History.txt
|
67
58
|
- LICENSE
|
68
59
|
- README.rdoc
|
69
60
|
- Rakefile
|
70
61
|
- lib/versions.rb
|
71
62
|
- lib/versions/auto.rb
|
63
|
+
- lib/versions/destroy.rb
|
72
64
|
- lib/versions/multi.rb
|
73
|
-
- lib/versions/property.rb
|
74
65
|
- lib/versions/shared_attachment.rb
|
75
66
|
- lib/versions/shared_attachment/attachment.rb
|
76
67
|
- lib/versions/shared_attachment/owner.rb
|
77
68
|
- lib/versions/transparent.rb
|
69
|
+
- lib/versions/version.rb
|
78
70
|
- test/fixtures.rb
|
79
71
|
- test/helper.rb
|
80
72
|
- test/unit/attachment_test.rb
|
81
73
|
- test/unit/auto_test.rb
|
82
74
|
- test/unit/multi_test.rb
|
75
|
+
- test/unit/property_test.rb
|
83
76
|
- test/unit/transparent_test.rb
|
77
|
+
- versions.gemspec
|
84
78
|
has_rdoc: true
|
85
79
|
homepage: http://zenadmin.org/650
|
86
80
|
licenses: []
|
@@ -115,4 +109,5 @@ test_files:
|
|
115
109
|
- test/unit/attachment_test.rb
|
116
110
|
- test/unit/auto_test.rb
|
117
111
|
- test/unit/multi_test.rb
|
112
|
+
- test/unit/property_test.rb
|
118
113
|
- test/unit/transparent_test.rb
|
data/lib/versions/property.rb
DELETED
@@ -1,98 +0,0 @@
|
|
1
|
-
module Zena
|
2
|
-
module Use
|
3
|
-
# This module lets the user use a node as if it was not versioned and will
|
4
|
-
# take care of routing the attributes between the node and the version.
|
5
|
-
module TransparentVersion
|
6
|
-
|
7
|
-
def self.included(base)
|
8
|
-
base.class_eval do
|
9
|
-
# When writing attributes, we send everything that we do not know of
|
10
|
-
# to the version.
|
11
|
-
def attributes_with_multi_version=(attributes)
|
12
|
-
columns = self.class.column_names
|
13
|
-
version_attributes = {}
|
14
|
-
attributes.keys.each do |k|
|
15
|
-
if !respond_to?("#{k}=") && !columns.include?(k)
|
16
|
-
version_attributes[k] = attributes.delete(k)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
version.attributes = version_attributes
|
20
|
-
self.attributes_without_multi_version = attributes
|
21
|
-
end
|
22
|
-
|
23
|
-
alias_method_chain :attributes=, :multi_version
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
private
|
28
|
-
# We need method_missing in forms, normal access in templates should be made
|
29
|
-
# through 'node.version.xxxx', not 'node.xxxx'.
|
30
|
-
def method_missing(meth, *args)
|
31
|
-
method = meth.to_s
|
32
|
-
if !args.empty? || method[-1..-1] == '?' || self.class.column_names.include?(method)
|
33
|
-
super
|
34
|
-
elsif version.respond_to?(meth)
|
35
|
-
version.send(meth)
|
36
|
-
else
|
37
|
-
#version.prop[meth.to_s]
|
38
|
-
super
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
# Any attribute starting with 'v_' belongs to the 'version' or 'redaction'
|
43
|
-
# Any attribute starting with 'c_' belongs to the 'version' or 'redaction' content
|
44
|
-
# FIXME: performance: create methods on the fly so that next calls will not pass through 'method_missing'. #189.
|
45
|
-
# FIXME: this should not be used anymore. Remove.
|
46
|
-
# def method_missing(meth, *args)
|
47
|
-
# if meth.to_s =~ /^(v_|c_|d_)(([\w_\?]+)(=?))$/
|
48
|
-
# target = $1
|
49
|
-
# method = $2
|
50
|
-
# value = $3
|
51
|
-
# mode = $4
|
52
|
-
# if mode == '='
|
53
|
-
# begin
|
54
|
-
# # set
|
55
|
-
# unless recipient = redaction
|
56
|
-
# # remove trailing '='
|
57
|
-
# redaction_error(meth.to_s[0..-2], "could not be set (no redaction)")
|
58
|
-
# return
|
59
|
-
# end
|
60
|
-
#
|
61
|
-
# case target
|
62
|
-
# when 'c_'
|
63
|
-
# if recipient.content_class && recipient = recipient.redaction_content
|
64
|
-
# recipient.send(method,*args)
|
65
|
-
# else
|
66
|
-
# redaction_error(meth.to_s[0..-2], "cannot be set") # remove trailing '='
|
67
|
-
# end
|
68
|
-
# when 'd_'
|
69
|
-
# recipient.prop[method[0..-2]] = args[0]
|
70
|
-
# else
|
71
|
-
# recipient.send(method,*args)
|
72
|
-
# end
|
73
|
-
# rescue NoMethodError
|
74
|
-
# # bad attribute, just ignore
|
75
|
-
# end
|
76
|
-
# else
|
77
|
-
# # read
|
78
|
-
# recipient = version
|
79
|
-
# if target == 'd_'
|
80
|
-
# version.prop[method]
|
81
|
-
# else
|
82
|
-
# recipient = recipient.content if target == 'c_'
|
83
|
-
# return nil unless recipient
|
84
|
-
# begin
|
85
|
-
# recipient.send(method,*args)
|
86
|
-
# rescue NoMethodError
|
87
|
-
# # bad attribute
|
88
|
-
# return nil
|
89
|
-
# end
|
90
|
-
# end
|
91
|
-
# end
|
92
|
-
# else
|
93
|
-
# super
|
94
|
-
# end
|
95
|
-
# end
|
96
|
-
end
|
97
|
-
end
|
98
|
-
end
|