rails_soft_deletable 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,24 +1,24 @@
1
1
  # RailsSoftDeletable
2
2
 
3
- TODO: Write a gem description
3
+ This gem provides soft delete behavior to ActiveRecord 3.2.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  Add this line to your application's Gemfile:
8
8
 
9
- gem 'rails_soft_deletable'
9
+ gem "rails_soft_deletable"
10
10
 
11
11
  And then execute:
12
12
 
13
13
  $ bundle
14
14
 
15
- Or install it yourself as:
16
-
17
- $ gem install rails_soft_deletable
18
-
19
15
  ## Usage
20
16
 
21
- TODO: Write usage instructions here
17
+ ```ruby
18
+ class Company < ActiveRecord::Base
19
+ soft_deletable
20
+ end
21
+ ```
22
22
 
23
23
  ## Contributing
24
24
 
@@ -1,4 +1,5 @@
1
1
  require "rails_soft_deletable/version"
2
+ require "active_record"
2
3
 
3
4
  module RailsSoftDeletable
4
5
  def self.included(base)
@@ -47,17 +48,34 @@ module RailsSoftDeletable
47
48
  end
48
49
  end
49
50
 
50
- def destroy
51
- if destroyed?
52
- delete_or_soft_delete(true)
51
+ def deleted_at
52
+ val = super
53
+ if val.zero? || val.nil?
54
+ nil
55
+ else
56
+ Time.at(val).in_time_zone
57
+ end
58
+ end
59
+
60
+ def destroy(destroy_mode = :soft)
61
+ if destroy_mode == :hard
62
+ _original_destroy
53
63
  else
54
- run_callbacks(:destroy) { delete_or_soft_delete(true) }
64
+ if destroyed?
65
+ delete_or_soft_delete(true)
66
+ else
67
+ run_callbacks(:destroy) { delete_or_soft_delete(true) }
68
+ end
55
69
  end
56
70
  end
57
71
 
58
- def delete
59
- return if new_record?
60
- delete_or_soft_delete
72
+ def delete(delete_mode = :soft)
73
+ if delete_mode == :hard
74
+ _original_delete
75
+ else
76
+ return if new_record?
77
+ delete_or_soft_delete
78
+ end
61
79
  end
62
80
 
63
81
  def restore!
@@ -77,16 +95,25 @@ module RailsSoftDeletable
77
95
  alias :restore :restore!
78
96
 
79
97
  def destroyed?
80
- value = send(soft_deletable_column)
81
- value && value != 0
98
+ !!send(soft_deletable_column)
99
+ end
100
+
101
+ def persisted?
102
+ @_pretend_persistence || super
82
103
  end
83
- alias :deleted? :destroyed?
84
104
 
85
105
  private
86
106
 
107
+ def _prepare_for_hard_delete(&block)
108
+ @_pretend_persistence = true
109
+ self.class.unscoped(&block)
110
+ ensure
111
+ @_pretend_persistence = false
112
+ end
113
+
87
114
  def delete_or_soft_delete(with_transaction = false)
88
115
  if destroyed?
89
- self.class.unscoped { hard_delete! }
116
+ _prepare_for_hard_delete { _original_delete }
90
117
  else
91
118
  touch_soft_deletable_column(with_transaction)
92
119
  end
@@ -103,7 +130,7 @@ module RailsSoftDeletable
103
130
  def touch_column
104
131
  raise ActiveRecordError, "can not touch on a new record object" unless persisted?
105
132
 
106
- current_time = current_time_from_proper_timezone.to_i
133
+ current_time = ("%0.6f" % current_time_from_proper_timezone).to_f
107
134
  changes = {}
108
135
 
109
136
  changes[soft_deletable_column.to_s] = write_attribute(soft_deletable_column.to_s, current_time)
@@ -117,9 +144,13 @@ module RailsSoftDeletable
117
144
  end
118
145
 
119
146
  class ActiveRecord::Base
120
- def self.acts_as_soft_deletable(options={})
121
- alias :hard_destroy! :destroy
122
- alias :hard_delete! :delete
147
+ def self.soft_deletable(options={})
148
+ alias :_original_destroy :destroy
149
+ alias :_original_delete :delete
150
+
151
+ private :_original_destroy
152
+ private :_original_delete
153
+
123
154
  include RailsSoftDeletable
