Organizando plugins e gems.

This commit is contained in:
2009-07-16 11:51:47 -03:00
parent 4e22c87074
commit dcfc38eb09
506 changed files with 10538 additions and 45562 deletions

View File

@@ -1,3 +1,8 @@
* (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

View File

@@ -1,26 +1,5 @@
= 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.
Overrides some basic methods for the current model so that calling #destroy sets a 'deleted_at' field to the current timestamp. ActiveRecord is required.
== Resources
Install
* gem install acts_as_paranoid
Rubyforge project
* http://rubyforge.org/projects/ar-paranoid
RDocs
* http://ar-paranoid.rubyforge.org
Subversion
* http://techno-weenie.net/svn/projects/acts_as_paranoid
Collaboa
* http://collaboa.techno-weenie.net/repository/browse/acts_as_paranoid
http://github.com/technoweenie/acts_as_paranoid

View File

@@ -1,41 +1,10 @@
== Creating the test database
1. Pick Rails version. Either dump this plugin in a Rails app and run it from there, or specify it as an ENV var:
The default name for the test databases is "activerecord_paranoid". 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.
RAILS=2.2.2 rake
RAILS=2.2.2 ruby test/paranoid_test.rb
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.
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.
== Running with Rake
DB=mysql 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
3. Profit!!

View File

@@ -11,6 +11,24 @@ class << ActiveRecord::Base
end
end
def has_many_without_deleted(association_id, options = {}, &extension)
with_deleted = options.delete :with_deleted
returning has_many_with_deleted(association_id, options, &extension) 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
ActiveRecord::Base.send :include, Caboose::Acts::Paranoid

View File

@@ -0,0 +1,27 @@
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

View File

@@ -16,8 +16,8 @@ module Caboose #:nodoc:
# 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_with_deleted(1).deleted?
# # Returns true if the record was previously destroyed, false if not
@@ -31,6 +31,9 @@ module Caboose #:nodoc:
# 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'
#
@@ -87,16 +90,49 @@ module Caboose #:nodoc:
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_deleted_scope { count_with_deleted(*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_deleted_scope { calculate_with_deleted(*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)
@@ -112,18 +148,28 @@ module Caboose #:nodoc:
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)
options.delete(:with_deleted) ?
find_every_with_deleted(options) :
with_deleted_scope { find_every_with_deleted(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.class.send(:current_time)]), ["#{self.class.primary_key} = ?", id]
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
@@ -143,11 +189,19 @@ module Caboose #:nodoc:
!!read_attribute(:deleted_at)
end
def restore!
def recover!
self.deleted_at = nil
self.save!
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

View File

@@ -0,0 +1,94 @@
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

View File

@@ -0,0 +1,9 @@
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

View File

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

View File

@@ -6,6 +6,9 @@ class Widget < ActiveRecord::Base
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
@@ -22,15 +25,47 @@ class Category < ActiveRecord::Base
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
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
@@ -50,6 +85,7 @@ class ParanoidTest < Test::Unit::TestCase
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)
@@ -94,6 +130,12 @@ class ParanoidTest < Test::Unit::TestCase
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
@@ -111,6 +153,16 @@ class ParanoidTest < Test::Unit::TestCase
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
@@ -208,6 +260,24 @@ class ParanoidTest < Test::Unit::TestCase
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

View File

@@ -16,5 +16,15 @@ ActiveRecord::Schema.define(:version => 1) do
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

View File

@@ -1,13 +1,27 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
require 'test/unit'
require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb'))
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'] || 'sqlite'])
ActiveRecord::Base.establish_connection(config[ENV['DB'] || 'sqlite3'])
load(File.dirname(__FILE__) + "/schema.rb")

View File

@@ -0,0 +1,82 @@
*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

View File

@@ -0,0 +1,20 @@
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.

28
vendor/plugins/acts_as_versioned/README vendored Normal file
View File

@@ -0,0 +1,28 @@
= 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>.

View File

@@ -0,0 +1,41 @@
== 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

View File

@@ -0,0 +1,180 @@
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

View File

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

View File

@@ -0,0 +1,29 @@
# -*- 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

View File

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

View File

@@ -0,0 +1,486 @@
# 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

View File

@@ -0,0 +1,48 @@
$:.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

View File

@@ -0,0 +1,18 @@
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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
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

View File

@@ -0,0 +1,27 @@
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

View File

@@ -0,0 +1,15 @@
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

View File

@@ -0,0 +1,43 @@
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

View File

@@ -0,0 +1,16 @@
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

View File

@@ -0,0 +1,8 @@
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"

View File

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

View File

@@ -0,0 +1,46 @@
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

View File

@@ -0,0 +1,82 @@
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

View File

@@ -0,0 +1,370 @@
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

View File

@@ -13,15 +13,9 @@ speed and saving bandwidth.
When in development, it allows you to use your original versions
and retain formatting and comments for readability and debugging.
Because not all browsers will dependably cache JavaScript and CSS
files with query string parameters, AssetPackager writes a timestamp
or subversion revision stamp (if available) into the merged file names.
Therefore files are correctly cached by the browser AND your users
always get the latest version when you re-deploy.
This code is released under the MIT license (like Ruby). You<6F>re free
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<EFBFBD>d like to know so I can add them back in. Thanks!
I'd like to know so I can add them back in. Thanks!
* Formerly known as MergeJS.
@@ -39,22 +33,30 @@ http://www.crockford.com/javascript/jsmin.html
* Merges and compresses JavaScript and CSS when running in production.
* Uses uncompressed originals when running in development.
* Handles caching correctly. (No querystring parameters - filename timestamps)
* Versions each package individually. Updates to files in one won't re-trigger downloading the others.
* Uses subversion revision numbers instead of timestamps if within a subversion controlled directory.
* Guarantees new version will get downloaded the next time you deploy.
* Generates packages on demand in production
== Components
* Rake Task for merging and compressing JavaScript and CSS files.
* 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 merged versions.
* 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 http://sbecker.net/shared/plugins/asset_packager
./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
@@ -64,6 +66,8 @@ IMPORTANT: JavaScript files can break once compressed if each statement doesn't
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.):
@@ -78,7 +82,7 @@ javascripts:
stylesheets:
- base: []
Example with multiple merged files:
Multiple packages:
---
javascripts:
@@ -101,8 +105,13 @@ stylesheets:
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.
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.
@@ -111,46 +120,43 @@ away some CSS hackery. To disable comment removal, comment out /lib/synthesis/as
== JavaScript Examples
Example call:
<%= javascript_include_merged 'prototype', 'effects', 'controls', 'dragdrop', 'application', 'foo', 'bar' %>
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"></script>
<script type="text/javascript" src="/javascripts/effects.js"></script>
<script type="text/javascript" src="/javascripts/controls.js"></script>
<script type="text/javascript" src="/javascripts/dragdrop.js"></script>
<script type="text/javascript" src="/javascripts/application.js"></script>
<script type="text/javascript" src="/javascripts/foo.js"></script>
<script type="text/javascript" src="/javascripts/bar.js"></script>
<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_1150571523.js"></script>
<script type="text/javascript" src="/javascripts/secondary_1150729166.js"></script>
Now supports symbols and :defaults as well:
<%= javascript_include_merged :defaults %>
<%= javascript_include_merged :foo, :bar %>
<script type="text/javascript" src="/javascripts/base_packaged.js?123456789"></script>
== Stylesheet Examples
Example call:
<%= stylesheet_link_merged 'screen', 'header' %>
<%= stylesheet_link_merged :base %>
In development, this generates:
<link href="/stylesheets/screen.css" media="screen" rel="Stylesheet" type="text/css" />
<link href="/stylesheets/header.css" media="screen" rel="Stylesheet" type="text/css" />
<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_1150729166.css" media="screen" rel="Stylesheet" type="text/css" />
<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' %>
== Running the tests
== Rake tasks
So you want to run the tests eh? Ok, then listen:
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
@@ -158,13 +164,12 @@ rails app, in the vendor/plugins directory. Observe:
> rails newtestapp
> cd newtestapp
> ./script/plugin install http://sbecker.net/shared/plugins/asset_packager
> cd vendor/plugins/asset_packager/
> rake # all tests pass
> ./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 Scott Becker - http://synthesis.sbecker.net
Contact Email: becker.scott@gmail.com
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:

View File

@@ -66,7 +66,7 @@ module Synthesis
def delete_all
@@asset_packages_yml.keys.each do |asset_type|
@@asset_packages_yml[asset_type].each { |p| self.new(asset_type, p).delete_all_builds }
@@asset_packages_yml[asset_type].each { |p| self.new(asset_type, p).delete_previous_build }
end
end
@@ -102,61 +102,38 @@ module Synthesis
@asset_path = ($asset_base_path ? "#{$asset_base_path}/" : "#{RAILS_ROOT}/public/") +
"#{@asset_type}#{@target_dir.gsub(/^(.+)$/, '/\1')}"
@extension = get_extension
@match_regex = Regexp.new("\\A#{@target}_\\d+.#{@extension}\\z")
@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
@target_dir.gsub(/^(.+)$/, '\1/') +
Dir.new(@asset_path).entries.delete_if { |x| ! (x =~ @match_regex) }.sort.reverse[0].chomp(".#{@extension}")
build unless package_exists?
path = @target_dir.gsub(/^(.+)$/, '\1/')
"#{path}#{@target}_packaged"
end
def build
delete_old_builds
delete_previous_build
create_new_build
end
def delete_old_builds
Dir.new(@asset_path).entries.delete_if { |x| ! (x =~ @match_regex) }.each do |x|
File.delete("#{@asset_path}/#{x}") unless x.index(revision.to_s)
end
end
def delete_all_builds
Dir.new(@asset_path).entries.delete_if { |x| ! (x =~ @match_regex) }.each do |x|
File.delete("#{@asset_path}/#{x}")
end
def delete_previous_build
File.delete(@full_path) if File.exists?(@full_path)
end
private
def revision
unless @revision
revisions = [1]
@sources.each do |source|
revisions << get_file_revision("#{@asset_path}/#{source}.#{@extension}")
end
@revision = revisions.max
end
@revision
end
def get_file_revision(path)
if File.exists?(path)
begin
`svn info #{path}`[/Last Changed Rev: (.*?)\n/][/(\d+)/].to_i
rescue # use filename timestamp if not in subversion
File.mtime(path).to_i
end
else
0
end
end
def create_new_build
if File.exists?("#{@asset_path}/#{@target}_#{revision}.#{@extension}")
log "Latest version already exists: #{@asset_path}/#{@target}_#{revision}.#{@extension}"
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("#{@asset_path}/#{@target}_#{revision}.#{@extension}", "w") {|f| f.write(compressed_file) }
log "Created #{@asset_path}/#{@target}_#{revision}.#{@extension}"
File.open(new_build_path, "w") {|f| f.write(compressed_file) }
log "Created #{new_build_path}"
end
end
@@ -179,7 +156,7 @@ module Synthesis
def compress_js(source)
jsmin_path = "#{RAILS_ROOT}/vendor/plugins/asset_packager/lib"
tmp_path = "#{RAILS_ROOT}/tmp/#{@target}_#{revision}"
tmp_path = "#{RAILS_ROOT}/tmp/#{@target}_packaged"
# write out to a temp file
File.open("#{tmp_path}_uncompressed.js", "w") {|f| f.write(source) }
@@ -200,7 +177,7 @@ module Synthesis
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!(/\/\*(.*?)\*\//, "") # 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

View File

@@ -32,36 +32,8 @@ module Synthesis
AssetPackage.targets_from_sources("stylesheets", sources) :
AssetPackage.sources_from_targets("stylesheets", sources))
sources.collect { |source|
source = stylesheet_path(source)
tag("link", { "rel" => "Stylesheet", "type" => "text/css", "media" => "screen", "href" => source }.merge(options))
}.join("\n")
sources.collect { |source| stylesheet_link_tag(source, options) }.join("\n")
end
private
# rewrite compute_public_path to allow us to not include the query string timestamp
# used by ActionView::Helpers::AssetTagHelper
def compute_public_path(source, dir, ext=nil, add_asset_id=true)
source = source.dup
source << ".#{ext}" if File.extname(source).blank? && ext
unless source =~ %r{^[-a-z]+://}
source = "/#{dir}/#{source}" unless source[0] == ?/
asset_id = rails_asset_id(source)
source << '?' + asset_id if defined?(RAILS_ROOT) and add_asset_id and not asset_id.blank?
source = "#{ActionController::Base.asset_host}#{@controller.request.relative_url_root}#{source}"
end
source
end
# rewrite javascript path function to not include query string timestamp
def javascript_path(source)
compute_public_path(source, 'javascripts', 'js', false)
end
# rewrite stylesheet path function to not include query string timestamp
def stylesheet_path(source)
compute_public_path(source, 'stylesheets', 'css', false)
end
end
end

View File

@@ -9,13 +9,12 @@ require 'mocha'
require 'action_controller/test_process'
ActionController::Base.logger = nil
ActionController::Base.ignore_missing_templates = false
ActionController::Routing::Routes.reload rescue nil
$asset_packages_yml = YAML.load_file("#{RAILS_ROOT}/vendor/plugins/asset_packager/test/asset_packages.yml")
$asset_base_path = "#{RAILS_ROOT}/vendor/plugins/asset_packager/test/assets"
class AssetPackageHelperProductionTest < Test::Unit::TestCase
class AssetPackageHelperDevelopmentTest < Test::Unit::TestCase
include ActionView::Helpers::TagHelper
include ActionView::Helpers::AssetTagHelper
include Synthesis::AssetPackageHelper
@@ -24,24 +23,18 @@ class AssetPackageHelperProductionTest < Test::Unit::TestCase
Synthesis::AssetPackage.any_instance.stubs(:log)
@controller = Class.new do
attr_reader :request
def initialize
@request = Class.new do
def relative_url_root
""
end
end.new
def request
@request ||= ActionController::TestRequest.new
end
end.new
end
def build_js_expected_string(*sources)
sources.map {|s| %(<script src="/javascripts/#{s}.js" type="text/javascript"></script>) }.join("\n")
sources.map {|s| javascript_include_tag(s) }.join("\n")
end
def build_css_expected_string(*sources)
sources.map {|s| %(<link href="/stylesheets/#{s}.css" rel="Stylesheet" type="text/css" media="screen" />) }.join("\n")
sources.map {|s| stylesheet_link_tag(s) }.join("\n")
end
def test_js_basic

View File

@@ -8,7 +8,6 @@ require 'mocha'
require 'action_controller/test_process'
ActionController::Base.logger = nil
ActionController::Base.ignore_missing_templates = false
ActionController::Routing::Routes.reload rescue nil
$asset_packages_yml = YAML.load_file("#{RAILS_ROOT}/vendor/plugins/asset_packager/test/asset_packages.yml")
@@ -26,16 +25,9 @@ class AssetPackageHelperProductionTest < Test::Unit::TestCase
self.stubs(:should_merge?).returns(true)
@controller = Class.new do
attr_reader :request
def initialize
@request = Class.new do
def relative_url_root
""
end
end.new
def request
@request ||= ActionController::TestRequest.new
end
end.new
build_packages_once
@@ -49,11 +41,11 @@ class AssetPackageHelperProductionTest < Test::Unit::TestCase
end
def build_js_expected_string(*sources)
sources.map {|s| %(<script src="/javascripts/#{s}.js" type="text/javascript"></script>) }.join("\n")
sources.map {|s| javascript_include_tag(s) }.join("\n")
end
def build_css_expected_string(*sources)
sources.map {|s| %(<link href="/stylesheets/#{s}.css" rel="Stylesheet" type="text/css" media="screen" />) }.join("\n")
sources.map {|s| stylesheet_link_tag(s) }.join("\n")
end
def test_js_basic
@@ -145,9 +137,4 @@ class AssetPackageHelperProductionTest < Test::Unit::TestCase
stylesheet_link_merged(:base, :secondary, "subdir/styles")
end
def test_image_tag
timestamp = rails_asset_id("images/rails.png")
assert_dom_equal %(<img alt="Rails" src="/images/rails.png?#{timestamp}" />), image_tag("rails")
end
end

View File

@@ -38,44 +38,44 @@ class AssetPackagerTest < Test::Unit::TestCase
def test_delete_and_build
Synthesis::AssetPackage.delete_all
js_package_names = Dir.new("#{$asset_base_path}/javascripts").entries.delete_if { |x| ! (x =~ /\A\w+_\d+.js/) }
css_package_names = Dir.new("#{$asset_base_path}/stylesheets").entries.delete_if { |x| ! (x =~ /\A\w+_\d+.css/) }
css_subdir_package_names = Dir.new("#{$asset_base_path}/stylesheets/subdir").entries.delete_if { |x| ! (x =~ /\A\w+_\d+.css/) }
js_package_names = Dir.new("#{$asset_base_path}/javascripts").entries.delete_if { |x| ! (x =~ /\A\w+_packaged.js/) }
css_package_names = Dir.new("#{$asset_base_path}/stylesheets").entries.delete_if { |x| ! (x =~ /\A\w+_packaged.css/) }
css_subdir_package_names = Dir.new("#{$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("#{$asset_base_path}/javascripts").entries.delete_if { |x| ! (x =~ /\A\w+_\d+.js/) }.sort
css_package_names = Dir.new("#{$asset_base_path}/stylesheets").entries.delete_if { |x| ! (x =~ /\A\w+_\d+.css/) }.sort
css_subdir_package_names = Dir.new("#{$asset_base_path}/stylesheets/subdir").entries.delete_if { |x| ! (x =~ /\A\w+_\d+.css/) }.sort
js_package_names = Dir.new("#{$asset_base_path}/javascripts").entries.delete_if { |x| ! (x =~ /\A\w+_packaged.js/) }.sort
css_package_names = Dir.new("#{$asset_base_path}/stylesheets").entries.delete_if { |x| ! (x =~ /\A\w+_packaged.css/) }.sort
css_subdir_package_names = Dir.new("#{$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_\d+.js\z/)
assert js_package_names[1].match(/\Asecondary_\d+.js\z/)
assert css_package_names[0].match(/\Abase_\d+.css\z/)
assert css_package_names[1].match(/\Asecondary_\d+.css\z/)
assert css_subdir_package_names[0].match(/\Astyles_\d+.css\z/)
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_\d+\z/)
assert package_names[0].match(/\Abase_packaged\z/)
assert_equal package_names[1], "noexist1"
assert package_names[2].match(/\Asecondary_\d+\z/)
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_\d+\z/)
assert package_names[0].match(/\Abase_packaged\z/)
assert_equal package_names[1], "noexist1"
assert package_names[2].match(/\Asecondary_\d+\z/)
assert package_names[2].match(/\Asecondary_packaged\z/)
assert_equal package_names[3], "noexist2"
end

View File

0
vendor/plugins/asset_packager/test/assets/javascripts/bar.js vendored Normal file → Executable file
View File

0
vendor/plugins/asset_packager/test/assets/javascripts/controls.js vendored Normal file → Executable file
View File

0
vendor/plugins/asset_packager/test/assets/javascripts/dragdrop.js vendored Normal file → Executable file
View File

0
vendor/plugins/asset_packager/test/assets/javascripts/effects.js vendored Normal file → Executable file
View File

0
vendor/plugins/asset_packager/test/assets/javascripts/foo.js vendored Normal file → Executable file
View File

0
vendor/plugins/asset_packager/test/assets/javascripts/prototype.js vendored Normal file → Executable file
View File

0
vendor/plugins/asset_packager/test/assets/stylesheets/header.css vendored Normal file → Executable file
View File

0
vendor/plugins/asset_packager/test/assets/stylesheets/screen.css vendored Normal file → Executable file
View File

View File

@@ -1,9 +0,0 @@
gems = Dir[File.join(RAILS_ROOT, "vendor/gems/*")]
if gems.any?
gems.each do |dir|
lib = File.join(dir, 'lib')
$LOAD_PATH.unshift(lib) if File.directory?(lib)
init_rb = File.join(dir, 'init.rb')
require init_rb if File.file?(init_rb)
end
end

View File

@@ -1,64 +0,0 @@
namespace :gems do
desc "Freeze a RubyGem into this Rails application; init.rb will be loaded on startup."
task :freeze do
unless gem_name = ENV['GEM']
puts <<-eos
Parameters:
GEM Name of gem (required)
VERSION Version of gem to freeze (optional)
ONLY RAILS_ENVs for which the GEM will be active (optional)
eos
break
end
# ONLY=development[,test] etc
only_list = (ENV['ONLY'] || "").split(',')
only_if_begin = only_list.size == 0 ? "" : <<-EOS
ENV['RAILS_ENV'] ||= 'development'
if %w[#{only_list.join(' ')}].include?(ENV['RAILS_ENV'])
EOS
only_if_end = only_list.size == 0 ? "" : "end"
require 'rubygems'
Gem.manage_gems
Gem::CommandManager.new
gem = (version = ENV['VERSION']) ?
Gem.cache.search(gem_name, "= #{version}").first :
Gem.cache.search(gem_name).sort_by { |g| g.version }.last
version ||= gem.version.version rescue nil
unpack_command_class = Gem::UnpackCommand rescue nil || Gem::Commands::UnpackCommand
unless gem && path = unpack_command_class.new.get_path(gem_name, version)
raise "No gem #{gem_name} #{version} is installed. Do 'gem list #{gem_name}' to see what you have available."
end
gems_dir = File.join(RAILS_ROOT, 'vendor', 'gems')
mkdir_p gems_dir, :verbose => false if !File.exists?(gems_dir)
target_dir = ENV['TO'] || File.basename(path).sub(/\.gem$/, '')
mkdir_p "vendor/gems/#{target_dir}", :verbose => false
chdir gems_dir, :verbose => false do
mkdir_p target_dir, :verbose => false
abs_target_dir = File.join(Dir.pwd, target_dir)
(gem = Gem::Installer.new(path)).unpack(abs_target_dir)
chdir target_dir, :verbose => false do
if !File.exists?('init.rb')
File.open('init.rb', 'w') do |file|
file << <<-eos
#{only_if_begin}
require File.join(File.dirname(__FILE__), 'lib', '#{gem_name}')
#{only_if_end}
eos
end
end
end
puts "Unpacked #{gem_name} #{version} to '#{target_dir}'"
end
end
end

View File

@@ -1,79 +0,0 @@
namespace :gems do
desc "Link a RubyGem into this Rails application; init.rb will be loaded on startup."
task :link do
unless gem_name = ENV['GEM']
puts <<-eos
Parameters:
GEM Name of gem (required)
ONLY RAILS_ENVs for which the GEM will be active (optional)
eos
break
end
# ONLY=development[,test] etc
only_list = (ENV['ONLY'] || "").split(',')
only_if_begin = only_list.size == 0 ? "" : <<-EOS
ENV['RAILS_ENV'] ||= 'development'
if %w[#{only_list.join(' ')}].include?(ENV['RAILS_ENV'])
EOS
only_if_end = only_list.size == 0 ? "" : "end"
require 'rubygems'
Gem.manage_gems
gem = Gem.cache.search(gem_name).sort_by { |g| g.version }.last
version ||= gem.version.version rescue nil
unpack_command_class = Gem::UnpackCommand rescue nil || Gem::Commands::UnpackCommand
unless gem && path = unpack_command_class.new.get_path(gem_name, version)
raise "No gem #{gem_name} is installed. Try 'gem install #{gem_name}' to install the gem."
end
gems_dir = File.join(RAILS_ROOT, 'vendor', 'gems')
mkdir_p gems_dir, :verbose => false if !File.exists?(gems_dir)
target_dir = ENV['TO'] || gem.name
mkdir_p "vendor/gems/#{target_dir}"
chdir gems_dir, :verbose => false do
mkdir_p target_dir + '/tasks', :verbose => false
chdir target_dir, :verbose => false do
File.open('init.rb', 'w') do |file|
file << <<-eos
#{only_if_begin}
require 'rubygems'
Gem.manage_gems
gem = Gem.cache.search('#{gem.name}').sort_by { |g| g.version }.last
if gem.autorequire
require gem.autorequire
else
require '#{gem.name}'
end
#{only_if_end}
eos
end
File.open(File.join('tasks', 'load_tasks.rake'), 'w') do |file|
file << <<-eos
# This file does not include any Rake files, but loads up the
# tasks in the /vendor/gems/ folders
#{only_if_begin}
require 'rubygems'
Gem.manage_gems
gem = Gem.cache.search('#{gem.name}').sort_by { |g| g.version }.last
raise \"Gem '#{gem.name}' is not installed\" if gem.nil?
path = gem.full_gem_path
Dir[File.join(path, "/**/tasks/**/*.rake")].sort.each { |ext| load ext }
#{only_if_end}
eos
end
puts "Linked #{gem_name} (currently #{version}) via 'vendor/gems/#{target_dir}'"
end
end
end
task :unfreeze do
raise "No gem specified" unless gem_name = ENV['GEM']
Dir["vendor/gems/#{gem_name}-*"].each { |d| rm_rf d }
end
end

View File

@@ -1,15 +0,0 @@
namespace :gems do
desc "Unfreeze/unlink a RubyGem from this Rails application"
task :unfreeze do
unless gem_name = ENV['GEM']
puts <<-eos
Parameters:
GEM Name of gem (required)
eos
break
end
Dir["vendor/gems/#{gem_name}*"].each { |d| rm_rf d }
end
end

View File

@@ -1,10 +0,0 @@
# This file does not include any Rake files, but loads up the
# tasks in the /vendor/gems/ folders
Dir[File.join(RAILS_ROOT, "vendor/gems/*/**/tasks/**/*.rake")].sort.each do |ext|
begin
load ext
rescue
puts $!
end
end

View File

@@ -1,8 +1,8 @@
require 'rubygems'
require 'haml'
require 'haml/template'
require 'sass'
require 'sass/plugin'
begin
require File.join(File.dirname(__FILE__), 'lib', 'haml') # From here
rescue LoadError
require 'haml' # From gem
end
ActionView::Base.register_template_handler('haml', Haml::Template)
Sass::Plugin.update_stylesheets
# Load Haml and Sass
Haml.init_rails(binding)

View File

@@ -1,22 +0,0 @@
Copyright (c) 2007, Tammer Saleh, Thoughtbot, Inc.
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.

View File

@@ -1,123 +0,0 @@
= Shoulda - Making tests easy on the fingers and eyes
Shoulda makes it easy to write elegant, understandable, and maintainable tests. Shoulda consists of test macros, assertions, and helpers added on to the Test::Unit framework. It's fully compatible with your existing tests, and requires no retooling to use.
Helpers:: #context and #should give you rSpec like test blocks.
In addition, you get nested contexts and a much more readable syntax.
Macros:: Generate hundreds of lines of Controller and ActiveRecord tests with these powerful macros.
They get you started quickly, and can help you ensure that your application is conforming to best practices.
Assertions:: Many common rails testing idioms have been distilled into a set of useful assertions.
= Usage
=== Context Helpers (ThoughtBot::Shoulda::Context)
Stop killing your fingers with all of those underscores... Name your tests with plain sentences!
class UserTest << Test::Unit::TestCase
context "A User instance" do
setup do
@user = User.find(:first)
end
should "return its full name"
assert_equal 'John Doe', @user.full_name
end
context "with a profile" do
setup do
@user.profile = Profile.find(:first)
end
should "return true when sent #has_profile?"
assert @user.has_profile?
end
end
end
end
Produces the following test methods:
"test: A User instance should return its full name."
"test: A User instance with a profile should return true when sent #has_profile?."
So readable!
=== ActiveRecord Tests (ThoughtBot::Shoulda::ActiveRecord)
Quick macro tests for your ActiveRecord associations and validations:
class PostTest < Test::Unit::TestCase
load_all_fixtures
should_belong_to :user
should_have_many :tags, :through => :taggings
should_require_unique_attributes :title
should_require_attributes :body, :message => /wtf/
should_require_attributes :title
should_only_allow_numeric_values_for :user_id
end
class UserTest < Test::Unit::TestCase
load_all_fixtures
should_have_many :posts
should_not_allow_values_for :email, "blah", "b lah"
should_allow_values_for :email, "a@b.com", "asdf@asdf.com"
should_ensure_length_in_range :email, 1..100
should_ensure_value_in_range :age, 1..100
should_protect_attributes :password
end
Makes TDD so much easier.
=== Controller Tests (ThoughtBot::Shoulda::Controller::ClassMethods)
Macros to test the most common controller patterns...
context "on GET to :show for first record" do
setup do
get :show, :id => 1
end
should_assign_to :user
should_respond_with :success
should_render_template :show
should_not_set_the_flash
should "do something else really cool" do
assert_equal 1, assigns(:user).id
end
end
Test entire controllers in a few lines...
class PostsControllerTest < Test::Unit::TestCase
should_be_restful do |resource|
resource.parent = :user
resource.create.params = { :title => "first post", :body => 'blah blah blah'}
resource.update.params = { :title => "changed" }
end
end
should_be_restful generates 40 tests on the fly, for both html and xml requests.
=== Helpful Assertions (ThoughtBot::Shoulda::General)
More to come here, but have fun with what's there.
load_all_fixtures
assert_same_elements([:a, :b, :c], [:c, :a, :b])
assert_contains(['a', '1'], /\d/)
assert_contains(['a', '1'], 'a')
= Credits
Shoulda is maintained by {Tammer Saleh}[mailto:tsaleh@thoughtbot.com], and is funded by Thoughtbot[http://www.thoughtbot.com], inc.
= License
Shoulda is Copyright © 2006-2007 Tammer Saleh, Thoughtbot. It is free software, and may be redistributed under the terms specified in the MIT-LICENSE file.

View File

@@ -1,32 +0,0 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
# Test::Unit::UI::VERBOSE
Rake::TestTask.new do |t|
t.libs << 'lib'
t.pattern = 'test/{unit,functional,other}/**/*_test.rb'
t.verbose = false
end
Rake::RDocTask.new { |rdoc|
rdoc.rdoc_dir = 'doc'
rdoc.title = "Shoulda -- Making tests easy on the fingers and eyes"
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.template = "#{ENV['template']}.rb" if ENV['template']
rdoc.rdoc_files.include('README', 'lib/**/*.rb')
}
desc 'Update documentation on website'
task :sync_docs => 'rdoc' do
`rsync -ave ssh doc/ dev@dev.thoughtbot.com:/home/dev/www/dev.thoughtbot.com/shoulda`
end
desc 'Default: run tests.'
task :default => ['test']
Dir['tasks/*.rake'].each do |f|
load f
end

View File

@@ -1,40 +0,0 @@
#!/usr/bin/env ruby
require 'fileutils'
def usage(msg = nil)
puts "Error: #{msg}" if msg
puts if msg
puts "Usage: #{File.basename(__FILE__)} normal_test_file.rb"
puts
puts "Will convert an existing test file with names like "
puts
puts " def test_should_do_stuff"
puts " ..."
puts " end"
puts
puts "to one using the new syntax: "
puts
puts " should \"be super cool\" do"
puts " ..."
puts " end"
puts
puts "A copy of the old file will be left under /tmp/ in case this script just seriously screws up"
puts
exit (msg ? 2 : 0)
end
usage("Wrong number of arguments.") unless ARGV.size == 1
usage("This system doesn't have a /tmp directory. wtf?") unless File.directory?('/tmp')
file = ARGV.shift
tmpfile = "/tmp/#{File.basename(file)}"
usage("File '#{file}' doesn't exist") unless File.exists?(file)
FileUtils.cp(file, tmpfile)
contents = File.read(tmpfile)
contents.gsub!(/def test_should_(.*)\s*$/, 'should "\1" do')
contents.gsub!(/def test_(.*)\s*$/, 'should "RENAME ME: test \1" do')
contents.gsub!(/should ".*" do$/) {|line| line.tr!('_', ' ')}
File.open(file, 'w') { |f| f.write(contents) }
puts "File '#{file}' has been converted to 'should' syntax. Old version has been stored in '#{tmpfile}'"

View File

@@ -1 +0,0 @@
/home/builder/public_repo_cruise_config.rb

View File

@@ -1,3 +0,0 @@
require 'rubygems'
require 'active_support'
require 'shoulda'

View File

@@ -1,45 +0,0 @@
require 'yaml'
require 'shoulda/private_helpers'
require 'shoulda/general'
require 'shoulda/gem/shoulda'
require 'shoulda/active_record_helpers'
require 'shoulda/controller_tests/controller_tests.rb'
shoulda_options = {}
possible_config_paths = []
possible_config_paths << File.join(ENV["HOME"], ".shoulda.conf") if ENV["HOME"]
possible_config_paths << "shoulda.conf"
possible_config_paths << File.join("test", "shoulda.conf")
possible_config_paths << File.join(RAILS_ROOT, "test", "shoulda.conf") if defined?(RAILS_ROOT)
possible_config_paths.each do |config_file|
if File.exists? config_file
shoulda_options = YAML.load_file(config_file).symbolize_keys
break
end
end
require 'shoulda/color' if shoulda_options[:color]
module Test # :nodoc: all
module Unit
class TestCase
include ThoughtBot::Shoulda::Controller
include ThoughtBot::Shoulda::General
class << self
include ThoughtBot::Shoulda::ActiveRecord
end
end
end
end
module ActionController #:nodoc: all
module Integration
class Session
include ThoughtBot::Shoulda::General
end
end
end

View File

@@ -1,446 +0,0 @@
module ThoughtBot # :nodoc:
module Shoulda # :nodoc:
# = Macro test helpers for your active record models
#
# These helpers will test most of the validations and associations for your ActiveRecord models.
#
# class UserTest < Test::Unit::TestCase
# should_require_attributes :name, :phone_number
# should_not_allow_values_for :phone_number, "abcd", "1234"
# should_allow_values_for :phone_number, "(123) 456-7890"
#
# should_protect_attributes :password
#
# should_have_one :profile
# should_have_many :dogs
# should_have_many :messes, :through => :dogs
# should_belong_to :lover
# end
#
# For all of these helpers, the last parameter may be a hash of options.
#
module ActiveRecord
# Ensures that the model cannot be saved if one of the attributes listed is not present.
# Requires an existing record.
#
# Options:
# * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
# Regexp or string. Default = <tt>/blank/</tt>
#
# Example:
# should_require_attributes :name, :phone_number
#
def should_require_attributes(*attributes)
message = get_options!(attributes, :message)
message ||= /blank/
klass = model_class
attributes.each do |attribute|
should "require #{attribute} to be set" do
object = klass.new
assert !object.valid?, "#{klass.name} does not require #{attribute}."
assert object.errors.on(attribute), "#{klass.name} does not require #{attribute}."
assert_contains(object.errors.on(attribute), message)
end
end
end
# Ensures that the model cannot be saved if one of the attributes listed is not unique.
# Requires an existing record
#
# Options:
# * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
# Regexp or string. Default = <tt>/taken/</tt>
#
# Example:
# should_require_unique_attributes :keyword, :username
#
def should_require_unique_attributes(*attributes)
message, scope = get_options!(attributes, :message, :scoped_to)
message ||= /taken/
klass = model_class
attributes.each do |attribute|
attribute = attribute.to_sym
should "require unique value for #{attribute}#{" scoped to #{scope}" if scope}" do
assert existing = klass.find(:first), "Can't find first #{klass}"
object = klass.new
object.send(:"#{attribute}=", existing.send(attribute))
if scope
assert_respond_to object, :"#{scope}=", "#{klass.name} doesn't seem to have a #{scope} attribute."
object.send(:"#{scope}=", existing.send(scope))
end
assert !object.valid?, "#{klass.name} does not require a unique value for #{attribute}."
assert object.errors.on(attribute), "#{klass.name} does not require a unique value for #{attribute}."
assert_contains(object.errors.on(attribute), message)
if scope
# Now test that the object is valid when changing the scoped attribute
# TODO: actually find all values for scope and create a unique one.
object.send(:"#{scope}=", existing.send(scope).nil? ? 1 : existing.send(scope).next)
object.errors.clear
object.valid?
assert_does_not_contain(object.errors.on(attribute), message,
"after :#{scope} set to #{object.send(scope.to_sym)}")
end
end
end
end
# Ensures that the attribute cannot be set on update
# Requires an existing record
#
# should_protect_attributes :password, :admin_flag
#
def should_protect_attributes(*attributes)
get_options!(attributes)
klass = model_class
attributes.each do |attribute|
attribute = attribute.to_sym
should "not allow #{attribute} to be changed by update" do
assert object = klass.find(:first), "Can't find first #{klass}"
value = object[attribute]
# TODO: 1 may not be a valid value for the attribute (due to validations)
assert object.update_attributes({ attribute => 1 }),
"Cannot update #{klass} with { :#{attribute} => 1 }, #{object.errors.full_messages.to_sentence}"
assert object.valid?, "#{klass} isn't valid after changing #{attribute}"
assert_equal value, object[attribute], "Was able to change #{klass}##{attribute}"
end
end
end
# Ensures that the attribute cannot be set to the given values
# Requires an existing record
#
# Options:
# * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
# Regexp or string. Default = <tt>/invalid/</tt>
#
# Example:
# should_not_allow_values_for :isbn, "bad 1", "bad 2"
#
def should_not_allow_values_for(attribute, *bad_values)
message = get_options!(bad_values, :message)
message ||= /invalid/
klass = model_class
bad_values.each do |v|
should "not allow #{attribute} to be set to \"#{v}\"" do
assert object = klass.find(:first), "Can't find first #{klass}"
object.send("#{attribute}=", v)
assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
assert_contains(object.errors.on(attribute), message, "when set to \"#{v}\"")
end
end
end
# Ensures that the attribute can be set to the given values.
# Requires an existing record
#
# Options:
# * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
# Regexp or string. Default = <tt>/invalid/</tt>
#
# Example:
# should_allow_values_for :isbn, "isbn 1 2345 6789 0", "ISBN 1-2345-6789-0"
#
def should_allow_values_for(attribute, *good_values)
message = get_options!(good_values, :message)
message ||= /invalid/
klass = model_class
good_values.each do |v|
should "allow #{attribute} to be set to \"#{v}\"" do
assert object = klass.find(:first), "Can't find first #{klass}"
object.send("#{attribute}=", v)
object.save
assert_does_not_contain(object.errors.on(attribute), message, "when set to \"#{v}\"")
end
end
end
# Ensures that the length of the attribute is in the given range
# Requires an existing record
#
# Options:
# * <tt>:short_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
# Regexp or string. Default = <tt>/short/</tt>
# * <tt>:long_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
# Regexp or string. Default = <tt>/long/</tt>
#
# Example:
# should_ensure_length_in_range :password, (6..20)
#
def should_ensure_length_in_range(attribute, range, opts = {})
short_message, long_message = get_options!([opts], :short_message, :long_message)
short_message ||= /short/
long_message ||= /long/
klass = model_class
min_length = range.first
max_length = range.last
if min_length > 0
min_value = "x" * (min_length - 1)
should "not allow #{attribute} to be less than #{min_length} chars long" do
assert object = klass.find(:first), "Can't find first #{klass}"
object.send("#{attribute}=", min_value)
assert !object.save, "Saved #{klass} with #{attribute} set to \"#{min_value}\""
assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{min_value}\""
assert_contains(object.errors.on(attribute), short_message, "when set to \"#{min_value}\"")
end
end
max_value = "x" * (max_length + 1)
should "not allow #{attribute} to be more than #{max_length} chars long" do
assert object = klass.find(:first), "Can't find first #{klass}"
object.send("#{attribute}=", max_value)
assert !object.save, "Saved #{klass} with #{attribute} set to \"#{max_value}\""
assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{max_value}\""
assert_contains(object.errors.on(attribute), long_message, "when set to \"#{max_value}\"")
end
end
# Ensure that the attribute is in the range specified
# Requires an existing record
#
# Options:
# * <tt>:low_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
# Regexp or string. Default = <tt>/included/</tt>
# * <tt>:high_message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
# Regexp or string. Default = <tt>/included/</tt>
#
# Example:
# should_ensure_value_in_range :age, (0..100)
#
def should_ensure_value_in_range(attribute, range, opts = {})
low_message, high_message = get_options!([opts], :low_message, :high_message)
low_message ||= /included/
high_message ||= /included/
klass = model_class
min = range.first
max = range.last
should "not allow #{attribute} to be less than #{min}" do
v = min - 1
assert object = klass.find(:first), "Can't find first #{klass}"
object.send("#{attribute}=", v)
assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
assert_contains(object.errors.on(attribute), low_message, "when set to \"#{v}\"")
end
should "not allow #{attribute} to be more than #{max}" do
v = max + 1
assert object = klass.find(:first), "Can't find first #{klass}"
object.send("#{attribute}=", v)
assert !object.save, "Saved #{klass} with #{attribute} set to \"#{v}\""
assert object.errors.on(attribute), "There are no errors set on #{attribute} after being set to \"#{v}\""
assert_contains(object.errors.on(attribute), high_message, "when set to \"#{v}\"")
end
end
# Ensure that the attribute is numeric
# Requires an existing record
#
# Options:
# * <tt>:message</tt> - value the test expects to find in <tt>errors.on(:attribute)</tt>.
# Regexp or string. Default = <tt>/number/</tt>
#
# Example:
# should_only_allow_numeric_values_for :age
#
def should_only_allow_numeric_values_for(*attributes)
message = get_options!(attributes, :message)
message ||= /number/
klass = model_class
attributes.each do |attribute|
attribute = attribute.to_sym
should "only allow numeric values for #{attribute}" do
assert object = klass.find(:first), "Can't find first #{klass}"
object.send(:"#{attribute}=", "abcd")
assert !object.valid?, "Instance is still valid"
assert_contains(object.errors.on(attribute), message)
end
end
end
# Ensures that the has_many relationship exists.
#
# Options:
# * <tt>:through</tt> - association name for <tt>has_many :through</tt>
#
# Example:
# should_have_many :friends
# should_have_many :enemies, :through => :friends
#
def should_have_many(*associations)
through = get_options!(associations, :through)
klass = model_class
associations.each do |association|
name = "have many #{association}"
name += " through #{through}" if through
should name do
reflection = klass.reflect_on_association(association)
assert reflection, "#{klass.name} does not have any relationship to #{association}"
assert_equal :has_many, reflection.macro
if through
through_reflection = klass.reflect_on_association(through)
assert through_reflection, "#{klass.name} does not have any relationship to #{through}"
assert_equal(through, reflection.options[:through])
end
unless reflection.options[:through]
# This is not a through association, so check for the existence of the foreign key on the other table
if reflection.options[:foreign_key]
fk = reflection.options[:foreign_key]
elsif reflection.options[:as]
fk = reflection.options[:as].to_s.foreign_key
else
fk = klass.name.foreign_key
end
associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize
assert associated_klass.column_names.include?(fk.to_s), "#{associated_klass.name} does not have a #{fk} foreign key."
end
end
end
end
# Ensures that the has_and_belongs_to_many relationship exists.
#
# should_have_and_belong_to_many :posts, :cars
#
def should_have_and_belong_to_many(*associations)
get_options!(associations)
klass = model_class
associations.each do |association|
should "should have and belong to many #{association}" do
assert klass.reflect_on_association(association), "#{klass.name} does not have any relationship to #{association}"
assert_equal :has_and_belongs_to_many, klass.reflect_on_association(association).macro
end
end
end
# Ensure that the has_one relationship exists.
#
# should_have_one :god # unless hindu
#
def should_have_one(*associations)
get_options!(associations)
klass = model_class
associations.each do |association|
should "have one #{association}" do
reflection = klass.reflect_on_association(association)
assert reflection, "#{klass.name} does not have any relationship to #{association}"
assert_equal :has_one, reflection.macro
if reflection.options[:foreign_key]
fk = reflection.options[:foreign_key]
elsif reflection.options[:as]
fk = reflection.options[:as].to_s.foreign_key
else
fk = klass.name.foreign_key
end
associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize
assert associated_klass.column_names.include?(fk.to_s), "#{associated_klass.name} does not have a #{fk} foreign key."
end
end
end
# Ensure that the belongs_to relationship exists.
#
# should_belong_to :parent
#
def should_belong_to(*associations)
get_options!(associations)
klass = model_class
associations.each do |association|
should "belong_to #{association}" do
reflection = klass.reflect_on_association(association)
assert reflection, "#{klass.name} does not have any relationship to #{association}"
assert_equal :belongs_to, reflection.macro
unless reflection.options[:polymorphic]
associated_klass = (reflection.options[:class_name] || association.to_s.classify).constantize
fk = reflection.options[:foreign_key] || associated_klass.name.foreign_key
assert klass.column_names.include?(fk.to_s), "#{klass.name} does not have a #{fk} foreign key."
end
end
end
end
# Ensure that the given class methods are defined on the model.
#
# should_have_class_methods :find, :destroy
#
def should_have_class_methods(*methods)
get_options!(methods)
klass = model_class
methods.each do |method|
should "respond to class method #{method}" do
assert_respond_to klass, method, "#{klass.name} does not have class method #{method}"
end
end
end
# Ensure that the given instance methods are defined on the model.
#
# should_have_instance_methods :email, :name, :name=
#
def should_have_instance_methods(*methods)
get_options!(methods)
klass = model_class
methods.each do |method|
should "respond to instance method #{method}" do
assert_respond_to klass.new, method, "#{klass.name} does not have instance method #{method}"
end
end
end
# Ensure that the given columns are defined on the models backing SQL table.
#
# should_have_db_columns :id, :email, :name, :created_at
#
def should_have_db_columns(*columns)
column_type = get_options!(columns, :type)
klass = model_class
columns.each do |name|
test_name = "have column #{name}"
test_name += " of type #{column_type}" if column_type
should test_name do
column = klass.columns.detect {|c| c.name == name.to_s }
assert column, "#{klass.name} does not have column #{name}"
end
end
end
# Ensure that the given column is defined on the models backing SQL table. The options are the same as
# the instance variables defined on the column definition: :precision, :limit, :default, :null,
# :primary, :type, :scale, and :sql_type.
#
# should_have_db_column :email, :type => "string", :default => nil, :precision => nil, :limit => 255,
# :null => true, :primary => false, :scale => nil, :sql_type => 'varchar(255)'
#
def should_have_db_column(name, opts = {})
klass = model_class
test_name = "have column named :#{name}"
test_name += " with options " + opts.inspect unless opts.empty?
should test_name do
column = klass.columns.detect {|c| c.name == name.to_s }
assert column, "#{klass.name} does not have column #{name}"
opts.each do |k, v|
assert_equal column.instance_variable_get("@#{k}").to_s, v.to_s, ":#{name} column on table for #{klass} does not match option :#{k}"
end
end
end
private
include ThoughtBot::Shoulda::Private
end
end
end

View File

@@ -1,77 +0,0 @@
require 'test/unit/ui/console/testrunner'
# Completely stolen from redgreen gem
#
# Adds colored output to your tests. Specify <tt>color: true</tt> in
# your <tt>~/.shoulda.conf</tt> file to enable.
#
# *Bug*: for some reason, this adds another line of output to the end of
# every rake task, as though there was another (empty) set of tests.
# A fix would be most welcome.
#
module ThoughtBot::Shoulda::Color
COLORS = { :clear => 0, :red => 31, :green => 32, :yellow => 33 } # :nodoc:
def self.method_missing(color_name, *args) # :nodoc:
color(color_name) + args.first + color(:clear)
end
def self.color(color) # :nodoc:
"\e[#{COLORS[color.to_sym]}m"
end
end
module Test # :nodoc:
module Unit # :nodoc:
class TestResult # :nodoc:
alias :old_to_s :to_s
def to_s
if old_to_s =~ /\d+ tests, \d+ assertions, (\d+) failures, (\d+) errors/
ThoughtBot::Shoulda::Color.send($1.to_i != 0 || $2.to_i != 0 ? :red : :green, $&)
end
end
end
class AutoRunner # :nodoc:
alias :old_initialize :initialize
def initialize(standalone)
old_initialize(standalone)
@runner = proc do |r|
Test::Unit::UI::Console::RedGreenTestRunner
end
end
end
class Failure # :nodoc:
alias :old_long_display :long_display
def long_display
# old_long_display.sub('Failure', ThoughtBot::Shoulda::Color.red('Failure'))
ThoughtBot::Shoulda::Color.red(old_long_display)
end
end
class Error # :nodoc:
alias :old_long_display :long_display
def long_display
# old_long_display.sub('Error', ThoughtBot::Shoulda::Color.yellow('Error'))
ThoughtBot::Shoulda::Color.yellow(old_long_display)
end
end
module UI # :nodoc:
module Console # :nodoc:
class RedGreenTestRunner < Test::Unit::UI::Console::TestRunner # :nodoc:
def output_single(something, level=NORMAL)
return unless (output?(level))
something = case something
when '.' then ThoughtBot::Shoulda::Color.green('.')
when 'F' then ThoughtBot::Shoulda::Color.red("F")
when 'E' then ThoughtBot::Shoulda::Color.yellow("E")
else something
end
@io.write(something)
@io.flush
end
end
end
end
end
end

View File

@@ -1,465 +0,0 @@
module ThoughtBot # :nodoc:
module Shoulda # :nodoc:
module Controller
def self.included(other) # :nodoc:
other.class_eval do
extend ThoughtBot::Shoulda::Controller::ClassMethods
include ThoughtBot::Shoulda::Controller::InstanceMethods
ThoughtBot::Shoulda::Controller::ClassMethods::VALID_FORMATS.each do |format|
include "ThoughtBot::Shoulda::Controller::#{format.to_s.upcase}".constantize
end
end
end
# = Macro test helpers for your controllers
#
# By using the macro helpers you can quickly and easily create concise and easy to read test suites.
#
# This code segment:
# context "on GET to :show for first record" do
# setup do
# get :show, :id => 1
# end
#
# should_assign_to :user
# should_respond_with :success
# should_render_template :show
# should_not_set_the_flash
#
# should "do something else really cool" do
# assert_equal 1, assigns(:user).id
# end
# end
#
# Would produce 5 tests for the +show+ action
#
# Furthermore, the should_be_restful helper will create an entire set of tests which will verify that your
# controller responds restfully to a variety of requested formats.
module ClassMethods
# Formats tested by #should_be_restful. Defaults to [:html, :xml]
VALID_FORMATS = Dir.glob(File.join(File.dirname(__FILE__), 'formats', '*.rb')).map { |f| File.basename(f, '.rb') }.map(&:to_sym) # :doc:
VALID_FORMATS.each {|f| require "shoulda/controller_tests/formats/#{f}.rb"}
# Actions tested by #should_be_restful
VALID_ACTIONS = [:index, :show, :new, :edit, :create, :update, :destroy] # :doc:
# A ResourceOptions object is passed into should_be_restful in order to configure the tests for your controller.
#
# Example:
# class UsersControllerTest < Test::Unit::TestCase
# load_all_fixtures
#
# def setup
# ...normal setup code...
# @user = User.find(:first)
# end
#
# should_be_restful do |resource|
# resource.identifier = :id
# resource.klass = User
# resource.object = :user
# resource.parent = []
# resource.actions = [:index, :show, :new, :edit, :update, :create, :destroy]
# resource.formats = [:html, :xml]
#
# resource.create.params = { :name => "bob", :email => 'bob@bob.com', :age => 13}
# resource.update.params = { :name => "sue" }
#
# resource.create.redirect = "user_url(@user)"
# resource.update.redirect = "user_url(@user)"
# resource.destroy.redirect = "users_url"
#
# resource.create.flash = /created/i
# resource.update.flash = /updated/i
# resource.destroy.flash = /removed/i
# end
# end
#
# Whenever possible, the resource attributes will be set to sensible defaults.
#
class ResourceOptions
# Configuration options for the create, update, destroy actions under should_be_restful
class ActionOptions
# String evaled to get the target of the redirection.
# All of the instance variables set by the controller will be available to the
# evaled code.
#
# Example:
# resource.create.redirect = "user_url(@user.company, @user)"
#
# Defaults to a generated url based on the name of the controller, the action, and the resource.parents list.
attr_accessor :redirect
# String or Regexp describing a value expected in the flash. Will match against any flash key.
#
# Defaults:
# destroy:: /removed/
# create:: /created/
# update:: /updated/
attr_accessor :flash
# Hash describing the params that should be sent in with this action.
attr_accessor :params
end
# Configuration options for the denied actions under should_be_restful
#
# Example:
# context "The public" do
# setup do
# @request.session[:logged_in] = false
# end
#
# should_be_restful do |resource|
# resource.parent = :user
#
# resource.denied.actions = [:index, :show, :edit, :new, :create, :update, :destroy]
# resource.denied.flash = /get outta here/i
# resource.denied.redirect = 'new_session_url'
# end
# end
#
class DeniedOptions
# String evaled to get the target of the redirection.
# All of the instance variables set by the controller will be available to the
# evaled code.
#
# Example:
# resource.create.redirect = "user_url(@user.company, @user)"
attr_accessor :redirect
# String or Regexp describing a value expected in the flash. Will match against any flash key.
#
# Example:
# resource.create.flash = /created/
attr_accessor :flash
# Actions that should be denied (only used by resource.denied). <i>Note that these actions will
# only be tested if they are also listed in +resource.actions+</i>
# The special value of :all will deny all of the REST actions.
attr_accessor :actions
end
# Name of key in params that references the primary key.
# Will almost always be :id (default), unless you are using a plugin or have patched rails.
attr_accessor :identifier
# Name of the ActiveRecord class this resource is responsible for. Automatically determined from
# test class if not explicitly set. UserTest => :user
attr_accessor :klass
# Name of the instantiated ActiveRecord object that should be used by some of the tests.
# Defaults to the underscored name of the AR class. CompanyManager => :company_manager
attr_accessor :object
# Name of the parent AR objects.
#
# Example:
# # in the routes...
# map.resources :companies do
# map.resources :people do
# map.resources :limbs
# end
# end
#
# # in the tests...
# class PeopleControllerTest < Test::Unit::TestCase
# should_be_restful do |resource|
# resource.parent = :companies
# end
# end
#
# class LimbsControllerTest < Test::Unit::TestCase
# should_be_restful do |resource|
# resource.parents = [:companies, :people]
# end
# end
attr_accessor :parent
alias parents parent
alias parents= parent=
# Actions that should be tested. Must be a subset of VALID_ACTIONS (default).
# Tests for each actionw will only be generated if the action is listed here.
# The special value of :all will test all of the REST actions.
#
# Example (for a read-only controller):
# resource.actions = [:show, :index]
attr_accessor :actions
# Formats that should be tested. Must be a subset of VALID_FORMATS (default).
# Each action will be tested against the formats listed here. The special value
# of :all will test all of the supported formats.
#
# Example:
# resource.actions = [:html, :xml]
attr_accessor :formats
# ActionOptions object specifying options for the create action.
attr_accessor :create
# ActionOptions object specifying options for the update action.
attr_accessor :update
# ActionOptions object specifying options for the desrtoy action.
attr_accessor :destroy
# DeniedOptions object specifying which actions should return deny a request, and what should happen in that case.
attr_accessor :denied
def initialize # :nodoc:
@create = ActionOptions.new
@update = ActionOptions.new
@destroy = ActionOptions.new
@denied = DeniedOptions.new
@create.flash ||= /created/i
@update.flash ||= /updated/i
@destroy.flash ||= /removed/i
@denied.flash ||= /denied/i
@create.params ||= {}
@update.params ||= {}
@actions = VALID_ACTIONS
@formats = VALID_FORMATS
@denied.actions = []
end
def normalize!(target) # :nodoc:
@denied.actions = VALID_ACTIONS if @denied.actions == :all
@actions = VALID_ACTIONS if @actions == :all
@formats = VALID_FORMATS if @formats == :all
@denied.actions = @denied.actions.map(&:to_sym)
@actions = @actions.map(&:to_sym)
@formats = @formats.map(&:to_sym)
ensure_valid_members(@actions, VALID_ACTIONS, 'actions')
ensure_valid_members(@denied.actions, VALID_ACTIONS, 'denied.actions')
ensure_valid_members(@formats, VALID_FORMATS, 'formats')
@identifier ||= :id
@klass ||= target.name.gsub(/ControllerTest$/, '').singularize.constantize
@object ||= @klass.name.tableize.singularize
@parent ||= []
@parent = [@parent] unless @parent.is_a? Array
collection_helper = [@parent, @object.pluralize, 'url'].flatten.join('_')
collection_args = @parent.map {|n| "@#{object}.#{n}"}.join(', ')
@destroy.redirect ||= "#{collection_helper}(#{collection_args})"
member_helper = [@parent, @object, 'url'].flatten.join('_')
member_args = [@parent.map {|n| "@#{object}.#{n}"}, "@#{object}"].flatten.join(', ')
@create.redirect ||= "#{member_helper}(#{member_args})"
@update.redirect ||= "#{member_helper}(#{member_args})"
@denied.redirect ||= "new_session_url"
end
private
def ensure_valid_members(ary, valid_members, name) # :nodoc:
invalid = ary - valid_members
raise ArgumentError, "Unsupported #{name}: #{invalid.inspect}" unless invalid.empty?
end
end
# :section: should_be_restful
# Generates a full suite of tests for a restful controller.
#
# The following definition will generate tests for the +index+, +show+, +new+,
# +edit+, +create+, +update+ and +destroy+ actions, in both +html+ and +xml+ formats:
#
# should_be_restful do |resource|
# resource.parent = :user
#
# resource.create.params = { :title => "first post", :body => 'blah blah blah'}
# resource.update.params = { :title => "changed" }
# end
#
# This generates about 40 tests, all of the format:
# "on GET to :show should assign @user."
# "on GET to :show should not set the flash."
# "on GET to :show should render 'show' template."
# "on GET to :show should respond with success."
# "on GET to :show as xml should assign @user."
# "on GET to :show as xml should have ContentType set to 'application/xml'."
# "on GET to :show as xml should respond with success."
# "on GET to :show as xml should return <user/> as the root element."
# The +resource+ parameter passed into the block is a ResourceOptions object, and
# is used to configure the tests for the details of your resources.
#
def should_be_restful(&blk) # :yields: resource
resource = ResourceOptions.new
blk.call(resource)
resource.normalize!(self)
resource.formats.each do |format|
resource.actions.each do |action|
if self.respond_to? :"make_#{action}_#{format}_tests"
self.send(:"make_#{action}_#{format}_tests", resource)
else
should "test #{action} #{format}" do
flunk "Test for #{action} as #{format} not implemented"
end
end
end
end
end
# :section: Test macros
# Macro that creates a test asserting that the flash contains the given value.
# val can be a String, a Regex, or nil (indicating that the flash should not be set)
#
# Example:
#
# should_set_the_flash_to "Thank you for placing this order."
# should_set_the_flash_to /created/i
# should_set_the_flash_to nil
def should_set_the_flash_to(val)
if val
should "have #{val.inspect} in the flash" do
assert_contains flash.values, val, ", Flash: #{flash.inspect}"
end
else
should "not set the flash" do
assert_equal({}, flash, "Flash was set to:\n#{flash.inspect}")
end
end
end
# Macro that creates a test asserting that the flash is empty. Same as
# @should_set_the_flash_to nil@
def should_not_set_the_flash
should_set_the_flash_to nil
end
# Macro that creates a test asserting that the controller assigned to @name
#
# Example:
#
# should_assign_to :user
def should_assign_to(name)
should "assign @#{name}" do
assert assigns(name.to_sym), "The action isn't assigning to @#{name}"
end
end
# Macro that creates a test asserting that the controller did not assign to @name
#
# Example:
#
# should_not_assign_to :user
def should_not_assign_to(name)
should "not assign to @#{name}" do
assert !assigns(name.to_sym), "@#{name} was visible"
end
end
# Macro that creates a test asserting that the controller responded with a 'response' status code.
# Example:
#
# should_respond_with :success
def should_respond_with(response)
should "respond with #{response}" do
assert_response response
end
end
# Macro that creates a test asserting that the controller rendered the given template.
# Example:
#
# should_render_template :new
def should_render_template(template)
should "render '#{template}' template" do
assert_template template.to_s
end
end
# Macro that creates a test asserting that the controller returned a redirect to the given path.
# The given string is evaled to produce the resulting redirect path. All of the instance variables
# set by the controller are available to the evaled string.
# Example:
#
# should_redirect_to '"/"'
# should_redirect_to "users_url(@user)"
def should_redirect_to(url)
should "redirect to \"#{url}\"" do
instantiate_variables_from_assigns do
assert_redirected_to eval(url, self.send(:binding), __FILE__, __LINE__)
end
end
end
# Macro that creates a test asserting that the rendered view contains a <form> element.
def should_render_a_form
should "display a form" do
assert_select "form", true, "The template doesn't contain a <form> element"
end
end
end
module InstanceMethods # :nodoc:
private # :enddoc:
SPECIAL_INSTANCE_VARIABLES = %w{
_cookies
_flash
_headers
_params
_request
_response
_session
action_name
before_filter_chain_aborted
cookies
flash
headers
ignore_missing_templates
logger
params
request
request_origin
response
session
template
template_class
template_root
url
variables_added
}.map(&:to_s)
def instantiate_variables_from_assigns(*names, &blk)
old = {}
names = (@response.template.assigns.keys - SPECIAL_INSTANCE_VARIABLES) if names.empty?
names.each do |name|
old[name] = instance_variable_get("@#{name}")
instance_variable_set("@#{name}", assigns(name.to_sym))
end
blk.call
names.each do |name|
instance_variable_set("@#{name}", old[name])
end
end
def get_existing_record(res) # :nodoc:
returning(instance_variable_get("@#{res.object}")) do |record|
assert(record, "This test requires you to set @#{res.object} in your setup block")
end
end
def make_parent_params(resource, record = nil, parent_names = nil) # :nodoc:
parent_names ||= resource.parents.reverse
return {} if parent_names == [] # Base case
parent_name = parent_names.shift
parent = record ? record.send(parent_name) : parent_name.to_s.classify.constantize.find(:first)
{ :"#{parent_name}_id" => parent.id }.merge(make_parent_params(resource, parent, parent_names))
end
end
end
end
end

View File

@@ -1,195 +0,0 @@
module ThoughtBot # :nodoc:
module Shoulda # :nodoc:
module Controller # :nodoc:
module HTML # :nodoc: all
def self.included(other)
other.class_eval do
extend ThoughtBot::Shoulda::Controller::HTML::ClassMethods
end
end
module ClassMethods
def make_show_html_tests(res)
context "on GET to :show" do
setup do
record = get_existing_record(res)
parent_params = make_parent_params(res, record)
get :show, parent_params.merge({ res.identifier => record.to_param })
end
if res.denied.actions.include?(:show)
should_not_assign_to res.object
should_redirect_to res.denied.redirect
should_set_the_flash_to res.denied.flash
else
should_assign_to res.object
should_respond_with :success
should_render_template :show
should_not_set_the_flash
end
end
end
def make_edit_html_tests(res)
context "on GET to :edit" do
setup do
@record = get_existing_record(res)
parent_params = make_parent_params(res, @record)
get :edit, parent_params.merge({ res.identifier => @record.to_param })
end
if res.denied.actions.include?(:edit)
should_not_assign_to res.object
should_redirect_to res.denied.redirect
should_set_the_flash_to res.denied.flash
else
should_assign_to res.object
should_respond_with :success
should_render_template :edit
should_not_set_the_flash
should_render_a_form
should "set @#{res.object} to requested instance" do
assert_equal @record, assigns(res.object)
end
end
end
end
def make_index_html_tests(res)
context "on GET to :index" do
setup do
record = get_existing_record(res) rescue nil
parent_params = make_parent_params(res, record)
get(:index, parent_params)
end
if res.denied.actions.include?(:index)
should_not_assign_to res.object.to_s.pluralize
should_redirect_to res.denied.redirect
should_set_the_flash_to res.denied.flash
else
should_respond_with :success
should_assign_to res.object.to_s.pluralize
should_render_template :index
should_not_set_the_flash
end
end
end
def make_new_html_tests(res)
context "on GET to :new" do
setup do
record = get_existing_record(res) rescue nil
parent_params = make_parent_params(res, record)
get(:new, parent_params)
end
if res.denied.actions.include?(:new)
should_not_assign_to res.object
should_redirect_to res.denied.redirect
should_set_the_flash_to res.denied.flash
else
should_respond_with :success
should_assign_to res.object
should_not_set_the_flash
should_render_template :new
should_render_a_form
end
end
end
def make_destroy_html_tests(res)
context "on DELETE to :destroy" do
setup do
@record = get_existing_record(res)
parent_params = make_parent_params(res, @record)
delete :destroy, parent_params.merge({ res.identifier => @record.to_param })
end
if res.denied.actions.include?(:destroy)
should_redirect_to res.denied.redirect
should_set_the_flash_to res.denied.flash
should "not destroy record" do
assert_nothing_raised { assert @record.reload }
end
else
should_set_the_flash_to res.destroy.flash
if res.destroy.redirect.is_a? Symbol
should_respond_with res.destroy.redirect
else
should_redirect_to res.destroy.redirect
end
should "destroy record" do
assert_raises(::ActiveRecord::RecordNotFound) { @record.reload }
end
end
end
end
def make_create_html_tests(res)
context "on POST to :create with #{res.create.params.inspect}" do
setup do
record = get_existing_record(res) rescue nil
parent_params = make_parent_params(res, record)
@count = res.klass.count
post :create, parent_params.merge(res.object => res.create.params)
end
if res.denied.actions.include?(:create)
should_redirect_to res.denied.redirect
should_set_the_flash_to res.denied.flash
should_not_assign_to res.object
should "not create new record" do
assert_equal @count, res.klass.count
end
else
should_assign_to res.object
should_set_the_flash_to res.create.flash
if res.create.redirect.is_a? Symbol
should_respond_with res.create.redirect
else
should_redirect_to res.create.redirect
end
should "not have errors on @#{res.object}" do
assert_equal [], assigns(res.object).errors.full_messages, "@#{res.object} has errors:"
end
end
end
end
def make_update_html_tests(res)
context "on PUT to :update with #{res.create.params.inspect}" do
setup do
@record = get_existing_record(res)
parent_params = make_parent_params(res, @record)
put :update, parent_params.merge(res.identifier => @record.to_param, res.object => res.update.params)
end
if res.denied.actions.include?(:update)
should_not_assign_to res.object
should_redirect_to res.denied.redirect
should_set_the_flash_to res.denied.flash
else
should_assign_to res.object
should_set_the_flash_to(res.update.flash)
if res.update.redirect.is_a? Symbol
should_respond_with res.update.redirect
else
should_redirect_to res.update.redirect
end
should "not have errors on @#{res.object}" do
assert_equal [], assigns(res.object).errors.full_messages, "@#{res.object} has errors:"
end
end
end
end
end
end
end
end
end

View File

@@ -1,162 +0,0 @@
module ThoughtBot # :nodoc:
module Shoulda # :nodoc:
module Controller # :nodoc:
module XML
def self.included(other) #:nodoc:
other.class_eval do
extend ThoughtBot::Shoulda::Controller::XML::ClassMethods
end
end
module ClassMethods
# Macro that creates a test asserting that the controller responded with an XML content-type
# and that the XML contains +<name/>+ as the root element.
def should_respond_with_xml_for(name = nil)
should "have ContentType set to 'application/xml'" do
assert_xml_response
end
if name
should "return <#{name}/> as the root element" do
body = @response.body.first(100).map {|l| " #{l}"}
assert_select name.to_s.dasherize, 1, "Body:\n#{body}...\nDoes not have <#{name}/> as the root element."
end
end
end
alias should_respond_with_xml should_respond_with_xml_for
protected
def make_show_xml_tests(res) # :nodoc:
context "on GET to :show as xml" do
setup do
request_xml
record = get_existing_record(res)
parent_params = make_parent_params(res, record)
get :show, parent_params.merge({ res.identifier => record.to_param })
end
if res.denied.actions.include?(:show)
should_not_assign_to res.object
should_respond_with 401
else
should_assign_to res.object
should_respond_with :success
should_respond_with_xml_for res.object
end
end
end
def make_edit_xml_tests(res) # :nodoc:
# XML doesn't need an :edit action
end
def make_new_xml_tests(res) # :nodoc:
# XML doesn't need a :new action
end
def make_index_xml_tests(res) # :nodoc:
context "on GET to :index as xml" do
setup do
request_xml
parent_params = make_parent_params(res)
get(:index, parent_params)
end
if res.denied.actions.include?(:index)
should_not_assign_to res.object.to_s.pluralize
should_respond_with 401
else
should_respond_with :success
should_respond_with_xml_for res.object.to_s.pluralize
should_assign_to res.object.to_s.pluralize
end
end
end
def make_destroy_xml_tests(res) # :nodoc:
context "on DELETE to :destroy as xml" do
setup do
request_xml
@record = get_existing_record(res)
parent_params = make_parent_params(res, @record)
delete :destroy, parent_params.merge({ res.identifier => @record.to_param })
end
if res.denied.actions.include?(:destroy)
should_respond_with 401
should "not destroy record" do
assert @record.reload
end
else
should "destroy record" do
assert_raises(::ActiveRecord::RecordNotFound) { @record.reload }
end
end
end
end
def make_create_xml_tests(res) # :nodoc:
context "on POST to :create as xml" do
setup do
request_xml
parent_params = make_parent_params(res)
@count = res.klass.count
post :create, parent_params.merge(res.object => res.create.params)
end
if res.denied.actions.include?(:create)
should_respond_with 401
should_not_assign_to res.object
should "not create new record" do
assert_equal @count, res.klass.count
end
else
should_assign_to res.object
should "not have errors on @#{res.object}" do
assert_equal [], assigns(res.object).errors.full_messages, "@#{res.object} has errors:"
end
end
end
end
def make_update_xml_tests(res) # :nodoc:
context "on PUT to :update as xml" do
setup do
request_xml
@record = get_existing_record(res)
parent_params = make_parent_params(res, @record)
put :update, parent_params.merge(res.identifier => @record.to_param, res.object => res.update.params)
end
if res.denied.actions.include?(:update)
should_not_assign_to res.object
should_respond_with 401
else
should_assign_to res.object
should "not have errors on @#{res.object}" do
assert_equal [], assigns(res.object).errors.full_messages, "@#{res.object} has errors:"
end
end
end
end
end
# Sets the next request's format to 'application/xml'
def request_xml
@request.accept = "application/xml"
end
# Asserts that the controller's response was 'application/xml'
def assert_xml_response
assert_equal "application/xml", @response.content_type, "Body: #{@response.body.first(100).chomp}..."
end
end
end
end
end

View File

@@ -1,14 +0,0 @@
# Stolen straight from ActiveSupport
class Proc #:nodoc:
def bind(object)
block, time = self, Time.now
(class << object; self end).class_eval do
method_name = "__bind_#{time.to_i}_#{time.usec}"
define_method(method_name, &block)
method = instance_method(method_name)
remove_method(method_name)
method
end.bind(object)
end
end

View File

@@ -1,165 +0,0 @@
require File.join(File.dirname(__FILE__), 'proc_extensions')
module Thoughtbot
class Shoulda
VERSION = '1.0.0'
# = context and should blocks
#
# A context block groups should statements under a common setup/teardown method.
# Context blocks can be arbitrarily nested, and can do wonders for improving the maintainability
# and readability of your test code.
#
# A context block can contain setup, should, should_eventually, and teardown blocks.
#
# class UserTest << Test::Unit::TestCase
# context "a User instance" do
# setup do
# @user = User.find(:first)
# end
#
# should "return its full name"
# assert_equal 'John Doe', @user.full_name
# end
# end
# end
#
# This code will produce the method <tt>"test a User instance should return its full name"</tt>.
#
# Contexts may be nested. Nested contexts run their setup blocks from out to in before each test.
# They then run their teardown blocks from in to out after each test.
#
# class UserTest << Test::Unit::TestCase
# context "a User instance" do
# setup do
# @user = User.find(:first)
# end
#
# should "return its full name"
# assert_equal 'John Doe', @user.full_name
# end
#
# context "with a profile" do
# setup do
# @user.profile = Profile.find(:first)
# end
#
# should "return true when sent :has_profile?"
# assert @user.has_profile?
# end
# end
# end
# end
#
# This code will produce the following methods
# * <tt>"test: a User instance should return its full name."</tt>
# * <tt>"test: a User instance with a profile should return true when sent :has_profile?."</tt>
#
# <b>A context block can exist next to normal <tt>def test_the_old_way; end</tt> tests</b>,
# meaning you do not have to fully commit to the context/should syntax in a test file.
#
module ClassMethods
def self.included(other) # :nodoc:
@@context_names = []
@@setup_blocks = []
@@teardown_blocks = []
end
# Defines a test method. Can be called either inside our outside of a context.
# Optionally specify <tt>:unimplimented => true</tt> (see should_eventually).
#
# Example:
#
# class UserTest << Test::Unit::TestCase
# should "return first user on find(:first)"
# assert_equal users(:first), User.find(:first)
# end
# end
#
# Would create a test named
# 'test: should return first user on find(:first)'
#
def should(name, opts = {}, &should_block)
test_name = ["test:", @@context_names, "should", "#{name}. "].flatten.join(' ').to_sym
name_defined = eval("self.instance_methods.include?('#{test_name.to_s.gsub(/['"]/, '\$1')}')", should_block.binding)
raise ArgumentError, "'#{test_name}' is already defined" and return if name_defined
setup_blocks = @@setup_blocks.dup
teardown_blocks = @@teardown_blocks.dup
if opts[:unimplemented]
define_method test_name do |*args|
# XXX find a better way of doing this.
assert true
STDOUT.putc "X" # Tests for this model are missing.
end
else
define_method test_name do |*args|
begin
setup_blocks.each {|b| b.bind(self).call }
should_block.bind(self).call(*args)
ensure
teardown_blocks.reverse.each {|b| b.bind(self).call }
end
end
end
end
# Creates a context block with the given name.
def context(name, &context_block)
saved_setups = @@setup_blocks.dup
saved_teardowns = @@teardown_blocks.dup
saved_contexts = @@context_names.dup
@@setup_defined = false
@@context_names << name
context_block.bind(self).call
@@context_names = saved_contexts
@@setup_blocks = saved_setups
@@teardown_blocks = saved_teardowns
end
# Run before every should block in the current context.
# If a setup block appears in a nested context, it will be run after the setup blocks
# in the parent contexts.
def setup(&setup_block)
if @@setup_defined
raise RuntimeError, "Either you have two setup blocks in one context, " +
"or a setup block outside of a context. Both are equally bad."
end
@@setup_defined = true
@@setup_blocks << setup_block
end
# Run after every should block in the current context.
# If a teardown block appears in a nested context, it will be run before the teardown
# blocks in the parent contexts.
def teardown(&teardown_block)
@@teardown_blocks << teardown_block
end
# Defines a specification that is not yet implemented.
# Will be displayed as an 'X' when running tests, and failures will not be shown.
# This is equivalent to:
# should(name, {:unimplemented => true}, &block)
def should_eventually(name, &block)
should("eventually #{name}", {:unimplemented => true}, &block)
end
end
end
end
module Test # :nodoc: all
module Unit
class TestCase
class << self
include Thoughtbot::Shoulda::ClassMethods
end
end
end
end

View File

@@ -1,101 +0,0 @@
module ThoughtBot # :nodoc:
module Shoulda # :nodoc:
module General
def self.included(other) # :nodoc:
other.class_eval do
extend ThoughtBot::Shoulda::General::ClassMethods
# include ThoughtBot::Shoulda::General::InstanceMethods
end
end
module ClassMethods
# Loads all fixture files (<tt>test/fixtures/*.yml</tt>)
def load_all_fixtures
all_fixtures = Dir.glob(File.join(Test::Unit::TestCase.fixture_path, "*.yml")).collect do |f|
File.basename(f, '.yml').to_sym
end
fixtures *all_fixtures
end
end
# Prints a message to stdout, tagged with the name of the calling method.
def report!(msg = "")
puts("#{caller.first}: #{msg}")
end
# Asserts that two arrays contain the same elements, the same number of times. Essentially ==, but unordered.
#
# assert_same_elements([:a, :b, :c], [:c, :a, :b]) => passes
def assert_same_elements(a1, a2, msg = nil)
[:select, :inject, :size].each do |m|
[a1, a2].each {|a| assert_respond_to(a, m, "Are you sure that #{a.inspect} is an array? It doesn't respond to #{m}.") }
end
assert a1h = a1.inject({}) { |h,e| h[e] = a1.select { |i| i == e }.size; h }
assert a2h = a2.inject({}) { |h,e| h[e] = a2.select { |i| i == e }.size; h }
assert_equal(a1h, a2h, msg)
end
# Asserts that the given collection contains item x. If x is a regular expression, ensure that
# at least one element from the collection matches x. +extra_msg+ is appended to the error message if the assertion fails.
#
# assert_contains(['a', '1'], /\d/) => passes
# assert_contains(['a', '1'], 'a') => passes
# assert_contains(['a', '1'], /not there/) => fails
def assert_contains(collection, x, extra_msg = "")
collection = [collection] unless collection.is_a?(Array)
msg = "#{x.inspect} not found in #{collection.to_a.inspect} " + extra_msg
case x
when Regexp: assert(collection.detect { |e| e =~ x }, msg)
else assert(collection.include?(x), msg)
end
end
# Asserts that the given collection does not contain item x. If x is a regular expression, ensure that
# none of the elements from the collection match x.
def assert_does_not_contain(collection, x, extra_msg = "")
collection = [collection] unless collection.is_a?(Array)
msg = "#{x.inspect} found in #{collection.to_a.inspect} " + extra_msg
case x
when Regexp: assert(!collection.detect { |e| e =~ x }, msg)
else assert(!collection.include?(x), msg)
end
end
# Asserts that the given object can be saved
#
# assert_save User.new(params)
def assert_save(obj)
assert obj.save, "Errors: #{obj.errors.full_messages.join('; ')}"
obj.reload
end
# Asserts that the given object is valid
#
# assert_save User.new(params)
def assert_valid(obj)
assert obj.valid?, "Errors: #{obj.errors.full_messages.join('; ')}"
end
# Asserts that the block uses ActionMailer to send emails
#
# assert_sends_email(2) { Mailer.deliver_messages }
def assert_sends_email(num = 1, &blk)
ActionMailer::Base.deliveries.clear
blk.call
msg = "Sent #{ActionMailer::Base.deliveries.size} emails, when #{num} expected:\n"
ActionMailer::Base.deliveries.each { |m| msg << " '#{m.subject}' sent to #{m.to.to_sentence}\n" }
assert(num == ActionMailer::Base.deliveries.size, msg)
end
# Asserts that the block does not send emails thorough ActionMailer
#
# assert_does_not_send_email { # do nothing }
def assert_does_not_send_email(&blk)
assert_sends_email 0, &blk
end
end
end
end

View File

@@ -1,17 +0,0 @@
module ThoughtBot # :nodoc:
module Shoulda # :nodoc:
module Private # :nodoc:
def get_options!(args, *wanted)
ret = []
opts = (args.last.is_a?(Hash) ? args.pop : {})
wanted.each {|w| ret << opts.delete(w)}
raise ArgumentError, "Unsuported options given: #{opts.keys.join(', ')}" unless opts.keys.empty?
return *ret
end
def model_class
self.name.gsub(/Test$/, '').constantize
end
end
end
end

View File

@@ -1,40 +0,0 @@
namespace :shoulda do
desc "List the names of the test methods in a specification like format"
task :list do
require 'test/unit'
require 'rubygems'
require 'active_support'
# bug in test unit. Set to true to stop from running.
Test::Unit.run = true
test_files = Dir.glob(File.join('test', '**', '*_test.rb'))
test_files.each do |file|
load file
klass = File.basename(file, '.rb').classify.constantize
puts
puts "#{klass.name.gsub(/Test$/, '')}"
test_methods = klass.instance_methods.grep(/^test/).map {|s| s.gsub(/^test: /, '')}.sort
test_methods.each {|m| puts " - #{m}" }
# puts "#{klass.name.gsub(/Test$/, '')}"
# test_methods = klass.instance_methods.grep(/^test/).sort
#
# method_hash = test_methods.inject({}) do |h, name|
# header = name.gsub(/^test: (.*)should.*$/, '\1')
# test = name.gsub(/^test:.*should (.*)$/, '\1')
# h[header] ||= []
# h[header] << test
# h
# end
#
# method_hash.keys.sort.each do |header|
# puts " #{header.chomp} should"
# method_hash[header].each do |test|
# puts " - #{test}"
# end
# end
end
end
end

View File

@@ -1,28 +0,0 @@
namespace :shoulda do
# From http://blog.internautdesign.com/2007/11/2/a-yaml_to_shoulda-rake-task
# David.Lowenfels@gmail.com
desc "Converts a YAML file (FILE=./path/to/yaml) into a Shoulda skeleton"
task :from_yaml do
require 'yaml'
def yaml_to_context(hash, indent = 0)
indent1 = ' ' * indent
indent2 = ' ' * (indent + 1)
hash.each_pair do |context, shoulds|
puts indent1 + "context \"#{context}\" do"
puts
shoulds.each do |should|
yaml_to_context( should, indent + 1 ) and next if should.is_a?( Hash )
puts indent2 + "should_eventually \"" + should.gsub(/^should +/,'') + "\" do"
puts indent2 + "end"
puts
end
puts indent1 + "end"
end
end
puts("Please pass in a FILE argument.") and exit unless ENV['FILE']
yaml_to_context( YAML.load_file( ENV['FILE'] ) )
end
end

View File

@@ -1,8 +0,0 @@
The tests for should have two dependencies that I know of:
* Rails version 1.2.3
* A working sqlite3 installation.
If you have problems running these tests, please notify the shoulda mailing list: shoulda@googlegroups.com
- Tammer Saleh

View File

@@ -1,5 +0,0 @@
first:
id: 1
title: My Cute Kitten!
body: This is totally a cute kitten
user_id: 1

View File

@@ -1,9 +0,0 @@
first:
id: 1
name: Stuff
second:
id: 2
name: Rails
third:
id: 3
name: Nothing

View File

@@ -1,5 +0,0 @@
first:
id: 1
name: Some dude
age: 2
email: none@none.com

View File

@@ -1,43 +0,0 @@
require File.dirname(__FILE__) + '/../test_helper'
require 'posts_controller'
# Re-raise errors caught by the controller.
class PostsController; def rescue_action(e) raise e end; end
class PostsControllerTest < Test::Unit::TestCase
load_all_fixtures
def setup
@controller = PostsController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
@post = Post.find(:first)
end
context "The public" do
setup do
@request.session[:logged_in] = false
end
should_be_restful do |resource|
resource.parent = :user
resource.denied.actions = [:index, :show, :edit, :new, :create, :update, :destroy]
resource.denied.flash = /what/i
resource.denied.redirect = '"/"'
end
end
context "Logged in" do
setup do
@request.session[:logged_in] = true
end
should_be_restful do |resource|
resource.parent = :user
resource.create.params = { :title => "first post", :body => 'blah blah blah'}
resource.update.params = { :title => "changed" }
end
end
end

View File

@@ -1,36 +0,0 @@
require File.dirname(__FILE__) + '/../test_helper'
require 'users_controller'
# Re-raise errors caught by the controller.
class UsersController; def rescue_action(e) raise e end; end
class UsersControllerTest < Test::Unit::TestCase
load_all_fixtures
def setup
@controller = UsersController.new
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
@user = User.find(:first)
end
should_be_restful do |resource|
resource.identifier = :id
resource.klass = User
resource.object = :user
resource.parent = []
resource.actions = [:index, :show, :new, :edit, :update, :create, :destroy]
resource.formats = [:html, :xml]
resource.create.params = { :name => "bob", :email => 'bob@bob.com', :age => 13}
resource.update.params = { :name => "sue" }
resource.create.redirect = "user_url(@user)"
resource.update.redirect = "user_url(@user)"
resource.destroy.redirect = "users_url"
resource.create.flash = /created/i
resource.update.flash = /updated/i
resource.destroy.flash = /removed/i
end
end

View File

@@ -1,71 +0,0 @@
require File.join(File.dirname(__FILE__), '..', 'test_helper')
class ContextTest < Test::Unit::TestCase # :nodoc:
context "context with setup block" do
setup do
@blah = "blah"
end
should "have @blah == 'blah'" do
assert_equal "blah", @blah
end
should "have name set right" do
assert_match(/^test: context with setup block/, self.to_s)
end
context "and a subcontext" do
setup do
@blah = "#{@blah} twice"
end
should "be named correctly" do
assert_match(/^test: context with setup block and a subcontext should be named correctly/, self.to_s)
end
should "run the setup methods in order" do
assert_equal @blah, "blah twice"
end
end
end
context "another context with setup block" do
setup do
@blah = "foo"
end
should "have @blah == 'foo'" do
assert_equal "foo", @blah
end
should "have name set right" do
assert_match(/^test: another context with setup block/, self.to_s)
end
end
context "context with method definition" do
setup do
def hello; "hi"; end
end
should "be able to read that method" do
assert_equal "hi", hello
end
should "have name set right" do
assert_match(/^test: context with method definition/, self.to_s)
end
end
context "another context" do
should "not define @blah" do
assert_nil @blah
end
end
should_eventually "should pass, since it's unimplemented" do
flunk "what?"
end
end

View File

@@ -1,40 +0,0 @@
require File.join(File.dirname(__FILE__), '..', 'test_helper')
class Val
@@val = 0
def self.val; @@val; end
def self.inc(i=1); @@val += i; end
end
class HelpersTest < Test::Unit::TestCase # :nodoc:
context "an array of values" do
setup do
@a = ['abc', 'def', 3]
end
[/b/, 'abc', 3].each do |x|
should "contain #{x.inspect}" do
assert_raises(Test::Unit::AssertionFailedError) do
assert_does_not_contain @a, x
end
assert_contains @a, x
end
end
should "not contain 'wtf'" do
assert_raises(Test::Unit::AssertionFailedError) {assert_contains @a, 'wtf'}
assert_does_not_contain @a, 'wtf'
end
should "be the same as another array, ordered differently" do
assert_same_elements(@a, [3, "def", "abc"])
assert_raises(Test::Unit::AssertionFailedError) do
assert_same_elements(@a, [3, 3, "def", "abc"])
end
assert_raises(Test::Unit::AssertionFailedError) do
assert_same_elements([@a, "abc"].flatten, [3, 3, "def", "abc"])
end
end
end
end

View File

@@ -1,26 +0,0 @@
require File.join(File.dirname(__FILE__), '..', 'test_helper')
class PrivateHelpersTest < Test::Unit::TestCase # :nodoc:
include ThoughtBot::Shoulda::ActiveRecord
context "get_options!" do
should "remove opts from args" do
args = [:a, :b, {}]
get_options!(args)
assert_equal [:a, :b], args
end
should "return wanted opts in order" do
args = [{:one => 1, :two => 2}]
one, two = get_options!(args, :one, :two)
assert_equal 1, one
assert_equal 2, two
end
should "raise ArgumentError if given unwanted option" do
args = [{:one => 1, :two => 2}]
assert_raises ArgumentError do
get_options!(args, :one)
end
end
end
end

View File

@@ -1,10 +0,0 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require(File.join(File.dirname(__FILE__), 'config', 'boot'))
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
require 'tasks/rails'

View File

@@ -1,25 +0,0 @@
# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.
class ApplicationController < ActionController::Base
# Pick a unique cookie name to distinguish our session data from others'
session :session_key => '_rails_root_session_id'
def ensure_logged_in
unless session[:logged_in]
respond_to do |accepts|
accepts.html do
flash[:error] = 'What do you think you\'re doing?'
redirect_to '/'
end
accepts.xml do
headers["Status"] = "Unauthorized"
headers["WWW-Authenticate"] = %(Basic realm="Web Password")
render :text => "Couldn't authenticate you", :status => '401 Unauthorized'
end
end
return false
end
return true
end
end

View File

@@ -1,78 +0,0 @@
class PostsController < ApplicationController
before_filter :ensure_logged_in
before_filter :load_user
def index
@posts = @user.posts
respond_to do |format|
format.html # index.rhtml
format.xml { render :xml => @posts.to_xml }
end
end
def show
@post = @user.posts.find(params[:id])
respond_to do |format|
format.html # show.rhtml
format.xml { render :xml => @post.to_xml }
end
end
def new
@post = @user.posts.build
end
def edit
@post = @user.posts.find(params[:id])
end
def create
@post = @user.posts.build(params[:post])
respond_to do |format|
if @post.save
flash[:notice] = 'Post was successfully created.'
format.html { redirect_to post_url(@post.user, @post) }
format.xml { head :created, :location => post_url(@post.user, @post) }
else
format.html { render :action => "new" }
format.xml { render :xml => @post.errors.to_xml }
end
end
end
def update
@post = @user.posts.find(params[:id])
respond_to do |format|
if @post.update_attributes(params[:post])
flash[:notice] = 'Post was successfully updated.'
format.html { redirect_to post_url(@post.user, @post) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @post.errors.to_xml }
end
end
end
def destroy
@post = @user.posts.find(params[:id])
@post.destroy
flash[:notice] = "Post was removed"
respond_to do |format|
format.html { redirect_to posts_url(@post.user) }
format.xml { head :ok }
end
end
private
def load_user
@user = User.find(params[:user_id])
end
end

View File

@@ -1,81 +0,0 @@
class UsersController < ApplicationController
# GET /users
# GET /users.xml
def index
@users = User.find(:all)
respond_to do |format|
format.html # index.rhtml
format.xml { render :xml => @users.to_xml }
end
end
# GET /users/1
# GET /users/1.xml
def show
@user = User.find(params[:id])
respond_to do |format|
format.html # show.rhtml
format.xml { render :xml => @user.to_xml }
end
end
# GET /users/new
def new
@user = User.new
end
# GET /users/1;edit
def edit
@user = User.find(params[:id])
end
# POST /users
# POST /users.xml
def create
@user = User.new(params[:user])
respond_to do |format|
if @user.save
flash[:notice] = 'User was successfully created.'
format.html { redirect_to user_url(@user) }
format.xml { head :created, :location => user_url(@user) }
else
format.html { render :action => "new" }
format.xml { render :xml => @user.errors.to_xml }
end
end
end
# PUT /users/1
# PUT /users/1.xml
def update
@user = User.find(params[:id])
respond_to do |format|
if @user.update_attributes(params[:user])
flash[:notice] = 'User was successfully updated.'
format.html { redirect_to user_url(@user) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @user.errors.to_xml }
end
end
end
# DELETE /users/1
# DELETE /users/1.xml
def destroy
@user = User.find(params[:id])
@user.destroy
flash[:notice] = "User was removed"
respond_to do |format|
format.html { redirect_to users_url }
format.xml { head :ok }
end
end
end

View File

@@ -1,3 +0,0 @@
# Methods added to this helper will be available to all templates in the application.
module ApplicationHelper
end

View File

@@ -1,2 +0,0 @@
module PostsHelper
end

View File

@@ -1,2 +0,0 @@
module UsersHelper
end

View File

@@ -1,3 +0,0 @@
class Dog < ActiveRecord::Base
belongs_to :user, :foreign_key => :owner_id
end

View File

@@ -1,11 +0,0 @@
class Post < ActiveRecord::Base
belongs_to :user
belongs_to :owner, :foreign_key => :user_id, :class_name => 'User'
has_many :taggings
has_many :tags, :through => :taggings
validates_uniqueness_of :title
validates_presence_of :title
validates_presence_of :body, :message => 'Seriously... wtf'
validates_numericality_of :user_id
end

View File

@@ -1,4 +0,0 @@
class Tag < ActiveRecord::Base
has_many :taggings
has_many :posts, :through => :taggings
end

View File

@@ -1,4 +0,0 @@
class Tagging < ActiveRecord::Base
belongs_to :post
belongs_to :tag
end

View File

@@ -1,9 +0,0 @@
class User < ActiveRecord::Base
has_many :posts
has_many :dogs, :foreign_key => :owner_id
attr_protected :password
validates_format_of :email, :with => /\w*@\w*.com/
validates_length_of :email, :in => 1..100
validates_inclusion_of :age, :in => 1..100
end

View File

@@ -1,17 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<title>Posts: <%= controller.action_name %></title>
<%= stylesheet_link_tag 'scaffold' %>
</head>
<body>
<p style="color: green"><%= flash[:notice] %></p>
<%= yield %>
</body>
</html>

View File

@@ -1,17 +0,0 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<title>Users: <%= controller.action_name %></title>
<%= stylesheet_link_tag 'scaffold' %>
</head>
<body>
<p style="color: green"><%= flash[:notice] %></p>
<%= yield %>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More