Permalink
Browse files

Import the plugin code from the FatJam Revisable branch.

Setup a simple gitignore.
Some initial hoe configuration.
  • Loading branch information...
1 parent 3172787 commit 3299d0bd5801138f546d6575e8acfc0325ac7108 Rich Cavanaugh committed May 3, 2008
View
1 .gitignore
@@ -0,0 +1 @@
+.DS_Store
View
30 config/hoe.rb
@@ -2,39 +2,20 @@
AUTHOR = 'Rich Cavanaugh' # can also be an array of Authors
EMAIL = "rich@fatjam.com"
-DESCRIPTION = "description of gem"
+DESCRIPTION = "Rails plugin to track revisions to your models."
GEM_NAME = 'acts_as_revisable' # what ppl will type to install your gem
-RUBYFORGE_PROJECT = 'acts_as_revisable' # The unix name for your project
-HOMEPATH = "http://github.com/rich/acts_as_revisable"
-DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
+HOMEPATH = "http://github.com/rich/acts_as_revisable/tree/master"
+DOWNLOAD_PATH = "http://github.com/rich/acts_as_revisable/tree/master"
EXTRA_DEPENDENCIES = [
- ['activesupport', '>= 2.1'], ['activerecord', '>= 2.1']
] # An array of rubygem dependencies [name, version]
@config_file = "~/.rubyforge/user-config.yml"
@config = nil
-RUBYFORGE_USERNAME = "unknown"
-def rubyforge_username
- unless @config
- begin
- @config = YAML.load(File.read(File.expand_path(@config_file)))
- rescue
- puts <<-EOS
-ERROR: No rubyforge config file found: #{@config_file}
-Run 'rubyforge setup' to prepare your env for access to Rubyforge
- - See http://newgem.rubyforge.org/rubyforge.html for more details
- EOS
- exit
- end
- end
- RUBYFORGE_USERNAME.replace @config["username"]
-end
-
REV = nil
# UNCOMMENT IF REQUIRED:
# REV = YAML.load(`svn info`)['Revision']
-VERS = ActsAsRevisable::VERSION::STRING + (REV ? ".#{REV}" : "")
+VERS = FatJam::ActsAsRevisable::VERSION::STRING + (REV ? ".#{REV}" : "")
RDOC_OPTS = ['--quiet', '--title', 'acts_as_revisable documentation',
"--opname", "index.html",
"--line-numbers",
@@ -55,7 +36,6 @@ def extra_deps
p.description = DESCRIPTION
p.summary = DESCRIPTION
p.url = HOMEPATH
- p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
p.test_globs = ["test/**/test_*.rb"]
p.clean_globs |= ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store'] #An array of file patterns to delete on clean.
@@ -67,7 +47,5 @@ def extra_deps
end
CHANGES = $hoe.paragraphs_of('History.txt', 0..1).join("\\n\\n")
-PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
-$hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
$hoe.rsync_args = '-av --delete --ignore-errors'
$hoe.spec.post_install_message = File.open(File.dirname(__FILE__) + "/../PostInstall.txt").read rescue ""
View
9 generators/revisable_migration/revisable_migration_generator.rb
@@ -0,0 +1,9 @@
+class RevisableMigrationGenerator < Rails::Generator::NamedBase
+ def manifest
+ record do |m|
+ revisable_columns = [["revisable_original_id", "integer"], ["revisable_branched_from_id", "integer"], ["revisable_number", "integer"], ["revisable_name", "string"], ["revisable_type", "string"], ["revisable_current_at", "datetime"], ["revisable_revised_at", "datetime"], ["revisable_deleted_at", "datetime"], ["revisable_is_current", "boolean"]]
+
+ m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => "make_#{class_name.downcase}_revisable", :assigns => {:cols => revisable_columns}
+ end
+ end
+end
View
13 generators/revisable_migration/templates/migration.rb
@@ -0,0 +1,13 @@
+class Make<%= class_name.underscore.camelize %>Revisable < ActiveRecord::Migration
+ def self.up
+ <% cols.each do |c| -%>
+ add_column :<%= class_name.downcase.pluralize %>, :<%= c.first %>, :<%= c.last %>
+ <% end -%>
+ end
+
+ def self.down
+ <% cols.each do |c| -%>
+ remove_column :<%= class_name.downcase.pluralize %>, :<%= c.first %>
+ <% end -%>
+ end
+end
View
11 lib/acts_as_revisable.rb
@@ -1,6 +1,11 @@
$:.unshift(File.dirname(__FILE__)) unless
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
-module ActsAsRevisable
-
-end
+require 'acts_as_revisable/version.rb'
+require 'acts_as_revisable/acts/scoped_model'
+require 'acts_as_revisable/quoted_columns'
+require 'acts_as_revisable/base'
+
+ActiveRecord::Base.send(:include, FatJam::ActsAsScopedModel)
+ActiveRecord::Base.send(:include, FatJam::QuotedColumnConditions)
+ActiveRecord::Base.send(:include, FatJam::ActsAsRevisable)
View
83 lib/acts_as_revisable/acts/common.rb
@@ -0,0 +1,83 @@
+module FatJam
+ module ActsAsRevisable
+ module Common
+ def self.included(base)
+ base.send(:extend, ClassMethods)
+
+ class << base
+ alias_method_chain :instantiate, :revisable
+ end
+
+ base.instance_eval do
+ define_callbacks :before_branch, :after_branch
+ has_many :branches, :class_name => base.class_name, :foreign_key => :revisable_branched_from_id
+ belongs_to :branch_source, :class_name => base.class_name, :foreign_key => :revisable_branched_from_id
+
+ end
+ base.alias_method_chain :branch_source, :open_scope
+ end
+
+ def branch_source_with_open_scope(*args, &block)
+ self.class.without_model_scope do
+ branch_source_without_open_scope(*args, &block)
+ end
+ end
+
+ def branch(*args)
+ unless run_callbacks(:before_branch) { |r, o| r == false}
+ raise ActiveRecord::RecordNotSaved
+ end
+
+ options = args.extract_options! || {}
+ options[:revisable_branched_from_id] = self.id
+ self.class.column_names.each do |col|
+ next unless self.class.revisable_should_clone_column? col
+ options[col.to_sym] ||= self[col]
+ end
+
+ returning(self.class.revisable_class.create!(options)) do |br|
+ run_callbacks(:after_branch)
+ br.run_callbacks(:after_branch_created)
+ end
+ end
+
+ def original_id
+ self[:revisable_original_id] || self[:id]
+ end
+ module ClassMethods
+ def revisable_should_clone_column?(col)
+ return false if (FatJam::REVISABLE_SYSTEM_COLUMNS + FatJam::REVISABLE_UNREVISABLE_COLUMNS).member? col
+ true
+ end
+
+ def instantiate_with_revisable(record)
+ is_current = columns_hash["revisable_is_current"].type_cast(
+ record["revisable_is_current"])
+
+ if (is_current && self == self.revisable_class) || (is_current && self == self.revision_class)
+ return instantiate_without_revisable(record)
+ end
+
+ object = if is_current
+ self.revisable_class
+ else
+ self.revision_class
+ end.allocate
+
+ object.instance_variable_set("@attributes", record)
+ object.instance_variable_set("@attributes_cache", Hash.new)
+
+ if object.respond_to_without_attributes?(:after_find)
+ object.send(:callback, :after_find)
+ end
+
+ if object.respond_to_without_attributes?(:after_initialize)
+ object.send(:callback, :after_initialize)
+ end
+
+ object
+ end
+ end
+ end
+ end
+end
View
236 lib/acts_as_revisable/acts/revisable.rb
@@ -0,0 +1,236 @@
+module FatJam
+ module ActsAsRevisable
+ module Revisable
+ def self.included(base)
+ base.send(:extend, ClassMethods)
+
+ base.class_inheritable_hash :aa_revisable_current_revisions
+ base.aa_revisable_current_revisions = {}
+
+ class << base
+ alias_method_chain :find, :revisable
+ alias_method_chain :with_scope, :revisable
+ end
+
+ base.instance_eval do
+ define_callbacks :before_revise, :after_revise, :before_revert, :after_revert, :before_changeset, :after_changeset, :after_branch_created
+
+ alias_method_chain :save, :revisable
+ alias_method_chain :save!, :revisable
+
+ acts_as_scoped_model :find => {:conditions => {:revisable_is_current => true}}
+
+ has_many :revisions, :class_name => revision_class_name, :foreign_key => :revisable_original_id, :order => "revisable_number DESC", :dependent => :destroy
+ has_many revision_class_name.pluralize.downcase.to_sym, :class_name => revision_class_name, :foreign_key => :revisable_original_id, :order => "revisable_number DESC", :dependent => :destroy
+
+ before_create :before_revisable_create
+ before_update :before_revisable_update
+ after_update :after_revisable_update
+ end
+ end
+
+ def before_revisable_create
+ self[:revisable_is_current] = true
+ end
+
+ def before_revisable_update
+ return unless @aa_revisable_force_revision == true || (self.changed? && !(@aa_revisable_no_revision === true) && !(self.changed.map(&:downcase) & self.class.revisable_columns).blank?)
+
+ return false unless run_callbacks(:before_revise) { |r, o| r == false}
+
+ @revisable_revision = self.to_revision
+ end
+
+ def after_revisable_update
+ if @revisable_revision
+ @revisable_revision.save
+ @aa_revisable_was_revised = true
+ revisions.reload
+ run_callbacks(:after_revise)
+ end
+ end
+
+ def to_revision
+ rev = self.class.revision_class.new(@aa_revisable_new_params)
+
+ rev.revisable_original_id = self.id
+
+ self.class.column_names.each do |col|
+ next unless self.class.revisable_should_clone_column? col
+ val = self.send("#{col}_changed?") ? self.send("#{col}_was") : self.send(col)
+ rev.send("#{col}=", val)
+ end
+
+ @aa_revisable_new_params = nil
+
+ rev
+ end
+
+ def save_with_revisable!(*args)
+ @aa_revisable_new_params ||= args.extract_options!
+ @aa_revisable_no_revision = true if @aa_revisable_new_params.delete :without_revision
+ save_without_revisable!(*args)
+ end
+
+ def save_with_revisable(*args)
+ @aa_revisable_new_params ||= args.extract_options!
+ @aa_revisable_no_revision = true if @aa_revisable_new_params.delete :without_revision
+ save_without_revisable(*args)
+ end
+
+ def find_revision(number)
+ revisions.find_by_revisable_number(number)
+ end
+
+ def revert_to!(*args)
+ unless run_callbacks(:before_revert) { |r, o| r == false}
+ raise ActiveRecord::RecordNotSaved
+ end
+
+ options = args.extract_options! || {}
+
+ rev = case args.first
+ when self.class.revision_class
+ args.first
+ when :first
+ revisions.last
+ when :previous
+ revisions.first
+ when Fixnum
+ revisions.find_by_revisable_number(args.first)
+ when Time
+ revisions.find(:first, :conditions => ["? >= ? and ? <= ?", :revisable_revised_at, args.first, :revisable_current_at, args.first])
+ end
+
+ unless rev.run_callbacks(:before_restore) { |r, o| r == false}
+ raise ActiveRecord::RecordNotSaved
+ end
+
+ self.class.column_names.each do |col|
+ next unless self.class.revisable_should_clone_column? col
+ self[col] = rev[col]
+ end
+
+ @aa_revisable_no_revision = true if options.delete :without_revision
+ @aa_revisable_new_params = options
+
+ returning(@aa_revisable_no_revision ? save! : revise!) do
+ rev.run_callbacks(:after_restore)
+ run_callbacks(:after_revert)
+ end
+ end
+
+ def revert_to_without_revision!(*args)
+ args << {} if args.grep(Hash).blank?
+ args.grep(Hash).first.update({:without_revision => true})
+ revert_to!(*args)
+ end
+
+ def revise!
+ return if in_revision?
+
+ @aa_revisable_force_revision = true
+ in_revision!
+ returning(save!) do
+ in_revision!(false)
+ @aa_revisable_force_revision = false
+ end
+ end
+
+ def revised?
+ @aa_revisable_was_revised || false
+ end
+
+ def in_revision?
+ key = self.read_attribute(self.class.primary_key)
+ aa_revisable_current_revisions[key] || false
+ end
+
+ def in_revision!(val=true)
+ key = self.read_attribute(self.class.primary_key)
+ aa_revisable_current_revisions[key] = val
+ aa_revisable_current_revisions.delete(key) unless val
+ end
+
+ def changeset(&block)
+ return unless block_given?
+
+ return yield(self) if in_revision?
+
+ unless run_callbacks(:before_changeset) { |r, o| r == false}
+ raise ActiveRecord::RecordNotSaved
+ end
+
+ in_revision!
+
+ returning(yield(self)) do
+ in_revision!(false)
+ run_callbacks(:after_changeset)
+ end
+ end
+
+ def revision_number
+ revisions.first.revisable_number
+ rescue NoMethodError
+ 0
+ end
+
+ module ClassMethods
+ def with_scope_with_revisable(*args, &block)
+ options = args.grep(Hash).first[:find]
+
+ if options && options.delete(:with_revisions)
+ without_model_scope do
+ with_scope_without_revisable(*args, &block)
+ end
+ else
+ with_scope_without_revisable(*args, &block)
+ end
+ end
+
+ def find_with_revisable(*args)
+ options = args.grep(Hash).first
+
+ if options && options.delete(:with_revisions)
+ without_model_scope do
+ find_without_revisable(*args)
+ end
+ else
+ find_without_revisable(*args)
+ end
+ end
+
+ def find_with_revisions(*args)
+ args << {} if args.grep(Hash).blank?
+ args.grep(Hash).first.update({:with_revisions => true})
+ find_with_revisable(*args)
+ end
+
+ def revision_class_name
+ self.revisable_options.revision_class || "#{self.class_name}Revision"
+ end
+
+ def revision_class
+ @aa_revision_class ||= revision_class_name.constantize
+ end
+
+ def revisable_class
+ self
+ end
+
+ def revisable_columns
+ return @aa_revisable_columns unless @aa_revisable_columns.blank?
+ return @aa_revisable_columns ||= [] if self.revisable_options.except == :all
+ return @aa_revisable_columns ||= [self.revisable_options.only].flatten.map(&:to_s).map(&:downcase) unless self.revisable_options.only.blank?
+
+ except = [self.revisable_options.except].flatten || []
+ except += FatJam::REVISABLE_SYSTEM_COLUMNS
+ except += FatJam::REVISABLE_UNREVISABLE_COLUMNS
+ except.uniq!
+
+ @aa_revisable_columns ||= (column_names - except.map(&:to_s)).flatten.map(&:downcase)
+ end
+ end
+ end
+ end
+end
View
80 lib/acts_as_revisable/acts/revision.rb
@@ -0,0 +1,80 @@
+module FatJam
+ module ActsAsRevisable
+ module Revision
+ def self.included(base)
+ base.send(:extend, ClassMethods)
+
+ base.instance_eval do
+ set_table_name(revisable_class.table_name)
+ acts_as_scoped_model :find => {:conditions => {:revisable_is_current => false}}
+ revision_cloned_associations.each do |key|
+ assoc = revisable_class.reflect_on_association(key)
+ options = assoc.options.clone
+ options[:foreign_key] ||= "revisable_original_id"
+ send(assoc.macro, assoc.name, options)
+ end
+
+ define_callbacks :before_restore, :after_restore, :before_branch, :after_branch
+
+ belongs_to :current_revision, :class_name => revisable_class_name, :foreign_key => :revisable_original_id
+ belongs_to revisable_class_name.downcase.to_sym, :class_name => revisable_class_name, :foreign_key => :revisable_original_id
+
+ before_create :revision_setup
+ end
+ end
+
+ def revision_name=(val)
+ self[:revisable_name] = val
+ end
+
+ def revision_name
+ self[:revisable_name]
+ end
+
+ def revision_number
+ self[:revisable_number]
+ end
+
+ def revision_setup
+ now = Time.now
+ prev = current_revision.revisions.first
+ prev.update_attribute(:revisable_revised_at, now) if prev
+ self[:revisable_current_at] = now + 1.second
+ self[:revisable_is_current] = false
+ self[:revisable_branched_from_id] = current_revision[:revisable_branched_from_id]
+ self[:revisable_type] = current_revision[:type]
+ self[:revisable_number] = (self.class.maximum(:revisable_number, :conditions => {:revisable_original_id => self[:revisable_original_id]}) || 0) + 1
+ end
+
+ module ClassMethods
+ def revisable_class_name
+ self.revisable_options.revisable_class || self.class_name.gsub(/Revision/, '')
+ end
+
+ def revisable_class
+ @revisable_class ||= revisable_class_name.constantize
+ end
+
+ def revision_class
+ self
+ end
+
+ def revision_cloned_associations
+ clone_associations = self.revisable_options.clone_associations
+
+ @aa_revisable_cloned_associations ||= if clone_associations.blank?
+ []
+ elsif clone_associations.eql? :all
+ revisable_class.reflect_on_all_associations.map(&:name)
+ elsif clone_associations.is_a? [].class
+ clone_associations
+ elsif clone_associations[:only]
+ [clone_associations[:only]].flatten
+ elsif clone_associations[:except]
+ revisable_class.reflect_on_all_associations.map(&:name) - [clone_associations[:except]].flatten
+ end
+ end
+ end
+ end
+ end
+end
View
51 lib/acts_as_revisable/acts/scoped_model.rb
@@ -0,0 +1,51 @@
+module FatJam
+ module ActsAsScopedModel
+ def self.included(base)
+ base.send(:extend, ClassMethods)
+ end
+
+ module ClassMethods
+ SCOPED_METHODS = %w(construct_calculation_sql construct_finder_sql update_all delete_all destroy_all).freeze
+
+ def call_method_with_static_scope(meth, args)
+ return send(meth, *args) unless self.scoped_model_enabled
+
+ with_scope(self.scoped_model_static_scope) do
+ send(meth, *args)
+ end
+ end
+
+ SCOPED_METHODS.each do |m|
+ module_eval <<-EVAL
+ def #{m}_with_static_scope(*args)
+ call_method_with_static_scope(:#{m}_without_static_scope, args)
+ end
+ EVAL
+ end
+
+ def without_model_scope
+ return unless block_given?
+
+ begin
+ self.scoped_model_enabled = false
+ rv = yield
+ ensure
+ self.scoped_model_enabled = true
+ end
+
+ rv
+ end
+
+ def acts_as_scoped_model(*args)
+ class << self
+ attr_accessor :scoped_model_static_scope, :scoped_model_enabled
+ SCOPED_METHODS.each do |m|
+ alias_method_chain m.to_sym, :static_scope
+ end
+ end
+ self.scoped_model_enabled = true
+ self.scoped_model_static_scope = args.grep(Hash).first
+ end
+ end
+ end
+end
View
36 lib/acts_as_revisable/base.rb
@@ -0,0 +1,36 @@
+require 'acts_as_revisable/options'
+require 'acts_as_revisable/acts/common'
+require 'acts_as_revisable/acts/revision'
+require 'acts_as_revisable/acts/revisable'
+
+module FatJam
+ REVISABLE_SYSTEM_COLUMNS = %w(revisable_original_id revisable_branched_from_id revisable_number revisable_name revisable_type revisable_current_at revisable_revised_at revisable_deleted_at revisable_is_current)
+ REVISABLE_UNREVISABLE_COLUMNS = %w(id type)
+
+ module ActsAsRevisable
+ def self.included(base)
+ base.send(:extend, ClassMethods)
+ end
+
+ module ClassMethods
+ def acts_as_revisable(*args, &block)
+ revisable_shared_setup(args, block)
+ self.send(:include, FatJam::ActsAsRevisable::Revisable)
+ end
+
+ def acts_as_revision(*args, &block)
+ revisable_shared_setup(args, block)
+ self.send(:include, FatJam::ActsAsRevisable::Revision)
+ end
+
+ def revisable_shared_setup(args, block)
+ self.send(:include, FatJam::ActsAsRevisable::Common)
+ class<<self
+ attr_accessor :revisable_options
+ end
+ options = args.grep(Hash).first || {}
+ @revisable_options = FatJam::ActsAsRevisable::Options.new(options, &block)
+ end
+ end
+ end
+end
View
18 lib/acts_as_revisable/options.rb
@@ -0,0 +1,18 @@
+module FatJam
+ module ActsAsRevisable
+ class Options
+ def initialize(options, &block)
+ @options = options
+ instance_eval(&block) if block_given?
+ end
+
+ def method_missing(key, *args)
+ if args.blank?
+ @options[key.to_sym]
+ else
+ @options[key.to_sym] = args.size == 1 ? args.first : args
+ end
+ end
+ end
+ end
+end
View
30 lib/acts_as_revisable/quoted_columns.rb
@@ -0,0 +1,30 @@
+module FatJam::QuotedColumnConditions
+ def self.included(base)
+ base.send(:extend, ClassMethods)
+
+ class << base
+ alias_method_chain :quote_bound_value, :quoted_column
+ end
+ end
+
+ module ClassMethods
+ def quote_bound_value_with_quoted_column(value)
+ if value.is_a?(Symbol) && column_names.member?(value.to_s)
+ # code borrowed from sanitize_sql_hash_for_conditions
+ attr = value.to_s
+
+ # Extract table name from qualified attribute names.
+ if attr.include?('.')
+ table_name, attr = attr.split('.', 2)
+ table_name = connection.quote_table_name(table_name)
+ else
+ table_name = quoted_table_name
+ end
+
+ return "#{table_name}.#{connection.quote_column_name(attr)}"
+ end
+
+ quote_bound_value_without_quoted_column(value)
+ end
+ end
+end
View
14 lib/acts_as_revisable/version.rb
@@ -1,9 +1,11 @@
-module ActsAsRevisable #:nodoc:
- module VERSION #:nodoc:
- MAJOR = 0
- MINOR = 5
- TINY = 0
+module FatJam #:nodoc:
+ module ActsAsRevisable
+ module VERSION #:nodoc:
+ MAJOR = 0
+ MINOR = 5
+ TINY = 0
- STRING = [MAJOR, MINOR, TINY].join('.')
+ STRING = [MAJOR, MINOR, TINY].join('.')
+ end
end
end
View
1 rails/init.rb
@@ -0,0 +1 @@
+require 'acts_as_revisable'
View
12 tasks/deployment.rake
@@ -1,15 +1,3 @@
-desc 'Release the website and new gem version'
-task :deploy => [:check_version, :website, :release] do
- puts "Remember to create SVN tag:"
- puts "svn copy svn+ssh://#{rubyforge_username}@rubyforge.org/var/svn/#{PATH}/trunk " +
- "svn+ssh://#{rubyforge_username}@rubyforge.org/var/svn/#{PATH}/tags/REL-#{VERS} "
- puts "Suggested comment:"
- puts "Tagging release #{CHANGES}"
-end
-
-desc 'Runs tasks website_generate and install_gem as a local deployment of the gem'
-task :local_deploy => [:website_generate, :install_gem]
-
task :check_version do
unless ENV['VERSION']
puts 'Must pass a VERSION=x.y.z release version'

0 comments on commit 3299d0b

Please sign in to comment.