124
155
  class_attribute :soft_deletable_column
125
156
 
@@ -135,13 +166,6 @@ class ActiveRecord::Base
135
166
  self.class.soft_deletable?
136
167
  end
137
168
 
138
- # Override the persisted method to allow for the paranoia gem.
139
- # If a paranoid record is selected, then we only want to check
140
- # if it's a new record, not if it is "destroyed".
141
- def persisted?
142
- soft_deletable? ? !new_record? : super
143
- end
144
-
145
169
  private
146
170
 
147
171
  def soft_deletable_column
@@ -1,3 +1,3 @@
1
1
  module RailsSoftDeletable
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
data/spec/models.rb ADDED
@@ -0,0 +1,75 @@
1
+ require "active_record"
2
+
3
+ module Spec
4
+ module Models
5
+ module SoftDeletableCallbacks
6
+ def self.included(base)
7
+ base.class_eval do
8
+ attr_reader :before_destroy_called
9
+ attr_reader :around_destroy_called
10
+ attr_reader :after_destroy_called
11
+
12
+ attr_reader :before_restore_called
13
+ attr_reader :around_restore_called
14
+ attr_reader :after_restore_called
15
+
16
+ before_destroy :call_before_destroy
17
+ around_destroy :call_around_destroy
18
+ after_destroy :call_after_destroy
19
+
20
+ before_restore :call_before_restore
21
+ around_restore :call_around_restore
22
+ after_restore :call_after_restore
23
+
24
+ def call_before_destroy
25
+ @before_destroy_called = true
26
+ end
27
+
28
+ def call_around_destroy
29
+ yield
30
+ @around_destroy_called = true
31
+ end
32
+
33
+ def call_after_destroy
34
+ @after_destroy_called = true
35
+ end
36
+
37
+ def call_before_restore
38
+ @before_restore_called = true
39
+ end
40
+
41
+ def call_around_restore
42
+ yield
43
+ @around_restore_called = true
44
+ end
45
+
46
+ def call_after_restore
47
+ @after_restore_called = true
48
+ end
49
+
50
+ def reset_callback_flags!
51
+ @before_destroy_called = nil
52
+ @around_destroy_called = nil
53
+ @after_destroy_called = nil
54
+
55
+ @before_restore_called = nil
56
+ @around_restore_called = nil
57
+ @after_restore_called = nil
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ class DecimalModel< ActiveRecord::Base
66
+ soft_deletable
67
+
68
+ include Spec::Models::SoftDeletableCallbacks
69
+ end
70
+
71
+ class IntegerModel < ActiveRecord::Base
72
+ soft_deletable
73
+
74
+ include Spec::Models::SoftDeletableCallbacks
75
+ end
@@ -0,0 +1,253 @@
1
+ require "spec_helper"
2
+
3
+ describe RailsSoftDeletable do
4
+ let (:model) { IntegerModel.create! }
5
+ let (:decimal_model) { DecimalModel.create! }
6
+ let (:integer_model) { IntegerModel.create! }
7
+
8
+ context "#destroy" do
9
+ it "marks deleted_at column" do
10
+ Timecop.freeze(Time.now) do
11
+ decimal_model.destroy
12
+ integer_model.destroy
13
+
14
+ decimal_deleted_at = DecimalModel.connection.select_value("SELECT deleted_at FROM #{DecimalModel.quoted_table_name} WHERE #{DecimalModel.primary_key} = #{decimal_model.id}")
15
+ integer_deleted_at = IntegerModel.connection.select_value("SELECT deleted_at FROM #{IntegerModel.quoted_table_name} WHERE #{IntegerModel.primary_key} = #{integer_model.id}")
16
+
17
+ expect(decimal_deleted_at).to eq(("%0.6f" % Time.now.to_f).to_f)
18
+ expect(integer_deleted_at.to_i).to eq(Time.now.to_i)
19
+ end
20
+ end
21
+
22
+ it "soft deletes the record" do
23
+ Timecop.freeze(Time.now) do
24
+ decimal_model.destroy
25
+ integer_model.destroy
26
+
27
+ expect(decimal_model.deleted_at).to eq(Time.now)
28
+ expect(integer_model.deleted_at.to_i).to eq(Time.now.to_i)
29
+ end
30
+ end
31
+
32
+ it "does not mark the deleted_at attribute as changed" do
33
+ model.destroy
34
+
35
+ expect(model).to_not be_deleted_at_changed
36
+ end
37
+
38
+ it "marks the record as destroyed" do
39
+ model.destroy
40
+
41
+ expect(model).to be_destroyed
42
+ end
43
+
44
+ it "marks the record as not persisted" do
45
+ model.destroy
46
+
47
+ expect(model).to_not be_persisted
48
+ end
49
+
50
+ it "does not freeze the record" do
51
+ model.destroy
52
+
53
+ expect(decimal_model).to_not be_frozen
54
+ end
55
+
56
+ it "performs destroy callbacks" do
57
+ model.destroy
58
+
59
+ expect(model.before_destroy_called).to eq(true)
60
+ expect(model.around_destroy_called).to eq(true)
61
+ expect(model.after_destroy_called).to eq(true)
62
+ end
63
+
64
+ context "when record has already been soft deleted" do
65
+ before do
66
+ model.destroy
67
+ model.reset_callback_flags!
68
+ end
69
+
70
+ it "does not perform destroy callbacks" do
71
+ model.destroy
72
+
73
+ expect(model.before_destroy_called).to be_nil
74
+ expect(model.around_destroy_called).to be_nil
75
+ expect(model.after_destroy_called).to be_nil
76
+ end
77
+
78
+ it "hard deletes the record from the database" do
79
+ model.destroy
80
+
81
+ count = model.class.connection.select_value("SELECT COUNT(*) FROM #{model.class.quoted_table_name} WHERE #{model.class.primary_key} = #{model.id}")
82
+ expect(count).to eq(0)
83
+ end
84
+ end
85
+
86
+ context "with hard destroy mode" do
87
+ it "hard deletes the record from the database" do
88
+ model.destroy(:hard)
89
+
90
+ count = model.class.connection.select_value("SELECT COUNT(*) FROM #{model.class.quoted_table_name} WHERE #{model.class.primary_key} = #{model.id}")
91
+ expect(count).to eq(0)
92
+ end
93
+
94
+ it "performs destroy callbacks" do
95
+ model.destroy(:hard)
96
+
97
+ expect(model.before_destroy_called).to eq(true)
98
+ expect(model.around_destroy_called).to eq(true)
99
+ expect(model.after_destroy_called).to eq(true)
100
+ end
101
+ end
102
+ end
103
+
104
+ context "#delete" do
105
+ it "marks deleted_at column" do
106
+ Timecop.freeze(Time.now) do
107
+ decimal_model.delete
108
+ integer_model.delete
109
+
110
+ decimal_deleted_at = DecimalModel.connection.select_value("SELECT deleted_at FROM #{DecimalModel.quoted_table_name} WHERE #{DecimalModel.primary_key} = #{decimal_model.id}")
111
+ integer_deleted_at = IntegerModel.connection.select_value("SELECT deleted_at FROM #{IntegerModel.quoted_table_name} WHERE #{IntegerModel.primary_key} = #{integer_model.id}")
112
+
113
+ expect(decimal_deleted_at).to eq(("%0.6f" % Time.now.to_f).to_f)
114
+ expect(integer_deleted_at.to_i).to eq(Time.now.to_i)
115
+ end
116
+ end
117
+
118
+ it "soft deletes the record" do
119
+ Timecop.freeze(Time.now) do
120
+ decimal_model.delete
121
+ integer_model.delete
122
+
123
+ expect(decimal_model.deleted_at).to eq(Time.now)
124
+ expect(integer_model.deleted_at.to_i).to eq(Time.now.to_i)
125
+ end
126
+ end
127
+
128
+ it "does not mark the deleted_at attribute as changed" do
129
+ model.delete
130
+
131
+ expect(model).to_not be_deleted_at_changed
132
+ end
133
+
134
+ it "marks the record as destroyed" do
135
+ model.delete
136
+
137
+ expect(model).to be_destroyed
138
+ end
139
+
140
+ it "marks the record as not persisted" do
141
+ model.delete
142
+
143
+ expect(model).to_not be_persisted
144
+ end
145
+
146
+ it "does not freeze the record" do
147
+ model.delete
148
+
149
+ expect(decimal_model).to_not be_frozen
150
+ end
151
+
152
+ it "does not perform destroy callbacks" do
153
+ model.delete
154
+
155
+ expect(model.before_destroy_called).to be_nil
156
+ expect(model.around_destroy_called).to be_nil
157
+ expect(model.after_destroy_called).to be_nil
158
+ end
159
+
160
+ context "when record has already been soft deleted" do
161
+ before do
162
+ model.delete
163
+ end
164
+
165
+ it "does not perform destroy callbacks" do
166
+ model.delete
167
+
168
+ expect(model.before_destroy_called).to be_nil
169
+ expect(model.around_destroy_called).to be_nil
170
+ expect(model.after_destroy_called).to be_nil
171
+ end
172
+
173
+ it "hard deletes the record from the database" do
174
+ model.delete
175
+
176
+ count = model.class.connection.select_value("SELECT COUNT(*) FROM #{model.class.quoted_table_name} WHERE #{model.class.primary_key} = #{model.id}")
177
+ expect(count).to eq(0)
178
+ end
179
+ end
180
+
181
+ context "with hard delete mode" do
182
+ it "hard deletes the record from the database" do
183
+ model.delete(:hard)
184
+
185
+ count = model.class.connection.select_value("SELECT COUNT(*) FROM #{model.class.quoted_table_name} WHERE #{model.class.primary_key} = #{model.id}")
186
+ expect(count).to eq(0)
187
+ end
188
+
189
+ it "performs destroy callbacks" do
190
+ model.delete(:hard)
191
+
192
+ expect(model.before_destroy_called).to be_nil
193
+ expect(model.around_destroy_called).to be_nil
194
+ expect(model.after_destroy_called).to be_nil
195
+ end
196
+ end
197
+ end
198
+
199
+ context "#restore!" do
200
+ before do
201
+ model.destroy
202
+ model.reset_callback_flags!
203
+ end
204
+
205
+ it "restores the record" do
206
+ model.restore!
207
+
208
+ expect(model).to be_persisted
209
+ expect(model.deleted_at).to be_nil
210
+ expect(model).to_not be_deleted_at_changed
211
+ expect(model).to_not be_destroyed
212
+ expect(model).to_not be_new_record
213
+ expect(model).to_not be_frozen
214
+ end
215
+
216
+ it "resets deleted_at in the database" do
217
+ model.restore!
218
+
219
+ model_deleted_at = model.class.connection.select_value("SELECT deleted_at FROM #{model.class.quoted_table_name} WHERE #{model.class.primary_key} = #{model.id}")
220
+ expect(model_deleted_at).to eq(0)
221
+ end
222
+
223
+ it "performs restore callbacks" do
224
+ model.restore!
225
+
226
+ expect(model.before_restore_called).to eq(true)
227
+ expect(model.around_restore_called).to eq(true)
228
+ expect(model.after_restore_called).to eq(true)
229
+ end
230
+
231
+ it "returns true" do
232
+ expect(model.restore!).to eq(true)
233
+ end
234
+ end
235
+
236
+ context "#deleted_at" do
237
+ context "when record has not been soft deleted" do
238
+ it "returns nil" do
239
+ expect(model.deleted_at).to be_nil
240
+ end
241
+ end
242
+
243
+ context "when record has been soft deleted" do
244
+ before do
245
+ model.destroy
246
+ end
247
+
248
+ it "returns a Time object" do
249
+ expect(model.deleted_at).to be_kind_of(Time)
250
+ end
251
+ end
252
+ end
253
+ end
data/spec/schema.rb ADDED
@@ -0,0 +1,11 @@
1
+ # encoding: UTF-8
2
+
3
+ ActiveRecord::Schema.define(version: Time.now.strftime("%Y%m%d%H%M%S")) do
4
+ create_table "decimal_models", force: true do |t|
5
+ t.decimal "deleted_at", default: 0
6
+ end
7
+
8
+ create_table "integer_models", force: true do |t|
9
+ t.integer "deleted_at", default: 0
10
+ end
11
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,6 +1,15 @@
1
1
  require "pathname"
2
2
  ROOT_PATH = Pathname.new(__FILE__).join("../..").expand_path
3
- $LOAD_PATH.unshift(ROOT_PATH.join("lib"))
3
+ $LOAD_PATH.unshift(ROOT_PATH.join("lib").to_s)
4
+
5
+ require "rails_soft_deletable"
6
+
7
+ # Requires supporting ruby files with custom matchers and macros, etc,
8
+ # in spec/support/ and its subdirectories.
9
+ Dir[ROOT_PATH.join("spec/support/**/*.rb")].each { |f| require f }
10
+
11
+ # Require all models.
12
+ require "models"
4
13
 
5
14
  RSpec.configure do |config|
6
15
  # Run specs in random order to surface order dependencies. If you find an
@@ -0,0 +1,27 @@
1
+ require "active_record"
2
+
3
+ module Spec
4
+ module Support
5
+ class Environment
6
+ DATABASE_FILE = ROOT_PATH.join("tmp/test.sqlite3")
7
+ SCHEMA_FILE = ROOT_PATH.join("spec/schema.rb")
8
+
9
+ def self.setup
10
+ DATABASE_FILE.dirname.mkpath
11
+ DATABASE_FILE.delete if DATABASE_FILE.exist?
12
+
13
+ ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: DATABASE_FILE.to_s)
14
+
15
+ silence_stream(STDOUT) do
16
+ load(SCHEMA_FILE)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ RSpec.configure do |config|
24
+ config.before(:suite) do
25
+ Spec::Support::Environment.setup
26
+ end
27
+ end
@@ -0,0 +1,7 @@
1
+ require "timecop"
2
+
3
+ RSpec.configure do |config|
4
+ config.after do
5
+ Timecop.return
6
+ end
7
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_soft_deletable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2013-11-21 00:00:00.000000000 Z
13
+ date: 2013-11-23 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -92,6 +92,38 @@ dependencies:
92
92
  - - ~>
