rubiety/has_draft

View on GitHub
README.rdoc

Summary

Maintainability
Test Coverage
{<img src="https://secure.travis-ci.org/rubiety/has_draft.svg?branch=master" alt="Build Status" />}[http://travis-ci.org/rubiety/has_draft]
{<img src="https://codeclimate.com/github/rubiety/has_draft.svg" />}[https://codeclimate.com/github/rubiety/has_draft]

== Has Draft

Allows for multiple "drafts" of a model which can be useful when developing:
* Draft/Live Version of Pages, for examples
* A workflow system whereby a live copy may need to be active while a draft copy is 
  awaiting approval.

This was built to be able to be tacked on to existing models, so the data schema doesn't need to change 
at all for the model this is applied to.  Drafts are actually stored in a nearly-identical table 
and there is a has_one relationship to this.  This separation allows the base model to really be treated 
just as before without having to apply conditions in queries to make sure you are really getting the 
"live" (non-draft) copy: Page.all will still only return the non-draft pages.  This separate table is backed by 
a model created on the fly as a constant on the original model class.  For example if a Page has_draft, 
a Page::Draft class will exist as the model for the page_drafts table.

== Installation

This gem supports ActiveRecord 3, 4, and 5.

In your Gemfile:
  
  gem "has_draft"

== Basic Example
  
  ## First Migration (If Creating base model and drafts at the same time):
  class InitialSchema < ActiveRecord::Migration
  
    [:articles, :article_drafts].each do |table_name|
      create_table table_name, :force => true do |t|
        t.references :article if table_name == :article_drafts
      
        t.string :title
        t.text :summary
        t.text :body
        t.date :post_date
      end
    end
    
  end
  
  
  ## Model Class
  class Article < ActiveRecord::Base
    has_draft
  end
  
  ## Exposed Class Methods & Scopes:
  Article.draft_class
  => Article::Draft
  Article.with_draft.all
  => (Articles that have an associated draft)
  Article.without_draft.all
  => (Articles with no associated draft)
  
  
  
  ## Usage Examples:
  
  article = Article.create(
    :title => "My Title", 
    :summary => "Information here.",
    :body => "Full body",
    :post_date => Date.today
  )
  
  article.has_draft?
  => false
  
  article.instantiate_draft!
  
  article.has_draft?
  => true
  
  article.draft
  => Article::Draft Instance
  
  article.draft.update_attributes(
    :title => "New Title"
  )
  
  article.replace_with_draft!
  
  article.title
  => "New Title"
  
  article.destroy_draft!
  
  article.has_draft?
  => false
  
== Custom Options
  
  ## First Migration (If Creating base model and drafts at the same time):
  class InitialSchema < ActiveRecord::Migration
  
    [:articles, :article_copies].each do |table_name|
      create_table table_name, :force => true do |t|
        t.integer :news_article_id if table_name == :article_copies
      
        t.string :title
        t.text :summary
        t.text :body
        t.date :post_date
      end
    end
    
  end
  
  ## Model Class
  class Article < ActiveRecord::Base
    has_draft :class_name => 'Copy', :foreign_key => :news_article_id, :table_name => 'article_copies'
  end
  
  ## Single Table Inheritance Example
  class InitialSchema < ActiveRecord::Migration
  
    [:elements, :element_drafts].each do |table_name|
      create_table table_name, :force => true do |t|
        t.integer :element_id if table_name == :element_drafts
      
        t.string :title
        t.text :content
        t.string :type
      end
    end
    
  end
  
  class Element < ActiveRecord::Base
    class Draft < ActiveRecord::Base
      def element_icon_path
        'assets/images/default.png'
      end
    end
    
    module HasDraftCallbacks
      def before_instantiate_draft
        # Need to append ::Draft so that it knows to use the Draft class
        self.draft.type = self.type + '::Draft'
      end
          
      def before_replace_with_draft
        # We are storing drafts with the ::Draft on the end so we have to strip it
        self.type = self.draft.type.split(':')[0]
      end
    end
  end
  
  class BlueElement < Element
    has_draft :belongs_to => :element, :extends => Element::Draft do
      def element_icon_path
        'assets/images/blue.png'
      end
    end
    
    include Element::HasDraftCallbacks
  end
  
  class UnknownElement < Element
    has_draft :belongs_to => :element, :extends => Element::Draft
    
    include Element::HasDraftCallbacks
  end
  
  This allows us to call element_draft.element on any subclass of Element::Draft
  instead of needing to know to call element_draft.blue_element or element_draft.unknown_element.
  
  Note: calling has_draft will have no effect if the parent class has already executed has_draft. 
    
  

== Method Callbacks

There are three callbacks you can specify directly as methods:

  class Article < ActiveRecord::Base
    has_draft
    
    def before_instantiate_draft
      # Do Something
    end
    
    def before_replace_with_draft
      # Do Something
    end
    
    def before_destroy_draft
      # Do Something
    end
  end



== Extending the Draft Class

Because you don't directly define the draft class, you can specify a block of code to be run in its
context by passing a block to has_draft:
  
  class Article < ActiveRecord::Base
    belongs_to :user
    
    has_draft do
      belongs_to :last_updated_user
      
      def approve!
        self.approved_at = Time.now
        self.save
      end
    end
    
  end

== Running Tests

This gem uses appraisal to test with different versions of the dependencies. See Appraisal first for which versions are tested.

  # Just the gems locked in Gemfile.lock
  $ bundle exec rake test

  # All of the Appraisals:
  $ bundle exec rake all