Migrate from vendor/plugins and vendor/gems to Gemfile

IHG: added vendor/gems/bluecloth/tests/data/antsugar.txt
master
Alinson S. Xavier 12 years ago
parent 88a02f9f15
commit 33cdee1c1c

@ -1,39 +1,23 @@
source 'https://rubygems.org'
gem 'rails', '3.2.13'
# Bundle edge Rails instead:
# gem 'rails', :git => 'git://github.com/rails/rails.git'
gem 'rails', '~>3.2'
gem 'sqlite3'
gem 'json'
# Gems used only for assets and not required
# in production environments by default.
group :assets do
gem 'sass-rails', '~> 3.2.3'
gem 'coffee-rails', '~> 3.2.1'
# See https://github.com/sstephenson/execjs#readme for more supported runtimes
# gem 'therubyracer', :platforms => :ruby
gem 'uglifier', '>= 1.0.3'
end
gem 'jquery-rails'
# To use ActiveModel has_secure_password
# gem 'bcrypt-ruby', '~> 3.0.0'
# To use Jbuilder templates for JSON
# gem 'jbuilder'
# Use unicorn as the app server
# gem 'unicorn'
# Deploy with Capistrano
# gem 'capistrano'
# To use debugger
# gem 'ruby-debug'
gem 'bluecloth', '1.0.0', :path => './vendor/gems/bluecloth'
gem 'haml', '~>3.1'
gem 'hpricot', '~>0.8'
gem 'icalendar', '~>1.0'
gem 'will_paginate', "~>3.0"
gem 'calendar_helper', "~>0.2"
gem 'shoulda', "~>2.11"
gem 'acts_as_paranoid', '~>0.4'
#gem 'acts_as_versioned', '~>0.6', :ignore_dependencies => true
gem 'acts_as_list', '~>0.2'

@ -1,3 +1,8 @@
PATH
remote: ./vendor/gems/bluecloth
specs:
bluecloth (1.0.0)
GEM
remote: https://rubygems.org/
specs:
@ -28,8 +33,14 @@ GEM
activesupport (3.2.13)
i18n (= 0.6.1)
multi_json (~> 1.0)
acts_as_list (0.2.0)
activerecord (>= 3.0)
acts_as_paranoid (0.4.2)
activerecord (~> 3.2)
arel (3.0.2)
builder (3.0.4)
calendar_helper (0.2.5)
open4
coffee-rails (3.2.2)
coffee-script (>= 2.2.0)
railties (~> 3.2.0)
@ -40,8 +51,11 @@ GEM
erubis (2.7.0)
execjs (1.4.0)
multi_json (~> 1.0)
haml (3.1.8)
hike (1.2.3)
hpricot (0.8.6)
i18n (0.6.1)
icalendar (1.4.1)
journey (1.0.4)
jquery-rails (3.0.4)
railties (>= 3.0, < 5.0)
@ -52,6 +66,7 @@ GEM
treetop (~> 1.4.8)
mime-types (1.23)
multi_json (1.7.7)
open4 (1.3.0)
polyglot (0.3.3)
rack (1.4.5)
rack-cache (1.2)
@ -83,6 +98,7 @@ GEM
railties (~> 3.2.0)
sass (>= 3.1.10)
tilt (~> 1.3)
shoulda (2.11.3)
sprockets (2.2.2)
hike (~> 1.2)
multi_json (~> 1.0)
@ -95,18 +111,28 @@ GEM
polyglot
polyglot (>= 0.3.1)
tzinfo (0.3.37)
uglifier (2.1.1)
uglifier (2.1.2)
execjs (>= 0.3.0)
multi_json (~> 1.0, >= 1.0.2)
will_paginate (3.0.4)
PLATFORMS
ruby
DEPENDENCIES
acts_as_list (~> 0.2)
acts_as_paranoid (~> 0.4)
bluecloth (= 1.0.0)!
calendar_helper (~> 0.2)
coffee-rails (~> 3.2.1)
haml (~> 3.1)
hpricot (~> 0.8)
icalendar (~> 1.0)
jquery-rails
json
rails (= 3.2.13)
rails (~> 3.2)
sass-rails (~> 3.2.3)
shoulda (~> 2.11)
sqlite3
uglifier (>= 1.0.3)
will_paginate (~> 3.0)

@ -1,23 +0,0 @@
ActsAsList
==========
This acts_as extension provides the capabilities for sorting and reordering a number of objects in a list. The class that has this specified needs to have a +position+ column defined as an integer on the mapped database table.
Example
=======
class TodoList < ActiveRecord::Base
has_many :todo_items, :order => "position"
end
class TodoItem < ActiveRecord::Base
belongs_to :todo_list
acts_as_list :scope => :todo_list
end
todo_list.first.move_to_bottom
todo_list.last.move_higher
Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license

@ -1,3 +0,0 @@
$:.unshift "#{File.dirname(__FILE__)}/lib"
require 'active_record/acts/list'
ActiveRecord::Base.class_eval { include ActiveRecord::Acts::List }

@ -1,256 +0,0 @@
module ActiveRecord
module Acts #:nodoc:
module List #:nodoc:
def self.included(base)
base.extend(ClassMethods)
end
# This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
# The class that has this specified needs to have a +position+ column defined as an integer on
# the mapped database table.
#
# Todo list example:
#
# class TodoList < ActiveRecord::Base
# has_many :todo_items, :order => "position"
# end
#
# class TodoItem < ActiveRecord::Base
# belongs_to :todo_list
# acts_as_list :scope => :todo_list
# end
#
# todo_list.first.move_to_bottom
# todo_list.last.move_higher
module ClassMethods
# Configuration options are:
#
# * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
# * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
# (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
# to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
# Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
def acts_as_list(options = {})
configuration = { :column => "position", :scope => "1 = 1" }
configuration.update(options) if options.is_a?(Hash)
configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
if configuration[:scope].is_a?(Symbol)
scope_condition_method = %(
def scope_condition
if #{configuration[:scope].to_s}.nil?
"#{configuration[:scope].to_s} IS NULL"
else
"#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
end
end
)
else
scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
end
class_eval <<-EOV
include ActiveRecord::Acts::List::InstanceMethods
def acts_as_list_class
::#{self.name}
end
def position_column
'#{configuration[:column]}'
end
#{scope_condition_method}
before_destroy :remove_from_list
before_create :add_to_list_bottom
EOV
end
end
# All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
# by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
# lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
# the first in the list of all chapters.
module InstanceMethods
# Insert the item at the given position (defaults to the top position of 1).
def insert_at(position = 1)
insert_at_position(position)
end
# Swap positions with the next lower item, if one exists.
def move_lower
return unless lower_item
acts_as_list_class.transaction do
lower_item.decrement_position
increment_position
end
end
# Swap positions with the next higher item, if one exists.
def move_higher
return unless higher_item
acts_as_list_class.transaction do
higher_item.increment_position
decrement_position
end
end
# Move to the bottom of the list. If the item is already in the list, the items below it have their
# position adjusted accordingly.
def move_to_bottom
return unless in_list?
acts_as_list_class.transaction do
decrement_positions_on_lower_items
assume_bottom_position
end
end
# Move to the top of the list. If the item is already in the list, the items above it have their
# position adjusted accordingly.
def move_to_top
return unless in_list?
acts_as_list_class.transaction do
increment_positions_on_higher_items
assume_top_position
end
end
# Removes the item from the list.
def remove_from_list
if in_list?
decrement_positions_on_lower_items
update_attribute position_column, nil
end
end
# Increase the position of this item without adjusting the rest of the list.
def increment_position
return unless in_list?
update_attribute position_column, self.send(position_column).to_i + 1
end
# Decrease the position of this item without adjusting the rest of the list.
def decrement_position
return unless in_list?
update_attribute position_column, self.send(position_column).to_i - 1
end
# Return +true+ if this object is the first in the list.
def first?
return false unless in_list?
self.send(position_column) == 1
end
# Return +true+ if this object is the last in the list.
def last?
return false unless in_list?
self.send(position_column) == bottom_position_in_list
end
# Return the next higher item in the list.
def higher_item
return nil unless in_list?
acts_as_list_class.find(:first, :conditions =>
"#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
)
end
# Return the next lower item in the list.
def lower_item
return nil unless in_list?
acts_as_list_class.find(:first, :conditions =>
"#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
)
end
# Test if this record is in a list
def in_list?
!send(position_column).nil?
end
private
def add_to_list_top
increment_positions_on_all_items
end
def add_to_list_bottom
self[position_column] = bottom_position_in_list.to_i + 1
end
# Overwrite this method to define the scope of the list changes
def scope_condition() "1" end
# Returns the bottom position number in the list.
# bottom_position_in_list # => 2
def bottom_position_in_list(except = nil)
item = bottom_item(except)
item ? item.send(position_column) : 0
end
# Returns the bottom item
def bottom_item(except = nil)
conditions = scope_condition
conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
end
# Forces item to assume the bottom position in the list.
def assume_bottom_position
update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
end
# Forces item to assume the top position in the list.
def assume_top_position
update_attribute(position_column, 1)
end
# This has the effect of moving all the higher items up one.
def decrement_positions_on_higher_items(position)
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
)
end
# This has the effect of moving all the lower items up one.
def decrement_positions_on_lower_items
return unless in_list?
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
)
end
# This has the effect of moving all the higher items down one.
def increment_positions_on_higher_items
return unless in_list?
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
)
end
# This has the effect of moving all the lower items down one.
def increment_positions_on_lower_items(position)
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
)
end
# Increments position (<tt>position_column</tt>) of all items in the list.
def increment_positions_on_all_items
acts_as_list_class.update_all(
"#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
)
end
def insert_at_position(position)
remove_from_list
increment_positions_on_lower_items(position)
self.update_attribute(position_column, position)
end
end
end
end
end

