rails_soft_deletable 0.0.3 → 0.0.4

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/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