Skip to content

Commit

Permalink
Improve rdoc support.
Browse files Browse the repository at this point in the history
Add rake task to build rdoc. Reformat README for rdoc. Fixup rdoc in code.
  • Loading branch information
ajh committed May 19, 2008
1 parent 0ff304f commit 2171bff
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 75 deletions.
110 changes: 49 additions & 61 deletions README
@@ -1,103 +1,91 @@
Copyright (C) 2008 Substantial and Andy Hartford <hartforda @ gmail.com>

=== acts_as_soft_deletable ===
== acts_as_soft_deletable

This plugin provides the ability to soft delete ActiveRecord models. When
models are destroyed, they will be archived into a special deleted table.
They can later be restored easily.

<pre>
class Artist < ActiveRecord::Base
acts_as_soft_deletable # This will wrap the destroy method to provide soft delete
# support and create a new ActiveRecord class called Artist::Deleted
end

class Artist < ActiveRecord::Base
acts_as_soft_deletable # This will wrap the destroy method to provide soft delete
# support and create a new ActiveRecord class called Artist::Deleted
end
model = Artist.find(34)
model.destroy # removes row from artists table, and adds a row to
# deleted_artists table

model = Artist.find(34)
model.destroy # removes row from artists table, and adds a row to
# deleted_artists table
deleted = Artist::Deleted.find(34)
deleted.undestroy! # adds the row back to the artists table, and removes
# if from the deleted_artists table

deleted = Artist::Deleted.find(34)
deleted.undestroy! # adds the row back to the artists table, and removes
# if from the deleted_artists table
restored = Artist.find(34) # The artist is restored with all the same
# information. The updated_at column will be
# Time.now if it exists.

restored = Artist.find(34) # The artist is restored with all the same
# information. The updated_at column will be
# Time.now if it exists.

</pre>

=== Compare to acts_as_paranoid ===
== Compare to acts_as_paranoid

Acts_as_paranoid takes the approach of using a deleted_at flag in the models table. If the deleted_at column has a value, the row is considered 'deleted'. The problem with this approach is that all finds on the model have to exclude 'deleted' rows. This turns out be be a challenge. Acts_as_paranoid patches the ActiveRecord internals to accomplish this, but it is fragile and could break with future changes to ActiveRecord. Also, some of the more exotic finds currently don't work (has_many :through with polymorphism as of March 2008) and supporting them means running on an upgrade treadmill to keep up with the evolution of ActiveRecord.

This plugin avoids these problems by allowing the row to be deleted and archiving it into another table. The behavior of ActiveRecord::Base#Find doesn't have to change which should mean that this plugin is more immune to breaking due to ActiveRecord development. Queries on the live table will also be faster in the case of lots of deleted rows, because they will be in a seperate table. The tradeoff is that the deleted table needs to be maintained along with the live table. For example, if the artists table adds a column in a future migration, then the deleted_artists table needs that column as well.

A migration helper is available (see below) that will help keep the deleted table in sync. Also, a unit test helper is provided (again, see below) which adds unit tests to the model ensuring soft delete is working. If this is used, the test will fail if the deleted table gets out of sync.

=== Setup ===
== Setup

---+ Model
=== Model

Any ActiveRecord class that wants the soft delete functionality should add
the following line to their class definition:

<pre>
class SomeModel < ActiveRecord::Base
acts_as_soft_deletable
...
</pre>
class SomeModel < ActiveRecord::Base
acts_as_soft_deletable
...

---+ Migration
=== Migration

and setup the deleted table with the following migration:

<pre>
class AddActsAsSoftDeletable < ActiveRecord::Migration
def self.up
SomeModel::Deleted.create_table
end
class AddActsAsSoftDeletable < ActiveRecord::Migration
def self.up
SomeModel::Deleted.create_table
end

def self.down
SomeModel::Deleted.drop_table
end
end
</pre>
def self.down
SomeModel::Deleted.drop_table
end
end

Any changes to the original table (such as adding a column) should be reflected in the deleted table. Use the update_columns method:

<pre>
class AddSkuColumn < ActiveRecord::Migration
def self.up
add_column 'items', 'sku', :string
Item::Deleted.update_columns # will add sku column
end
class AddSkuColumn < ActiveRecord::Migration
def self.up
add_column 'items', 'sku', :string
Item::Deleted.update_columns # will add sku column
end

def self.down
remove_column 'items', 'sku'
Item::Deleted.update_columns # will remove sku column
end
end
</pre>
def self.down
remove_column 'items', 'sku'
Item::Deleted.update_columns # will remove sku column
end
end

Note that update_columns will happily delete columns if asked which will lose data forever. I'm not sure if this behavior is fine or horrifying, but it's definitely something to keep in mind. I'd appreciate feedback on this.

---+ Unit tests
=== Unit tests

A model's soft delete capabilities can be easily unit tested by using this provided assert:

<pre>
def test_soft_delete_works
# will run the model through a destroy and undestroy while making sure all values were saved
assert_model_soft_deletes( items(:radar_detector) )
end
</pre>
def test_soft_delete_works
# will run the model through a destroy and undestroy while making sure all values were saved
assert_model_soft_deletes( items(:radar_detector) )
end

This was developed with Test::Unit in mind. Not sure how well it works with rspec.

=== Thanks ===
=== Thanks