@ -1,332 +0,0 @@
require 'test/unit'
require 'rubygems'
gem 'activerecord', '>= 1.15.4.7794'
require 'active_record'
require "#{File.dirname(__FILE__)}/../init"
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
def setup_db
ActiveRecord::Schema.define(:version => 1) do
create_table :mixins do |t|
t.column :pos, :integer
t.column :parent_id, :integer
t.column :created_at, :datetime
t.column :updated_at, :datetime
end
end
end
def teardown_db
ActiveRecord::Base.connection.tables.each do |table|
ActiveRecord::Base.connection.drop_table(table)
end
end
class Mixin < ActiveRecord::Base
end
class ListMixin < Mixin
acts_as_list :column => "pos", :scope => :parent
def self.table_name() "mixins" end
end
class ListMixinSub1 < ListMixin
end
class ListMixinSub2 < ListMixin
end
class ListWithStringScopeMixin < ActiveRecord::Base
acts_as_list :column => "pos", :scope => 'parent_id = #{parent_id}'
def self.table_name() "mixins" end
end
class ListTest < Test::Unit::TestCase
def setup
setup_db
(1..4).each { |counter| ListMixin.create! :pos => counter, :parent_id => 5 }
end
def teardown
teardown_db
end
def test_reordering
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(2).move_lower
assert_equal [1, 3, 2, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(2).move_higher
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(1).move_to_bottom
assert_equal [2, 3, 4, 1], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(1).move_to_top
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(2).move_to_bottom
assert_equal [1, 3, 4, 2], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(4).move_to_top
assert_equal [4, 1, 3, 2], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
end
def test_move_to_bottom_with_next_to_last_item
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(3).move_to_bottom
assert_equal [1, 2, 4, 3], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
end
def test_next_prev
assert_equal ListMixin.find(2), ListMixin.find(1).lower_item
assert_nil ListMixin.find(1).higher_item
assert_equal ListMixin.find(3), ListMixin.find(4).higher_item
assert_nil ListMixin.find(4).lower_item
end
def test_injection
item = ListMixin.new(:parent_id => 1)
assert_equal "parent_id = 1", item.scope_condition
assert_equal "pos", item.position_column
end
def test_insert
new = ListMixin.create(:parent_id => 20)
assert_equal 1, new.pos
assert new.first?
assert new.last?
new = ListMixin.create(:parent_id => 20)
assert_equal 2, new.pos
assert !new.first?
assert new.last?
new = ListMixin.create(:parent_id => 20)
assert_equal 3, new.pos
assert !new.first?
assert new.last?
new = ListMixin.create(:parent_id => 0)
assert_equal 1, new.pos
assert new.first?
assert new.last?
end
def test_insert_at
new = ListMixin.create(:parent_id => 20)
assert_equal 1, new.pos
new = ListMixin.create(:parent_id => 20)
assert_equal 2, new.pos
new = ListMixin.create(:parent_id => 20)
assert_equal 3, new.pos
new4 = ListMixin.create(:parent_id => 20)
assert_equal 4, new4.pos
new4.insert_at(3)
assert_equal 3, new4.pos
new.reload
assert_equal 4, new.pos
new.insert_at(2)
assert_equal 2, new.pos
new4.reload
assert_equal 4, new4.pos
new5 = ListMixin.create(:parent_id => 20)
assert_equal 5, new5.pos
new5.insert_at(1)
assert_equal 1, new5.pos
new4.reload
assert_equal 5, new4.pos
end
def test_delete_middle
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(2).destroy
assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
assert_equal 1, ListMixin.find(1).pos
assert_equal 2, ListMixin.find(3).pos
assert_equal 3, ListMixin.find(4).pos
ListMixin.find(1).destroy
assert_equal [3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
assert_equal 1, ListMixin.find(3).pos
assert_equal 2, ListMixin.find(4).pos
end
def test_with_string_based_scope
new = ListWithStringScopeMixin.create(:parent_id => 500)
assert_equal 1, new.pos
assert new.first?
assert new.last?
end
def test_nil_scope
new1, new2, new3 = ListMixin.create, ListMixin.create, ListMixin.create
new2.move_higher
assert_equal [new2, new1, new3], ListMixin.find(:all, :conditions => 'parent_id IS NULL', :order => 'pos')
end
def test_remove_from_list_should_then_fail_in_list?
assert_equal true, ListMixin.find(1).in_list?
ListMixin.find(1).remove_from_list
assert_equal false, ListMixin.find(1).in_list?
end
def test_remove_from_list_should_set_position_to_nil
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(2).remove_from_list
assert_equal [2, 1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
assert_equal 1, ListMixin.find(1).pos
assert_equal nil, ListMixin.find(2).pos
assert_equal 2, ListMixin.find(3).pos
assert_equal 3, ListMixin.find(4).pos
end
def test_remove_before_destroy_does_not_shift_lower_items_twice
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
ListMixin.find(2).remove_from_list
ListMixin.find(2).destroy
assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5', :order => 'pos').map(&:id)
assert_equal 1, ListMixin.find(1).pos
assert_equal 2, ListMixin.find(3).pos
assert_equal 3, ListMixin.find(4).pos
end
end
class ListSubTest < Test::Unit::TestCase
def setup
setup_db
(1..4).each { |i| ((i % 2 == 1) ? ListMixinSub1 : ListMixinSub2).create! :pos => i, :parent_id => 5000 }
end
def teardown
teardown_db
end
def test_reordering
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(2).move_lower
assert_equal [1, 3, 2, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(2).move_higher
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(1).move_to_bottom
assert_equal [2, 3, 4, 1], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(1).move_to_top
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(2).move_to_bottom
assert_equal [1, 3, 4, 2], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(4).move_to_top
assert_equal [4, 1, 3, 2], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
end
def test_move_to_bottom_with_next_to_last_item
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(3).move_to_bottom
assert_equal [1, 2, 4, 3], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
end
def test_next_prev
assert_equal ListMixin.find(2), ListMixin.find(1).lower_item
assert_nil ListMixin.find(1).higher_item
assert_equal ListMixin.find(3), ListMixin.find(4).higher_item
assert_nil ListMixin.find(4).lower_item
end
def test_injection
item = ListMixin.new("parent_id"=>1)
assert_equal "parent_id = 1", item.scope_condition
assert_equal "pos", item.position_column
end
def test_insert_at
new = ListMixin.create("parent_id" => 20)
assert_equal 1, new.pos
new = ListMixinSub1.create("parent_id" => 20)
assert_equal 2, new.pos
new = ListMixinSub2.create("parent_id" => 20)
assert_equal 3, new.pos
new4 = ListMixin.create("parent_id" => 20)
assert_equal 4, new4.pos
new4.insert_at(3)
assert_equal 3, new4.pos
new.reload
assert_equal 4, new.pos
new.insert_at(2)
assert_equal 2, new.pos
new4.reload
assert_equal 4, new4.pos
new5 = ListMixinSub1.create("parent_id" => 20)
assert_equal 5, new5.pos
new5.insert_at(1)
assert_equal 1, new5.pos
new4.reload
assert_equal 5, new4.pos
end
def test_delete_middle
assert_equal [1, 2, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
ListMixin.find(2).destroy
assert_equal [1, 3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
assert_equal 1, ListMixin.find(1).pos
assert_equal 2, ListMixin.find(3).pos
assert_equal 3, ListMixin.find(4).pos
ListMixin.find(1).destroy
assert_equal [3, 4], ListMixin.find(:all, :conditions => 'parent_id = 5000', :order => 'pos').map(&:id)
assert_equal 1, ListMixin.find(3).pos
assert_equal 2, ListMixin.find(4).pos
end
end

@ -1,79 +0,0 @@
* (16 Apr 2009)
Allow :with_deleted and :only_deleted options to work with count and calculate.
Fixes compatibility with will_paginate. [James Le Cuirot]
* (4 Oct 2007)
Update for Edge rails: remove support for legacy #count args
* (2 Feb 2007)
Add support for custom primary keys [Jeff Dean]
* (2 July 2006)
Add paranoid delete_all implementation [Marshall Roch]
* (23 May 2006)
Allow setting of future dates for content expiration.
* (15 May 2006)
Added support for dynamic finders
* (28 Mar 2006)
Updated for Rails 1.1. I love removing code.
Refactored #find method
Nested Scopes
*0.3.1* (20 Dec 2005)
* took out deleted association code for 'chainsaw butchery of base classes' [sorry Erik Terpstra]
* verified tests pass on Rails 1.0
*0.3* (27 Nov 2005)
* Deleted models will find deleted associations by default now [Erik Terpstra]
* Added :group as valid option for find [Michael Dabney]
* Changed the module namespace to Caboose::Acts::Paranoid
*0.2.0* (6 Nov 2005)
* Upgrade to Rails 1.0 RC4. ActiveRecord::Base#constrain has been replaced with scope_with.
*0.1.7* (22 Oct 2005)
* Added :with_deleted as a valid option of ActiveRecord::Base#find
*0.1.6* (25 Sep 2005)
* Fixed bug where nested constrains would get clobbered after multiple queries
*0.1.5* (22 Sep 2005)
* Fixed bug where acts_as_paranoid would clobber other constrains
* Simplified acts_as_paranoid mixin including.
*0.1.4* (18 Sep 2005)
* First RubyForge release
*0.1.3* (18 Sep 2005)
* ignore multiple calls to acts_as_paranoid on the same model
*0.1.2* (18 Sep 2005)
* fixed a bug that kept you from selecting the first deleted record
*0.1.1* (18 Sep 2005)
* Fixed bug that kept you from selecting deleted records by ID
*0.1* (17 Sep 2005)
* Initial gem

@ -1,20 +0,0 @@
Copyright (c) 2005 Rick Olson
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.

@ -1,5 +0,0 @@
= acts_as_paranoid
Overrides some basic methods for the current model so that calling #destroy sets a 'deleted_at' field to the current timestamp. ActiveRecord is required.
http://github.com/technoweenie/acts_as_paranoid

@ -1,10 +0,0 @@
1. Pick Rails version. Either dump this plugin in a Rails app and run it from there, or specify it as an ENV var:
RAILS=2.2.2 rake
RAILS=2.2.2 ruby test/paranoid_test.rb
2. Setup your database. By default sqlite3 is used, and no further setup is necessary. You can pick any of the listed databases in test/database.yml. Be sure to create the database first.
DB=mysql rake
3. Profit!!

@ -1,180 +0,0 @@
require 'rubygems'
Gem::manage_gems
require 'rake/rdoctask'
require 'rake/packagetask'
require 'rake/gempackagetask'
require 'rake/testtask'
require 'rake/contrib/rubyforgepublisher'
PKG_NAME = 'acts_as_paranoid'
PKG_VERSION = '0.3.1'
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
PROD_HOST = "technoweenie@bidwell.textdrive.com"
RUBY_FORGE_PROJECT = 'ar-paranoid'
RUBY_FORGE_USER = 'technoweenie'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the calculations plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the acts_as_paranoid plugin.'
Rake::RDocTask.new do |rdoc|
rdoc.rdoc_dir = 'html'
rdoc.title = "#{PKG_NAME} -- protect your ActiveRecord objects from accidental deletion"
rdoc.options << '--line-numbers --inline-source --accessor cattr_accessor=object'
rdoc.template = "#{ENV['template']}.rb" if ENV['template']
rdoc.rdoc_files.include('README', 'CHANGELOG', 'RUNNING_UNIT_TESTS')
rdoc.rdoc_files.include('lib/**/*.rb')
end
spec = Gem::Specification.new do |s|
s.name = PKG_NAME
s.version = PKG_VERSION
s.platform = Gem::Platform::RUBY
s.summary = "acts_as_paranoid keeps models from actually being deleted by setting a deleted_at field."
s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG RUNNING_UNIT_TESTS)
s.files.delete "acts_as_paranoid_plugin.sqlite.db"
s.files.delete "acts_as_paranoid_plugin.sqlite3.db"
s.require_path = 'lib'
s.autorequire = 'acts_as_paranoid'
s.has_rdoc = true
s.test_files = Dir['test/**/*_test.rb']
s.author = "Rick Olson"
s.email = "technoweenie@gmail.com"
s.homepage = "http://techno-weenie.net"
end
Rake::GemPackageTask.new(spec) do |pkg|
pkg.need_tar = true
end
desc "Publish the API documentation"
task :pdoc => [:rdoc] do
Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload
end
desc 'Publish the gem and API docs'
task :publish => [:pdoc, :rubyforge_upload]
desc "Publish the release files to RubyForge."
task :rubyforge_upload => :package do
files = %w(gem tgz).map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" }
if RUBY_FORGE_PROJECT then
require 'net/http'
require 'open-uri'
project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/"
project_data = open(project_uri) { |data| data.read }
group_id = project_data[/[?&]group_id=(\d+)/, 1]
raise "Couldn't get group id" unless group_id
# This echos password to shell which is a bit sucky
if ENV["RUBY_FORGE_PASSWORD"]
password = ENV["RUBY_FORGE_PASSWORD"]
else
print "#{RUBY_FORGE_USER}@rubyforge.org's password: "
password = STDIN.gets.chomp
end
login_response = Net::HTTP.start("rubyforge.org", 80) do |http|
data = [
"login=1",
"form_loginname=#{RUBY_FORGE_USER}",
"form_pw=#{password}"
].join("&")
http.post("/account/login.php", data)
end
cookie = login_response["set-cookie"]
raise "Login failed" unless cookie
headers = { "Cookie" => cookie }
release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}"
release_data = open(release_uri, headers) { |data| data.read }
package_id = release_data[/[?&]package_id=(\d+)/, 1]
raise "Couldn't get package id" unless package_id
first_file = true
release_id = ""
files.each do |filename|
basename = File.basename(filename)
file_ext = File.extname(filename)
file_data = File.open(filename, "rb") { |file| file.read }
puts "Releasing #{basename}..."
release_response = Net::HTTP.start("rubyforge.org", 80) do |http|
release_date = Time.now.strftime("%Y-%m-%d %H:%M")
type_map = {
".zip" => "3000",
".tgz" => "3110",
".gz" => "3110",
".gem" => "1400"
}; type_map.default = "9999"
type = type_map[file_ext]
boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor"
query_hash = if first_file then
{
"group_id" => group_id,
"package_id" => package_id,
"release_name" => PKG_FILE_NAME,
"release_date" => release_date,
"type_id" => type,
"processor_id" => "8000", # Any
"release_notes" => "",
"release_changes" => "",
"preformatted" => "1",
"submit" => "1"
}
else
{
"group_id" => group_id,
"release_id" => release_id,
"package_id" => package_id,
"step2" => "1",
"type_id" => type,
"processor_id" => "8000", # Any
"submit" => "Add This File"
}
end
query = "?" + query_hash.map do |(name, value)|
[name, URI.encode(value)].join("=")
end.join("&")
data = [
"--" + boundary,
"Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"",
"Content-Type: application/octet-stream",
"Content-Transfer-Encoding: binary",
"", file_data, ""
].join("\x0D\x0A")
release_headers = headers.merge(
"Content-Type" => "multipart/form-data; boundary=#{boundary}"
)
target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php"
http.post(target + query, data, release_headers)
end
if first_file then
release_id = release_response.body[/release_id=(\d+)/, 1]
raise("Couldn't get release id") unless release_id
end
first_file = false
end
end
end

@ -1,34 +0,0 @@
class << ActiveRecord::Base
def belongs_to_with_deleted(association_id, options = {})
with_deleted = options.delete :with_deleted
belongs_to_without_deleted(association_id, options).tap do
if with_deleted
reflection = reflect_on_association(association_id)
association_accessor_methods(reflection, Caboose::Acts::BelongsToWithDeletedAssociation)
association_constructor_method(:build, reflection, Caboose::Acts::BelongsToWithDeletedAssociation)
association_constructor_method(:create, reflection, Caboose::Acts::BelongsToWithDeletedAssociation)
end
end
end
def has_many_without_deleted(association_id, options = {}, &extension)
with_deleted = options.delete :with_deleted
has_many_with_deleted(association_id, options, &extension).tap do
if options[:through] && !with_deleted
reflection = reflect_on_association(association_id)
collection_reader_method(reflection, Caboose::Acts::HasManyThroughWithoutDeletedAssociation)
collection_accessor_methods(reflection, Caboose::Acts::HasManyThroughWithoutDeletedAssociation, false)
end
end
end
alias_method_chain :belongs_to, :deleted
alias_method :has_many_with_deleted, :has_many
alias_method :has_many, :has_many_without_deleted
alias_method :exists_with_deleted?, :exists?
end
ActiveRecord::Base.send :include, Caboose::Acts::Paranoid
ActiveRecord::Base.send :include, Caboose::Acts::ParanoidFindWrapper
class << ActiveRecord::Base
alias_method_chain :acts_as_paranoid, :find_wrapper
end

@ -1,14 +0,0 @@
module Caboose # :nodoc:
module Acts # :nodoc:
class BelongsToWithDeletedAssociation < ActiveRecord::Associations::BelongsToAssociation
private
def find_target
@reflection.klass.find_with_deleted(
@owner[@reflection.primary_key_name],
:conditions => conditions,
:include => @reflection.options[:include]
)
end
end
end
end

@ -1,27 +0,0 @@
module Caboose # :nodoc:
module Acts # :nodoc:
class HasManyThroughWithoutDeletedAssociation < ActiveRecord::Associations::HasManyThroughAssociation
protected
def current_time
ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
end
def construct_conditions
return super unless @reflection.through_reflection.klass.paranoid?
table_name = @reflection.through_reflection.table_name
conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value|
"#{table_name}.#{attr} = #{value}"
end
deleted_attribute = @reflection.through_reflection.klass.deleted_attribute
quoted_current_time = @reflection.through_reflection.klass.quote_value(
current_time,
@reflection.through_reflection.klass.columns_hash[deleted_attribute.to_s])
conditions << "#{table_name}.#{deleted_attribute} IS NULL OR #{table_name}.#{deleted_attribute} > #{quoted_current_time}"
conditions << sql_conditions if sql_conditions
"(" + conditions.join(') AND (') + ")"
end
end
end
end

@ -1,208 +0,0 @@
module Caboose #:nodoc:
module Acts #:nodoc:
# Overrides some basic methods for the current model so that calling #destroy sets a 'deleted_at' field to the current timestamp.
# This assumes the table has a deleted_at date/time field. Most normal model operations will work, but there will be some oddities.
#
# class Widget < ActiveRecord::Base
# acts_as_paranoid
# end
#
# Widget.find(:all)
# # SELECT * FROM widgets WHERE widgets.deleted_at IS NULL
#
# Widget.find(:first, :conditions => ['title = ?', 'test'], :order => 'title')
# # SELECT * FROM widgets WHERE widgets.deleted_at IS NULL AND title = 'test' ORDER BY title LIMIT 1
#
# Widget.find_with_deleted(:all)
# # SELECT * FROM widgets
#
# Widget.find_only_deleted(:all)
# # SELECT * FROM widgets WHERE widgets.deleted_at IS NOT NULL
#
# Widget.find_with_deleted(1).deleted?
# # Returns true if the record was previously destroyed, false if not
#
# Widget.count
# # SELECT COUNT(*) FROM widgets WHERE widgets.deleted_at IS NULL
#
# Widget.count ['title = ?', 'test']
# # SELECT COUNT(*) FROM widgets WHERE widgets.deleted_at IS NULL AND title = 'test'
#
# Widget.count_with_deleted
# # SELECT COUNT(*) FROM widgets
#
# Widget.count_only_deleted
# # SELECT COUNT(*) FROM widgets WHERE widgets.deleted_at IS NOT NULL
#
# Widget.delete_all
# # UPDATE widgets SET deleted_at = '2005-09-17 17:46:36'
#
# Widget.delete_all!
# # DELETE FROM widgets
#
# @widget.destroy
# # UPDATE widgets SET deleted_at = '2005-09-17 17:46:36' WHERE id = 1
#
# @widget.destroy!
# # DELETE FROM widgets WHERE id = 1
#
module Paranoid
def self.included(base) # :nodoc:
base.extend ClassMethods
end
module ClassMethods
def acts_as_paranoid(options = {})
unless paranoid? # don't let AR call this twice
cattr_accessor :deleted_attribute
self.deleted_attribute = options[:with] || :deleted_at
alias_method :destroy_without_callbacks!, :destroy_without_callbacks
class << self
alias_method :find_every_with_deleted, :find_every
alias_method :calculate_with_deleted, :calculate
alias_method :delete_all!, :delete_all
end
end
include InstanceMethods
end
def paranoid?
self.included_modules.include?(InstanceMethods)
end
end
module InstanceMethods #:nodoc:
def self.included(base) # :nodoc:
base.extend ClassMethods
end
module ClassMethods
def find_with_deleted(*args)
options = args.extract_options!
validate_find_options(options)
set_readonly_option!(options)
options[:with_deleted] = true # yuck!
case args.first
when :first then find_initial(options)
when :all then find_every(options)
else find_from_ids(args, options)
end
end
def find_only_deleted(*args)
options = args.extract_options!
validate_find_options(options)
set_readonly_option!(options)
options[:only_deleted] = true # yuck!
case args.first
when :first then find_initial(options)
when :all then find_every(options)
else find_from_ids(args, options)
end
end
def exists?(*args)
with_deleted_scope { exists_with_deleted?(*args) }
end
def exists_only_deleted?(*args)
with_only_deleted_scope { exists_with_deleted?(*args) }
end
def count_with_deleted(*args)
calculate_with_deleted(:count, *construct_count_options_from_args(*args))
end
def count_only_deleted(*args)
with_only_deleted_scope { count_with_deleted(*args) }
end
def count(*args)
with, only = extract_deleted_options(args.last) if args.last.is_a?(Hash)
with ? count_with_deleted(*args) :
only ? count_only_deleted(*args) :
with_deleted_scope { count_with_deleted(*args) }
end
def calculate(*args)
with, only = extract_deleted_options(args.last) if args.last.is_a?(Hash)
with ? calculate_with_deleted(*args) :
only ? calculate_only_deleted(*args) :
with_deleted_scope { calculate_with_deleted(*args) }
end
def delete_all(conditions = nil)
self.update_all ["#{self.deleted_attribute} = ?", current_time], conditions
end
protected
def current_time
default_timezone == :utc ? Time.now.utc : Time.now
end
def with_deleted_scope(&block)
with_scope({:find => { :conditions => ["#{table_name}.#{deleted_attribute} IS NULL OR #{table_name}.#{deleted_attribute} > ?", current_time] } }, :merge, &block)
end
def with_only_deleted_scope(&block)
with_scope({:find => { :conditions => ["#{table_name}.#{deleted_attribute} IS NOT NULL AND #{table_name}.#{deleted_attribute} <= ?", current_time] } }, :merge, &block)
end
private
# all find calls lead here
def find_every(options)
with, only = extract_deleted_options(options)
with ? find_every_with_deleted(options) :
only ? with_only_deleted_scope { find_every_with_deleted(options) } :
with_deleted_scope { find_every_with_deleted(options) }
end
def extract_deleted_options(options)
return options.delete(:with_deleted), options.delete(:only_deleted)
end
end
def destroy_without_callbacks
unless new_record?
self.class.update_all self.class.send(:sanitize_sql, ["#{self.class.deleted_attribute} = ?", (self.deleted_at = self.class.send(:current_time))]), ["#{self.class.primary_key} = ?", id]
end
freeze
end
def destroy_with_callbacks!
return false if callback(:before_destroy) == false
result = destroy_without_callbacks!
callback(:after_destroy)
result
end
def destroy!
transaction { destroy_with_callbacks! }
end
def deleted?
!!read_attribute(:deleted_at)
end
def recover!
self.deleted_at = nil
save!
end
def recover_with_associations!(*associations)
self.recover!
associations.to_a.each do |assoc|
self.send(assoc).find_with_deleted(:all).each do |a|
a.recover! if a.class.paranoid?
end
end
end
end
end
end
end

@ -1,94 +0,0 @@
module Caboose #:nodoc:
module Acts #:nodoc:
# Adds a wrapper find method which can identify :with_deleted or :only_deleted options
# and would call the corresponding acts_as_paranoid finders find_with_deleted or
# find_only_deleted methods.
#
# With this wrapper you can easily change from using this pattern:
#
# if some_condition_enabling_access_to_deleted_records?
# @post = Post.find_with_deleted(params[:id])
# else
# @post = Post.find(params[:id])
# end
#
# to this:
#
# @post = Post.find(params[:id], :with_deleted => some_condition_enabling_access_to_deleted_records?)
#
# Examples
#
# class Widget < ActiveRecord::Base
# acts_as_paranoid
# end
#
# Widget.find(:all)
# # SELECT * FROM widgets WHERE widgets.deleted_at IS NULL
#
# Widget.find(:all, :with_deleted => false)
# # SELECT * FROM widgets WHERE widgets.deleted_at IS NULL
#
# Widget.find_with_deleted(:all)
# # SELECT * FROM widgets
#
# Widget.find(:all, :with_deleted => true)
# # SELECT * FROM widgets
#
# Widget.find_only_deleted(:all)
# # SELECT * FROM widgets WHERE widgets.deleted_at IS NOT NULL
#
# Widget.find(:all, :only_deleted => true)
# # SELECT * FROM widgets WHERE widgets.deleted_at IS NOT NULL
#
# Widget.find(:all, :only_deleted => false)
# # SELECT * FROM widgets WHERE widgets.deleted_at IS NULL
#
module ParanoidFindWrapper
def self.included(base) # :nodoc:
base.extend ClassMethods
end
module ClassMethods
def acts_as_paranoid_with_find_wrapper(options = {})
unless paranoid? # don't let AR call this twice
acts_as_paranoid_without_find_wrapper(options)
class << self
alias_method :find_without_find_wrapper, :find
alias_method :validate_find_options_without_find_wrapper, :validate_find_options
end
end
include InstanceMethods
end
end
module InstanceMethods #:nodoc:
def self.included(base) # :nodoc:
base.extend ClassMethods
end
module ClassMethods
# This is a wrapper for the regular "find" so you can pass acts_as_paranoid related
# options and determine which finder to call.
def find(*args)
options = args.extract_options!
# Determine who to call.
finder_option = VALID_PARANOID_FIND_OPTIONS.detect { |key| options.delete(key) } || :without_find_wrapper
finder_method = "find_#{finder_option}".to_sym
# Put back the options in the args now that they don't include the extended keys.
args << options
send(finder_method, *args)
end
protected
VALID_PARANOID_FIND_OPTIONS = [:with_deleted, :only_deleted]
def validate_find_options(options) #:nodoc:
cleaned_options = options.reject { |k, v| VALID_PARANOID_FIND_OPTIONS.include?(k) }
validate_find_options_without_find_wrapper(cleaned_options)
end
end
end
end
end
end

@ -1,18 +0,0 @@
sqlite:
:adapter: sqlite
:dbfile: acts_as_paranoid_plugin.sqlite.db
sqlite3:
:adapter: sqlite3
:dbfile: acts_as_paranoid_plugin.sqlite3.db
postgresql:
:adapter: postgresql
:username: postgres
:password: postgres
:database: acts_as_paranoid_plugin_test
:min_messages: ERROR
mysql:
:adapter: mysql
:host: localhost
:username: rails
:password:
:database: acts_as_paranoid_plugin_test

@ -1,19 +0,0 @@
category_1:
id: 1
widget_id: 1
title: 'category 1'
category_2:
id: 2
widget_id: 1
title: 'category 2'
deleted_at: '2005-01-01 00:00:00'
category_3:
id: 3
widget_id: 2
title: 'category 3'
deleted_at: '2005-01-01 00:00:00'
category_4:
id: 4
widget_id: 2
title: 'category 4'
deleted_at: '2005-01-01 00:00:00'

@ -1,12 +0,0 @@
cw_1:
category_id: 1
widget_id: 1
cw_2:
category_id: 2
widget_id: 1
cw_3:
category_id: 3
widget_id: 2
cw_4:
category_id: 4
widget_id: 2

@ -1,9 +0,0 @@
tagging_1:
id: 1
tag_id: 1
widget_id: 1
deleted_at: '2005-01-01 00:00:00'
tagging_2:
id: 2
tag_id: 2
widget_id: 1

@ -1,6 +0,0 @@
tag_1:
id: 1
name: 'tag 1'
tag_2:
id: 2
name: 'tag 1'

@ -1,8 +0,0 @@
widget_1:
id: 1
title: 'widget 1'
widget_2:
id: 2
title: 'deleted widget 2'
deleted_at: '2005-01-01 00:00:00'
category_id: 3

@ -1,287 +0,0 @@
require File.join(File.dirname(__FILE__), 'test_helper')
class Widget < ActiveRecord::Base
acts_as_paranoid
has_many :categories, :dependent => :destroy
has_and_belongs_to_many :habtm_categories, :class_name => 'Category'
has_one :category
belongs_to :parent_category, :class_name => 'Category'
has_many :taggings
has_many :tags, :through => :taggings
has_many :any_tags, :through => :taggings, :class_name => 'Tag', :source => :tag, :with_deleted => true
end
class Category < ActiveRecord::Base
belongs_to :widget
belongs_to :any_widget, :class_name => 'Widget', :foreign_key => 'widget_id', :with_deleted => true
acts_as_paranoid
def self.search(name, options = {})
find :all, options.merge(:conditions => ['LOWER(title) LIKE ?', "%#{name.to_s.downcase}%"])
end
def self.search_with_deleted(name, options = {})
find_with_deleted :all, options.merge(:conditions => ['LOWER(title) LIKE ?', "%#{name.to_s.downcase}%"])
end
end
class Tag < ActiveRecord::Base
has_many :taggings
has_many :widgets, :through => :taggings
end
class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :widget
acts_as_paranoid
end
class NonParanoidAndroid < ActiveRecord::Base
end
class ParanoidTest < Test::Unit::TestCase
fixtures :widgets, :categories, :categories_widgets, :tags, :taggings
def test_should_recognize_with_deleted_option
assert_equal [1, 2], Widget.find(:all, :with_deleted => true).collect { |w| w.id }
assert_equal [1], Widget.find(:all, :with_deleted => false).collect { |w| w.id }
end
def test_should_recognize_only_deleted_option
assert_equal [2], Widget.find(:all, :only_deleted => true).collect { |w| w.id }
assert_equal [1], Widget.find(:all, :only_deleted => false).collect { |w| w.id }
end
def test_should_exists_with_deleted
assert Widget.exists_with_deleted?(2)
assert !Widget.exists?(2)
end
def test_should_exists_only_deleted
assert Widget.exists_only_deleted?(2)
assert !Widget.exists_only_deleted?(1)
end
def test_should_count_with_deleted
assert_equal 1, Widget.count
assert_equal 2, Widget.count_with_deleted
assert_equal 1, Widget.count_only_deleted
assert_equal 2, Widget.calculate_with_deleted(:count, :all)
end
def test_should_set_deleted_at
assert_equal 1, Widget.count
assert_equal 1, Category.count
widgets(:widget_1).destroy
assert_equal 0, Widget.count
assert_equal 0, Category.count
assert_equal 2, Widget.calculate_with_deleted(:count, :all)
assert_equal 4, Category.calculate_with_deleted(:count, :all)
end
def test_should_destroy
assert_equal 1, Widget.count
assert_equal 1, Category.count
widgets(:widget_1).destroy!
assert_equal 0, Widget.count
assert_equal 0, Category.count
assert_equal 1, Widget.count_only_deleted
assert_equal 1, Widget.calculate_with_deleted(:count, :all)
# Category doesn't get destroyed because the dependent before_destroy callback uses #destroy
assert_equal 4, Category.calculate_with_deleted(:count, :all)
end
def test_should_delete_all
assert_equal 1, Widget.count
assert_equal 2, Widget.calculate_with_deleted(:count, :all)
assert_equal 1, Category.count
Widget.delete_all
assert_equal 0, Widget.count
# delete_all doesn't call #destroy, so the dependent callback never fires
assert_equal 1, Category.count
assert_equal 2, Widget.calculate_with_deleted(:count, :all)
end
def test_should_delete_all_with_conditions
assert_equal 1, Widget.count
assert_equal 2, Widget.calculate_with_deleted(:count, :all)
Widget.delete_all("id < 3")
assert_equal 0, Widget.count
assert_equal 2, Widget.calculate_with_deleted(:count, :all)
end
def test_should_delete_all2
assert_equal 1, Category.count
assert_equal 4, Category.calculate_with_deleted(:count, :all)
Category.delete_all!
assert_equal 0, Category.count
assert_equal 0, Category.calculate_with_deleted(:count, :all)
end
def test_should_delete_all_with_conditions2
assert_equal 1, Category.count
assert_equal 4, Category.calculate_with_deleted(:count, :all)
Category.delete_all!("id < 3")
assert_equal 0, Category.count
assert_equal 2, Category.calculate_with_deleted(:count, :all)
end
def test_should_not_count_deleted
assert_equal 1, Widget.count
assert_equal 1, Widget.count(:all, :conditions => ['title=?', 'widget 1'])
assert_equal 2, Widget.calculate_with_deleted(:count, :all)
assert_equal 1, Widget.count_only_deleted
end
def test_should_find_only_deleted
assert_equal [2], Widget.find_only_deleted(:all).collect { |w| w.id }
assert_equal [1, 2], Widget.find_with_deleted(:all, :order => 'id').collect { |w| w.id }
end
def test_should_not_find_deleted
assert_equal [widgets(:widget_1)], Widget.find(:all)
assert_equal [1, 2], Widget.find_with_deleted(:all, :order => 'id').collect { |w| w.id }
end
def test_should_not_find_deleted_has_many_associations
assert_equal 1, widgets(:widget_1).categories.size
assert_equal [categories(:category_1)], widgets(:widget_1).categories
end
def test_should_not_find_deleted_habtm_associations
assert_equal 1, widgets(:widget_1).habtm_categories.size
assert_equal [categories(:category_1)], widgets(:widget_1).habtm_categories
end
def test_should_not_find_deleted_has_many_through_associations
assert_equal 1, widgets(:widget_1).tags.size
assert_equal [tags(:tag_2)], widgets(:widget_1).tags
end
def test_should_find_has_many_through_associations_with_deleted
assert_equal 2, widgets(:widget_1).any_tags.size
assert_equal Tag.find(:all), widgets(:widget_1).any_tags
end
def test_should_not_find_deleted_belongs_to_associations
assert_nil Category.find_with_deleted(3).widget
end
def test_should_find_belongs_to_assocation_with_deleted
assert_equal Widget.find_with_deleted(2), Category.find_with_deleted(3).any_widget
end
def test_should_find_first_with_deleted
assert_equal widgets(:widget_1), Widget.find(:first)
assert_equal 2, Widget.find_with_deleted(:first, :order => 'id desc').id
end
def test_should_find_single_id
assert Widget.find(1)
assert Widget.find_with_deleted(2)
assert_raises(ActiveRecord::RecordNotFound) { Widget.find(2) }
end
def test_should_find_multiple_ids
assert_equal [1,2], Widget.find_with_deleted(1,2).sort_by { |w| w.id }.collect { |w| w.id }
assert_equal [1,2], Widget.find_with_deleted([1,2]).sort_by { |w| w.id }.collect { |w| w.id }
assert_raises(ActiveRecord::RecordNotFound) { Widget.find(1,2) }
end
def test_should_ignore_multiple_includes
Widget.class_eval { acts_as_paranoid }
assert Widget.find(1)
end
def test_should_not_override_scopes_when_counting
assert_equal 1, Widget.send(:with_scope, :find => { :conditions => "title = 'widget 1'" }) { Widget.count }
assert_equal 0, Widget.send(:with_scope, :find => { :conditions => "title = 'deleted widget 2'" }) { Widget.count }
assert_equal 1, Widget.send(:with_scope, :find => { :conditions => "title = 'deleted widget 2'" }) { Widget.calculate_with_deleted(:count, :all) }
end
def test_should_not_override_scopes_when_finding
assert_equal [1], Widget.send(:with_scope, :find => { :conditions => "title = 'widget 1'" }) { Widget.find(:all) }.ids
assert_equal [], Widget.send(:with_scope, :find => { :conditions => "title = 'deleted widget 2'" }) { Widget.find(:all) }.ids
assert_equal [2], Widget.send(:with_scope, :find => { :conditions => "title = 'deleted widget 2'" }) { Widget.find_with_deleted(:all) }.ids
end
def test_should_allow_multiple_scoped_calls_when_finding
Widget.send(:with_scope, :find => { :conditions => "title = 'deleted widget 2'" }) do
assert_equal [2], Widget.find_with_deleted(:all).ids
assert_equal [2], Widget.find_with_deleted(:all).ids, "clobbers the constrain on the unmodified find"
assert_equal [], Widget.find(:all).ids
assert_equal [], Widget.find(:all).ids, 'clobbers the constrain on a paranoid find'
end
end
def test_should_allow_multiple_scoped_calls_when_counting
Widget.send(:with_scope, :find => { :conditions => "title = 'deleted widget 2'" }) do
assert_equal 1, Widget.calculate_with_deleted(:count, :all)
assert_equal 1, Widget.calculate_with_deleted(:count, :all), "clobbers the constrain on the unmodified find"
assert_equal 0, Widget.count
assert_equal 0, Widget.count, 'clobbers the constrain on a paranoid find'
end
end
def test_should_give_paranoid_status
assert Widget.paranoid?
assert !NonParanoidAndroid.paranoid?
end
def test_should_give_record_status
assert_equal false, Widget.find(1).deleted?
Widget.find(1).destroy
assert Widget.find_with_deleted(1).deleted?
end
def test_should_find_deleted_has_many_assocations_on_deleted_records_by_default
w = Widget.find_with_deleted 2
assert_equal 2, w.categories.find_with_deleted(:all).length
assert_equal 2, w.categories.find_with_deleted(:all).size
end
def test_should_find_deleted_habtm_assocations_on_deleted_records_by_default
w = Widget.find_with_deleted 2
assert_equal 2, w.habtm_categories.find_with_deleted(:all).length
assert_equal 2, w.habtm_categories.find_with_deleted(:all).size
end
def test_dynamic_finders
assert Widget.find_by_id(1)
assert_nil Widget.find_by_id(2)
end
def test_custom_finder_methods
w = Widget.find_with_deleted(:all).inject({}) { |all, w| all.merge(w.id => w) }
assert_equal [1], Category.search('c').ids
assert_equal [1,2,3,4], Category.search_with_deleted('c', :order => 'id').ids
assert_equal [1], widgets(:widget_1).categories.search('c').collect(&:id)
assert_equal [1,2], widgets(:widget_1).categories.search_with_deleted('c').ids
assert_equal [], w[2].categories.search('c').ids
assert_equal [3,4], w[2].categories.search_with_deleted('c').ids
end
def test_should_recover_record
Widget.find(1).destroy
assert_equal true, Widget.find_with_deleted(1).deleted?
Widget.find_with_deleted(1).recover!
assert_equal false, Widget.find(1).deleted?
end
def test_should_recover_record_and_has_many_associations
Widget.find(1).destroy
assert_equal true, Widget.find_with_deleted(1).deleted?
assert_equal true, Category.find_with_deleted(1).deleted?
Widget.find_with_deleted(1).recover_with_associations!(:categories)
assert_equal false, Widget.find(1).deleted?
assert_equal false, Category.find(1).deleted?
end
end
class Array
def ids
collect &:id
end
end

@ -1,30 +0,0 @@
ActiveRecord::Schema.define(:version => 1) do
create_table :widgets, :force => true do |t|
t.column :title, :string, :limit => 50
t.column :category_id, :integer
t.column :deleted_at, :timestamp
end
create_table :categories, :force => true do |t|
t.column :widget_id, :integer
t.column :title, :string, :limit => 50
t.column :deleted_at, :timestamp
end
create_table :categories_widgets, :force => true, :id => false do |t|
t.column :category_id, :integer
t.column :widget_id, :integer
end
create_table :tags, :force => true do |t|
t.column :name, :string, :limit => 50
end
create_table :taggings, :force => true do |t|
t.column :tag_id, :integer
t.column :widget_id, :integer
t.column :deleted_at, :timestamp
end
end

@ -1,47 +0,0 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
require 'test/unit'
require 'rubygems'
if ENV['RAILS'].nil?
require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb'))
else
# specific rails version targeted
# load activerecord and plugin manually
gem 'activerecord', "=#{ENV['RAILS']}"
require 'active_record'
$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
Dir["#{$LOAD_PATH.last}/**/*.rb"].each do |path|
require path[$LOAD_PATH.last.size + 1..-1]
end
require File.join(File.dirname(__FILE__), '..', 'init.rb')
end
require 'active_record/fixtures'
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
# do this so fixtures will load
ActiveRecord::Base.configurations.update config
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite3'])
load(File.dirname(__FILE__) + "/schema.rb")
Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
$LOAD_PATH.unshift(Test::Unit::TestCase.fixture_path)
class Test::Unit::TestCase #:nodoc:
def create_fixtures(*table_names)
if block_given?
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield }
else
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names)
end
end
# Turn off transactional fixtures if you're working with MyISAM tables in MySQL
self.use_transactional_fixtures = true
# Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
self.use_instantiated_fixtures = false
# Add more helper methods to be used by all tests here...
end

@ -1,82 +0,0 @@
*GIT* (version numbers are overrated)
* (16 Jun 2008) Backwards Compatibility is overrated (big updates for rails 2.1)
* Use ActiveRecord 2.1's dirty attribute checking instead [Asa Calow]
* Remove last traces of #non_versioned_fields
* Remove AR::Base.find_version and AR::Base.find_versions, rely on AR association proxies and named_scope
* Remove #versions_count, rely on AR association counter caching.
* Remove #versioned_attributes, basically the same as AR::Base.versioned_columns
* (5 Oct 2006) Allow customization of #versions association options [Dan Peterson]
*0.5.1*
* (8 Aug 2006) Versioned models now belong to the unversioned model. @article_version.article.class => Article [Aslak Hellesoy]
*0.5* # do versions even matter for plugins?
* (21 Apr 2006) Added without_locking and without_revision methods.
Foo.without_revision do
@foo.update_attributes ...
end
*0.4*
* (28 March 2006) Rename non_versioned_fields to non_versioned_columns (old one is kept for compatibility).
* (28 March 2006) Made explicit documentation note that string column names are required for non_versioned_columns.
*0.3.1*
* (7 Jan 2006) explicitly set :foreign_key option for the versioned model's belongs_to assocation for STI [Caged]
* (7 Jan 2006) added tests to prove has_many :through joins work
*0.3*
* (2 Jan 2006) added ability to share a mixin with versioned class
* (2 Jan 2006) changed the dynamic version model to MyModel::Version
*0.2.4*
* (27 Nov 2005) added note about possible destructive behavior of if_changed? [Michael Schuerig]
*0.2.3*
* (12 Nov 2005) fixed bug with old behavior of #blank? [Michael Schuerig]
* (12 Nov 2005) updated tests to use ActiveRecord Schema
*0.2.2*
* (3 Nov 2005) added documentation note to #acts_as_versioned [Martin Jul]
*0.2.1*
* (6 Oct 2005) renamed dirty? to changed? to keep it uniform. it was aliased to keep it backwards compatible.
*0.2*
* (6 Oct 2005) added find_versions and find_version class methods.
* (6 Oct 2005) removed transaction from create_versioned_table().
this way you can specify your own transaction around a group of operations.
* (30 Sep 2005) fixed bug where find_versions() would order by 'version' twice. (found by Joe Clark)
* (26 Sep 2005) added :sequence_name option to acts_as_versioned to set the sequence name on the versioned model
*0.1.3* (18 Sep 2005)
* First RubyForge release
*0.1.2*
* check if module is already included when acts_as_versioned is called
*0.1.1*
* Adding tests and rdocs
*0.1*
* Initial transfer from Rails ticket: http://dev.rubyonrails.com/ticket/1974

@ -1,20 +0,0 @@
Copyright (c) 2005 Rick Olson
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.

@ -1,28 +0,0 @@
= acts_as_versioned
This library adds simple versioning to an ActiveRecord module. ActiveRecord is required.
== Resources
Install
* gem install acts_as_versioned
Rubyforge project
* http://rubyforge.org/projects/ar-versioned
RDocs
* http://ar-versioned.rubyforge.org
Subversion
* http://techno-weenie.net/svn/projects/acts_as_versioned
Collaboa
* http://collaboa.techno-weenie.net/repository/browse/acts_as_versioned
Special thanks to Dreamer on ##rubyonrails for help in early testing. His ServerSideWiki (http://serversidewiki.com)
was the first project to use acts_as_versioned <em>in the wild</em>.

@ -1,41 +0,0 @@
== Creating the test database
The default name for the test databases is "activerecord_versioned". If you
want to use another database name then be sure to update the connection
adapter setups you want to test with in test/connections/<your database>/connection.rb.
When you have the database online, you can import the fixture tables with
the test/fixtures/db_definitions/*.sql files.
Make sure that you create database objects with the same user that you specified in i
connection.rb otherwise (on Postgres, at least) tests for default values will fail.
== Running with Rake
The easiest way to run the unit tests is through Rake. The default task runs
the entire test suite for all the adapters. You can also run the suite on just
one adapter by using the tasks test_mysql_ruby, test_ruby_mysql, test_sqlite,
or test_postresql. For more information, checkout the full array of rake tasks with "rake -T"
Rake can be found at http://rake.rubyforge.org
== Running by hand
Unit tests are located in test directory. If you only want to run a single test suite,
or don't want to bother with Rake, you can do so with something like:
cd test; ruby -I "connections/native_mysql" base_test.rb
That'll run the base suite using the MySQL-Ruby adapter. Change the adapter
and test suite name as needed.
== Faster tests
If you are using a database that supports transactions, you can set the
"AR_TX_FIXTURES" environment variable to "yes" to use transactional fixtures.
This gives a very large speed boost. With rake:
rake AR_TX_FIXTURES=yes
Or, by hand:
AR_TX_FIXTURES=yes ruby -I connections/native_sqlite3 base_test.rb

@ -1,180 +0,0 @@
require 'rubygems'
require 'rake/rdoctask'
require 'rake/packagetask'
require 'rake/gempackagetask'
require 'rake/testtask'
require 'rake/contrib/rubyforgepublisher'
PKG_NAME = 'acts_as_versioned'
PKG_VERSION = '0.3.1'
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
PROD_HOST = "technoweenie@bidwell.textdrive.com"
RUBY_FORGE_PROJECT = 'ar-versioned'
RUBY_FORGE_USER = 'technoweenie'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the calculations plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the calculations plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = "#{PKG_NAME} -- Simple versioning with active record models"
rdoc.options << '--line-numbers --inline-source'
rdoc.rdoc_files.include('README', 'CHANGELOG', 'RUNNING_UNIT_TESTS')
rdoc.rdoc_files.include('lib/**/*.rb')
end
spec = Gem::Specification.new do |s|
s.name = PKG_NAME
s.version = PKG_VERSION
s.platform = Gem::Platform::RUBY
s.summary = "Simple versioning with active record models"
s.files = FileList["{lib,test}/**/*"].to_a + %w(README MIT-LICENSE CHANGELOG RUNNING_UNIT_TESTS)
s.files.delete "acts_as_versioned_plugin.sqlite.db"
s.files.delete "acts_as_versioned_plugin.sqlite3.db"
s.files.delete "test/debug.log"
s.require_path = 'lib'
s.autorequire = 'acts_as_versioned'
s.has_rdoc = true
s.test_files = Dir['test/**/*_test.rb']
s.add_dependency 'activerecord', '>= 1.10.1'
s.add_dependency 'activesupport', '>= 1.1.1'
s.author = "Rick Olson"
s.email = "technoweenie@gmail.com"
s.homepage = "http://techno-weenie.net"
end
Rake::GemPackageTask.new(spec) do |pkg|
pkg.need_tar = true
end
desc "Publish the API documentation"
task :pdoc => [:rdoc] do
Rake::RubyForgePublisher.new(RUBY_FORGE_PROJECT, RUBY_FORGE_USER).upload
end
desc 'Publish the gem and API docs'
task :publish => [:pdoc, :rubyforge_upload]
desc "Publish the release files to RubyForge."
task :rubyforge_upload => :package do
files = %w(gem tgz).map { |ext| "pkg/#{PKG_FILE_NAME}.#{ext}" }
if RUBY_FORGE_PROJECT then
require 'net/http'
require 'open-uri'
project_uri = "http://rubyforge.org/projects/#{RUBY_FORGE_PROJECT}/"
project_data = open(project_uri) { |data| data.read }
group_id = project_data[/[?&]group_id=(\d+)/, 1]
raise "Couldn't get group id" unless group_id
# This echos password to shell which is a bit sucky
if ENV["RUBY_FORGE_PASSWORD"]
password = ENV["RUBY_FORGE_PASSWORD"]
else
print "#{RUBY_FORGE_USER}@rubyforge.org's password: "
password = STDIN.gets.chomp
end
login_response = Net::HTTP.start("rubyforge.org", 80) do |http|
data = [
"login=1",
"form_loginname=#{RUBY_FORGE_USER}",
"form_pw=#{password}"
].join("&")
http.post("/account/login.php", data)
end
cookie = login_response["set-cookie"]
raise "Login failed" unless cookie
headers = { "Cookie" => cookie }
release_uri = "http://rubyforge.org/frs/admin/?group_id=#{group_id}"
release_data = open(release_uri, headers) { |data| data.read }
package_id = release_data[/[?&]package_id=(\d+)/, 1]
raise "Couldn't get package id" unless package_id
first_file = true
release_id = ""
files.each do |filename|
basename = File.basename(filename)
file_ext = File.extname(filename)
file_data = File.open(filename, "rb") { |file| file.read }
puts "Releasing #{basename}..."
release_response = Net::HTTP.start("rubyforge.org", 80) do |http|
release_date = Time.now.strftime("%Y-%m-%d %H:%M")
type_map = {
".zip" => "3000",
".tgz" => "3110",
".gz" => "3110",
".gem" => "1400"
}; type_map.default = "9999"
type = type_map[file_ext]
boundary = "rubyqMY6QN9bp6e4kS21H4y0zxcvoor"
query_hash = if first_file then
{
"group_id" => group_id,
"package_id" => package_id,
"release_name" => PKG_FILE_NAME,
"release_date" => release_date,
"type_id" => type,
"processor_id" => "8000", # Any
"release_notes" => "",
"release_changes" => "",
"preformatted" => "1",
"submit" => "1"
}
else
{
"group_id" => group_id,
"release_id" => release_id,
"package_id" => package_id,
"step2" => "1",
"type_id" => type,
"processor_id" => "8000", # Any
"submit" => "Add This File"
}
end
query = "?" + query_hash.map do |(name, value)|
[name, URI.encode(value)].join("=")
end.join("&")
data = [
"--" + boundary,
"Content-Disposition: form-data; name=\"userfile\"; filename=\"#{basename}\"",
"Content-Type: application/octet-stream",
"Content-Transfer-Encoding: binary",
"", file_data, ""
].join("\x0D\x0A")
release_headers = headers.merge(
"Content-Type" => "multipart/form-data; boundary=#{boundary}"
)
target = first_file ? "/frs/admin/qrs.php" : "/frs/admin/editrelease.php"
http.post(target + query, data, release_headers)
end
if first_file then
release_id = release_response.body[/release_id=(\d+)/, 1]
raise("Couldn't get release id") unless release_id
end
first_file = false
end
end
end

@ -1,4 +0,0 @@
---
:patch: 2
:major: 0
:minor: 5

@ -1,29 +0,0 @@
# -*- encoding: utf-8 -*-
Gem::Specification.new do |s|
s.name = %q{acts_as_versioned}
s.version = "0.5.2"
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["technoweenie"]
s.date = %q{2009-01-20}
s.description = %q{TODO}
s.email = %q{technoweenie@bidwell.textdrive.com}
s.files = ["VERSION.yml", "lib/acts_as_versioned.rb", "test/abstract_unit.rb", "test/database.yml", "test/fixtures", "test/fixtures/authors.yml", "test/fixtures/landmark.rb", "test/fixtures/landmark_versions.yml", "test/fixtures/landmarks.yml", "test/fixtures/locked_pages.yml", "test/fixtures/locked_pages_revisions.yml", "test/fixtures/migrations", "test/fixtures/migrations/1_add_versioned_tables.rb", "test/fixtures/page.rb", "test/fixtures/page_versions.yml", "test/fixtures/pages.yml", "test/fixtures/widget.rb", "test/migration_test.rb", "test/schema.rb", "test/versioned_test.rb"]
s.has_rdoc = true
s.homepage = %q{http://github.com/technoweenie/acts_as_versioned}
s.rdoc_options = ["--inline-source", "--charset=UTF-8"]
s.require_paths = ["lib"]
s.rubygems_version = %q{1.3.1}
s.summary = %q{TODO}
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
else
end
else
end
end

@ -1 +0,0 @@
require 'acts_as_versioned'

@ -1,486 +0,0 @@
# Copyright (c) 2005 Rick Olson
#
# 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.
module ActiveRecord #:nodoc:
module Acts #:nodoc:
# Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
# versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
# column is present as well.
#
# The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
# your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
#
# class Page < ActiveRecord::Base
# # assumes pages_versions table
# acts_as_versioned
# end
#
# Example:
#
# page = Page.create(:title => 'hello world!')
# page.version # => 1
#
# page.title = 'hello world'
# page.save
# page.version # => 2
# page.versions.size # => 2
#
# page.revert_to(1) # using version number
# page.title # => 'hello world!'
#
# page.revert_to(page.versions.last) # using versioned instance
# page.title # => 'hello world'
#
# page.versions.earliest # efficient query to find the first version
# page.versions.latest # efficient query to find the most recently created version
#
#
# Simple Queries to page between versions
#
# page.versions.before(version)
# page.versions.after(version)
#
# Access the previous/next versions from the versioned model itself
#
# version = page.versions.latest
# version.previous # go back one version
# version.next # go forward one version
#
# See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
module Versioned
CALLBACKS = [:set_new_version, :save_version, :save_version?]
def self.included(base) # :nodoc:
base.extend ClassMethods
end
module ClassMethods
# == Configuration options
#
# * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
# * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
# * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
# * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
# * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
# * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
# * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
# * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
# For finer control, pass either a Proc or modify Model#version_condition_met?
#
# acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
#
# or...
#
# class Auction
# def version_condition_met? # totally bypasses the <tt>:if</tt> option
# !expired?
# end
# end
#
# * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
# either a symbol or array of symbols.
#
# * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block
# to create an anonymous mixin:
#
# class Auction
# acts_as_versioned do
# def started?
# !started_at.nil?
# end
# end
# end
#
# or...
#
# module AuctionExtension
# def started?
# !started_at.nil?
# end
# end
# class Auction
# acts_as_versioned :extend => AuctionExtension
# end
#
# Example code:
#
# @auction = Auction.find(1)
# @auction.started?
# @auction.versions.first.started?
#
# == Database Schema
#
# The model that you're versioning needs to have a 'version' attribute. The model is versioned
# into a table called #{model}_versions where the model name is singlular. The _versions table should
# contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
#
# A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
# then that field is reflected in the versioned model as 'versioned_type' by default.
#
# Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
# method, perfect for a migration. It will also create the version column if the main model does not already have it.
#
# class AddVersions < ActiveRecord::Migration
# def self.up
# # create_versioned_table takes the same options hash
# # that create_table does
# Post.create_versioned_table
# end
#
# def self.down
# Post.drop_versioned_table
# end
# end
#
# == Changing What Fields Are Versioned
#
# By default, acts_as_versioned will version all but these fields:
#
# [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
#
# You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
#
# class Post < ActiveRecord::Base
# acts_as_versioned
# self.non_versioned_columns << 'comments_count'
# end
#
def acts_as_versioned(options = {}, &extension)
# don't allow multiple calls
return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
send :include, ActiveRecord::Acts::Versioned::ActMethods
cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
:version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
:version_association_options, :version_if_changed
self.versioned_class_name = options[:class_name] || "Version"
self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
self.version_column = options[:version_column] || 'version'
self.version_sequence_name = options[:sequence_name]
self.max_version_limit = options[:limit].to_i
self.version_condition = options[:if] || true
self.non_versioned_columns = [self.primary_key, inheritance_column, self.version_column, 'lock_version', versioned_inheritance_column] + options[:non_versioned_columns].to_a.map(&:to_s)
self.version_association_options = {
:class_name => "#{self.to_s}::#{versioned_class_name}",
:foreign_key => versioned_foreign_key,
:dependent => :delete_all
}.merge(options[:association_options] || {})
if block_given?
extension_module_name = "#{versioned_class_name}Extension"
silence_warnings do
self.const_set(extension_module_name, Module.new(&extension))
end
options[:extend] = self.const_get(extension_module_name)
end
class_eval <<-CLASS_METHODS
has_many :versions, version_association_options do
# finds earliest version of this record
def earliest
@earliest ||= find(:first, :order => '#{version_column}')
end
# find latest version of this record
def latest
@latest ||= find(:first, :order => '#{version_column} desc')
end
end
before_save :set_new_version
after_save :save_version
after_save :clear_old_versions
unless options[:if_changed].nil?
self.track_altered_attributes = true
options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
self.version_if_changed = options[:if_changed].map(&:to_s)
end
include options[:extend] if options[:extend].is_a?(Module)
CLASS_METHODS
# create the dynamic versioned model
const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
def self.reloadable? ; false ; end
# find first version before the given version
def self.before(version)
find :first, :order => 'version desc',
:conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]
end
# find first version after the given version.
def self.after(version)
find :first, :order => 'version',
:conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]
end
def previous
self.class.before(self)
end
def next
self.class.after(self)
end
def versions_count
page.version
end
end
versioned_class.cattr_accessor :original_class
versioned_class.original_class = self
versioned_class.set_table_name versioned_table_name
versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
:class_name => "::#{self.to_s}",
:foreign_key => versioned_foreign_key
versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
versioned_class.set_sequence_name version_sequence_name if version_sequence_name
end
end
module ActMethods
def self.included(base) # :nodoc:
base.extend ClassMethods
end
# Saves a version of the model in the versioned table. This is called in the after_save callback by default
def save_version
if @saving_version
@saving_version = nil
rev = self.class.versioned_class.new
clone_versioned_model(self, rev)
rev.send("#{self.class.version_column}=", send(self.class.version_column))
rev.send("#{self.class.versioned_foreign_key}=", id)
rev.save
end
end
# Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
# Override this method to set your own criteria for clearing old versions.
def clear_old_versions
return if self.class.max_version_limit == 0
excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
if excess_baggage > 0
self.class.versioned_class.delete_all ["#{self.class.version_column} <= ? and #{self.class.versioned_foreign_key} = ?", excess_baggage, id]
end
end
# Reverts a model to a given version. Takes either a version number or an instance of the versioned model
def revert_to(version)
if version.is_a?(self.class.versioned_class)
return false unless version.send(self.class.versioned_foreign_key) == id and !version.new_record?
else
return false unless version = versions.send("find_by_#{self.class.version_column}", version)
end
self.clone_versioned_model(version, self)
send("#{self.class.version_column}=", version.send(self.class.version_column))
true
end
# Reverts a model to a given version and saves the model.
# Takes either a version number or an instance of the versioned model
def revert_to!(version)
revert_to(version) ? save_without_revision : false
end
# Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
def save_without_revision
save_without_revision!
true
rescue
false
end
def save_without_revision!
without_locking do
without_revision do
save!
end
end
end
def altered?
track_altered_attributes ? (version_if_changed - changed).length < version_if_changed.length : changed?
end
# Clones a model. Used when saving a new version or reverting a model's version.
def clone_versioned_model(orig_model, new_model)
self.class.versioned_columns.each do |col|
new_model.send("#{col.name}=", orig_model.send(col.name)) if orig_model.has_attribute?(col.name)
end
if orig_model.is_a?(self.class.versioned_class)
new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
elsif new_model.is_a?(self.class.versioned_class)
new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
end
end
# Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
def save_version?
version_condition_met? && altered?
end
# Checks condition set in the :if option to check whether a revision should be created or not. Override this for
# custom version condition checking.
def version_condition_met?
case
when version_condition.is_a?(Symbol)
send(version_condition)
when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
version_condition.call(self)
else
version_condition
end
end
# Executes the block with the versioning callbacks disabled.
#
# @foo.without_revision do
# @foo.save
# end
#
def without_revision(&block)
self.class.without_revision(&block)
end
# Turns off optimistic locking for the duration of the block
#
# @foo.without_locking do
# @foo.save
# end
#
def without_locking(&block)
self.class.without_locking(&block)
end
def empty_callback() end #:nodoc:
protected
# sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
def set_new_version
@saving_version = new_record? || save_version?
self.send("#{self.class.version_column}=", next_version) if new_record? || (!locking_enabled? && save_version?)
end
# Gets the next available version for the current record, or 1 for a new record
def next_version
(new_record? ? 0 : versions.calculate(:max, version_column).to_i) + 1
end
module ClassMethods
# Returns an array of columns that are versioned. See non_versioned_columns
def versioned_columns
@versioned_columns ||= columns.select { |c| !non_versioned_columns.include?(c.name) }
end
# Returns an instance of the dynamic versioned model
def versioned_class
const_get versioned_class_name
end
# Rake migration task to create the versioned table using options passed to acts_as_versioned
def create_versioned_table(create_table_options = {})
# create version column in main table if it does not exist
if !self.content_columns.find { |c| [version_column.to_s, 'lock_version'].include? c.name }
self.connection.add_column table_name, version_column, :integer
self.reset_column_information
end
return if connection.table_exists?(versioned_table_name)
self.connection.create_table(versioned_table_name, create_table_options) do |t|
t.column versioned_foreign_key, :integer
t.column version_column, :integer
end
self.versioned_columns.each do |col|
self.connection.add_column versioned_table_name, col.name, col.type,
:limit => col.limit,
:default => col.default,
:scale => col.scale,
:precision => col.precision
end
if type_col = self.columns_hash[inheritance_column]
self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
:limit => type_col.limit,
:default => type_col.default,
:scale => type_col.scale,
:precision => type_col.precision
end
self.connection.add_index versioned_table_name, versioned_foreign_key
end
# Rake migration task to drop the versioned table
def drop_versioned_table
self.connection.drop_table versioned_table_name
end
# Executes the block with the versioning callbacks disabled.
#
# Foo.without_revision do
# @foo.save
# end
#
def without_revision(&block)
class_eval do
CALLBACKS.each do |attr_name|
alias_method "orig_#{attr_name}".to_sym, attr_name
alias_method attr_name, :empty_callback
end
end
block.call
ensure
class_eval do
CALLBACKS.each do |attr_name|
alias_method attr_name, "orig_#{attr_name}".to_sym
end
end
end
# Turns off optimistic locking for the duration of the block
#
# Foo.without_locking do
# @foo.save
# end
#
def without_locking(&block)
current = ActiveRecord::Base.lock_optimistically
ActiveRecord::Base.lock_optimistically = false if current
begin
block.call
ensure
ActiveRecord::Base.lock_optimistically = true if current
end
end
end
end
end
end
end
ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned

@ -1,48 +0,0 @@
$:.unshift(File.dirname(__FILE__) + '/../../../rails/activesupport/lib')
$:.unshift(File.dirname(__FILE__) + '/../../../rails/activerecord/lib')
$:.unshift(File.dirname(__FILE__) + '/../lib')
require 'test/unit'
begin
require 'active_support'
require 'active_record'
require 'active_record/fixtures'
rescue LoadError
require 'rubygems'
retry
end
begin
require 'ruby-debug'
Debugger.start
rescue LoadError
end
require 'acts_as_versioned'
config = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
ActiveRecord::Base.configurations = {'test' => config[ENV['DB'] || 'sqlite3']}
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
load(File.dirname(__FILE__) + "/schema.rb")
# set up custom sequence on widget_versions for DBs that support sequences
if ENV['DB'] == 'postgresql'
ActiveRecord::Base.connection.execute "DROP SEQUENCE widgets_seq;" rescue nil
ActiveRecord::Base.connection.remove_column :widget_versions, :id
ActiveRecord::Base.connection.execute "CREATE SEQUENCE widgets_seq START 101;"
ActiveRecord::Base.connection.execute "ALTER TABLE widget_versions ADD COLUMN id INTEGER PRIMARY KEY DEFAULT nextval('widgets_seq');"
end
Test::Unit::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
$:.unshift(Test::Unit::TestCase.fixture_path)
class Test::Unit::TestCase #:nodoc:
# Turn off transactional fixtures if you're working with MyISAM tables in MySQL
self.use_transactional_fixtures = true
# Instantiated fixtures are slow, but give you @david where you otherwise would need people(:david)
self.use_instantiated_fixtures = false
# Add more helper methods to be used by all tests here...
end

@ -1,18 +0,0 @@
sqlite:
:adapter: sqlite
:dbfile: acts_as_versioned_plugin.sqlite.db
sqlite3:
:adapter: sqlite3
:dbfile: acts_as_versioned_plugin.sqlite3.db
postgresql:
:adapter: postgresql
:username: postgres
:password: postgres
:database: acts_as_versioned_plugin_test
:min_messages: ERROR
mysql:
:adapter: mysql
:host: localhost
:username: rails
:password:
:database: acts_as_versioned_plugin_test

@ -1,6 +0,0 @@
caged:
id: 1
name: caged
mly:
id: 2
name: mly

@ -1,3 +0,0 @@
class Landmark < ActiveRecord::Base
acts_as_versioned :if_changed => [ :name, :longitude, :latitude ]
end

@ -1,7 +0,0 @@
washington:
id: 1
landmark_id: 1
version: 1
name: Washington, D.C.
latitude: 38.895
longitude: -77.036667

@ -1,7 +0,0 @@
washington:
id: 1
name: Washington, D.C.
latitude: 38.895
longitude: -77.036667
doesnt_trigger_version: This is not important
version: 1

@ -1,10 +0,0 @@
welcome:
id: 1
title: Welcome to the weblog
lock_version: 24
type: LockedPage
thinking:
id: 2
title: So I was thinking
lock_version: 24
type: SpecialLockedPage

@ -1,27 +0,0 @@
welcome_1:
id: 1
page_id: 1
title: Welcome to the weblg
lock_version: 23
version_type: LockedPage
welcome_2:
id: 2
page_id: 1
title: Welcome to the weblog
lock_version: 24
version_type: LockedPage
thinking_1:
id: 3
page_id: 2
title: So I was thinking!!!
lock_version: 23
version_type: SpecialLockedPage
thinking_2:
id: 4
page_id: 2
title: So I was thinking
lock_version: 24
version_type: SpecialLockedPage

@ -1,15 +0,0 @@
class AddVersionedTables < ActiveRecord::Migration
def self.up
create_table("things") do |t|
t.column :title, :text
t.column :price, :decimal, :precision => 7, :scale => 2
t.column :type, :string
end
Thing.create_versioned_table
end
def self.down
Thing.drop_versioned_table
drop_table "things" rescue nil
end
end

@ -1,43 +0,0 @@
class Page < ActiveRecord::Base
belongs_to :author
has_many :authors, :through => :versions, :order => 'name'
belongs_to :revisor, :class_name => 'Author'
has_many :revisors, :class_name => 'Author', :through => :versions, :order => 'name'
acts_as_versioned :if => :feeling_good? do
def self.included(base)
base.cattr_accessor :feeling_good
base.feeling_good = true
base.belongs_to :author
base.belongs_to :revisor, :class_name => 'Author'
end
def feeling_good?
@@feeling_good == true
end
end
end
module LockedPageExtension
def hello_world
'hello_world'
end
end
class LockedPage < ActiveRecord::Base
acts_as_versioned \
:inheritance_column => :version_type,
:foreign_key => :page_id,
:table_name => :locked_pages_revisions,
:class_name => 'LockedPageRevision',
:version_column => :lock_version,
:limit => 2,
:if_changed => :title,
:extend => LockedPageExtension
end
class SpecialLockedPage < LockedPage
end
class Author < ActiveRecord::Base
has_many :pages
end

@ -1,16 +0,0 @@
welcome_2:
id: 1
page_id: 1
title: Welcome to the weblog
body: Such a lovely day
version: 24
author_id: 1
revisor_id: 1
welcome_1:
id: 2
page_id: 1
title: Welcome to the weblg
body: Such a lovely day
version: 23
author_id: 2
revisor_id: 2

@ -1,8 +0,0 @@
welcome:
id: 1
title: Welcome to the weblog
body: Such a lovely day
version: 24
author_id: 1
revisor_id: 1
created_on: "2008-01-01 00:00:00"

@ -1,6 +0,0 @@
class Widget < ActiveRecord::Base
acts_as_versioned :sequence_name => 'widgets_seq', :association_options => {
:dependent => :nullify, :order => 'version desc'
}
non_versioned_columns << 'foo'
end

@ -1,46 +0,0 @@
require File.join(File.dirname(__FILE__), 'abstract_unit')
if ActiveRecord::Base.connection.supports_migrations?
class Thing < ActiveRecord::Base
attr_accessor :version
acts_as_versioned
end
class MigrationTest < Test::Unit::TestCase
self.use_transactional_fixtures = false
def teardown
if ActiveRecord::Base.connection.respond_to?(:initialize_schema_information)
ActiveRecord::Base.connection.initialize_schema_information
ActiveRecord::Base.connection.update "UPDATE schema_info SET version = 0"
else
ActiveRecord::Base.connection.initialize_schema_migrations_table
ActiveRecord::Base.connection.assume_migrated_upto_version(0)
end
Thing.connection.drop_table "things" rescue nil
Thing.connection.drop_table "thing_versions" rescue nil
Thing.reset_column_information
end
def test_versioned_migration
assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
# take 'er up
ActiveRecord::Migrator.up(File.dirname(__FILE__) + '/fixtures/migrations/')
t = Thing.create :title => 'blah blah', :price => 123.45, :type => 'Thing'
assert_equal 1, t.versions.size
# check that the price column has remembered its value correctly
assert_equal t.price, t.versions.first.price
assert_equal t.title, t.versions.first.title
assert_equal t[:type], t.versions.first[:type]
# make sure that the precision of the price column has been preserved
assert_equal 7, Thing::Version.columns.find{|c| c.name == "price"}.precision
assert_equal 2, Thing::Version.columns.find{|c| c.name == "price"}.scale
# now lets take 'er back down
ActiveRecord::Migrator.down(File.dirname(__FILE__) + '/fixtures/migrations/')
assert_raises(ActiveRecord::StatementInvalid) { Thing.create :title => 'blah blah' }
end
end
end

@ -1,82 +0,0 @@
ActiveRecord::Schema.define(:version => 0) do
create_table :pages, :force => true do |t|
t.column :version, :integer
t.column :title, :string, :limit => 255
t.column :body, :text
t.column :created_on, :datetime
t.column :updated_on, :datetime
t.column :author_id, :integer
t.column :revisor_id, :integer
end
create_table :page_versions, :force => true do |t|
t.column :page_id, :integer
t.column :version, :integer
t.column :title, :string, :limit => 255
t.column :body, :text
t.column :created_on, :datetime
t.column :updated_on, :datetime
t.column :author_id, :integer
t.column :revisor_id, :integer
end
add_index :page_versions, [:page_id, :version], :unique => true
create_table :authors, :force => true do |t|
t.column :page_id, :integer
t.column :name, :string
end
create_table :locked_pages, :force => true do |t|
t.column :lock_version, :integer
t.column :title, :string, :limit => 255
t.column :body, :text
t.column :type, :string, :limit => 255
end
create_table :locked_pages_revisions, :force => true do |t|
t.column :page_id, :integer
t.column :lock_version, :integer
t.column :title, :string, :limit => 255
t.column :body, :text
t.column :version_type, :string, :limit => 255
t.column :updated_at, :datetime
end
add_index :locked_pages_revisions, [:page_id, :lock_version], :unique => true
create_table :widgets, :force => true do |t|
t.column :name, :string, :limit => 50
t.column :foo, :string
t.column :version, :integer
t.column :updated_at, :datetime
end
create_table :widget_versions, :force => true do |t|
t.column :widget_id, :integer
t.column :name, :string, :limit => 50
t.column :version, :integer
t.column :updated_at, :datetime
end
add_index :widget_versions, [:widget_id, :version], :unique => true
create_table :landmarks, :force => true do |t|
t.column :name, :string
t.column :latitude, :float
t.column :longitude, :float
t.column :doesnt_trigger_version,:string
t.column :version, :integer
end
create_table :landmark_versions, :force => true do |t|
t.column :landmark_id, :integer
t.column :name, :string
t.column :latitude, :float
t.column :longitude, :float
t.column :doesnt_trigger_version,:string
t.column :version, :integer
end
add_index :landmark_versions, [:landmark_id, :version], :unique => true
end

@ -1,370 +0,0 @@
require File.join(File.dirname(__FILE__), 'abstract_unit')
require File.join(File.dirname(__FILE__), 'fixtures/page')
require File.join(File.dirname(__FILE__), 'fixtures/widget')
class VersionedTest < Test::Unit::TestCase
fixtures :pages, :page_versions, :locked_pages, :locked_pages_revisions, :authors, :landmarks, :landmark_versions
set_fixture_class :page_versions => Page::Version
def test_saves_versioned_copy
p = Page.create! :title => 'first title', :body => 'first body'
assert !p.new_record?
assert_equal 1, p.versions.size
assert_equal 1, p.version
assert_instance_of Page.versioned_class, p.versions.first
end
def test_saves_without_revision
p = pages(:welcome)
old_versions = p.versions.count
p.save_without_revision
p.without_revision do
p.update_attributes :title => 'changed'
end
assert_equal old_versions, p.versions.count
end
def test_rollback_with_version_number
p = pages(:welcome)
assert_equal 24, p.version
assert_equal 'Welcome to the weblog', p.title
assert p.revert_to!(23), "Couldn't revert to 23"
assert_equal 23, p.version
assert_equal 'Welcome to the weblg', p.title
end
def test_versioned_class_name
assert_equal 'Version', Page.versioned_class_name
assert_equal 'LockedPageRevision', LockedPage.versioned_class_name
end
def test_versioned_class
assert_equal Page::Version, Page.versioned_class
assert_equal LockedPage::LockedPageRevision, LockedPage.versioned_class
end
def test_special_methods
assert_nothing_raised { pages(:welcome).feeling_good? }
assert_nothing_raised { pages(:welcome).versions.first.feeling_good? }
assert_nothing_raised { locked_pages(:welcome).hello_world }
assert_nothing_raised { locked_pages(:welcome).versions.first.hello_world }
end
def test_rollback_with_version_class
p = pages(:welcome)
assert_equal 24, p.version
assert_equal 'Welcome to the weblog', p.title
assert p.revert_to!(p.versions.find_by_version(23)), "Couldn't revert to 23"
assert_equal 23, p.version
assert_equal 'Welcome to the weblg', p.title
end
def test_rollback_fails_with_invalid_revision
p = locked_pages(:welcome)
assert !p.revert_to!(locked_pages(:thinking))
end
def test_saves_versioned_copy_with_options
p = LockedPage.create! :title => 'first title'
assert !p.new_record?
assert_equal 1, p.versions.size
assert_instance_of LockedPage.versioned_class, p.versions.first
end
def test_rollback_with_version_number_with_options
p = locked_pages(:welcome)
assert_equal 'Welcome to the weblog', p.title
assert_equal 'LockedPage', p.versions.first.version_type
assert p.revert_to!(p.versions.first.lock_version), "Couldn't revert to 23"
assert_equal 'Welcome to the weblg', p.title
assert_equal 'LockedPage', p.versions.first.version_type
end
def test_rollback_with_version_class_with_options
p = locked_pages(:welcome)
assert_equal 'Welcome to the weblog', p.title
assert_equal 'LockedPage', p.versions.first.version_type
assert p.revert_to!(p.versions.first), "Couldn't revert to 1"
assert_equal 'Welcome to the weblg', p.title
assert_equal 'LockedPage', p.versions.first.version_type
end
def test_saves_versioned_copy_with_sti
p = SpecialLockedPage.create! :title => 'first title'
assert !p.new_record?
assert_equal 1, p.versions.size
assert_instance_of LockedPage.versioned_class, p.versions.first
assert_equal 'SpecialLockedPage', p.versions.first.version_type
end
def test_rollback_with_version_number_with_sti
p = locked_pages(:thinking)
assert_equal 'So I was thinking', p.title
assert p.revert_to!(p.versions.first.lock_version), "Couldn't revert to 1"
assert_equal 'So I was thinking!!!', p.title
assert_equal 'SpecialLockedPage', p.versions.first.version_type
end
def test_lock_version_works_with_versioning
p = locked_pages(:thinking)
p2 = LockedPage.find(p.id)
p.title = 'fresh title'
p.save
assert_equal 2, p.versions.size # limit!
assert_raises(ActiveRecord::StaleObjectError) do
p2.title = 'stale title'
p2.save
end
end
def test_version_if_condition
p = Page.create! :title => "title"
assert_equal 1, p.version
Page.feeling_good = false
p.save
assert_equal 1, p.version
Page.feeling_good = true
end
def test_version_if_condition2
# set new if condition
Page.class_eval do
def new_feeling_good() title[0..0] == 'a'; end
alias_method :old_feeling_good, :feeling_good?
alias_method :feeling_good?, :new_feeling_good
end
p = Page.create! :title => "title"
assert_equal 1, p.version # version does not increment
assert_equal 1, p.versions.count
p.update_attributes(:title => 'new title')
assert_equal 1, p.version # version does not increment
assert_equal 1, p.versions.count
p.update_attributes(:title => 'a title')
assert_equal 2, p.version
assert_equal 2, p.versions.count
# reset original if condition
Page.class_eval { alias_method :feeling_good?, :old_feeling_good }
end
def test_version_if_condition_with_block
# set new if condition
old_condition = Page.version_condition
Page.version_condition = Proc.new { |page| page.title[0..0] == 'b' }
p = Page.create! :title => "title"
assert_equal 1, p.version # version does not increment
assert_equal 1, p.versions.count
p.update_attributes(:title => 'a title')
assert_equal 1, p.version # version does not increment
assert_equal 1, p.versions.count
p.update_attributes(:title => 'b title')
assert_equal 2, p.version
assert_equal 2, p.versions.count
# reset original if condition
Page.version_condition = old_condition
end
def test_version_no_limit
p = Page.create! :title => "title", :body => 'first body'
p.save
p.save
5.times do |i|
p.title = "title#{i}"
p.save
assert_equal "title#{i}", p.title
assert_equal (i+2), p.version
end
end
def test_version_max_limit
p = LockedPage.create! :title => "title"
p.update_attributes(:title => "title1")
p.update_attributes(:title => "title2")
5.times do |i|
p.title = "title#{i}"
p.save
assert_equal "title#{i}", p.title
assert_equal (i+4), p.lock_version
assert p.versions(true).size <= 2, "locked version can only store 2 versions"
end
end
def test_track_altered_attributes_default_value
assert !Page.track_altered_attributes
assert LockedPage.track_altered_attributes
assert SpecialLockedPage.track_altered_attributes
end
def test_track_altered_attributes
p = LockedPage.create! :title => "title"
assert_equal 1, p.lock_version
assert_equal 1, p.versions(true).size
p.body = 'whoa'
assert !p.save_version?
p.save
assert_equal 2, p.lock_version # still increments version because of optimistic locking
assert_equal 1, p.versions(true).size
p.title = 'updated title'
assert p.save_version?
p.save
assert_equal 3, p.lock_version
assert_equal 1, p.versions(true).size # version 1 deleted
p.title = 'updated title!'
assert p.save_version?
p.save
assert_equal 4, p.lock_version
assert_equal 2, p.versions(true).size # version 1 deleted
end
def test_find_versions
assert_equal 1, locked_pages(:welcome).versions.find(:all, :conditions => ['title LIKE ?', '%weblog%']).size
end
def test_find_version
assert_equal page_versions(:welcome_1), pages(:welcome).versions.find_by_version(23)
end
def test_with_sequence
assert_equal 'widgets_seq', Widget.versioned_class.sequence_name
3.times { Widget.create! :name => 'new widget' }
assert_equal 3, Widget.count
assert_equal 3, Widget.versioned_class.count
end
def test_has_many_through
assert_equal [authors(:caged), authors(:mly)], pages(:welcome).authors
end
def test_has_many_through_with_custom_association
assert_equal [authors(:caged), authors(:mly)], pages(:welcome).revisors
end
def test_referential_integrity
pages(:welcome).destroy
assert_equal 0, Page.count
assert_equal 0, Page::Version.count
end
def test_association_options
association = Page.reflect_on_association(:versions)
options = association.options
assert_equal :delete_all, options[:dependent]
association = Widget.reflect_on_association(:versions)
options = association.options
assert_equal :nullify, options[:dependent]
assert_equal 'version desc', options[:order]
assert_equal 'widget_id', options[:foreign_key]
widget = Widget.create! :name => 'new widget'
assert_equal 1, Widget.count
assert_equal 1, Widget.versioned_class.count
widget.destroy
assert_equal 0, Widget.count
assert_equal 1, Widget.versioned_class.count
end
def test_versioned_records_should_belong_to_parent
page = pages(:welcome)
page_version = page.versions.last
assert_equal page, page_version.page
end
def test_unaltered_attributes
landmarks(:washington).attributes = landmarks(:washington).attributes.except("id")
assert !landmarks(:washington).changed?
end
def test_unchanged_string_attributes
landmarks(:washington).attributes = landmarks(:washington).attributes.except("id").inject({}) { |params, (key, value)| params.update(key => value.to_s) }
assert !landmarks(:washington).changed?
end
def test_should_find_earliest_version
assert_equal page_versions(:welcome_1), pages(:welcome).versions.earliest
end
def test_should_find_latest_version
assert_equal page_versions(:welcome_2), pages(:welcome).versions.latest
end
def test_should_find_previous_version
assert_equal page_versions(:welcome_1), page_versions(:welcome_2).previous
assert_equal page_versions(:welcome_1), pages(:welcome).versions.before(page_versions(:welcome_2))
end
def test_should_find_next_version
assert_equal page_versions(:welcome_2), page_versions(:welcome_1).next
assert_equal page_versions(:welcome_2), pages(:welcome).versions.after(page_versions(:welcome_1))
end
def test_should_find_version_count
assert_equal 2, pages(:welcome).versions.size
end
def test_if_changed_creates_version_if_a_listed_column_is_changed
landmarks(:washington).name = "Washington"
assert landmarks(:washington).changed?
assert landmarks(:washington).altered?
end
def test_if_changed_creates_version_if_all_listed_columns_are_changed
landmarks(:washington).name = "Washington"
landmarks(:washington).latitude = 1.0
landmarks(:washington).longitude = 1.0
assert landmarks(:washington).changed?
assert landmarks(:washington).altered?
end
def test_if_changed_does_not_create_new_version_if_unlisted_column_is_changed
landmarks(:washington).doesnt_trigger_version = "This should not trigger version"
assert landmarks(:washington).changed?
assert !landmarks(:washington).altered?
end
def test_without_locking_temporarily_disables_optimistic_locking
enabled1 = false
block_called = false
ActiveRecord::Base.lock_optimistically = true
LockedPage.without_locking do
enabled1 = ActiveRecord::Base.lock_optimistically
block_called = true
end
enabled2 = ActiveRecord::Base.lock_optimistically
assert block_called
assert !enabled1
assert enabled2
end
def test_without_locking_reverts_optimistic_locking_settings_if_block_raises_exception
assert_raises(RuntimeError) do
LockedPage.without_locking do
raise RuntimeError, "oh noes"
end
end
assert ActiveRecord::Base.lock_optimistically
end
end

@ -1,122 +0,0 @@
------------------------------------------------------------------------
r52 | sbecker | 2007-11-04 01:38:21 -0400 (Sun, 04 Nov 2007) | 3 lines
* Allow configuration of which environments the helpers should merge scripts with the Synthesis::AssetPackage.merge_environments variable.
* Refactored tests so they can all run together, and not depend on what the RAILS_ENV constant is.
* Only add file extension if it was explicitly passed in, fixes other helpers in rails.
------------------------------------------------------------------------
r51 | sbecker | 2007-10-26 16:24:48 -0400 (Fri, 26 Oct 2007) | 1 line
* Updated jsmin.rb to latest version from 2007-07-20
------------------------------------------------------------------------
r50 | sbecker | 2007-10-23 23:16:07 -0400 (Tue, 23 Oct 2007) | 1 line
Updated CHANGELOG
------------------------------------------------------------------------
r49 | sbecker | 2007-10-23 23:13:27 -0400 (Tue, 23 Oct 2007) | 1 line
* Finally committed the subdirectory patch. (Thanks James Coglan!)
------------------------------------------------------------------------
r48 | sbecker | 2007-10-15 15:10:43 -0400 (Mon, 15 Oct 2007) | 1 line
* Speed up rake tasks and remove rails environment dependencies
------------------------------------------------------------------------
r43 | sbecker | 2007-07-02 15:30:29 -0400 (Mon, 02 Jul 2007) | 1 line
* Updated the docs regarding testing.
------------------------------------------------------------------------
r42 | sbecker | 2007-07-02 15:27:00 -0400 (Mon, 02 Jul 2007) | 1 line
* For production helper test, build packages once - on first setup.
------------------------------------------------------------------------
r41 | sbecker | 2007-07-02 15:14:13 -0400 (Mon, 02 Jul 2007) | 1 line
* Put build_all in test setup and delete_all in test teardown so all tests will pass the on first run of test suite.
------------------------------------------------------------------------
r40 | sbecker | 2007-07-02 14:55:28 -0400 (Mon, 02 Jul 2007) | 1 line
* Fix quotes, add contact info
------------------------------------------------------------------------
r39 | sbecker | 2007-07-02 14:53:52 -0400 (Mon, 02 Jul 2007) | 1 line
* Add note on how to run the tests for asset packager.
------------------------------------------------------------------------
r38 | sbecker | 2007-01-25 15:36:42 -0500 (Thu, 25 Jan 2007) | 1 line
added CHANGELOG w/ subversion log entries
------------------------------------------------------------------------
r37 | sbecker | 2007-01-25 15:34:39 -0500 (Thu, 25 Jan 2007) | 1 line
updated jsmin with new version from 2007-01-23
------------------------------------------------------------------------
r35 | sbecker | 2007-01-15 19:22:16 -0500 (Mon, 15 Jan 2007) | 1 line
require synthesis/asset_package in rake tasks, as Rails 1.2 seems to necessitate
------------------------------------------------------------------------
r34 | sbecker | 2007-01-05 12:22:09 -0500 (Fri, 05 Jan 2007) | 1 line
do a require before including in action view, because when running migrations, the plugin lib files don't automatically get required, causing the include to error out
------------------------------------------------------------------------
r33 | sbecker | 2006-12-23 02:03:41 -0500 (Sat, 23 Dec 2006) | 1 line
updating readme with various tweaks
------------------------------------------------------------------------
r32 | sbecker | 2006-12-23 02:03:12 -0500 (Sat, 23 Dec 2006) | 1 line
updating readme with various tweaks
------------------------------------------------------------------------
r31 | sbecker | 2006-12-23 01:52:25 -0500 (Sat, 23 Dec 2006) | 1 line
updated readme to show how to use different media for stylesheets
------------------------------------------------------------------------
r28 | sbecker | 2006-11-27 21:02:14 -0500 (Mon, 27 Nov 2006) | 1 line
updated compute_public_path, added check for images
------------------------------------------------------------------------
r27 | sbecker | 2006-11-10 18:28:29 -0500 (Fri, 10 Nov 2006) | 1 line
tolerate extra periods in source asset names. fixed subversion revision checking to be file specific, instead of repository specific.
------------------------------------------------------------------------
r26 | sbecker | 2006-06-24 17:04:27 -0400 (Sat, 24 Jun 2006) | 1 line
convert asset_packages_yml var to a class var
------------------------------------------------------------------------
r25 | sbecker | 2006-06-24 12:37:47 -0400 (Sat, 24 Jun 2006) | 1 line
Added ability to include assets by package name. In development, include all uncompressed asset files. In production, include the single compressed asset.
------------------------------------------------------------------------
r24 | sbecker | 2006-06-19 21:57:23 -0400 (Mon, 19 Jun 2006) | 1 line
Updates to README and about.yml
------------------------------------------------------------------------
r23 | sbecker | 2006-06-19 14:55:39 -0400 (Mon, 19 Jun 2006) | 2 lines
Modifying about.yml and README
------------------------------------------------------------------------
r21 | sbecker | 2006-06-19 12:18:32 -0400 (Mon, 19 Jun 2006) | 2 lines
added "formerly known as MergeJS"
------------------------------------------------------------------------
r20 | sbecker | 2006-06-19 12:14:46 -0400 (Mon, 19 Jun 2006) | 2 lines
Updating docs
------------------------------------------------------------------------
r19 | sbecker | 2006-06-19 11:26:08 -0400 (Mon, 19 Jun 2006) | 2 lines
removing compiled test assets from subversion
------------------------------------------------------------------------
r18 | sbecker | 2006-06-19 11:19:59 -0400 (Mon, 19 Jun 2006) | 2 lines
Initial import.
------------------------------------------------------------------------
r17 | sbecker | 2006-06-19 11:18:56 -0400 (Mon, 19 Jun 2006) | 2 lines
Creating directory.
------------------------------------------------------------------------

@ -1,178 +0,0 @@
= AssetPackager
JavaScript and CSS Asset Compression for Production Rails Apps
== Description
When it comes time to deploy your new web application, instead of
sending down a dozen JavaScript and CSS files full of formatting
and comments, this Rails plugin makes it simple to merge and
compress JavaScript and CSS down into one or more files, increasing
speed and saving bandwidth.
When in development, it allows you to use your original versions
and retain formatting and comments for readability and debugging.
This code is released under the MIT license (like Ruby). You're free
to rip it up, enhance it, etc. And if you make any enhancements,
I'd like to know so I can add them back in. Thanks!
* Formerly known as MergeJS.
== Credit
This Rails Plugin was inspired by Cal Henderson's article
"Serving JavaScript Fast" on Vitamin:
http://www.thinkvitamin.com/features/webapps/serving-javascript-fast
It also uses the Ruby JavaScript Minifier created by
Douglas Crockford.
http://www.crockford.com/javascript/jsmin.html
== Key Features
* Merges and compresses JavaScript and CSS when running in production.
* Uses uncompressed originals when running in development.
* Generates packages on demand in production
== Components
* Rake tasks for managing packages
* Helper functions for including these JavaScript and CSS files in your views.
* YAML configuration file for mapping JavaScript and CSS files to packages.
* Rake Task for auto-generating the YAML file from your existing JavaScript files.
== Updates
November '08:
* Rails 2.2 compatibility fixes
* No more mucking with internal Rails functions, which means:
* Return to use of query-string timestamps. Greatly simplifies things.
* Multiple asset-hosts supported
* Filenames with "."'s in them, such as "jquery-x.x.x" are supported.
* Now compatible with any revision control system since it no longer uses revision numbers.
* Packages generated on demand in production mode. Running create_all rake task no longer necessary.
== How to Use:
1. Download and install the plugin:
./script/plugin install git://github.com/sbecker/asset_packager.git
2. Run the rake task "asset:packager:create_yml" to generate the /config/asset_packages.yml
file the first time. You will need to reorder files under 'base' so dependencies are loaded
in correct order. Feel free to rename or create new file packages.
IMPORTANT: JavaScript files can break once compressed if each statement doesn't end with a semi-colon.
The minifier puts multiple statements on one line, so if the semi-colon is missing, the statement may no
longer makes sense and cause a syntax error.
== Examples of config/asset_packages.yml
Example from a fresh rails app after running the rake task. (Stylesheets is blank because a
default rails app has no stylesheets yet.):
---
javascripts:
- base:
- prototype
- effects
- dragdrop
- controls
- application
stylesheets:
- base: []
Multiple packages:
---
javascripts:
- base:
- prototype
- effects
- controls
- dragdrop
- application
- secondary:
- foo
- bar
stylesheets:
- base:
- screen
- header
- secondary:
- foo
- bar
3. Run the rake task "asset:packager:build_all" to generate the compressed, merged versions
for each package. Whenever you rearrange the yaml file, you'll need to run this task again.
Merging and compressing is expensive, so this is something we want to do once, not every time
your app starts. Thats why its a rake task. You can run this task via Capistrano when deploying
to avoid an initially slow request the first time a page is generated.
Note: The package will be generated on the fly if it doesn't yet exist, so you don't *need*
to run the rake task when deploying, its just recommended for speeding up initial requests.
4. Use the helper functions whenever including these files in your application. See below for examples.
5. Potential warning: css compressor function currently removes CSS comments. This might blow
away some CSS hackery. To disable comment removal, comment out /lib/synthesis/asset_package.rb line 176.
== JavaScript Examples
Example call (based on above /config/asset_packages.yml):
<%= javascript_include_merged :base %>
In development, this generates:
<script type="text/javascript" src="/javascripts/prototype.js?1228027240"></script>
<script type="text/javascript" src="/javascripts/effects.js?1228027240"></script>
<script type="text/javascript" src="/javascripts/controls.js?1228027240"></script>
<script type="text/javascript" src="/javascripts/dragdrop.js?1228027240"></script>
<script type="text/javascript" src="/javascripts/application.js?1228027240"></script>
In production, this generates:
<script type="text/javascript" src="/javascripts/base_packaged.js?123456789"></script>
== Stylesheet Examples
Example call:
<%= stylesheet_link_merged :base %>
In development, this generates:
<link href="/stylesheets/screen.css?1228027240" media="screen" rel="Stylesheet" type="text/css" />
<link href="/stylesheets/header.css?1228027240" media="screen" rel="Stylesheet" type="text/css" />
In production this generates:
<link href="/stylesheets/base_packaged.css?1228027240" media="screen" rel="Stylesheet" type="text/css" />
== Different CSS Media
All options for stylesheet_link_tag still work, so if you want to specify a different media type:
<%= stylesheet_link_merged :secondary, 'media' => 'print' %>
== Rake tasks
rake asset:packager:build_all # Merge and compress assets
rake asset:packager:create_yml # Generate asset_packages.yml from existing assets
rake asset:packager:delete_all # Delete all asset builds
== Running the tests
This plugin has a full suite of tests. But since they
depend on rails, it has to be run in the context of a
rails app, in the vendor/plugins directory. Observe:
> rails newtestapp
> cd newtestapp
> ./script/plugin install ./script/plugin install git://github.com/sbecker/asset_packager.git
> rake test:plugins PLUGIN=asset_packager # all tests pass
== License
Copyright (c) 2006-2008 Scott Becker - http://synthesis.sbecker.net
Contact via Github for change requests, etc.
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.

@ -1,22 +0,0 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the asset_packager plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the asset_packager plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'AssetPackager'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end

@ -1,8 +0,0 @@
author: Scott Becker
name: AssetPackager
summary: JavaScript and CSS Asset Compression for Production Rails Apps
homepage: http://synthesis.sbecker.net/pages/asset_packager
plugin: http://sbecker.net/shared/plugins/asset_packager
license: MIT
version: 0.2
rails_version: 1.1.2+

@ -1,3 +0,0 @@
require 'synthesis/asset_package'
require 'synthesis/asset_package_helper'
ActionView::Base.send :include, Synthesis::AssetPackageHelper

@ -1 +0,0 @@
# Install hook code here

@ -1,211 +0,0 @@
#!/usr/bin/ruby
# jsmin.rb 2007-07-20
# Author: Uladzislau Latynski
# This work is a translation from C to Ruby of jsmin.c published by
# Douglas Crockford. Permission is hereby granted to use the Ruby
# version under the same conditions as the jsmin.c on which it is
# based.
#
# /* jsmin.c
# 2003-04-21
#
# Copyright (c) 2002 Douglas Crockford (www.crockford.com)
#
# 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 shall be used for Good, not Evil.
#
# 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.
# Ruby 1.9 Compatibility Fix - the below isAlphanum uses Fixnum#ord to be compatible with Ruby 1.9 and 1.8.7
# Fixnum#ord is not found by default in 1.8.6, so monkey patch it in:
if RUBY_VERSION == '1.8.6'
class Fixnum; def ord; return self; end; end
end
EOF = -1
$theA = ""
$theB = ""
# isAlphanum -- return true if the character is a letter, digit, underscore,
# dollar sign, or non-ASCII character
def isAlphanum(c)
return false if !c || c == EOF
return ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
(c >= 'A' && c <= 'Z') || c == '_' || c == '$' ||
c == '\\' || c[0].ord > 126)
end
# get -- return the next character from stdin. Watch out for lookahead. If
# the character is a control character, translate it to a space or linefeed.
def get()
c = $stdin.getc
return EOF if(!c)
c = c.chr
return c if (c >= " " || c == "\n" || c.unpack("c") == EOF)
return "\n" if (c == "\r")
return " "
end
# Get the next character without getting it.
def peek()
lookaheadChar = $stdin.getc
$stdin.ungetc(lookaheadChar)
return lookaheadChar.chr
end
# mynext -- get the next character, excluding comments.
# peek() is used to see if a '/' is followed by a '/' or '*'.
def mynext()
c = get
if (c == "/")
if(peek == "/")
while(true)
c = get
if (c <= "\n")
return c
end
end
end
if(peek == "*")
get
while(true)
case get
when "*"
if (peek == "/")
get
return " "
end
when EOF
raise "Unterminated comment"
end
end
end
end
return c
end
# action -- do something! What you do is determined by the argument: 1
# Output A. Copy B to A. Get the next B. 2 Copy B to A. Get the next B.
# (Delete A). 3 Get the next B. (Delete B). action treats a string as a
# single character. Wow! action recognizes a regular expression if it is
# preceded by ( or , or =.
def action(a)
if(a==1)
$stdout.write $theA
end
if(a==1 || a==2)
$theA = $theB
if ($theA == "\'" || $theA == "\"")
while (true)
$stdout.write $theA
$theA = get
break if ($theA == $theB)
raise "Unterminated string literal" if ($theA <= "\n")
if ($theA == "\\")
$stdout.write $theA
$theA = get
end
end
end
end
if(a==1 || a==2 || a==3)
$theB = mynext
if ($theB == "/" && ($theA == "(" || $theA == "," || $theA == "=" ||
$theA == ":" || $theA == "[" || $theA == "!" ||
$theA == "&" || $theA == "|" || $theA == "?" ||
$theA == "{" || $theA == "}" || $theA == ";" ||
$theA == "\n"))
$stdout.write $theA
$stdout.write $theB
while (true)
$theA = get
if ($theA == "/")
break
elsif ($theA == "\\")
$stdout.write $theA
$theA = get
elsif ($theA <= "\n")
raise "Unterminated RegExp Literal"
end
$stdout.write $theA
end
$theB = mynext
end
end
end
# jsmin -- Copy the input to the output, deleting the characters which are
# insignificant to JavaScript. Comments will be removed. Tabs will be
# replaced with spaces. Carriage returns will be replaced with linefeeds.
# Most spaces and linefeeds will be removed.
def jsmin
$theA = "\n"
action(3)
while ($theA != EOF)
case $theA
when " "
if (isAlphanum($theB))
action(1)
else
action(2)
end
when "\n"
case ($theB)
when "{","[","(","+","-"
action(1)
when " "
action(3)
else
if (isAlphanum($theB))
action(1)
else
action(2)
end
end
else
case ($theB)
when " "
if (isAlphanum($theA))
action(1)
else
action(3)
end
when "\n"
case ($theA)
when "}","]",")","+","-","\"","\\", "'", '"'
action(1)
else
if (isAlphanum($theA))
action(1)
else
action(3)
end
end
else
action(1)
end
end
end
end
ARGV.each do |anArg|
$stdout.write "// #{anArg}\n"
end
jsmin

