versions 0.0.1 → 0.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/.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
|