versions 0.0.1

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/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Gaspard Bucher (http://teti.ch)
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.
data/README.rdoc ADDED
@@ -0,0 +1,24 @@
1
+ == Versions
2
+
3
+ A list of libraries to work with ActiveRecord model versioning.
4
+
5
+ website: http://zenadmin.org/650
6
+
7
+ license: MIT
8
+
9
+ == Auto
10
+
11
+ Duplicate on save if should_clone? returns true.
12
+
13
+ == Multi
14
+
15
+ Hide many versions behind a single current one.
16
+
17
+ == Transparent
18
+
19
+ Hide versions from outside world.
20
+
21
+ == Property
22
+
23
+ Define properties on model, store them in versions.
24
+
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ require 'pathname'
2
+ $LOAD_PATH.unshift((Pathname(__FILE__).dirname + 'lib').expand_path)
3
+
4
+ require 'versions/version'
5
+ require 'rake'
6
+ require 'rake/testtask'
7
+
8
+ Rake::TestTask.new(:test) do |test|
9
+ test.libs << 'lib' << 'test'
10
+ test.pattern = 'test/**/**_test.rb'
11
+ test.verbose = true
12
+ end
13
+
14
+ begin
15
+ require 'rcov/rcovtask'
16
+ Rcov::RcovTask.new do |test|
17
+ test.libs << 'test' << 'lib'
18
+ test.pattern = 'test/**/auto_test.rb'
19
+ test.verbose = true
20
+ test.rcov_opts = ['-T', '--exclude-only', '"test\/,^\/"']
21
+ end
22
+ rescue LoadError
23
+ task :rcov do
24
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install rcov"
25
+ end
26
+ end
27
+
28
+ task :default => :test
29
+
30
+ # GEM management
31
+ begin
32
+ require 'jeweler'
33
+ Jeweler::Tasks.new do |gemspec|
34
+ gemspec.version = Versions::VERSION
35
+ gemspec.name = "versions"
36
+ gemspec.summary = %Q{A list of libraries to work with ActiveRecord model versioning}
37
+ gemspec.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)}
38
+ gemspec.email = "gaspard@teti.ch"
39
+ gemspec.homepage = "http://zenadmin.org/650"
40
+ gemspec.authors = ["Gaspard Bucher"]
41
+ gemspec.add_development_dependency "shoulda", ">= 0"
42
+
43
+ gemspec.add_development_dependency('shoulda')
44
+ gemspec.add_dependency('activerecord')
45
+ gemspec.add_dependency('property', '>= 0.8.0')
46
+ end
47
+ Jeweler::GemcutterTasks.new
48
+ rescue LoadError
49
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
50
+ end
@@ -0,0 +1,38 @@
1
+ module Versions
2
+ # When you include this module into a class, it will automatically clone itself
3
+ # depending on the call to should_clone?
4
+ module Auto
5
+ attr_reader :previous_id
6
+
7
+ def self.included(base)
8
+ base.before_save :prepare_save_or_clone
9
+ end
10
+
11
+ def should_clone?
12
+ raise NoMethodError.new("You should implement 'should_clone?' in your model (return true for a new version, false to update).")
13
+ end
14
+
15
+ # This method provides a hook to alter values after a clone operation (just before save: no validation).
16
+ def cloned
17
+ end
18
+
19
+ # Return true if the record was cloned just before the last save
20
+ def cloned?
21
+ !@previous_id.nil?
22
+ end
23
+
24
+ def prepare_save_or_clone
25
+ if !new_record? && should_clone?
26
+ @previous_id = self[:id]
27
+ self[:id] = nil
28
+ self[:created_at] = nil
29
+ self[:updated_at] = nil
30
+ @new_record = true
31
+ cloned
32
+ else
33
+ @previous_id = nil
34
+ end
35
+ true
36
+ end
37
+ end # Auto
38
+ end # Versions
@@ -0,0 +1,142 @@
1
+ module Zena
2
+ module Use
3
+ # This module hides 'has_many' versions as if there was only a 'belongs_to' version,
4
+ # automatically registering the latest version's id.
5
+ module MultiVersion
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 :node, :secure
17
+ alias_method_chain :save, :destroy
18
+ end
19
+ end
20
+
21
+ def node_with_secure
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
30
+
31
+ def save_with_destroy(*args)
32
+ if @__destroy
33
+ node = self.node
34
+ if destroy
35
+ # reset @version
36
+ node.send(:version_destroyed)
37
+ true
38
+ end
39
+ else
40
+ save_without_destroy(*args)
41
+ end
42
+ end
43
+
44
+ private
45
+ def setup_version_on_create
46
+ raise "You should define 'setup_version_on_create' method in '#{self.class}' class."
47
+ end
48
+ end
49
+
50
+ def self.included(base)
51
+ base.has_many :versions, :order=>"number DESC", :dependent => :destroy #, :inverse_of => :node
52
+ base.validate :validate_version
53
+ base.before_update :save_version_before_update
54
+ base.after_create :save_version_after_create
55
+ #base.accepts_nested_attributes_for :version
56
+
57
+ base.alias_method_chain :save, :destroy
58
+ end
59
+
60
+ def save_with_destroy(*args)
61
+ version = self.version
62
+ # TODO: we could use 'version.mark_for_destruction' instead of __destroy...
63
+ if version.__destroy && versions.count == 1
64
+ self.destroy # will destroy last version
65
+ else
66
+ self.save_without_destroy(*args)
67
+ end
68
+ end
69
+
70
+ def version_attributes=(attributes)
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
@@ -0,0 +1,98 @@
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
@@ -0,0 +1,66 @@
1
+ module Versions
2
+ # This module enables file attachments to versions. The file is shared between versions if
3
+ # it is not changed. The Attachment only stores a reference to the file which is saved in the
4
+ # filesystem.
5
+ module SharedAttachment
6
+ class Attachment < ::ActiveRecord::Base
7
+ set_table_name 'attachments'
8
+ after_destroy :destroy_file
9
+ after_save :write_file
10
+
11
+ def unlink(model)
12
+ link_count = model.class.count(:conditions => ["attachment_id = ? AND id != ?", self.id, model.id])
13
+ if link_count == 0
14
+ destroy
15
+ end
16
+ end
17
+
18
+ def file=(file)
19
+ @file = file
20
+ self[:filename] = get_filename(file)
21
+ end
22
+
23
+ def filepath
24
+ @filepath ||= begin
25
+ digest = Digest::SHA1.hexdigest(self[:id].to_s)
26
+ "#{digest[0..0]}/#{digest[1..1]}/#{filename}"
27
+ end
28
+ end
29
+
30
+ private
31
+ def write_file
32
+ after_commit do
33
+ make_file(filepath, @file)
34
+ end
35
+ end
36
+
37
+ def destroy_file
38
+ after_commit do
39
+ remove_file
40
+ end
41
+ end
42
+
43
+ def make_file(path, data)
44
+ FileUtils::mkpath(File.dirname(path)) unless File.exist?(File.dirname(path))
45
+ if data.respond_to?(:rewind)
46
+ data.rewind
47
+ end
48
+ File.open(path, "wb") { |f| f.syswrite(data.read) }
49
+ end
50
+
51
+ def remove_file
52
+ FileUtils.rm(filepath)
53
+ end
54
+
55
+ def get_filename(file)
56
+ # make sure name is not corrupted
57
+ fname = file.original_filename.gsub(/[^a-zA-Z\-_0-9\.]/,'')
58
+ if fname[0..0] == '.'
59
+ # Forbid names starting with a dot
60
+ fname = Digest::SHA1.hexdigest(Time.now.to_i.to_s)[0..6]
61
+ end
62
+ fname
63
+ end
64
+ end # Attachment
65
+ end # SharedAttachment
66
+ end # Versions
@@ -0,0 +1,77 @@
1
+
2
+ module Zena
3
+ module Use
4
+
5
+ # The attachement module provides shared file attachments to a class with a copy-on-write
6
+ # pattern.
7
+ # Basically the module provides 'file=' and 'file' methods.
8
+ module SharedAttachment
9
+ module ClassMethods
10
+ def set_attachment_class(class_name)
11
+ belongs_to :attachment,
12
+ :class_name => class_name,
13
+ :foreign_key => 'attachment_id'
14
+ end
15
+ end
16
+
17
+ def self.included(base)
18
+ base.class_eval do
19
+ before_create :save_attachment
20
+ before_update :attachment_before_update
21
+ before_destroy :attachment_before_destroy
22
+
23
+ extend Zena::Use::SharedAttachment::ClassMethods
24
+ set_attachment_class 'Zena::Use::SharedAttachment::Attachment'
25
+ end
26
+ end
27
+
28
+ def file=(file)
29
+ if attachment
30
+ @attachment_to_unlink = self.attachment
31
+ self.attachment = nil
32
+ end
33
+ @attachment_need_save = true
34
+ self.build_attachment(:file => file)
35
+ end
36
+
37
+ def filepath
38
+ attachment ? attachment.filepath : nil
39
+ end
40
+
41
+ private
42
+ def save_attachment
43
+ if @attachment_need_save
44
+ @attachment_need_save = nil
45
+ attachment.save
46
+ else
47
+ true
48
+ end
49
+ end
50
+
51
+ def attachment_before_update
52
+ if @attachment_to_unlink
53
+ @attachment_to_unlink.unlink(self)
54
+ @attachment_to_unlink = nil
55
+ end
56
+ save_attachment
57
+ end
58
+
59
+ def attachment_before_destroy
60
+ if attachment = self.attachment
61
+ attachment.unlink(self)
62
+ else
63
+ true
64
+ end
65
+ end
66
+
67
+ def unlink_attachment_mark
68
+ @attachment_to_unlink = self.attachment
69
+ end
70
+
71
+ def unlink_attachment
72
+ end
73
+
74
+
75
+ end # SharedAttachment
76
+ end # Use
77
+ end # Zena
@@ -0,0 +1,2 @@
1
+ require 'versions/shared_attachment/attachment'
2
+ require 'versions/shared_attachment/owner'
@@ -0,0 +1,98 @@
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
data/lib/versions.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'versions/shared_attachment'
2
+ require 'versions/auto'
3
+ require 'versions/multi'
4
+ require 'versions/property'
5
+ require 'versions/transparent'
data/test/fixtures.rb ADDED
@@ -0,0 +1,34 @@
1
+ begin
2
+ class VersionsMigration < ActiveRecord::Migration
3
+ def self.down
4
+ drop_table 'pages'
5
+ drop_table 'versions'
6
+ end
7
+ def self.up
8
+ create_table 'pages' do |t|
9
+ t.integer 'version_id'
10
+ t.string 'name'
11
+ t.timestamps
12
+ end
13
+
14
+ create_table 'versions' do |t|
15
+ t.string 'title'
16
+ t.text 'text'
17
+ t.string 'properties'
18
+ t.integer 'attachment_id'
19
+ t.timestamps
20
+ end
21
+
22
+ create_table 'attachments' do |t|
23
+ t.string 'owner_table'
24
+ t.string 'filename'
25
+ t.timestamps
26
+ end
27
+ end
28
+ end
29
+
30
+ ActiveRecord::Base.establish_connection(:adapter=>'sqlite3', :database=>':memory:')
31
+ ActiveRecord::Migration.verbose = false
32
+ VersionsMigration.migrate(:up)
33
+ ActiveRecord::Migration.verbose = true
34
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+
8
+ require 'active_record'
9
+ require 'active_support'
10
+ require 'active_support/testing/assertions'
11
+ require 'versions'
12
+ require 'fixtures'
13
+
14
+ class Test::Unit::TestCase
15
+ include ActiveSupport::Testing::Assertions
16
+ end
@@ -0,0 +1,222 @@
1
+ require 'helper'
2
+
3
+ #class SharedAttachmentTest < Zena::Unit::TestCase
4
+ # self.use_transactional_fixtures = false
5
+ #
6
+ # Attachment = Class.new(Zena::Use::SharedAttachment::Attachment) do
7
+ # def filepath
8
+ # File.join(RAILS_ROOT, 'tmp', 'attachments', super)
9
+ # end
10
+ # end
11
+ #
12
+ # # Mock a document class with many versions
13
+ # class Document < ActiveRecord::Base
14
+ # include Zena::Use::MultiVersion
15
+ #
16
+ # set_table_name :nodes
17
+ # before_save :set_dummy_defaults
18
+ #
19
+ # def title=(title)
20
+ # version.title = title
21
+ # end
22
+ #
23
+ # def file=(file)
24
+ # version.file = file
25
+ # end
26
+ #
27
+ # def version
28
+ # @version ||= new_record? ? versions.build : versions.first(:order => 'id DESC')
29
+ # end
30
+ # private
31
+ # def set_dummy_defaults
32
+ # self[:user_id] ||= 0
33
+ # end
34
+ # end
35
+ #
36
+ # # Mock a version class with shared attachments (between versions of the same document)
37
+ # class Version < ActiveRecord::Base
38
+ # include Zena::Use::MultiVersion::Version
39
+ # include Zena::Use::AutoVersion
40
+ # include Zena::Use::SharedAttachment
41
+ # set_attachment_class 'SharedAttachmentTest::Attachment'
42
+ # set_table_name :versions
43
+ #
44
+ # def should_clone?
45
+ # true
46
+ # end
47
+ #
48
+ # private
49
+ # def setup_version_on_create
50
+ # # Dummy values when testing Version without a Document
51
+ # self[:node_id] ||= 0
52
+ # self[:user_id] ||= 0
53
+ # self[:status] ||= 0
54
+ # end
55
+ # end
56
+ #
57
+ # def setup
58
+ # FileUtils.rmtree(File.join(RAILS_ROOT, 'tmp', 'attachments'))
59
+ # end
60
+ #
61
+ # context 'When creating a new owner' do
62
+ # setup do
63
+ # @owner = Version.create(:file => uploaded_jpg('bird.jpg'))
64
+ # end
65
+ #
66
+ # should 'store file in the filesystem' do
67
+ # assert File.exist?(@owner.filepath)
68
+ # assert_equal uploaded_jpg('bird.jpg').read, File.read(@owner.filepath)
69
+ # end
70
+ #
71
+ # should 'restore the filepath from the database' do
72
+ # attachment = Attachment.find(@owner.attachment_id)
73
+ # assert_equal @owner.filepath, attachment.filepath
74
+ # end
75
+ # end
76
+ #
77
+ # context 'When the transaction fails' do
78
+ # should 'not write file on create' do
79
+ # owner = nil
80
+ # filepath = nil
81
+ # assert_difference('Attachment.count', 0) do
82
+ # Version.transaction do
83
+ # owner = Version.create(:file => uploaded_jpg('bird.jpg'))
84
+ # filepath = owner.filepath
85
+ # assert !File.exist?(filepath)
86
+ # raise ActiveRecord::Rollback
87
+ # end
88
+ # end
89
+ # assert !File.exist?(filepath)
90
+ # end
91
+ #
92
+ # should 'not remove file on destroy' do
93
+ # @owner = Version.create(:file => uploaded_jpg('bird.jpg'))
94
+ # filepath = @owner.filepath
95
+ # assert File.exist?(filepath)
96
+ # assert_difference('Attachment.count', 0) do
97
+ # Version.transaction do
98
+ # @owner.destroy
99
+ # assert File.exist?(filepath)
100
+ # raise ActiveRecord::Rollback
101
+ # end
102
+ # end
103
+ # assert File.exist?(filepath)
104
+ # end
105
+ # end
106
+ #
107
+ # context 'On an owner with a file' do
108
+ # setup do
109
+ # @owner = Version.create(:file => uploaded_jpg('bird.jpg'))
110
+ # @owner = Version.find(@owner.id)
111
+ # end
112
+ #
113
+ # should 'remove file in the filesystem when updating file' do
114
+ # old_filepath = @owner.filepath
115
+ # puts "Start"
116
+ # assert_difference('Attachment.count', 0) do # destroy + create
117
+ # assert @owner.update_attributes(:file => uploaded_jpg('lake.jpg'))
118
+ # end
119
+ # assert_not_equal old_filepath, @owner.filepath
120
+ # assert File.exist?(@owner.filepath)
121
+ # assert_equal uploaded_jpg('lake.jpg').read, File.read(@owner.filepath)
122
+ # assert !File.exist?(old_filepath)
123
+ # end
124
+ # end
125
+ #
126
+ # context 'Updating document' do
127
+ # setup do
128
+ # begin
129
+ # @doc = Document.create(:title => 'birdy', :file => uploaded_jpg('bird.jpg'))
130
+ # rescue => err
131
+ # puts err.message
132
+ # puts err.backtrace.join("\n")
133
+ # end
134
+ # end
135
+ #
136
+ # # Updating document ...attributes
137
+ # context 'attributes' do
138
+ # setup do
139
+ # assert_difference('Version.count', 1) do
140
+ # @doc.update_attributes(:title => 'hopla')
141
+ # end
142
+ # end
143
+ #
144
+ # should 'reuse the same filepath in new versions' do
145
+ # filepath = nil
146
+ # @doc.versions.each do |version|
147
+ # if filepath
148
+ # assert_equal filepath, version.filepath
149
+ # else
150
+ # filepath = version.filepath
151
+ # end
152
+ # end
153
+ # end
154
+ # end
155
+ #
156
+ # # Updating document ...file
157
+ # context 'file' do
158
+ # setup do
159
+ # assert_difference('Version.count', 1) do
160
+ # @doc.update_attributes(:file => uploaded_jpg('lake.jpg'))
161
+ # end
162
+ # end
163
+ #
164
+ # should 'create new filepath' do
165
+ # filepath = nil
166
+ # @doc.versions.each do |version|
167
+ # if filepath
168
+ # assert_not_equal filepath, version.filepath
169
+ # else
170
+ # filepath = version.filepath
171
+ # end
172
+ # end
173
+ # end
174
+ # end # Updating document .. file
175
+ # end # Updating document
176
+ #
177
+ # context 'On a document with many versions' do
178
+ # setup do
179
+ # assert_difference('Version.count', 2) do
180
+ # @doc = Document.create(:title => 'birdy', :file => uploaded_jpg('bird.jpg'))
181
+ # @doc.update_attributes(:title => 'Vögel')
182
+ # @doc = Document.find(@doc.id)
183
+ # end
184
+ # end
185
+ #
186
+ # context 'removing a version' do
187
+ #
188
+ # should 'not remove shared attachment' do
189
+ # filepath = @doc.version.filepath
190
+ #
191
+ # assert_difference('Version.count', -1) do
192
+ # assert_difference('Attachment.count', 0) do
193
+ # assert @doc.version.destroy
194
+ # end
195
+ # end
196
+ # assert File.exist?(filepath)
197
+ # end
198
+ # end
199
+ #
200
+ # context 'removing the last version' do
201
+ #
202
+ # should 'remove shared attachment' do
203
+ # filepath = @doc.version.filepath
204
+ #
205
+ # assert_difference('Version.count', -2) do
206
+ # assert_difference('Attachment.count', -1) do
207
+ # @doc.versions.each do |version|
208
+ # assert version.destroy
209
+ # end
210
+ # end
211
+ # end
212
+ # assert !File.exist?(filepath)
213
+ # end
214
+ # end
215
+ # end
216
+ #
217
+ # private
218
+ # def filepath(attachment_id, filename)
219
+ # digest = Digest::SHA1.hexdigest(attachment_id.to_s)
220
+ # "#{digest[0..0]}/#{digest[1..1]}/#{filename}"
221
+ # end
222
+ #end
@@ -0,0 +1,85 @@
1
+ require 'helper'
2
+
3
+ class AutoTest < Test::Unit::TestCase
4
+
5
+ class BadVersion < ActiveRecord::Base
6
+ include Versions::Auto
7
+ set_table_name :versions
8
+ end
9
+
10
+ class Version < ActiveRecord::Base
11
+ attr_accessor :should_clone, :messages
12
+ include Versions::Auto
13
+
14
+ def should_clone?
15
+ @should_clone
16
+ end
17
+
18
+ def cloned
19
+ @messages ||= []
20
+ @messages << 'cloned'
21
+ end
22
+ end
23
+
24
+ context 'An instance of a class with Auto included' do
25
+ subject { @version }
26
+
27
+ context 'without should_clone' do
28
+ setup do
29
+ @version = BadVersion.create('title' => 'Socrate')
30
+ end
31
+
32
+ should 'raise an exception on update' do
33
+ assert_raise(NoMethodError) { subject.update_attributes('title' => 'Aristotle') }
34
+ end
35
+ end
36
+
37
+ context 'with should_clone' do
38
+ setup do
39
+ @version = Version.create('title' => 'Socrate')
40
+ end
41
+
42
+ context 'returning false' do
43
+ should 'update record if should_clone is false' do
44
+ assert_difference('Version.count', 0) do
45
+ assert subject.update_attributes('title' => 'Aristotle')
46
+ end
47
+ end
48
+
49
+ should 'not call cloned before saving' do
50
+ assert_nil subject.messages
51
+ subject.update_attributes('title' => 'Aristotle')
52
+ assert_nil subject.messages
53
+ end
54
+
55
+ should 'return false on cloned?' do
56
+ subject.update_attributes('title' => 'Aristotle')
57
+ assert !subject.cloned?
58
+ end
59
+ end
60
+
61
+ context 'returning true' do
62
+ setup do
63
+ subject.should_clone = true
64
+ end
65
+
66
+ should 'duplicate record' do
67
+ assert_difference('Version.count', 1) do
68
+ assert subject.update_attributes('title' => 'Aristotle')
69
+ end
70
+ end
71
+
72
+ should 'call cloned before saving' do
73
+ assert_nil subject.messages
74
+ subject.update_attributes('title' => 'Aristotle')
75
+ assert_equal ['cloned'], subject.messages
76
+ end
77
+
78
+ should 'return true on cloned?' do
79
+ subject.update_attributes('title' => 'Aristotle')
80
+ assert subject.cloned?
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class MultiVersionTest < Test::Unit::TestCase
4
+ def test_truth
5
+ assert true
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TransparentTest < Test::Unit::TestCase
4
+ def test_truth
5
+ assert true
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: versions
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Gaspard Bucher
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-02-13 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: shoulda
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: shoulda
27
+ type: :development
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: activerecord
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
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
+ 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
+ email: gaspard@teti.ch
57
+ executables: []
58
+
59
+ extensions: []
60
+
61
+ extra_rdoc_files:
62
+ - LICENSE
63
+ - README.rdoc
64
+ files:
65
+ - .document
66
+ - .gitignore
67
+ - LICENSE
68
+ - README.rdoc
69
+ - Rakefile
70
+ - lib/versions.rb
71
+ - lib/versions/auto.rb
72
+ - lib/versions/multi.rb
73
+ - lib/versions/property.rb
74
+ - lib/versions/shared_attachment.rb
75
+ - lib/versions/shared_attachment/attachment.rb
76
+ - lib/versions/shared_attachment/owner.rb
77
+ - lib/versions/transparent.rb
78
+ - test/fixtures.rb
79
+ - test/helper.rb
80
+ - test/unit/attachment_test.rb
81
+ - test/unit/auto_test.rb
82
+ - test/unit/multi_test.rb
83
+ - test/unit/transparent_test.rb
84
+ has_rdoc: true
85
+ homepage: http://zenadmin.org/650
86
+ licenses: []
87
+
88
+ post_install_message:
89
+ rdoc_options:
90
+ - --charset=UTF-8
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: "0"
98
+ version:
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: "0"
104
+ version:
105
+ requirements: []
106
+
107
+ rubyforge_project:
108
+ rubygems_version: 1.3.5
109
+ signing_key:
110
+ specification_version: 3
111
+ summary: A list of libraries to work with ActiveRecord model versioning
112
+ test_files:
113
+ - test/fixtures.rb
114
+ - test/helper.rb
115
+ - test/unit/attachment_test.rb
116
+ - test/unit/auto_test.rb
117
+ - test/unit/multi_test.rb
118
+ - test/unit/transparent_test.rb