@ -1,210 +0,0 @@
module Synthesis
class AssetPackage
@asset_base_path = "#{Rails.root}/public"
@asset_packages_yml = File.exists?("#{Rails.root}/config/asset_packages.yml") ? YAML.load_file("#{Rails.root}/config/asset_packages.yml") : nil
# singleton methods
class << self
attr_accessor :asset_base_path,
:asset_packages_yml
attr_writer :merge_environments
def merge_environments
@merge_environments ||= ["production"]
end
def parse_path(path)
/^(?:(.*)\/)?([^\/]+)$/.match(path).to_a
end
def find_by_type(asset_type)
asset_packages_yml[asset_type].map { |p| self.new(asset_type, p) }
end
def find_by_target(asset_type, target)
package_hash = asset_packages_yml[asset_type].find {|p| p.keys.first == target }
package_hash ? self.new(asset_type, package_hash) : nil
end
def find_by_source(asset_type, source)
path_parts = parse_path(source)
package_hash = asset_packages_yml[asset_type].find do |p|
key = p.keys.first
p[key].include?(path_parts[2]) && (parse_path(key)[1] == path_parts[1])
end
package_hash ? self.new(asset_type, package_hash) : nil
end
def targets_from_sources(asset_type, sources)
package_names = Array.new
sources.each do |source|
package = find_by_target(asset_type, source) || find_by_source(asset_type, source)
package_names << (package ? package.current_file : source)
end
package_names.uniq
end
def sources_from_targets(asset_type, targets)
source_names = Array.new
targets.each do |target|
package = find_by_target(asset_type, target)
source_names += (package ? package.sources.collect do |src|
package.target_dir.gsub(/^(.+)$/, '\1/') + src
end : target.to_a)
end
source_names.uniq
end
def build_all
asset_packages_yml.keys.each do |asset_type|
asset_packages_yml[asset_type].each { |p| self.new(asset_type, p).build }
end
end
def delete_all
asset_packages_yml.keys.each do |asset_type|
asset_packages_yml[asset_type].each { |p| self.new(asset_type, p).delete_previous_build }
end
end
def create_yml
unless File.exists?("#{Rails.root}/config/asset_packages.yml")
asset_yml = Hash.new
asset_yml['javascripts'] = [{"base" => build_file_list("#{Rails.root}/public/javascripts", "js")}]
asset_yml['stylesheets'] = [{"base" => build_file_list("#{Rails.root}/public/stylesheets", "css")}]
File.open("#{Rails.root}/config/asset_packages.yml", "w") do |out|
YAML.dump(asset_yml, out)
end
log "config/asset_packages.yml example file created!"
log "Please reorder files under 'base' so dependencies are loaded in correct order."
else
log "config/asset_packages.yml already exists. Aborting task..."
end
end
end
# instance methods
attr_accessor :asset_type, :target, :target_dir, :sources
def initialize(asset_type, package_hash)
target_parts = self.class.parse_path(package_hash.keys.first)
@target_dir = target_parts[1].to_s
@target = target_parts[2].to_s
@sources = package_hash[package_hash.keys.first]
@asset_type = asset_type
@asset_path = "#{self.class.asset_base_path}/#{@asset_type}#{@target_dir.gsub(/^(.+)$/, '/\1')}"
@extension = get_extension
@file_name = "#{@target}_packaged.#{@extension}"
@full_path = File.join(@asset_path, @file_name)
end
def package_exists?
File.exists?(@full_path)
end
def current_file
build unless package_exists?
path = @target_dir.gsub(/^(.+)$/, '\1/')
"#{path}#{@target}_packaged"
end
def build
delete_previous_build
create_new_build
end
def delete_previous_build
File.delete(@full_path) if File.exists?(@full_path)
end
private
def create_new_build
new_build_path = "#{@asset_path}/#{@target}_packaged.#{@extension}"
if File.exists?(new_build_path)
log "Latest version already exists: #{new_build_path}"
else
File.open(new_build_path, "w") {|f| f.write(compressed_file) }
log "Created #{new_build_path}"
end
end
def merged_file
merged_file = ""
@sources.each {|s|
File.open("#{@asset_path}/#{s}.#{@extension}", "r") { |f|
merged_file += f.read + "\n"
}
}
merged_file
end
def compressed_file
case @asset_type
when "javascripts" then compress_js(merged_file)
when "stylesheets" then compress_css(merged_file)
end
end
def compress_js(source)
jsmin_path = "#{Rails.root}/vendor/plugins/asset_packager/lib"
tmp_path = "#{Rails.root}/tmp/#{@target}_packaged"
# write out to a temp file
File.open("#{tmp_path}_uncompressed.js", "w") {|f| f.write(source) }
# compress file with JSMin library
`ruby #{jsmin_path}/jsmin.rb <#{tmp_path}_uncompressed.js >#{tmp_path}_compressed.js \n`
# read it back in and trim it
result = ""
File.open("#{tmp_path}_compressed.js", "r") { |f| result += f.read.strip }
# delete temp files if they exist
File.delete("#{tmp_path}_uncompressed.js") if File.exists?("#{tmp_path}_uncompressed.js")
File.delete("#{tmp_path}_compressed.js") if File.exists?("#{tmp_path}_compressed.js")
result
end
def compress_css(source)
source.gsub!(/\s+/, " ") # collapse space
source.gsub!(/\/\*(.*?)\*\//, "") # remove comments - caution, might want to remove this if using css hacks
source.gsub!(/\} /, "}\n") # add line breaks
source.gsub!(/\n$/, "") # remove last break
source.gsub!(/ \{ /, " {") # trim inside brackets
source.gsub!(/; \}/, "}") # trim inside brackets
source
end
def get_extension
case @asset_type
when "javascripts" then "js"
when "stylesheets" then "css"
end
end
def log(message)
self.class.log(message)
end
def self.log(message)
puts message
end
def self.build_file_list(path, extension)
re = Regexp.new(".#{extension}\\z")
file_list = Dir.new(path).entries.delete_if { |x| ! (x =~ re) }.map {|x| x.chomp(".#{extension}")}
# reverse javascript entries so prototype comes first on a base rails app
file_list.reverse! if extension == "js"
file_list
end
end
end

