public
Description: A framework for building heavily versioned applications
Homepage:
Clone URL: git://github.com/rich/acts_as_revisable.git
name age message
file .gitignore Wed May 14 14:22:35 -0700 2008 more rake tasks [rich]
file LICENSE Fri May 09 12:12:10 -0700 2008 fixup the copyrights [rich]
file README.rdoc Tue Mar 31 11:48:51 -0700 2009 support generating the revision class at runtime [rich]
file Rakefile Thu Mar 26 12:27:59 -0700 2009 a bit more cleanup [rich]
file acts_as_revisable.gemspec Sat Sep 19 16:26:15 -0700 2009 allow the unique validations to be scoped and o... [Rich Cavanaugh]
directory generators/ Wed Jul 08 16:06:58 -0700 2009 Set defaults for existing objects to still be f... [tommeier]
directory lib/ Sun Oct 18 08:35:40 -0700 2009 * Implement 'branch_source' as a class method i... [mheffner]
directory rails/ Sat May 03 11:48:37 -0700 2008 Import the plugin code from the FatJam Revisabl... [Rich Cavanaugh]
directory spec/ Sun Oct 18 08:37:52 -0700 2009 spec the branch reassociating [Rich Cavanaugh]
README.rdoc

acts_as_revisable

github.com/rich/acts_as_revisable

DESCRIPTION:

acts_as_revisable enables revision tracking, querying, reverting and branching of ActiveRecord models. It does this while providing more Rails-like API than similar plugins. This includes extensions of standard ActiveRecord methods and numerous custom callbacks for the entire AAR life-cycle.

This plugin wouldn’t exist without Rick Olsen’s acts_as_versioned. AAV has been a critical part of practically every Rails project I’ve developed. It’s only through extensive usage of AAV that the concepts for AAR came about.

FEATURES:

  • Both the revisable and revision models must be explicitly defined. Yes, this is a feature. The less magic needed the better. This allows you to build up your revision models just as you would any other.

    If you absolutely need a generated revision model, you may pass ":generate_revision_class => true" to acts_as_revisable and it will generate the class at runtime for you. Think of this like scaffolding and not to be kept around for a real application.

  • Numerous custom callbacks for both revisable and revision models.
    • revisable models
      • before_revise
      • after_revise
      • before_revert
      • after_revert
      • before_changeset
      • after_changeset
      • after_branch_created
      • before_revise_on_destroy (when :on_destroy => :revise is set)
      • after_revise_on_destroy (when :on_destroy => :revise is set)
    • revision models
      • before_restore
      • after_restore
    • both revisable and revision models
      • before_branch
      • after_branch

    These work like any other ActiveRecord callbacks. The before_* callbacks can stop the the action. This uses the Callbacks module in ActiveSupport.

  • Works with a single table.
    • Provides migration generators to add the revisable columns.
  • Grouping several revisable actions into a single revision (changeset).
  • Monitor all or just specified columns to trigger a revision.
  • Uses ActiveRecord’s dirty attribute tracking.
  • Several ways to find revisions including:
    • revision number
    • relative keywords (:first, :previous and :last)
    • timestamp
  • Reverting
  • Branching
  • Selectively disable revision tracking
  • Naming revisions

SYNOPSIS:

Given a simple model:

  class Project < ActiveRecord::Base
    # columns: id, name, unimportant, created_at
  end

Let’s make the projects table revisable:

  ruby script/generate revisable_migration Project
  rake db:migrate

Now Project itself:

  class Project < ActiveRecord::Base
    has_one :owner

    acts_as_revisable do
      revision_class_name "Session"
      except :unimportant
    end
  end

Create the revision class:

  class Session < ActiveRecord::Base
    # we can accept the more standard hash syntax
    acts_as_revision :revisable_class_name => "Project"
  end

Some example usage:

  @project = Project.create(:name => "Rich", :unimportant => "some text")
  @project.revision_number        # => 0

  @project.update_attribute(:unimportant, "more text")
  @project.revision_number        # => 0

  @project.name = "Stephen"
  @project.save(:without_revision => true)
  @project.name                   # => "Stephen"
  @project.revision_number        # => 0

  @project.name = "Sam"
  @project.save(:revision_name => "Changed name")
  @project.revision_number        # => 1

  @project.updated_attribute(:name, "Third")
  @project.revision_number        # => 2

Navigating revisions:

  @previous = @project.find_revision(:previous)
  # or
  @previous = @project.revisions.first

  @previous.name                  # => "Sam"
  @previous.current_revision.name # => "Third"
  @previous.project.name          # => "Third"
  @previous.revision_name         # => "Changed name"

  @previous.previous.name         # => "Rich"

  # Forcing the creation of a new revision.
  @project.updated_attribute("Rogelio")
  @project.revision_number        # => 3

  @newest = @project.find_revision(:previous)
  @newest.ancestors.map(&:name)   # => ["Third", "Rich"]

  @oldest = @project.find_revision(:first)
  @oldest.descendants.map(&:name) # => ["Sam", "Third"]

Reverting:

  @project.revert_to!(:previous)
  @project.revision_number        # => 2
  @project.name                   # => "Rich"

  @project.revert_to!(1, :without_revision => true)
  @project.revision_number        # => 2
  @project.name                   # => "Sam"

Branching:

  @branch = @project.branch(:name => "Bruno")
  @branch.revision_number         # => 0
  @branch.branch_source.name      # => "Sam"

Changesets:

  @project.revision_number        # => 2

  @project.changeset! do
    @project.name = "Josh"

    # save would normally trigger a revision
    @project.save

    # update_attribute triggers a save triggering a revision (normally)
    @project.updated_attribute(:name, "Chris")

    # revise! normally forces a revision to be created
    @project.revise!
  end

  # our revision number has only incremented by one
  @project.revision_number        # => 3

Maybe we don’t want to be able to branch from revisions:

  class Session < ActiveRecord::Base
    # assuming we still have the other code from Session above

    before_branch do
      false
    end
  end

  @project.revisions.first.branch # Raises an exception
  @project.branch                 # works as expected

If the owner isn’t set let’s prevent reverting:

  class Project < ActiveRecord::Base
    # assuming we still have the other code from Project above

    before_revert :check_owner_befor_reverting
    def check_owner_befor_reverting
      false unless self.owner?
    end
  end

REQUIREMENTS:

This plugin requires Rails 2.3. Use version 0.9.8 of this plugin for Rails 2.1 and 2.2.

INSTALL:

acts_as_revisable uses Rails’ new ability to use gems as plugins. Installing AAR is as simple as installing a gem:

  sudo gem install rich-acts_as_revisable --source=http://gems.github.com

Once the gem is installed you’ll want to activate it in your Rails app by adding the following line to config/environment.rb:

  config.gem "rich-acts_as_revisable", :lib => "acts_as_revisable", :source => "http://gems.github.com"

LICENSE:

(The MIT License)

Copyright © 2009 Rich Cavanaugh

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.