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 +1 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +37 -0
- data/Rakefile +47 -0
- data/VERSION +1 -0
- data/acts_as_trashable.gemspec +66 -0
- data/lib/acts_as_trashable/trash_record.rb +166 -0
- data/lib/acts_as_trashable.rb +64 -0
- data/spec/acts_as_trashable_spec.rb +71 -0
- data/spec/full_spec.rb +258 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/trash_record_spec.rb +323 -0
- metadata +141 -0
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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|