@ -1,39 +0,0 @@
module Synthesis
module AssetPackageHelper
def should_merge?
AssetPackage.merge_environments.include?(Rails.env)
end
def javascript_include_merged(*sources)
options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { }
if sources.include?(:defaults)
sources = sources[0..(sources.index(:defaults))] +
['prototype', 'effects', 'dragdrop', 'controls'] +
(File.exists?("#{Rails.root}/public/javascripts/application.js") ? ['application'] : []) +
sources[(sources.index(:defaults) + 1)..sources.length]
sources.delete(:defaults)
end
sources.collect!{|s| s.to_s}
sources = (should_merge? ?
AssetPackage.targets_from_sources("javascripts", sources) :
AssetPackage.sources_from_targets("javascripts", sources))
sources.collect {|source| javascript_include_tag(source, options) }.join("\n")
end
def stylesheet_link_merged(*sources)
options = sources.last.is_a?(Hash) ? sources.pop.stringify_keys : { }
sources.collect!{|s| s.to_s}
sources = (should_merge? ?
AssetPackage.targets_from_sources("stylesheets", sources) :
AssetPackage.sources_from_targets("stylesheets", sources))
sources.collect { |source| stylesheet_link_tag(source, options) }.join("\n")
end
end
end

@ -1,23 +0,0 @@
require 'yaml'
require File.dirname(__FILE__) + '/../../lib/synthesis/asset_package'
namespace :asset do
namespace :packager do
desc "Merge and compress assets"
task :build_all do
Synthesis::AssetPackage.build_all
end
desc "Delete all asset builds"
task :delete_all do
Synthesis::AssetPackage.delete_all
end
desc "Generate asset_packages.yml from existing assets"
task :create_yml do
Synthesis::AssetPackage.create_yml
end
end
end

