acts_as_trashable 1.0.3

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ pkg
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Brian Durand
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,37 @@
1
+ = ActsAsTrashable
2
+
3
+ This gem is designed to reduce the risk of adding a delete function to your application by allowing you to restore records that have been destroyed.
4
+
5
+ Often it makes sense to add a function to delete records so that your production database isn't polluted with bad records. However, you can quickly regret this feature should one of your users get a little trigger happy and delete something they shouldn't have. Restoring the lost data from the database can be time consuming and may not even be possible. The inspiration for this gem was a user who didn't understand that the "reject" function deleted the data for everyone and not just from that user's view.
6
+
7
+ To protect yourself, simply declare acts_as_trashable in any active record model. This will cause the record to be serialized to a trash_records table before it is deleted when you call destroy. In addition, any has_and_belongs_to_many associations or any has_many or has_one associations declared with :dependent => :destroy will also be serialized. These associations should not have acts_as_trashable (there's no harm if they do, you just end up with twice as much trash). You must run the included migration to create the trash_records table.
8
+
9
+ == Restoring Records
10
+
11
+ These trash records and all their associations can be restored later by calling restore! on the TrashRecord records. Calling restore! will also delete the TrashRecord. You can also call restore (without the exclamation) which will just restore the original record and associations to memory without saving to the database or deleting the trash. This can be useful if you only need to inspect the trash record or if you've changed the model since the original record was created so that requires some additional processing before it can be saved. If you have a record which cannot be restored due to validation errors, you can try calling save_without_validation on the restored record.
12
+
13
+ ActsAsTrashable::TrashRecord.find(id).restore.save_without_validation
14
+
15
+ In many cases this should work just fine since presumably the record was valid when it was originally destroyed. When records are restored, the id values are also restored to the original values.
16
+
17
+ The ActsAsTrashable::ClassMethods are mixed into your model when you call acts_as_trashable. These provide some convenience methods for restoring and emptying the trash for only a specific class.
18
+
19
+ == Disabling
20
+
21
+ If you wish to destroy a record without trashing it, perform the destroy inside a disable_trash block on the model (i.e. record.disable_trash{record.destroy}). Also, this gem does not affect the delete or delete_all methods on active record. These will still delete the records directly from the database.
22
+
23
+ == Setup
24
+
25
+ To create the table structure for ActsAsTrashable::TrashRecord, simply add a migration to your project that calls
26
+
27
+ ActsAsTrashable::TrashRecord.create_table
28
+
29
+ == Keeping It Clean
30
+
31
+ To keep your trash table from filling up, you can call empty_trash on TrashRecord or on any ActsAsTrashable model. These methods clear out records older than a specified number of seconds. Trashed records are compressed in the database to conserve the amount of disk space they take up.
32
+
33
+ == Other Solutions
34
+
35
+ Another method of solving the issue that this gem addresses is to add a status flag on your model and consider records with the flag set to be deleted (for example see ActsAsParanoid[http://ar-paranoid.rubyforge.org/]). This is definitely a safer method of keeping the records since the data is never actually deleted. However, it presents a more complicated solution and you'll need to guard against ever accidentally showing a deleted record. The best method to use will depend on your application.
36
+
37
+ Finally, this gem has a companion gem ActsAsRevisionable[http://github.com/bdurand/acts_as_revisionable] that allows you to keep a history of revisions to records each time they get updated. Using both together gives you a more robust restoration system. They are not packaged together because some applications may not have a use for the revisioning but do need the protection against accidental deletion.
data/Rakefile ADDED
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ begin
9
+ require 'spec/rake/spectask'
10
+ desc 'Test the gem.'
11
+ Spec::Rake::SpecTask.new(:test) do |t|
12
+ t.spec_files = FileList.new('spec/**/*_spec.rb')
13
+ end
14
+ rescue LoadError
15
+ tast :test do
16
+ STDERR.puts "You must have rspec >= 1.3.0 to run the tests"
17
+ end
18
+ end
19
+
20
+ desc 'Generate documentation for the gem.'
21
+ Rake::RDocTask.new(:rdoc) do |rdoc|
22
+ rdoc.rdoc_dir = 'rdoc'
23
+ rdoc.options << '--title' << 'Acts As Trashable' << '--line-numbers' << '--inline-source' << '--main' << 'README.rdoc'
24
+ rdoc.rdoc_files.include('README.rdoc')
25
+ rdoc.rdoc_files.include('lib/**/*.rb')
26
+ end
27
+
28
+ begin
29
+ require 'jeweler'
30
+ Jeweler::Tasks.new do |gem|
31
+ gem.name = "acts_as_trashable"
32
+ gem.summary = %Q{ActiveRecord extension that serializes destroyed records into a trash table from which they can be restored.}
33
+ gem.description = %Q(ActiveRecord extension that serializes destroyed records into a trash table from which they can be restored. This is intended to reduce the risk of users misusing your application's delete function and losing data.)
34
+ gem.email = "brian@embellishedvisions.com"
35
+ gem.homepage = "http://github.com/bdurand/acts_as_trashable"
36
+ gem.authors = ["Brian Durand"]
37
+ gem.rdoc_options = ["--charset=UTF-8", "--main", "README.rdoc"]
38
+
39
+ gem.add_dependency('activerecord', '>= 2.2')
40
+ gem.add_development_dependency('sqlite3')
41
+ gem.add_development_dependency('rspec', '>= 1.3.0')
42
+ gem.add_development_dependency('jeweler')
43
+ end
44
+
45
+ Jeweler::GemcutterTasks.new
46
+ rescue LoadError
47
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.3
@@ -0,0 +1,66 @@
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{acts_as_trashable}
8
+ s.version = "1.0.3"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Brian Durand"]
12
+ s.date = %q{2010-06-22}
13
+ s.description = %q{ActiveRecord extension that serializes destroyed records into a trash table from which they can be restored. This is intended to reduce the risk of users misusing your application's delete function and losing data.}
14
+ s.email = %q{brian@embellishedvisions.com}
15
+ s.extra_rdoc_files = [
16
+ "README.rdoc"
17
+ ]
18
+ s.files = [
19
+ ".gitignore",
20
+ "MIT-LICENSE",
21
+ "README.rdoc",
22
+ "Rakefile",
23
+ "VERSION",
24
+ "acts_as_trashable.gemspec",
25
+ "lib/acts_as_trashable.rb",
26
+ "lib/acts_as_trashable/trash_record.rb",
27
+ "spec/acts_as_trashable_spec.rb",
28
+ "spec/full_spec.rb",
29
+ "spec/spec_helper.rb",
30
+ "spec/trash_record_spec.rb"
31
+ ]
32
+ s.homepage = %q{http://github.com/bdurand/acts_as_trashable}
33
+ s.rdoc_options = ["--charset=UTF-8", "--main", "README.rdoc"]
34
+ s.require_paths = ["lib"]
35
+ s.rubygems_version = %q{1.3.7}
36
+ s.summary = %q{ActiveRecord extension that serializes destroyed records into a trash table from which they can be restored.}
37
+ s.test_files = [
38
+ "spec/acts_as_trashable_spec.rb",
39
+ "spec/full_spec.rb",
40
+ "spec/spec_helper.rb",
41
+ "spec/trash_record_spec.rb"
42
+ ]
43
+
44
+ if s.respond_to? :specification_version then
45
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
46
+ s.specification_version = 3
47
+
48
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
49
+ s.add_runtime_dependency(%q<activerecord>, [">= 2.2"])
50
+ s.add_development_dependency(%q<sqlite3>, [">= 0"])
51
+ s.add_development_dependency(%q<rspec>, [">= 1.3.0"])
52
+ s.add_development_dependency(%q<jeweler>, [">= 0"])
53
+ else
54
+ s.add_dependency(%q<activerecord>, [">= 2.2"])
55
+ s.add_dependency(%q<sqlite3>, [">= 0"])
56
+ s.add_dependency(%q<rspec>, [">= 1.3.0"])
57
+ s.add_dependency(%q<jeweler>, [">= 0"])
58
+ end
59
+ else
60
+ s.add_dependency(%q<activerecord>, [">= 2.2"])
61
+ s.add_dependency(%q<sqlite3>, [">= 0"])
62
+ s.add_dependency(%q<rspec>, [">= 1.3.0"])
63
+ s.add_dependency(%q<jeweler>, [">= 0"])
64
+ end
65
+ end
66
+
@@ -0,0 +1,166 @@
1
+ require 'zlib'
2
+
3
+ module ActsAsTrashable
4
+ class TrashRecord < ActiveRecord::Base
5
+
6
+ set_table_name "trash_records"
7
+
8
+ class << self
9
+ # Find a trash entry by class and id.
10
+ def find_trash (klass, id)
11
+ find(:all, :conditions => {:trashable_type => klass.base_class.name, :trashable_id => id}).last
12
+ end
13
+
14
+ # Empty the trash by deleting records older than the specified maximum age. You can optionally specify
15
+ # :only or :except in the options hash with a class or array of classes as the value to limit the trashed
16
+ # classes which should be cleared. This is useful if you want to keep different classes for different
17
+ # lengths of time.
18
+ def empty_trash (max_age, options = {})
19
+ sql = 'created_at <= ?'
20
+ args = [max_age.ago]
21
+
22
+ vals = options[:only] || options[:except]
23
+ if vals
24
+ vals = [vals] unless vals.kind_of?(Array)
25
+ sql << ' AND trashable_type'
26
+ sql << ' NOT' unless options[:only]
27
+ sql << " IN (#{vals.collect{|v| '?'}.join(', ')})"
28
+ args.concat(vals.collect{|v| v.kind_of?(Class) ? v.base_class.name : v.to_s.camelize})
29
+ end
30
+
31
+ delete_all([sql] + args)
32
+ end
33
+
34
+ def create_table
35
+ connection.create_table :trash_records do |t|
36
+ t.string :trashable_type, :null => false
37
+ t.integer :trashable_id, :null => false
38
+ t.binary :data, :limit => 5.megabytes
39
+ t.timestamp :created_at
40
+ end
41
+
42
+ connection.add_index :trash_records, [:trashable_type, :trashable_id], :name => "trashable"
43
+ connection.add_index :trash_records, [:created_at, :trashable_type], :name => "created_at_type"
44
+ end
45
+ end
46
+
47
+ # Create a new trash record for the provided record.
48
+ def initialize (record)
49
+ super({})
50
+ self.trashable_type = record.class.base_class.name
51
+ self.trashable_id = record.id
52
+ self.data = Zlib::Deflate.deflate(Marshal.dump(serialize_attributes(record)))
53
+ end
54
+
55
+ # Restore a trashed record into an object. The record will not be saved.
56
+ def restore
57
+ restore_class = self.trashable_type.constantize
58
+
59
+ # Check if we have a type field, if yes, assume single table inheritance and restore the actual class instead of the stored base class
60
+ sti_type = self.trashable_attributes[restore_class.inheritance_column]
61
+ if sti_type
62
+ begin
63
+ restore_class = self.trashable_type.send(:type_name_with_module, sti_type).constantize
64
+ rescue NameError
65
+ # Seems our assumption was wrong and we have no STI
66
+ end
67
+ end
68
+
69
+ attrs, association_attrs = attributes_and_associations(restore_class, self.trashable_attributes)
70
+
71
+ record = restore_class.new
72
+ attrs.each_pair do |key, value|
73
+ record.send("#{key}=", value)
74
+ end
75
+
76
+ association_attrs.each_pair do |association, attribute_values|
77
+ restore_association(record, association, attribute_values)
78
+ end
79
+
80
+ return record
81
+ end
82
+
83
+ # Restore a trashed record into an object, save it, and delete the trash entry.
84
+ def restore!
85
+ record = self.restore
86
+ record.save!
87
+ self.destroy
88
+ return record
89
+ end
90
+
91
+ # Attributes of the trashed record as a hash.
92
+ def trashable_attributes
93
+ return nil unless self.data
94
+ uncompressed = Zlib::Inflate.inflate(self.data) rescue uncompressed = self.data # backward compatibility with uncompressed data
95
+ Marshal.load(uncompressed)
96
+ end
97
+
98
+ private
99
+
100
+ def serialize_attributes (record, already_serialized = {})
101
+ return if already_serialized["#{record.class}.#{record.id}"]
102
+ attrs = record.attributes.dup
103
+ already_serialized["#{record.class}.#{record.id}"] = true
104
+
105
+ record.class.reflections.values.each do |association|
106
+ if association.macro == :has_many and [:destroy, :delete_all].include?(association.options[:dependent])
107
+ attrs[association.name] = record.send(association.name).collect{|r| serialize_attributes(r, already_serialized)}
108
+ elsif association.macro == :has_one and [:destroy, :delete_all].include?(association.options[:dependent])
109
+ associated = record.send(association.name)
110
+ attrs[association.name] = serialize_attributes(associated, already_serialized) unless associated.nil?
111
+ elsif association.macro == :has_and_belongs_to_many
112
+ attrs[association.name] = record.send("#{association.name.to_s.singularize}_ids".to_sym)
113
+ end
114
+ end
115
+
116
+ return attrs
117
+ end
118
+
119
+ def attributes_and_associations (klass, hash)
120
+ attrs = {}
121
+ association_attrs = {}
122
+
123
+ hash.each_pair do |key, value|
124
+ if klass.reflections.include?(key)
125
+ association_attrs[key] = value
126
+ else
127
+ attrs[key] = value
128
+ end
129
+ end
130
+
131
+ return [attrs, association_attrs]
132
+ end
133
+
134
+ def restore_association (record, association, attributes)
135
+ reflection = record.class.reflections[association]
136
+ associated_record = nil
137
+ if reflection.macro == :has_many
138
+ if attributes.kind_of?(Array)
139
+ attributes.each do |association_attributes|
140
+ restore_association(record, association, association_attributes)
141
+ end
142
+ else
143
+ associated_record = record.send(association).build
144
+ end
145
+ elsif reflection.macro == :has_one
146
+ associated_record = reflection.klass.new
147
+ record.send("#{association}=", associated_record)
148
+ elsif reflection.macro == :has_and_belongs_to_many
149
+ record.send("#{association.to_s.singularize}_ids=", attributes)
150
+ return
151
+ end
152
+
153
+ return unless associated_record
154
+
155
+ attrs, association_attrs = attributes_and_associations(associated_record.class, attributes)
156
+ attrs.each_pair do |key, value|
157
+ associated_record.send("#{key}=", value)
158
+ end
159
+
160
+ association_attrs.each_pair do |key, values|
161
+ restore_association(associated_record, key, values)
162
+ end
163
+ end
164
+
165
+ end
166
+ end
@@ -0,0 +1,64 @@
1
+ require 'active_record'
2
+ require 'active_support/all'
3
+
4
+ module ActsAsTrashable
5
+
6
+ autoload :TrashRecord, File.expand_path('../acts_as_trashable/trash_record', __FILE__)
7
+
8
+ def self.included (base)
9
+ base.extend(ActsMethods)
10
+ end
11
+
12
+ module ActsMethods
13
+ # Class method that injects the trash behavior into the class.
14
+ def acts_as_trashable
15
+ extend ClassMethods
16
+ include InstanceMethods
17
+ alias_method_chain :destroy, :trash
18
+ end
19
+ end
20
+
21
+ module ClassMethods
22
+ # Empty the trash for this class of all entries older than the specified maximum age in seconds.
23
+ def empty_trash (max_age)
24
+ TrashRecord.empty_trash(max_age, :only => self)
25
+ end
26
+
27
+ # Restore a particular entry by id from the trash into an object in memory. The record will not be saved.
28
+ def restore_trash (id)
29
+ trash = TrashRecord.find_trash(self, id)
30
+ return trash.restore if trash
31
+ end
32
+
33
+ # Restore a particular entry by id from the trash, save it, and delete the trash entry.
34
+ def restore_trash! (id)
35
+ trash = TrashRecord.find_trash(self, id)
36
+ return trash.restore! if trash
37
+ end
38
+ end
39
+
40
+ module InstanceMethods
41
+ def destroy_with_trash
42
+ return destroy_without_trash if @acts_as_trashable_disabled
43
+ TrashRecord.transaction do
44
+ trash = TrashRecord.new(self)
45
+ trash.save!
46
+ return destroy_without_trash
47
+ end
48
+ end
49
+
50
+ # Call this method to temporarily disable the trash feature within a block.
51
+ def disable_trash
52
+ save_val = @acts_as_trashable_disabled
53
+ begin
54
+ @acts_as_trashable_disabled = true
55
+ yield if block_given?
56
+ ensure
57
+ @acts_as_trashable_disabled = save_val
58
+ end
59
+ end
60
+ end
61
+
62
+ end
63
+
64
+ ActiveRecord::Base.send(:include, ActsAsTrashable)
@@ -0,0 +1,71 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe ActsAsTrashable do
4
+
5
+ before :all do
6
+ ActsAsTrashable::Test.create_database
7
+ end
8
+
9
+ after :all do
10
+ ActsAsTrashable::Test.delete_database
11
+ end
12
+
13
+ class TestTrashableModel
14
+ include ActsAsTrashable
15
+
16
+ def destroy
17
+ really_destroy
18
+ end
19
+
20
+ def really_destroy
21
+ end
22
+
23
+ acts_as_trashable
24
+ end
25
+
26
+ it "should be able to inject trashable behavior onto ActiveRecord::Base" do
27
+ ActiveRecord::Base.included_modules.should include(ActsAsTrashable)
28
+ end
29
+
30
+ it "should create a trash entry when a model is destroyed" do
31
+ record = TestTrashableModel.new
32
+ trash = mock(:trash)
33
+ ActsAsTrashable::TrashRecord.should_receive(:transaction).and_yield
34
+ ActsAsTrashable::TrashRecord.should_receive(:new).with(record).and_return(trash)
35
+ trash.should_receive(:save!)
36
+ record.should_receive(:really_destroy).and_return(:retval)
37
+ record.destroy.should == :retval
38
+ end
39
+
40
+ it "should not create a trash entry when a model is destroyed inside a disable block" do
41
+ record = TestTrashableModel.new
42
+ ActsAsTrashable::TrashRecord.should_not_receive(:transaction)
43
+ ActsAsTrashable::TrashRecord.should_not_receive(:new)
44
+ record.should_receive(:really_destroy).and_return(:retval)
45
+ record.disable_trash do
46
+ record.destroy.should == :retval
47
+ end
48
+ end
49
+
50
+ it "should be able to empty the trash based on age" do
51
+ ActsAsTrashable::TrashRecord.should_receive(:empty_trash).with(1.day, :only => TestTrashableModel)
52
+ TestTrashableModel.empty_trash(1.day)
53
+ end
54
+
55
+ it "should be able to restore a record by id" do
56
+ trash = mock(:trash)
57
+ record = mock(:record)
58
+ ActsAsTrashable::TrashRecord.should_receive(:find_trash).with(TestTrashableModel, 1).and_return(trash)
59
+ trash.should_receive(:restore).and_return(record)
60
+ TestTrashableModel.restore_trash(1).should == record
61
+ end
62
+
63
+ it "should be able to restore a record by id and save it" do
64
+ trash = mock(:trash)
65
+ record = mock(:record)
66
+ ActsAsTrashable::TrashRecord.should_receive(:find_trash).with(TestTrashableModel, 1).and_return(trash)
67
+ trash.should_receive(:restore!).and_return(record)
68
+ TestTrashableModel.restore_trash!(1).should == record
69
+ end
70
+
71
+ end
data/spec/full_spec.rb ADDED
@@ -0,0 +1,258 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "ActsAsTrashable Full Test" do
4
+
5
+ before :all do
6
+ ActsAsTrashable::Test.create_database
7
+
8
+ class TrashableTestSubThing < ActiveRecord::Base
9
+ connection.create_table(:trashable_test_sub_things) do |t|
10
+ t.column :name, :string
11
+ t.column :trashable_test_many_thing_id, :integer
12
+ end unless table_exists?
13
+ end
14
+
15
+ class TrashableTestManyThing < ActiveRecord::Base
16
+ connection.create_table(:trashable_test_many_things) do |t|
17
+ t.column :name, :string
18
+ t.column :trashable_test_model_id, :integer
19
+ end unless table_exists?
20
+
21
+ has_many :sub_things, :class_name => 'TrashableTestSubThing', :dependent => :destroy
22
+ end
23
+
24
+ class TrashableTestManyOtherThing < ActiveRecord::Base
25
+ connection.create_table(:trashable_test_many_other_things) do |t|
26
+ t.column :name, :string
27
+ t.column :trashable_test_model_id, :integer
28
+ end unless table_exists?
29
+ end
30
+
31
+ class TrashableTestOneThing < ActiveRecord::Base
32
+ connection.create_table(:trashable_test_one_things) do |t|
33
+ t.column :name, :string
34
+ t.column :trashable_test_model_id, :integer
35
+ end unless table_exists?
36
+ end
37
+
38
+ class NonTrashableTestModel < ActiveRecord::Base
39
+ connection.create_table(:non_trashable_test_models) do |t|
40
+ t.column :name, :string
41
+ end unless table_exists?
42
+ end
43
+
44
+ class NonTrashableTestModelsTrashableTestModel < ActiveRecord::Base
45
+ connection.create_table(:non_trashable_test_models_trashable_test_models, :id => false) do |t|
46
+ t.column :non_trashable_test_model_id, :integer
47
+ t.column :trashable_test_model_id, :integer
48
+ end unless table_exists?
49
+ end
50
+
51
+ class TrashableTestModel < ActiveRecord::Base
52
+ connection.create_table(:trashable_test_models) do |t|
53
+ t.column :name, :string
54
+ t.column :secret, :integer
55
+ end unless table_exists?
56
+
57
+ has_many :many_things, :class_name => 'TrashableTestManyThing', :dependent => :destroy
58
+ has_many :many_other_things, :class_name => 'TrashableTestManyOtherThing'
59
+ has_one :one_thing, :class_name => 'TrashableTestOneThing', :dependent => :destroy
60
+ has_and_belongs_to_many :non_trashable_test_models
61
+
62
+ attr_protected :secret
63
+
64
+ acts_as_trashable
65
+
66
+ def set_secret (val)
67
+ self.secret = val
68
+ end
69
+
70
+ private
71
+
72
+ def secret= (val)
73
+ self[:secret] = val
74
+ end
75
+ end
76
+
77
+ module ActsAsTrashable
78
+ class TrashableNamespaceModel < ActiveRecord::Base
79
+ connection.create_table(:trashable_namespace_models) do |t|
80
+ t.column :name, :string
81
+ t.column :type_name, :string
82
+ end unless table_exists?
83
+
84
+ set_inheritance_column :type_name
85
+ acts_as_trashable
86
+ end
87
+
88
+ class TrashableSubclassModel < TrashableNamespaceModel
89
+ end
90
+ end
91
+ end
92
+
93
+ after :all do
94
+ ActsAsTrashable::Test.delete_database
95
+ end
96
+
97
+ before :each do
98
+ TrashableTestModel.delete_all
99
+ TrashableTestManyThing.delete_all
100
+ TrashableTestManyOtherThing.delete_all
101
+ TrashableTestSubThing.delete_all
102
+ TrashableTestOneThing.delete_all
103
+ NonTrashableTestModelsTrashableTestModel.delete_all
104
+ NonTrashableTestModel.delete_all
105
+ ActsAsTrashable::TrashRecord.delete_all
106
+ ActsAsTrashable::TrashableNamespaceModel.delete_all
107
+ end
108
+
109
+ it "should be able to trash a record and restore without associations" do
110
+ model = TrashableTestModel.new
111
+ model.name = 'test'
112
+ model.send :secret=, 123
113
+ model.save!
114
+ ActsAsTrashable::TrashRecord.count.should == 0
115
+
116
+ model.destroy
117
+ ActsAsTrashable::TrashRecord.count.should == 1
118
+ TrashableTestModel.count.should == 0
119
+
120
+ restored = TrashableTestModel.restore_trash!(model.id)
121
+ restored.reload
122
+ restored.name.should == 'test'
123
+ restored.secret.should == 123
124
+ ActsAsTrashable::TrashRecord.count.should == 0
125
+ TrashableTestModel.count.should == 1
126
+ end
127
+
128
+ it "should be able to disable trash behavior" do
129
+ model = TrashableTestModel.new
130
+ model.name = 'test'
131
+ model.save!
132
+ ActsAsTrashable::TrashRecord.count.should == 0
133
+
134
+ model.disable_trash do
135
+ model.destroy
136
+ end
137
+ ActsAsTrashable::TrashRecord.count.should == 0
138
+ TrashableTestModel.count.should == 0
139
+ end
140
+
141
+ it "should be able to trash a record and restore it with has_many associations" do
142
+ many_thing_1 = TrashableTestManyThing.new(:name => 'many_thing_1')
143
+ many_thing_1.sub_things.build(:name => 'sub_thing_1')
144
+ many_thing_1.sub_things.build(:name => 'sub_thing_2')
145
+
146
+ model = TrashableTestModel.new(:name => 'test')
147
+ model.many_things << many_thing_1
148
+ model.many_things.build(:name => 'many_thing_2')
149
+ model.many_other_things.build(:name => 'many_other_thing_1')
150
+ model.many_other_things.build(:name => 'many_other_thing_2')
151
+ model.save!
152
+ model.reload
153
+ TrashableTestManyThing.count.should == 2
154
+ TrashableTestSubThing.count.should == 2
155
+ TrashableTestManyOtherThing.count.should == 2
156
+ ActsAsTrashable::TrashRecord.count.should == 0
157
+
158
+ model.destroy
159
+ ActsAsTrashable::TrashRecord.count.should == 1
160
+ TrashableTestModel.count.should == 0
161
+ TrashableTestManyThing.count.should == 0
162
+ TrashableTestSubThing.count.should == 0
163
+ TrashableTestManyOtherThing.count.should == 2
164
+
165
+ restored = TrashableTestModel.restore_trash!(model.id)
166
+ restored.reload
167
+ restored.name.should == 'test'
168
+ restored.many_things.collect{|t| t.name}.sort.should == ['many_thing_1', 'many_thing_2']
169
+ restored.many_things.detect{|t| t.name == 'many_thing_1'}.sub_things.collect{|t| t.name}.sort.should == ['sub_thing_1', 'sub_thing_2']
170
+ restored.many_other_things.collect{|t| t.name}.sort.should == ['many_other_thing_1', 'many_other_thing_2']
171
+ ActsAsTrashable::TrashRecord.count.should == 0
172
+ TrashableTestModel.count.should == 1
173
+ TrashableTestManyThing.count.should == 2
174
+ TrashableTestSubThing.count.should == 2
175
+ TrashableTestManyOtherThing.count.should == 2
176
+ end
177
+
178
+ it "should be able to trash a record and restore it with has_one associations" do
179
+ model = TrashableTestModel.new(:name => 'test')
180
+ model.build_one_thing(:name => 'other')
181
+ model.save!
182
+ ActsAsTrashable::TrashRecord.count.should == 0
183
+ TrashableTestOneThing.count.should == 1
184
+
185
+ model.destroy
186
+ ActsAsTrashable::TrashRecord.count.should == 1
187
+ TrashableTestModel.count.should == 0
188
+ TrashableTestOneThing.count.should == 0
189
+
190
+ restored = TrashableTestModel.restore_trash!(model.id)
191
+ restored.reload
192
+ restored.name.should == 'test'
193
+ restored.one_thing.name.should == 'other'
194
+ restored.one_thing.id.should == model.one_thing.id
195
+ ActsAsTrashable::TrashRecord.count.should == 0
196
+ TrashableTestModel.count.should == 1
197
+ TrashableTestOneThing.count.should == 1
198
+ end
199
+
200
+ it "should be able to trash a record and restore it with has_and_belongs_to_many associations" do
201
+ other_1 = NonTrashableTestModel.create(:name => 'one')
202
+ other_2 = NonTrashableTestModel.create(:name => 'two')
203
+ model = TrashableTestModel.new(:name => 'test')
204
+ model.non_trashable_test_models = [other_1, other_2]
205
+ model.save!
206
+ model.reload
207
+ ActsAsTrashable::TrashRecord.count.should == 0
208
+ NonTrashableTestModel.count.should == 2
209
+
210
+ model.destroy
211
+ ActsAsTrashable::TrashRecord.count.should == 1
212
+ TrashableTestModel.count.should == 0
213
+ NonTrashableTestModelsTrashableTestModel.count.should == 0
214
+
215
+ restored = TrashableTestModel.restore_trash!(model.id)
216
+ restored.reload
217
+ restored.name.should == 'test'
218
+ restored.non_trashable_test_models.collect{|r| r.name}.sort.should == ['one', 'two']
219
+ ActsAsTrashable::TrashRecord.count.should == 0
220
+ TrashableTestModel.count.should == 1
221
+ NonTrashableTestModelsTrashableTestModel.count.should == 2
222
+ end
223
+
224
+ it "should be able to trash a record and restore without associations" do
225
+ model = ActsAsTrashable::TrashableNamespaceModel.new
226
+ model.name = 'test'
227
+ model.save!
228
+ ActsAsTrashable::TrashRecord.count.should == 0
229
+
230
+ model.destroy
231
+ ActsAsTrashable::TrashRecord.count.should == 1
232
+ ActsAsTrashable::TrashableNamespaceModel.count.should == 0
233
+
234
+ restored = ActsAsTrashable::TrashableNamespaceModel.restore_trash!(model.id)
235
+ restored.reload
236
+ restored.name.should == 'test'
237
+ ActsAsTrashable::TrashRecord.count.should == 0
238
+ ActsAsTrashable::TrashableNamespaceModel.count.should == 1
239
+ end
240
+
241
+ it "should be able to trash a record and restore without associations" do
242
+ model = ActsAsTrashable::TrashableSubclassModel.new
243
+ model.name = 'test'
244
+ model.save!
245
+ ActsAsTrashable::TrashRecord.count.should == 0
246
+
247
+ model.destroy
248
+ ActsAsTrashable::TrashRecord.count.should == 1
249
+ ActsAsTrashable::TrashableSubclassModel.count.should == 0
250
+
251
+ restored = ActsAsTrashable::TrashableSubclassModel.restore_trash!(model.id)
252
+ restored.reload
253
+ restored.name.should == 'test'
254
+ ActsAsTrashable::TrashRecord.count.should == 0
255
+ ActsAsTrashable::TrashableSubclassModel.count.should == 1
256
+ end
257
+
258
+ end
@@ -0,0 +1,23 @@
1
+ require 'rubygems'
2
+ require File.expand_path('../../lib/acts_as_trashable', __FILE__)
3
+ require 'sqlite3'
4
+
5
+ module ActsAsTrashable
6
+ module Test
7
+ def self.create_database
8
+ db_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'tmp'))
9
+ Dir.mkdir(db_dir) unless File.exist?(db_dir)
10
+ db = File.join(db_dir, 'test.sqlite3')
11
+ ActiveRecord::Base.establish_connection("adapter" => "sqlite3", "database" => db)
12
+ ActsAsTrashable::TrashRecord.create_table
13
+ end
14
+
15
+ def self.delete_database
16
+ db_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'tmp'))
17
+ db = File.join(db_dir, 'test.sqlite3')
18
+ ActiveRecord::Base.connection.disconnect!
19
+ File.delete(db) if File.exist?(db)
20
+ Dir.delete(db_dir) if File.exist?(db_dir) and Dir.entries(db_dir).reject{|f| f.match(/^\.+$/)}.empty?
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,323 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require 'zlib'
3
+
4
+ describe ActsAsTrashable::TrashRecord do
5
+
6
+ class TestTrashableRecord
7
+ attr_accessor :attributes
8
+
9
+ def initialize (attributes = {})
10
+ @attributes = attributes
11
+ end
12
+
13
+ def self.reflections
14
+ @reflections || {}
15
+ end
16
+
17
+ def self.reflections= (vals)
18
+ @reflections = vals
19
+ end
20
+
21
+ def self.base_class
22
+ self
23
+ end
24
+
25
+ def self.inheritance_column
26
+ 'type'
27
+ end
28
+
29
+ def id
30
+ attributes['id']
31
+ end
32
+
33
+ def id= (val)
34
+ attributes['id'] = val
35
+ end
36
+
37
+ def name= (val)
38
+ attributes['name'] = val
39
+ end
40
+
41
+ def value= (val)
42
+ attributes['value'] = val
43
+ end
44
+ end
45
+
46
+ class TestTrashableAssociationRecord < TestTrashableRecord
47
+ def self.reflections
48
+ @reflections || {}
49
+ end
50
+
51
+ def self.reflections= (vals)
52
+ @reflections = vals
53
+ end
54
+ end
55
+
56
+ class TestTrashableSubAssociationRecord < TestTrashableRecord
57
+ def self.reflections
58
+ @reflections || {}
59
+ end
60
+
61
+ def self.reflections= (vals)
62
+ @reflections = vals
63
+ end
64
+ end
65
+
66
+ before :all do
67
+ ActsAsTrashable::Test.create_database
68
+ end
69
+
70
+ after :all do
71
+ ActsAsTrashable::Test.delete_database
72
+ end
73
+
74
+ before :each do
75
+ TestTrashableRecord.reflections = nil
76
+ TestTrashableAssociationRecord.reflections = nil
77
+ TestTrashableSubAssociationRecord.reflections = nil
78
+ end
79
+
80
+ it "should serialize all the attributes of the original model" do
81
+ attributes = {'id' => 1, 'name' => 'trash', 'value' => 5}
82
+ original = TestTrashableRecord.new(attributes)
83
+ trash = ActsAsTrashable::TrashRecord.new(original)
84
+ trash.trashable_id.should == 1
85
+ trash.trashable_type.should == "TestTrashableRecord"
86
+ trash.trashable_attributes.should == attributes
87
+ end
88
+
89
+ it "should be backward compatible with uncompressed data" do
90
+ attributes_1 = {'id' => 1, 'name' => 'trash', 'value' => 5}
91
+ attributes_2 = {'id' => 2, 'name' => 'trash2', 'value' => 10}
92
+ trash = ActsAsTrashable::TrashRecord.new(TestTrashableRecord.new({}))
93
+ uncompressed = Marshal.dump(attributes_1)
94
+ compressed = Zlib::Deflate.deflate(Marshal.dump(attributes_2))
95
+
96
+ trash.data = uncompressed
97
+ trash.trashable_attributes.should == attributes_1
98
+ trash.data = compressed
99
+ trash.trashable_attributes.should == attributes_2
100
+ end
101
+
102
+ it "should serialize all the attributes of has_many associations with :dependent => :destroy" do
103
+ attributes = {'id' => 1, 'name' => 'trash', 'value' => Time.now}
104
+ association_attributes_1 = {'id' => 2, 'name' => 'association_1'}
105
+ association_attributes_2 = {'id' => 3, 'name' => 'association_2'}
106
+ original = TestTrashableRecord.new(attributes)
107
+ dependent_associations = [TestTrashableAssociationRecord.new(association_attributes_1), TestTrashableAssociationRecord.new(association_attributes_2)]
108
+ dependent_associations_reflection = stub(:association, :name => :dependent_associations, :macro => :has_many, :options => {:dependent => :destroy})
109
+ non_dependent_associations_reflection = stub(:association, :name => :non_dependent_associations, :macro => :has_many, :options => {})
110
+
111
+ TestTrashableRecord.reflections = {:dependent_associations => dependent_associations_reflection, :non_dependent_associations => non_dependent_associations_reflection}
112
+ original.should_not_receive(:non_dependent_associations)
113
+ original.should_receive(:dependent_associations).and_return(dependent_associations)
114
+
115
+ trash = ActsAsTrashable::TrashRecord.new(original)
116
+ trash.trashable_attributes.should == attributes.merge(:dependent_associations => [association_attributes_1, association_attributes_2])
117
+ end
118
+
119
+ it "should serialize all the attributes of has_one associations with :dependent => :destroy" do
120
+ attributes = {'id' => 1, 'name' => 'trash', 'value' => Date.today}
121
+ association_attributes = {'id' => 2, 'name' => 'association_1'}
122
+ original = TestTrashableRecord.new(attributes)
123
+ dependent_association = TestTrashableAssociationRecord.new(association_attributes)
124
+ dependent_association_reflection = stub(:association, :name => :dependent_association, :macro => :has_one, :options => {:dependent => :destroy})
125
+ non_dependent_association_reflection = stub(:association, :name => :non_dependent_association, :macro => :has_one, :options => {})
126
+
127
+ TestTrashableRecord.reflections = {:dependent_association => dependent_association_reflection, :non_dependent_association => non_dependent_association_reflection}
128
+ original.should_not_receive(:non_dependent_association)
129
+ original.should_receive(:dependent_association).and_return(dependent_association)
130
+
131
+ trash = ActsAsTrashable::TrashRecord.new(original)
132
+ trash.trashable_attributes.should == attributes.merge(:dependent_association => association_attributes)
133
+ end
134
+
135
+ it "should serialize all has_many_and_belongs_to_many associations" do
136
+ attributes = {'id' => 1, 'name' => 'trash'}
137
+ original = TestTrashableRecord.new(attributes)
138
+ association_reflection = stub(:association, :name => :associations, :macro => :has_and_belongs_to_many)
139
+
140
+ TestTrashableRecord.reflections = {:dependent_association => association_reflection}
141
+ original.should_receive(:association_ids).and_return([2, 3, 4])
142
+
143
+ trash = ActsAsTrashable::TrashRecord.new(original)
144
+ trash.trashable_attributes.should == attributes.merge(:associations => [2, 3, 4])
145
+ end
146
+
147
+ it "should serialize associations with :dependent => :destroy of associations with :dependent => :destroy" do
148
+ attributes = {'id' => 1, 'name' => 'trash', 'value' => Time.now}
149
+ association_attributes_1 = {'id' => 2, 'name' => 'association_1'}
150
+ association_attributes_2 = {'id' => 3, 'name' => 'association_2'}
151
+ original = TestTrashableRecord.new(attributes)
152
+ association_1 = TestTrashableAssociationRecord.new(association_attributes_1)
153
+ association_2 = TestTrashableAssociationRecord.new(association_attributes_2)
154
+ dependent_associations = [association_1, association_2]
155
+ dependent_associations_reflection = stub(:association, :name => :dependent_associations, :macro => :has_many, :options => {:dependent => :destroy})
156
+ sub_association_attributes = {'id' => 4, 'name' => 'sub_association_1'}
157
+ sub_association = TestTrashableSubAssociationRecord.new(sub_association_attributes)
158
+ sub_association_reflection = stub(:sub_association, :name => :sub_association, :macro => :has_one, :options => {:dependent => :destroy})
159
+
160
+ TestTrashableRecord.reflections = {:dependent_associations => dependent_associations_reflection}
161
+ TestTrashableAssociationRecord.reflections = {:sub_association => sub_association_reflection}
162
+ original.should_receive(:dependent_associations).and_return(dependent_associations)
163
+ association_1.should_receive(:sub_association).and_return(sub_association)
164
+ association_2.should_receive(:sub_association).and_return(nil)
165
+
166
+ trash = ActsAsTrashable::TrashRecord.new(original)
167
+ trash.trashable_attributes.should == attributes.merge(:dependent_associations => [association_attributes_1.merge(:sub_association => sub_association_attributes), association_attributes_2])
168
+ end
169
+
170
+ it "should be able to restore the original model" do
171
+ attributes = {'id' => 1, 'name' => 'trash', 'value' => 5}
172
+ trash = ActsAsTrashable::TrashRecord.new(TestTrashableRecord.new(attributes))
173
+ trash.data = Zlib::Deflate.deflate(Marshal.dump(attributes))
174
+ restored = trash.restore
175
+ restored.class.should == TestTrashableRecord
176
+ restored.id.should == 1
177
+ restored.attributes.should == attributes
178
+ end
179
+
180
+ it "should be able to restore associations" do
181
+ restored = TestTrashableRecord.new
182
+ attributes = {'id' => 1, 'name' => 'trash', 'value' => Time.now, :associations => {'id' => 2, 'value' => 'val'}}
183
+ trash = ActsAsTrashable::TrashRecord.new(TestTrashableRecord.new)
184
+ trash.data = Zlib::Deflate.deflate(Marshal.dump(attributes))
185
+ associations_reflection = stub(:associations, :name => :associations, :macro => :has_many, :options => {:dependent => :destroy})
186
+ TestTrashableRecord.reflections = {:associations => associations_reflection}
187
+ TestTrashableRecord.should_receive(:new).and_return(restored)
188
+ trash.should_receive(:restore_association).with(restored, :associations, {'id' => 2, 'value' => 'val'})
189
+ restored = trash.restore
190
+ end
191
+
192
+ it "should be able to restore the has_many associations" do
193
+ trash = ActsAsTrashable::TrashRecord.new(TestTrashableRecord.new)
194
+ record = TestTrashableRecord.new
195
+
196
+ associations_reflection = stub(:associations, :name => :associations, :macro => :has_many, :options => {:dependent => :destroy})
197
+ TestTrashableRecord.reflections = {:associations => associations_reflection}
198
+ associations = mock(:associations)
199
+ record.should_receive(:associations).and_return(associations)
200
+ associated_record = TestTrashableAssociationRecord.new
201
+ associations.should_receive(:build).and_return(associated_record)
202
+
203
+ trash.send(:restore_association, record, :associations, {'id' => 1, 'value' => 'val'})
204
+ associated_record.id.should == 1
205
+ associated_record.attributes.should == {'id' => 1, 'value' => 'val'}
206
+ end
207
+
208
+ it "should be able to restore the has_one associations" do
209
+ trash = ActsAsTrashable::TrashRecord.new(TestTrashableRecord.new)
210
+ record = TestTrashableRecord.new
211
+
212
+ association_reflection = stub(:associations, :name => :association, :macro => :has_one, :klass => TestTrashableAssociationRecord, :options => {:dependent => :destroy})
213
+ TestTrashableRecord.reflections = {:association => association_reflection}
214
+ associated_record = TestTrashableAssociationRecord.new
215
+ TestTrashableAssociationRecord.should_receive(:new).and_return(associated_record)
216
+ record.should_receive(:association=).with(associated_record)
217
+
218
+ trash.send(:restore_association, record, :association, {'id' => 1, 'value' => 'val'})
219
+ associated_record.id.should == 1
220
+ associated_record.attributes.should == {'id' => 1, 'value' => 'val'}
221
+ end
222
+
223
+ it "should be able to restore the has_and_belongs_to_many associations" do
224
+ trash = ActsAsTrashable::TrashRecord.new(TestTrashableRecord.new)
225
+ record = TestTrashableRecord.new
226
+
227
+ associations_reflection = stub(:associations, :name => :associations, :macro => :has_and_belongs_to_many, :options => {})
228
+ TestTrashableRecord.reflections = {:associations => associations_reflection}
229
+ record.should_receive(:association_ids=).with([2, 3, 4])
230
+
231
+ trash.send(:restore_association, record, :associations, [2, 3, 4])
232
+ end
233
+
234
+ it "should be able to restore associations of associations" do
235
+ trash = ActsAsTrashable::TrashRecord.new(TestTrashableRecord.new)
236
+ record = TestTrashableRecord.new
237
+
238
+ associations_reflection = stub(:associations, :name => :associations, :macro => :has_many, :options => {:dependent => :destroy})
239
+ TestTrashableRecord.reflections = {:associations => associations_reflection}
240
+ associations = mock(:associations)
241
+ record.should_receive(:associations).and_return(associations)
242
+ associated_record = TestTrashableAssociationRecord.new
243
+ associations.should_receive(:build).and_return(associated_record)
244
+
245
+ sub_associated_record = TestTrashableSubAssociationRecord.new
246
+ TestTrashableAssociationRecord.should_receive(:new).and_return(sub_associated_record)
247
+ sub_association_reflection = stub(:sub_association, :name => :sub_association, :macro => :has_one, :klass => TestTrashableAssociationRecord, :options => {:dependent => :destroy})
248
+ TestTrashableAssociationRecord.reflections = {:sub_association => sub_association_reflection}
249
+ associated_record.should_receive(:sub_association=).with(sub_associated_record)
250
+
251
+ trash.send(:restore_association, record, :associations, {'id' => 1, 'value' => 'val', :sub_association => {'id' => 2, 'value' => 'sub'}})
252
+ associated_record.id.should == 1
253
+ associated_record.attributes.should == {'id' => 1, 'value' => 'val'}
254
+ sub_associated_record.id.should == 2
255
+ sub_associated_record.attributes.should == {'id' => 2, 'value' => 'sub'}
256
+ end
257
+
258
+ it "should be able to restore original model and save it" do
259
+ attributes = {'id' => 1, 'name' => 'trash', 'value' => 5}
260
+ original = TestTrashableRecord.new(attributes)
261
+ trash = ActsAsTrashable::TrashRecord.new(original)
262
+ new_record = mock(:record)
263
+ new_record.should_receive(:save!)
264
+ trash.should_receive(:restore).and_return(new_record)
265
+ trash.should_receive(:destroy)
266
+ trash.restore!
267
+ end
268
+
269
+ it "should be able to empty the trash by max age" do
270
+ max_age = mock(:max_age)
271
+ time = 1.day.ago
272
+ max_age.should_receive(:ago).and_return(time)
273
+ ActsAsTrashable::TrashRecord.should_receive(:delete_all).with(['created_at <= ?', time])
274
+ ActsAsTrashable::TrashRecord.empty_trash(max_age)
275
+ end
276
+
277
+ it "should be able to empty the trash for only certain types" do
278
+ max_age = mock(:max_age)
279
+ time = 1.day.ago
280
+ max_age.should_receive(:ago).and_return(time)
281
+ mock_class_1 = stub(:class_1, :base_class => stub(:base_class_1, :name => 'TypeOne'))
282
+ mock_class_1.should_receive(:kind_of?).with(Class).and_return(true)
283
+ mock_class_2 = 'TypeTwo'
284
+ ActsAsTrashable::TrashRecord.should_receive(:delete_all).with(['created_at <= ? AND trashable_type IN (?, ?)', time, 'TypeOne', 'TypeTwo'])
285
+ ActsAsTrashable::TrashRecord.empty_trash(max_age, :only => [mock_class_1, mock_class_2])
286
+ end
287
+
288
+ it "should be able to empty the trash for all except certain types" do
289
+ max_age = mock(:max_age)
290
+ time = 1.day.ago
291
+ max_age.should_receive(:ago).and_return(time)
292
+ ActsAsTrashable::TrashRecord.should_receive(:delete_all).with(['created_at <= ? AND trashable_type NOT IN (?)', time, 'TypeOne'])
293
+ ActsAsTrashable::TrashRecord.empty_trash(max_age, :except => :type_one)
294
+ end
295
+
296
+ it "should be able to find a record by trashed type and id" do
297
+ trash = ActsAsTrashable::TrashRecord.new(TestTrashableRecord.new(:name => 'name'))
298
+ ActsAsTrashable::TrashRecord.should_receive(:find).with(:all, :conditions => {:trashable_type => 'TestTrashableRecord', :trashable_id => 1}).and_return([trash])
299
+ ActsAsTrashable::TrashRecord.find_trash(TestTrashableRecord, 1).should == trash
300
+ end
301
+
302
+ it "should really save the trash record to the database and restore without any mocking" do
303
+ ActsAsTrashable::TrashRecord.empty_trash(0)
304
+ ActsAsTrashable::TrashRecord.count.should == 0
305
+
306
+ attributes = {'id' => 1, 'name' => 'name value', 'value' => rand(1000000)}
307
+ original = TestTrashableRecord.new(attributes)
308
+ trash = ActsAsTrashable::TrashRecord.new(original)
309
+ trash.save!
310
+ ActsAsTrashable::TrashRecord.count.should == 1
311
+
312
+ record = ActsAsTrashable::TrashRecord.find_trash(TestTrashableRecord, 1).restore
313
+ record.class.should == TestTrashableRecord
314
+ record.id.should == 1
315
+ record.attributes.should == attributes
316
+
317
+ ActsAsTrashable::TrashRecord.empty_trash(0, :except => TestTrashableRecord)
318
+ ActsAsTrashable::TrashRecord.count.should == 1
319
+ ActsAsTrashable::TrashRecord.empty_trash(0, :only => TestTrashableRecord)
320
+ ActsAsTrashable::TrashRecord.count.should == 0
321
+ end
322
+
323
+ end
metadata ADDED
@@ -0,0 +1,141 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acts_as_trashable
3
+ version: !ruby/object:Gem::Version
4
+ hash: 17
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 3
10
+ version: 1.0.3
11
+ platform: ruby
12
+ authors:
13
+ - Brian Durand
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-06-22 00:00:00 -05:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: activerecord
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 7
30
+ segments:
31
+ - 2
32
+ - 2
33
+ version: "2.2"
34
+ type: :runtime
35
+ version_requirements: *id001
36
+ - !ruby/object:Gem::Dependency
37
+ name: sqlite3
38
+ prerelease: false
39
+ requirement: &id002 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ hash: 3
45
+ segments:
46
+ - 0
47
+ version: "0"
48
+ type: :development
49
+ version_requirements: *id002
50
+ - !ruby/object:Gem::Dependency
51
+ name: rspec
52
+ prerelease: false
53
+ requirement: &id003 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ hash: 27
59
+ segments:
60
+ - 1
61
+ - 3
62
+ - 0
63
+ version: 1.3.0
64
+ type: :development
65
+ version_requirements: *id003
66
+ - !ruby/object:Gem::Dependency
67
+ name: jeweler
68
+ prerelease: false
69
+ requirement: &id004 !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ hash: 3
75
+ segments:
76
+ - 0
77
+ version: "0"
78
+ type: :development
79
+ version_requirements: *id004
80
+ description: ActiveRecord extension that serializes destroyed records into a trash table from which they can be restored. This is intended to reduce the risk of users misusing your application's delete function and losing data.
81
+ email: brian@embellishedvisions.com
82
+ executables: []
83
+
84
+ extensions: []
85
+
86
+ extra_rdoc_files:
87
+ - README.rdoc
88
+ files:
89
+ - .gitignore
90
+ - MIT-LICENSE
91
+ - README.rdoc
92
+ - Rakefile
93
+ - VERSION
94
+ - acts_as_trashable.gemspec
95
+ - lib/acts_as_trashable.rb
96
+ - lib/acts_as_trashable/trash_record.rb
97
+ - spec/acts_as_trashable_spec.rb
98
+ - spec/full_spec.rb
99
+ - spec/spec_helper.rb
100
+ - spec/trash_record_spec.rb
101
+ has_rdoc: true
102
+ homepage: http://github.com/bdurand/acts_as_trashable
103
+ licenses: []
104
+
105
+ post_install_message:
106
+ rdoc_options:
107
+ - --charset=UTF-8
108
+ - --main
109
+ - README.rdoc
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ hash: 3
118
+ segments:
119
+ - 0
120
+ version: "0"
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ hash: 3
127
+ segments:
128
+ - 0
129
+ version: "0"
130
+ requirements: []
131
+
132
+ rubyforge_project:
133
+ rubygems_version: 1.3.7
134
+ signing_key:
135
+ specification_version: 3
136
+ summary: ActiveRecord extension that serializes destroyed records into a trash table from which they can be restored.
137
+ test_files:
138
+ - spec/acts_as_trashable_spec.rb
139
+ - spec/full_spec.rb
140
+ - spec/spec_helper.rb
141
+ - spec/trash_record_spec.rb