acts_as_soft_delete_by_field 1.0.2

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 ADDED
@@ -0,0 +1,114 @@
1
+ Soft Delete By Field
2
+ ===================
3
+
4
+ Provides mechanism for soft delete functionality. Soft delete means that on
5
+ delete, an object is flagged as deleted rather than being removed from the
6
+ database. This makes it far easier to recover objects that have
7
+ been deleted by mistake.
8
+
9
+ This soft delete mechanism uses a field in a models table, to flag and
10
+ record the date of deletion.
11
+
12
+ Requirements
13
+ ------------
14
+ This plugin is designed for Rails 3. The syntax of the scope options
15
+ will not work with Rails 2.
16
+
17
+ The model object needs to have a datetime field, that on
18
+ delete will be set to the current time. By default, this field should
19
+ be :deleted_at, but you can also specify your own field.
20
+
21
+ Usage
22
+ -----
23
+
24
+ class Thing < ActiveRecord::Base
25
+
26
+ acts_as_soft_delete_by_field
27
+
28
+ end
29
+
30
+ If you wish to use a field other than :deleted_at, for example :soft_deleted_at,
31
+ you can do this via the acts_as_soft_delete_by_field declaration:
32
+
33
+ class Thing < ActiveRecord::Base
34
+
35
+ acts_as_soft_delete_by_field(:soft_deleted_at)
36
+
37
+ end
38
+
39
+ Functionality
40
+ -------------
41
+ Adds 'extant' and 'deleted' scopes to the model. For example, if added
42
+ to a Thing model, would allow you to do the following:
43
+
44
+ thing.soft_delete ---- Soft deletes an instance of thing
45
+
46
+ Thing.extant ---- Finds all things that are not deleted
47
+ Thing.deleted.count ---- Counts how many things have been deleted
48
+
49
+ Thing.extant.find(
50
+ :all,
51
+ :conditions => ["colour = ?", 'red']
52
+ ) ---- Finds all extant things that have colour set as red
53
+
54
+ Thing.extant.where(:colour => 'red') ---- As above using where
55
+
56
+
57
+ Also if Box has_many things
58
+
59
+ box.things.deleted ---- Finds all the deleted things in the box
60
+
61
+ Note that Thing's delete instance method is not effected by this functionality
62
+ so:
63
+
64
+ thing.soft_delete --- alters the thing instance to be flagged as deleted
65
+ thing.delete --- deletes the thing from the database.
66
+
67
+ If you want delete actions to soft_delete, overwrite delete:
68
+
69
+ class Thing
70
+
71
+ acts_as_soft_delete_by_field
72
+
73
+ def delete
74
+ soft_delete
75
+ end
76
+
77
+ end
78
+
79
+ Callbacks
80
+ ---------
81
+
82
+ There are also two callback methods: before_soft_delete and after_soft_delete
83
+ Overwrite these methods in the model to use them. For example:
84
+
85
+ class Thing
86
+ acts_as_soft_delete_by_field
87
+
88
+ def after_soft_delete
89
+ puts "Thing soft deleted"
90
+ end
91
+ end
92
+
93
+ Now, when a thing is soft deleted, "Thing soft deleted" will printed be to the
94
+ console.
95
+
96
+ Testing
97
+ -------
98
+
99
+ A number of custom assertions are made available when this plugin is installed.
100
+ They are held in the module ActsAsSoftDeleteByFieldAssertions. In particular,
101
+ assert_soft_delete_working_for works through each of the assertions and can
102
+ be used as a test that soft deletion is working correctly on any model where
103
+ it is used.
104
+
105
+ class ThingTest < ActiveSupport::TestCase
106
+
107
+ def test_soft_delete
108
+ assert_soft_delete_working_for(Thing.first)
109
+ end
110
+
111
+ end
112
+
113
+ Copyright (c) 2011 Rob Nichols, released under the MIT license
114
+
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rdoc/task'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the acts_as_soft_delete 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
15
+
16
+ desc 'Generate documentation for the acts_as_soft_delete plugin.'
17
+ RDoc::Task.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'ActsAsSoftDelete'
20
+ rdoc.options << '--line-numbers' << '--inline-source'
21
+ rdoc.rdoc_files.include('README')
22
+ rdoc.rdoc_files.include('lib/**/*.rb')
23
+ end
@@ -0,0 +1,5 @@
1
+ require 'soft_delete_by_field'
2
+ require 'acts_as_soft_delete_by_field_assertions'
3
+
4
+ ActiveRecord::Base.send(:include, ActiveRecord::Acts::SoftDeleteByField)
5
+ ActiveSupport::TestCase.send(:include, ActsAsSoftDeleteByFieldAssertions)
@@ -0,0 +1,240 @@
1
+ # Provides assertion methods that make it easier to test soft deletion functionality
2
+ #
3
+ # The soft delete functionality can be tested via:
4
+ #
5
+ # class ThingTest < ActiveSupport::TestCase
6
+ #
7
+ # def test_soft_delete
8
+ # assert_soft_delete_working_for(Thing.first)
9
+ # end
10
+ #
11
+ # end
12
+ #
13
+ module ActsAsSoftDeleteByFieldAssertions
14
+
15
+ def assert_soft_delete_by_field_working_for(item)
16
+ # Using clones, as otherwise other assertions affected
17
+ @starting_number = item.class.count
18
+ assert_before_soft_delete_all_correct_for(item)
19
+ @start_time = Time.now
20
+ item.soft_delete
21
+ assert_extant_working_for(item)
22
+ assert_deleted_working_for(item)
23
+ assert_still_in_database_but_with_deleted_at_set(item)
24
+ assert_soft_deleted(item)
25
+ assert_recover_soft_deleted(item)
26
+ assert_deletion_prevented_if_reasons_not_to_delete_overwritten_with_method_returning_true(item.clone)
27
+ assert_deletion_prevented_if_reasons_not_to_delete_detected(item)
28
+ assert_before_soft_delete(item.clone)
29
+ assert_after_soft_delete(item.clone)
30
+ end
31
+
32
+ def assert_soft_deleted(item)
33
+ assert_equal(true, item.reload.is_deleted?, error_messages_for(item, "Not deleted: #{item.inspect}"))
34
+ end
35
+
36
+ def assert_extant(item)
37
+ deletion_object_details = ". Deletion_object: #{@deletion_objects.inspect}" if @deletion_objects
38
+ assert_equal(true, item.reload.is_extant?, error_messages_for(item, "Not extant: #{item.inspect}"))
39
+ end
40
+
41
+ def assert_get_request_to_delete_raises_error
42
+ login_as User.first
43
+ assert_raise RuntimeError do
44
+ get :delete
45
+ end
46
+ end
47
+
48
+ def assert_recover_soft_deleted(item)
49
+ item.recover_soft_deleted
50
+ assert_before_soft_delete_all_correct_for(item)
51
+ end
52
+
53
+ def assert_before_soft_delete_all_correct_for(item)
54
+ assert_all_extant(item)
55
+ assert_extant_includes(item)
56
+ assert_none_soft_deleted(item)
57
+ assert_extant(item)
58
+ end
59
+
60
+ def assert_extant_includes(item)
61
+ assert item.class.extant.include?(item)
62
+ end
63
+
64
+ def assert_all_extant(item)
65
+ assert_equal item.class.count, item.class.extant.count
66
+ end
67
+
68
+ def assert_none_soft_deleted(item)
69
+ assert_equal 0, item.class.deleted.count
70
+ end
71
+
72
+ def assert_extant_working_for(item)
73
+ assert_equal @starting_number - 1, item.class.extant.count
74
+ assert !item.class.extant.include?(item)
75
+ end
76
+
77
+ def assert_deleted_working_for(item)
78
+ assert_equal 1, item.class.deleted.count
79
+ assert_equal item, item.class.deleted.first
80
+ end
81
+
82
+ def assert_still_in_database_but_with_deleted_at_set(item)
83
+ assert_object_still_in_database(item)
84
+ assert_deleted_at_set_as_time_after_test_started_for(item)
85
+ end
86
+
87
+ def assert_deleted_at_set_as_time_after_test_started_for(object)
88
+ assert object.deleted_via_soft_delete_by_field_name_at >= @start_time, "Deletion occured before test was run (#{object.deleted_via_soft_delete_by_field_name_at})"
89
+ end
90
+
91
+ def assert_object_still_in_database(object)
92
+ assert_equal object, object.class.find(object.id)
93
+ end
94
+
95
+ def create_soft_deletion_objects(object, source_path)
96
+ @source_path = source_path
97
+ @object_class = object.class.to_s
98
+ @object_id = object.id.to_s
99
+ @deletion_objects = {
100
+ :source_path => @source_path,
101
+ :object_class => @object_class,
102
+ :object_id => @object_id
103
+ }
104
+ end
105
+
106
+ def assert_soft_deletion_via_delete_action(object, path)
107
+ create_deletion_objects_and_pass_to_delete(object, path)
108
+ assert_redirection_to_path(@source_path)
109
+ assert_soft_deleted(object)
110
+ end
111
+
112
+ def assert_failure_to_soft_delete_via_delete_action(object, path)
113
+ create_deletion_objects_and_pass_to_delete(object, path)
114
+ assert_redirection_to_path(@source_path)
115
+ assert_extant(object)
116
+ end
117
+
118
+ def create_deletion_objects_and_pass_to_delete(object, path)
119
+ create_soft_deletion_objects(object, path)
120
+ @deletion_objects
121
+ post :delete, @deletion_objects
122
+ end
123
+
124
+ def assert_undeletion_via_recover_soft_deleted_action(object, path)
125
+ object.soft_delete
126
+ create_deletion_objects_and_pass_to_recover_soft_deleted(object, path)
127
+ assert_redirection_to_path(@source_path)
128
+ assert_extant(object.reload)
129
+ end
130
+
131
+ def create_deletion_objects_and_pass_to_recover_soft_deleted(object, path)
132
+ create_soft_deletion_objects(object, path)
133
+ @deletion_objects
134
+ post :recover_soft_deleted, @deletion_objects
135
+ end
136
+
137
+ def assert_deletion_prevented_if_reasons_not_to_delete_overwritten_with_method_returning_true(item)
138
+ if model_has_custom_reason_not_to_delete_defined(item)
139
+ # This test not valid in this case
140
+ else
141
+ assert_soft_deletion_working(item)
142
+ method_to_add_to_item = <<EOF
143
+
144
+ def reasons_not_to_delete?
145
+ true
146
+ end
147
+
148
+ EOF
149
+
150
+ item.class_eval(method_to_add_to_item)
151
+ assert_reasons_not_to_delete_prevents_soft_deletion(item)
152
+ end
153
+ end
154
+
155
+ def assert_deletion_prevented_if_reasons_not_to_delete_detected(item)
156
+ if model_has_custom_reason_not_to_delete_defined(item)
157
+ # This test not valid in this case
158
+ else
159
+ assert_soft_deletion_working(item)
160
+ initial_reasons_not_to_delete = item.reasons_not_to_delete
161
+ item.reasons_not_to_delete = "Don't delete"
162
+ assert_reasons_not_to_delete_prevents_soft_deletion(item)
163
+ item.reasons_not_to_delete = initial_reasons_not_to_delete
164
+ end
165
+ end
166
+
167
+ def assert_before_soft_delete(item)
168
+ message = "Hello world"
169
+ assert_soft_deletion_working(item)
170
+ assert(!item.respond_to?(:goodbye))
171
+ method_to_add_to_item = <<EOF
172
+
173
+ def goodbye
174
+ '#{message}'
175
+ end
176
+
177
+ EOF
178
+ item.class_eval(method_to_add_to_item)
179
+ item.soft_delete
180
+ assert item.is_deleted?, 'Item not deleted'
181
+ assert_equal message, item.goodbye
182
+ end
183
+
184
+ def assert_after_soft_delete(item)
185
+ message = 'Hello there'
186
+ assert_soft_deletion_working(item)
187
+ assert(!item.respond_to?(:hello))
188
+ method_to_add_to_item = <<EOF
189
+
190
+ def hello
191
+ '#{message}'
192
+ end
193
+
194
+ EOF
195
+ item.class_eval(method_to_add_to_item)
196
+ item.soft_delete
197
+ assert item.is_deleted?, 'Item not deleted'
198
+ assert_equal message, item.hello
199
+ end
200
+
201
+
202
+ private
203
+ def error_messages_for(item, first_message)
204
+ error_messages = [first_message]
205
+ error_messages << item.errors.full_messages unless item.errors.empty?
206
+ error_messages << "Flash: #{@request.session['flash'].inspect}" if @request
207
+ error_messages << "Deletion_object: #{@deletion_objects.inspect}" if @deletion_objects
208
+ error_messages.join(". ")
209
+ end
210
+
211
+ def assert_extant_at_start_of_test(item)
212
+ assert(item.is_extant?, "Item must be extant at start of test #{item.inspect}")
213
+ end
214
+
215
+ def assert_reasons_not_to_delete_prevents_soft_deletion(item)
216
+ item.soft_delete
217
+ assert(item.is_extant?, "Item was deleted in spite of reasons_not_to_delete? being true")
218
+ assert_errors_on_base(item)
219
+ end
220
+
221
+ def assert_soft_deletion_working(item)
222
+ item.soft_delete
223
+ assert(item.is_deleted?, "Item should be deleted. item: #{item.inspect}")
224
+ item.recover_soft_deleted
225
+ assert(item.is_extant?, "Item should be extant")
226
+ end
227
+
228
+ def model_has_custom_reason_not_to_delete_defined(item)
229
+ item.class.private_method_defined?('reasons_not_to_delete?') or (item.reasons_not_to_delete != item.reasons_not_to_delete?)
230
+ end
231
+
232
+ def assert_errors_on_base(object)
233
+ assert(object.errors[:base],
234
+ "Should have Errors on base detected for object: #{object.inspect}"
235
+ )
236
+ end
237
+
238
+ end
239
+
240
+ ActiveSupport::TestCase.send(:include, ActsAsSoftDeleteByFieldAssertions)
@@ -0,0 +1,112 @@
1
+ module ActiveRecord #:nodoc:
2
+ module Acts #:nodoc:
3
+ module SoftDeleteByField
4
+ DEFAULT_FIELD_NAME = :deleted_at
5
+
6
+ def self.included(base)
7
+ base.send :extend, ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+
12
+ def soft_delete_by_field_name
13
+ @soft_delete_by_field_name
14
+ end
15
+
16
+ def acts_as_soft_delete_by_field(field_name = nil)
17
+ attr_accessor :reasons_not_to_delete
18
+ field_name ||= DEFAULT_FIELD_NAME
19
+ @soft_delete_by_field_name = field_name.to_sym
20
+ send :include, SoftDeleteByField::InstanceMethods
21
+ scope :extant, where(["#{(self.class.name.tableize + ".") if self.class.kind_of?(SoftDeleteByField)}#{field_name} IS NULL"])
22
+ scope :deleted, where(["#{(self.class.name.tableize + ".") if self.class.kind_of?(SoftDeleteByField)}#{field_name} IS NOT NULL"])
23
+ end
24
+
25
+ end
26
+
27
+ module InstanceMethods
28
+
29
+
30
+ # Overwrite this method to add actions that are triggered before soft_delete operates.
31
+ def before_soft_delete
32
+
33
+ end
34
+
35
+ # Rename this 'delete' if you wish this functionality to replace the
36
+ # default delete behaviour.
37
+ def soft_delete
38
+ before_soft_delete
39
+ unless reasons_not_to_delete?
40
+ update_attribute(soft_delete_by_field_name, Time.now)
41
+ after_soft_delete
42
+ return 'deleted'
43
+ else
44
+ errors.add(:base, "Unable to delete. reasons_not_to_delete returns #{@reasons_not_to_delete || 'true'} for #{inspect}")
45
+ end
46
+ end
47
+
48
+ # Overwrite this method to add actions that are triggered after soft_delete operates.
49
+ def after_soft_delete
50
+
51
+ end
52
+
53
+ # Overwrite this method to add actions that are triggered before recover_soft_deleted operates.
54
+ def before_recover_soft_deleted
55
+
56
+ end
57
+
58
+ def recover_soft_deleted
59
+ before_recover_soft_deleted
60
+ update_attribute(soft_delete_by_field_name, nil)
61
+ after_recover_soft_deleted
62
+ end
63
+
64
+ # Overwrite this method to add actions that are triggered after recover_soft_deleted operates.
65
+ def after_recover_soft_deleted
66
+
67
+ end
68
+
69
+ def is_deleted?
70
+ send(soft_delete_by_field_name) != nil
71
+ end
72
+
73
+ def deleted?
74
+ is_deleted?
75
+ end
76
+
77
+ def is_extant?
78
+ !send(soft_delete_by_field_name)
79
+ end
80
+
81
+ def extant?
82
+ is_extant?
83
+ end
84
+
85
+ # You can either overwrite this method to add methods that prevent soft
86
+ # deletion, or set @reasons_not_to_delete to true. For example, set
87
+ # @reasons_not_to_delete = 'Component needs to be deleted first'
88
+ def reasons_not_to_delete?
89
+ @reasons_not_to_delete
90
+ end
91
+
92
+ def deleted_via_soft_delete_by_field_name_at
93
+ send(soft_delete_by_field_name)
94
+ end
95
+
96
+ private
97
+ def table_name_modifier
98
+ klass = self.class
99
+ if klass.kind_of?(SoftDeleteByField) and !klass.instance_of(SoftDeleteByField)
100
+ "#{klass.name.tableize}."
101
+ else
102
+ ""
103
+ end
104
+ end
105
+
106
+ def soft_delete_by_field_name
107
+ self.class.soft_delete_by_field_name
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acts_as_soft_delete_by_field
3
+ version: !ruby/object:Gem::Version
4
+ hash: 19
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 2
10
+ version: 1.0.2
11
+ platform: ruby
12
+ authors:
13
+ - Rob Nichols
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-08-05 00:00:00 +01: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
+ - 3
32
+ - 0
33
+ - 0
34
+ version: 3.0.0
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ description: "Acts as Soft Delete by Field: Provides soft deletion for ActiveRecord models using a field to flag the datetime of deletion"
38
+ email: rob@nicholshayes.co.uk
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ extra_rdoc_files: []
44
+
45
+ files:
46
+ - README
47
+ - Rakefile
48
+ - lib/acts_as_soft_delete_by_field_assertions.rb
49
+ - lib/acts_as_soft_delete_by_field.rb
50
+ - lib/soft_delete_by_field.rb
51
+ has_rdoc: true
52
+ homepage: https://github.com/reggieb/acts_as_soft_delete_by_field
53
+ licenses: []
54
+
55
+ post_install_message:
56
+ rdoc_options: []
57
+
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ hash: 3
66
+ segments:
67
+ - 0
68
+ version: "0"
69
+ required_rubygems_version: !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
+ requirements: []
79
+
80
+ rubyforge_project:
81
+ rubygems_version: 1.3.7
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: Provides soft deletion for ActiveRecord models
85
+ test_files: []
86
+