forgot to add these

This commit is contained in:
Reinier Balt 2012-04-05 22:21:28 +02:00
parent 86afd42148
commit fde64e0b3d
145 changed files with 9044 additions and 0 deletions

View file

@ -0,0 +1,2 @@
require 'extra_validations'
ActiveRecord::Base.extend ExtraValidations

View file

@ -0,0 +1,29 @@
module ExtraValidations
# Validates the value of the specified attribute by checking for a forbidden string
#
# class Person < ActiveRecord::Base
# validates_does_not_contain :first_name, :string => ','
# end
#
# A string must be provided or else an exception will be raised.
#
# Configuration options:
# * <tt>message</tt> - A custom error message (default is: "is invalid")
# * <tt>string</tt> - The string to verify is not included (note: must be supplied!)
# * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
# * <tt>if</tt> - Specifies a method, proc or string to call to determine if the validation should
# occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). The
# method, proc or string should return or evaluate to a true or false value.
def validates_does_not_contain(*attr_names)
configuration = { :message => I18n.translate('activerecord.errors.messages')[:invalid], :on => :save, :string => nil }
configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
raise(ArgumentError, "A string must be supplied as the :string option of the configuration hash") unless configuration[:string].is_a?(String)
validates_each(attr_names, configuration) do |record, attr_name, value|
record.errors.add(attr_name, configuration[:message]) if value.to_s =~ Regexp.new(Regexp.escape(configuration[:string]))
end
end
end

View file

@ -0,0 +1,35 @@
* Fake HTTP method from OpenID server since they only support a GET. Eliminates the need to set an extra route to match the server's reply. [Josh Peek]
* OpenID 2.0 recommends that forms should use the field name "openid_identifier" rather than "openid_url" [Josh Peek]
* Return open_id_response.display_identifier to the application instead of .endpoints.claimed_id. [nbibler]
* Add Timeout protection [Rick]
* An invalid identity url passed through authenticate_with_open_id will no longer raise an InvalidOpenId exception. Instead it will return Result[:missing] to the completion block.
* Allow a return_to option to be used instead of the requested url [Josh Peek]
* Updated plugin to use Ruby OpenID 2.x.x [Josh Peek]
* Tied plugin to ruby-openid 1.1.4 gem until we can make it compatible with 2.x [DHH]
* Use URI instead of regexps to normalize the URL and gain free, better matching #8136 [dkubb]
* Allow -'s in #normalize_url [Rick]
* remove instance of mattr_accessor, it was breaking tests since they don't load ActiveSupport. Fix Timeout test [Rick]
* Throw a InvalidOpenId exception instead of just a RuntimeError when the URL can't be normalized [DHH]
* Just use the path for the return URL, so extra query parameters don't interfere [DHH]
* Added a new default database-backed store after experiencing trouble with the filestore on NFS. The file store is still available as an option [DHH]
* Added normalize_url and applied it to all operations going through the plugin [DHH]
* Removed open_id? as the idea of using the same input box for both OpenID and username has died -- use using_open_id? instead (which checks for the presence of params[:openid_url] by default) [DHH]
* Added OpenIdAuthentication::Result to make it easier to deal with default situations where you don't care to do something particular for each error state [DHH]
* Stop relying on root_url being defined, we can just grab the current url instead [DHH]

View file

@ -0,0 +1,231 @@
OpenIdAuthentication
====================
Provides a thin wrapper around the excellent ruby-openid gem from JanRan. Be sure to install that first:
gem install ruby-openid
To understand what OpenID is about and how it works, it helps to read the documentation for lib/openid/consumer.rb
from that gem.
The specification used is http://openid.net/specs/openid-authentication-2_0.html.
Prerequisites
=============
OpenID authentication uses the session, so be sure that you haven't turned that off. It also relies on a number of
database tables to store the authentication keys. So you'll have to run the migration to create these before you get started:
rake open_id_authentication:db:create
Or, use the included generators to install or upgrade:
./script/generate open_id_authentication_tables MigrationName
./script/generate upgrade_open_id_authentication_tables MigrationName
Alternatively, you can use the file-based store, which just relies on on tmp/openids being present in RAILS_ROOT. But be aware that this store only works if you have a single application server. And it's not safe to use across NFS. It's recommended that you use the database store if at all possible. To use the file-based store, you'll also have to add this line to your config/environment.rb:
OpenIdAuthentication.store = :file
This particular plugin also relies on the fact that the authentication action allows for both POST and GET operations.
If you're using RESTful authentication, you'll need to explicitly allow for this in your routes.rb.
The plugin also expects to find a root_url method that points to the home page of your site. You can accomplish this by using a root route in config/routes.rb:
map.root :controller => 'articles'
This plugin relies on Rails Edge revision 6317 or newer.
Example
=======
This example is just to meant to demonstrate how you could use OpenID authentication. You might well want to add
salted hash logins instead of plain text passwords and other requirements on top of this. Treat it as a starting point,
not a destination.
Note that the User model referenced in the simple example below has an 'identity_url' attribute. You will want to add the same or similar field to whatever
model you are using for authentication.
Also of note is the following code block used in the example below:
authenticate_with_open_id do |result, identity_url|
...
end
In the above code block, 'identity_url' will need to match user.identity_url exactly. 'identity_url' will be a string in the form of 'http://example.com' -
If you are storing just 'example.com' with your user, the lookup will fail.
There is a handy method in this plugin called 'normalize_url' that will help with validating OpenID URLs.
OpenIdAuthentication.normalize_url(user.identity_url)
The above will return a standardized version of the OpenID URL - the above called with 'example.com' will return 'http://example.com/'
It will also raise an InvalidOpenId exception if the URL is determined to not be valid.
Use the above code in your User model and validate OpenID URLs before saving them.
config/routes.rb
map.root :controller => 'articles'
map.resource :session
app/views/sessions/new.erb
<% form_tag(session_url) do %>
<p>
<label for="name">Username:</label>
<%= text_field_tag "name" %>
</p>
<p>
<label for="password">Password:</label>
<%= password_field_tag %>
</p>
<p>
...or use:
</p>
<p>
<label for="openid_identifier">OpenID:</label>
<%= text_field_tag "openid_identifier" %>
</p>
<p>
<%= submit_tag 'Sign in', :disable_with => "Signing in&hellip;" %>
</p>
<% end %>
app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
def create
if using_open_id?
open_id_authentication
else
password_authentication(params[:name], params[:password])
end
end
protected
def password_authentication(name, password)
if @current_user = @account.users.authenticate(params[:name], params[:password])
successful_login
else
failed_login "Sorry, that username/password doesn't work"
end
end
def open_id_authentication
authenticate_with_open_id do |result, identity_url|
if result.successful?
if @current_user = @account.users.find_by_identity_url(identity_url)
successful_login
else
failed_login "Sorry, no user by that identity URL exists (#{identity_url})"
end
else
failed_login result.message
end
end
end
private
def successful_login
session[:user_id] = @current_user.id
redirect_to(root_url)
end
def failed_login(message)
flash[:error] = message
redirect_to(new_session_url)
end
end
If you're fine with the result messages above and don't need individual logic on a per-failure basis,
you can collapse the case into a mere boolean:
def open_id_authentication
authenticate_with_open_id do |result, identity_url|
if result.successful? && @current_user = @account.users.find_by_identity_url(identity_url)
successful_login
else
failed_login(result.message || "Sorry, no user by that identity URL exists (#{identity_url})")
end
end
end
Simple Registration OpenID Extension
====================================
Some OpenID Providers support this lightweight profile exchange protocol. See more: http://www.openidenabled.com/openid/simple-registration-extension
You can support it in your app by changing #open_id_authentication
def open_id_authentication(identity_url)
# Pass optional :required and :optional keys to specify what sreg fields you want.
# Be sure to yield registration, a third argument in the #authenticate_with_open_id block.
authenticate_with_open_id(identity_url,
:required => [ :nickname, :email ],
:optional => :fullname) do |result, identity_url, registration|
case result.status
when :missing
failed_login "Sorry, the OpenID server couldn't be found"
when :invalid
failed_login "Sorry, but this does not appear to be a valid OpenID"
when :canceled
failed_login "OpenID verification was canceled"
when :failed
failed_login "Sorry, the OpenID verification failed"
when :successful
if @current_user = @account.users.find_by_identity_url(identity_url)
assign_registration_attributes!(registration)
if current_user.save
successful_login
else
failed_login "Your OpenID profile registration failed: " +
@current_user.errors.full_messages.to_sentence
end
else
failed_login "Sorry, no user by that identity URL exists"
end
end
end
end
# registration is a hash containing the valid sreg keys given above
# use this to map them to fields of your user model
def assign_registration_attributes!(registration)
model_to_registration_mapping.each do |model_attribute, registration_attribute|
unless registration[registration_attribute].blank?
@current_user.send("#{model_attribute}=", registration[registration_attribute])
end
end
end
def model_to_registration_mapping
{ :login => 'nickname', :email => 'email', :display_name => 'fullname' }
end
Attribute Exchange OpenID Extension
===================================
Some OpenID providers also support the OpenID AX (attribute exchange) protocol for exchanging identity information between endpoints. See more: http://openid.net/specs/openid-attribute-exchange-1_0.html
Accessing AX data is very similar to the Simple Registration process, described above -- just add the URI identifier for the AX field to your :optional or :required parameters. For example:
authenticate_with_open_id(identity_url,
:required => [ :email, 'http://schema.openid.net/birthDate' ]) do |result, identity_url, registration|
This would provide the sreg data for :email, and the AX data for 'http://schema.openid.net/birthDate'
Copyright (c) 2007 David Heinemeier Hansson, released under the MIT license

View file

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

View file

@ -0,0 +1,11 @@
class OpenIdAuthenticationTablesGenerator < Rails::Generator::NamedBase
def initialize(runtime_args, runtime_options = {})
super
end
def manifest
record do |m|
m.migration_template 'migration.rb', 'db/migrate'
end
end
end

View file

@ -0,0 +1,20 @@
class <%= class_name %> < ActiveRecord::Migration
def self.up
create_table :open_id_authentication_associations, :force => true do |t|
t.integer :issued, :lifetime
t.string :handle, :assoc_type
t.binary :server_url, :secret
end
create_table :open_id_authentication_nonces, :force => true do |t|
t.integer :timestamp, :null => false
t.string :server_url, :null => true
t.string :salt, :null => false
end
end
def self.down
drop_table :open_id_authentication_associations
drop_table :open_id_authentication_nonces
end
end

View file

@ -0,0 +1,26 @@
class <%= class_name %> < ActiveRecord::Migration
def self.up
drop_table :open_id_authentication_settings
drop_table :open_id_authentication_nonces
create_table :open_id_authentication_nonces, :force => true do |t|
t.integer :timestamp, :null => false
t.string :server_url, :null => true
t.string :salt, :null => false
end
end
def self.down
drop_table :open_id_authentication_nonces
create_table :open_id_authentication_nonces, :force => true do |t|
t.integer :created
t.string :nonce
end
create_table :open_id_authentication_settings, :force => true do |t|
t.string :setting
t.binary :value
end
end
end

View file

@ -0,0 +1,11 @@
class UpgradeOpenIdAuthenticationTablesGenerator < Rails::Generator::NamedBase
def initialize(runtime_args, runtime_options = {})
super
end
def manifest
record do |m|
m.migration_template 'migration.rb', 'db/migrate'
end
end
end

View file

@ -0,0 +1,18 @@
if config.respond_to?(:gems)
config.gem 'ruby-openid', :lib => 'openid', :version => '>=2.0.4'
else
begin
require 'openid'
rescue LoadError
begin
gem 'ruby-openid', '>=2.0.4'
rescue Gem::LoadError
puts "Install the ruby-openid gem to enable OpenID support"
end
end
end
config.to_prepare do
OpenID::Util.logger = Rails.logger
ActionController::Base.send :include, OpenIdAuthentication
end

View file