93
93
  - !ruby/object:Gem::Version
94
94
  version: '1.6'
95
+ - !ruby/object:Gem::Dependency
96
+ name: timecop
97
+ requirement: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ none: false
107
+ requirements:
108
+ - - ! '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: sqlite3
113
+ requirement: !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ! '>='
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ! '>='
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
95
127
  description: Soft deletable for ActiveRecord on Rails 3+.
96
128
  email:
97
129
  - quan@listia.com
@@ -102,7 +134,12 @@ extra_rdoc_files: []
102
134
  files:
103
135
  - lib/rails_soft_deletable/version.rb
104
136
  - lib/rails_soft_deletable.rb
137
+ - spec/models.rb
138
+ - spec/rails_soft_deletable_spec.rb
139
+ - spec/schema.rb
105
140
  - spec/spec_helper.rb
141
+ - spec/support/environment.rb
142
+ - spec/support/timecop.rb
106
143
  - LICENSE.txt
107
144
  - Rakefile
108
145
  - README.md
@@ -119,12 +156,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
119
156
  - - ! '>='
120
157
  - !ruby/object:Gem::Version
121
158
  version: '0'
159
+ segments:
160
+ - 0
161
+ hash: -741598862832616831
122
162
  required_rubygems_version: !ruby/object:Gem::Requirement
123
163
  none: false
124
164
  requirements:
125
165
  - - ! '>='
126
166
  - !ruby/object:Gem::Version
127
167
  version: '0'
168
+ segments:
169
+ - 0
170
+ hash: -741598862832616831
128
171
  requirements: []
129
172
  rubyforge_project:
130
173
  rubygems_version: 1.8.24
@@ -132,4 +175,9 @@ signing_key:
132
175
  specification_version: 3
133
176
  summary: Soft deletable for ActiveRecord on Rails 3+
134
177
  test_files:
178
+ - spec/models.rb
179
+ - spec/rails_soft_deletable_spec.rb
180
+ - spec/schema.rb
135
181
  - spec/spec_helper.rb
182
+ - spec/support/environment.rb
183
+ - spec/support/timecop.rb