Substantial, my employer for letting me release this
acts_as_paranoid and technoweenie, for a plugin I've got good years of use out of
acts_as_versioned, who's approach influenced this plugin
Danimal, for the feedback on rubyonrails-talk
* Substantial, my employer for letting me release this
* acts_as_paranoid and technoweenie, for a plugin I've got good years of use out of
* acts_as_versioned, who's approach influenced this plugin
* Danimal, for the feedback on rubyonrails-talk
10 changes: 10 additions & 0 deletions Rakefile
@@ -1,5 +1,6 @@
require 'rubygems'
require 'rake/testtask'
require 'rake/rdoctask'

namespace :test do
%w(sqlite sqlite3 postgresql mysql).each do |adapter|
Expand All @@ -20,3 +21,12 @@ task :test => ["test:sqlite", "test:sqlite3", "test:postgresql", "test:mysql"]
desc 'Default: run unit tests'
task :default => :test

Rake::RDocTask.new("doc") do |rdoc|
rdoc.rdoc_dir = 'doc'
rdoc.template = ENV['template'] if ENV['template']
rdoc.title = "Acts As Soft Deletable Documentation"
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.options << '--charset' << 'utf-8'
rdoc.rdoc_files.include('lib/*.rb')
rdoc.rdoc_files.include('README')
end
30 changes: 21 additions & 9 deletions lib/acts_as_soft_deletable.rb
@@ -1,9 +1,14 @@
module ActiveRecord #:nodoc:
module Acts #:nodoc:
# Specify this act if you wish to save a copy of the row in a special deleted table so that it can be
# restored later.
module SoftDeletable
module SoftDeletable #:nodoc:
module ClassMethods
# Specify this act if you wish to archive deleted rows in a special deleted table so that
# they can be later restored.
#
# This includes and extends Live::InstanceMethods and Live::ClassMethods into this class.
#
# It will also create a new ActiveRecord::Base class named after this class with the suffix <tt>::Deleted</tt> added.
# The new class is used for dealing with rows that have been deleted. See the README for more info and examples.
def acts_as_soft_deletable
# don't allow multiple calls
return if self.included_modules.include?(Live::InstanceMethods)
Expand All @@ -25,10 +30,11 @@ def acts_as_soft_deletable
end
end

module Deleted
module Deleted #:nodoc:

# These methods will be available as class methods on the deleted class.
module ClassMethods
# Creates a deleted table by introspecting on the live table
# Creates a deleted table by introspecting on the live table. Useful in a migration #up method.
def create_table(create_table_options = {})
connection.create_table(table_name, create_table_options) do |t|
live_class.columns.select{|col| col.name != live_class.primary_key}.each do |col|
Expand All @@ -38,7 +44,7 @@ def create_table(create_table_options = {})
end
end

# Drops the deleted table
# Drops the deleted table. Useful a migration #down method.
def drop_table(drop_table_options = {})
connection.drop_table(table_name, drop_table_options)
end
Expand Down Expand Up @@ -76,8 +82,9 @@ def update_columns
end
end

# These methods will be available as instance methods on the deleted class.
module InstanceMethods
# restore the model from deleted status. Will destroy the deleted record and recreate the live record
# Restore the model from deleted status. Will destroy the deleted record and recreate the live record. This is done in a transaction and will rollback if problems occur.
def undestroy!
self.class.transaction do
model = self.class.live_class.new
Expand All @@ -92,15 +99,17 @@ def undestroy!
end
end

module Live
module Live #:nodoc:

# These methods will be available as class methods for the Model class that invoked acts_as_soft_deletable
module ClassMethods
# returns instance of deleted class
# Returns Class object of deleted class
def deleted_class
@deleted_class
end
end

# These methods will be available as instance methods for the Model class that invoked acts_as_soft_deletable
module InstanceMethods
def self.included(base)
base.class_eval do
Expand All @@ -109,6 +118,9 @@ def self.included(base)
end
end

# Wraps ActiveRecord::Base#destroy to provide the soft deleting behavoir.
# The insert into the deleted table is protected with a transaciton and
# will be rolled back if destroy raises any exception.
def destroy_with_soft_delete
self.class.transaction do
deleted = self.class.deleted_class.new
Expand Down
11 changes: 6 additions & 5 deletions lib/unit_test_helper.rb
@@ -1,8 +1,9 @@
module Test
module Unit
module Test #:nodoc:
module Unit #:nodoc:
# This module is included into Test::Unit::TestCase and in that way is available in your test cases.
module ActsAsDeleted
# Takes a saved model and runs assertions testing whether soft deleting is working
def assert_model_soft_deletes(model)
# Takes a saved model and runs assertions testing whether soft deleting is working.
def assert_model_soft_deletes(model) # TODO: should accept a message argument
klass = model.class
deleted_klass = model.class.deleted_class

Expand All @@ -19,7 +20,7 @@ def assert_model_soft_deletes(model)
end

# Asserts whether a two soft deleting models are equal. Intended to be passed
# an instance of a model and an instance of the deleted class's model. Checks that
# an instance of a model and an instance of the deleted class's model (in any order). Checks that
# all attributes were saved off correctly.
def assert_soft_delete_models_are_equal(a, b, message = "models weren't equal")
reject_attrs = %q(deleted_at, updated_at)
Expand Down

0 comments on commit 2171bff

Please sign in to comment.