Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Jeffrey Chupp committed Mar 21, 2009
0 parents commit 5ccd889
Show file tree
Hide file tree
Showing 10 changed files with 322 additions and 0 deletions.
19 changes: 19 additions & 0 deletions MIT-LICENSE
@@ -0,0 +1,19 @@
Copyright (c) 2009 Jeffrey Chupp

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
68 changes: 68 additions & 0 deletions README.textile
@@ -0,0 +1,68 @@
h1. is_paranoid ( same as it ever was )

h3. and you may ask yourself, well, how did I get here?

Sometimes you want to delete something in ActiveRecord, but you realize you might need it later (for an undo feature, or just as a safety net, etc.). There are a plethora of plugins that accomplish this, the most famous of which is the venerable acts_as_paranoid which is great but not really actively developed any more. What's more, acts_as_paranoid was written for an older version of ActiveRecord and, with default_scope in 2.3, it is now possible to do the same thing with significantly less complexity. Thus, *is_paranoid*.

h3. and you may ask yourself, how do I work this?

You should read the specs, or the RDOC, or even the source itself (which is very readable), but for the lazy, here's the hand-holding:

You need ActiveRecord 2.3 and you need to properly install this gem. Then you need a model with a deleted_at timestamp column on its database table. If that column is null, the item isn't deleted. If it has a timestamp, it should count as deleted.

So let's assume we have a model Automobile that has a deleted_at column on the automobiles table.

If you're working with Rails, in your environment.rb, add the following to your initializer block.

<pre>
Rails::Initializer.run do |config|
# ...
config.gem "jchupp-is_paranoid", :lib => 'is_paranoid', :version => ">= 0.0.1"
end
</pre>

Then in your ActiveRecord model

<pre>
class Automobile < ActiveRecord::Base
is_paranoid
end
</pre>

Now our automobiles are now soft-deleteable.

<pre>
that_large_automobile = Automobile.create()
Automobile.count # => 1

that_large_automobile.destroy
Automobile.count # => 0
Automobile.count_with_deleted # => 1

# where is that large automobile?
that_large_automobile = Automobile.find_with_deleted(:all).first
that_large_automobile.restore
Automobile.count # => 1
</pre>

One thing to note, destroying is always undo-able, but deleting is not.

<pre>
Automobile.destroy_all
Automobile.count # => 0
Automobile.count_with_deleted # => 1

Automobile.delete_all
Automobile.count_with_deleted # => 0
# And you may tell yourself, "My god! What have I done?"
</pre>

h3. and you may ask yourself, where does that highway go to?

If you find any bugs, have any ideas of features you think are missing, or find things you're like to see work differently, feel free to send me a message or a pull request.

h3. Thanks

Thanks to Rick Olson for acts_as_paranoid which is obviously an inspiration in concept and execution, Ryan Bates for mentioning the idea of using default_scope for this on Ryan Daigle's "post introducing default_scope":defscope, and the Talking Heads for being the Talking Heads.

[defscope]http://ryandaigle.com/articles/2008/11/18/what-s-new-in-edge-rails-default-scoping
14 changes: 14 additions & 0 deletions Rakefile
@@ -0,0 +1,14 @@
require "spec"
require "spec/rake/spectask"
require 'lib/is_paranoid.rb'

Spec::Rake::SpecTask.new do |t|
t.spec_opts = ['--options', "\"#{File.dirname(__FILE__)}/spec/spec.opts\""]
t.spec_files = FileList['spec/**/*_spec.rb']
end

task :install do
rm_rf "*.gem"
puts `gem build is_paranoid.gemspec`
puts `sudo gem install is_paranoid-0.0.1.gem`
end
40 changes: 40 additions & 0 deletions is_paranoid.gemspec
@@ -0,0 +1,40 @@
# -*- encoding: utf-8 -*-

Gem::Specification.new do |s|
s.name = %q{is_paranoid}
s.version = "0.0.1"