@ -1,102 +0,0 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
ENV['RAILS_ENV'] = "development"
require File.dirname(__FILE__) + '/../../../../config/environment'
require 'test/unit'
require 'rubygems'
require 'mocha'
require 'action_controller/test_process'
ActionController::Base.logger = nil
ActionController::Routing::Routes.reload rescue nil
class AssetPackageHelperDevelopmentTest < Test::Unit::TestCase
include ActionController::Assertions::DomAssertions
include ActionController::TestCase::Assertions
include ActionView::Helpers::TagHelper
include ActionView::Helpers::AssetTagHelper
include Synthesis::AssetPackageHelper
def setup
Synthesis::AssetPackage.asset_base_path = "#{Rails.root}/vendor/plugins/asset_packager/test/assets"
Synthesis::AssetPackage.asset_packages_yml = YAML.load_file("#{Rails.root}/vendor/plugins/asset_packager/test/asset_packages.yml")
Synthesis::AssetPackage.any_instance.stubs(:log)
@controller = Class.new do
def request
@request ||= ActionController::TestRequest.new
end
end.new
end
def build_js_expected_string(*sources)
sources.map {|s| javascript_include_tag(s) }.join("\n")
end
def build_css_expected_string(*sources)
sources.map {|s| stylesheet_link_tag(s) }.join("\n")
end
def test_js_basic
assert_dom_equal build_js_expected_string("prototype"),
javascript_include_merged("prototype")
end
def test_js_multiple_packages
assert_dom_equal build_js_expected_string("prototype", "foo"),
javascript_include_merged("prototype", "foo")
end
def test_js_unpackaged_file
assert_dom_equal build_js_expected_string("prototype", "foo", "not_part_of_a_package"),
javascript_include_merged("prototype", "foo", "not_part_of_a_package")
end
def test_js_multiple_from_same_package
assert_dom_equal build_js_expected_string("prototype", "effects", "controls", "not_part_of_a_package", "foo"),
javascript_include_merged("prototype", "effects", "controls", "not_part_of_a_package", "foo")
end
def test_js_by_package_name
assert_dom_equal build_js_expected_string("prototype", "effects", "controls", "dragdrop"),
javascript_include_merged(:base)
end
def test_js_multiple_package_names
assert_dom_equal build_js_expected_string("prototype", "effects", "controls", "dragdrop", "foo", "bar", "application"),
javascript_include_merged(:base, :secondary)
end
def test_css_basic
assert_dom_equal build_css_expected_string("screen"),
stylesheet_link_merged("screen")
end
def test_css_multiple_packages
assert_dom_equal build_css_expected_string("screen", "foo", "subdir/bar"),
stylesheet_link_merged("screen", "foo", "subdir/bar")
end
def test_css_unpackaged_file
assert_dom_equal build_css_expected_string("screen", "foo", "not_part_of_a_package", "subdir/bar"),
stylesheet_link_merged("screen", "foo", "not_part_of_a_package", "subdir/bar")
end
def test_css_multiple_from_same_package
assert_dom_equal build_css_expected_string("screen", "header", "not_part_of_a_package", "foo", "bar", "subdir/foo", "subdir/bar"),
stylesheet_link_merged("screen", "header", "not_part_of_a_package", "foo", "bar", "subdir/foo", "subdir/bar")
end
def test_css_by_package_name
assert_dom_equal build_css_expected_string("screen", "header"),
stylesheet_link_merged(:base)
end
def test_css_multiple_package_names
assert_dom_equal build_css_expected_string("screen", "header", "foo", "bar", "subdir/foo", "subdir/bar"),
stylesheet_link_merged(:base, :secondary, "subdir/styles")
end
end

@ -1,142 +0,0 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
require File.dirname(__FILE__) + '/../../../../config/environment'
require 'test/unit'
require 'rubygems'
require 'mocha'
require 'action_controller/test_process'
ActionController::Base.logger = nil
ActionController::Routing::Routes.reload rescue nil
class AssetPackageHelperProductionTest < Test::Unit::TestCase
include ActionController::Assertions::DomAssertions
include ActionController::TestCase::Assertions
include ActionView::Helpers::TagHelper
include ActionView::Helpers::AssetTagHelper
include Synthesis::AssetPackageHelper
cattr_accessor :packages_built
def setup
Synthesis::AssetPackage.asset_base_path = "#{Rails.root}/vendor/plugins/asset_packager/test/assets"
Synthesis::AssetPackage.asset_packages_yml = YAML.load_file("#{Rails.root}/vendor/plugins/asset_packager/test/asset_packages.yml")
Synthesis::AssetPackage.any_instance.stubs(:log)
self.stubs(:should_merge?).returns(true)
@controller = Class.new do
def request
@request ||= ActionController::TestRequest.new
end
end.new
build_packages_once
end
def build_packages_once
unless @@packages_built
Synthesis::AssetPackage.build_all
@@packages_built = true
end
end
def build_js_expected_string(*sources)
sources.map {|s| javascript_include_tag(s) }.join("\n")
end
def build_css_expected_string(*sources)
sources.map {|s| stylesheet_link_tag(s) }.join("\n")
end
def test_js_basic
current_file = Synthesis::AssetPackage.find_by_source("javascripts", "prototype").current_file
assert_dom_equal build_js_expected_string(current_file),
javascript_include_merged("prototype")
end
def test_js_multiple_packages
current_file1 = Synthesis::AssetPackage.find_by_source("javascripts", "prototype").current_file
current_file2 = Synthesis::AssetPackage.find_by_source("javascripts", "foo").current_file
assert_dom_equal build_js_expected_string(current_file1, current_file2),
javascript_include_merged("prototype", "foo")
end
def test_js_unpackaged_file
current_file1 = Synthesis::AssetPackage.find_by_source("javascripts", "prototype").current_file
current_file2 = Synthesis::AssetPackage.find_by_source("javascripts", "foo").current_file
assert_dom_equal build_js_expected_string(current_file1, current_file2, "not_part_of_a_package"),
javascript_include_merged("prototype", "foo", "not_part_of_a_package")
end
def test_js_multiple_from_same_package
current_file1 = Synthesis::AssetPackage.find_by_source("javascripts", "prototype").current_file
current_file2 = Synthesis::AssetPackage.find_by_source("javascripts", "foo").current_file
assert_dom_equal build_js_expected_string(current_file1, "not_part_of_a_package", current_file2),
javascript_include_merged("prototype", "effects", "controls", "not_part_of_a_package", "foo")
end
def test_js_by_package_name
package_name = Synthesis::AssetPackage.find_by_target("javascripts", "base").current_file
assert_dom_equal build_js_expected_string(package_name),
javascript_include_merged(:base)
end
def test_js_multiple_package_names
package_name1 = Synthesis::AssetPackage.find_by_target("javascripts", "base").current_file
package_name2 = Synthesis::AssetPackage.find_by_target("javascripts", "secondary").current_file
assert_dom_equal build_js_expected_string(package_name1, package_name2),
javascript_include_merged(:base, :secondary)
end
def test_css_basic
current_file = Synthesis::AssetPackage.find_by_source("stylesheets", "screen").current_file
assert_dom_equal build_css_expected_string(current_file),
stylesheet_link_merged("screen")
end
def test_css_multiple_packages
current_file1 = Synthesis::AssetPackage.find_by_source("stylesheets", "screen").current_file
current_file2 = Synthesis::AssetPackage.find_by_source("stylesheets", "foo").current_file
current_file3 = Synthesis::AssetPackage.find_by_source("stylesheets", "subdir/bar").current_file
assert_dom_equal build_css_expected_string(current_file1, current_file2, current_file3),
stylesheet_link_merged("screen", "foo", "subdir/bar")
end
def test_css_unpackaged_file
current_file1 = Synthesis::AssetPackage.find_by_source("stylesheets", "screen").current_file
current_file2 = Synthesis::AssetPackage.find_by_source("stylesheets", "foo").current_file
assert_dom_equal build_css_expected_string(current_file1, current_file2, "not_part_of_a_package"),
stylesheet_link_merged("screen", "foo", "not_part_of_a_package")
end
def test_css_multiple_from_same_package
current_file1 = Synthesis::AssetPackage.find_by_source("stylesheets", "screen").current_file
current_file2 = Synthesis::AssetPackage.find_by_source("stylesheets", "foo").current_file
current_file3 = Synthesis::AssetPackage.find_by_source("stylesheets", "subdir/bar").current_file
assert_dom_equal build_css_expected_string(current_file1, "not_part_of_a_package", current_file2, current_file3),
stylesheet_link_merged("screen", "header", "not_part_of_a_package", "foo", "bar", "subdir/foo", "subdir/bar")
end
def test_css_by_package_name
package_name = Synthesis::AssetPackage.find_by_target("stylesheets", "base").current_file
assert_dom_equal build_css_expected_string(package_name),
stylesheet_link_merged(:base)
end
def test_css_multiple_package_names
package_name1 = Synthesis::AssetPackage.find_by_target("stylesheets", "base").current_file
package_name2 = Synthesis::AssetPackage.find_by_target("stylesheets", "secondary").current_file
package_name3 = Synthesis::AssetPackage.find_by_target("stylesheets", "subdir/styles").current_file
assert_dom_equal build_css_expected_string(package_name1, package_name2, package_name3),
stylesheet_link_merged(:base, :secondary, "subdir/styles")
end
end

@ -1,91 +0,0 @@
require File.dirname(__FILE__) + '/../../../../config/environment'
require 'test/unit'
require 'mocha'
class AssetPackagerTest < Test::Unit::TestCase
include Synthesis
def setup
Synthesis::AssetPackage.asset_base_path = "#{Rails.root}/vendor/plugins/asset_packager/test/assets"
Synthesis::AssetPackage.asset_packages_yml = YAML.load_file("#{Rails.root}/vendor/plugins/asset_packager/test/asset_packages.yml")
Synthesis::AssetPackage.any_instance.stubs(:log)
Synthesis::AssetPackage.build_all
end
def teardown
Synthesis::AssetPackage.delete_all
end
def test_find_by_type
js_asset_packages = Synthesis::AssetPackage.find_by_type("javascripts")
assert_equal 2, js_asset_packages.length
assert_equal "base", js_asset_packages[0].target
assert_equal ["prototype", "effects", "controls", "dragdrop"], js_asset_packages[0].sources
end
def test_find_by_target
package = Synthesis::AssetPackage.find_by_target("javascripts", "base")
assert_equal "base", package.target
assert_equal ["prototype", "effects", "controls", "dragdrop"], package.sources
end
def test_find_by_source
package = Synthesis::AssetPackage.find_by_source("javascripts", "controls")
assert_equal "base", package.target
assert_equal ["prototype", "effects", "controls", "dragdrop"], package.sources
end
def test_delete_and_build
Synthesis::AssetPackage.delete_all
js_package_names = Dir.new("#{Synthesis::AssetPackage.asset_base_path}/javascripts").entries.delete_if { |x| ! (x =~ /\A\w+_packaged.js/) }
css_package_names = Dir.new("#{Synthesis::AssetPackage.asset_base_path}/stylesheets").entries.delete_if { |x| ! (x =~ /\A\w+_packaged.css/) }
css_subdir_package_names = Dir.new("#{Synthesis::AssetPackage.asset_base_path}/stylesheets/subdir").entries.delete_if { |x| ! (x =~ /\A\w+_packaged.css/) }
assert_equal 0, js_package_names.length
assert_equal 0, css_package_names.length
assert_equal 0, css_subdir_package_names.length
Synthesis::AssetPackage.build_all
js_package_names = Dir.new("#{Synthesis::AssetPackage.asset_base_path}/javascripts").entries.delete_if { |x| ! (x =~ /\A\w+_packaged.js/) }.sort
css_package_names = Dir.new("#{Synthesis::AssetPackage.asset_base_path}/stylesheets").entries.delete_if { |x| ! (x =~ /\A\w+_packaged.css/) }.sort
css_subdir_package_names = Dir.new("#{Synthesis::AssetPackage.asset_base_path}/stylesheets/subdir").entries.delete_if { |x| ! (x =~ /\A\w+_packaged.css/) }.sort
assert_equal 2, js_package_names.length
assert_equal 2, css_package_names.length
assert_equal 1, css_subdir_package_names.length
assert js_package_names[0].match(/\Abase_packaged.js\z/)
assert js_package_names[1].match(/\Asecondary_packaged.js\z/)
assert css_package_names[0].match(/\Abase_packaged.css\z/)
assert css_package_names[1].match(/\Asecondary_packaged.css\z/)
assert css_subdir_package_names[0].match(/\Astyles_packaged.css\z/)
end
def test_js_names_from_sources
package_names = Synthesis::AssetPackage.targets_from_sources("javascripts", ["prototype", "effects", "noexist1", "controls", "foo", "noexist2"])
assert_equal 4, package_names.length
assert package_names[0].match(/\Abase_packaged\z/)
assert_equal package_names[1], "noexist1"
assert package_names[2].match(/\Asecondary_packaged\z/)
assert_equal package_names[3], "noexist2"
end
def test_css_names_from_sources
package_names = Synthesis::AssetPackage.targets_from_sources("stylesheets", ["header", "screen", "noexist1", "foo", "noexist2"])
assert_equal 4, package_names.length
assert package_names[0].match(/\Abase_packaged\z/)
assert_equal package_names[1], "noexist1"
assert package_names[2].match(/\Asecondary_packaged\z/)
assert_equal package_names[3], "noexist2"
end
def test_should_return_merge_environments_when_set
Synthesis::AssetPackage.merge_environments = ["staging", "production"]
assert_equal ["staging", "production"], Synthesis::AssetPackage.merge_environments
end
def test_should_only_return_production_merge_environment_when_not_set
assert_equal ["production"], Synthesis::AssetPackage.merge_environments
end
end

@ -1,20 +0,0 @@
javascripts:
- base:
- prototype
- effects
- controls
- dragdrop
- secondary:
- foo
- bar
- application
stylesheets:
- base:
- screen
- header
- secondary:
- foo
- bar
- subdir/styles:
- foo
- bar

@ -1,2 +0,0 @@
// Place your application-specific JavaScript functions and classes here
// This file is automatically included by javascript_include_tag :defaults

@ -1,4 +0,0 @@
bar bar bar
bar bar bar
bar bar bar

