historical_society 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +7 -0
- data/README.md +43 -0
- data/Rakefile +7 -0
- data/historical_society.gemspec +27 -0
- data/lib/historical_society.rb +36 -0
- data/lib/historical_society/version.rb +3 -0
- data/spec/spec_helper.rb +38 -0
- data/spec/user_spec.rb +57 -0
- metadata +115 -0
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use ruby-1.9.2@historical_society --create
|
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2011 New Leaders Ventures, LLC., Robert Bousquet
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
HistoricalSociety -- Preserve the history of your data, starting with soft delete.
|
2
|
+
====================================
|
3
|
+
|
4
|
+
## DESCRIPTION
|
5
|
+
|
6
|
+
The historical society gem currently provides basic soft delete functionality on an ActiveRecord instance through the use
|
7
|
+
of a "deleted_at" timestamp column and a default_scope that excludes the soft deleted records.
|
8
|
+
|
9
|
+
_Note: This currently provides a default_scope that excludes soft deleted records. It also overrides
|
10
|
+
the normal #destroy and #delete instance methods so they just set the deleted_at timestamp without
|
11
|
+
actually deleting the records from the db._
|
12
|
+
|
13
|
+
## INSTALLATION
|
14
|
+
|
15
|
+
$ gem install historical_society
|
16
|
+
|
17
|
+
## USAGE
|
18
|
+
|
19
|
+
In your model:
|
20
|
+
|
21
|
+
class User < ActiveRecord::Base
|
22
|
+
include HistoricalSociety
|
23
|
+
end
|
24
|
+
|
25
|
+
In your queries:
|
26
|
+
|
27
|
+
User.all # does not include deleted records
|
28
|
+
User.unscoped.all # includes deleted records
|
29
|
+
|
30
|
+
User.find(1) # raises ActiveRecord::RecordNotFound if user has been soft deleted
|
31
|
+
User.unscoped.find(1) # finds user record regardless of soft deletion
|
32
|
+
|
33
|
+
In relationships:
|
34
|
+
|
35
|
+
@account.users # finds only users within the account scope that have not been soft deleted
|
36
|
+
@account.users.unscoped # CAREFUL! THIS REMOVES THE ACCOUNT SCOPE TOO!
|
37
|
+
|
38
|
+
# THIS IS PROBABLY WHAT YOU WANT
|
39
|
+
# Removes the default scope on user, but then adds the account scope through an explicit where method.
|
40
|
+
|
41
|
+
User.unscoped.all.where(:account_id => @account.id)
|
42
|
+
|
43
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "historical_society/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "historical_society"
|
7
|
+
s.version = HistoricalSociety::VERSION
|
8
|
+
s.authors = ["Robert Bousquet"]
|
9
|
+
s.email = ["rbousquet@newleaders.com"]
|
10
|
+
s.homepage = "https://github.com/bousquet/historical_society"
|
11
|
+
s.summary = "Preserve the history of a db record"
|
12
|
+
s.description = "Currently offers soft deletion, and a default scope that excludes 'deleted' records"
|
13
|
+
|
14
|
+
s.rubyforge_project = "historical_society"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
s.add_development_dependency "rspec"
|
23
|
+
s.add_development_dependency "rake"
|
24
|
+
s.add_development_dependency "sqlite3"
|
25
|
+
s.add_runtime_dependency "activerecord", "~> 3.1.0"
|
26
|
+
s.add_runtime_dependency "activesupport", "~> 3.1.0"
|
27
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "historical_society/version"
|
2
|
+
|
3
|
+
module HistoricalSociety
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
# Model must include a deleted_at datetime column, default NULL
|
6
|
+
included do
|
7
|
+
default_scope lambda {
|
8
|
+
field = [self.table_name, "deleted_at"].map{|str|"`#{str}`"}.join(".")
|
9
|
+
where(["#{field} IS NULL OR (#{field} IS NOT NULL AND #{field} > ?)", historical_time])
|
10
|
+
}
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
def historical_time
|
15
|
+
(Time.zone || Time).now
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module InstanceMethods
|
20
|
+
|
21
|
+
def destroy
|
22
|
+
set_deleted_at
|
23
|
+
end
|
24
|
+
|
25
|
+
def delete
|
26
|
+
set_deleted_at
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def set_deleted_at
|
32
|
+
update_attribute(:deleted_at, self.class.historical_time)
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler/setup'
|
3
|
+
|
4
|
+
require 'active_record'
|
5
|
+
require 'active_support'
|
6
|
+
|
7
|
+
require 'historical_society'
|
8
|
+
|
9
|
+
RSpec.configure do |config|
|
10
|
+
|
11
|
+
root = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
12
|
+
|
13
|
+
ActiveRecord::Base.establish_connection(
|
14
|
+
:adapter => "sqlite3",
|
15
|
+
:database => "#{root}/db/historical_society.db"
|
16
|
+
)
|
17
|
+
|
18
|
+
ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS 'users'")
|
19
|
+
ActiveRecord::Base.connection.create_table(:users) do |t|
|
20
|
+
t.string :name
|
21
|
+
t.datetime :deleted_at
|
22
|
+
end
|
23
|
+
|
24
|
+
class User < ActiveRecord::Base
|
25
|
+
include HistoricalSociety
|
26
|
+
end
|
27
|
+
|
28
|
+
config.before(:each) do
|
29
|
+
ActiveRecord::Base.connection.increment_open_transactions
|
30
|
+
ActiveRecord::Base.connection.begin_db_transaction
|
31
|
+
end
|
32
|
+
|
33
|
+
config.after(:each) do
|
34
|
+
ActiveRecord::Base.connection.rollback_db_transaction
|
35
|
+
ActiveRecord::Base.connection.decrement_open_transactions
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
data/spec/user_spec.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
describe User do
|
3
|
+
|
4
|
+
describe "User queries with paranoia" do
|
5
|
+
let!(:user) { User.create(:name => "Bob") }
|
6
|
+
let!(:deleted_user) { User.create(:name => "Steve", :deleted_at => 5.minutes.ago) }
|
7
|
+
|
8
|
+
it "should be in default scope" do
|
9
|
+
User.all.should include(user)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should not include undeleted user" do
|
13
|
+
User.all.should_not include(deleted_user)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should have deleted users in unscoped queries" do
|
17
|
+
User.unscoped.all.should include(user)
|
18
|
+
User.unscoped.all.should include(deleted_user)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "User deletion with paranoia" do
|
23
|
+
let!(:user) { User.create(:name => "Bob") }
|
24
|
+
|
25
|
+
before :each do
|
26
|
+
user.destroy
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should have deleted_at time" do
|
30
|
+
user.deleted_at.is_a?(Time).should be_true
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should not be findable by default" do
|
34
|
+
expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should be findable via unscoped" do
|
38
|
+
expect { User.unscoped.find(user.id) }.to_not raise_error(ActiveRecord::RecordNotFound)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "User with another default_scope" do
|
43
|
+
before :each do
|
44
|
+
class User < ActiveRecord::Base
|
45
|
+
include HistoricalSociety
|
46
|
+
default_scope order(:name)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should combine default scopes" do
|
51
|
+
User.scoped.to_sql.should include("ORDER BY name")
|
52
|
+
User.scoped.to_sql.should include("`users`.`deleted_at` IS NOT NULL")
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
end
|
metadata
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: historical_society
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Robert Bousquet
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-09-12 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: &70115113351260 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70115113351260
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rake
|
27
|
+
requirement: &70115113350340 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70115113350340
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: sqlite3
|
38
|
+
requirement: &70115113349880 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70115113349880
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: activerecord
|
49
|
+
requirement: &70115113349320 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ~>
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.1.0
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70115113349320
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: activesupport
|
60
|
+
requirement: &70115113348560 !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ~>
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 3.1.0
|
66
|
+
type: :runtime
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70115113348560
|
69
|
+
description: Currently offers soft deletion, and a default scope that excludes 'deleted'
|
70
|
+
records
|
71
|
+
email:
|
72
|
+
- rbousquet@newleaders.com
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- .gitignore
|
78
|
+
- .rspec
|
79
|
+
- .rvmrc
|
80
|
+
- Gemfile
|
81
|
+
- MIT-LICENSE
|
82
|
+
- README.md
|
83
|
+
- Rakefile
|
84
|
+
- historical_society.gemspec
|
85
|
+
- lib/historical_society.rb
|
86
|
+
- lib/historical_society/version.rb
|
87
|
+
- spec/spec_helper.rb
|
88
|
+
- spec/user_spec.rb
|
89
|
+
homepage: https://github.com/bousquet/historical_society
|
90
|
+
licenses: []
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options: []
|
93
|
+
require_paths:
|
94
|
+
- lib
|
95
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
96
|
+
none: false
|
97
|
+
requirements:
|
98
|
+
- - ! '>='
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
102
|
+
none: false
|
103
|
+
requirements:
|
104
|
+
- - ! '>='
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
requirements: []
|
108
|
+
rubyforge_project: historical_society
|
109
|
+
rubygems_version: 1.8.6
|
110
|
+
signing_key:
|
111
|
+
specification_version: 3
|
112
|
+
summary: Preserve the history of a db record
|
113
|
+
test_files:
|
114
|
+
- spec/spec_helper.rb
|
115
|
+
- spec/user_spec.rb
|