s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Jeffrey Chupp"]
s.date = %q{2009-03-20}
s.email = %q{jeff@semanticart.com}
s.files = [
"lib/is_paranoid.rb",
"README.textile",
"Rakefile",
"MIT-LICENSE",
"spec/android_spec.rb",
"spec/database.yml",
"spec/spec.opts",
"spec/spec_helper.rb",
"spec/schema.rb"
]
s.has_rdoc = true
s.homepage = %q{http://github.com/jchupp/is_paranoid/}
s.require_paths = ["lib"]
s.rubygems_version = %q{1.3.1}
s.summary = %q{ActiveRecord 2.3 compatible gem "allowing you to hide and restore records without actually deleting them." Yes, like acts_as_paranoid, only with less code and less complexity.}

if s.respond_to? :specification_version then
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
s.specification_version = 2

if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
s.add_runtime_dependency(%q<activerecord>, [">=2.3.0"])
else
s.add_dependency(%q<activerecord>, [">=2.3.0"])
end
else
s.add_dependency(%q<activerecord>, [">=2.3.0"])
end
end
72 changes: 72 additions & 0 deletions lib/is_paranoid.rb
@@ -0,0 +1,72 @@
require 'activerecord'

module IsParanoid
def self.included(base) # :nodoc:
base.extend SafetyNet
end

module SafetyNet
# Call this in your model to enable all the safety-net goodness
#
# Example:
#
# class Android < ActiveRecord::Base
# is_paranoid
# end
def is_paranoid
class_eval do
# This is the real magic. All calls made to this model will append
# the conditions deleted_at => nil. Exceptions require using
# exclusive_scope (see self.delete_all, self.count_with_deleted,
# and self.find_with_deleted )
default_scope :conditions => {:deleted_at => nil}

# Actually delete the model, bypassing the safety net. Because
# this method is called internally by Model.delete and on the
# delete method in each instance, we don't need to specify those
# methods separately
def self.delete_all conditions = nil
self.with_exclusive_scope do
super conditions
end
end

# Return a count that includes the soft-deleted models.
def self.count_with_deleted *args
self.with_exclusive_scope { count(*args) }
end

# Return instances of all models matching the query regardless
# of whether or not they have been soft-deleted.
def self.find_with_deleted *args
self.with_exclusive_scope { find(*args) }
end

# Mark the model deleted_at as now.
def destroy_without_callbacks
self.update_attribute(:deleted_at, Time.now.utc)
end

# Override the default destroy to allow us to flag deleted_at.
# This preserves the before_destroy and after_destroy callbacks.
# Because this is also called internally by Model.destroy_all and
# the destroy Model.destroy, we don't need to specify those methods
# separately.
def destroy
return false if callback(:before_destroy) == false
result = destroy_without_callbacks
callback(:after_destroy)
result
end

# Set deleted_at flag on a model to nil, effectively undoing the
# soft-deletion.
def restore
self.update_attribute(:deleted_at, nil)
end
end
end
end
end

ActiveRecord::Base.send(:include, IsParanoid)
76 changes: 76 additions & 0 deletions spec/android_spec.rb
@@ -0,0 +1,76 @@
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')

class Person < ActiveRecord::Base
has_many :androids, :foreign_key => :owner_id, :dependent => :destroy
end

class Android < ActiveRecord::Base
is_paranoid
end

describe Android do
before(:each) do
Android.delete_all
Person.delete_all

@luke = Person.create(:name => 'Luke Skywalker')
@r2d2 = Android.create(:name => 'R2D2', :owner_id => @luke.id)
@c3p0 = Android.create(:name => 'C3P0', :owner_id => @luke.id)
end

it "should delete normally" do
Android.count_with_deleted.should == 2
Android.delete_all
Android.count_with_deleted.should == 0
end

it "should handle Model.destroy_all properly" do
lambda{
Android.destroy_all("owner_id = #{@luke.id}")
}.should change(Android, :count).from(2).to(0)
Android.count_with_deleted.should == 2
end

it "should handle Model.destroy(id) properly" do
lambda{
Android.destroy(@r2d2.id)
}.should change(Android, :count).from(2).to(1)

Android.count_with_deleted.should == 2
end

it "should be not show up in the relationship to the owner once deleted" do
@luke.androids.size.should == 2
@r2d2.destroy
@luke.androids.size.should == 1
Android.count.should == 1
Android.first(:conditions => {:name => 'R2D2'}).should be_blank
end

it "should be able to find deleted items via find_with_deleted" do
@r2d2.destroy
Android.find(:first, :conditions => {:name => 'R2D2'}).should be_blank
Android.find_with_deleted(:first, :conditions => {:name => 'R2D2'}).should_not be_blank
end

it "should have a proper count inclusively and exclusively of deleted items" do
@r2d2.destroy
@c3p0.destroy
Android.count.should == 0
Android.count_with_deleted.should == 2
end

it "should mark deleted on dependent destroys" do
lambda{
@luke.destroy
}.should change(Android, :count).from(2).to(0)
Android.count_with_deleted.should == 2
end

it "should allow restoring" do
@r2d2.destroy
lambda{
@r2d2.restore
}.should change(Android, :count).from(1).to(2)
end
end
3 changes: 3 additions & 0 deletions spec/database.yml
@@ -0,0 +1,3 @@
test:
:adapter: sqlite3
:dbfile: is_paranoid.db
15 changes: 15 additions & 0 deletions spec/schema.rb
@@ -0,0 +1,15 @@
ActiveRecord::Schema.define(:version => 20090317164830) do
create_table "androids", :force => true do |t|
t.string "name"
t.integer "owner_id"
t.datetime "deleted_at"
t.datetime "created_at"
t.datetime "updated_at"
end

create_table "people", :force => true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
end
1 change: 1 addition & 0 deletions spec/spec.opts
@@ -0,0 +1 @@
--color
14 changes: 14 additions & 0 deletions spec/spec_helper.rb
@@ -0,0 +1,14 @@
require 'rubygems'
require "#{File.dirname(__FILE__)}/../lib/is_paranoid"
require 'activerecord'
require 'yaml'
require 'spec'

def connect(environment)
conf = YAML::load(File.open(File.dirname(__FILE__) + '/database.yml'))
ActiveRecord::Base.establish_connection(conf[environment])
end

# Open ActiveRecord connection
connect('test')
load(File.dirname(__FILE__) + "/schema.rb")

0 comments on commit 5ccd889

Please sign in to comment.