@ -1,815 +0,0 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
// (c) 2005 Jon Tirsen (http://www.tirsen.com)
// Contributors:
// Richard Livsey
// Rahul Bhargava
// Rob Wills
//
// See scriptaculous.js for full license.
// Autocompleter.Base handles all the autocompletion functionality
// that's independent of the data source for autocompletion. This
// includes drawing the autocompletion menu, observing keyboard
// and mouse events, and similar.
//
// Specific autocompleters need to provide, at the very least,
// a getUpdatedChoices function that will be invoked every time
// the text inside the monitored textbox changes. This method
// should get the text for which to provide autocompletion by
// invoking this.getToken(), NOT by directly accessing
// this.element.value. This is to allow incremental tokenized
// autocompletion. Specific auto-completion logic (AJAX, etc)
// belongs in getUpdatedChoices.
//
// Tokenized incremental autocompletion is enabled automatically
// when an autocompleter is instantiated with the 'tokens' option
// in the options parameter, e.g.:
// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
// will incrementally autocomplete with a comma as the token.
// Additionally, ',' in the above example can be replaced with
// a token array, e.g. { tokens: [',', '\n'] } which
// enables autocompletion on multiple tokens. This is most
// useful when one of the tokens is \n (a newline), as it
// allows smart autocompletion after linebreaks.
var Autocompleter = {}
Autocompleter.Base = function() {};
Autocompleter.Base.prototype = {
baseInitialize: function(element, update, options) {
this.element = $(element);
this.update = $(update);
this.hasFocus = false;
this.changed = false;
this.active = false;
this.index = 0;
this.entryCount = 0;
if (this.setOptions)
this.setOptions(options);
else
this.options = options || {};
this.options.paramName = this.options.paramName || this.element.name;
this.options.tokens = this.options.tokens || [];
this.options.frequency = this.options.frequency || 0.4;
this.options.minChars = this.options.minChars || 1;
this.options.onShow = this.options.onShow ||
function(element, update){
if(!update.style.position || update.style.position=='absolute') {
update.style.position = 'absolute';
Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
}
Effect.Appear(update,{duration:0.15});
};
this.options.onHide = this.options.onHide ||
function(element, update){ new Effect.Fade(update,{duration:0.15}) };
if (typeof(this.options.tokens) == 'string')
this.options.tokens = new Array(this.options.tokens);
this.observer = null;
this.element.setAttribute('autocomplete','off');
Element.hide(this.update);
Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
},
show: function() {
if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
if(!this.iefix &&
(navigator.appVersion.indexOf('MSIE')>0) &&
(navigator.userAgent.indexOf('Opera')<0) &&
(Element.getStyle(this.update, 'position')=='absolute')) {
new Insertion.After(this.update,
'<iframe id="' + this.update.id + '_iefix" '+
'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
this.iefix = $(this.update.id+'_iefix');
}
if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
},
fixIEOverlapping: function() {
Position.clone(this.update, this.iefix);
this.iefix.style.zIndex = 1;
this.update.style.zIndex = 2;
Element.show(this.iefix);
},
hide: function() {
this.stopIndicator();
if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
if(this.iefix) Element.hide(this.iefix);
},
startIndicator: function() {
if(this.options.indicator) Element.show(this.options.indicator);
},
stopIndicator: function() {
if(this.options.indicator) Element.hide(this.options.indicator);
},
onKeyPress: function(event) {
if(this.active)
switch(event.keyCode) {
case Event.KEY_TAB:
case Event.KEY_RETURN:
this.selectEntry();
Event.stop(event);
case Event.KEY_ESC:
this.hide();
this.active = false;
Event.stop(event);
return;
case Event.KEY_LEFT:
case Event.KEY_RIGHT:
return;
case Event.KEY_UP:
this.markPrevious();
this.render();
if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
return;
case Event.KEY_DOWN:
this.markNext();
this.render();
if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
return;
}
else
if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN ||
(navigator.appVersion.indexOf('AppleWebKit') > 0 && event.keyCode == 0)) return;
this.changed = true;
this.hasFocus = true;
if(this.observer) clearTimeout(this.observer);
this.observer =
setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
},
activate: function() {
this.changed = false;
this.hasFocus = true;
this.getUpdatedChoices();
},
onHover: function(event) {
var element = Event.findElement(event, 'LI');
if(this.index != element.autocompleteIndex)
{
this.index = element.autocompleteIndex;
this.render();
}
Event.stop(event);
},
onClick: function(event) {
var element = Event.findElement(event, 'LI');
this.index = element.autocompleteIndex;
this.selectEntry();
this.hide();
},
onBlur: function(event) {
// needed to make click events working
setTimeout(this.hide.bind(this), 250);
this.hasFocus = false;
this.active = false;
},
render: function() {
if(this.entryCount > 0) {
for (var i = 0; i < this.entryCount; i++)
this.index==i ?
Element.addClassName(this.getEntry(i),"selected") :
Element.removeClassName(this.getEntry(i),"selected");
if(this.hasFocus) {
this.show();
this.active = true;
}
} else {
this.active = false;
this.hide();
}
},
markPrevious: function() {
if(this.index > 0) this.index--
else this.index = this.entryCount-1;
},
markNext: function() {
if(this.index < this.entryCount-1) this.index++
else this.index = 0;
},
getEntry: function(index) {
return this.update.firstChild.childNodes[index];
},
getCurrentEntry: function() {
return this.getEntry(this.index);
},
selectEntry: function() {
this.active = false;
this.updateElement(this.getCurrentEntry());
},
updateElement: function(selectedElement) {
if (this.options.updateElement) {
this.options.updateElement(selectedElement);
return;
}
var value = '';
if (this.options.select) {
var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
} else
value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
var lastTokenPos = this.findLastToken();
if (lastTokenPos != -1) {
var newValue = this.element.value.substr(0, lastTokenPos + 1);
var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
if (whitespace)
newValue += whitespace[0];
this.element.value = newValue + value;
} else {
this.element.value = value;
}
this.element.focus();
if (this.options.afterUpdateElement)
this.options.afterUpdateElement(this.element, selectedElement);
},
updateChoices: function(choices) {
if(!this.changed && this.hasFocus) {
this.update.innerHTML = choices;
Element.cleanWhitespace(this.update);
Element.cleanWhitespace(this.update.firstChild);
if(this.update.firstChild && this.update.firstChild.childNodes) {
this.entryCount =
this.update.firstChild.childNodes.length;
for (var i = 0; i < this.entryCount; i++) {
var entry = this.getEntry(i);
entry.autocompleteIndex = i;
this.addObservers(entry);
}
} else {
this.entryCount = 0;
}
this.stopIndicator();
this.index = 0;
this.render();
}
},
addObservers: function(element) {
Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
Event.observe(element, "click", this.onClick.bindAsEventListener(this));
},
onObserverEvent: function() {
this.changed = false;
if(this.getToken().length>=this.options.minChars) {
this.startIndicator();
this.getUpdatedChoices();
} else {
this.active = false;
this.hide();
}
},
getToken: function() {
var tokenPos = this.findLastToken();
if (tokenPos != -1)
var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
else
var ret = this.element.value;
return /\n/.test(ret) ? '' : ret;
},
findLastToken: function() {
var lastTokenPos = -1;
for (var i=0; i<this.options.tokens.length; i++) {
var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
if (thisTokenPos > lastTokenPos)
lastTokenPos = thisTokenPos;
}
return lastTokenPos;
}
}
Ajax.Autocompleter = Class.create();
Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
initialize: function(element, update, url, options) {
this.baseInitialize(element, update, options);
this.options.asynchronous = true;
this.options.onComplete = this.onComplete.bind(this);
this.options.defaultParams = this.options.parameters || null;
this.url = url;
},
getUpdatedChoices: function() {
entry = encodeURIComponent(this.options.paramName) + '=' +
encodeURIComponent(this.getToken());
this.options.parameters = this.options.callback ?
this.options.callback(this.element, entry) : entry;
if(this.options.defaultParams)
this.options.parameters += '&' + this.options.defaultParams;
new Ajax.Request(this.url, this.options);
},
onComplete: function(request) {
this.updateChoices(request.responseText);
}
});
// The local array autocompleter. Used when you'd prefer to
// inject an array of autocompletion options into the page, rather
// than sending out Ajax queries, which can be quite slow sometimes.
//
// The constructor takes four parameters. The first two are, as usual,
// the id of the monitored textbox, and id of the autocompletion menu.
// The third is the array you want to autocomplete from, and the fourth
// is the options block.
//
// Extra local autocompletion options:
// - choices - How many autocompletion choices to offer
//
// - partialSearch - If false, the autocompleter will match entered
// text only at the beginning of strings in the
// autocomplete array. Defaults to true, which will
// match text at the beginning of any *word* in the
// strings in the autocomplete array. If you want to
// search anywhere in the string, additionally set
// the option fullSearch to true (default: off).
//
// - fullSsearch - Search anywhere in autocomplete array strings.
//
// - partialChars - How many characters to enter before triggering
// a partial match (unlike minChars, which defines
// how many characters are required to do any match
// at all). Defaults to 2.
//
// - ignoreCase - Whether to ignore case when autocompleting.
// Defaults to true.
//
// It's possible to pass in a custom function as the 'selector'
// option, if you prefer to write your own autocompletion logic.
// In that case, the other options above will not apply unless
// you support them.
Autocompleter.Local = Class.create();
Autocompleter.Local.prototype = Object.extend(new Autocompleter.Base(), {
initialize: function(element, update, array, options) {
this.baseInitialize(element, update, options);
this.options.array = array;
},
getUpdatedChoices: function() {
this.updateChoices(this.options.selector(this));
},
setOptions: function(options) {
this.options = Object.extend({
choices: 10,
partialSearch: true,
partialChars: 2,
ignoreCase: true,
fullSearch: false,
selector: function(instance) {
var ret = []; // Beginning matches
var partial = []; // Inside matches
var entry = instance.getToken();
var count = 0;
for (var i = 0; i < instance.options.array.length &&
ret.length < instance.options.choices ; i++) {
var elem = instance.options.array[i];
var foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase()) :
elem.indexOf(entry);
while (foundPos != -1) {
if (foundPos == 0 && elem.length != entry.length) {
ret.push("<li><strong>" + elem.substr(0, entry.length) + "</strong>" +
elem.substr(entry.length) + "</li>");
break;
} else if (entry.length >= instance.options.partialChars &&
instance.options.partialSearch && foundPos != -1) {
if (instance.options.fullSearch || /\s/.test(elem.substr(foundPos-1,1))) {
partial.push("<li>" + elem.substr(0, foundPos) + "<strong>" +
elem.substr(foundPos, entry.length) + "</strong>" + elem.substr(
foundPos + entry.length) + "</li>");
break;
}
}
foundPos = instance.options.ignoreCase ?
elem.toLowerCase().indexOf(entry.toLowerCase(), foundPos + 1) :
elem.indexOf(entry, foundPos + 1);
}
}
if (partial.length)
ret = ret.concat(partial.slice(0, instance.options.choices - ret.length))
return "<ul>" + ret.join('') + "</ul>";
}
}, options || {});
}
});
// AJAX in-place editor
//
// see documentation on http://wiki.script.aculo.us/scriptaculous/show/Ajax.InPlaceEditor
// Use this if you notice weird scrolling problems on some browsers,
// the DOM might be a bit confused when this gets called so do this
// waits 1 ms (with setTimeout) until it does the activation
Field.scrollFreeActivate = function(field) {
setTimeout(function() {
Field.activate(field);
}, 1);
}
Ajax.InPlaceEditor = Class.create();
Ajax.InPlaceEditor.defaultHighlightColor = "#FFFF99";
Ajax.InPlaceEditor.prototype = {
initialize: function(element, url, options) {
this.url = url;
this.element = $(element);
this.options = Object.extend({
okButton: true,
okText: "ok",
cancelLink: true,
cancelText: "cancel",
savingText: "Saving...",
clickToEditText: "Click to edit",
okText: "ok",
rows: 1,
onComplete: function(transport, element) {
new Effect.Highlight(element, {startcolor: this.options.highlightcolor});
},
onFailure: function(transport) {
alert("Error communicating with the server: " + transport.responseText.stripTags());
},
callback: function(form) {
return Form.serialize(form);
},
handleLineBreaks: true,
loadingText: 'Loading...',
savingClassName: 'inplaceeditor-saving',
loadingClassName: 'inplaceeditor-loading',
formClassName: 'inplaceeditor-form',
highlightcolor: Ajax.InPlaceEditor.defaultHighlightColor,
highlightendcolor: "#FFFFFF",
externalControl: null,
submitOnBlur: false,
ajaxOptions: {},
evalScripts: false
}, options || {});
if(!this.options.formId && this.element.id) {
this.options.formId = this.element.id + "-inplaceeditor";
if ($(this.options.formId)) {
// there's already a form with that name, don't specify an id
this.options.formId = null;
}
}
if (this.options.externalControl) {
this.options.externalControl = $(this.options.externalControl);
}
this.originalBackground = Element.getStyle(this.element, 'background-color');
if (!this.originalBackground) {
this.originalBackground = "transparent";
}
this.element.title = this.options.clickToEditText;
this.onclickListener = this.enterEditMode.bindAsEventListener(this);
this.mouseoverListener = this.enterHover.bindAsEventListener(this);
this.mouseoutListener = this.leaveHover.bindAsEventListener(this);
Event.observe(this.element, 'click', this.onclickListener);
Event.observe(this.element, 'mouseover', this.mouseoverListener);
Event.observe(this.element, 'mouseout', this.mouseoutListener);
if (this.options.externalControl) {
Event.observe(this.options.externalControl, 'click', this.onclickListener);
Event.observe(this.options.externalControl, 'mouseover', this.mouseoverListener);
Event.observe(this.options.externalControl, 'mouseout', this.mouseoutListener);
}
},
enterEditMode: function(evt) {
if (this.saving) return;
if (this.editing) return;
this.editing = true;
this.onEnterEditMode();
if (this.options.externalControl) {
Element.hide(this.options.externalControl);
}
Element.hide(this.element);
this.createForm();
this.element.parentNode.insertBefore(this.form, this.element);
Field.scrollFreeActivate(this.editField);
// stop the event to avoid a page refresh in Safari
if (evt) {
Event.stop(evt);
}
return false;
},
createForm: function() {
this.form = document.createElement("form");
this.form.id = this.options.formId;
Element.addClassName(this.form, this.options.formClassName)
this.form.onsubmit = this.onSubmit.bind(this);
this.createEditField();
if (this.options.textarea) {
var br = document.createElement("br");
this.form.appendChild(br);
}
if (this.options.okButton) {
okButton = document.createElement("input");
okButton.type = "submit";
okButton.value = this.options.okText;
okButton.className = 'editor_ok_button';
this.form.appendChild(okButton);
}
if (this.options.cancelLink) {
cancelLink = document.createElement("a");
cancelLink.href = "#";
cancelLink.appendChild(document.createTextNode(this.options.cancelText));
cancelLink.onclick = this.onclickCancel.bind(this);
cancelLink.className = 'editor_cancel';
this.form.appendChild(cancelLink);
}
},
hasHTMLLineBreaks: function(string) {
if (!this.options.handleLineBreaks) return false;
return string.match(/<br/i) || string.match(/<p>/i);
},
convertHTMLLineBreaks: function(string) {
return string.replace(/<br>/gi, "\n").replace(/<br\/>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<p>/gi, "");
},
createEditField: function() {
var text;
if(this.options.loadTextURL) {
text = this.options.loadingText;
} else {
text = this.getText();
}
var obj = this;
if (this.options.rows == 1 && !this.hasHTMLLineBreaks(text)) {
this.options.textarea = false;
var textField = document.createElement("input");
textField.obj = this;
textField.type = "text";
textField.name = "value";
textField.value = text;
textField.style.backgroundColor = this.options.highlightcolor;
textField.className = 'editor_field';
var size = this.options.size || this.options.cols || 0;
if (size != 0) textField.size = size;
if (this.options.submitOnBlur)
textField.onblur = this.onSubmit.bind(this);
this.editField = textField;
} else {
this.options.textarea = true;
var textArea = document.createElement("textarea");
textArea.obj = this;
textArea.name = "value";
textArea.value = this.convertHTMLLineBreaks(text);
textArea.rows = this.options.rows;
textArea.cols = this.options.cols || 40;
textArea.className = 'editor_field';
if (this.options.submitOnBlur)
textArea.onblur = this.onSubmit.bind(this);
this.editField = textArea;
}
if(this.options.loadTextURL) {
this.loadExternalText();
}
this.form.appendChild(this.editField);
},
getText: function() {
return this.element.innerHTML;
},
loadExternalText: function() {
Element.addClassName(this.form, this.options.loadingClassName);
this.editField.disabled = true;
new Ajax.Request(
this.options.loadTextURL,
Object.extend({
asynchronous: true,
onComplete: this.onLoadedExternalText.bind(this)
}, this.options.ajaxOptions)
);
},
onLoadedExternalText: function(transport) {
Element.removeClassName(this.form, this.options.loadingClassName);
this.editField.disabled = false;
this.editField.value = transport.responseText.stripTags();
},
onclickCancel: function() {
this.onComplete();
this.leaveEditMode();
return false;
},
onFailure: function(transport) {
this.options.onFailure(transport);
if (this.oldInnerHTML) {
this.element.innerHTML = this.oldInnerHTML;
this.oldInnerHTML = null;
}
return false;
},
onSubmit: function() {
// onLoading resets these so we need to save them away for the Ajax call
var form = this.form;
var value = this.editField.value;
// do this first, sometimes the ajax call returns before we get a chance to switch on Saving...
// which means this will actually switch on Saving... *after* we've left edit mode causing Saving...
// to be displayed indefinitely
this.onLoading();
if (this.options.evalScripts) {
new Ajax.Request(
this.url, Object.extend({
parameters: this.options.callback(form, value),
onComplete: this.onComplete.bind(this),
onFailure: this.onFailure.bind(this),
asynchronous:true,
evalScripts:true
}, this.options.ajaxOptions));
} else {
new Ajax.Updater(
{ success: this.element,
// don't update on failure (this could be an option)
failure: null },
this.url, Object.extend({
parameters: this.options.callback(form, value),
onComplete: this.onComplete.bind(this),
onFailure: this.onFailure.bind(this)
}, this.options.ajaxOptions));
}
// stop the event to avoid a page refresh in Safari
if (arguments.length > 1) {
Event.stop(arguments[0]);
}
return false;
},
onLoading: function() {
this.saving = true;
this.removeForm();
this.leaveHover();
this.showSaving();
},
showSaving: function() {
this.oldInnerHTML = this.element.innerHTML;
this.element.innerHTML = this.options.savingText;
Element.addClassName(this.element, this.options.savingClassName);
this.element.style.backgroundColor = this.originalBackground;
Element.show(this.element);
},
removeForm: function() {
if(this.form) {
if (this.form.parentNode) Element.remove(this.form);
this.form = null;
}
},
enterHover: function() {
if (this.saving) return;
this.element.style.backgroundColor = this.options.highlightcolor;
if (this.effect) {
this.effect.cancel();
}
Element.addClassName(this.element, this.options.hoverClassName)
},
leaveHover: function() {
if (this.options.backgroundColor) {
this.element.style.backgroundColor = this.oldBackground;
}
Element.removeClassName(this.element, this.options.hoverClassName)
if (this.saving) return;
this.effect = new Effect.Highlight(this.element, {
startcolor: this.options.highlightcolor,
endcolor: this.options.highlightendcolor,
restorecolor: this.originalBackground
});
},
leaveEditMode: function() {
Element.removeClassName(this.element, this.options.savingClassName);
this.removeForm();
this.leaveHover();
this.element.style.backgroundColor = this.originalBackground;
Element.show(this.element);
if (this.options.externalControl) {
Element.show(this.options.externalControl);
}
this.editing = false;
this.saving = false;
this.oldInnerHTML = null;
this.onLeaveEditMode();
},
onComplete: function(transport) {
this.leaveEditMode();
this.options.onComplete.bind(this)(transport, this.element);
},
onEnterEditMode: function() {},
onLeaveEditMode: function() {},
dispose: function() {
if (this.oldInnerHTML) {
this.element.innerHTML = this.oldInnerHTML;
}
this.leaveEditMode();
Event.stopObserving(this.element, 'click', this.onclickListener);
Event.stopObserving(this.element, 'mouseover', this.mouseoverListener);
Event.stopObserving(this.element, 'mouseout', this.mouseoutListener);
if (this.options.externalControl) {
Event.stopObserving(this.options.externalControl, 'click', this.onclickListener);
Event.stopObserving(this.options.externalControl, 'mouseover', this.mouseoverListener);
Event.stopObserving(this.options.externalControl, 'mouseout', this.mouseoutListener);
}
}
};
Ajax.InPlaceCollectionEditor = Class.create();
Object.extend(Ajax.InPlaceCollectionEditor.prototype, Ajax.InPlaceEditor.prototype);
Object.extend(Ajax.InPlaceCollectionEditor.prototype, {
createEditField: function() {
if (!this.cached_selectTag) {
var selectTag = document.createElement("select");
var collection = this.options.collection || [];
var optionTag;
collection.each(function(e,i) {
optionTag = document.createElement("option");
optionTag.value = (e instanceof Array) ? e[0] : e;
if(this.options.value==optionTag.value) optionTag.selected = true;
optionTag.appendChild(document.createTextNode((e instanceof Array) ? e[1] : e));
selectTag.appendChild(optionTag);
}.bind(this));
this.cached_selectTag = selectTag;
}
this.editField = this.cached_selectTag;
if(this.options.loadTextURL) this.loadExternalText();
this.form.appendChild(this.editField);
this.options.callback = function(form, value) {
return "value=" + encodeURIComponent(value);
}
}
});
// Delayed observer, like Form.Element.Observer,
// but waits for delay after last key input
// Ideal for live-search fields
Form.Element.DelayedObserver = Class.create();
Form.Element.DelayedObserver.prototype = {
initialize: function(element, delay, callback) {
this.delay = delay || 0.5;
this.element = $(element);
this.callback = callback;
this.timer = null;
this.lastValue = $F(this.element);
Event.observe(this.element,'keyup',this.delayedListener.bindAsEventListener(this));
},
delayedListener: function(event) {
if(this.lastValue == $F(this.element)) return;
if(this.timer) clearTimeout(this.timer);
this.timer = setTimeout(this.onTimerEvent.bind(this), this.delay * 1000);
this.lastValue = $F(this.element);
},
onTimerEvent: function() {
this.timer = null;
this.callback(this.element, $F(this.element));
}
};

