acts_as_soft_delete_by_field 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
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
+