versions 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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