@ -1,913 +0,0 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// (c) 2005 Sammi Williams (http://www.oriontransfer.co.nz, sammi@oriontransfer.co.nz)
//
// See scriptaculous.js for full license.
/*--------------------------------------------------------------------------*/
var Droppables = {
drops: [],
remove: function(element) {
this.drops = this.drops.reject(function(d) { return d.element==$(element) });
},
add: function(element) {
element = $(element);
var options = Object.extend({
greedy: true,
hoverclass: null,
tree: false
}, arguments[1] || {});
// cache containers
if(options.containment) {
options._containers = [];
var containment = options.containment;
if((typeof containment == 'object') &&
(containment.constructor == Array)) {
containment.each( function(c) { options._containers.push($(c)) });
} else {
options._containers.push($(containment));
}
}
if(options.accept) options.accept = [options.accept].flatten();
Element.makePositioned(element); // fix IE
options.element = element;
this.drops.push(options);
},
findDeepestChild: function(drops) {
deepest = drops[0];
for (i = 1; i < drops.length; ++i)
if (Element.isParent(drops[i].element, deepest.element))
deepest = drops[i];
return deepest;
},
isContained: function(element, drop) {
var containmentNode;
if(drop.tree) {
containmentNode = element.treeNode;
} else {
containmentNode = element.parentNode;
}
return drop._containers.detect(function(c) { return containmentNode == c });
},
isAffected: function(point, element, drop) {
return (
(drop.element!=element) &&
((!drop._containers) ||
this.isContained(element, drop)) &&
((!drop.accept) ||
(Element.classNames(element).detect(
function(v) { return drop.accept.include(v) } ) )) &&
Position.within(drop.element, point[0], point[1]) );
},
deactivate: function(drop) {
if(drop.hoverclass)
Element.removeClassName(drop.element, drop.hoverclass);
this.last_active = null;
},
activate: function(drop) {
if(drop.hoverclass)
Element.addClassName(drop.element, drop.hoverclass);
this.last_active = drop;
},
show: function(point, element) {
if(!this.drops.length) return;
var affected = [];
if(this.last_active) this.deactivate(this.last_active);
this.drops.each( function(drop) {
if(Droppables.isAffected(point, element, drop))
affected.push(drop);
});
if(affected.length>0) {
drop = Droppables.findDeepestChild(affected);
Position.within(drop.element, point[0], point[1]);
if(drop.onHover)
drop.onHover(element, drop.element, Position.overlap(drop.overlap, drop.element));
Droppables.activate(drop);
}
},
fire: function(event, element) {
if(!this.last_active) return;
Position.prepare();
if (this.isAffected([Event.pointerX(event), Event.pointerY(event)], element, this.last_active))
if (this.last_active.onDrop)
this.last_active.onDrop(element, this.last_active.element, event);
},
reset: function() {
if(this.last_active)
this.deactivate(this.last_active);
}
}
var Draggables = {
drags: [],
observers: [],
register: function(draggable) {
if(this.drags.length == 0) {
this.eventMouseUp = this.endDrag.bindAsEventListener(this);
this.eventMouseMove = this.updateDrag.bindAsEventListener(this);
this.eventKeypress = this.keyPress.bindAsEventListener(this);
Event.observe(document, "mouseup", this.eventMouseUp);
Event.observe(document, "mousemove", this.eventMouseMove);
Event.observe(document, "keypress", this.eventKeypress);
}
this.drags.push(draggable);
},
unregister: function(draggable) {
this.drags = this.drags.reject(function(d) { return d==draggable });
if(this.drags.length == 0) {
Event.stopObserving(document, "mouseup", this.eventMouseUp);
Event.stopObserving(document, "mousemove", this.eventMouseMove);
Event.stopObserving(document, "keypress", this.eventKeypress);
}
},
activate: function(draggable) {
window.focus(); // allows keypress events if window isn't currently focused, fails for Safari
this.activeDraggable = draggable;
},
deactivate: function() {
this.activeDraggable = null;
},
updateDrag: function(event) {
if(!this.activeDraggable) return;
var pointer = [Event.pointerX(event), Event.pointerY(event)];
// Mozilla-based browsers fire successive mousemove events with
// the same coordinates, prevent needless redrawing (moz bug?)
if(this._lastPointer && (this._lastPointer.inspect() == pointer.inspect())) return;
this._lastPointer = pointer;
this.activeDraggable.updateDrag(event, pointer);
},
endDrag: function(event) {
if(!this.activeDraggable) return;
this._lastPointer = null;
this.activeDraggable.endDrag(event);
this.activeDraggable = null;
},
keyPress: function(event) {
if(this.activeDraggable)
this.activeDraggable.keyPress(event);
},
addObserver: function(observer) {
this.observers.push(observer);
this._cacheObserverCallbacks();
},
removeObserver: function(element) { // element instead of observer fixes mem leaks
this.observers = this.observers.reject( function(o) { return o.element==element });
this._cacheObserverCallbacks();
},
notify: function(eventName, draggable, event) { // 'onStart', 'onEnd', 'onDrag'
if(this[eventName+'Count'] > 0)
this.observers.each( function(o) {
if(o[eventName]) o[eventName](eventName, draggable, event);
});
},
_cacheObserverCallbacks: function() {
['onStart','onEnd','onDrag'].each( function(eventName) {
Draggables[eventName+'Count'] = Draggables.observers.select(
function(o) { return o[eventName]; }
).length;
});
}
}
/*--------------------------------------------------------------------------*/
var Draggable = Class.create();
Draggable.prototype = {
initialize: function(element) {
var options = Object.extend({
handle: false,
starteffect: function(element) {
new Effect.Opacity(element, {duration:0.2, from:1.0, to:0.7});
},
reverteffect: function(element, top_offset, left_offset) {
var dur = Math.sqrt(Math.abs(top_offset^2)+Math.abs(left_offset^2))*0.02;
element._revert = new Effect.Move(element, { x: -left_offset, y: -top_offset, duration: dur});
},
endeffect: function(element) {
new Effect.Opacity(element, {duration:0.2, from:0.7, to:1.0});
},
zindex: 1000,
revert: false,
scroll: false,
scrollSensitivity: 20,
scrollSpeed: 15,
snap: false // false, or xy or [x,y] or function(x,y){ return [x,y] }
}, arguments[1] || {});
this.element = $(element);
if(options.handle && (typeof options.handle == 'string')) {
var h = Element.childrenWithClassName(this.element, options.handle, true);
if(h.length>0) this.handle = h[0];
}
if(!this.handle) this.handle = $(options.handle);
if(!this.handle) this.handle = this.element;
if(options.scroll && !options.scroll.scrollTo && !options.scroll.outerHTML)
options.scroll = $(options.scroll);
Element.makePositioned(this.element); // fix IE
this.delta = this.currentDelta();
this.options = options;
this.dragging = false;
this.eventMouseDown = this.initDrag.bindAsEventListener(this);
Event.observe(this.handle, "mousedown", this.eventMouseDown);
Draggables.register(this);
},
destroy: function() {
Event.stopObserving(this.handle, "mousedown", this.eventMouseDown);
Draggables.unregister(this);
},
currentDelta: function() {
return([
parseInt(Element.getStyle(this.element,'left') || '0'),
parseInt(Element.getStyle(this.element,'top') || '0')]);
},
initDrag: function(event) {
if(Event.isLeftClick(event)) {
// abort on form elements, fixes a Firefox issue
var src = Event.element(event);
if(src.tagName && (
src.tagName=='INPUT' ||
src.tagName=='SELECT' ||
src.tagName=='OPTION' ||
src.tagName=='BUTTON' ||
src.tagName=='TEXTAREA')) return;
if(this.element._revert) {
this.element._revert.cancel();
this.element._revert = null;
}
var pointer = [Event.pointerX(event), Event.pointerY(event)];
var pos = Position.cumulativeOffset(this.element);
this.offset = [0,1].map( function(i) { return (pointer[i] - pos[i]) });
Draggables.activate(this);
Event.stop(event);
}
},
startDrag: function(event) {
this.dragging = true;
if(this.options.zindex) {
this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0);
this.element.style.zIndex = this.options.zindex;
}
if(this.options.ghosting) {
this._clone = this.element.cloneNode(true);
Position.absolutize(this.element);
this.element.parentNode.insertBefore(this._clone, this.element);
}
if(this.options.scroll) {
if (this.options.scroll == window) {
var where = this._getWindowScroll(this.options.scroll);
this.originalScrollLeft = where.left;
this.originalScrollTop = where.top;
} else {
this.originalScrollLeft = this.options.scroll.scrollLeft;
this.originalScrollTop = this.options.scroll.scrollTop;
}
}
Draggables.notify('onStart', this, event);
if(this.options.starteffect) this.options.starteffect(this.element);
},
updateDrag: function(event, pointer) {
if(!this.dragging) this.startDrag(event);
Position.prepare();
Droppables.show(pointer, this.element);
Draggables.notify('onDrag', this, event);
this.draw(pointer);
if(this.options.change) this.options.change(this);
if(this.options.scroll) {
this.stopScrolling();
var p;
if (this.options.scroll == window) {
with(this._getWindowScroll(this.options.scroll)) { p = [ left, top, left+width, top+height ]; }
} else {
p = Position.page(this.options.scroll);
p[0] += this.options.scroll.scrollLeft;
p[1] += this.options.scroll.scrollTop;
p.push(p[0]+this.options.scroll.offsetWidth);
p.push(p[1]+this.options.scroll.offsetHeight);
}
var speed = [0,0];
if(pointer[0] < (p[0]+this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[0]+this.options.scrollSensitivity);
if(pointer[1] < (p[1]+this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[1]+this.options.scrollSensitivity);
if(pointer[0] > (p[2]-this.options.scrollSensitivity)) speed[0] = pointer[0]-(p[2]-this.options.scrollSensitivity);
if(pointer[1] > (p[3]-this.options.scrollSensitivity)) speed[1] = pointer[1]-(p[3]-this.options.scrollSensitivity);
this.startScrolling(speed);
}
// fix AppleWebKit rendering
if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
Event.stop(event);
},
finishDrag: function(event, success) {
this.dragging = false;
if(this.options.ghosting) {
Position.relativize(this.element);
Element.remove(this._clone);
this._clone = null;
}
if(success) Droppables.fire(event, this.element);
Draggables.notify('onEnd', this, event);
var revert = this.options.revert;
if(revert && typeof revert == 'function') revert = revert(this.element);
var d = this.currentDelta();
if(revert && this.options.reverteffect) {
this.options.reverteffect(this.element,
d[1]-this.delta[1], d[0]-this.delta[0]);
} else {
this.delta = d;
}
if(this.options.zindex)
this.element.style.zIndex = this.originalZ;
if(this.options.endeffect)
this.options.endeffect(this.element);
Draggables.deactivate(this);
Droppables.reset();
},
keyPress: function(event) {
if(event.keyCode!=Event.KEY_ESC) return;
this.finishDrag(event, false);
Event.stop(event);
},
endDrag: function(event) {
if(!this.dragging) return;
this.stopScrolling();
this.finishDrag(event, true);
Event.stop(event);
},
draw: function(point) {
var pos = Position.cumulativeOffset(this.element);
var d = this.currentDelta();
pos[0] -= d[0]; pos[1] -= d[1];
if(this.options.scroll && (this.options.scroll != window)) {
pos[0] -= this.options.scroll.scrollLeft-this.originalScrollLeft;
pos[1] -= this.options.scroll.scrollTop-this.originalScrollTop;
}
var p = [0,1].map(function(i){
return (point[i]-pos[i]-this.offset[i])
}.bind(this));
if(this.options.snap) {
if(typeof this.options.snap == 'function') {
p = this.options.snap(p[0],p[1]);
} else {
if(this.options.snap instanceof Array) {
p = p.map( function(v, i) {
return Math.round(v/this.options.snap[i])*this.options.snap[i] }.bind(this))
} else {
p = p.map( function(v) {
return Math.round(v/this.options.snap)*this.options.snap }.bind(this))
}
}}
var style = this.element.style;
if((!this.options.constraint) || (this.options.constraint=='horizontal'))
style.left = p[0] + "px";
if((!this.options.constraint) || (this.options.constraint=='vertical'))
style.top = p[1] + "px";
if(style.visibility=="hidden") style.visibility = ""; // fix gecko rendering
},
stopScrolling: function() {
if(this.scrollInterval) {
clearInterval(this.scrollInterval);
this.scrollInterval = null;
Draggables._lastScrollPointer = null;
}
},
startScrolling: function(speed) {
this.scrollSpeed = [speed[0]*this.options.scrollSpeed,speed[1]*this.options.scrollSpeed];
this.lastScrolled = new Date();
this.scrollInterval = setInterval(this.scroll.bind(this), 10);
},
scroll: function() {
var current = new Date();
var delta = current - this.lastScrolled;
this.lastScrolled = current;
if(this.options.scroll == window) {
with (this._getWindowScroll(this.options.scroll)) {
if (this.scrollSpeed[0] || this.scrollSpeed[1]) {
var d = delta / 1000;
this.options.scroll.scrollTo( left + d*this.scrollSpeed[0], top + d*this.scrollSpeed[1] );
}
}
} else {
this.options.scroll.scrollLeft += this.scrollSpeed[0] * delta / 1000;
this.options.scroll.scrollTop += this.scrollSpeed[1] * delta / 1000;
}
Position.prepare();
Droppables.show(Draggables._lastPointer, this.element);
Draggables.notify('onDrag', this);
Draggables._lastScrollPointer = Draggables._lastScrollPointer || $A(Draggables._lastPointer);
Draggables._lastScrollPointer[0] += this.scrollSpeed[0] * delta / 1000;
Draggables._lastScrollPointer[1] += this.scrollSpeed[1] * delta / 1000;
if (Draggables._lastScrollPointer[0] < 0)
Draggables._lastScrollPointer[0] = 0;
if (Draggables._lastScrollPointer[1] < 0)
Draggables._lastScrollPointer[1] = 0;
this.draw(Draggables._lastScrollPointer);
if(this.options.change) this.options.change(this);
},
_getWindowScroll: function(w) {
var T, L, W, H;
with (w.document) {
if (w.document.documentElement && documentElement.scrollTop) {
T = documentElement.scrollTop;
L = documentElement.scrollLeft;
} else if (w.document.body) {
T = body.scrollTop;
L = body.scrollLeft;
}
if (w.innerWidth) {
W = w.innerWidth;
H = w.innerHeight;
} else if (w.document.documentElement && documentElement.clientWidth) {
W = documentElement.clientWidth;
H = documentElement.clientHeight;
} else {
W = body.offsetWidth;
H = body.offsetHeight
}
}
return { top: T, left: L, width: W, height: H };
}
}
/*--------------------------------------------------------------------------*/
var SortableObserver = Class.create();
SortableObserver.prototype = {
initialize: function(element, observer) {
this.element = $(element);
this.observer = observer;
this.lastValue = Sortable.serialize(this.element);
},
onStart: function() {
this.lastValue = Sortable.serialize(this.element);
},
onEnd: function() {
Sortable.unmark();
if(this.lastValue != Sortable.serialize(this.element))
this.observer(this.element)
}
}
var Sortable = {
sortables: {},
_findRootElement: function(element) {
while (element.tagName != "BODY") {
if(element.id && Sortable.sortables[element.id]) return element;
element = element.parentNode;
}
},
options: function(element) {
element = Sortable._findRootElement($(element));
if(!element) return;
return Sortable.sortables[element.id];
},
destroy: function(element){
var s = Sortable.options(element);
if(s) {
Draggables.removeObserver(s.element);
s.droppables.each(function(d){ Droppables.remove(d) });
s.draggables.invoke('destroy');
delete Sortable.sortables[s.element.id];
}
},
create: function(element) {
element = $(element);
var options = Object.extend({
element: element,
tag: 'li', // assumes li children, override with tag: 'tagname'
dropOnEmpty: false,
tree: false,
treeTag: 'ul',
overlap: 'vertical', // one of 'vertical', 'horizontal'
constraint: 'vertical', // one of 'vertical', 'horizontal', false
containment: element, // also takes array of elements (or id's); or false
handle: false, // or a CSS class
only: false,
hoverclass: null,
ghosting: false,
scroll: false,
scrollSensitivity: 20,
scrollSpeed: 15,
format: /^[^_]*_(.*)$/,
onChange: Prototype.emptyFunction,
onUpdate: Prototype.emptyFunction
}, arguments[1] || {});
// clear any old sortable with same element
this.destroy(element);
// build options for the draggables
var options_for_draggable = {
revert: true,
scroll: options.scroll,
scrollSpeed: options.scrollSpeed,
scrollSensitivity: options.scrollSensitivity,
ghosting: options.ghosting,
constraint: options.constraint,
handle: options.handle };
if(options.starteffect)
options_for_draggable.starteffect = options.starteffect;
if(options.reverteffect)
options_for_draggable.reverteffect = options.reverteffect;
else
if(options.ghosting) options_for_draggable.reverteffect = function(element) {
element.style.top = 0;
element.style.left = 0;
};
if(options.endeffect)
options_for_draggable.endeffect = options.endeffect;
if(options.zindex)
options_for_draggable.zindex = options.zindex;
// build options for the droppables
var options_for_droppable = {
overlap: options.overlap,
containment: options.containment,
tree: options.tree,
hoverclass: options.hoverclass,
onHover: Sortable.onHover
//greedy: !options.dropOnEmpty
}
var options_for_tree = {
onHover: Sortable.onEmptyHover,
overlap: options.overlap,
containment: options.containment,
hoverclass: options.hoverclass
}
// fix for gecko engine
Element.cleanWhitespace(element);
options.draggables = [];
options.droppables = [];
// drop on empty handling
if(options.dropOnEmpty || options.tree) {
Droppables.add(element, options_for_tree);
options.droppables.push(element);
}
(this.findElements(element, options) || []).each( function(e) {
// handles are per-draggable
var handle = options.handle ?
Element.childrenWithClassName(e, options.handle)[0] : e;
options.draggables.push(
new Draggable(e, Object.extend(options_for_draggable, { handle: handle })));
Droppables.add(e, options_for_droppable);
if(options.tree) e.treeNode = element;
options.droppables.push(e);
});
if(options.tree) {
(Sortable.findTreeElements(element, options) || []).each( function(e) {
Droppables.add(e, options_for_tree);
e.treeNode = element;
options.droppables.push(e);
});
}
// keep reference
this.sortables[element.id] = options;
// for onupdate
Draggables.addObserver(new SortableObserver(element, options.onUpdate));
},
// return all suitable-for-sortable elements in a guaranteed order
findElements: function(element, options) {
return Element.findChildren(
element, options.only, options.tree ? true : false, options.tag);
},
findTreeElements: function(element, options) {
return Element.findChildren(
element, options.only, options.tree ? true : false, options.treeTag);
},
onHover: function(element, dropon, overlap) {
if(Element.isParent(dropon, element)) return;
if(overlap > .33 && overlap < .66 && Sortable.options(dropon).tree) {
return;
} else if(overlap>0.5) {
Sortable.mark(dropon, 'before');
if(dropon.previousSibling != element) {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering
dropon.parentNode.insertBefore(element, dropon);
if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
} else {
Sortable.mark(dropon, 'after');
var nextElement = dropon.nextSibling || null;
if(nextElement != element) {
var oldParentNode = element.parentNode;
element.style.visibility = "hidden"; // fix gecko rendering
dropon.parentNode.insertBefore(element, nextElement);
if(dropon.parentNode!=oldParentNode)
Sortable.options(oldParentNode).onChange(element);
Sortable.options(dropon.parentNode).onChange(element);
}
}
},
onEmptyHover: function(element, dropon, overlap) {
var oldParentNode = element.parentNode;
var droponOptions = Sortable.options(dropon);
if(!Element.isParent(dropon, element)) {
var index;
var children = Sortable.findElements(dropon, {tag: droponOptions.tag});
var child = null;
if(children) {
var offset = Element.offsetSize(dropon, droponOptions.overlap) * (1.0 - overlap);
for (index = 0; index < children.length; index += 1) {
if (offset - Element.offsetSize (children[index], droponOptions.overlap) >= 0) {
offset -= Element.offsetSize (children[index], droponOptions.overlap);
} else if (offset - (Element.offsetSize (children[index], droponOptions.overlap) / 2) >= 0) {
child = index + 1 < children.length ? children[index + 1] : null;
break;
} else {
child = children[index];
break;
}
}
}
dropon.insertBefore(element, child);
Sortable.options(oldParentNode).onChange(element);
droponOptions.onChange(element);
}
},
unmark: function() {
if(Sortable._marker) Element.hide(Sortable._marker);
},
mark: function(dropon, position) {
// mark on ghosting only
var sortable = Sortable.options(dropon.parentNode);
if(sortable && !sortable.ghosting) return;
if(!Sortable._marker) {
Sortable._marker = $('dropmarker') || document.createElement('DIV');
Element.hide(Sortable._marker);
Element.addClassName(Sortable._marker, 'dropmarker');
Sortable._marker.style.position = 'absolute';
document.getElementsByTagName("body").item(0).appendChild(Sortable._marker);
}
var offsets = Position.cumulativeOffset(dropon);
Sortable._marker.style.left = offsets[0] + 'px';
Sortable._marker.style.top = offsets[1] + 'px';
if(position=='after')
if(sortable.overlap == 'horizontal')
Sortable._marker.style.left = (offsets[0]+dropon.clientWidth) + 'px';
else
Sortable._marker.style.top = (offsets[1]+dropon.clientHeight) + 'px';
Element.show(Sortable._marker);
},
_tree: function(element, options, parent) {
var children = Sortable.findElements(element, options) || [];
for (var i = 0; i < children.length; ++i) {
var match = children[i].id.match(options.format);
if (!match) continue;
var child = {
id: encodeURIComponent(match ? match[1] : null),
element: element,
parent: parent,
children: new Array,
position: parent.children.length,
container: Sortable._findChildrenElement(children[i], options.treeTag.toUpperCase())
}
/* Get the element containing the children and recurse over it */
if (child.container)
this._tree(child.container, options, child)
parent.children.push (child);
}
return parent;
},
/* Finds the first element of the given tag type within a parent element.
Used for finding the first LI[ST] within a L[IST]I[TEM].*/
_findChildrenElement: function (element, containerTag) {
if (element && element.hasChildNodes)
for (var i = 0; i < element.childNodes.length; ++i)
if (element.childNodes[i].tagName == containerTag)
return element.childNodes[i];
return null;
},
tree: function(element) {
element = $(element);
var sortableOptions = this.options(element);
var options = Object.extend({
tag: sortableOptions.tag,
treeTag: sortableOptions.treeTag,
only: sortableOptions.only,
name: element.id,
format: sortableOptions.format
}, arguments[1] || {});
var root = {
id: null,
parent: null,
children: new Array,
container: element,
position: 0
}
return Sortable._tree (element, options, root);
},
/* Construct a [i] index for a particular node */
_constructIndex: function(node) {
var index = '';
do {
if (node.id) index = '[' + node.position + ']' + index;
} while ((node = node.parent) != null);
return index;
},
sequence: function(element) {
element = $(element);
var options = Object.extend(this.options(element), arguments[1] || {});
return $(this.findElements(element, options) || []).map( function(item) {
return item.id.match(options.format) ? item.id.match(options.format)[1] : '';
});
},
setSequence: function(element, new_sequence) {
element = $(element);
var options = Object.extend(this.options(element), arguments[2] || {});
var nodeMap = {};
this.findElements(element, options).each( function(n) {
if (n.id.match(options.format))
nodeMap[n.id.match(options.format)[1]] = [n, n.parentNode];
n.parentNode.removeChild(n);
});
new_sequence.each(function(ident) {
var n = nodeMap[ident];
if (n) {
n[1].appendChild(n[0]);
delete nodeMap[ident];
}
});
},
serialize: function(element) {
element = $(element);
var options = Object.extend(Sortable.options(element), arguments[1] || {});
var name = encodeURIComponent(
(arguments[1] && arguments[1].name) ? arguments[1].name : element.id);
if (options.tree) {
return Sortable.tree(element, arguments[1]).children.map( function (item) {
return [name + Sortable._constructIndex(item) + "=" +
encodeURIComponent(item.id)].concat(item.children.map(arguments.callee));
}).flatten().join('&');
} else {
return Sortable.sequence(element, arguments[1]).map( function(item) {
return name + "[]=" + encodeURIComponent(item);
}).join('&');
}
}
}
/* Returns true if child is contained within element */
Element.isParent = function(child, element) {
if (!child.parentNode || child == element) return false;
if (child.parentNode == element) return true;
return Element.isParent(child.parentNode, element);
}
Element.findChildren = function(element, only, recursive, tagName) {
if(!element.hasChildNodes()) return null;
tagName = tagName.toUpperCase();
if(only) only = [only].flatten();
var elements = [];
$A(element.childNodes).each( function(e) {
if(e.tagName && e.tagName.toUpperCase()==tagName &&
(!only || (Element.classNames(e).detect(function(v) { return only.include(v) }))))
elements.push(e);
if(recursive) {
var grandchildren = Element.findChildren(e, only, recursive, tagName);
if(grandchildren) elements.push(grandchildren);
}
});
return (elements.length>0 ? elements.flatten() : []);
}
Element.offsetSize = function (element, type) {
if (type == 'vertical' || type == 'height')
return element.offsetHeight;
else
return element.offsetWidth;
}

@ -1,958 +0,0 @@
// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
// Contributors:
// Justin Palmer (http://encytemedia.com/)
// Mark Pilgrim (http://diveintomark.org/)
// Martin Bialasinki
//
// See scriptaculous.js for full license.
// converts rgb() and #xxx to #xxxxxx format,
// returns self (or first argument) if not convertable
String.prototype.parseColor = function() {
var color = '#';
if(this.slice(0,4) == 'rgb(') {
var cols = this.slice(4,this.length-1).split(',');
var i=0; do { color += parseInt(cols[i]).toColorPart() } while (++i<3);
} else {
if(this.slice(0,1) == '#') {
if(this.length==4) for(var i=1;i<4;i++) color += (this.charAt(i) + this.charAt(i)).toLowerCase();
if(this.length==7) color = this.toLowerCase();
}
}
return(color.length==7 ? color : (arguments[0] || this));
}
/*--------------------------------------------------------------------------*/
Element.collectTextNodes = function(element) {
return $A($(element).childNodes).collect( function(node) {
return (node.nodeType==3 ? node.nodeValue :
(node.hasChildNodes() ? Element.collectTextNodes(node) : ''));
}).flatten().join('');
}
Element.collectTextNodesIgnoreClass = function(element, className) {
return $A($(element).childNodes).collect( function(node) {
return (node.nodeType==3 ? node.nodeValue :
((node.hasChildNodes() && !Element.hasClassName(node,className)) ?
Element.collectTextNodesIgnoreClass(node, className) : ''));
}).flatten().join('');
}
Element.setContentZoom = function(element, percent) {
element = $(element);
Element.setStyle(element, {fontSize: (percent/100) + 'em'});
if(navigator.appVersion.indexOf('AppleWebKit')>0) window.scrollBy(0,0);
}
Element.getOpacity = function(element){
var opacity;
if (opacity = Element.getStyle(element, 'opacity'))
return parseFloat(opacity);
if (opacity = (Element.getStyle(element, 'filter') || '').match(/alpha\(opacity=(.*)\)/))
if(opacity[1]) return parseFloat(opacity[1]) / 100;
return 1.0;
}
Element.setOpacity = function(element, value){
element= $(element);
if (value == 1){
Element.setStyle(element, { opacity:
(/Gecko/.test(navigator.userAgent) && !/Konqueror|Safari|KHTML/.test(navigator.userAgent)) ?
0.999999 : null });
if(/MSIE/.test(navigator.userAgent))
Element.setStyle(element, {filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'')});
} else {
if(value < 0.00001) value = 0;
Element.setStyle(element, {opacity: value});
if(/MSIE/.test(navigator.userAgent))
Element.setStyle(element,
{ filter: Element.getStyle(element,'filter').replace(/alpha\([^\)]*\)/gi,'') +
'alpha(opacity='+value*100+')' });
}
}
Element.getInlineOpacity = function(element){
return $(element).style.opacity || '';
}
Element.childrenWithClassName = function(element, className, findFirst) {
var classNameRegExp = new RegExp("(^|\\s)" + className + "(\\s|$)");
var results = $A($(element).getElementsByTagName('*'))[findFirst ? 'detect' : 'select']( function(c) {
return (c.className && c.className.match(classNameRegExp));
});
if(!results) results = [];
return results;
}
Element.forceRerendering = function(element) {
try {
element = $(element);
var n = document.createTextNode(' ');
element.appendChild(n);
element.removeChild(n);
} catch(e) { }
};
/*--------------------------------------------------------------------------*/
Array.prototype.call = function() {
var args = arguments;
this.each(function(f){ f.apply(this, args) });
}
/*--------------------------------------------------------------------------*/
var Effect = {
tagifyText: function(element) {
var tagifyStyle = 'position:relative';
if(/MSIE/.test(navigator.userAgent)) tagifyStyle += ';zoom:1';
element = $(element);
$A(element.childNodes).each( function(child) {
if(child.nodeType==3) {
child.nodeValue.toArray().each( function(character) {
element.insertBefore(
Builder.node('span',{style: tagifyStyle},
character == ' ' ? String.fromCharCode(160) : character),
child);
});
Element.remove(child);
}
});
},
multiple: function(element, effect) {
var elements;
if(((typeof element == 'object') ||
(typeof element == 'function')) &&
(element.length))
elements = element;
else
elements = $(element).childNodes;
var options = Object.extend({
speed: 0.1,
delay: 0.0
}, arguments[2] || {});
var masterDelay = options.delay;
$A(elements).each( function(element, index) {
new effect(element, Object.extend(options, { delay: index * options.speed + masterDelay }));
});
},
PAIRS: {
'slide': ['SlideDown','SlideUp'],
'blind': ['BlindDown','BlindUp'],
'appear': ['Appear','Fade']
},
toggle: function(element, effect) {
element = $(element);
effect = (effect || 'appear').toLowerCase();
var options = Object.extend({
queue: { position:'end', scope:(element.id || 'global'), limit: 1 }
}, arguments[2] || {});
Effect[element.visible() ?
Effect.PAIRS[effect][1] : Effect.PAIRS[effect][0]](element, options);
}
};
var Effect2 = Effect; // deprecated
/* ------------- transitions ------------- */
Effect.Transitions = {}
Effect.Transitions.linear = function(pos) {
return pos;
}
Effect.Transitions.sinoidal = function(pos) {
return (-Math.cos(pos*Math.PI)/2) + 0.5;
}
Effect.Transitions.reverse = function(pos) {
return 1-pos;
}
Effect.Transitions.flicker = function(pos) {
return ((-Math.cos(pos*Math.PI)/4) + 0.75) + Math.random()/4;
}
Effect.Transitions.wobble = function(pos) {
return (-Math.cos(pos*Math.PI*(9*pos))/2) + 0.5;
}
Effect.Transitions.pulse = function(pos) {
return (Math.floor(pos*10) % 2 == 0 ?
(pos*10-Math.floor(pos*10)) : 1-(pos*10-Math.floor(pos*10)));
}
Effect.Transitions.none = function(pos) {
return 0;
}
Effect.Transitions.full = function(pos) {
return 1;
}
/* ------------- core effects ------------- */
Effect.ScopedQueue = Class.create();
Object.extend(Object.extend(Effect.ScopedQueue.prototype, Enumerable), {
initialize: function() {
this.effects = [];
this.interval = null;
},
_each: function(iterator) {
this.effects._each(iterator);
},
add: function(effect) {
var timestamp = new Date().getTime();
var position = (typeof effect.options.queue == 'string') ?
effect.options.queue : effect.options.queue.position;
switch(position) {
case 'front':
// move unstarted effects after this effect
this.effects.findAll(function(e){ return e.state=='idle' }).each( function(e) {
e.startOn += effect.finishOn;
e.finishOn += effect.finishOn;
});
break;
case 'end':
// start effect after last queued effect has finished
timestamp = this.effects.pluck('finishOn').max() || timestamp;
break;
}
effect.startOn += timestamp;
effect.finishOn += timestamp;
if(!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit))
this.effects.push(effect);
if(!this.interval)
this.interval = setInterval(this.loop.bind(this), 40);
},
remove: function(effect) {
this.effects = this.effects.reject(function(e) { return e==effect });
if(this.effects.length == 0) {
clearInterval(this.interval);
this.interval = null;
}
},
loop: function() {
var timePos = new Date().getTime();
this.effects.invoke('loop', timePos);
}
});
Effect.Queues = {
instances: $H(),
get: function(queueName) {
if(typeof queueName != 'string') return queueName;
if(!this.instances[queueName])
this.instances[queueName] = new Effect.ScopedQueue();
return this.instances[queueName];
}
}
Effect.Queue = Effect.Queues.get('global');
Effect.DefaultOptions = {
transition: Effect.Transitions.sinoidal,
duration: 1.0, // seconds
fps: 25.0, // max. 25fps due to Effect.Queue implementation
sync: false, // true for combining
from: 0.0,
to: 1.0,
delay: 0.0,
queue: 'parallel'
}
Effect.Base = function() {};
Effect.Base.prototype = {
position: null,
start: function(options) {
this.options = Object.extend(Object.extend({},Effect.DefaultOptions), options || {});
this.currentFrame = 0;
this.state = 'idle';
this.startOn = this.options.delay*1000;
this.finishOn = this.startOn + (this.options.duration*1000);
this.event('beforeStart');
if(!this.options.sync)
Effect.Queues.get(typeof this.options.queue == 'string' ?
'global' : this.options.queue.scope).add(this);
},
loop: function(timePos) {
if(timePos >= this.startOn) {
if(timePos >= this.finishOn) {
this.render(1.0);
this.cancel();
this.event('beforeFinish');
if(this.finish) this.finish();
this.event('afterFinish');
return;
}
var pos = (timePos - this.startOn) / (this.finishOn - this.startOn);
var frame = Math.round(pos * this.options.fps * this.options.duration);
if(frame > this.currentFrame) {
this.render(pos);
this.currentFrame = frame;
}
}
},
render: function(pos) {
if(this.state == 'idle') {
this.state = 'running';
this.event('beforeSetup');
if(this.setup) this.setup();
this.event('afterSetup');
}
if(this.state == 'running') {
if(this.options.transition) pos = this.options.transition(pos);
pos *= (this.options.to-this.options.from);
pos += this.options.from;
this.position = pos;
this.event('beforeUpdate');
if(this.update) this.update(pos);
this.event('afterUpdate');
}
},
cancel: function() {
if(!this.options.sync)
Effect.Queues.get(typeof this.options.queue == 'string' ?
'global' : this.options.queue.scope).remove(this);
this.state = 'finished';
},
event: function(eventName) {
if(this.options[eventName + 'Internal']) this.options[eventName + 'Internal'](this);
if(this.options[eventName]) this.options[eventName](this);
},
inspect: function() {
return '#<Effect:' + $H(this).inspect() + ',options:' + $H(this.options).inspect() + '>';
}
}
Effect.Parallel = Class.create();
Object.extend(Object.extend(Effect.Parallel.prototype, Effect.Base.prototype), {
initialize: function(effects) {
this.effects = effects || [];
this.start(arguments[1]);
},
update: function(position) {
this.effects.invoke('render', position);
},
finish: function(position) {
this.effects.each( function(effect) {
effect.render(1.0);
effect.cancel();
effect.event('beforeFinish');
if(effect.finish) effect.finish(position);
effect.event('afterFinish');
});
}
});
Effect.Opacity = Class.create();
Object.extend(Object.extend(Effect.Opacity.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
// make this work on IE on elements without 'layout'
if(/MSIE/.test(navigator.userAgent) && (!this.element.hasLayout))
this.element.setStyle({zoom: 1});
var options = Object.extend({
from: this.element.getOpacity() || 0.0,
to: 1.0
}, arguments[1] || {});
this.start(options);
},
update: function(position) {
this.element.setOpacity(position);
}
});
Effect.Move = Class.create();
Object.extend(Object.extend(Effect.Move.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
var options = Object.extend({
x: 0,
y: 0,
mode: 'relative'
}, arguments[1] || {});
this.start(options);
},
setup: function() {
// Bug in Opera: Opera returns the "real" position of a static element or
// relative element that does not have top/left explicitly set.
// ==> Always set top and left for position relative elements in your stylesheets
// (to 0 if you do not need them)
this.element.makePositioned();
this.originalLeft = parseFloat(this.element.getStyle('left') || '0');
this.originalTop = parseFloat(this.element.getStyle('top') || '0');
if(this.options.mode == 'absolute') {
// absolute movement, so we need to calc deltaX and deltaY
this.options.x = this.options.x - this.originalLeft;
this.options.y = this.options.y - this.originalTop;
}
},
update: function(position) {
this.element.setStyle({
left: this.options.x * position + this.originalLeft + 'px',
top: this.options.y * position + this.originalTop + 'px'
});
}
});
// for backwards compatibility
Effect.MoveBy = function(element, toTop, toLeft) {
return new Effect.Move(element,
Object.extend({ x: toLeft, y: toTop }, arguments[3] || {}));
};
Effect.Scale = Class.create();
Object.extend(Object.extend(Effect.Scale.prototype, Effect.Base.prototype), {
initialize: function(element, percent) {
this.element = $(element)
var options = Object.extend({
scaleX: true,
scaleY: true,
scaleContent: true,
scaleFromCenter: false,
scaleMode: 'box', // 'box' or 'contents' or {} with provided values
scaleFrom: 100.0,
scaleTo: percent
}, arguments[2] || {});
this.start(options);
},
setup: function() {
this.restoreAfterFinish = this.options.restoreAfterFinish || false;
this.elementPositioning = this.element.getStyle('position');
this.originalStyle = {};
['top','left','width','height','fontSize'].each( function(k) {
this.originalStyle[k] = this.element.style[k];
}.bind(this));
this.originalTop = this.element.offsetTop;
this.originalLeft = this.element.offsetLeft;
var fontSize = this.element.getStyle('font-size') || '100%';
['em','px','%'].each( function(fontSizeType) {
if(fontSize.indexOf(fontSizeType)>0) {
this.fontSize = parseFloat(fontSize);
this.fontSizeType = fontSizeType;
}
}.bind(this));
this.factor = (this.options.scaleTo - this.options.scaleFrom)/100;
this.dims = null;
if(this.options.scaleMode=='box')
this.dims = [this.element.offsetHeight, this.element.offsetWidth];
if(/^content/.test(this.options.scaleMode))
this.dims = [this.element.scrollHeight, this.element.scrollWidth];
if(!this.dims)
this.dims = [this.options.scaleMode.originalHeight,
this.options.scaleMode.originalWidth];
},
update: function(position) {
var currentScale = (this.options.scaleFrom/100.0) + (this.factor * position);
if(this.options.scaleContent && this.fontSize)
this.element.setStyle({fontSize: this.fontSize * currentScale + this.fontSizeType });
this.setDimensions(this.dims[0] * currentScale, this.dims[1] * currentScale);
},
finish: function(position) {
if (this.restoreAfterFinish) this.element.setStyle(this.originalStyle);
},
setDimensions: function(height, width) {
var d = {};
if(this.options.scaleX) d.width = width + 'px';
if(this.options.scaleY) d.height = height + 'px';
if(this.options.scaleFromCenter) {
var topd = (height - this.dims[0])/2;
var leftd = (width - this.dims[1])/2;
if(this.elementPositioning == 'absolute') {
if(this.options.scaleY) d.top = this.originalTop-topd + 'px';
if(this.options.scaleX) d.left = this.originalLeft-leftd + 'px';
} else {
if(this.options.scaleY) d.top = -topd + 'px';
if(this.options.scaleX) d.left = -leftd + 'px';
}
}
this.element.setStyle(d);
}
});
Effect.Highlight = Class.create();
Object.extend(Object.extend(Effect.Highlight.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
var options = Object.extend({ startcolor: '#ffff99' }, arguments[1] || {});
this.start(options);
},
setup: function() {
// Prevent executing on elements not in the layout flow
if(this.element.getStyle('display')=='none') { this.cancel(); return; }
// Disable background image during the effect
this.oldStyle = {
backgroundImage: this.element.getStyle('background-image') };
this.element.setStyle({backgroundImage: 'none'});
if(!this.options.endcolor)
this.options.endcolor = this.element.getStyle('background-color').parseColor('#ffffff');
if(!this.options.restorecolor)
this.options.restorecolor = this.element.getStyle('background-color');
// init color calculations
this._base = $R(0,2).map(function(i){ return parseInt(this.options.startcolor.slice(i*2+1,i*2+3),16) }.bind(this));
this._delta = $R(0,2).map(function(i){ return parseInt(this.options.endcolor.slice(i*2+1,i*2+3),16)-this._base[i] }.bind(this));
},
update: function(position) {
this.element.setStyle({backgroundColor: $R(0,2).inject('#',function(m,v,i){
return m+(Math.round(this._base[i]+(this._delta[i]*position)).toColorPart()); }.bind(this)) });
},
finish: function() {
this.element.setStyle(Object.extend(this.oldStyle, {
backgroundColor: this.options.restorecolor
}));
}
});
Effect.ScrollTo = Class.create();
Object.extend(Object.extend(Effect.ScrollTo.prototype, Effect.Base.prototype), {
initialize: function(element) {
this.element = $(element);
this.start(arguments[1] || {});
},
setup: function() {
Position.prepare();
var offsets = Position.cumulativeOffset(this.element);
if(this.options.offset) offsets[1] += this.options.offset;
var max = window.innerHeight ?
window.height - window.innerHeight :
document.body.scrollHeight -
(document.documentElement.clientHeight ?
document.documentElement.clientHeight : document.body.clientHeight);
this.scrollStart = Position.deltaY;
this.delta = (offsets[1] > max ? max : offsets[1]) - this.scrollStart;
},
update: function(position) {
Position.prepare();
window.scrollTo(Position.deltaX,
this.scrollStart + (position*this.delta));
}
});
/* ------------- combination effects ------------- */
Effect.Fade = function(element) {
element = $(element);
var oldOpacity = element.getInlineOpacity();
var options = Object.extend({
from: element.getOpacity() || 1.0,
to: 0.0,
afterFinishInternal: function(effect) {
if(effect.options.to!=0) return;
effect.element.hide();
effect.element.setStyle({opacity: oldOpacity});
}}, arguments[1] || {});
return new Effect.Opacity(element,options);
}
Effect.Appear = function(element) {
element = $(element);
var options = Object.extend({
from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
to: 1.0,
// force Safari to render floated elements properly
afterFinishInternal: function(effect) {
effect.element.forceRerendering();
},
beforeSetup: function(effect) {
effect.element.setOpacity(effect.options.from);
effect.element.show();
}}, arguments[1] || {});
return new Effect.Opacity(element,options);
}
Effect.Puff = function(element) {
element = $(element);
var oldStyle = { opacity: element.getInlineOpacity(), position: element.getStyle('position') };
return new Effect.Parallel(
[ new Effect.Scale(element, 200,
{ sync: true, scaleFromCenter: true, scaleContent: true, restoreAfterFinish: true }),
new Effect.Opacity(element, { sync: true, to: 0.0 } ) ],
Object.extend({ duration: 1.0,
beforeSetupInternal: function(effect) {
effect.effects[0].element.setStyle({position: 'absolute'}); },
afterFinishInternal: function(effect) {
effect.effects[0].element.hide();
effect.effects[0].element.setStyle(oldStyle); }
}, arguments[1] || {})
);
}
Effect.BlindUp = function(element) {
element = $(element);
element.makeClipping();
return new Effect.Scale(element, 0,
Object.extend({ scaleContent: false,
scaleX: false,
restoreAfterFinish: true,
afterFinishInternal: function(effect) {
effect.element.hide();
effect.element.undoClipping();
}
}, arguments[1] || {})
);
}
Effect.BlindDown = function(element) {
element = $(element);
var elementDimensions = element.getDimensions();
return new Effect.Scale(element, 100,
Object.extend({ scaleContent: false,
scaleX: false,
scaleFrom: 0,
scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
restoreAfterFinish: true,
afterSetup: function(effect) {
effect.element.makeClipping();
effect.element.setStyle({height: '0px'});
effect.element.show();
},
afterFinishInternal: function(effect) {
effect.element.undoClipping();
}
}, arguments[1] || {})
);
}
Effect.SwitchOff = function(element) {
element = $(element);
var oldOpacity = element.getInlineOpacity();
return new Effect.Appear(element, {
duration: 0.4,
from: 0,
transition: Effect.Transitions.flicker,
afterFinishInternal: function(effect) {
new Effect.Scale(effect.element, 1, {
duration: 0.3, scaleFromCenter: true,
scaleX: false, scaleContent: false, restoreAfterFinish: true,
beforeSetup: function(effect) {
effect.element.makePositioned();
effect.element.makeClipping();
},
afterFinishInternal: function(effect) {
effect.element.hide();
effect.element.undoClipping();
effect.element.undoPositioned();
effect.element.setStyle({opacity: oldOpacity});
}
})
}
});
}
Effect.DropOut = function(element) {
element = $(element);
var oldStyle = {
top: element.getStyle('top'),
left: element.getStyle('left'),
opacity: element.getInlineOpacity() };
return new Effect.Parallel(
[ new Effect.Move(element, {x: 0, y: 100, sync: true }),
new Effect.Opacity(element, { sync: true, to: 0.0 }) ],
Object.extend(
{ duration: 0.5,
beforeSetup: function(effect) {
effect.effects[0].element.makePositioned();
},
afterFinishInternal: function(effect) {
effect.effects[0].element.hide();
effect.effects[0].element.undoPositioned();
effect.effects[0].element.setStyle(oldStyle);
}
}, arguments[1] || {}));
}
Effect.Shake = function(element) {
element = $(element);
var oldStyle = {
top: element.getStyle('top'),
left: element.getStyle('left') };
return new Effect.Move(element,
{ x: 20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
{ x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
{ x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
{ x: -40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
{ x: 40, y: 0, duration: 0.1, afterFinishInternal: function(effect) {
new Effect.Move(effect.element,
{ x: -20, y: 0, duration: 0.05, afterFinishInternal: function(effect) {
effect.element.undoPositioned();
effect.element.setStyle(oldStyle);
}}) }}) }}) }}) }}) }});
}
Effect.SlideDown = function(element) {
element = $(element);
element.cleanWhitespace();
// SlideDown need to have the content of the element wrapped in a container element with fixed height!
var oldInnerBottom = $(element.firstChild).getStyle('bottom');
var elementDimensions = element.getDimensions();
return new Effect.Scale(element, 100, Object.extend({
scaleContent: false,
scaleX: false,
scaleFrom: 0,
scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
restoreAfterFinish: true,
afterSetup: function(effect) {
effect.element.makePositioned();
effect.element.firstChild.makePositioned();
if(window.opera) effect.element.setStyle({top: ''});
effect.element.makeClipping();
effect.element.setStyle({height: '0px'});
effect.element.show(); },
afterUpdateInternal: function(effect) {
effect.element.firstChild.setStyle({bottom:
(effect.dims[0] - effect.element.clientHeight) + 'px' });
},
afterFinishInternal: function(effect) {
effect.element.undoClipping();
// IE will crash if child is undoPositioned first
if(/MSIE/.test(navigator.userAgent)){
effect.element.undoPositioned();
effect.element.firstChild.undoPositioned();
}else{
effect.element.firstChild.undoPositioned();
effect.element.undoPositioned();
}
effect.element.firstChild.setStyle({bottom: oldInnerBottom}); }
}, arguments[1] || {})
);
}
Effect.SlideUp = function(element) {
element = $(element);
element.cleanWhitespace();
var oldInnerBottom = $(element.firstChild).getStyle('bottom');
return new Effect.Scale(element, 0,
Object.extend({ scaleContent: false,
scaleX: false,
scaleMode: 'box',
scaleFrom: 100,
restoreAfterFinish: true,
beforeStartInternal: function(effect) {
effect.element.makePositioned();
effect.element.firstChild.makePositioned();
if(window.opera) effect.element.setStyle({top: ''});
effect.element.makeClipping();
effect.element.show(); },
afterUpdateInternal: function(effect) {
effect.element.firstChild.setStyle({bottom:
(effect.dims[0] - effect.element.clientHeight) + 'px' }); },
afterFinishInternal: function(effect) {
effect.element.hide();
effect.element.undoClipping();
effect.element.firstChild.undoPositioned();
effect.element.undoPositioned();
effect.element.setStyle({bottom: oldInnerBottom}); }
}, arguments[1] || {})
);
}
// Bug in opera makes the TD containing this element expand for a instance after finish
Effect.Squish = function(element) {
return new Effect.Scale(element, window.opera ? 1 : 0,
{ restoreAfterFinish: true,
beforeSetup: function(effect) {
effect.element.makeClipping(effect.element); },
afterFinishInternal: function(effect) {
effect.element.hide(effect.element);
effect.element.undoClipping(effect.element); }
});
}
Effect.Grow = function(element) {
element = $(element);
var options = Object.extend({
direction: 'center',
moveTransition: Effect.Transitions.sinoidal,
scaleTransition: Effect.Transitions.sinoidal,
opacityTransition: Effect.Transitions.full
}, arguments[1] || {});
var oldStyle = {
top: element.style.top,
left: element.style.left,
height: element.style.height,
width: element.style.width,
opacity: element.getInlineOpacity() };
var dims = element.getDimensions();
var initialMoveX, initialMoveY;
var moveX, moveY;
switch (options.direction) {
case 'top-left':
initialMoveX = initialMoveY = moveX = moveY = 0;
break;
case 'top-right':
initialMoveX = dims.width;
initialMoveY = moveY = 0;
moveX = -dims.width;
break;
case 'bottom-left':
initialMoveX = moveX = 0;
initialMoveY = dims.height;
moveY = -dims.height;
break;
case 'bottom-right':
initialMoveX = dims.width;
initialMoveY = dims.height;
moveX = -dims.width;
moveY = -dims.height;
break;
case 'center':
initialMoveX = dims.width / 2;
initialMoveY = dims.height / 2;
moveX = -dims.width / 2;
moveY = -dims.height / 2;
break;
}
return new Effect.Move(element, {
x: initialMoveX,
y: initialMoveY,
duration: 0.01,
beforeSetup: function(effect) {
effect.element.hide();
effect.element.makeClipping();
effect.element.makePositioned();
},
afterFinishInternal: function(effect) {
new Effect.Parallel(
[ new Effect.Opacity(effect.element, { sync: true, to: 1.0, from: 0.0, transition: options.opacityTransition }),
new Effect.Move(effect.element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition }),
new Effect.Scale(effect.element, 100, {
scaleMode: { originalHeight: dims.height, originalWidth: dims.width },
sync: true, scaleFrom: window.opera ? 1 : 0, transition: options.scaleTransition, restoreAfterFinish: true})
], Object.extend({
beforeSetup: function(effect) {
effect.effects[0].element.setStyle({height: '0px'});
effect.effects[0].element.show();
},
afterFinishInternal: function(effect) {
effect.effects[0].element.undoClipping();
effect.effects[0].element.undoPositioned();
effect.effects[0].element.setStyle(oldStyle);
}
}, options)
)
}
});
}
Effect.Shrink = function(element) {
element = $(element);
var options = Object.extend({
direction: 'center',
moveTransition: Effect.Transitions.sinoidal,
scaleTransition: Effect.Transitions.sinoidal,
opacityTransition: Effect.Transitions.none
}, arguments[1] || {});
var oldStyle = {
top: element.style.top,
left: element.style.left,
height: element.style.height,
width: element.style.width,
opacity: element.getInlineOpacity() };
var dims = element.getDimensions();
var moveX, moveY;
switch (options.direction) {
case 'top-left':
moveX = moveY = 0;
break;
case 'top-right':
moveX = dims.width;
moveY = 0;
break;
case 'bottom-left':
moveX = 0;
moveY = dims.height;
break;
case 'bottom-right':
moveX = dims.width;
moveY = dims.height;
break;
case 'center':
moveX = dims.width / 2;
moveY = dims.height / 2;
break;
}
return new Effect.Parallel(
[ new Effect.Opacity(element, { sync: true, to: 0.0, from: 1.0, transition: options.opacityTransition }),
new Effect.Scale(element, window.opera ? 1 : 0, { sync: true, transition: options.scaleTransition, restoreAfterFinish: true}),
new Effect.Move(element, { x: moveX, y: moveY, sync: true, transition: options.moveTransition })
], Object.extend({
beforeStartInternal: function(effect) {
effect.effects[0].element.makePositioned();
effect.effects[0].element.makeClipping(); },
afterFinishInternal: function(effect) {
effect.effects[0].element.hide();
effect.effects[0].element.undoClipping();
effect.effects[0].element.undoPositioned();
effect.effects[0].element.setStyle(oldStyle); }
}, options)
);
}
Effect.Pulsate = function(element) {
element = $(element);
var options = arguments[1] || {};
var oldOpacity = element.getInlineOpacity();
var transition = options.transition || Effect.Transitions.sinoidal;
var reverser = function(pos){ return transition(1-Effect.Transitions.pulse(pos)) };
reverser.bind(transition);
return new Effect.Opacity(element,
Object.extend(Object.extend({ duration: 3.0, from: 0,
afterFinishInternal: function(effect) { effect.element.setStyle({opacity: oldOpacity}); }
}, options), {transition: reverser}));
}
Effect.Fold = function(element) {
element = $(element);
var oldStyle = {
top: element.style.top,
left: element.style.left,
width: element.style.width,
height: element.style.height };
Element.makeClipping(element);
return new Effect.Scale(element, 5, Object.extend({
scaleContent: false,
scaleX: false,
afterFinishInternal: function(effect) {
new Effect.Scale(element, 1, {
scaleContent: false,
scaleY: false,
afterFinishInternal: function(effect) {
effect.element.hide();
effect.element.undoClipping();
effect.element.setStyle(oldStyle);
} });
}}, arguments[1] || {}));
};
['setOpacity','getOpacity','getInlineOpacity','forceRerendering','setContentZoom',
'collectTextNodes','collectTextNodesIgnoreClass','childrenWithClassName'].each(
function(f) { Element.Methods[f] = Element[f]; }
);
Element.Methods.visualEffect = function(element, effect, options) {
s = effect.gsub(/_/, '-').camelize();
effect_class = s.charAt(0).toUpperCase() + s.substring(1);
new Effect[effect_class](element, options);
return $(element);
};
Element.addMethods();