@ -0,0 +1,240 @@
require 'uri'
require 'openid/extensions/sreg'
require 'openid/extensions/ax'
require 'openid/store/filesystem'
require File.dirname(__FILE__) + '/open_id_authentication/association'
require File.dirname(__FILE__) + '/open_id_authentication/nonce'
require File.dirname(__FILE__) + '/open_id_authentication/db_store'
require File.dirname(__FILE__) + '/open_id_authentication/request'
require File.dirname(__FILE__) + '/open_id_authentication/timeout_fixes' if OpenID::VERSION == "2.0.4"
module OpenIdAuthentication
OPEN_ID_AUTHENTICATION_DIR = RAILS_ROOT + "/tmp/openids"
def self.store
@@store
end
def self.store=(*store_option)
store, *parameters = *([ store_option ].flatten)
@@store = case store
when :db
OpenIdAuthentication::DbStore.new
when :file
OpenID::Store::Filesystem.new(OPEN_ID_AUTHENTICATION_DIR)
else
store
end
end
self.store = :db
class InvalidOpenId < StandardError
end
class Result
ERROR_MESSAGES = {
:missing => "Sorry, the OpenID server couldn't be found",
:invalid => "Sorry, but this does not appear to be a valid OpenID",
:canceled => "OpenID verification was canceled",
:failed => "OpenID verification failed",
:setup_needed => "OpenID verification needs setup"
}
def self.[](code)
new(code)
end
def initialize(code)
@code = code
end
def status
@code
end
ERROR_MESSAGES.keys.each { |state| define_method("#{state}?") { @code == state } }
def successful?
@code == :successful
end
def unsuccessful?
ERROR_MESSAGES.keys.include?(@code)
end
def message
ERROR_MESSAGES[@code]
end
end
# normalizes an OpenID according to http://openid.net/specs/openid-authentication-2_0.html#normalization
def self.normalize_identifier(identifier)
# clean up whitespace
identifier = identifier.to_s.strip
# if an XRI has a prefix, strip it.
identifier.gsub!(/xri:\/\//i, '')
# dodge XRIs -- TODO: validate, don't just skip.
unless ['=', '@', '+', '$', '!', '('].include?(identifier.at(0))
# does it begin with http? if not, add it.
identifier = "http://#{identifier}" unless identifier =~ /^http/i
# strip any fragments
identifier.gsub!(/\#(.*)$/, '')
begin
uri = URI.parse(identifier)
uri.scheme = uri.scheme.downcase # URI should do this
identifier = uri.normalize.to_s
rescue URI::InvalidURIError
raise InvalidOpenId.new("#{identifier} is not an OpenID identifier")
end
end
return identifier
end
# deprecated for OpenID 2.0, where not all OpenIDs are URLs
def self.normalize_url(url)
ActiveSupport::Deprecation.warn "normalize_url has been deprecated, use normalize_identifier instead"
self.normalize_identifier(url)
end
protected
def normalize_url(url)
OpenIdAuthentication.normalize_url(url)
end
def normalize_identifier(url)
OpenIdAuthentication.normalize_identifier(url)
end
# The parameter name of "openid_identifier" is used rather than the Rails convention "open_id_identifier"
# because that's what the specification dictates in order to get browser auto-complete working across sites
def using_open_id?(identity_url = nil) #:doc:
identity_url ||= params[:openid_identifier] || params[:openid_url]
!identity_url.blank? || params[:open_id_complete]
end
def authenticate_with_open_id(identity_url = nil, options = {}, &block) #:doc:
identity_url ||= params[:openid_identifier] || params[:openid_url]
if params[:open_id_complete].nil?
begin_open_id_authentication(identity_url, options, &block)
else
complete_open_id_authentication(&block)
end
end
private
def begin_open_id_authentication(identity_url, options = {})
identity_url = normalize_identifier(identity_url)
return_to = options.delete(:return_to)
method = options.delete(:method)
options[:required] ||= [] # reduces validation later
options[:optional] ||= []
open_id_request = open_id_consumer.begin(identity_url)
add_simple_registration_fields(open_id_request, options)
add_ax_fields(open_id_request, options)
redirect_to(open_id_redirect_url(open_id_request, return_to, method))
rescue OpenIdAuthentication::InvalidOpenId => e
yield Result[:invalid], identity_url, nil
rescue OpenID::OpenIDError, Timeout::Error => e
logger.error("[OPENID] #{e}")
yield Result[:missing], identity_url, nil
end
def complete_open_id_authentication
params_with_path = params.reject { |key, value| request.path_parameters[key] }
params_with_path.delete(:format)
open_id_response = timeout_protection_from_identity_server { open_id_consumer.complete(params_with_path, requested_url) }
identity_url = normalize_identifier(open_id_response.display_identifier) if open_id_response.display_identifier
case open_id_response.status
when OpenID::Consumer::SUCCESS
profile_data = {}
# merge the SReg data and the AX data into a single hash of profile data
[ OpenID::SReg::Response, OpenID::AX::FetchResponse ].each do |data_response|
if data_response.from_success_response( open_id_response )
profile_data.merge! data_response.from_success_response( open_id_response ).data
end
end
yield Result[:successful], identity_url, profile_data
when OpenID::Consumer::CANCEL
yield Result[:canceled], identity_url, nil
when OpenID::Consumer::FAILURE
yield Result[:failed], identity_url, nil
when OpenID::Consumer::SETUP_NEEDED
yield Result[:setup_needed], open_id_response.setup_url, nil
end
end
def open_id_consumer
OpenID::Consumer.new(session, OpenIdAuthentication.store)
end
def add_simple_registration_fields(open_id_request, fields)
sreg_request = OpenID::SReg::Request.new
# filter out AX identifiers (URIs)
required_fields = fields[:required].collect { |f| f.to_s unless f =~ /^https?:\/\// }.compact
optional_fields = fields[:optional].collect { |f| f.to_s unless f =~ /^https?:\/\// }.compact
sreg_request.request_fields(required_fields, true) unless required_fields.blank?
sreg_request.request_fields(optional_fields, false) unless optional_fields.blank?
sreg_request.policy_url = fields[:policy_url] if fields[:policy_url]
open_id_request.add_extension(sreg_request)
end
def add_ax_fields( open_id_request, fields )
ax_request = OpenID::AX::FetchRequest.new
# look through the :required and :optional fields for URIs (AX identifiers)
fields[:required].each do |f|
next unless f =~ /^https?:\/\//
ax_request.add( OpenID::AX::AttrInfo.new( f, nil, true ) )
end
fields[:optional].each do |f|
next unless f =~ /^https?:\/\//
ax_request.add( OpenID::AX::AttrInfo.new( f, nil, false ) )
end
open_id_request.add_extension( ax_request )
end
def open_id_redirect_url(open_id_request, return_to = nil, method = nil)
open_id_request.return_to_args['_method'] = (method || request.method).to_s
open_id_request.return_to_args['open_id_complete'] = '1'
open_id_request.redirect_url(root_url, return_to || requested_url)
end
def requested_url
relative_url_root = self.class.respond_to?(:relative_url_root) ?
self.class.relative_url_root.to_s :
request.relative_url_root
"#{request.protocol}#{request.host_with_port}#{ActionController::Base.relative_url_root}#{request.path}"
end
def timeout_protection_from_identity_server
yield
rescue Timeout::Error
Class.new do
def status
OpenID::FAILURE
end
def msg
"Identity server timed out"
end
end.new
end
end

View file

@ -0,0 +1,9 @@
module OpenIdAuthentication
class Association < ActiveRecord::Base
set_table_name :open_id_authentication_associations
def from_record
OpenID::Association.new(handle, secret, issued, lifetime, assoc_type)
end
end
end

View file

@ -0,0 +1,55 @@
require 'openid/store/interface'
module OpenIdAuthentication
class DbStore < OpenID::Store::Interface
def self.cleanup_nonces
now = Time.now.to_i
Nonce.delete_all(["timestamp > ? OR timestamp < ?", now + OpenID::Nonce.skew, now - OpenID::Nonce.skew])
end
def self.cleanup_associations
now = Time.now.to_i
Association.delete_all(['issued + lifetime > ?',now])
end
def store_association(server_url, assoc)
remove_association(server_url, assoc.handle)
Association.create(:server_url => server_url,
:handle => assoc.handle,
:secret => assoc.secret,
:issued => assoc.issued,
:lifetime => assoc.lifetime,
:assoc_type => assoc.assoc_type)
end
def get_association(server_url, handle = nil)
assocs = if handle.blank?
Association.find_all_by_server_url(server_url)
else
Association.find_all_by_server_url_and_handle(server_url, handle)
end
assocs.reverse.each do |assoc|
a = assoc.from_record
if a.expires_in == 0
assoc.destroy
else
return a
end
end if assocs.any?
return nil
end
def remove_association(server_url, handle)
Association.delete_all(['server_url = ? AND handle = ?', server_url, handle]) > 0
end
def use_nonce(server_url, timestamp, salt)
return false if Nonce.find_by_server_url_and_timestamp_and_salt(server_url, timestamp, salt)
return false if (timestamp - Time.now.to_i).abs > OpenID::Nonce.skew
Nonce.create(:server_url => server_url, :timestamp => timestamp, :salt => salt)
return true
end
end
end

View file

@ -0,0 +1,5 @@
module OpenIdAuthentication
class Nonce < ActiveRecord::Base
set_table_name :open_id_authentication_nonces
end
end

View file

@ -0,0 +1,23 @@
module OpenIdAuthentication
module Request
def self.included(base)
base.alias_method_chain :request_method, :openid
end
def request_method_with_openid
if !parameters[:_method].blank? && parameters[:open_id_complete] == '1'
parameters[:_method].to_sym
else
request_method_without_openid
end
end
end
end
# In Rails 2.3, the request object has been renamed
# from AbstractRequest to Request
if defined? ActionController::Request
ActionController::Request.send :include, OpenIdAuthentication::Request
else
ActionController::AbstractRequest.send :include, OpenIdAuthentication::Request
end

View file

@ -0,0 +1,20 @@
# http://trac.openidenabled.com/trac/ticket/156
module OpenID
@@timeout_threshold = 20
def self.timeout_threshold
@@timeout_threshold
end
def self.timeout_threshold=(value)
@@timeout_threshold = value
end
class StandardFetcher
def make_http(uri)
http = @proxy.new(uri.host, uri.port)
http.read_timeout = http.open_timeout = OpenID.timeout_threshold
http
end
end
end

View file

@ -0,0 +1,30 @@
namespace :open_id_authentication do
namespace :db do
desc "Creates authentication tables for use with OpenIdAuthentication"
task :create => :environment do
generate_migration(["open_id_authentication_tables", "add_open_id_authentication_tables"])
end
desc "Upgrade authentication tables from ruby-openid 1.x.x to 2.x.x"
task :upgrade => :environment do
generate_migration(["upgrade_open_id_authentication_tables", "upgrade_open_id_authentication_tables"])
end
def generate_migration(args)
require 'rails_generator'
require 'rails_generator/scripts/generate'
if ActiveRecord::Base.connection.supports_migrations?
Rails::Generator::Scripts::Generate.new.run(args)
else
raise "Task unavailable to this database (no migration support)"
end
end
desc "Clear the authentication tables"
task :clear => :environment do
OpenIdAuthentication::DbStore.cleanup_nonces
OpenIdAuthentication::DbStore.cleanup_associations
end
end
end

View file

@ -0,0 +1,32 @@
require File.dirname(__FILE__) + '/test_helper'
class NormalizeTest < Test::Unit::TestCase
include OpenIdAuthentication
NORMALIZATIONS = {
"openid.aol.com/nextangler" => "http://openid.aol.com/nextangler",
"http://openid.aol.com/nextangler" => "http://openid.aol.com/nextangler",
"https://openid.aol.com/nextangler" => "https://openid.aol.com/nextangler",
"HTTP://OPENID.AOL.COM/NEXTANGLER" => "http://openid.aol.com/NEXTANGLER",
"HTTPS://OPENID.AOL.COM/NEXTANGLER" => "https://openid.aol.com/NEXTANGLER",
"loudthinking.com" => "http://loudthinking.com/",
"http://loudthinking.com" => "http://loudthinking.com/",
"http://loudthinking.com:80" => "http://loudthinking.com/",
"https://loudthinking.com:443" => "https://loudthinking.com/",
"http://loudthinking.com:8080" => "http://loudthinking.com:8080/",
"techno-weenie.net" => "http://techno-weenie.net/",
"http://techno-weenie.net" => "http://techno-weenie.net/",
"http://techno-weenie.net " => "http://techno-weenie.net/",
"=name" => "=name"
}
def test_normalizations
NORMALIZATIONS.each do |from, to|
assert_equal to, normalize_identifier(from)
end
end
def test_broken_open_id
assert_raises(InvalidOpenId) { normalize_identifier(nil) }
end
end

View file

@ -0,0 +1,46 @@
require File.dirname(__FILE__) + '/test_helper'
class OpenIdAuthenticationTest < Test::Unit::TestCase
def setup
@controller = Class.new do
include OpenIdAuthentication
def params() {} end
end.new
end
def test_authentication_should_fail_when_the_identity_server_is_missing
open_id_consumer = mock()
open_id_consumer.expects(:begin).raises(OpenID::OpenIDError)
@controller.expects(:open_id_consumer).returns(open_id_consumer)
@controller.expects(:logger).returns(mock(:error => true))
@controller.send(:authenticate_with_open_id, "http://someone.example.com") do |result, identity_url|
assert result.missing?
assert_equal "Sorry, the OpenID server couldn't be found", result.message
end
end
def test_authentication_should_be_invalid_when_the_identity_url_is_invalid
@controller.send(:authenticate_with_open_id, "!") do |result, identity_url|
assert result.invalid?, "Result expected to be invalid but was not"
assert_equal "Sorry, but this does not appear to be a valid OpenID", result.message
end
end
def test_authentication_should_fail_when_the_identity_server_times_out
open_id_consumer = mock()
open_id_consumer.expects(:begin).raises(Timeout::Error, "Identity Server took too long.")
@controller.expects(:open_id_consumer).returns(open_id_consumer)
@controller.expects(:logger).returns(mock(:error => true))
@controller.send(:authenticate_with_open_id, "http://someone.example.com") do |result, identity_url|
assert result.missing?
assert_equal "Sorry, the OpenID server couldn't be found", result.message
end
end
def test_authentication_should_begin_when_the_identity_server_is_present
@controller.expects(:begin_open_id_authentication)
@controller.send(:authenticate_with_open_id, "http://someone.example.com")
end
end

View file

@ -0,0 +1,14 @@
require File.dirname(__FILE__) + '/test_helper'
class StatusTest < Test::Unit::TestCase
include OpenIdAuthentication
def test_state_conditional
assert Result[:missing].missing?
assert Result[:missing].unsuccessful?
assert !Result[:missing].successful?
assert Result[:successful].successful?
assert !Result[:successful].unsuccessful?
end
end

View file

@ -0,0 +1,17 @@
require 'test/unit'
require 'rubygems'
gem 'activesupport'
require 'active_support'
gem 'actionpack'
require 'action_controller'
gem 'mocha'
require 'mocha'
gem 'ruby-openid'
require 'openid'
RAILS_ROOT = File.dirname(__FILE__) unless defined? RAILS_ROOT
require File.dirname(__FILE__) + "/../lib/open_id_authentication"

View file

@ -0,0 +1,7 @@
ResourceFeeder
==============
Simple feeds for resources
NOTE: This plugin depends on the latest version of simply_helpful, available here:
http://dev.rubyonrails.org/svn/rails/plugins/simply_helpful/

View file

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

View file

@ -0,0 +1,2 @@
require 'resource_feeder'
ActionController::Base.send(:include, ResourceFeeder::Rss, ResourceFeeder::Atom)

View file

@ -0,0 +1,2 @@
require 'resource_feeder/rss'
require 'resource_feeder/atom'

View file

@ -0,0 +1,67 @@
require 'resource_feeder/common'
module ResourceFeeder
module Atom
include ResourceFeeder::Common
include ActionController::Routing
extend self
def render_atom_feed_for(resources, options = {})
render :text => atom_feed_for(resources, options), :content_type => Mime::ATOM
end
def atom_feed_for(resources, options = {})
xml = Builder::XmlMarkup.new(:indent => 2)
options[:feed] ||= {}
options[:item] ||= {}
options[:url_writer] ||= self
if options[:class] || resources.first
klass = options[:class] || resources.first.class
new_record = klass.new
else
options[:feed] = { :title => "Empty", :link => "http://example.com" }
end
options[:feed][:title] ||= klass.name.pluralize
options[:feed][:id] ||= "tag:#{request.host_with_port}:#{klass.name.pluralize}"
options[:feed][:link] ||= polymorphic_url(new_record, :controller => options[:url_writer].controller_name)
options[:item][:title] ||= [ :title, :subject, :headline, :name ]
options[:item][:description] ||= [ :description, :body, :content ]
options[:item][:pub_date] ||= [ :updated_at, :updated_on, :created_at, :created_on ]
options[:item][:author] ||= [ :author, :creator ]
resource_link = lambda { |r| polymorphic_url(r, :controller => options[:url_writer].controller_name) }
xml.instruct!
xml.feed "xml:lang" => "en-US", "xmlns" => 'http://www.w3.org/2005/Atom' do
xml.title(options[:feed][:title])
xml.id(options[:feed][:id])
xml.link(:rel => 'alternate', :type => 'text/html', :href => options[:feed][:link])
xml.link(:rel => 'self', :type => 'application/atom+xml', :href => options[:feed][:self]) if options[:feed][:self]
xml.subtitle(options[:feed][:description]) if options[:feed][:description]
for resource in resources
published_at = call_or_read(options[:item][:pub_date], resource)
xml.entry do
xml.title(call_or_read(options[:item][:title], resource))
xml.content(call_or_read(options[:item][:description], resource), :type => 'html')
xml.id("tag:#{request.host_with_port},#{published_at.xmlschema}:#{call_or_read(options[:item][:guid] || options[:item][:link] || resource_link, resource)}")
xml.published(published_at.xmlschema)
xml.updated((resource.respond_to?(:updated_at) ? call_or_read(options[:item][:pub_date] || :updated_at, resource) : published_at).xmlschema)
xml.link(:rel => 'alternate', :type => 'text/html', :href => call_or_read(options[:item][:link] || options[:item][:guid] || resource_link, resource))
if author = call_or_read(options[:item][:author], resource)
xml.author do
xml.name()
end
end
end
end
end
end
end
end

View file

@ -0,0 +1,24 @@
module ResourceFeeder
module Common
private
def call_or_read(procedure_or_attributes, resource)
case procedure_or_attributes
when nil
raise ArgumentError, "WTF is nil here? #{resource.inspect}"
when Array
attributes = procedure_or_attributes
if attr = attributes.select { |a| resource.respond_to?(a) }.first
resource.send attr
end
when Symbol
attribute = procedure_or_attributes
resource.send(attribute)
when Proc
procedure = procedure_or_attributes
procedure.call(resource)
else
raise ArgumentError, "WTF is #{procedure_or_attributes.inspect} here? #{resource.inspect}"
end
end
end
end

View file

@ -0,0 +1,68 @@
require 'resource_feeder/common'
module ResourceFeeder
module Rss
include ResourceFeeder::Common
include ActionController::Routing
extend self
def render_rss_feed_for(resources, options = {})
render :text => rss_feed_for(resources, options), :content_type => Mime::RSS
end
def rss_feed_for(resources, options = {})
xml = Builder::XmlMarkup.new(:indent => 2)
options[:feed] ||= {}
options[:item] ||= {}
options[:url_writer] ||= self
if options[:class] || resources.first
klass = options[:class] || resources.first.class
new_record = klass.new
else
options[:feed] = { :title => "Empty", :link => "http://example.com" }
end
use_content_encoded = options[:item].has_key?(:content_encoded)
options[:feed][:title] ||= klass.name.pluralize
options[:feed][:link] ||= polymorphic_url(new_record, :controller => options[:url_writer].controller_name)
options[:feed][:language] ||= "en-us"
options[:feed][:ttl] ||= "40"
options[:item][:title] ||= [ :title, :subject, :headline, :name ]
options[:item][:description] ||= [ :description, :body, :content ]
options[:item][:pub_date] ||= [ :updated_at, :updated_on, :created_at, :created_on ]
resource_link = lambda { |r| polymorphic_url(r, :controller => options[:url_writer].controller_name) }
rss_root_attributes = { :version => 2.0 }
rss_root_attributes.merge!("xmlns:content" => "http://purl.org/rss/1.0/modules/content/") if use_content_encoded
xml.instruct!
xml.rss(rss_root_attributes) do
xml.channel do
xml.title(options[:feed][:title])
xml.link(options[:feed][:link])
xml.description(options[:feed][:description]) if options[:feed][:description]
xml.language(options[:feed][:language])
xml.ttl(options[:feed][:ttl])
for resource in resources
xml.item do
xml.title(call_or_read(options[:item][:title], resource))
xml.description(call_or_read(options[:item][:description], resource))
if use_content_encoded then
xml.content(:encoded) { xml.cdata!(call_or_read(options[:item][:content_encoded], resource)) }
end
xml.pubDate(call_or_read(options[:item][:pub_date], resource).to_s(:rfc822))
xml.guid(call_or_read(options[:item][:guid] || options[:item][:link] || resource_link, resource))
xml.link(call_or_read(options[:item][:link] || options[:item][:guid] || resource_link, resource))
end
end
end
end
end
end
end

View file

@ -0,0 +1,85 @@
require File.dirname(__FILE__) + '/test_helper'
class AtomFeedTest < Test::Unit::TestCase
attr_reader :request
def setup
@request = OpenStruct.new
@request.host_with_port = 'example.com'
@records = Array.new(5).fill(Post.new)
@records.each &:save
end
def test_default_atom_feed
atom_feed_for @records
assert_select 'feed' do
assert_select '>title', 'Posts'
assert_select '>id', "tag:#{request.host_with_port}:Posts"
assert_select '>link' do
assert_select "[rel='alternate']"
assert_select "[type='text/html']"
assert_select "[href='http://example.com/posts']"
end
assert_select 'entry', 5 do
assert_select 'title', :text => 'feed title (title)'
assert_select "content[type='html']", '&lt;p&gt;feed description (description)&lt;/p&gt;'
assert_select 'id', "tag:#{request.host_with_port},#{@records.first.created_at.xmlschema}:#{'http://example.com/posts/1'}"
assert_select 'published', @records.first.created_at.xmlschema
assert_select 'updated', @records.first.created_at.xmlschema
assert_select 'link' do
assert_select "[rel='alternate']"
assert_select "[type='text/html']"
assert_select "[href='http://example.com/posts/1']"
end
end
end
end
def test_should_allow_custom_feed_options
atom_feed_for @records, :feed => { :title => 'Custom Posts', :link => '/posts', :description => 'stuff', :self => '/posts.atom' }
assert_select 'feed>title', 'Custom Posts'
assert_select "feed>link[href='/posts']"
assert_select 'feed>subtitle', 'stuff'
assert_select 'feed>link' do
assert_select "[rel='self']"
assert_select "[type='application/atom+xml']"
assert_select "[href='/posts.atom']"
end
end
def test_should_allow_custom_item_attributes
atom_feed_for @records, :item => { :title => :name, :description => :body, :pub_date => :create_date, :link => :id }
assert_select 'entry', 5 do
assert_select 'title', :text => 'feed title (name)'
assert_select "content[type='html']", '&lt;p&gt;feed description (body)&lt;/p&gt;'
assert_select 'published', (@records.first.created_at - 5.minutes).xmlschema
assert_select 'updated', (@records.first.created_at - 5.minutes).xmlschema
assert_select 'id', "tag:#{request.host_with_port},#{(@records.first.created_at - 5.minutes).xmlschema}:1"
assert_select 'link' do
assert_select "[rel='alternate']"
assert_select "[type='text/html']"
assert_select "[href='1']"
end
end
end
def test_should_allow_custom_item_attribute_blocks
atom_feed_for @records, :item => { :title => lambda { |r| r.name }, :description => lambda { |r| r.body }, :pub_date => lambda { |r| r.create_date },
:link => lambda { |r| "/#{r.created_at.to_i}" }, :guid => lambda { |r| r.created_at.to_i } }
assert_select 'entry', 5 do
assert_select 'title', :text => 'feed title (name)'
assert_select "content[type='html']", '&lt;p&gt;feed description (body)&lt;/p&gt;'
assert_select 'published', (@records.first.created_at - 5.minutes).xmlschema
assert_select 'updated', (@records.first.created_at - 5.minutes).xmlschema
assert_select 'id', /:\d+$/
assert_select 'link' do
assert_select "[rel='alternate']"
assert_select "[type='text/html']"
assert_select "[href=?]", /^\/\d+$/
end
end
end
end

View file

@ -0,0 +1,86 @@
require File.dirname(__FILE__) + '/test_helper'
class RssFeedTest < Test::Unit::TestCase
def setup
@records = Array.new(5).fill(Post.new)
@records.each &:save
end
def test_default_rss_feed
rss_feed_for @records
assert_select 'rss[version="2.0"]' do
assert_select 'channel' do
assert_select '>title', 'Posts'
assert_select '>link', 'http://example.com/posts'
assert_select 'language', 'en-us'
assert_select 'ttl', '40'
end
assert_select 'item', 5 do
assert_select 'title', :text => 'feed title (title)'
assert_select 'description', '&lt;p&gt;feed description (description)&lt;/p&gt;'
%w(guid link).each do |node|
assert_select node, 'http://example.com/posts/1'
end
assert_select 'pubDate', @records.first.created_at.to_s(:rfc822)
end
end
end
def test_should_allow_custom_feed_options
rss_feed_for @records, :feed => { :title => 'Custom Posts', :link => '/posts', :description => 'stuff', :language => 'en-gb', :ttl => '80' }
assert_select 'channel>title', 'Custom Posts'
assert_select 'channel>link', '/posts'
assert_select 'channel>description', 'stuff'
assert_select 'channel>language', 'en-gb'
assert_select 'channel>ttl', '80'
end
def test_should_allow_custom_item_attributes
rss_feed_for @records, :item => { :title => :name, :description => :body, :pub_date => :create_date, :link => :id }
assert_select 'item', 5 do
assert_select 'title', :text => 'feed title (name)'
assert_select 'description', '&lt;p&gt;feed description (body)&lt;/p&gt;'
assert_select 'pubDate', (@records.first.created_at - 5.minutes).to_s(:rfc822)
assert_select 'link', '1'
assert_select 'guid', '1'
end
end
def test_should_allow_custom_item_attribute_blocks
rss_feed_for @records, :item => { :title => lambda { |r| r.name }, :description => lambda { |r| r.body }, :pub_date => lambda { |r| r.create_date },
:link => lambda { |r| "/#{r.created_at.to_i}" }, :guid => lambda { |r| r.created_at.to_i } }
assert_select 'item', 5 do
assert_select 'title', :text => 'feed title (name)'
assert_select 'description', '&lt;p&gt;feed description (body)&lt;/p&gt;'
assert_select 'pubDate', (@records.first.created_at - 5.minutes).to_s(:rfc822)
end
end
# note that assert_select isnt easily able to get elements that have xml namespaces (as it thinks they are
# invalid html psuedo children), so we do some manual testing with the response body
def test_should_allow_content_encoded_for_items
rss_feed_for @records, :item => { :content_encoded => :full_html_body }
html_content = "<strong>Here is some <i>full</i> content, with out any excerpts</strong>"
assert_equal 5, @response.body.scan("<![CDATA[#{html_content}]]>").size
assert_select 'item', 5 do
assert_select 'description + *', "<![CDATA[#{html_content}" # assert_select seems to strip the ending cdata tag
end
end
def test_should_have_content_encoded_namespace_if_used
rss_feed_for @records, :item => { :content_encoded => :full_html_body }
assert_equal %[<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">\n],
@response.body.grep(/<rss version="2\.0.*"/).first
end
def test_should_have_normal_rss_root_without_content_encoded
rss_feed_for @records
assert_equal %[<rss version="2.0">\n],
@response.body.grep(/<rss version="2\.0.*"/).first
end
end

View file

@ -0,0 +1,64 @@
RAILS_ENV = 'test'
require File.expand_path(File.join(File.dirname(__FILE__), '../../../../config/environment.rb'))
require 'action_controller/test_process'
require 'breakpoint'
require 'ostruct'
class Post
attr_reader :id, :created_at
def save; @id = 1; @created_at = Time.now.utc end
def new_record?; @id.nil? end
[:title, :name].each do |attr_name|
define_method attr_name do
"feed title (#{attr_name})"
end
end
[:description, :body].each do |attr_name|
define_method attr_name do
"<p>feed description (#{attr_name})</p>"
end
end
def full_html_body
"<strong>Here is some <i>full</i> content, with out any excerpts</strong>"
end
def create_date
@created_at - 5.minutes
end
end
class Test::Unit::TestCase
include ResourceFeeder::Rss, ResourceFeeder::Atom
def render_feed(xml)
@response = OpenStruct.new
@response.headers = {'Content-Type' => 'text/xml'}
@response.body = xml
end
def rss_feed_for_with_ostruct(resources, options = {})
render_feed rss_feed_for_without_ostruct(resources, options)
end
def atom_feed_for_with_ostruct(resources, options = {})
render_feed atom_feed_for_without_ostruct(resources, options)
end
alias_method_chain :rss_feed_for, :ostruct
alias_method_chain :atom_feed_for, :ostruct
def html_document
@html_document ||= HTML::Document.new(@response.body, false, true)
end
def posts_url
"http://example.com/posts"
end
def post_url(post)
"http://example.com/posts/#{post.id}"
end
end

View file

@ -0,0 +1,5 @@
SimpleLdapAuthenticator
=======================
Allows for simple authentication to an LDAP server with a minimum of
configuration. See the RDoc for details.

View file

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

View file

@ -0,0 +1,2 @@
# Include hook code here
#require 'simple_ldap_authenticator'

View file

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

View file

@ -0,0 +1,127 @@
# SimpleLdapAuthenticator
#
# This plugin supports both Ruby/LDAP and Net::LDAP, defaulting to Ruby/LDAP
# if it is available. If both are installed and you want to force the use of
# Net::LDAP, set SimpleLdapAuthenticator.ldap_library = 'net/ldap'.
# Allows for easily authenticating users via LDAP (or LDAPS). If authenticating
# via LDAP to a server running on localhost, you should only have to configure
# the login_format.
#
# Can be configured using the following accessors (with examples):
# * login_format = '%s@domain.com' # Active Directory, OR
# * login_format = 'cn=%s,cn=users,o=organization,c=us' # Other LDAP servers
# * servers = ['dc1.domain.com', 'dc2.domain.com'] # names/addresses of LDAP servers to use
# * use_ssl = true # for logging in via LDAPS
# * port = 3289 # instead of 389 for LDAP or 636 for LDAPS
# * logger = RAILS_DEFAULT_LOGGER # for logging authentication successes/failures
#
# The class is used as a global variable, you are not supposed to create an
# instance of it. For example:
#
# require 'simple_ldap_authenticator'
# SimpleLdapAuthenticator.servers = %w'dc1.domain.com dc2.domain.com'
# SimpleLdapAuthenticator.use_ssl = true
# SimpleLdapAuthenticator.login_format = '%s@domain.com'
# SimpleLdapAuthenticator.logger = RAILS_DEFAULT_LOGGER
# class LoginController < ApplicationController
# def login
# return redirect_to(:action=>'try_again') unless SimpleLdapAuthenticator.valid?(params[:username], params[:password])
# session[:username] = params[:username]
# end
# end
class SimpleLdapAuthenticator
class << self
@servers = ['127.0.0.1']
@use_ssl = false
@login_format = '%s'
attr_accessor :servers, :use_ssl, :port, :login_format, :logger, :connection, :ldap_library
# Load the required LDAP library, either 'ldap' or 'net/ldap'
def load_ldap_library
return if @ldap_library_loaded
if ldap_library
if ldap_library == 'net/ldap'
require 'net/ldap'
else
require 'ldap'
require 'ldap/control'
end
else
begin
require 'ldap'
require 'ldap/control'
ldap_library = 'ldap'
rescue LoadError
require 'net/ldap'
ldap_library = 'net/ldap'
end
end
@ldap_library_loaded = true
end
# The next LDAP server to which to connect
def server
servers[0]
end
# The connection to the LDAP server. A single connection is made and the
# connection is only changed if a server returns an error other than
# invalid password.
def connection
return @connection if @connection
load_ldap_library
@connection = if ldap_library == 'net/ldap'
Net::LDAP.new(:host=>server, :port=>(port), :encryption=>(:simple_tls if use_ssl))
else
(use_ssl ? LDAP::SSLConn : LDAP::Conn).new(server, port)
end
end
# The port to use. Defaults to 389 for LDAP and 636 for LDAPS.
def port
@port ||= use_ssl ? 636 : 389
end
# Disconnect from current LDAP server and use a different LDAP server on the
# next authentication attempt
def switch_server
self.connection = nil
servers << servers.shift
end
# Check the validity of a login/password combination
def valid?(login, password)
if ldap_library == 'net/ldap'
connection.authenticate(login_format % login.to_s, password.to_s)
begin
if connection.bind
logger.info("Authenticated #{login.to_s} by #{server}") if logger
true
else
logger.info("Error attempting to authenticate #{login.to_s} by #{server}: #{connection.get_operation_result.code} #{connection.get_operation_result.message}") if logger
switch_server unless connection.get_operation_result.code == 49
false
end
rescue Net::LDAP::LdapError => error
logger.info("Error attempting to authenticate #{login.to_s} by #{server}: #{error.message}") if logger
switch_server
false
end
else
connection.unbind if connection.bound?
begin
connection.bind(login_format % login.to_s, password.to_s)
connection.unbind
logger.info("Authenticated #{login.to_s} by #{server}") if logger
true
rescue LDAP::ResultError => error
connection.unbind if connection.bound?
logger.info("Error attempting to authenticate #{login.to_s} by #{server}: #{error.message}") if logger
switch_server unless error.message == 'Invalid credentials'
false
end
end
end
end
end

View file

@ -0,0 +1,4 @@
# desc "Explaining what the task does"
# task :simple_ldap_authenticator do
# # Task goes here
# end

View file

@ -0,0 +1,8 @@
require 'test/unit'
class SimpleLdapAuthenticatorTest < Test::Unit::TestCase
# Replace this with your real tests.
def test_this_plugin
flunk
end
end

View file

@ -0,0 +1,2 @@
.DS_Store
doc

View file

@ -0,0 +1,270 @@
= Skinny Spec
Skinny Spec is a collection of spec helper methods designed to help trim the fat and DRY up
some of the bloat that sometimes results from properly specing your classes and templates.
== Requirements and Recommendations
Obviously you'll need to be using RSpec[http://github.com/dchelimsky/rspec/tree/master] and
Rspec-Rails[http://github.com/dchelimsky/rspec-rails/tree/master] as your testing framework.
Skinny Spec was originally designed [and best enjoyed] if you're using
Haml[http://github.com/nex3/haml/tree/master] and
make_resourceful[http://github.com/rsl/make_resourceful/tree/master] but will default to
ERb and a facsimile of Rails' default scaffolding [for the views and controllers, respectively]
if Haml and/or make_resourceful are not available. I recommend using them though. :)
In addition, Skinny Spec uses Ruby2Ruby to make nicer expectation messages and you'll want to
have that installed as well. It's not a dependency or anything but it <i>is</i> highly
recommended.
== Setup
Once you've installed the plugin in your app's vendor/plugins folder, you're ready to rock!
Skinny Spec includes itself into the proper RSpec classes so there's no configuration on your
part. Sweet!
== Usage
The simplest way to use Skinny Specs is to generate a resource scaffold:
script/generate skinny_scaffold User
This command takes the usual complement of attribute definitions like
<tt>script/generate scaffold</tt>. Then have a look at the generated files (particularly the
specs) to see what's new and different with Skinny Spec.
=== Controller Specs
Let's look at the controller specs.
describe UsersController do
describe "GET :index" do
before(:each) do
@users = stub_index(User)
end
it_should_find_and_assign :users
it_should_render :template, "index"
end
# ...
describe "POST :create" do
describe "when successful" do
before(:each) do
@user = stub_create(User)
end
it_should_initialize_and_save :user
it_should_redirect_to { user_url(@user) }
end
# ...
First thing you should see is an example group for <tt>GET :index</tt>. That <tt>stub_index</tt> method there
does a lot of work behind the curtain. I'll leave it up to you to check the documentation for it
(and its brothers and sister methods like <tt>stub_new</tt>) but I will point out that the
methods named <tt>stub_<i>controller_method</i></tt> should only be used for stubbing and
mocking the main object of the method. To create mocks for other ancillary objects, please
use <tt>stub_find_all</tt>, <tt>stub_find_one</tt>, and <tt>stub_initialize</tt>. The reason
for this is because the former methods actually save us a step by defining an implicit
controller method request. If you add a new method to your resource routing, you'll want to
use the helper method <tt>define_request</tt> in those example groups to define an explicit
request, like so:
describe "PUT :demote" do
define_request { put :demote }
# ...
end
You can also define a method called <tt>shared_request</tt> to "share" a
<tt>define_request</tt> across nested describe blocks, like so:
describe "POST :create" do
def shared_request
post :create
end
describe "when successful" do
# ...
end
describe "when unsuccessful" do
# ...
end
end
Note: When you're adding longer, more complicated controller specs you can still leverage
implicit and explicit requests by calling <tt>do_request</tt> in your spec as in the following
example:
# Note this controller is UsersController and _not_ CategoriesController
# and that loading the categories isn't part of the default actions
# and cannot use the <tt>stub_<i>controller_method</i></tt> helpers
# [which create implicit requests based on the controller method in the name]
# but uses <tt>stub_find_all</tt> instead
describe "GET :new" do
before(:each) do
@user = stub_new(User)
@categories = stub_find_all(Category)
end
# ...
it "should preload categories" do
Category.should_receive(:find).with(:all)
do_request
end
it "should assign @categories" do
do_request
assigns[:categories].should == @categories
end
end
Finally we get to the meat of the spec and of Skinny Specs itself: the actual expectations.
The first thing you'll notice is the use of example group (read: "describe" block) level methods
instead of the usual example (read: "it") blocks. Using this helper at the example group level
saves us three lines over using an example block. (If this isn't significant to you, this is
probably the wrong plugin for you as well. Sorry.) Note that none of these methods use the
instance variables defined in the "before" block because they are all nil at the example block
level. Let's look at a sample method to see how it works:
it_should_find_and_assign :users
This actually wraps two different expectations: one that <tt>User.should_receive(:find).with(:all)</tt>
and another that the instance variable <tt>@users</tt> is assigned with the return value from that finder call.
If you need to add more detailed arguments to the find, you can easily break this into two different
expectations like:
it_should_find :users, :limit => 2
it_should_assign :users
See the documentation for the <tt>it_should_find</tt> for more information. You might have guessed that
<tt>it_should_initialize_assign</tt> and <tt>it_should_render_template</tt> work in a similar
fashion and you'd be right. Again, see the documentation for these individual methods for more
information. Lots of information in those docs.
A useful helper method that doesn't appear in any of the scaffolding is <tt>with_default_restful_actions</tt>
which takes a block and evaluates it for each of the RESTful controller actions. Very useful for
spec'ing that these methods redirect to the login page when the user isn't logged in, for example. This
method is designed to be used inside an example like so:
describe "when not logged in" do
it "should redirect all requests to the login page" do
with_default_restful_actions do
response.should redirect_to(login_url)
end
end
end
Before we're through with the controller specs, let me point out one more important detail. In
order to use <tt>it_should_redirect_to</tt> we have to send the routing inside a block argument
there so it can be evaluated in the example context instead of the example group, where it
completely blows up. This methodology is used anywhere routing is referred to in a "skinny",
example group level spec.
=== View Specs
Now let's move to the view specs!
describe "/users/form.html.haml" do
before(:each) do
@user = mock_and_assign(User, :stub => {
:name => "foo",
:birthday => 1.week.ago,
:adult => false
})
end
it_should_have_form_for :user
it_should_allow_editing :user, :name
it_should_allow_editing :user, :birthday
it_should_allow_editing :user, :adult
it_should_link_to_show :user
it_should_link_to { users_path }
end
Like the special <tt>stub_index</tt> methods in the controller
specs, the view specs have a shorthand mock and stub helpers: <tt>mock_and_assign</tt> and
<tt>mock_and_assign_collection</tt>. These are well documented so please check them out.
There are also some really nice helper methods that I'd like point out. First is
<tt>it_should_have_form_for</tt>. This is a really good convenience wrapper that basically wraps
the much longer:
it "should use form_for to generate the proper form action and options" do
template.should_receive(:form_for).with(@user)
do_render
end
Next up is the <tt>it_should_allow_editing</tt> helper. I love this method the most because it
really helps DRY up that view spec while at the same time being amazingly unbrittle. Instead of
creating an expectation for a specific form element, this method creates a generalized expectation
that there's a form element with the <tt>name</tt> attribute set in such away that it will
generate the proper <tt>params</tt> to use in the controller to edit or create the instance.
Check out the docs and the source for more information on this. Also check out
<tt>it_should_have_form_element_for</tt> which is roughly equivalent for those times when you use
<tt>form_tag</tt> instead.
Finally let's look at those <tt>it_should_link_to_<i>controller_method</i></tt> helpers.
These methods (and there's one each for the controller methods
<tt>new</tt>, <tt>edit</tt>, <tt>show</tt>, and <tt>delete</tt>) point to instance variables
which you should be created in the "before" blocks with <tt>mock_and_assign</tt>. The other is
<tt>it_should_allow_editing</tt> which is likewise covered extensively in the documentation and
I will just point out here that, like <tt>it_should_link_to_edit</tt> and such, it takes a
symbol for the name of the instance variable it refers to and <i>additionally</i> takes
a symbol for the name of the attribute to be edited.
Also note that, when constructing a long form example, instead of defining an instance variable
for the name of the template and calling <tt>render @that_template</tt> you can simply call
<tt>do_render</tt> which takes the name of the template from the outermost example group where
it is customarily stated.
=== Model Specs
Skinny Spec adds a matcher for the various ActiveRecord associations. On the example group level
you call them like:
it_should_belong_to :manager
it_should_have_many :clients
Within an example you can call them on either the class or the instance setup in the
"before" block. These are equivalent:
@user.should belong_to(:group)
User.should belong_to(:group)
I've also added some very basic validation helpers like <tt>it_should_validate_presence_of</tt>,
<tt>it_should_validate_uniqueness_of</tt>, <tt>it_should_not_mass_assign</tt>. Please consult
the documentation for more information.
== Miscellaneous Notes
In the scaffolding, I have used my own idiomatic Rails usage:
* All controller actions which use HTML forms [<tt>new</tt>, <tt>edit</tt>, etc] use a shared
form and leverage <tt>form_for</tt> to its fullest by letting it create the appropriate
action and options.
* Some instances where you might expect link_to are button_to. This is to provide a common
interface element which can be styled the same instead of a mishmash of links and buttons and
inputs everywhere. To take full advantage of this, I usually override many of Rails' default
helpers with custom ones that all use actual HTML <tt>BUTTON</tt> elements which are much
easier to style than "button" typed <tt>INPUT</tt>. I've provided a text file in the
"additional" folder of this plugin which you can use in your ApplicationHelper. (I also
provide an optional override helper for the <tt>label</tt> method which uses
<tt>#titleize</tt> instead of <tt>humanize</tt> for stylistic reasons).
* Probably more that I can't think of.
== Credits and Thanks
Sections of this code were taken from or inspired by Rick Olsen's
rspec_on_rails_on_crack[http://github.com/technoweenie/rspec_on_rails_on_crack/tree/master].
Also thanks and props to Hampton Catlin and Nathan Weizenbaum for the lovely and imminently useable
Haml and make_resourceful. Also also praises and glory to David Chelimsky and the Rspec crew.
Also thanks to Don Petersen, Nicolas Mérouze, Mikkel Malmberg, and Brandan Lennox for their suggestions and fixes.

View file

@ -0,0 +1,11 @@
require 'rake'
require 'rake/rdoctask'
desc 'Generate documentation for the Skinny Spec plugin'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'doc'
rdoc.title = 'Skinny Spec'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README.rdoc')
rdoc.rdoc_files.include('lib/**/*.rb')
end

View file

@ -0,0 +1,58 @@
# Please insert these into your ApplicationHelper
# Replacement for Rails' default submit_tag helper
# using HTML button element rather than HTML input element
def submit_tag(text, options = {})
content_tag :button, text, options.merge(:type => :submit)
end
# Replacement for Rails' default button_to helper
# using HTML button element rather than HTML input element
def button_to(name, options = {}, html_options = {})
html_options = html_options.stringify_keys
convert_boolean_attributes!(html_options, %w( disabled ))
method_tag = ''
if (method = html_options.delete('method')) && %w{put delete}.include?(method.to_s)
method_tag = tag('input', :type => 'hidden', :name => '_method', :value => method.to_s)
end
form_method = method.to_s == 'get' ? 'get' : 'post'
request_token_tag = ''
if form_method == 'post' && protect_against_forgery?
request_token_tag = tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => form_authenticity_token)
end
if confirm = html_options.delete("confirm")
html_options["onclick"] = "return #{confirm_javascript_function(confirm)};"
end
url = options.is_a?(String) ? options : self.url_for(options)
name ||= url
html_options.merge!("type" => "submit", "value" => name)
"<form method=\"#{form_method}\" action=\"#{escape_once url}\" class=\"button-to\"><div>" +
method_tag + content_tag("button", name, html_options) + request_token_tag + "</div></form>"
end
# Replacement for Rails' default button_to_function helper
# using HTML button element rather than HTML input element
def button_to_function(name, *args, &block)
html_options = args.extract_options!
function = args[0] || ''
html_options.symbolize_keys!
function = update_page(&block) if block_given?
content_tag(:button, name, html_options.merge({
:onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
}))
end
# Replacement for Rails' default label helper
# using String#titleize rather than String#humanize
def label(object_name, method, text = nil, options = {})
text ||= method.to_s[].titleize
super
end

View file

@ -0,0 +1,102 @@
class SkinnyScaffoldGenerator < Rails::Generator::NamedBase
attr_reader :controller_class_path, :controller_file_path, :controller_class_nesting,
:controller_class_nesting_depth, :controller_class_name, :controller_underscore_name,
:controller_plural_name, :template_language
alias_method :controller_file_name, :controller_underscore_name
alias_method :controller_singular_name, :controller_file_name
alias_method :controller_table_name, :controller_plural_name
default_options :skip_migration => false
def initialize(runtime_args, runtime_options = {})
super
base_name, @controller_class_path, @controller_file_path, @controller_class_nesting, @controller_class_nesting_depth = extract_modules(@name.pluralize)
@controller_class_name_without_nesting, @controller_underscore_name, @controller_plural_name = inflect_names(base_name)
if @controller_class_nesting.empty?
@controller_class_name = @controller_class_name_without_nesting
else
@controller_class_name = "#{@controller_class_nesting}::#{@controller_class_name_without_nesting}"
end
end
def manifest
record do |m|
# Check for class naming collisions
m.class_collisions controller_class_path, "#{controller_class_name}Controller", "#{controller_class_name}Helper"
m.class_collisions class_path, "#{class_name}"
# # Controller, helper, and views directories
m.directory File.join('app', 'views', controller_class_path, controller_file_name)
m.directory File.join('spec', 'views', controller_class_path, controller_file_name)
m.directory File.join('app', 'helpers', controller_class_path)
m.directory File.join('spec', 'helpers', controller_class_path)
m.directory File.join('app', 'controllers', controller_class_path)
m.directory File.join('spec', 'controllers', controller_class_path)
m.directory File.join('app', 'models', class_path)
m.directory File.join('spec', 'models', class_path)
# Views
@template_language = defined?(Haml) ? "haml" : "erb"
%w{index show form}.each do |action|
m.template "#{action}.html.#{template_language}",
File.join('app/views', controller_class_path, controller_file_name, "#{action}.html.#{template_language}")
m.template "#{action}.html_spec.rb",
File.join('spec/views', controller_class_path, controller_file_name, "#{action}.html.#{template_language}_spec.rb")
end
m.template "index_partial.html.#{template_language}",
File.join('app/views', controller_class_path, controller_file_name, "_#{file_name}.html.#{template_language}")
m.template 'index_partial.html_spec.rb',
File.join('spec/views', controller_class_path, controller_file_name, "_#{file_name}.html.#{template_language}_spec.rb")
# Helper
m.template 'helper.rb',
File.join('app/helpers', controller_class_path, "#{controller_file_name}_helper.rb")
m.template 'helper_spec.rb',
File.join('spec/helpers', controller_class_path, "#{controller_file_name}_helper_spec.rb")
# Controller
m.template 'controller.rb',
File.join('app/controllers', controller_class_path, "#{controller_file_name}_controller.rb")
m.template 'controller_spec.rb',
File.join('spec/controllers', controller_class_path, "#{controller_file_name}_controller_spec.rb")
# Model
m.template 'model.rb',
File.join('app/models', class_path, "#{file_name}.rb")
m.template 'model_spec.rb',
File.join('spec/models', class_path, "#{file_name}_spec.rb")
# Routing
m.route_resources controller_file_name
unless options[:skip_migration]
m.migration_template(
'migration.rb', 'db/migrate',
:assigns => {
:migration_name => "Create#{class_name.pluralize.gsub(/::/, '')}",
:attributes => attributes
},
:migration_file_name => "create_#{file_path.gsub(/\//, '_').pluralize}"
)
end
end
end
protected
def banner
"Usage: #{$0} skinny_scaffold ModelName [field:type, field:type]"
end
def add_options!(opt)
opt.separator ''
opt.separator 'Options:'
opt.on("--skip-migration",
"Don't generate a migration file for this model") { |v| options[:skip_migration] = v }
end
def model_name
class_name.demodulize
end
end

View file

@ -0,0 +1,105 @@
class <%= controller_class_name %>Controller < ApplicationController
<%- if defined?(Resourceful::Maker) -%>
make_resourceful do
actions :all
# Let's get the most use from form_for and share a single form here!
response_for :new, :edit do
render :template => "<%= plural_name %>/form"
end
response_for :create_fails, :update_fails do
flash[:error] = "There was a problem!"
render :template => "<%= plural_name %>/form"
end
end
<%- else -%>
# GET /<%= table_name %>
# GET /<%= table_name %>.xml
def index
@<%= table_name %> = <%= class_name %>.find(:all)
respond_to do |format|
format.html # index.html.erb
format.xml { render :xml => @<%= table_name %> }
end
end
# GET /<%= table_name %>/1
# GET /<%= table_name %>/1.xml
def show
@<%= file_name %> = <%= class_name %>.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @<%= file_name %> }
end
end
# GET /<%= table_name %>/new
# GET /<%= table_name %>/new.xml
def new
@<%= file_name %> = <%= class_name %>.new
respond_to do |format|
format.html { render :template => "<%= plural_name %>/form" }
format.xml { render :xml => @<%= file_name %> }
end
end
# GET /<%= table_name %>/1/edit
def edit
@<%= file_name %> = <%= class_name %>.find(params[:id])
render :template => "<%= plural_name %>/form"
end
# POST /<%= table_name %>
# POST /<%= table_name %>.xml
def create
@<%= file_name %> = <%= class_name %>.new(params[:<%= file_name %>])
respond_to do |format|
if @<%= file_name %>.save
flash[:notice] = '<%= class_name %> was successfully created.'
format.html { redirect_to(@<%= file_name %>) }
format.xml { render :xml => @<%= file_name %>, :status => :created, :location => @<%= file_name %> }
else
flash.now[:error] = '<%= class_name %> could not be created.'
format.html { render :template => "<%= plural_name %>/form" }
format.xml { render :xml => @<%= file_name %>.errors, :status => :unprocessable_entity }
end
end
end
# PUT /<%= table_name %>/1
# PUT /<%= table_name %>/1.xml
def update
@<%= file_name %> = <%= class_name %>.find(params[:id])
respond_to do |format|
if @<%= file_name %>.update_attributes(params[:<%= file_name %>])
flash[:notice] = '<%= class_name %> was successfully updated.'
format.html { redirect_to(@<%= file_name %>) }
format.xml { head :ok }
else
flash.now[:error] = '<%= class_name %> could not be created.'
format.html { render :template => "<%= plural_name %>/form" }
format.xml { render :xml => @<%= file_name %>.errors, :status => :unprocessable_entity }
end
end
end
# DELETE /<%= table_name %>/1
# DELETE /<%= table_name %>/1.xml
def destroy
@<%= file_name %> = <%= class_name %>.find(params[:id])
@<%= file_name %>.destroy
respond_to do |format|
flash[:notice] = '<%= class_name %> was successfully deleted.'
format.html { redirect_to(<%= table_name %>_url) }
format.xml { head :ok }
end
end
<%- end -%>
end

View file

@ -0,0 +1,93 @@
require File.dirname(__FILE__) + '/../spec_helper'
describe <%= controller_class_name %>Controller do
describe "GET :index" do
before(:each) do
@<%= plural_name %> = stub_index(<%= class_name %>)
end
it_should_find_and_assign :<%= plural_name %>
it_should_render_template "index"
end
describe "GET :new" do
before(:each) do
@<%= singular_name %> = stub_new(<%= class_name %>)
end
it_should_initialize_and_assign :<%= singular_name %>
it_should_render_template "form"
end
describe "POST :create" do
describe "when successful" do
before(:each) do
@<%= singular_name %> = stub_create(<%= class_name %>)
end
it_should_initialize_and_save :<%= singular_name %>
it_should_set_flash :notice
it_should_redirect_to { <%= singular_name %>_url(@<%= singular_name %>) }
end
describe "when unsuccessful" do
before(:each) do
@<%= singular_name %> = stub_create(<%= class_name %>, :return => :false)
end
it_should_initialize_and_assign :<%= singular_name %>
it_should_set_flash :error
it_should_render_template "form"
end
end
describe "GET :show" do
before(:each) do
@<%= singular_name %> = stub_show(<%= class_name %>)
end
it_should_find_and_assign :<%= singular_name %>
it_should_render_template "show"
end
describe "GET :edit" do
before(:each) do
@<%= singular_name %> = stub_edit(<%= class_name %>)
end
it_should_find_and_assign :<%= singular_name %>
it_should_render_template "form"
end
describe "PUT :update" do
describe "when successful" do
before(:each) do
@<%= singular_name %> = stub_update(<%= class_name %>)
end
it_should_find_and_update :<%= singular_name %>
it_should_set_flash :notice
it_should_redirect_to { <%= singular_name %>_url(@<%= singular_name %>) }
end
describe "when unsuccessful" do
before(:each) do
@<%= singular_name %> = stub_update(<%= class_name %>, :return => :false)
end
it_should_find_and_assign :<%= singular_name %>
it_should_set_flash :error
it_should_render_template "form"
end
end
describe "DELETE :destroy" do
before(:each) do
@<%= singular_name %> = stub_destroy(<%= class_name %>)
end
it_should_find_and_destroy :<%= singular_name %>
it_should_set_flash :notice
it_should_redirect_to { <%= plural_name %>_url }
end
end

View file

@ -0,0 +1,25 @@
<h1><%= singular_name %>.new_record? ? "New" : "Edit" %> <%= model_name %></h1>
<%% form_for(@<%= singular_name %>) do |f| %>
<div id="form_errors">
<%%= f.error_messages %>
</div>
<%- if attributes.blank? -%>
<p>Add your form elements here, please!</p>
<%- else -%>
<%- attributes.each do |attribute| -%>
<p>
<%%= f.label :<%= attribute.name %> %><br />
<%%= f.<%= attribute.field_type %> :<%= attribute.name %> %>
</p>
<%- end -%>
<%- end -%>
<div id="commands">
<%%= submit_tag "Save" %>
<div id="navigation_commands">
<%% unless @<%= singular_name %>.new_record? -%>
<%%= button_to "Show", <%= singular_name %>_path(@<%= singular_name %>), :method => "get", :title => "Show <%= singular_name %>. Unsaved changes will be lost." %>
<%% end -%>
<%%= button_to "Back to List", <%= plural_name %>_path, :class => "cancel", :method => "get", :title => "Return to <%= singular_name %> list without saving changes" %>
</div>
</div>
<%% end -%>

View file

@ -0,0 +1,18 @@
%h1== #{@<%= singular_name %>.new_record? ? "New" : "Edit"} #{<%= model_name %>}
- form_for @<%= singular_name %> do |f|
#form_errors= f.error_messages
<% if attributes.blank? -%>
%p Add your form elements here, please!
<% else -%>
<%- attributes.each do |attribute| -%>
%p
= f.label :<%= attribute.name %>
= f.<%= attribute.field_type %> :<%= attribute.name %>
<%- end -%>
<% end -%>
#commands
= submit_tag "Save"
#navigation_commands
- unless @<%= singular_name %>.new_record?
= button_to "Show", <%= singular_name %>_path(@<%= singular_name %>), :method => "get", :title => "Show <%= singular_name %>. Unsaved changes will be lost."
= button_to "Back to List", <%= plural_name %>_path, :class => "cancel", :method => "get", :title => "Return to <%= singular_name %> list without saving changes"

View file

@ -0,0 +1,40 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../../spec_helper'
describe "<%= File.join(controller_class_path, controller_singular_name) %>/form.html.<%= template_language %>" do
before(:each) do
@<%= singular_name %> = mock_and_assign(<%= model_name %>, :stub => {
<% if attributes.blank? -%>
# Add your stub attributes and return values here like:
# :name => "Foo", :address => "815 Oceanic Drive"
<% else -%>
<%- attributes.each_with_index do |attribute, index| -%>
<%- case attribute.type when :string, :text -%>
:<%= attribute.name %> => "foo"<%= index < attributes.size - 1 ? "," : "" %>
<%- when :integer, :float, :decimal -%>
:<%= attribute.name %> => 815<%= index < attributes.size - 1 ? "," : "" %>
<%- when :boolean -%>
:<%= attribute.name %> => false<%= index < attributes.size - 1 ? "," : "" %>
<%- when :date, :datetime, :time, :timestamp -%>
:<%= attribute.name %> => 1.week.ago<%= index < attributes.size - 1 ? "," : "" %>
<%- else -%>
:<%= attribute.name %> => nil<%= index < attributes.size - 1 ? "," : "" %> # Could not determine valid attribute
<%- end -%>
<%- end -%>
<% end -%>
})
end
it_should_have_form_for :<%= singular_name %>
<% if attributes.blank? -%>
# Add specs for editing attributes here, please! Like this:
#
# it_should_allow_editing :<%= singular_name %>, :foo
<% else -%>
<%- attributes.each do |attribute| -%>
it_should_allow_editing :<%= singular_name %>, :<%= attribute.name %>
<%- end -%>
<% end -%>
it_should_link_to_show :<%= singular_name %>
it_should_link_to { <%= plural_name %>_path }
end

View file

@ -0,0 +1,2 @@
module <%= controller_class_name %>Helper
end

View file

@ -0,0 +1,5 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../spec_helper'
describe <%= controller_class_name %>Helper do
# Add your specs here or remove this file completely, please!
end

View file

@ -0,0 +1,31 @@
<h1><%= model_name %> List</h1>
<table>
<%%- if @<%= plural_name %>.empty? -%>
<tbody>
<tr class="empty">
<td>There are no <%= plural_name.humanize.downcase %></td>
</tr>
</tbody>
<%%- else -%>
<thead>
<tr>
<%- if attributes.blank? -%>
<th><!-- Generic display column --></th>
<%- else -%>
<%- attributes.each do |attribute| -%>
<th><%= attribute.name.titleize %></th>
<%- end -%>
<%- end -%>
<th class="show"><!-- "Show" link column --></th>
<th class="edit"><!-- "Edit" link column --></th>
<th class="delete"><!-- "Delete" link column --></th>
</tr>
</thead>
<tbody>
<%%= render :partial => @<%= plural_name %> %>
</tbody>
<%%- end -%>
</table>
<div id="commands">
<%%= button_to "New <%= singular_name.titleize %>", new_<%= singular_name %>_path, :method => "get" %>
</div>

View file

@ -0,0 +1,23 @@
%h1 <%= model_name %> List
%table
- if @<%= plural_name %>.empty?
%tbody
%tr.empty
%td== There are no <%= plural_name.humanize.downcase %>
- else
%thead
%tr
<% if attributes.blank? -%>
%th= # Generic display column
<% else -%>
<%- attributes.each do |attribute| -%>
%th <%= attribute.name.titleize %>
<%- end -%>
<% end -%>
%th.show= # 'Show' link column
%th.edit= # 'Edit' link column
%th.delete= # 'Delete' link column
%tbody
= render :partial => @<%= plural_name %>
#commands
= button_to "New <%= singular_name.titleize %>", new_<%= singular_name %>_path, :method => "get"

View file

@ -0,0 +1,15 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../../spec_helper'
describe "<%= File.join(controller_class_path, controller_singular_name) %>/index.html.<%= template_language %>" do
before(:each) do
@<%= plural_name %> = mock_and_assign_collection(<%= model_name %>)
template.stub! :render
end
it "should render :partial => @<%= plural_name %>" do
template.should_receive(:render).with(:partial => @<%= plural_name %>)
do_render
end
it_should_link_to_new :<%= singular_name %>
end

View file

@ -0,0 +1,12 @@
<tr class="<%%= cycle("odd", "even") %>">
<% if attributes.blank? -%>
<td><%= model_name %> #<%%= <%= singular_name %>.id %></td>
<% else -%>
<%- attributes.each do |attribute| -%>
<td><%%=h <%= singular_name %>.<%= attribute.name %> %></td>
<%- end %>
<% end -%>
<td class="show"><%%= button_to "Show", <%= singular_name %>, :method => "get" %></td>
<td class="edit"><%%= button_to "Edit", edit_<%= singular_name %>_path(<%= singular_name %>), :method => "get" %></td>
<td class="delete"><%%= button_to "Delete", <%= singular_name %>, :method => "delete" %></td>
</tr>

View file

@ -0,0 +1,11 @@
%tr{:class => cycle("odd", "even")}
<% if attributes.blank? -%>
%td== <%= model_name %> #{<%= singular_name %>.id}
<% else -%>
<%- attributes.each do |attribute| -%>
%td=h <%= singular_name %>.<%= attribute.name %>
<%- end -%>
<% end -%>
%td.show= button_to "Show", <%= singular_name %>_path(<%= singular_name %>), :method => "get"
%td.edit= button_to "Edit", edit_<%= singular_name %>_path(<%= singular_name %>), :method => "get"
%td.delete= button_to "Delete", <%= singular_name %>, :method => "delete"

View file

@ -0,0 +1,31 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../../spec_helper'
describe "<%= File.join(controller_class_path, controller_singular_name) %>/_<%= singular_name %>.html.<%= template_language %>" do
before(:each) do
@<%= singular_name %> = mock_and_assign(<%= model_name %>, :stub => {
<% if attributes.blank? -%>
# Add your stub attributes and return values here like:
# :name => "Foo", :created_at => 1.week.ago, :updated_at => nil
<% else -%>
<%- attributes.each_with_index do |attribute, index| -%>
<%- case attribute.type when :string, :text -%>
:<%= attribute.name %> => "foo"<%= index < attributes.size - 1 ? "," : "" %>
<%- when :integer, :float, :decimal -%>
:<%= attribute.name %> => 815<%= index < attributes.size - 1 ? "," : "" %>
<%- when :boolean -%>
:<%= attribute.name %> => false<%= index < attributes.size - 1 ? "," : "" %>
<%- when :date, :datetime, :time, :timestamp -%>
:<%= attribute.name %> => 1.week.ago<%= index < attributes.size - 1 ? "," : "" %>
<%- else -%>
:<%= attribute.name %> => nil<%= index < attributes.size - 1 ? "," : "" %>
<%- end -%>
<%- end -%>
<% end -%>
})
template.stub!(:<%= singular_name %>).and_return(@<%= singular_name %>)
end
it_should_link_to_show :<%= singular_name %>
it_should_link_to_edit :<%= singular_name %>
it_should_link_to_delete :<%= singular_name %>
end

View file

@ -0,0 +1,14 @@
class <%= migration_name %> < ActiveRecord::Migration
def self.up
create_table :<%= table_name %>, :force => true do |t|
<% attributes.each do |attribute| -%>
t.column :<%= attribute.name %>, :<%= attribute.type %>
<% end -%>
t.timestamps
end
end
def self.down
drop_table :<%= table_name %>
end
end

View file

@ -0,0 +1,2 @@
class <%= class_name %> < ActiveRecord::Base
end

View file

@ -0,0 +1,25 @@
require File.dirname(__FILE__) + '<%= '/..' * class_nesting_depth %>/../spec_helper'
describe <%= class_name %> do
def valid_attributes(args = {})
{
# Add valid attributes for building your model instances here!
}.merge(args)
end
before(:each) do
@<%= singular_name %> = <%= class_name %>.new
end
after(:each) do
@<%= singular_name %>.destroy
end
# Add your model specs here, please!
# And don't forget about the association matchers built-in to skinny_spec like:
#
# it_should_have_many :foos
# it_should_validate_presence_of :bar
#
# Check out the docs for more information.
end

View file

@ -0,0 +1,15 @@
<h1>Show <%= model_name %></h1>
<% if attributes.blank? -%>
<p>Add your customized markup here, please!</p>
<% else -%>
<%- attributes.each do |attribute| -%>
<p>
<label><%= attribute.name.titleize %>:</label>
<%%=h @<%= singular_name %>.<%= attribute.name %> %>
</p>
<%- end -%>
<% end -%>
<div id="commands">
<%%= button_to "Edit", edit_<%= singular_name %>_path(@<%= singular_name %>), :method => "get" %>
<%%= button_to "Back to List", <%= plural_name %>_path, :method => "get" %>
</div>

View file

@ -0,0 +1,13 @@
%h1== Show #{<%= model_name %>}
<% if attributes.blank? -%>
%p Add your customized markup here, please!
<% else -%>
<%- attributes.each do |attribute| -%>
%p
%label <%= attribute.name.titleize %>:
=h @<%= singular_name %>.<%= attribute.name %>
<%- end -%>
<% end -%>
#commands
= button_to "Edit", edit_<%= singular_name %>_path(@<%= singular_name %>), :method => "get"
= button_to "Back to List", <%= plural_name %>_path, :method => "get"

View file

@ -0,0 +1,31 @@
require File.dirname(__FILE__) + '<%= '/..' * controller_class_nesting_depth %>/../../spec_helper'
describe "<%= File.join(controller_class_path, controller_singular_name) %>/show.html.<%= template_language %>" do
before(:each) do
<% if attributes.blank? -%>
@<%= singular_name %> = mock_and_assign(<%= model_name %>)
<% else -%>
@<%= singular_name %> = mock_and_assign(<%= model_name %>, :stub => {
<%- attributes.each_with_index do |attribute, index| -%>
<%- case attribute.type when :string, :text -%>
:<%= attribute.name %> => "foo"<%= index < attributes.size - 1 ? "," : "" %>
<%- when :integer, :float, :decimal -%>
:<%= attribute.name %> => 815<%= index < attributes.size - 1 ? "," : "" %>
<%- when :boolean -%>
:<%= attribute.name %> => false<%= index < attributes.size - 1 ? "," : "" %>
<%- when :date, :datetime, :time, :timestamp -%>
:<%= attribute.name %> => 1.week.ago<%= index < attributes.size - 1 ? "," : "" %>
<%- else -%>
:<%= attribute.name %> => nil<%= index < attributes.size - 1 ? "," : "" %>
<%- end -%>
<%- end -%>
})
<% end -%>
end
# Add your specs here, please! But remember not to make them brittle
# by specing specing specific HTML elements and classes.
it_should_link_to_edit :<%= singular_name %>
it_should_link_to { <%= plural_name %>_path }
end

View file

@ -0,0 +1,83 @@
module LuckySneaks
# These methods are mostly just called internally by various other spec helper
# methods but you're welcome to use them as needed in your own specs.
module CommonSpecHelpers
# Stubs out Time.now and returns value to use when comparing it. Example:
#
# time_now = stub_time_now
# @foo.some_method_that_resets_updated_at
# @foo.updated_at.should == time_now
def stub_time_now
returning Time.now do |now|
Time.stub!(:now).and_return(now)
end
end
# Returns class for the specified name. Example:
#
# class_for("foo") # => Foo
def class_for(name)
name.to_s.constantize
rescue NameError
name.to_s.pluralize.classify.constantize
# Let any other error rise!
end
# Returns instance variable for the specified name. Example:
#
# instance_for("foo") # => @foo
def instance_for(name)
instance_variable_get("@#{name.to_s.underscore}")
end
# Wraps a matcher that checks if the receiver contains an <tt>A</tt> element (link)
# whose <tt>href</tt> attribute is set to the specified path.
def have_link_to(path)
have_tag("a[href='#{path}']")
end
# Returns dummy value for specified attribute based on the datatype expected for that
# attribute.
def dummy_value_for(instance, attribute)
if datatype = instance.column_for_attribute(attribute)
actual = instance.send(attribute)
case datatype.type
when :string, :text
actual == "foo" ? "bar" : "food"
when :integer, :float, :decimal
actual == 108 ? 815 : 108
when :boolean
actual ? false : true
when :date, :datetime, :time, :timestamp
actual == 1.week.ago ? 2.years.ago : 1.week.ago
end
end
end
# Returns class description text
def class_description_text
if self.class.respond_to?(:description_text)
# Old school
self.class.description_text
else
# New school
self.class.description
end
end
# Returns description text
def self_description_text
if respond_to?(:description_text)
# Old school
description_text
else
# New school
description
end
end
def described_type
self.class.described_type
end
end
end

View file

@ -0,0 +1,67 @@
module LuckySneaks
module ControllerRequestHelpers # :nodoc:
def self.included(base)
base.extend ExampleGroupMethods
end
private
def define_implicit_request(method)
@controller_method = method
@implicit_request = case method
when :index, :new, :show, :edit
proc { get method, params }
when :create
proc { post :create, params }
when :update
proc { put :update, params }
when :destroy
proc { put :destroy, params }
end
end
def eval_request
instance_eval &self.class.instance_variable_get("@the_request")
rescue ArgumentError # missing block
try_shared_request_definition
end
alias do_request eval_request
def try_shared_request_definition
if defined?(shared_request) == "method"
shared_request
elsif @implicit_request
try_implicit_request
else
error_message = "Could not determine request definition for 'describe' context. "
error_message << "Please use define_request or define a shared_request."
raise ArgumentError, error_message
end
end
def try_implicit_request
@implicit_request.call
end
def get_response(&block)
eval_request
block.call(response) if block_given?
response
end
module ExampleGroupMethods
# Defines a request at the example group ("describe") level to be evaluated in the examples. Example:
#
# define_request { get :index, params }
#
# <b>Note:</b> The following methods all define implicit requests: <tt>stub_index</tt>, <tt>stub_new</tt>,
# <tt>stub_create</tt>, <tt>stub_show</tt>, <tt>stub_edit</tt>, <tt>stub_update</tt>, and
# <tt>stub_destroy</tt>. Using them in your <tt>before</tt> blocks will allow you to forego
# defining explicit requests using <tt>define_request</tt>. See
# LuckySneaks::ControllerStubHelpers for information on these methods.
def define_request(&block)
raise ArgumentError, "Must provide a block to define a request!" unless block_given?
@the_request = block
end
end
end
end

View file

@ -0,0 +1,571 @@
$:.unshift File.join(File.dirname(__FILE__), "..")
require "skinny_spec"
module LuckySneaks
module ControllerSpecHelpers # :nodoc:
include LuckySneaks::CommonSpecHelpers
include LuckySneaks::ControllerRequestHelpers
include LuckySneaks::ControllerStubHelpers
def self.included(base)
base.extend ExampleGroupMethods
base.extend ControllerRequestHelpers::ExampleGroupMethods
end
# Evaluates the specified block for each of the RESTful controller methods.
# This is useful to spec that all controller methods redirect when no user is
# logged in.
def with_default_restful_actions(params = {}, &block)
{
:get => :index,
:get => :new,
:post => :create
}.each do |method_id, message|
self.send method_id, message, params
block.call
end
{
:get => :edit,
:put => :update,
:delete => :destroy
}.each do |method_id, message|
if params[:before]
params.delete(:before).call
end
# Presuming any id will do
self.send method_id, message, params.merge(:id => 1)
block.call
end
end
private
def create_ar_class_expectation(name, method, argument = nil, options = {})
args = []
unless options.delete(:only_method)
args << argument unless argument.nil?
args << hash_including(options) unless options.empty?
end
method = options.delete(:find_method) if options[:find_method]
if args.empty?
class_for(name).should_receive(method).and_return(instance_for(name))
else
class_for(name).should_receive(method).with(*args).and_return(instance_for(name))
end
end
def create_positive_ar_instance_expectation(name, method, *args)
instance = instance_for(name)
if args.empty?
instance.should_receive(method).and_return(true)
else
instance.should_receive(method).with(*args).and_return(true)
end
end
# These methods are designed to be used at the example group [read: "describe"] level
# to simplify and DRY up common expectations.
module ExampleGroupMethods
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base.find</tt>.
# Examples:
#
# it_should_find :foos # => Foo.should_receive(:find).with(:all)
# it_should_find :foos, :all # An explicit version of the above
# it_should_find :foos, :conditions => {:foo => "bar"} # => Foo.should_receive(:find).with(:all, :conditions => {"foo" => "bar"}
# it_should_find :foos, "joe", :method => :find_all_by_name # Foo.should_receive(:find_all_by_name).with("joe")
# it_should_find :foo # => Foo.should_recieve(:find).with(@foo.id.to_s)
# it_should_find :foo, :params => "id" # => Foo.should_receive(:find).with(params[:id].to_s)
# it_should_find :foo, 2 # => Foo.should_receive(:find).with("2")
# it_should_find :foo, "joe", :method => :find_by_name # => Foo.should_recieve(:find_by_name).with("joe")
#
# <b>Note:</b> All params (key and value) will be strings if they come from a form element and are handled
# internally with this expectation.
def it_should_find(name, *args)
name_string = name.to_s
name_message = if name_string == name_string.singularize
"a #{name}"
else
name
end
it "should find #{name_message}" do
options = args.extract_options!
# Blech!
argument = if param = params[options.delete(:params)]
param.to_s
else
if args.first
args.first
elsif (instance = instance_variable_get("@#{name}")).is_a?(ActiveRecord::Base)
instance.id.to_s
else
:all
end
end
find_method = options.delete(:method) || :find
create_ar_class_expectation name, find_method, argument, options
eval_request
end
end
# Negative version of <tt>it_should_find</tt>. This creates an expectation that
# the class never receives <tt>find</tt> at all.
def it_should_not_find(name)
name_string = name.to_s
name_message = if name_string == name_string.singularize
"a #{name}"
else
name
end
it "should not find #{name_message}" do
if name_string == name_string.singularize
class_for(name).should_not_receive(:find)
else
class_for(name).should_not_receive(:find).with(:all)
end
eval_request
end
end
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base.new</tt>.
# Takes optional <tt>params</tt> for the initialization arguments. Example
#
# it_should_initialize :foo # => Foo.should_receive(:new)
# it_should_initialize :foo, :params => :bar # => Foo.should_receive(:new).with(params[:bar])
# it_should_initialize :foo, :bar => "baz" # => Foo.should_receive(:new).with(:bar => "baz")
def it_should_initialize(name, options = {})
it "should initialize a #{name}" do
create_ar_class_expectation name, :new, params[options.delete(:params)], options
eval_request
end
end
# Negative version of <tt>it_should_initialize</tt>. This creates an expectation
# that the class never recieves <tt>new</tt> at all.
def it_should_not_initialize(name)
it "should initialize a #{name}" do
class_for(name).should_not_receive(:new)
eval_request
end
end
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base#save</tt> on the
# named instance. Example:
#
# it_should_save :foo # => @foo.should_receive(:save).and_return(true)
#
# <b>Note:</b> This helper should not be used to spec a failed <tt>save</tt> call. Use <tt>it_should_assign</tt>
# instead, to verify that the instance is captured in an instance variable for the inevitable re-rendering
# of the form template.
def it_should_save(name)
it "should save the #{name}" do
create_positive_ar_instance_expectation name, :save
eval_request
end
end
# Negative version of <tt>it_should_update</tt>. This creates an expectation
# that the instance never receives <tt>save</tt> at all.
def it_should_not_save(name)
it "should not save the #{name}" do
instance_for(name).should_not_receive(:save)
eval_request
end
end
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base#update_attributes</tt>
# on the named instance. Takes optional argument for <tt>params</tt> to specify in the
# expectation. Examples:
#
# it_should_update :foo # => @foo.should_receive(:update_attributes).and_return(true)
# it_should_update :foo, :params => :bar # => @foo.should_receive(:update_attributes).with(params[:bar]).and_return(true)
#
# <b>Note:</b> This helper should not be used to spec a failed <tt>update_attributes</tt> call. Use
# <tt>it_should_assign</tt> instead, to verify that the instance is captured in an instance variable
# for the inevitable re-rendering of the form template.
def it_should_update(name, options = {})
it "should update the #{name}" do
create_positive_ar_instance_expectation name, :update_attributes, params[name]
eval_request
end
end
# Negative version of <tt>it_should_update</tt>. This creates an expectation
# that the instance never receives <tt>update_attributes</tt> at all.
def it_should_not_update(name)
it "should not update the #{name}" do
instance_for(name).should_not_receive(:update_attributes)
eval_request
end
end
# Creates an expectation that the controller method calls <tt>ActiveRecord::Base#destroy</tt> on the named
# instance. Example:
#
# it_should_destroy :foo # => @foo.should_receive(:destroy).and_return(true)
#
# <b>Note:</b> This helper should not be used to spec a failed <tt>destroy</tt> call. Use
# <tt>it_should_assign</tt> instead, if you need to verify that the instance is captured in an instance
# variable if it is re-rendered somehow. This is probably a really edge use case.
def it_should_destroy(name, options = {})
it "should delete the #{name}" do
create_positive_ar_instance_expectation name, :destroy
eval_request
end
end
# Negative version of <tt>it_should_destroy</tt>. This creates an expectation
# that the instance never receives <tt>destroy</tt> at all.
def it_should_not_destroy(name)
it "should not destroy the #{name}" do
instance_for(name).should_not_receive(:destroy)
eval_request
end
end
# Creates expectation[s] that the controller method should assign the specified
# instance variables along with any specified values. Examples:
#
# it_should_assign :foo # => assigns[:foo].should == @foo
# it_should_assign :foo => "bar" # => assigns[:foo].should == "bar"
# it_should_assign :foo => :nil # => assigns[:foo].should be_nil
# it_should_assign :foo => :not_nil # => assigns[:foo].should_not be_nil
# it_should_assign :foo => :undefined # => controller.send(:instance_variables).should_not include("@foo")
#
# Very special thanks to Rick Olsen for the basis of this code. The only reason I even
# redefine it at all is purely an aesthetic choice for specs like "it should foo"
# over ones like "it foos".
def it_should_assign(*names)
names.each do |name|
if name.is_a?(Symbol)
it_should_assign name => name
elsif name.is_a?(Hash)
name.each do |key, value|
it_should_assign_instance_variable key, value
end
end
end
end
# Essentially shorthand for <tt>it_should_assign name => :nil</tt>. This method can take multiple
# instance variable names, creating this shorthand for each name. See the docs for
# <tt>it_should_assign</tt> for more information.
def it_should_not_assign(*names)
names.each do |name|
# Assuming name is a symbol
it_should_assign name => :nil
end
end
# Wraps the separate expectations <tt>it_should_find</tt> and <tt>it_should_assign</tt>
# for simple cases. If you need more control over the parameters of the find, this
# isn't the right helper method and you should write out the two expectations separately.
def it_should_find_and_assign(*names)
names.each do |name|
it_should_find name, :only_method => true
it_should_assign name
end
end
# Negative version of <tt>it_should_find_and_assign</tt>. This creates an
# expectation that the class never receives <tt>find</tt> at all and that
# no matching instance variable is ever created.
def it_should_not_find_and_assign(*names)
names.each do |name|
it_should_not_find name
it_should_assign name => :nil
end
end
# Wraps the separate expectations <tt>it_should_initialize</tt> and <tt>it_should_assign</tt>
# for simple cases. If you need more control over the parameters of the initialization, this
# isn't the right helper method and you should write out the two expectations separately.
#
# <b>Note:</b> This method is used for controller methods like <tt>new</tt>, where the instance
# is initialized without being saved (this includes failed <tt>create</tt> requests).
# If you want to spec that the controller method successfully saves the instance,
# please use <tt>it_should_initialize_and_save</tt>.
def it_should_initialize_and_assign(*names)
names.each do |name|
it_should_initialize name, :only_method => true
it_should_assign name
end
end
# Negative version of <tt>it_should_initialize_and_assign</tt>. This creates an
# expectation that the class never receives <tt>new</tt> at all and that
# no matching instance variable is ever created.
def it_should_not_initialize_and_assign(*names)
names.each do |name|
it_should_not_initialize name
it_should_assign name => :nil
end
end
# Wraps the separate expectations <tt>it_should_initialize</tt> and <tt>it_should_save</tt>
# for simple cases. If you need more control over the parameters of the initialization, this
# isn't the right helper method and you should write out the two expectations separately.
#
# <b>Note:</b> This method is used for controller methods like <tt>create</tt>, where the instance
# is initialized and successfully saved. If you want to spec that the instance is created
# but not saved, just use <tt>it_should_initialize_and_assign</tt>.
def it_should_initialize_and_save(*names)
names.each do |name|
it_should_initialize name, :only_method => true
it_should_save name
end
end
# Wraps the separate expectations <tt>it_should_find</tt> and <tt>it_should_update</tt>
# for simple cases. If you need more control over the parameters of the find, this
# isn't the right helper method and you should write out the two expectations separately.
#
# <b>Note:</b> This method is used for controller methods like <tt>update</tt>, where the
# instance is loaded from the database and successfully saved. If you want to spec that the
# instance is found but not saved, just use <tt>it_should_find_and_assign</tt>.
def it_should_find_and_update(*names)
names.each do |name|
it_should_find name, :only_method => true
it_should_update name
end
end
# Wraps the separate expectations <tt>it_should_find</tt> and <tt>it_should_destroy</tt>
# for simple cases. If you need more control over the parameters of the find, this
# isn't the right helper method and you should write out the two expectations separately.
def it_should_find_and_destroy(*names)
names.each do |name|
it_should_find name, :only_method => true
it_should_destroy name
end
end
# Creates an expectation that the specified collection (<tt>flash</tt>, <tt>session</tt>,
# <tt>params</tt>, <tt>cookies</tt>) contains the specified key and value. To specify that
# the collection should be set to <tt>nil</tt>, specify the value as :nil instead.
def it_should_set(collection, key, value = nil, &block)
it "should set #{collection}[:#{key}]#{' with ' + value.inspect if value}" do
# Allow flash.now[:foo] to remain in the flash
flash.stub!(:sweep) if collection == :flash
eval_request
if value
if value == :nil
self.send(collection)[key].should be_nil
else
self.send(collection)[key].should == value
end
elsif block_given?
self.send(collection)[key].should == instance_eval(&block)
else
self.send(collection)[key].should_not be_nil
end
end
end
# Wraps <tt>it_should_set :flash</tt>. To specify that the collection should be set
# to <tt>nil</tt>, specify the value as :nil instead.
def it_should_set_flash(name, value = nil, &block)
it_should_set :flash, name, value, &block
end
# Wraps <tt>it_should_set :flash, :nil</tt>.
def it_should_not_set_flash(name)
it_should_set :flash, name, :nil
end
# Wraps <tt>it_should_set :session</tt>. To specify that the collection should be set
# to <tt>nil</tt>, specify the value as :nil instead.
def it_should_set_session(name, value = nil, &block)
it_should_set :session, name, value, &block
end
# Wraps <tt>it_should_set :session, :nil</tt>.
def it_should_not_set_session(name)
it_should_set :session, name, :nil
end
# Wraps <tt>it_should_set :params</tt>. To specify that the collection should be set
# to <tt>nil</tt>, specify the value as :nil instead.
def it_should_set_params(name, value = nil, &block)
it_should_set :params, name, value, &block
end
# Wraps <tt>it_should_set :params, :nil</tt>.
def it_should_not_set_params(name)
it_should_set :params, name, :nil
end
# Wraps <tt>it_should_set :cookies</tt>. To specify that the collection should be set
# to <tt>nil</tt>, specify the value as :nil instead.
def it_should_set_cookies(name, value = nil, &block)
it_should_set :cookies, name, value, &block
end
# Wraps <tt>it_should_set :cookies, :nil</tt>.
def it_should_not_set_cookies(name)
it_should_set :cookies, name, :nil
end
# Wraps the various <tt>it_should_render_<i>foo</i></tt> methods:
# <tt>it_should_render_template</tt>, <tt>it_should_render_partial</tt>,
# <tt>it_should_render_xml</tt>, <tt>it_should_render_json</tt>,
# <tt>it_should_render_formatted</tt>, and <tt>it_should_render_nothing</tt>.
def it_should_render(render_method, *args)
send "it_should_render_#{render_method}", *args
end
# Creates an expectation that the controller method renders the specified template.
# Accepts the following options which create additional expectations.
#
# <tt>:content_type</tt>:: Creates an expectation that the Content-Type header for the response
# matches the one specified
# <tt>:status</tt>:: Creates an expectation that the HTTP status for the response
# matches the one specified
def it_should_render_template(name, options = {})
create_status_expectation options[:status] if options[:status]
it "should render '#{name}' template" do
eval_request
response.should render_template(name)
end
create_content_type_expectation(options[:content_type]) if options[:content_type]
end
# Creates an expectation that the controller method renders the specified partial.
# Accepts the following options which create additional expectations.
#
# <tt>:content_type</tt>:: Creates an expectation that the Content-Type header for the response
# matches the one specified
# <tt>:status</tt>:: Creates an expectation that the HTTP status for the response
# matches the one specified
def it_should_render_partial(name, options = {})
create_status_expectation options[:status] if options[:status]
it "should render '#{name}' partial" do
controller.expect_render(:partial => name)
eval_request
end
create_content_type_expectation(options[:content_type]) if options[:content_type]
end
# Creates an expectation that the controller method renders the specified record via <tt>to_xml</tt>.
# Accepts the following options which create additional expectations.
#
# <tt>:content_type</tt>:: Creates an expectation that the Content-Type header for the response
# matches the one specified
# <tt>:status</tt>:: Creates an expectation that the HTTP status for the response
# matches the one specified
def it_should_render_xml(record = nil, options = {}, &block)
it_should_render_formatted :xml, record, options, &block
end
# Creates an expectation that the controller method renders the specified record via <tt>to_json</tt>.
# Accepts the following options which create additional expectations.
#
# <tt>:content_type</tt>:: Creates an expectation that the Content-Type header for the response
# matches the one specified
# <tt>:status</tt>:: Creates an expectation that the HTTP status for the response
# matches the one specified
def it_should_render_json(record = nil, options = {}, &block)
it_should_render_formatted :json, record, options, &block
end
# Called internally by <tt>it_should_render_xml</tt> and <tt>it_should_render_json</tt>
# but should not really be called much externally unless you have defined your own
# formats with a matching <tt>to_foo</tt> method on the record.
#
# Which is probably never.
def it_should_render_formatted(format, record = nil, options = {}, &block)
create_status_expectation options[:status] if options[:status]
it "should render #{format.inspect}" do
if record.is_a?(Hash)
options = record
record = nil
end
if record.nil? && !block_given?
raise ArgumentError, "it_should_render must be called with either a record or a block and neither was given."
else
if record
pieces = record.to_s.split(".")
record = instance_variable_get("@#{pieces.shift}")
record = record.send(pieces.shift) until pieces.empty?
end
block ||= proc { record.send("to_#{format}") }
get_response do |response|
response.should have_text(block.call)
end
end
end
create_content_type_expectation(options[:content_type]) if options[:content_type]
end
# Creates an expectation that the controller method returns a blank page. You'd already
# know when and why to use this so I'm not typing it out.
def it_should_render_nothing(options = {})
create_status_expectation options[:status] if options[:status]
it "should render :nothing" do
get_response do |response|
response.body.strip.should be_blank
end
end
end
# Creates an expectation that the controller method redirects to the specified destination. Example:
#
# it_should_redirect_to { foos_url }
#
# <b>Note:</b> This method takes a block to evaluate the route in the example
# context rather than the example group context.
def it_should_redirect_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should redirect to #{(hint || route)}" do
eval_request
response.should redirect_to(instance_eval(&route))
end
end
# Negative version of <tt>it_should_redirect_to</tt>.
def it_should_not_redirect_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should not redirect to #{(hint || route)}" do
eval_request
response.should_not redirect_to(instance_eval(&route))
end
end
# Creates an expectation that the controller method redirects back to the previous page
def it_should_redirect_to_referer
it "should redirect to the referring page" do
request.env["HTTP_REFERER"] = "http://test.host/referer"
eval_request
response.should redirect_to("http://test.host/referer")
end
end
alias it_should_redirect_to_referrer it_should_redirect_to_referer
private
def it_should_assign_instance_variable(name, value)
expectation_proc = case value
when :nil
proc { assigns[name].should be_nil }
when :not_nil
proc { assigns[name].should_not be_nil }
when :undefined
proc { controller.send(:instance_variables).should_not include("@{name}") }
when Symbol
if (instance_variable = instance_variable_get("@#{name}")).nil?
proc { assigns[name].should_not be_nil }
else
proc { assigns[name].should == instance_variable }
end
else
proc { assigns[name].should == value }
end
it "should #{value == :nil ? 'not ' : ''}assign @#{name}" do
eval_request
instance_eval &expectation_proc
end
end
end
end
end

View file

@ -0,0 +1,238 @@
module LuckySneaks # :nodoc:
# These methods are designed to be used in your example <tt>before</tt> blocks to accomplish
# a whole lot of functionality with just a tiny bit of effort. The methods which correspond
# to the controller methods perform the most duties as they create the mock_model instances,
# stub out all the necessary methods, and also create implicit requests to DRY up your spec
# file even more. You are encouraged to use these methods to setup the basic calls for your
# resources and only resort to the other methods when mocking and stubbing secondary objects
# and calls.
module ControllerStubHelpers
# Stubs out <tt>find :all</tt> and returns a collection of <tt>mock_model</tt>
# instances of that class. Accepts the following options:
#
# <b>:find_method</b>:: Method to use as finder call. Default is <tt>:find</tt>.
# <b>Note:</b> When specifying the method, the call is stubbed
# to accept any arguments. Caveat programmer.
# <b>:format</b>:: Format of the request. Used to only add <tt>to_xml</tt> and
# <tt>to_json</tt> when actually needed.
# <b>:size</b>:: Number of instances to return in the result. Default is 3.
# <b>:stub</b>:: Additional methods to stub on the instances
#
# Any additional options will be passed as arguments to the class find.
# You will want to make sure to pass those arguments to the <tt>it_should_find</tt> spec as well.
def stub_find_all(klass, options = {})
returning(Array.new(options[:size] || 3){mock_model(klass)}) do |collection|
stub_out klass, options.delete(:stub)
if format = options.delete(:format)
stub_formatted collection, format
params[:format] = format
end
if find_method = options[:find_method]
# Not stubbing specific arguments here
# If you need more specificity, write a custom example
klass.stub!(find_method).and_return(collection)
else
klass.stub!(:find).with(:all).and_return(collection)
klass.stub!(:find).with(:all, hash_including(options)).and_return(collection)
end
end
end
# Alias for <tt>stub_find_all</tt> but additionally defines an implicit request <tt>get :index</tt>.
def stub_index(klass, options = {})
define_implicit_request :index
stub_find_all klass, options
end
# Stubs out <tt>new</tt> method and returns a <tt>mock_model</tt> instance marked as a new record.
# Accepts the following options:
#
# <b>:format</b>:: Format of the request. Used to only add <tt>to_xml</tt> and
# <tt>to_json</tt> when actually needed.
# <b>:stub</b>:: Additional methods to stub on the instances
#
# It also accepts some options used to stub out <tt>save</tt> with a specified <tt>true</tt>
# or <tt>false</tt> but you should be using <tt>stub_create</tt> in that case.
def stub_initialize(klass, options = {})
returning mock_model(klass) do |member|
stub_out member, options.delete(:stub)
if format = options[:format]
stub_formatted member, format
params[:format] = format
end
klass.stub!(:new).and_return(member)
if options[:params]
klass.stub!(:new).with(hash_including(options[:params])).and_return(member)
end
if options[:stub_save]
stub_ar_method member, :save, options[:return]
else
member.stub!(:new_record?).and_return(true)
member.stub!(:id).and_return(nil)
end
end
end
# Alias for <tt>stub_initialize</tt> which additionally defines an implicit request <tt>get :new</tt>.
def stub_new(klass, options = {})
define_implicit_request :new
stub_initialize klass, options
end
# Alias for <tt>stub_initialize</tt> which additionally defines an implicit request <tt>post :create</tt>.
#
# <b>Note:</b> If <tt>stub_create<tt> is provided an optional <tt>:params</tt> hash,
# those params will be added to the example's <tt>params</tt> object.
def stub_create(klass, options = {})
define_implicit_request :create
class_name = klass.name.underscore
options[:params] ||= params[class_name]
stub_initialize klass, options.merge(:stub_save => true)
end
# Stubs out <tt>find</tt> and returns a single <tt>mock_model</tt>
# instances of that class. Accepts the following options:
#
# <b>:find_method</b>:: Method to use as finder call. Default is <tt>:find</tt>.
# <b>:format</b>:: Format of the request. Used to only add <tt>to_xml</tt> and
# <tt>to_json</tt> when actually needed.
# <b>:stub</b>:: Additional methods to stub on the instances
# <b>:current_object</b>:: If set to true, <tt>find</tt> will set <tt>params[:id]</tt>
# using the <tt>id</tt> of the <tt>mock_model</tt> instance
# and use that value as an argument when stubbing <tt>find</tt>
#
# Any additional options will be passed as arguments to <tt>find</tt>.You will want
# to make sure to pass those arguments to the <tt>it_should_find</tt> spec as well.
#
# <b>Note:</b> The option <tt>:stub_ar</tt> is used internally by <tt>stub_update</tt>
# and <tt>stub_destroy</tt>. If you need to stub <tt>update_attributes</tt> or
# <tt>destroy</tt> you should be using the aforementioned methods instead.
def stub_find_one(klass, options = {})
returning mock_model(klass) do |member|
stub_out member, options.delete(:stub)
if format = options.delete(:format)
stub_formatted member, format
params[:format] = format
end
if options.delete(:current_object)
params[:id] = member.id
if ar_stub = options.delete(:stub_ar)
stub_ar_method member, ar_stub, options.delete(:return), options.delete(:update_params)
end
end
if find_method = options.delete(:find_method)
klass.stub!(find_method).and_return(member)
else
# Stubbing string and non-string just to be safe
klass.stub!(:find).with(member.id).and_return(member)
klass.stub!(:find).with(member.id.to_s).and_return(member)
unless options.empty?
klass.stub!(:find).with(member.id, hash_including(options)).and_return(member)
klass.stub!(:find).with(member.id.to_s, hash_including(options)).and_return(member)
end
end
end
end
# <b>Note:</b> Use of this method with :child options (to mock
# association) is deprecated. Please use <tt>stub_association</tt>.
#
# Same as <tt>stub_find_one</tt> but setups the instance as the parent
# of the specified association. Example:
#
# stub_parent(Document, :child => :comments)
#
# This stubs <tt>Document.find</tt>, <tt>@document.comments</tt> (which
# will return <tt>Comment</tt> class), as well as <tt>params[:document_id]</tt>.
# This method is meant to be used in the controller for the specified child
# (<tt>CommentsController</tt> in this instance) in situations like:
#
# def index
# @document = Document.find(params[:document_id])
# @comments = @document.comments.find(:all)
# end
def stub_parent(klass, options = {})
returning stub_find_one(klass, options) do |member|
params[klass.name.foreign_key] = member.id
if offspring = options.delete(:child)
puts "stub_parent with :child option has been marked for deprecation"
puts "please use stub_association to create the mock instead"
member.stub!(offspring).and_return(class_for(offspring))
end
end
end
# Alias for <tt>stub_find_one</tt> which additionally defines an implicit request <tt>get :show</tt>.
def stub_show(klass, options = {})
define_implicit_request :show
stub_find_one klass, options.merge(:current_object => true)
end
# Alias for <tt>stub_find_one</tt> which additionally defines an implicit request <tt>get :edit</tt>.
def stub_edit(klass, options = {})
define_implicit_request :edit
stub_find_one klass, options.merge(:current_object => true)
end
# Alias for <tt>stub_find_one</tt> which additionally defines an implicit request <tt>put :update</tt>
# and stubs out the <tt>update_attribute</tt> method on the instance as well.
#
# <b>Note:</b> If <tt>stub_update<tt> is provided an optional <tt>:params</tt> hash,
# those params will be added to the example's <tt>params</tt> object.
def stub_update(klass, options = {})
define_implicit_request :update
stub_find_one klass, options.merge(:current_object => true, :stub_ar => :update_attributes)
end
# Alias for <tt>stub_find_one</tt> which additionally defines an implicit request <tt>delete :destroy</tt>
# and stubs out the <tt>destroy</tt> method on the instance as well.
def stub_destroy(klass, options = {})
define_implicit_request :destroy
stub_find_one klass, options.merge(:current_object => true, :stub_ar => :destroy)
end
# Stubs <tt>to_xml</tt> or <tt>to_json</tt> respectively based on <tt>format</tt> argument.
def stub_formatted(object, format)
return unless format
object.stub!("to_#{format}").and_return("#{object.class} formatted as #{format}")
end
# Creates a mock object representing an association proxy, stubs the appropriate
# method on the parent object and returns that association proxy.
# Accepts the following option:
#
# <b>:stub</b>:: Additional methods to stub on the mock proxy object
def stub_association(object, association, options = {})
# I know options isn't implemented anywhere
object_name = instance_variables.select{|name| instance_variable_get(name) == object}
returning mock("Association proxy for #{object_name}.#{association}") do |proxy|
stub_out proxy, options[:stub] if options[:stub]
object.stub!(association).and_return(proxy)
end
end
private
# Stubs out multiple methods. You shouldn't be calling this yourself and if you do
# you should be able to understand the code yourself, right?
def stub_out(object, stubs = {})
return if stubs.nil?
stubs.each do |method, value|
if value
object.stub!(method).and_return(value)
else
object.stub!(method)
end
end
end
# Stubs out ActiveRecord::Base methods like #save, #update_attributes, etc
# that may be called on a found or instantiated mock_model instance.
def stub_ar_method(object, method, return_value, params = {})
if params.blank?
object.stub!(method).and_return(return_value ? false : true)
else
object.stub!(method).with(hash_including(params)).and_return(return_value ? false : true)
end
end
end
end

View file

@ -0,0 +1,496 @@
$:.unshift File.join(File.dirname(__FILE__), "..")
require "skinny_spec"
module LuckySneaks
# These methods are designed to be used in your example [read: "it"] blocks
# to make your model specs a little more DRY. You might also be interested
# in checking out the example block [read: "describe"] level versions in of these
# methods which can DRY things up even more:
# LuckySneaks::ModelSpecHelpers::ExampleGroupLevelMethods.
#
# Also check out the methods in LuckySneaks::ModelSpecHelpers::AssociationMatcher
# for some helpful matcher helper methods to use with these methods if you want to spec
# options on your association setups.
module ModelSpecHelpers
include LuckySneaks::CommonSpecHelpers
def self.included(base) # :nodoc:
base.extend ExampleGroupLevelMethods
end
# These methods cannot be used alone but are used in compliment with the association
# matchers in LuckySneaks::ModelSpecHelpers like <tt>have_many</tt>. Example:
#
# describe User do
# it "should have many memberships" do
# User.should have_many(:memberships)
# end
#
# it "should have many sites through memberships" do
# User.should have_many(:sites).through(:memberships)
# end
#
# it "should belong to a manager" do
# User.should belong_to(:manager).with_counter_cache
# end
# end
#
# <b>Note:</b> To spec these sorts of options using the example block helpers like
# <tt>it_should_have_many</tt>, just add them as options directly. This will use
# <tt>with_options</tt> rather than any specific matcher helpers but will have the same
# effects. Example:
#
# describe User do
# it_should_have_many :sites, :through => :memberships
# end
class AssociationMatcher
def initialize(associated, macro) # :nodoc:
@associated = associated
@macro = macro
@options = {}
end
def matches?(main_model) # :nodoc:
unless main_model.respond_to?(:reflect_on_association)
if main_model.class.respond_to?(:reflect_on_association)
main_model = main_model.class
else
@not_model = main_model
return false
end
end
if @association = main_model.reflect_on_association(@associated)
@options.all?{|k, v| @association.options[k] == v ||
[@association.options[k]] == v} # Stupid to_a being obsoleted!
end
end
def failure_message # :nodoc:
if @not_model
" expected: #{@not_model} to be a subclass of ActiveRecord::Base class, but was not"
elsif @association
" expected: #{association_with(@options)}\n got: #{association_with(@association.options)}"
else
" expected: #{association_with(@options)}, but the association does not exist"
end
end
def negative_failure_message # :nodoc:
if @association
" expected: #{association_with(@options)}\n got: #{association_with(@association.options)}"
else
" expected: #{association_with(@options)} to not occur but it does"
end
end
# The following public methods are chainable extensions on the main matcher
# Examples:
#
# Foo.should have_many(:bars).through(:foobars).with_dependent(:destroy)
# Bar.should belong_to(:baz).with_class_name("Unbaz")
def through(through_model)
@options[:through] = through_model
self
end
def and_includes(included_models)
@options[:include] = included_models
self
end
def and_extends(*modules)
@options[:extends] = modules
self
end
def with_counter_cache(counter_cache = true)
if counter_cache
@options[:counter_cache] = counter_cache
end
self
end
def uniq(*irrelevant_args)
@options[:uniq] = true
self
end
alias and_is_unique uniq
alias with_unique uniq
def polymorphic(*irrelevant_args)
@options[:polymorphic] = true
self
end
alias and_is_polymorphic polymorphic
alias with_polymorphic polymorphic
def as(interface)
@options[:as] = interface
end
# Use this to just specify the options as a hash.
# Note: It will completely override any previously set options
def with_options(options = {})
options.each{|k, v| @options[k] = v}
self
end
private
# Takes care of methods like with_dependent(:destroy)
def method_missing(method_id, *args, &block)
method_name = method_id.to_s
if method_name =~ /^with_(.*)/
@options[$1.to_sym] = args
self
else
super method_id, *args, &block
end
end
def association_with(options)
option_string = (options.nil? || options.empty?) ? "" : options.inspect
unless option_string.blank?
option_string.sub! /^\{(.*)\}$/, ', \1'
option_string.gsub! /\=\>/, ' => '
end
"#{@macro} :#{@associated}#{option_string}"
end
end
# Creates matcher that checks if the receiver has a <tt>belongs_to</tt> association
# with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def belong_to(model)
AssociationMatcher.new model, :belongs_to
end
# Creates matcher that checks if the receiver has a <tt>have_one</tt> association
# with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def have_one(model)
AssociationMatcher.new model, :has_one
end
# Creates matcher that checks if the receiver has a <tt>have_many</tt> association
# with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def have_many(models)
AssociationMatcher.new models, :has_many
end
# Creates matcher that checks if the receiver has a <tt>have_and_belong_to_many</tt> association
# with the specified model.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def have_and_belong_to_many(models)
AssociationMatcher.new models, :has_and_belongs_to_many
end
private
def class_or_instance
@model_spec_class_or_instance ||= class_for(described_type) || instance
end
def instance
@model_spec_instance ||= instance_for(described_type)
end
# These methods are designed to be used at the example group [read: "describe"] level
# to simplify and DRY up common expectations. Some of these methods are wrappers for
# matchers which can also be used on the example level [read: within an "it" block]. See
# LuckySneaks::ModelSpecHelpers for more information.
#
# <b>Note:</b> The validation matchers are only meant to be used for simple validation checking
# not as a one-size-fits-all solution.
module ExampleGroupLevelMethods
# Creates an expectation that the current model being spec'd has a <tt>belong_to</tt>
# association with the specified model. Accepts optional arguments which are appended to
# the <tt>belong_to</tt> spec like this:
#
# it_should_belong_to :document, :counter_cache => true
#
# which is the same as writing out:
#
# it "should belong to document" do
# Comment.should belong_to(:document).with_options(:counter_cache => true)
# end
#
# If you want a more detailed spec description text, feel free to write this out in the long
# form and use <tt>belong_to</tt> and its related matcher helpers.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def it_should_belong_to(model, options = {})
it "should belong to a #{model}" do
if options.empty?
class_or_instance.should belong_to(model)
else
class_or_instance.should belong_to(model).with_options(options)
end
end
end
# Creates an expectation that the current model being spec'd has a <tt>have_one</tt>
# association with the specified model. Accepts optional arguments which are appended to
# the <tt>have_one</tt> spec like this:
#
# it_should_have_one :last_comment, :class_name => "Comment", :order => "created_at DESC"
#
# which is the same as writing out:
#
# it "should have one document" do
# Document.should have_one(:last_comment).with_options(:class_name => "Comment", :order => "created_at DESC")
# end
#
# If you want a more detailed spec description text, feel free to write this out in the long
# form and use <tt>have_one</tt> and its related matcher helpers.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def it_should_have_one(model, options = {})
it "should have one #{model}" do
if options.empty?
class_or_instance.should have_one(model)
else
class_or_instance.should have_one(model).with_options(options)
end
end
end
# Creates an expectation that the current model being spec'd has a <tt>have_many</tt>
# association with the specified model. Accepts optional arguments which are appended to
# the <tt>have_many</tt> spec like this:
#
# it_should_have_many :memberships, :through => :sites
#
# which is the same as writing out:
#
# it "should have many memberships" do
# User.should have_many(:memberships).with_options(:through => :sites)
# end
#
# If you want a more detailed spec description text, feel free to write this out in the long
# form and use <tt>have_many</tt> and its related matcher helpers.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def it_should_have_many(models, options = {})
it "should have many #{models}" do
if options.empty?
class_or_instance.should have_many(models)
else
class_or_instance.should have_many(models).with_options(options)
end
end
end
# Creates an expectation that the current model being spec'd has a <tt>have_and_belong_to_many</tt>
# association with the specified model. Accepts optional arguments which are appended to
# the <tt>have_and_belong_to_many</tt> spec like this:
#
# it_should_have_and_belong_to_many :documents, :include => :attachments
#
# which is the same as writing out:
#
# it "should belong to document" do
# User.should have_and_belong_to_many(:documents).with_options(:include => :attachments)
# end
#
# If you want a more detailed spec description text, feel free to write this out in the long
# form and use <tt>have_and_belong_to_many</tt> and its related matcher helpers.
#
# <b>Note:</b> The argument should be a symbol as in the model's association definition
# and not the model's class name.
def it_should_have_and_belong_to_many(models, options = {})
it "should have and belong to many #{models}" do
if options.empty?
class_or_instance.should have_and_belong_to_many(models)
else
class_or_instance.should have_and_belong_to_many(models).with_options(options)
end
end
end
# Creates an expectation that new instances of the model being spec'd
# should initialise the specified attributes with a default value.
#
# it_should_default_attributes :status => 'new'
#
def it_should_default_attributes(hash_attribute_values)
hash_attribute_values.each_pair do |a,v|
it "should default #{a} attribute to #{v}" do
class_or_instance.new.send(a).should == v
end
end
end
# Creates an expectation that the current model being spec'd <tt>validates_presence_of</tt>
# the specified attribute. Takes an optional custom message to match the one in the model's
# validation.
def it_should_validate_presence_of(attribute, message = default_error_message(:blank))
it "should not be valid if #{attribute} is blank" do
instance.send "#{attribute}=", nil
instance.errors_on(attribute).should include(message)
end
end
# Negative version of <tt>it_should_validate_presence_of</tt>. See that method for more
# details. You'd probably only be using this in a nested example block to compare that
# one scenario validates presence and another does not (because of conditions in
# <tt>:if/:unless</tt>).
def it_should_not_validate_presence_of(attribute, message = default_error_message(:blank))
it "should be valid if #{attribute} is blank" do
instance.send "#{attribute}=", nil
instance.errors_on(attribute).should_not include(message)
end
end
# Creates an expectation that the current model being spec'd <tt>validates_inclusion_of</tt>
# the specified attribute. Takes an optional custom message to match the one in the model's
# validation.
def it_should_validate_inclusion_of(attribute, options = {}, message = default_error_message(:inclusion))
it "should validate #{attribute} is in #{options[:in].to_s}" do
# We specifically do not try to go below the range on String and character ranges because that problem set is unpredictable.
lower = options[:in].first.respond_to?(:-) ? options[:in].first - 0.0001 : nil
higher = options[:in].last.succ
instance.send "#{attribute}=", lower
instance.errors_on(attribute).should include(message)
instance.send "#{attribute}=", higher
instance.errors_on(attribute).should include(message)
instance.send "#{attribute}=", (lower+higher)/2
instance.errors_on(attribute).should_not include(message)
end
end
# Creates an expectation that the current model being spec'd <tt>validates_numericality_of</tt>
# the specified attribute. Takes an optional custom message to match the one in the model's
# validation.
def it_should_validate_numericality_of(attribute, message = default_error_message(:not_a_number))
it "should validate #{attribute} is a numeric" do
instance.send "#{attribute}=", "NaN"
instance.errors_on(attribute).should include(message)
end
end
# Negative version of <tt>it_should_validate_numericality_of</tt>. See that method for more
# details. You'd probably only be using this in a nested example block to compare that
# one scenario validates presence and another does not (because of conditions in
# <tt>:if/:unless</tt>).
def it_should_not_validate_numericality_of(attribute, message = default_error_message(:not_a_number))
it "should not validate #{attribute} is a numeric" do
instance.send "#{attribute}=", "NaN"
instance.errors_on(attribute).should_not include(message)
end
end
# Creates an expectation that the current model being spec'd <tt>validates_confirmation_of</tt>
# the specified attribute. Takes an optional custom message to match the one in the model's
# validation.
def it_should_validate_confirmation_of(attribute, message = default_error_message(:confirmation))
it "should validate confirmation of #{attribute}" do
dummy_value = dummy_value_for(instance, attribute) || "try a string"
instance.send "#{attribute}=", dummy_value
instance.send "#{attribute}_confirmation=", dummy_value.succ
instance.errors_on(attribute).should include(message)
end
end
# Creates an expectation that the current model being spec'd <tt>validates_uniqueness_of</tt>
# the specified attribute. Takes an optional custom message to match the one in the model's
# validation.
#
# <b>Note:</b> This method will fail completely if <tt>valid_attributes</tt>
# does not provide all the attributes needed to create a valid record.
def it_should_validate_uniqueness_of(attribute, message = default_error_message(:taken))
it "should validate uniqueness of #{attribute}" do
previous_instance = instance.class.create!(valid_attributes)
instance.attributes = valid_attributes
instance.errors_on(attribute).should include(message)
previous_instance.destroy
end
end
# Negative version of <tt>it_should_validate_uniqueness_of</tt>. See that method for more
# details. You'd probably only be using this in a nested example block to compare that
# one scenario validates presence and another does not (because of conditions in
# <tt>:if/:unless</tt>).
def it_should_not_validate_uniqueness_of(attribute, message = default_error_message(:taken))
it "should not validate uniqueness of #{attribute}" do
previous_instance = instance.class.create!(valid_attributes)
instance.attributes = valid_attributes
instance.errors_on(attribute).should_not include(message)
previous_instance.destroy
end
end
# Creates an expectation that the current model being spec'd accepts the specified values as
# valid for the specified attribute. This is most likely used with <tt>validates_format_of</tt>
# but there's nothing saying it couldn't be another validation.
def it_should_accept_as_valid(attribute, *values)
values.flatten.each do |value|
value_inspect = case value
when String : "'#{value}'"
when NilClass : "nil"
else value
end
it "should accept #{value_inspect} as a valid #{attribute}" do
instance.send "#{attribute}=", value
instance.errors_on(attribute).should == []
end
end
end
# Creates an expectation that the current model being spec'd does not accept the specified
# values as valid for the specified attribute. This is most likely used with
# <tt>validates_format_of</tt> but there's nothing saying it couldn't be another validation.
# Takes an optional argument <tt>:message => "some custom error messsage"</tt> for
# spec'ing the actual error message.
def it_should_not_accept_as_valid(attribute, *values)
options = values.extract_options!
values.flatten.each do |value|
value_inspect = case value
when String : "'#{value}'"
when NilClass : "nil"
else value
end
it "should not accept #{value_inspect} as a valid #{attribute}" do
instance.send "#{attribute}=", value
if options[:message]
instance.errors_on(attribute).should include(options[:message])
else
instance.should have_at_least(1).errors_on(attribute)
end
end
end
end
# Creates an expectation that the current model being spec'd doesn't allow mass-assignment
# of the specified attribute.
def it_should_not_mass_assign(attribute)
it "should not allow mass-assignment of #{attribute}" do
lambda {
instance.send :attributes=, {attribute => dummy_value_for(instance, attribute)}
}.should_not change(instance, attribute)
end
end
def default_error_message(attribute)
if defined?(I18n)
I18n.translate attribute, :scope => "activerecord.errors.messages"
else
ActiveRecord::Errors.default_error_messages[attribute]
end
end
end
end
end

View file

@ -0,0 +1,577 @@
$:.unshift File.join(File.dirname(__FILE__), "..")
require "skinny_spec"
module LuckySneaks
# These methods are designed to be used in your example [read: "it"] blocks
# to make your view specs less brittle and more DRY. You might also be interested
# in checking out the example block [read: "describe"] level versions in of these
# methods which can DRY things up even more:
# LuckySneaks::ViewSpecHelpers::ExampleGroupLevelMethods.
module ViewSpecHelpers
include LuckySneaks::CommonSpecHelpers
include LuckySneaks::ViewStubHelpers
include ActionController::PolymorphicRoutes
def self.included(base) # :nodoc:
base.extend ExampleGroupLevelMethods
end
# Wraps a matcher that checks if the receiver contains a <tt>FORM</tt> element with
# its <tt>action</tt> attribute set to the specified path.
def submit_to(path)
have_tag("form[action=#{path}]")
end
# Wraps a matcher that checks if the receiver contains any of several form elements
# that would return sufficient named parameters to allow editing of the specified
# attribute on the specified instance. Example:
#
# response.should allow_editing(@foo, "bar")
#
# can be satisfied by any of the following HTML elements:
#
# <input name="foo[bar]" type="text" />
# <input name="foo[bar]" type="checkbox" />
# <input name="foo[bar_ids][]" type="checkbox" />
# <select name="foo[bar]"></select>
# <textarea name="foo[bar]"></textarea>
def allow_editing(instance, attribute)
instance_name = instance.class.name.underscore.downcase
column = instance.column_for_attribute(attribute)
if column && [Date, Time].include?(column.klass)
have_tag(
"input[name='#{instance_name}[#{attribute}]'],
select[name=?]", /#{instance_name}\[#{attribute}\(.*\)\]/
)
else
have_tag(
"input[type='text'][name='#{instance_name}[#{attribute}]'],
input[type='password'][name='#{instance_name}[#{attribute}]'],
select[name='#{instance_name}[#{attribute}]'],
textarea[name='#{instance_name}[#{attribute}]'],
input[type='checkbox'][name='#{instance_name}[#{attribute}]'],
input[type='checkbox'][name='#{instance_name}[#{attribute.to_s.tableize.singularize}_ids][]'],
input[type='radio'][name='#{instance_name}[#{attribute}]']"
)
end
end
# Wraps a matcher that checks if the receiver contains a <tt>FORM</tt> element
# whose <tt>enctype</tt> attribute is set to <tt>"multipart/form-data"<tt>
# and contains an <tt>INPUT</tt> element whose <tt>name</tt> attribute correlates
# with the provided instance and attribute.
def allow_uploading(instance, attribute)
instance_name = instance.class.name.underscore.downcase
have_tag("form[enctype='multipart/form-data'] input[type='file'][name='#{instance_name}[#{attribute}]']")
end
# Wraps a matcher that checks if the receiver contains an <tt>A</tt> element (link)
# whose <tt>href</tt> attribute is set to the specified path or a <tt>FORM</tt>
# element whose <tt>action</tt> attribute is set to the specified path.
def have_link_or_button_to(path)
have_tag(
"a[href='#{path}'],
form[action='#{path}'] input,
form[action='#{path}'] button"
)
end
alias have_link_to have_link_or_button_to
alias have_button_to have_link_or_button_to
# Wraps <tt>have_link_or_button_to new_polymorphic_path<tt> for the specified class which
# corresponds with the <tt>new</tt> method of the controller.
#
# <b>Note:</b> This method may takes a string or symbol representing the model's name
# to send to <tt>have_link_or_button_to_show</tt> or the model's name itself.
def have_link_or_button_to_new(name)
have_link_or_button_to new_polymorphic_path(name.is_a?(ActiveRecord::Base) ? name : class_for(name))
end
# Wraps <tt>have_link_or_button_to polymorphic_path(instance)<tt> which
# corresponds with the <tt>show</tt> method of the controller.
def have_link_or_button_to_show(instance)
path = polymorphic_path(instance)
have_tag(
"a[href='#{path}'],
form[action='#{path}'][method='get'] input,
form[action='#{path}'][method='get'] button,
form[action='#{path}'] input[name='_method'][value='get'] + input,
form[action='#{path}'] input[name='_method'][value='get'] + button"
)
end
alias have_link_to_show have_link_or_button_to_show
alias have_button_to_show have_link_or_button_to_show
# Wraps <tt>have_link_or_button_to edit_polymorphic_path(instance)<tt> which
# corresponds with the <tt>edit</tt> method of the controller.
def have_link_or_button_to_edit(instance)
have_link_or_button_to edit_polymorphic_path(instance)
end
alias have_link_to_edit have_link_or_button_to_edit
alias have_button_to_edit have_link_or_button_to_edit
# Wraps a matcher that checks if the receiver contains the HTML created by Rails'
# <tt>button_to</tt> helper: to wit, a <tt>FORM</tt> element whose <tt>action</tt>
# attribute is pointed at the <tt>polymorphic_path</tt> of the instance
# and contains an <tt>INPUT</tt> named "_method" with a value of "delete".
def have_button_to_delete(instance)
path = polymorphic_path(instance)
have_tag(
"form[action='#{path}'] input[name='_method'][value='delete'] + input,
form[action='#{path}'] input[name='_method'][value='delete'] + button,
a[href=\"#{path}\"][onclick*=\"f.method = 'POST'\"][onclick*=\"m.setAttribute('name', '_method'); m.setAttribute('value', 'delete')\"]"
)
end
# Creates a <tt>mock_model</tt> instance and adds it to the <tt>assigns</tt> collection
# using either the name passed as the first argument or the underscore version
# of its class name. Accepts optional arguments to stub out additional methods
# (and their return values) on the <tt>mock_model</tt> instance. Example:
#
# mock_and_assign(Foo, :stub => {:bar => "bar"})
#
# is the same as running <tt>assigns[:foo] = mock_model(Foo, :bar => "bar")</tt>.
#
# mock_and_assign(Foo, "special_foo", :stub => {:bar => "baz"})
#
# is the same as running <tt>assigns[:special_foo] = mock_model(Foo, :bar => "baz").
#
# <b>Note:</b> Adding to the assigns collection returns the object added, so this can
# be chained a la <tt>@foo = mock_and_assign(Foo)</tt>.
def mock_and_assign(klass, *args)
options = args.extract_options!
mocked = if options[:stub]
self.respond_to?(:stub_model) ? stub_model(klass, options[:stub]) : mock_model(klass, options[:stub])
else
self.respond_to?(:stub_model) ? stub_model(klass) : mock_model(klass)
end
yield mocked if block_given?
self.assigns[args.first || "#{klass}".underscore] = mocked
end
# Creates an array of <tt>mock_model</tt> instances in the manner of
# <tt>mock_and_assign</tt>. Accepts <tt>option[:size]</tt> which sets the size
# of the array (default is 3).
def mock_and_assign_collection(klass, *args)
options = args.extract_options!
return_me = Array.new(options[:size] || 3) do
mocked = if options[:stub]
self.respond_to?(:stub_model) ? stub_model(klass, options[:stub]) : mock_model(klass, options[:stub])
else
self.respond_to?(:stub_model) ? stub_model(klass) : mock_model(klass)
end
yield mocked if block_given?
mocked
end
self.assigns[args.first || "#{klass}".tableize] = return_me
end
private
def do_render
if @the_template
render @the_template
elsif File.exists?(File.join(RAILS_ROOT, "app/views", class_description_text))
render class_description_text
else
error_message = "Cannot determine template for render. "
error_message << "Please define @the_template in the before block "
error_message << "or name your describe block so that it indicates the correct template."
raise NameError, error_message
end
end
# These methods are designed to be used at the example group [read: "describe"] level
# to simplify and DRY up common expectations. Most of these methods are wrappers for
# matchers which can also be used on the example level [read: within an "it" block]. See
# LuckySneaks::ViewSpecHelpers for more information.
module ExampleGroupLevelMethods
include LuckySneaks::CommonSpecHelpers
# Creates an expectation which calls <tt>submit_to</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a Proc to evaluate the route not simply a named route
# helper, which would be undefined in the scope of the example block.
def it_should_submit_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should submit to #{(hint || route)}" do
do_render
response.should submit_to(instance_eval(&route))
end
end
# Negative version of <tt>it_should_submit_to</tt>. See that method for more
# details.
def it_should_not_submit_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should not submit to #{(hint || route)}" do
do_render
response.should_not submit_to(instance_eval(&route))
end
end
# Creates an expectation that the template uses Rails' <tt>form_for</tt> to generate
# the proper form action and method to create or update the specified object.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to create the expectation for <tt>form_for</tt>
# not an instance variable, which would be nil in the scope of the example block.
# If you use namespacing for your <tt>form_for</tt>, you'll have to manually write out
# a similar spec.
def it_should_have_form_for(name, options = {})
it "should have a form_for(@#{name})" do
if options.empty?
template.should_receive(:form_for).with(instance_for(name))
else
template.should_receive(:form_for).with(instance_for(name), hash_including(options))
end
do_render
end
end
# Negative version of <tt>it_should_have_form_for</tt>. See that method for more
# details.
def it_should_not_have_form_for(name, options = {})
it "should not have a form_for(@#{name})" do
if options.empty?
template.should_not_receive(:form_for).with(instance_for(name))
else
template.should_not_receive(:form_for).with(instance_for(name), hash_including(options))
end
do_render
end
end
# Creates an expectation which calls <tt>allow_editing</tt> on the rendered
# template for each attribute specified. See the docs for <tt>allow_editing</tt>
# for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to send to <tt>allow_editing</tt>
# not an instance variable, which would be nil in the scope of the example block.
def it_should_allow_editing(instance_name, *attributes)
attributes.flatten!
attributes.each do |attribute|
it "should allow editing of @#{instance_name}##{attribute}" do
do_render
response.should allow_editing(instance_for(instance_name), attribute)
end
end
end
# Negative version of <tt>it_should_allow_editing</tt>. See that method for more
# details.
def it_should_not_allow_editing(instance_name, *attributes)
attributes.flatten!
attributes.each do |attribute|
it "should not allow editing of @#{instance_name}##{attribute}" do
do_render
response.should_not allow_editing(instance_for(instance_name), attribute)
end
end
end
# Creates an expectation which calls <tt>allow_uploading</tt> on the rendered
# template for each attribute specified. See the docs for <tt>allow_uploading</tt>
# for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to send to <tt>allow_uploading</tt>
# not an instance variable, which would be nil in the scope of the example block.
def it_should_allow_uploading(instance_name, *attributes)
attributes.flatten!
attributes.each do |attribute|
it "should allow editing of @#{instance_name}##{attribute}" do
do_render
response.should allow_uploading(instance_for(instance_name), attribute)
end
end
end
# Negative version of <tt>it_should_allow_uploading</tt>. See that method for more
# details.
def it_should_not_allow_uploading(instance_name, *attributes)
attributes.flatten!
attributes.each do |attribute|
it "should not allow editing of @#{instance_name}##{attribute}" do
do_render
response.should_not allow_uploading(instance_for(instance_name), attribute)
end
end
end
# Creates an expectation that the rendered template contains a <tt>FORM</tt> element
# (<tt>INPUT</tt>, <tt>TEXTAREA</tt>, or <tt>SELECT</tt>) with the specified name.
def it_should_have_form_element_for(name)
it "should have a form element named '#{name}'" do
do_render
response.should have_tag(
"form input[name='#{name}'],
form textarea[name='#{name}'],
form select[name='#{name}']"
)
end
end
# Negative version of <tt>it_should_have_form_element_for</tt>. See that method
# for more details.
def it_should_not_have_form_element_for(name)
it "should not have a form element named '#{name}'" do
do_render
response.should_not have_tag(
"form input[name='#{name}'],
form textarea[name='#{name}'],
form select[name='#{name}']"
)
end
end
# Creates an expectation which calls <tt>have_link_or_button_to</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a block to evaluate the route in the example context
# instead of the example group context.
def it_should_link_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should have a link/button to #{(hint || route)}" do
do_render
response.should have_link_or_button_to(instance_eval(&route))
end
end
alias it_should_have_link_to it_should_link_to
alias it_should_have_button_to it_should_link_to
alias it_should_have_button_or_link_to it_should_link_to
# Negative version of <tt>it_should_link_to</tt>. See that method
# for more details.
def it_should_not_link_to(hint = nil, &route)
if hint.nil? && route.respond_to?(:to_ruby)
hint = route.to_ruby.gsub(/(^proc \{)|(\}$)/, '').strip
end
it "should have a link/button to #{(hint || route)}" do
do_render
response.should_not have_link_or_button_to(instance_eval(&route))
end
end
alias it_should_not_have_link_to it_should_not_link_to
alias it_should_not_have_button_to it_should_not_link_to
alias it_should_not_have_button_or_link_to it_should_not_link_to
# Creates an expectation which calls <tt>have_link_or_button_to_new</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method may takes a string or symbol representing the model's name
# to send to <tt>have_link_or_button_to_show</tt> or the model's name itself.
def it_should_link_to_new(name)
it "should have a link/button to create a new #{name}" do
do_render
response.should have_link_or_button_to_new(name)
end
end
alias it_should_have_link_to_new it_should_link_to_new
alias it_should_have_button_to_new it_should_link_to_new
alias it_should_have_button_or_link_to_new it_should_link_to_new
# Negative version of <tt>it_should_link_to_show</tt>. See that method
# for more details.
def it_should_not_link_to_new(name)
it "should have a link/button to create a new #{name}" do
do_render
response.should_not have_link_or_button_to_new(name)
end
end
alias it_should_not_have_link_to_new it_should_not_link_to_new
alias it_should_not_have_button_to_new it_should_not_link_to_new
alias it_should_not_have_button_or_link_to_new it_should_not_link_to_new
# Creates an expectation which calls <tt>have_link_or_button_to_show</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to send to <tt>have_link_or_button_to_show</tt>
# not an instance variable, which would be nil in the scope of the example block.
def it_should_link_to_show(name)
it "should have a link/button to show @#{name}" do
do_render
response.should have_link_or_button_to_show(instance_for(name))
end
end
alias it_should_have_link_to_show it_should_link_to_show
alias it_should_have_button_to_show it_should_link_to_show
alias it_should_have_button_or_link_to_show it_should_link_to_show
# Negative version of <tt>it_should_link_to_show</tt>. See that method
# for more details.
def it_should_not_link_to_show(name)
it "should have a link/button to show @#{name}" do
do_render
response.should_not have_link_or_button_to_show(instance_for(name))
end
end
alias it_should_not_have_link_to_show it_should_not_link_to_show
alias it_should_not_have_button_to_show it_should_not_link_to_show
alias it_should_not_have_button_or_link_to_show it_should_not_link_to_show
# Creates an expectation which calls <tt>have_link_or_button_to_show</tt>
# for each member of the instance variable matching the specified name
# on the response from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name and not an instance variable, which would be nil
# in the scope of the example block.
def it_should_link_to_show_each(name)
it "should have a link/button to show each member of @#{name}" do
do_render
instance_for(name).each do |member|
response.should have_link_or_button_to_show(member)
end
end
end
alias it_should_have_link_to_show_each it_should_link_to_show_each
alias it_should_have_button_to_show_each it_should_link_to_show_each
alias it_should_have_button_or_link_to_show_each it_should_link_to_show_each
# Creates an expectation which calls <tt>have_link_or_button_to_edit</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to send to <tt>have_link_or_button_to_edit</tt>
# not an instance variable, which would be nil in the scope of the example block.
def it_should_link_to_edit(name)
it "should have a link/button to edit @#{name}" do
do_render
response.should have_link_or_button_to_edit(instance_for(name))
end
end
alias it_should_have_link_to_edit it_should_link_to_edit
alias it_should_have_button_to_edit it_should_link_to_edit
alias it_should_have_button_or_link_to_edit it_should_link_to_edit
# Negative version of <tt>it_should_link_to_edit</tt>. See that method
# for more details.
def it_should_not_link_to_edit(name)
it "should have a link/button to edit @#{name}" do
do_render
response.should_not have_link_or_button_to_edit(instance_for(name))
end
end
alias it_should_not_have_link_to_edit it_should_not_link_to_edit
alias it_should_not_have_button_to_edit it_should_not_link_to_edit
alias it_should_not_have_button_or_link_to_edit it_should_not_link_to_edit
# Creates an expectation which calls <tt>have_link_or_button_to_edit</tt>
# for each member of the instance variable matching the specified name
# on the response from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name and not an instance variable, which would be nil
# in the scope of the example block.
def it_should_link_to_edit_each(name)
it "should have a link/button to edit each member of @#{name}" do
do_render
instance_for(name).each do |member|
response.should have_link_or_button_to_edit(member)
end
end
end
alias it_should_have_link_to_edit_each it_should_link_to_edit_each
alias it_should_have_button_to_edit_each it_should_link_to_edit_each
alias it_should_have_button_or_link_to_edit_each it_should_link_to_edit_each
# Creates an expectation which calls <tt>have_link_or_button_to_delete</tt> on the response
# from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name to send to <tt>have_link_or_button_to_delete</tt>
# not an instance variable, which would be nil in the scope of the example block.
def it_should_link_to_delete(name)
it "should have a link/button to delete @#{name}" do
do_render
response.should have_button_to_delete(instance_for(name))
end
end
alias it_should_have_link_to_delete it_should_link_to_delete
alias it_should_have_button_to_delete it_should_link_to_delete
alias it_should_have_button_or_link_to_delete it_should_link_to_delete
# Negative version of <tt>it_should_link_to_delete</tt>. See that method
# for more details.
def it_should_not_link_to_delete(name)
it "should not have a link/button to delete @#{name}" do
do_render
response.should_not have_button_to_delete(instance_for(name))
end
end
alias it_should_not_have_link_to_delete it_should_not_link_to_delete
alias it_should_not_have_button_to_delete it_should_not_link_to_delete
alias it_should_not_have_button_or_link_to_delete it_should_not_link_to_delete
# Creates an expectation which calls <tt>have_link_or_button_to_delete</tt>
# for each member of the instance variable matching the specified name
# on the response from rendering the template. See that method for more details.
#
# <b>Note:</b> This method takes a string or symbol representing the instance
# variable's name and not an instance variable, which would be nil
# in the scope of the example block.
def it_should_link_to_delete_each(name)
it "should have a link/button to delete each member of @#{name}" do
do_render
instance_for(name).each do |member|
response.should have_button_to_delete(member)
end
end
end
alias it_should_have_link_to_delete_each it_should_link_to_delete_each
alias it_should_have_button_to_delete_each it_should_link_to_delete_each
alias it_should_have_button_or_link_to_delete_each it_should_link_to_delete_each
# Creates an expectation that the template should call <tt>render :partial</tt>
# with the specified template.
def it_should_render_partial(name)
it "should render :partial => '#{name}'" do
template.should_receive(:render).with(hash_including(:partial => name))
do_render
end
end
# Negative version of <tt>it_should_render_partial</tt>. See that method
# for more details.
def it_should_not_render_partial(name)
it "should not render :partial => '#{name}'" do
template.should_not_receive(:render).with(hash_including(:partial => name))
do_render
end
end
# Sets <tt>@the_template</tt> (for use in <tt>do_render</tt>) using the current
# example group description. Example:
#
# describe "users/index.haml.erb" do
# use_describe_for_template!
# # ...
# end
#
# This is equivalent to setting <tt>@the_template = "users/index.haml.erb"</tt>
# in a before block.
def use_describe_for_template!
template = self_description_text
if File.exists?(File.join(RAILS_ROOT, "app/views", template))
before(:each) do
@the_template = template
end
else
error_message = "You called use_describe_for_template! "
error_message << "but 'app/views/#{template}' does not exist. "
raise NameError, error_message
end
end
end
end
end

View file

@ -0,0 +1,15 @@
$:.unshift File.join(File.dirname(__FILE__), "..")
require "skinny_spec"
module LuckySneaks
# These methods are designed to be used in your example <tt>before</tt> blocks to accomplish
# a whole lot of functionality with just a tiny bit of effort.
module ViewStubHelpers
# Shorthand for the following stub:
#
# template.stub!(:render).with(hash_including(:partial => anything))
def stub_partial_rendering!
template.stub!(:render).with(hash_including(:partial => anything))
end
end
end

View file

@ -0,0 +1,26 @@
# Let's make sure everyone else is loaded
require File.expand_path(RAILS_ROOT + "/config/environment")
require 'spec'
require 'spec/rails'
begin
require 'ruby2ruby'
rescue LoadError
puts "-----"
puts "Attention: skinny_spec requires ruby2ruby for nicer route descriptions"
puts "It is highly recommended that you install it: sudo gem install ruby2ruby"
puts "-----"
end
# Let's load our family now
require "lucky_sneaks/common_spec_helpers"
require "lucky_sneaks/controller_request_helpers"
require "lucky_sneaks/controller_spec_helpers"
require "lucky_sneaks/controller_stub_helpers"
require "lucky_sneaks/model_spec_helpers"
require "lucky_sneaks/view_spec_helpers"
# Let's all come together
Spec::Rails::Example::ViewExampleGroup.send :include, LuckySneaks::ViewSpecHelpers
Spec::Rails::Example::HelperExampleGroup.send :include, LuckySneaks::CommonSpecHelpers
Spec::Rails::Example::ControllerExampleGroup.send :include, LuckySneaks::ControllerSpecHelpers
Spec::Rails::Example::ModelExampleGroup.send :include, LuckySneaks::ModelSpecHelpers

View file

@ -0,0 +1,46 @@
= swf_fu --- History
== Version 1.4.0 - May 8, 2010
* Any option can be a block, in which case it is called (with the source swf passed as argument)
== Version 1.3.1 - February 5, 2010
* Improved compatibility with Rails 3.0: swf_tag now outputs html_safe content.
* Got rid of deprecation warning in Rails 2.2+ when using swf_tag in block form.
== Version 1.3.0 - June 20, 2009
* Updated to swf_object v2.2. Change should not be noticeable to users, except compatibility improvements and better auto install. Added the option +switch_off_auto_hide_show+.
== Version 1.2.0 - January 14, 2009
* flashvars[:id] will now default to the DOM id of the object. I didn't want to have any extra defaults than the very basic ones, but there is no easy way to get this from Flash (see http://www.actionscript.org/forums/showthread.php3?t=136044 ) and no easy way to specify that using +swf_default_options+.
* If flashvars is a string (e.g. "myVar=myValue") it will be parsed into a hash so that the behaviour for default values apply to strings or hashes. swf_default_options[:flashvars] can also be a string and will also be parsed before being merged.
* Small bug fix: the options passed as hashes (:flashvars, :parameters and :html_options) were changed if swf_default_options[:flashvars, ...] existed. They are now left unchanged.
== Version 1.1.0 - January 3, 2009
* Improved the way to specify alternate content
== Version 1.0.3 - January 3, 2009
* Improved javascript initialization
:initialize => [1, 2, 3] # produces in javascript: obj.initialize(1,2,3) instead of ([1,2,3])
# no :initialize produces in javascript: obj.initialize() instead of (null)
:initialize => nil # stil produces obj.initialize(null)
== Version 1.0.2 - January 3, 2009
* Bug fix for flashvars in dynamic method
== Version 1.0.1 - January 2, 2009
* File reorganization
* Bug fix for default options
== Version 1.0 - X-mas, 2008
=== Initial release.

View file

@ -0,0 +1,31 @@
== Compatibility with FlashObject
This document is intended for users of FlashObject, a (much older) swf embedding plugin that inspired swf_fu.
You can choose to:
1) keep both. They won't interfere and +flashobject_tag+ will continue to use the older SWFObject 1.5 library.
2) remove FlashObject:
script/plugin remove flashobject_helper
You can also manually remove <tt>javascripts/flashobject.js</tt>
+swf_fu+ will take over the +flashobject_tag+ and will use the new SWFObject 2.2 library.
This should not have impacts as long as:
* your swf path is absolute (e.g. "/path/to/my_flash.swf"). If it is relative, move your swf file from 'public/' to the new 'public/swfs/' asset folder
* you include the default javascripts (otherwise you need to include 'swfobject' explicitely and stop including 'flashobject')
* you don't use the javascript object before the page is loaded. SWFObject 2.2 makes the changes to the web page later
* you don't rely on the +verify_file_exists+ option (it doesn't do anything anymore)
In either case 1 or 2, you change existing calls to +flashobject_tag+ for +swf_tag+ at your leisure.
The interface is similar and the main differences are some options name changes:
:flash_id => :id
:variables => :flashvars
:background_color => options[:parameters][:bgcolor]
Moreover, the following defaults are gone:
:flashvars[:lzproxied]
:parameters[:scale]
:parameters[:bgcolor]

View file

@ -0,0 +1,29 @@
# swf_fu plugin for rails
# Copyright (c) 2010, Marc-André Lafortune.
# All rights reserved.
# Inspired by FlashObject by Davide D'Agostino aka DAddYE (http://www.lipsiasoft.com)
# Uses SWFObject.js 2.1 (http://code.google.com/p/swfobject)
#
# Licensed under the terms of the (modified) BSD License below:
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the <organization> nor the
# names of its contributors may be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ''AS IS'' AND ANY
# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,92 @@
= +swf_fu+
With the +swf_fu+ plugin, rails treats your swf files like any other asset (images, javascripts, etc...).
+swf_fu+ (pronounced "swif-fu", bonus joke for french speakers) uses SWFObject 2.2 to embed swf objects in HTML and supports all its options.
SWFObject 2 is such a nice library that Adobe now uses it as the official way to embed swf!
SWFObject's project can be found at http://code.google.com/p/swfobject
+swf_fu+ has been tested with rails v2.0 up to v3.0b and has decent test coverage so <tt>rake test:plugins</tt> should reveal any incompatibility. Comments and pull requests welcome: http://github.com/marcandre/swf_fu
== Install
Assuming you have git[http://git-scm.com/] installed (check with <tt>git version</tt>), it is easy to install from your applications directory:
rails plugin install git://github.com/marcandre/swf_fu.git # rails 3
script/plugin install git://github.com/marcandre/swf_fu.git # rails 2 (starting at 2.0.2)
For older versions of +rails+ or without +git+, you can always download
+swf_fu+ from github[http://github.com/marcandre/swf_fu/archives/master] and then install it manually:
rails plugin install ~/Download/swf_fu # rails 3
script/plugin install ~/Downloads/swf_fu # rails 2.x
== Usage
=== Embedding in HTML
To embed a swf file, use +swf_tag+:
<%= swf_tag "i_like_flashing" %>
Exactly like images and javascripts, +swf_tag+ will use +swf_path+
to determine the path of the swf file; it will assume it is in <tt>/public/swfs/</tt>
unless specified otherwise and it will add the ".swf" extension automatically.
You can specify alternate content either with the options <tt>:alt => "Get Flash!"</tt> or you can use +swf_tag+ as a block:
<% swf_tag "i_like_flashing" do %>
Get Flash
<% end %>
=== Options
* <tt>:id</tt> - the DOM +id+ of the flash +object+ element that is used to contain the Flash object; defaults to the name of the swf in +source+
* <tt>:width, :height</tt> - the width & height of the Flash object. Defaults to "100%". These could also specified using :size
* <tt>:size</tt> - the size of the Flash object, in the form "400x300".
* <tt>:mode</tt> - Either :dynamic (default) or :static. Refer to SWFObject's doc[http://code.google.com/p/swfobject/wiki/documentation#Should_I_use_the_static_or_dynamic_publishing_method?]
* <tt>:flashvars</tt> - a Hash of variables that are passed to the swf. Can also be a string like <tt>"foo=bar&hello=world"</tt>. Defaults to <tt>{:id => the DOM id}</tt>
* <tt>:parameters</tt> - a Hash of configuration parameters for the swf. See Adobe's doc[http://kb.adobe.com/selfservice/viewContent.do?externalId=tn_12701#optional]
* <tt>:html_options</tt> - a Hash of extra html options for the <tt>object</tt> tag.
* <tt>:alt</tt> - HTML text that is displayed when the Flash player is not available. Defaults to a "Get Flash" image pointing to Adobe Flash's installation page. This can also be specified as a block (see embedding section). In Rails 3, this text is _assumed_ to be HTML, so there is no need to call +html_safe+ on it.
* <tt>:flash_version</tt> - the version of the Flash player that is required (e.g. "7" (default) or "8.1.0")
* <tt>:auto_install</tt> - a swf file that will upgrade flash player if needed (defaults to "expressInstall" which was installed by +swf_fu+)
* <tt>:javascript_class</tt> - specify a javascript class (e.g. "MyFlash") for your flash object. If it exists, the initialize method will be called.
* <tt>:initialize</tt> - arguments to pass to the initialization method of your javascript class.
* <tt>:div_id</tt> - the DOM +id+ of the containing div itself. Defaults to <tt>"#{option[:id]}_div"</tt>
* <tt>:switch_off_auto_hide_show</tt> - switch off SWFObject's default hide/show behavior. SWFObject temporarily hides your SWF or alternative content until the library has decided which content to display. Defaults to nil.
You can override these default options with a global setting:
ActionView::Base.swf_default_options = {:mode => :static} # All swf_tag will use the static mode by default
Any of these options can be a +Proc+, in which case it will be called each time swf_tag is called.
For example, the following will generate unique IDs:
my_swf_counter = 0
ActionView::Base.swf_default_options[:id] = Proc.new{"swf_unique_id_#{my_swf_counter+=1}"}
=== Javascript
+swf_fu+ will add 'swfobject' to the list of default javascript files. If you don't include
the default javascripts, a simple <tt>javascript_include "swfobject"</tt> is needed.
=== swf_path
+swf_tag+ implements and relies on +swf_path+ which behaves in a similar fashion to +image_path+, +javascript_path+, etc...:
swf_path("example") => "/swfs/example.swf"
swf_path("example.swf") => "/swfs/example.swf"
swf_path("fonts/optima") => "/swfs/fonts/optima.swf"
swf_path("/fonts/optima") => "/fonts/optima.swf"
swf_path("http://www.example.com/game.swf") => "http://www.example.com/game.swf"
It takes into account the global setting +asset_host+, like any other asset:
ActionController::Base.asset_host = "http://assets.example.com"
image_path("logo.jpg") => "http://assets.example.com/images/logo.jpg"
swf_path("fonts/optima") => "http://assets.example.com/swfs/fonts/optima.swf""
Copyright (c) 2010 Marc-André Lafortune, released under the BSD license

View file

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

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,14 @@
ActionView::Helpers::AssetTagHelper rescue require 'action_view/helpers/asset_tag_helper' # Might be needed in some testing environments
require File.dirname(__FILE__) + "/lib/action_view/helpers/swf_fu_helper"
require File.dirname(__FILE__) + "/lib/action_view/helpers/asset_tag_helper/swf_asset"
# ActionView::Helpers is for recent rails version, ActionView::Base for older ones (in which case ActionView::Helpers::AssetTagHelper is also needed for tests...)
ActionView::Helpers.class_eval { include ActionView::Helpers::SwfFuHelper } # For recent rails version...
ActionView::Base.class_eval { include ActionView::Helpers::SwfFuHelper } # ...and for older ones
ActionView::TestCase.class_eval { include ActionView::Helpers::SwfFuHelper } if defined? ActionView::TestCase # ...for tests in older versions
begin
ActionView::Helpers::AssetTagHelper.register_javascript_expansion :default => ["swfobject"]
rescue NoMethodError # I think this might fail in Rails 2.1.x
ActionView::Helpers::AssetTagHelper.register_javascript_include_default 'swfobject'
end

View file

@ -0,0 +1,24 @@
require "fileutils"
# Some paths
src = File.dirname(__FILE__)+"/assets"
dest = File.dirname(__FILE__)+"/../../../public"
filename = "#{dest}/javascripts/swfobject.js"
unless File.exist?(filename)
FileUtils.cp "#{src}/javascripts/swfobject.js", filename
puts "Copying 'swfobject.js'"
end
unless File.exist?("#{dest}/swfs/")
FileUtils.mkdir "#{dest}/swfs/"
puts "Creating new 'swfs' directory for swf assets"
end
filename = "#{dest}/swfs/expressInstall.swf"
unless File.exist?(filename)
FileUtils.cp "#{src}/swfs/expressInstall.swf", filename
puts "Copying 'expressInstall.swf', the default flash auto-installer."
end
puts "Installation done."

View file

@ -0,0 +1,61 @@
module ActionView #:nodoc:
# <tt>ActionView::Base.swf_default_options</tt> is a hash that
# will be used to specify defaults in priority to the standard
# defaults.
class Base
@@swf_default_options = {}
cattr_accessor :swf_default_options
end
module Helpers # :nodoc:
module AssetTagHelper
# Computes the path to an swf asset in the public 'swfs' directory.
# Full paths from the document root will be passed through.
# Used internally by +swf_tag+ to build the swf path.
#
# ==== Examples
# swf_path("example") # => /swfs/example.swf
# swf_path("example.swf") # => /swfs/example.swf
# swf_path("fonts/optima") # => /swfs/fonts/optima.swf
# swf_path("/fonts/optima") # => /fonts/optima.swf
# swf_path("http://www.example.com/game.swf") # => http://www.example.com/game.swf
#
# It takes into account the global setting +asset_host+, like any other asset:
#
# ActionController::Base.asset_host = "http://assets.example.com"
# image_path("logo.jpg") # => http://assets.example.com/images/logo.jpg
# swf_path("fonts/optima") # => http://assets.example.com/swfs/fonts/optima.swf
#
def swf_path(source)
if defined? SwfTag
SwfTag.new(self, @controller, source).public_path
else
compute_public_path(source, SwfAsset::DIRECTORY, SwfAsset::EXTENSION)
end
end
alias_method :path_to_swf, :swf_path # aliased to avoid conflicts with a swf_path named route
private
module SwfAsset # :nodoc:
DIRECTORY = 'swfs'.freeze
EXTENSION = 'swf'.freeze
def directory
DIRECTORY
end
def extension
EXTENSION
end
end
# AssetTag is available since 2.1.1 (http://github.com/rails/rails/commit/900fd6eca9dd97d2341e89bcb27d7a82d62965bf )
class SwfTag < AssetTag # :nodoc:
include SwfAsset
end if defined? AssetTag
end
end
end

View file

@ -0,0 +1,197 @@
module ActionView #:nodoc:
module Helpers # :nodoc:
module SwfFuHelper
# Returns a set of tags that display a Flash object within an
# HTML page.
#
# Options:
# * <tt>:id</tt> - the DOM +id+ of the flash +object+ element that is used to contain the Flash object; defaults to the name of the swf in +source+
# * <tt>:width, :height</tt> - the width & height of the Flash object. Defaults to "100%". These could also specified using :size
# * <tt>:size</tt> - the size of the Flash object, in the form "400x300".
# * <tt>:mode</tt> - Either :dynamic (default) or :static. Refer to SWFObject's doc[http://code.google.com/p/swfobject/wiki/documentation#Should_I_use_the_static_or_dynamic_publishing_method?]
# * <tt>:flashvars</tt> - a Hash of variables that are passed to the swf. Can also be a string like <tt>"foo=bar&hello=world"</tt>
# * <tt>:parameters</tt> - a Hash of configuration parameters for the swf. See Adobe's doc[http://kb.adobe.com/selfservice/viewContent.do?externalId=tn_12701#optional]
# * <tt>:alt</tt> - HTML text that is displayed when the Flash player is not available. Defaults to a "Get Flash" image pointing to Adobe Flash's installation page.
# * <tt>:flash_version</tt> - the version of the Flash player that is required (e.g. "7" (default) or "8.1.0")
# * <tt>:auto_install</tt> - a swf file that will upgrade flash player if needed (defaults to "expressInstall" which was installed by swf_fu)
# * <tt>:javascript_class</tt> - specify a javascript class (e.g. "MyFlash") for your flash object. The initialize method will be called when the flash object is ready.
# * <tt>:initialize</tt> - arguments to pass to the initialization method of your javascript class.
# * <tt>:div_id</tt> - the DOM +id+ of the containing div itself. Defaults to <tt>"#{option[:id]}"_div</tt>
#
def swf_tag(source, options={}, &block)
Generator.new(source, options, self).generate(&block)
end
# For compatibility with the older FlashObject.
# It modifies the given options before calling +swf_tag+.
# See FLASH_OBJECT.rdoc
def flashobject_tag_for_compatibility(source, options={})
options = options.reverse_merge(
:auto_install => nil,
:parameters => {:scale => "noscale"},
:variables => {:lzproxied => false},
:flash_id => "flashcontent_#{rand(1_100)}",
:background_color => "#ffffff"
)
{ :variables => :flashvars, :flash_id => :id }.each{|from, to| options[to] ||= options.delete(from) }
options[:parameters][:bgcolor] ||= options.delete(:background_color)
swf_tag(source, options)
end
alias_method :flashobject_tag, :flashobject_tag_for_compatibility unless defined? flashobject_tag
private
DEFAULTS = {
:width => "100%",
:height => "100%",
:flash_version => 7,
:mode => :dynamic,
:auto_install => "expressInstall",
:alt => <<-"EOS".squeeze(" ").strip.freeze
<a href="http://www.adobe.com/go/getflashplayer">
<img src="http://www.adobe.com/images/shared/download_buttons/get_flash_player.gif" alt="Get Adobe Flash player" />
</a>
EOS
}.freeze
class Generator # :nodoc:
VALID_MODES = [:static, :dynamic]
def initialize(source, options, view)
@view = view
@source = view.swf_path(source)
options = ActionView::Base.swf_default_options.merge(options)
options.each do |key, value|
options[key] = value.call(source) if value.respond_to?(:call)
end
[:html_options, :parameters, :flashvars].each do |k|
options[k] = convert_to_hash(options[k]).reverse_merge convert_to_hash(ActionView::Base.swf_default_options[k])
end
options.reverse_merge!(DEFAULTS)
options[:id] ||= source.gsub(/^.*\//, '').gsub(/\.swf$/,'')
options[:id] = force_to_valid_id(options[:id])
options[:div_id] ||= options[:id]+"_div"
options[:div_id] = force_to_valid_id(options[:div_id])
options[:width], options[:height] = options[:size].scan(/^(\d*%?)x(\d*%?)$/).first if options[:size]
options[:auto_install] &&= @view.swf_path(options[:auto_install])
options[:flashvars][:id] ||= options[:id]
@mode = options.delete(:mode)
@options = options
unless VALID_MODES.include? @mode
raise ArgumentError, "options[:mode] should be either #{VALID_MODES.join(' or ')}"
end
end
def force_to_valid_id(id)
id = id.gsub /[^A-Za-z0-9\-_]/, "_" # HTML id can only contain these characters
id = "swf_" + id unless id =~ /^[A-Z]/i # HTML id must start with alpha
id
end
def generate(&block)
if block_given?
@options[:alt] = @view.capture(&block)
if Rails::VERSION::STRING >= "3.0"
send(@mode)
elsif Rails::VERSION::STRING < "2.2"
@view.concat(send(@mode), block.binding)
else
@view.concat(send(@mode))
end
else
send(@mode)
end
end
private
CONCAT = ActiveSupport.const_defined?(:SafeBuffer) ? :safe_concat : :concat
def convert_to_hash(s)
case s
when Hash
s
when nil
{}
when String
s.split("&").inject({}) do |h, kvp|
key, value = kvp.split("=")
h[key.to_sym] = CGI::unescape(value)
h
end
else
raise ArgumentError, "#{s} should be a Hash, a String or nil"
end
end
def convert_to_string(h)
h.map do |key_value|
key_value.map{|val| CGI::escape(val.to_s)}.join("=")
end.join("&")
end
def static
param_list = @options[:parameters].map{|k,v| %(<param name="#{k}" value="#{v}"/>) }.join("\n")
param_list += %(\n<param name="flashvars" value="#{convert_to_string(@options[:flashvars])}"/>) unless @options[:flashvars].empty?
html_options = @options[:html_options].map{|k,v| %(#{k}="#{v}")}.join(" ")
r = @view.javascript_tag(
%(swfobject.registerObject("#{@options[:id]}_container", "#{@options[:flash_version]}", #{@options[:auto_install].to_json});)
)
r.send CONCAT, <<-"EOS".strip
<div id="#{@options[:div_id]}"><object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="#{@options[:width]}" height="#{@options[:height]}" id="#{@options[:id]}_container" #{html_options}>
<param name="movie" value="#{@source}" />
#{param_list}
<!--[if !IE]>-->
<object type="application/x-shockwave-flash" data="#{@source}" width="#{@options[:width]}" height="#{@options[:height]}" id="#{@options[:id]}">
#{param_list}
<!--<![endif]-->
#{@options[:alt]}
<!--[if !IE]>-->
</object>
<!--<![endif]-->
</object></div>
EOS
r << @view.javascript_tag(extend_js) if @options[:javascript_class]
r.send CONCAT, library_check
r
end
def dynamic
@options[:html_options] = @options[:html_options].merge(:id => @options[:id])
@options[:parameters] = @options[:parameters].dup # don't modify the original parameters
args = (([@source] + @options.values_at(:div_id,:width,:height,:flash_version)).map(&:to_s) +
@options.values_at(:auto_install,:flashvars,:parameters,:html_options)
).map(&:to_json).join(",")
preambule = @options[:switch_off_auto_hide_show] ? "swfobject.switchOffAutoHideShow();" : ""
r = @view.javascript_tag(preambule + "swfobject.embedSWF(#{args})")
r.send CONCAT, <<-"EOS".strip
<div id="#{@options[:div_id]}">
#{@options[:alt]}
</div>
EOS
r << @view.javascript_tag("swfobject.addDomLoadEvent(function(){#{extend_js}})") if @options[:javascript_class]
r.send CONCAT, library_check
r
end
def extend_js
arglist = case
when @options[:initialize].instance_of?(Array)
@options[:initialize].map(&:to_json).join(",")
when @options.has_key?(:initialize)
@options[:initialize].to_json
else
""
end
"Object.extend($('#{@options[:id]}'), #{@options[:javascript_class]}.prototype).initialize(#{arglist})"
end
def library_check
return "" unless 'development' == ENV['RAILS_ENV']
@view.javascript_tag(<<-"EOS")
if (typeof swfobject == 'undefined') {
document.getElementById('#{@options[:div_id]}').innerHTML = '<strong>Warning:</strong> SWFObject.js was not loaded properly. Make sure you <tt>&lt;%= javascript_include_tag :defaults %&gt;</tt> or <tt>&lt;%= javascript_include_tag :swfobject %&gt;</tt>';
}
EOS
end
end #class Generator
end
end
end

View file

@ -0,0 +1,42 @@
DYNAMIC_RESULT = <<'EOS'
<script type="text/javascript">
//<![CDATA[
swfobject.embedSWF("/swfs/mySwf.swf","mySwf_div","456","123","7","/swfs/expressInstall.swf",{"myVar":"value 1 \u003E 2","id":"mySwf"},{"play":true},{"id":"mySwf"})
//]]>
</script><div id="mySwf_div">
<a href="http://www.adobe.com/go/getflashplayer">
<img src="http://www.adobe.com/images/shared/download_buttons/get_flash_player.gif" alt="Get Adobe Flash player" />
</a>
</div><script type="text/javascript">
//<![CDATA[
swfobject.addDomLoadEvent(function(){Object.extend($('mySwf'), SomeClass.prototype).initialize({"be":"good"})})
//]]>
</script>
EOS
STATIC_RESULT = <<'EOS'
<script type="text/javascript">
//<![CDATA[
swfobject.registerObject("mySwf_container", "7", "/swfs/expressInstall.swf");
//]]>
</script><div id="mySwf_div"><object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" width="456" height="123" id="mySwf_container" class="lots">
<param name="movie" value="/swfs/mySwf.swf" />
<param name="play" value="true"/>
<param name="flashvars" value="myVar=value+1+%3E+2&id=mySwf"/>
<!--[if !IE]>-->
<object type="application/x-shockwave-flash" data="/swfs/mySwf.swf" width="456" height="123" id="mySwf">
<param name="play" value="true"/>
<param name="flashvars" value="myVar=value+1+%3E+2&id=mySwf"/>
<!--<![endif]-->
<a href="http://www.adobe.com/go/getflashplayer">
<img src="http://www.adobe.com/images/shared/download_buttons/get_flash_player.gif" alt="Get Adobe Flash player" />
</a>
<!--[if !IE]>-->
</object>
<!--<![endif]-->
</object></div><script type="text/javascript">
//<![CDATA[
Object.extend($('mySwf'), SomeClass.prototype).initialize({"be":"good"})
//]]>
</script>
EOS

View file

@ -0,0 +1,159 @@
require File.expand_path(File.dirname(__FILE__)+'/test_helper')
require File.expand_path(File.dirname(__FILE__)+'/results')
class SwfFuTest < ActionView::TestCase
def assert_same_stripped(expect, test)
expect, test = [expect, test].map{|s| s.split("\n").map(&:strip)}
same = expect & test
delta_expect, delta_test = [expect, test].map{|a| a-same}
STDOUT << "\n\n---- Actual result: ----\n" << test.join("\n") << "\n---------\n" unless delta_expect == delta_test
assert_equal delta_expect, delta_test
end
context "swf_path" do
context "with no special asset host" do
should "deduce the extension" do
assert_equal swf_path("example.swf"), swf_path("example")
assert_starts_with "/swfs/example.swf", swf_path("example.swf")
end
should "accept relative paths" do
assert_starts_with "/swfs/whatever/example.swf", swf_path("whatever/example.swf")
end
should "leave full paths alone" do
["/full/path.swf", "http://www.example.com/whatever.swf"].each do |p|
assert_starts_with p, swf_path(p)
end
end
end
context "with custom asset host" do
HOST = "http://assets.example.com"
setup do
ActionController::Base.asset_host = HOST
end
teardown do
ActionController::Base.asset_host = nil
end
should "take it into account" do
assert_equal "#{HOST}/swfs/whatever.swf", swf_path("whatever")
end
end
end
context "swf_tag" do
COMPLEX_OPTIONS = { :width => "456", :height => 123,
:flashvars => {:myVar => "value 1 > 2"}.freeze,
:javascript_class => "SomeClass",
:initialize => {:be => "good"}.freeze,
:parameters => {:play => true}.freeze
}.freeze
should "understand size" do
assert_equal swf_tag("hello", :size => "123x456"),
swf_tag("hello", :width => 123, :height => "456")
end
should "only accept valid modes" do
assert_raise(ArgumentError) { swf_tag("xyz", :mode => :xyz) }
end
context "with custom defaults" do
setup do
test = {:flashvars=> {:xyz => "abc", :hello => "world"}.freeze, :mode => :static, :size => "400x300"}.freeze
@expect = swf_tag("test", test)
@expect_with_hello = swf_tag("test", :flashvars => {:xyz => "abc", :hello => "my friend"}, :mode => :static, :size => "400x300")
ActionView::Base.swf_default_options = test
end
should "respect them" do
assert_equal @expect, swf_tag("test")
end
should "merge suboptions" do
assert_equal @expect_with_hello, swf_tag("test", :flashvars => {:hello => "my friend"}.freeze)
end
teardown { ActionView::Base.swf_default_options = {} }
end
context "with proc options" do
should "call them" do
expect = swf_tag("test", :id => "generated_id_for_test")
assert_equal expect, swf_tag("test", :id => Proc.new{|arg| "generated_id_for_#{arg}"})
end
should "call global default's everytime" do
expect1 = swf_tag("test", :id => "call_number_1")
expect2 = swf_tag("test", :id => "call_number_2")
cnt = 0
ActionView::Base.swf_default_options = { :id => Proc.new{ "call_number_#{cnt+=1}" }}
assert_equal expect1, swf_tag("test")
assert_equal expect2, swf_tag("test")
end
end
context "with static mode" do
setup { ActionView::Base.swf_default_options = {:mode => :static} }
should "deal with string flashvars" do
assert_equal swf_tag("hello", :flashvars => "xyz=abc", :mode => :static),
swf_tag("hello", :flashvars => {:xyz => "abc"}, :mode => :static)
end
should "produce the expected code" do
assert_same_stripped STATIC_RESULT, swf_tag("mySwf", COMPLEX_OPTIONS.merge(:html_options => {:class => "lots"}.freeze).freeze)
end
teardown { ActionView::Base.swf_default_options = {} }
end
context "with dynamic mode" do
should "produce the expected code" do
assert_same_stripped DYNAMIC_RESULT, swf_tag("mySwf", COMPLEX_OPTIONS)
end
end
should "enforce HTML id validity" do
div_result = '<div id="swf_123-456_ok___X_div">'
assert_match /#{div_result}/, swf_tag("123-456_ok$!+X")
obj_result = '"id":"swf_123-456_ok___X"'
assert_match /#{obj_result}/, swf_tag("123-456_ok$!+X")
end
should "treat initialize arrays as list of parameters" do
assert_match 'initialize("hello","world")', swf_tag("mySwf", :initialize => ["hello", "world"], :javascript_class => "SomeClass")
end
if ActiveSupport.const_defined?(:SafeBuffer)
should "be html safe" do
assert swf_tag("test").html_safe?
end
end
end
context "flashobject_tag" do
should "be the same as swf_tag with different defaults" do
assert_same_stripped swf_tag("mySwf",
:auto_install => nil,
:parameters => {:scale => "noscale", :bgcolor => "#ffffff"},
:flashvars => {:lzproxied => false},
:id => "myFlash"
), flashobject_tag("mySwf", :flash_id => "myFlash")
end
should "be the same with custom settings" do
assert_same_stripped swf_tag("mySwf",
:auto_install => nil,
:parameters => {:scale => "noborder", :bgcolor => "#ffffff"},
:flashvars => {:answer_is => 42},
:id => "myFlash"
), flashobject_tag("mySwf", :flash_id => "myFlash", :parameters => {:scale => "noborder"}, :variables => {:answer_is => 42})
end
end
end

View file

@ -0,0 +1,20 @@
require 'test/unit'
require 'rubygems'
gem 'activesupport', '~> 2.3'
require 'active_support'
gem 'activerecord', '~> 2.3'
require 'active_record'
gem 'actionpack', '~> 2.3'
require 'active_support'
require 'action_view'
require 'action_controller'
#require File.dirname(__FILE__)+'/../../../../config/environment.rb'
require 'action_view/test_case'
require "action_controller/test_process"
require 'shoulda'
require File.dirname(__FILE__) + '/../init'
def assert_starts_with(start, what)
assert what.starts_with?(start), "#{what} does not start with #{start}"
end

View file

@ -0,0 +1,6 @@
require "fileutils"
dest = File.dirname(__FILE__) + "/../../../public"
FileUtils.rm "#{dest}/javascripts/swfobject.js" rescue puts "Warning: swfobject.js could not be deleted"
FileUtils.rm "#{dest}/swfs/expressInstall.swf" rescue puts "Warning: expressInstall.swf could not be deleted"
Dir.rmdir "#{dest}/swfs/" rescue "don't worry if directory is not empty"

View file

@ -0,0 +1,20 @@
Copyright (c) 2009 [name of plugin creator]
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

@ -0,0 +1,63 @@
Translate
=========
This plugin provides a web interface for translating Rails I18n texts (requires Rails 2.2 or higher) from one locale to another. The plugin has been tested only with the simple I18n backend that ships with Rails. I18n texts are read from and written to YAML files under config/locales.
To translate to a new locale you need to add a YAML file for that locale that contains the locale as the top key and at least one translation.
Please note that there are certain I18n keys that map to Array objects rather than strings and those are currently not dealt with by the translation UI. This means that Rails built in keys such as date.day_names need to be translated manually directly in the YAML file.
To get the translation UI to write the YAML files in UTF8 you need to install the ya2yaml gem.
The translation UI finds all I18n keys by extracting them from I18n lookups in your application source code. In addition it adds all :en and default locale keys from the I18n backend.
- Updated: Each string in the UI now has an "Auto Translate" link which will send the original text to Google Translate and will input the returned translation into the form field for further clean up and review prior to saving.
Rake Tasks
=========
In addition to the web UI this plugin adds the following rake tasks:
translate:untranslated
translate:missing
translate:remove_obsolete_keys
translate:merge_keys
translate:google
translate:changed
The missing task shows you any I18n keys in your code that do not have translations in the YAML file for your default locale, i.e. config/locales/sv.yml.
The merge_keys task is supposed to be used in conjunction with Sven Fuch's Rails I18n TextMate bundle (http://github.com/svenfuchs/rails-i18n/tree/master). Texts and keys extracted with the TextMate bundle end up in the temporary file log/translations.yml. When you run the merge_keys rake task the keys are moved over to the corresponding I18n locale file, i.e. config/locales/sv.yml. The merge_keys task also checks for overwrites of existing keys by warning you that one of your extracted keys already exists with a different translation.
The google task is used for auto translating from one locale to another using Google Translate.
The changed rake task can show you between one YAML file to another which keys have had their texts changed.
Installation
=========
Obtain the source with:
./script/plugin install git://github.com/newsdesk/translate.git
To mount the plugin, add the following to your config/routes.rb file:
Translate::Routes.translation_ui(map) if RAILS_ENV != "production"
Now visit /translate in your web browser to start translating.
Dependencies
=========
- Rails 2.2 or higher
- The ya2yaml gem if you want your YAML files written in UTF8 encoding.
Authors
=========
- Peter Marklund (programming)
- Joakim Westerlund (web design)
Many thanks to http://newsdesk.se for sponsoring the development of this plugin.
Copyright (c) 2009 Peter Marklund, released under the MIT license

View file

@ -0,0 +1,11 @@
require 'rake'
require 'spec/rake/spectask'
desc 'Default: run specs.'
task :default => :spec
desc 'Run the specs'
Spec::Rake::SpecTask.new(:spec) do |t|
t.spec_opts = ['--colour --format progress --loadby mtime --reverse']
t.spec_files = FileList['spec/**/*_spec.rb']
end

View file

@ -0,0 +1,8 @@
require 'translate'
# TODO: Use new method available_locales once Rails is upgraded, see:
# http://github.com/svenfuchs/i18n/commit/411f8fe7c8f3f89e9b6b921fa62ed66cb92f3af4
def I18n.valid_locales
I18n.backend.send(:init_translations) unless I18n.backend.initialized?
backend.send(:translations).keys.reject { |locale| locale == :root }
end

View file

@ -0,0 +1,8 @@
module Translate
end
require File.join(File.dirname(__FILE__), "translate_controller")
require File.join(File.dirname(__FILE__), "translate_helper")
Dir[File.join(File.dirname(__FILE__), "translate", "*.rb")].each do |file|
require file
end

View file

@ -0,0 +1,35 @@
require 'fileutils'
class Translate::File
attr_accessor :path
def initialize(path)
self.path = path
end
def write(keys)
FileUtils.mkdir_p File.dirname(path)
File.open(path, "w") do |file|
file.puts keys_to_yaml(Translate::File.deep_stringify_keys(keys))
end
end
def read
File.exists?(path) ? YAML::load(IO.read(path)) : {}
end
# Stringifying keys for prettier YAML
def self.deep_stringify_keys(hash)
hash.inject({}) { |result, (key, value)|
value = deep_stringify_keys(value) if value.is_a? Hash
result[(key.to_s rescue key) || key] = value
result
}
end
private
def keys_to_yaml(keys)
# Using ya2yaml, if available, for UTF8 support
keys.respond_to?(:ya2yaml) ? keys.ya2yaml(:escape_as_utf8 => true) : keys.to_yaml
end
end

View file

@ -0,0 +1,152 @@
require 'pathname'
class Translate::Keys
# Allows keys extracted from lookups in files to be cached
def self.files
@@files ||= Translate::Keys.new.files
end
# Allows flushing of the files cache
def self.files=(files)
@@files = files
end
def files
@files ||= extract_files
end
alias_method :to_hash, :files
def keys
files.keys
end
alias_method :to_a, :keys
def i18n_keys(locale)
I18n.backend.send(:init_translations) unless I18n.backend.initialized?
Translate::Keys.to_shallow_hash(I18n.backend.send(:translations)[locale.to_sym]).keys.sort
end
def untranslated_keys
Translate::Keys.translated_locales.inject({}) do |missing, locale|
missing[locale] = i18n_keys(I18n.default_locale).map do |key|
I18n.backend.send(:lookup, locale, key).nil? ? key : nil
end.compact
missing
end
end
def missing_keys
locale = I18n.default_locale; yaml_keys = {}
yaml_keys = Translate::Storage.file_paths(locale).inject({}) do |keys, path|
keys = keys.deep_merge(Translate::File.new(path).read[locale.to_s])
end
files.reject { |key, file| self.class.contains_key?(yaml_keys, key) }
end
def self.translated_locales
I18n.available_locales.reject { |locale| [:root, I18n.default_locale.to_sym].include?(locale) }
end
# Checks if a nested hash contains the keys in dot separated I18n key.
#
# Example:
#
# hash = {
# :foo => {
# :bar => {
# :baz => 1
# }
# }
# }
#
# contains_key?("foo", key) # => true
# contains_key?("foo.bar", key) # => true
# contains_key?("foo.bar.baz", key) # => true
# contains_key?("foo.bar.baz.bla", key) # => false
#
def self.contains_key?(hash, key)
keys = key.to_s.split(".")
return false if keys.empty?
!keys.inject(HashWithIndifferentAccess.new(hash)) do |memo, key|
memo.is_a?(Hash) ? memo.try(:[], key) : nil
end.nil?
end
# Convert something like:
#
# {
# :pressrelease => {
# :label => {
# :one => "Pressmeddelande"
# }
# }
# }
#
# to:
#
# {'pressrelease.label.one' => "Pressmeddelande"}
#
def self.to_shallow_hash(hash)
hash.inject({}) do |shallow_hash, (key, value)|
if value.is_a?(Hash)
to_shallow_hash(value).each do |sub_key, sub_value|
shallow_hash[[key, sub_key].join(".")] = sub_value
end
else
shallow_hash[key.to_s] = value
end
shallow_hash
end
end
# Convert something like:
#
# {'pressrelease.label.one' => "Pressmeddelande"}
#
# to:
#
# {
# :pressrelease => {
# :label => {
# :one => "Pressmeddelande"
# }
# }
# }
def self.to_deep_hash(hash)
hash.inject({}) do |deep_hash, (key, value)|
keys = key.to_s.split('.').reverse
leaf_key = keys.shift
key_hash = keys.inject({leaf_key.to_sym => value}) { |hash, key| {key.to_sym => hash} }
deep_merge!(deep_hash, key_hash)
deep_hash
end
end
# deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
def self.deep_merge!(hash1, hash2)
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
hash1.merge!(hash2, &merger)
end
private
def extract_files
files_to_scan.inject(HashWithIndifferentAccess.new) do |files, file|
IO.read(file).scan(i18n_lookup_pattern).flatten.map(&:to_sym).each do |key|
files[key] ||= []
path = Pathname.new(File.expand_path(file)).relative_path_from(Pathname.new(Rails.root)).to_s
files[key] << path if !files[key].include?(path)
end
files
end
end
def i18n_lookup_pattern
/\b(?:I18n\.t|I18n\.translate|t)(?:\s|\():?'([a-z0-9_]+.[a-z0-9_.]+)'\)?/
end
def files_to_scan
Dir.glob(File.join(Translate::Storage.root_dir, "{app,config,lib}", "**","*.{rb,erb,rhtml}")) +
Dir.glob(File.join(Translate::Storage.root_dir, "public", "javascripts", "**","*.js"))
end
end

View file

@ -0,0 +1,35 @@
class Translate::Log
attr_accessor :from_locale, :to_locale, :keys
def initialize(from_locale, to_locale, keys)
self.from_locale = from_locale
self.to_locale = to_locale
self.keys = keys
end
def write_to_file
current_texts = File.exists?(file_path) ? file.read : {}
current_texts.merge!(from_texts)
file.write(current_texts)
end
def read
file.read
end
private
def file
@file ||= Translate::File.new(file_path)
end
def from_texts
Translate::File.deep_stringify_keys(Translate::Keys.to_deep_hash(keys.inject({}) do |hash, key|
hash[key] = I18n.backend.send(:lookup, from_locale, key)
hash
end))
end
def file_path
File.join(Rails.root, "config", "locales", "log", "from_#{from_locale}_to_#{to_locale}.yml")
end
end

View file

@ -0,0 +1,11 @@
module Translate
class Routes
def self.translation_ui(map)
map.with_options(:controller => 'translate') do |t|
t.translate_list 'translate'
t.translate 'translate/translate', :action => 'translate'
t.translate_reload 'translate/reload', :action => 'reload'
end
end
end
end

View file

@ -0,0 +1,28 @@
class Translate::Storage
attr_accessor :locale
def initialize(locale)
self.locale = locale.to_sym
end
def write_to_file
Translate::File.new(file_path).write(keys)
end
def self.file_paths(locale)
Dir.glob(File.join(root_dir, "config", "locales", "**","#{locale}.yml"))
end
def self.root_dir
Rails.root
end
private
def keys
{locale => I18n.backend.send(:translations)[locale]}
end
def file_path
File.join(Translate::Storage.root_dir, "config", "locales", "#{locale}.yml")
end
end

View file

@ -0,0 +1,165 @@
class TranslateController < ActionController::Base
# It seems users with active_record_store may get a "no :secret given" error if we don't disable csrf protection,
skip_before_filter :verify_authenticity_token
prepend_view_path(File.join(File.dirname(__FILE__), "..", "views"))
layout 'translate'
before_filter :init_translations
before_filter :set_locale
def index
initialize_keys
filter_by_key_pattern
filter_by_text_pattern
filter_by_translated_or_changed
sort_keys
paginate_keys
@total_entries = @keys.size
end
def translate
I18n.backend.store_translations(@to_locale, Translate::Keys.to_deep_hash(params[:key]))
Translate::Storage.new(@to_locale).write_to_file
Translate::Log.new(@from_locale, @to_locale, params[:key].keys).write_to_file
force_init_translations # Force reload from YAML file
flash[:notice] = "Translations stored"
redirect_to params.slice(:filter, :sort_by, :key_type, :key_pattern, :text_type, :text_pattern).merge({:action => :index})
end
def reload
Translate::Keys.files = nil
redirect_to :action => 'index'
end
private
def initialize_keys
@files = Translate::Keys.files
@keys = (@files.keys.map(&:to_s) + Translate::Keys.new.i18n_keys(@from_locale)).uniq
@keys.reject! do |key|
from_text = lookup(@from_locale, key)
# When translating from one language to another, make sure there is a text to translate from.
# Always exclude non string translation objects as we don't support editing them in the UI.
(@from_locale != @to_locale && !from_text.present?) || (from_text.present? && !from_text.is_a?(String))
end
end
def lookup(locale, key)
I18n.backend.send(:lookup, locale, key)
end
helper_method :lookup
def filter_by_translated_or_changed
params[:filter] ||= 'all'
return if params[:filter] == 'all'
@keys.reject! do |key|
case params[:filter]
when 'untranslated'
lookup(@to_locale, key).present?
when 'translated'
lookup(@to_locale, key).blank?
when 'changed'
old_from_text(key).blank? || lookup(@from_locale, key) == old_from_text(key)
else
raise "Unknown filter '#{params[:filter]}'"
end
end
end
def filter_by_key_pattern
return if params[:key_pattern].blank?
@keys.reject! do |key|
case params[:key_type]
when "starts_with"
!key.starts_with?(params[:key_pattern])
when "contains"
key.index(params[:key_pattern]).nil?
else
raise "Unknown key_type '#{params[:key_type]}'"
end
end
end
def filter_by_text_pattern
return if params[:text_pattern].blank?
@keys.reject! do |key|
case params[:text_type]
when 'contains'
!lookup(@from_locale, key).present? || !lookup(@from_locale, key).to_s.downcase.index(params[:text_pattern].downcase)
when 'equals'
!lookup(@from_locale, key).present? || lookup(@from_locale, key).to_s.downcase != params[:text_pattern].downcase
else
raise "Unknown text_type '#{params[:text_type]}'"
end
end
end
def sort_keys
params[:sort_by] ||= "key"
case params[:sort_by]
when "key"
@keys.sort!
when "text"
@keys.sort! do |key1, key2|
if lookup(@from_locale, key1).present? && lookup(@from_locale, key2).present?
lookup(@from_locale, key1).to_s.downcase <=> lookup(@from_locale, key2).to_s.downcase
elsif lookup(@from_locale, key1).present?
-1
else
1
end
end
else
raise "Unknown sort_by '#{params[:sort_by]}'"
end
end
def paginate_keys
params[:page] ||= 1
@paginated_keys = @keys[offset, per_page]
end
def offset
(params[:page].to_i - 1) * per_page
end
def per_page
50
end
helper_method :per_page
def init_translations
I18n.backend.send(:init_translations) unless I18n.backend.initialized?
end
def force_init_translations
I18n.backend.send(:init_translations)
end
def default_locale
I18n.default_locale
end
def set_locale
session[:from_locale] ||= default_locale
session[:to_locale] ||= :en
session[:from_locale] = params[:from_locale] if params[:from_locale].present?
session[:to_locale] = params[:to_locale] if params[:to_locale].present?
@from_locale = session[:from_locale].to_sym
@to_locale = session[:to_locale].to_sym
end
def old_from_text(key)
return @old_from_text[key] if @old_from_text && @old_from_text[key]
@old_from_text = {}
text = key.split(".").inject(log_hash) do |hash, k|
hash ? hash[k] : nil
end
@old_from_text[key] = text
end
helper_method :old_from_text
def log_hash
@log_hash ||= Translate::Log.new(@from_locale, @to_locale, {}).read
end
end

View file

@ -0,0 +1,45 @@
module TranslateHelper
def simple_filter(labels, param_name = 'filter', selected_value = nil)
selected_value ||= params[param_name]
filter = []
labels.each do |item|
if item.is_a?(Array)
type, label = item
else
type = label = item
end
if type.to_s == selected_value.to_s
filter << "<i>#{label}</i>"
else
link_params = params.merge({param_name.to_s => type})
link_params.merge!({"page" => nil}) if param_name.to_s != "page"
filter << link_to(label, link_params)
end
end
filter.join(" | ")
end
def n_lines(text, line_size)
n_lines = 1
if text.present?
n_lines = text.split("\n").size
if n_lines == 1 && text.length > line_size
n_lines = text.length / line_size + 1
end
end
n_lines
end
def translate_javascript_includes
sources = []
if File.exists?(File.join(Rails.root, "public", "javascripts", "prototype.js"))
sources << "/javascripts/prototype.js"
else
sources << "http://ajax.googleapis.com/ajax/libs/prototype/1.6.1.0/prototype.js"
end
sources << "http://www.google.com/jsapi"
sources.map do |src|
%Q{<script src="#{src}" type="text/javascript"></script>}
end.join("\n")
end
end

View file

@ -0,0 +1,129 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe TranslateController do
describe "index" do
before(:each) do
controller.stub!(:per_page).and_return(1)
I18n.backend.stub!(:translations).and_return(i18n_translations)
I18n.backend.instance_eval { @initialized = true }
keys = mock(:keys)
keys.stub!(:i18n_keys).and_return(['vendor.foobar'])
Translate::Keys.should_receive(:new).and_return(keys)
Translate::Keys.should_receive(:files).and_return(files)
I18n.stub!(:valid_locales).and_return([:en, :sv])
I18n.stub!(:default_locale).and_return(:sv)
end
it "shows sorted paginated keys from the translate from locale and extracted keys by default" do
get_page :index
assigns(:from_locale).should == :sv
assigns(:to_locale).should == :en
assigns(:files).should == files
assigns(:keys).sort.should == ['articles.new.page_title', 'home.page_title', 'vendor.foobar']
assigns(:paginated_keys).should == ['articles.new.page_title']
end
it "can be paginated with the page param" do
get_page :index, :page => 2
assigns(:files).should == files
assigns(:paginated_keys).should == ['home.page_title']
end
it "accepts a key_pattern param with key_type=starts_with" do
get_page :index, :key_pattern => 'articles', :key_type => 'starts_with'
assigns(:files).should == files
assigns(:paginated_keys).should == ['articles.new.page_title']
assigns(:total_entries).should == 1
end
it "accepts a key_pattern param with key_type=contains" do
get_page :index, :key_pattern => 'page_', :key_type => 'contains'
assigns(:files).should == files
assigns(:total_entries).should == 2
assigns(:paginated_keys).should == ['articles.new.page_title']
end
it "accepts a filter=untranslated param" do
get_page :index, :filter => 'untranslated'
assigns(:total_entries).should == 2
assigns(:paginated_keys).should == ['articles.new.page_title']
end
it "accepts a filter=translated param" do
get_page :index, :filter => 'translated'
assigns(:total_entries).should == 1
assigns(:paginated_keys).should == ['vendor.foobar']
end
it "accepts a filter=changed param" do
log = mock(:log)
old_translations = {:home => {:page_title => "Skapar ny artikel"}}
log.should_receive(:read).and_return(Translate::File.deep_stringify_keys(old_translations))
Translate::Log.should_receive(:new).with(:sv, :en, {}).and_return(log)
get_page :index, :filter => 'changed'
assigns(:total_entries).should == 1
assigns(:keys).should == ["home.page_title"]
end
def i18n_translations
HashWithIndifferentAccess.new({
:en => {
:vendor => {
:foobar => "Foo Baar"
}
},
:sv => {
:articles => {
:new => {
:page_title => "Skapa ny artikel"
}
},
:home => {
:page_title => "Välkommen till I18n"
},
:vendor => {
:foobar => "Fobar"
}
}
})
end
def files
HashWithIndifferentAccess.new({
:'home.page_title' => ["app/views/home/index.rhtml"],
:'general.back' => ["app/views/articles/new.rhtml", "app/views/categories/new.rhtml"],
:'articles.new.page_title' => ["app/views/articles/new.rhtml"]
})
end
end
describe "translate" do
it "should store translations to I18n backend and then write them to a YAML file" do
session[:from_locale] = :sv
session[:to_locale] = :en
translations = {
:articles => {
:new => {
:title => "New Article"
}
},
:category => "Category"
}
key_param = {'articles.new.title' => "New Article", "category" => "Category"}
I18n.backend.should_receive(:store_translations).with(:en, translations)
storage = mock(:storage)
storage.should_receive(:write_to_file)
Translate::Storage.should_receive(:new).with(:en).and_return(storage)
log = mock(:log)
log.should_receive(:write_to_file)
Translate::Log.should_receive(:new).with(:sv, :en, key_param.keys).and_return(log)
post :translate, "key" => key_param
response.should be_redirect
end
end
def get_page(*args)
get(*args)
response.should be_success
end
end

View file

@ -0,0 +1,54 @@
require 'fileutils'
require File.dirname(__FILE__) + '/spec_helper'
describe Translate::File do
describe "write" do
before(:each) do
@file = Translate::File.new(file_path)
end
after(:each) do
FileUtils.rm(file_path)
end
it "writes all I18n messages for a locale to YAML file" do
@file.write(translations)
@file.read.should == Translate::File.deep_stringify_keys(translations)
end
def translations
{
:en => {
:article => {
:title => "One Article"
},
:category => "Category"
}
}
end
end
describe "deep_stringify_keys" do
it "should convert all keys in a hash to strings" do
Translate::File.deep_stringify_keys({
:en => {
:article => {
:title => "One Article"
},
:category => "Category"
}
}).should == {
"en" => {
"article" => {
"title" => "One Article"
},
"category" => "Category"
}
}
end
end
def file_path
File.join(File.dirname(__FILE__), "files", "en.yml")
end
end

View file

@ -0,0 +1,12 @@
class Article < ActiveRecord::Base
def validate
# t('li')
errors.add_to_base([t(:'article.key1') + "#{t('article.key2')}"])
I18n.t 'article.key3'
I18n.t 'article.key3'
I18n.t :'article.key4'
I18n.translate :'article.key5'
'bla bla t' + "blubba bla" + ' foobar'
'bla bla t ' + "blubba bla" + ' foobar'
end
end

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