delete_soft 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/Gemfile +5 -0
- data/Rakefile +14 -0
- data/delete_soft.gemspec +26 -0
- data/lib/class_methods.rb +79 -0
- data/lib/delete_soft/version.rb +3 -0
- data/lib/delete_soft.rb +68 -0
- data/lib/instance_methods.rb +50 -0
- data/test/db.sqlite3 +0 -0
- data/test/delete_softly_test.rb +56 -0
- data/test/test_helper.rb +66 -0
- metadata +143 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
desc 'Test the delete_softly plugin.'
|
9
|
+
Rake::TestTask.new(:test) do |t|
|
10
|
+
t.libs << 'lib'
|
11
|
+
t.libs << 'test'
|
12
|
+
t.pattern = 'test/**/*_test.rb'
|
13
|
+
t.verbose = true
|
14
|
+
end
|
data/delete_soft.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "delete_soft/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "delete_soft"
|
7
|
+
s.version = DeleteSoft::VERSION
|
8
|
+
s.authors = ["Orban Botond"]
|
9
|
+
s.email = ["orbanbotond@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{A gem which adds soft delete functionality}
|
12
|
+
s.description = %q{A gem which adds soft delete functionality}
|
13
|
+
|
14
|
+
s.rubyforge_project = "delete_soft"
|
15
|
+
|
16
|
+
s.add_dependency('squeel', '>= 0.9.3')
|
17
|
+
|
18
|
+
s.add_development_dependency('sqlite3', ['>= 1.3.4'])
|
19
|
+
s.add_development_dependency('activerecord', ['>= 3.1.0'])
|
20
|
+
s.add_development_dependency('rake', ['0.9.2'])
|
21
|
+
|
22
|
+
s.files = `git ls-files`.split("\n")
|
23
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
24
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
25
|
+
s.require_paths = ["lib"]
|
26
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'arel'
|
2
|
+
|
3
|
+
module DeleteSoftly
|
4
|
+
module ClassMethods
|
5
|
+
# Give the representation of items at a certain date/time.
|
6
|
+
# class Item < ActiveRecord::Base
|
7
|
+
# delete_softly
|
8
|
+
# end
|
9
|
+
# Will result in:
|
10
|
+
# Item.at_time(DateTime.parse('2010-01-01')) #=> (SELECT "items".* FROM "items" WHERE (((("items"."deleted_at" > '2010-01-01 00:00:00') OR ("items"."deleted_at" IS NULL)) AND ("items"."created_at" < '2010-01-01 00:00:00')))
|
11
|
+
def at_time(date = Time.now.utc)
|
12
|
+
with_deleted.where{ (deleted_at > date || deleted_at != nil) && created_at < date }
|
13
|
+
end
|
14
|
+
alias_method :at_date, :at_time
|
15
|
+
|
16
|
+
# Give the currently active items. When delete_soflty is added this is invoked by default
|
17
|
+
# But when false is added, items are shown by default
|
18
|
+
# class Item < ActiveRecord::Base
|
19
|
+
# delete_softly false
|
20
|
+
# end
|
21
|
+
# Or similar:
|
22
|
+
# class Item < ActiveRecord::Base
|
23
|
+
# delete_softly :default => false
|
24
|
+
# end
|
25
|
+
# You need to call active on the model where you want to hide deleted items
|
26
|
+
# Item.all #=> SELECT "items".* FROM "items"
|
27
|
+
# Item.active #=> SELECT "items".* FROM "items" WHERE ("items"."deleted_at" IS NULL)
|
28
|
+
def active
|
29
|
+
where{deleted_at == nil}
|
30
|
+
end
|
31
|
+
|
32
|
+
# Same as active, but not to be overwritten. Active might become with disabled => false
|
33
|
+
# or something like that. Without deleted should remain intact
|
34
|
+
def without_deleted
|
35
|
+
where{deleted_at.nil?}
|
36
|
+
end
|
37
|
+
|
38
|
+
# Include deleted items when performing queries
|
39
|
+
# class Item < ActiveRecord::Base
|
40
|
+
# default_scope order(:content)
|
41
|
+
# delete_softly
|
42
|
+
# end
|
43
|
+
# Will result in:
|
44
|
+
# Item.first #=> SELECT "items".* FROM "items" WHERE ("items"."deleted_at" IS NULL) ORDER BY "items"."content" LIMIT 1
|
45
|
+
# Item.with_deleted.first #=> SELECT "items".* FROM "items" ORDER BY "items"."content" LIMIT 1
|
46
|
+
# Item.where(:content.matches => 'a%') #=> SELECT "items".* FROM "items" WHERE ("items"."deleted_at" IS NULL) AND ("items"."content" ILIKE 'a%') ORDER BY "items"."content"
|
47
|
+
# Item.with_deleted do
|
48
|
+
# Item.where(:content.matches => 'a%') #=> SELECT "items".* FROM "items" WHERE ("items"."content" ILIKE 'a%') ORDER BY "items"."content"
|
49
|
+
# end
|
50
|
+
# IHaveManyItems.first.items #=> SELECT "items".* FROM "items" WHERE ("items"."deleted_at" IS NULL) AND ("items".i_have_many_items_id = 1) ORDER BY "items"."content"
|
51
|
+
# IHaveManyItems.first.items.with_deleted #=> SELECT "items".* FROM "items" WHERE ("items".i_have_many_items_id = 1) ORDER BY "items"."content"
|
52
|
+
def with_deleted(&block)
|
53
|
+
unscoped
|
54
|
+
end
|
55
|
+
|
56
|
+
def deleted
|
57
|
+
with_deleted.where{deleted_at != nil}
|
58
|
+
end
|
59
|
+
|
60
|
+
# Support for paper_trail if it is installed as well. Then you can use:
|
61
|
+
# class Post < ActiveRecord::Base
|
62
|
+
# default_scope order(:created_at)
|
63
|
+
# delete_softly
|
64
|
+
# has_paper_trail
|
65
|
+
# has_many :comments
|
66
|
+
# end
|
67
|
+
# Then
|
68
|
+
# Post.version_at(1.week.ago)
|
69
|
+
# Will return all post of one week ago, with the appropriate field values at that time
|
70
|
+
def version_at(time)
|
71
|
+
if respond_to?(:at_time)
|
72
|
+
at_time(time).map{|i| i.respond_to?(:version_at) ? i.version_at(time) : i}.compact
|
73
|
+
else
|
74
|
+
scoped.map{|i| i.respond_to?(:version_at) ? i.version_at(time) : i}.compact
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
data/lib/delete_soft.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require "delete_soft/version"
|
2
|
+
|
3
|
+
require 'active_record'
|
4
|
+
require 'squeel'
|
5
|
+
require 'class_methods'
|
6
|
+
require 'instance_methods'
|
7
|
+
|
8
|
+
module DeleteSoftly
|
9
|
+
module ARExtender
|
10
|
+
|
11
|
+
# Always have Model.active available. It is a good practice to use it
|
12
|
+
# when you want the active records. Even use it when no logic is in
|
13
|
+
# place yet. Now it is an alias for scoped, but can be overwritten
|
14
|
+
# for custom behaviour, for example:
|
15
|
+
# class Post < ActiveRecord::Base
|
16
|
+
# delete_softly
|
17
|
+
# has_many :comments
|
18
|
+
# def self.active
|
19
|
+
# super.where(:disabled.ne => true)
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
# class Comment < ActiveRecord::Base
|
23
|
+
# belongs_to :post
|
24
|
+
# end
|
25
|
+
# will result in:
|
26
|
+
# Post.all #=> SELECT * FROM posts WHERE deleted_at IS NULL;
|
27
|
+
# Post.active #=> SELECT * FROM posts WHERE deleted_at IS NULL AND disabled != 't';
|
28
|
+
# Comment.all #=> SELECT * FROM comments;
|
29
|
+
# Comment.active #=> SELECT * FROM comments;
|
30
|
+
def active
|
31
|
+
scoped
|
32
|
+
end
|
33
|
+
|
34
|
+
# Make the model delete softly. A deleted_at:datetime column is required
|
35
|
+
# for this to work.
|
36
|
+
# The two most important differences are that it can be enforce on a model
|
37
|
+
# or be more free.
|
38
|
+
# class Post < ActiveRecord::Base
|
39
|
+
# delete_softly
|
40
|
+
# end
|
41
|
+
# will enforce soft delete on the post model. A deleted post will never appear,
|
42
|
+
# unless the explicit with_deleted is called. When the model is:
|
43
|
+
# class Post < ActiveRecord::Base
|
44
|
+
# delete_softly :enforce => false
|
45
|
+
# end
|
46
|
+
# An object will still be available after destroy is called,
|
47
|
+
def delete_softly(options = {:enforce => :active})
|
48
|
+
# Make destroy! the old destroy
|
49
|
+
alias_method :destroy!, :destroy
|
50
|
+
|
51
|
+
include DeleteSoftly::InstanceMethods
|
52
|
+
extend DeleteSoftly::ClassMethods
|
53
|
+
# Support single argument
|
54
|
+
# delete_softly :active # Same as :enforce => :active, default behaviour
|
55
|
+
# delete_softly :enforce=> :with_deleted # Same as without argument
|
56
|
+
options = {:enforce=> options } unless options.is_a?(Hash)
|
57
|
+
if options[:enforce]
|
58
|
+
if options[:enforce].is_a?(Symbol) && respond_to?(options[:enforce])
|
59
|
+
default_scope send(options[:enforce])
|
60
|
+
else
|
61
|
+
default_scope active
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
ActiveRecord::Base.send(:extend, DeleteSoftly::ARExtender)
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module DeleteSoftly
|
2
|
+
module InstanceMethods
|
3
|
+
|
4
|
+
# This method reports whether or not the record has been soft deleted.
|
5
|
+
#
|
6
|
+
def deleted?
|
7
|
+
self.deleted_at?
|
8
|
+
end
|
9
|
+
|
10
|
+
# Custom destroy method for models using delete_softly
|
11
|
+
def destroy
|
12
|
+
if persisted?
|
13
|
+
with_transaction_returning_status do
|
14
|
+
_run_destroy_callbacks do
|
15
|
+
update_attribute :deleted_at, Time.now
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
@destroyed = true
|
21
|
+
freeze
|
22
|
+
end
|
23
|
+
|
24
|
+
# Revive a destroyed item. For a model like:
|
25
|
+
# class Post < ActiveRecord::Base
|
26
|
+
# delete_softly
|
27
|
+
# end
|
28
|
+
# Then the following can be done:
|
29
|
+
# p = Post.find(1)
|
30
|
+
# p.destroy
|
31
|
+
# p = Post.find(1) # raise error
|
32
|
+
# p = Post.with_deleted.find(1) # Original object but with deleted_at attribute set
|
33
|
+
# p.revive #=> deleted_at => nil
|
34
|
+
# p = Post.find(1) # business as usual
|
35
|
+
# If papertrail is used for this model it will not store a copy
|
36
|
+
def revive
|
37
|
+
# Disable paper_trail when it is present and active
|
38
|
+
if self.class.respond_to?(:paper_trail_active) && self.class.paper_trail_active
|
39
|
+
self.class.paper_trail_off
|
40
|
+
update_attribute :deleted_at, nil
|
41
|
+
self.class.paper_trail_on
|
42
|
+
else
|
43
|
+
update_attribute :deleted_at, nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
alias_method :undelete, :revive
|
47
|
+
alias_method :undestroy, :revive
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
data/test/db.sqlite3
ADDED
Binary file
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class DeleteSoftlyTest < ActiveSupport::TestCase
|
4
|
+
# Replace this with your real tests.
|
5
|
+
setup do
|
6
|
+
puts "New setup"
|
7
|
+
Post.with_deleted.delete_all
|
8
|
+
Comment.with_deleted.delete_all
|
9
|
+
Tag.delete_all
|
10
|
+
@post1 = Post.create(:title => "post1")
|
11
|
+
@post1_id = @post1.id
|
12
|
+
@comment1_1 = @post1.comments.create(:email => "test@gmail.com", :body => "Comment 1 for post 1")
|
13
|
+
@comment2_1 = @post1.comments.create(:email => "test@gmail.com", :body => "Comment 2 for post 1")
|
14
|
+
@comment3_1 = @post1.comments.create(:email => "testother@gmail.com", :body => "Comment 2 for post 1")
|
15
|
+
|
16
|
+
@post2 = Post.create(:title => "post2")
|
17
|
+
@post2_id = @post2.id
|
18
|
+
|
19
|
+
end
|
20
|
+
test "two records available" do
|
21
|
+
assert_equal 2, Post.count
|
22
|
+
end
|
23
|
+
|
24
|
+
test "destroy count test" do
|
25
|
+
@post1.destroy
|
26
|
+
assert_equal 1, Post.count
|
27
|
+
puts "It is 1"
|
28
|
+
assert_equal 2, Post.with_deleted.count
|
29
|
+
puts "It is 2"
|
30
|
+
assert_nil Post.find_by_id(@post1_id)
|
31
|
+
@post1 = Post.with_deleted.find(@post1_id)
|
32
|
+
assert @post1.deleted_at
|
33
|
+
@post1.revive
|
34
|
+
assert_equal 2, Post.count
|
35
|
+
end
|
36
|
+
|
37
|
+
test "deleted, without_deleted methods" do
|
38
|
+
assert_equal [@post1, @post2], Post.all.sort_by{|p| p.title}
|
39
|
+
@post1.destroy
|
40
|
+
assert_equal [@post2], Post.without_deleted
|
41
|
+
assert_equal [@post1], Post.deleted
|
42
|
+
end
|
43
|
+
|
44
|
+
test "at_time methods" do
|
45
|
+
t = Time.now
|
46
|
+
@post1.destroy
|
47
|
+
assert_equal [@post2], Post.without_deleted
|
48
|
+
assert_equal [@post1, @post2], Post.at_time(t)
|
49
|
+
end
|
50
|
+
|
51
|
+
test "other default scopes" do
|
52
|
+
assert_equal 2, Comment.count
|
53
|
+
@comment2_1.destroy
|
54
|
+
assert_equal 1, Comment.count
|
55
|
+
end
|
56
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'arel'
|
4
|
+
require 'active_support'
|
5
|
+
require 'active_support/all'
|
6
|
+
require 'delete_soft'
|
7
|
+
require 'sqlite3'
|
8
|
+
|
9
|
+
|
10
|
+
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
11
|
+
ActiveRecord::Base.establish_connection(
|
12
|
+
:adapter => 'sqlite3',
|
13
|
+
:database => 'test/db.sqlite3'
|
14
|
+
)
|
15
|
+
|
16
|
+
class Post < ActiveRecord::Base
|
17
|
+
delete_softly
|
18
|
+
has_many :comments
|
19
|
+
end
|
20
|
+
|
21
|
+
class Comment < ActiveRecord::Base
|
22
|
+
delete_softly
|
23
|
+
belongs_to :post
|
24
|
+
has_many :tags
|
25
|
+
default_scope where(:email => 'test@gmail.com')
|
26
|
+
end
|
27
|
+
|
28
|
+
class Tag < ActiveRecord::Base
|
29
|
+
belongs_to :comment
|
30
|
+
end
|
31
|
+
|
32
|
+
if Post.table_exists?
|
33
|
+
ActiveRecord::Base.connection.drop_table "posts"
|
34
|
+
end
|
35
|
+
|
36
|
+
puts "Creating table posts"
|
37
|
+
ActiveRecord::Base.connection.create_table "posts" do |t|
|
38
|
+
t.string :title
|
39
|
+
t.text :body
|
40
|
+
t.datetime :deleted_at
|
41
|
+
t.timestamps
|
42
|
+
end
|
43
|
+
|
44
|
+
if Comment.table_exists?
|
45
|
+
ActiveRecord::Base.connection.drop_table "comments"
|
46
|
+
end
|
47
|
+
|
48
|
+
puts "Creating table comments"
|
49
|
+
ActiveRecord::Base.connection.create_table "comments" do |t|
|
50
|
+
t.string :email
|
51
|
+
t.text :body
|
52
|
+
t.integer :post_id
|
53
|
+
t.datetime :deleted_at
|
54
|
+
t.timestamps
|
55
|
+
end
|
56
|
+
|
57
|
+
if Tag.table_exists?
|
58
|
+
ActiveRecord::Base.connection.drop_table "tags"
|
59
|
+
end
|
60
|
+
|
61
|
+
puts "Creating table tags"
|
62
|
+
ActiveRecord::Base.connection.create_table "tags" do |t|
|
63
|
+
t.string :name
|
64
|
+
t.integer :comment_id
|
65
|
+
t.timestamps
|
66
|
+
end
|
metadata
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: delete_soft
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Orban Botond
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-01-03 00:00:00 +02:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 61
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
- 9
|
32
|
+
- 3
|
33
|
+
version: 0.9.3
|
34
|
+
type: :runtime
|
35
|
+
name: squeel
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 19
|
45
|
+
segments:
|
46
|
+
- 1
|
47
|
+
- 3
|
48
|
+
- 4
|
49
|
+
version: 1.3.4
|
50
|
+
type: :development
|
51
|
+
name: sqlite3
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
prerelease: false
|
55
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 3
|
61
|
+
segments:
|
62
|
+
- 3
|
63
|
+
- 1
|
64
|
+
- 0
|
65
|
+
version: 3.1.0
|
66
|
+
type: :development
|
67
|
+
name: activerecord
|
68
|
+
version_requirements: *id003
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
prerelease: false
|
71
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - "="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
hash: 63
|
77
|
+
segments:
|
78
|
+
- 0
|
79
|
+
- 9
|
80
|
+
- 2
|
81
|
+
version: 0.9.2
|
82
|
+
type: :development
|
83
|
+
name: rake
|
84
|
+
version_requirements: *id004
|
85
|
+
description: A gem which adds soft delete functionality
|
86
|
+
email:
|
87
|
+
- orbanbotond@gmail.com
|
88
|
+
executables: []
|
89
|
+
|
90
|
+
extensions: []
|
91
|
+
|
92
|
+
extra_rdoc_files: []
|
93
|
+
|
94
|
+
files:
|
95
|
+
- .gitignore
|
96
|
+
- Gemfile
|
97
|
+
- Rakefile
|
98
|
+
- delete_soft.gemspec
|
99
|
+
- lib/class_methods.rb
|
100
|
+
- lib/delete_soft.rb
|
101
|
+
- lib/delete_soft/version.rb
|
102
|
+
- lib/instance_methods.rb
|
103
|
+
- test/db.sqlite3
|
104
|
+
- test/delete_softly_test.rb
|
105
|
+
- test/test_helper.rb
|
106
|
+
has_rdoc: true
|
107
|
+
homepage: ""
|
108
|
+
licenses: []
|
109
|
+
|
110
|
+
post_install_message:
|
111
|
+
rdoc_options: []
|
112
|
+
|
113
|
+
require_paths:
|
114
|
+
- lib
|
115
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
116
|
+
none: false
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
hash: 3
|
121
|
+
segments:
|
122
|
+
- 0
|
123
|
+
version: "0"
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
none: false
|
126
|
+
requirements:
|
127
|
+
- - ">="
|
128
|
+
- !ruby/object:Gem::Version
|
129
|
+
hash: 3
|
130
|
+
segments:
|
131
|
+
- 0
|
132
|
+
version: "0"
|
133
|
+
requirements: []
|
134
|
+
|
135
|
+
rubyforge_project: delete_soft
|
136
|
+
rubygems_version: 1.6.2
|
137
|
+
signing_key:
|
138
|
+
specification_version: 3
|
139
|
+
summary: A gem which adds soft delete functionality
|
140
|
+
test_files:
|
141
|
+
- test/db.sqlite3
|
142
|
+
- test/delete_softly_test.rb
|
143
|
+
- test/test_helper.rb
|