@ -1,4 +0,0 @@
foo foo foo
foo foo foo
foo foo foo

File diff suppressed because it is too large Load Diff

@ -1,16 +0,0 @@
#bar1 {
background: #fff;
color: #000;
text-align: center;
}
#bar2 {
background: #fff;
color: #000;
text-align: center;
}
#bar3 {
background: #fff;
color: #000;
text-align: center;
}

@ -1,16 +0,0 @@
#foo1 {
background: #fff;
color: #000;
text-align: center;
}
#foo2 {
background: #fff;
color: #000;
text-align: center;
}
#foo3 {
background: #fff;
color: #000;
text-align: center;
}

@ -1,16 +0,0 @@
#header1 {
background: #fff;
color: #000;
text-align: center;
}
#header2 {
background: #fff;
color: #000;
text-align: center;
}
#header3 {
background: #fff;
color: #000;
text-align: center;
}

@ -1,16 +0,0 @@
#screen1 {
background: #fff;
color: #000;
text-align: center;
}
#screen2 {
background: #fff;
color: #000;
text-align: center;
}
#screen3 {
background: #fff;
color: #000;
text-align: center;
}

@ -1,16 +0,0 @@
#bar1 {
background: #fff;
color: #000;
text-align: center;
}
#bar2 {
background: #fff;
color: #000;
text-align: center;
}
#bar3 {
background: #fff;
color: #000;
text-align: center;
}

@ -1,16 +0,0 @@
#foo1 {
background: #fff;
color: #000;
text-align: center;
}
#foo2 {
background: #fff;
color: #000;
text-align: center;
}
#foo3 {
background: #fff;
color: #000;
text-align: center;
}

@ -1,18 +0,0 @@
begin
require File.join(File.dirname(__FILE__), 'lib', 'haml') # From here
rescue LoadError
begin
require 'haml' # From gem
rescue LoadError => e
# gems:install may be run to install Haml with the skeleton plugin
# but not the gem itself installed.
# Don't die if this is the case.
raise e unless defined?(Rake) &&
(Rake.application.top_level_tasks.include?('gems') ||
Rake.application.top_level_tasks.include?('gems:install'))
end
end
# Load Haml.
# Haml may be undefined if we're running gems:install.
Haml.init_rails(binding) if defined?(Haml)