Permalink
Browse files

first commit

  • Loading branch information...
0 parents commit 5ccd889a0c898bfe0dd7943f83cc8d11e62a81f6 Jeffrey Chupp committed Mar 21, 2009
Showing with 322 additions and 0 deletions.
  1. +19 −0 MIT-LICENSE
  2. +68 −0 README.textile
  3. +14 −0 Rakefile
  4. +40 −0 is_paranoid.gemspec
  5. +72 −0 lib/is_paranoid.rb
  6. +76 −0 spec/android_spec.rb
  7. +3 −0 spec/database.yml
  8. +15 −0 spec/schema.rb
  9. +1 −0 spec/spec.opts
  10. +14 −0 spec/spec_helper.rb
@@ -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.
@@ -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
@@ -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
@@ -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
@@ -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)
@@ -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
@@ -0,0 +1,3 @@
+test:
+ :adapter: sqlite3
+ :dbfile: is_paranoid.db
@@ -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
@@ -0,0 +1 @@
+--color
@@ -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.