Skip to content

Instantly share code, notes, and snippets.

@TimFletcher
Created November 7, 2012 17:05
Show Gist options
  • Save TimFletcher/4032984 to your computer and use it in GitHub Desktop.
Save TimFletcher/4032984 to your computer and use it in GitHub Desktop.
Trashable 'concern' for Rails models
# db/migrate/20120625030355_add_deleted_at_to_user.rb
class AddDeletedAtToUser < ActiveRecord::Migration
def change
add_column :users, :deleted_at, :time
end
end
# config/application.rb
module YourApp
class Application < Rails::Application
config.autoload_paths += %W(
#{config.root}/lib
#{config.root}/app/controllers/concerns
#{config.root}/app/models/concerns
)
end
# app/models/concerns/trashable.rb
module Trashable
extend ActiveSupport::Concern
included do
default_scope where(deleted_at: nil)
end
module ClassMethods
def trashed
self.unscoped.where(self.arel_table[:deleted_at].not_eq(nil))
end
end
def trash
run_callbacks :destroy do
update_column :deleted_at, Time.now
end
end
def recover
# update_column not appropriate here as it uses the default scope
update_attribute :deleted_at, nil
end
end
# app/models/user.rb
class User < ActiveRecord::Base
include Trashable
end
@mlangenberg
Copy link

Any reason why a :time instead of a :datetime field type is used? How is recording the time of the day useful?

@dbryand
Copy link

dbryand commented Jun 3, 2014

I liked this and wrote some specs for it...

shared_examples 'trashable' do
  let(:class_symbol)  { described_class.name.underscore }
  let(:deleted)       { create(class_symbol, deleted_at: Time.now) }
  let(:not_deleted)   { create(class_symbol) }

  describe '.trashed' do
    before do
      deleted
      not_deleted
    end

    it 'finds deleted items' do
      item_found = described_class.trashed

      expect(item_found.count).to eq 1
      expect(item_found.first).to eq deleted
    end
  end

  describe '#trash' do
    it "sets deleted_at" do
      expect(not_deleted.deleted_at).to be_nil
      not_deleted.trash
      expect(not_deleted.deleted_at).to be
    end
  end

  describe '#recover' do
    it "unsets deleted_at" do
      expect(deleted.deleted_at).to be
      deleted.recover
      expect(deleted.deleted_at).to be_nil
    end
  end
end

Which I use like:

describe Photo do
  it_behaves_like 'trashable'

  it "has a valid factory" do
....

@bouchard
Copy link

Can also be written, I believe (without touching Arel directly) as:

module ClassMethods
  def trashed
    self.unscoped.where.not(deleted_at: nil)
   end
end

@nativestranger
Copy link

I like to override #destroy like this so that dependent models are protected during 'dependent: :destroy' callbacks.

def destroy
  self.trash
end

For example, this way, the comments of a user will be trashed rather than destroyed in a situation like the one below.

class User < ApplicationRecord
  include Trashable
  has_many :comments, dependent: :destroy
end

class Comment < ApplicationRecord
  include Trashable
  belongs_to :user
end

Now it'd be nice if #recover could handle recovering trashed dependents.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment