Merge branch 'master' into sprockets

This commit is contained in:
Joshua Peek 2011-04-12 21:56:00 -05:00
commit ed24595647
184 changed files with 2616 additions and 1032 deletions

@ -48,7 +48,7 @@ more separate. Each of these packages can be used independently outside of
"Welcome aboard: You're riding Ruby on Rails!"
5. Follow the guidelines to start developing your application. You can find the following resources handy:
5. Follow the guidelines to start developing your application. You may find the following resources handy:
* The README file created within your application.
* The {Getting Started with Rails}[http://guides.rubyonrails.org/getting_started.html].

@ -32,7 +32,7 @@ This can be as simple as:
end
The body of the email is created by using an Action View template (regular
ERb) that has the instance variables that are declared in the mailer action.
ERB) that has the instance variables that are declared in the mailer action.
So the corresponding body template for the method above could look like this:
@ -72,6 +72,19 @@ Or you can just chain the methods together like:
Notifier.welcome.deliver # Creates the email and sends it immediately
== Setting defaults
It is possible to set default values that will be used in every method in your Action Mailer class. To implement this functionality, you just call the public class method <tt>default</tt> which you get for free from ActionMailer::Base. This method accepts a Hash as the parameter. You can use any of the headers e-mail messages has, like <tt>:from</tt> as the key. You can also pass in a string as the key, like "Content-Type", but Action Mailer does this out of the box for you, so you wont need to worry about that. Finally it is also possible to pass in a Proc that will get evaluated when it is needed.
Note that every value you set with this method will get over written if you use the same key in your mailer method.
Example:
class Authenticationmailer < ActionMailer::Base
default :from => "awesome@application.com", :subject => Proc.new { "E-mail was generated at #{Time.now}" }
.....
end
== Receiving emails
To receive emails, you need to implement a public instance method called <tt>receive</tt> that takes an

@ -17,8 +17,6 @@
s.require_path = 'lib'
s.requirements << 'none'
s.has_rdoc = true
s.add_dependency('actionpack', version)
s.add_dependency('mail', '~> 2.2.15')
end

@ -4,6 +4,7 @@
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/proc'
require 'active_support/core_ext/string/inflections'
require 'action_mailer/log_subscriber'
module ActionMailer #:nodoc:
@ -349,9 +350,6 @@ class Base < AbstractController::Base
helper ActionMailer::MailHelper
include ActionMailer::OldApi
delegate :register_observer, :to => Mail
delegate :register_interceptor, :to => Mail
private_class_method :new #:nodoc:
class_attribute :default_params
@ -363,6 +361,32 @@ class Base < AbstractController::Base
}.freeze
class << self
# Register one or more Observers which will be notified when mail is delivered.
def register_observers(*observers)
observers.flatten.compact.each { |observer| register_observer(observer) }
end
# Register one or more Interceptors which will be called before mail is sent.
def register_interceptors(*interceptors)
interceptors.flatten.compact.each { |interceptor| register_interceptor(interceptor) }
end
# Register an Observer which will be notified when mail is delivered.
# Either a class or a string can be passed in as the Observer. If a string is passed in
# it will be <tt>constantize</tt>d.
def register_observer(observer)
delivery_observer = (observer.is_a?(String) ? observer.constantize : observer)
Mail.register_observer(delivery_observer)
end
# Register an Inteceptor which will be called before mail is sent.
# Either a class or a string can be passed in as the Observer. If a string is passed in
# it will be <tt>constantize</tt>d.
def register_interceptor(interceptor)
delivery_interceptor = (interceptor.is_a?(String) ? interceptor.constantize : interceptor)
Mail.register_interceptor(delivery_interceptor)
end
def mailer_name
@mailer_name ||= name.underscore
end

@ -19,13 +19,17 @@ class Railtie < Rails::Railtie
options.stylesheets_dir ||= paths["public/stylesheets"].first
# make sure readers methods get compiled
options.asset_path ||= app.config.asset_path
options.asset_host ||= app.config.asset_host
options.asset_path ||= app.config.asset_path
options.asset_host ||= app.config.asset_host
ActiveSupport.on_load(:action_mailer) do
include AbstractController::UrlFor
extend ::AbstractController::Railties::RoutesHelpers.with(app.routes)
include app.routes.mounted_helpers
register_interceptors(options.delete(:interceptors))
register_observers(options.delete(:observers))
options.each { |k,v| send("#{k}=", v) }
end
end

@ -478,6 +478,11 @@ def self.delivered_email(mail)
end
end
class MySecondObserver
def self.delivered_email(mail)
end
end
test "you can register an observer to the mail object that gets informed on email delivery" do
ActionMailer::Base.register_observer(MyObserver)
mail = BaseMailer.welcome
@ -485,11 +490,31 @@ def self.delivered_email(mail)
mail.deliver
end
test "you can register an observer using its stringified name to the mail object that gets informed on email delivery" do
ActionMailer::Base.register_observer("BaseTest::MyObserver")
mail = BaseMailer.welcome
MyObserver.expects(:delivered_email).with(mail)
mail.deliver
end
test "you can register multiple observers to the mail object that both get informed on email delivery" do
ActionMailer::Base.register_observers("BaseTest::MyObserver", MySecondObserver)
mail = BaseMailer.welcome
MyObserver.expects(:delivered_email).with(mail)
MySecondObserver.expects(:delivered_email).with(mail)
mail.deliver
end
class MyInterceptor
def self.delivering_email(mail)
end
end
class MySecondInterceptor
def self.delivering_email(mail)
end
end
test "you can register an interceptor to the mail object that gets passed the mail object before delivery" do
ActionMailer::Base.register_interceptor(MyInterceptor)
mail = BaseMailer.welcome
@ -497,6 +522,21 @@ def self.delivering_email(mail)
mail.deliver
end
test "you can register an interceptor using its stringified name to the mail object that gets passed the mail object before delivery" do
ActionMailer::Base.register_interceptor("BaseTest::MyInterceptor")
mail = BaseMailer.welcome
MyInterceptor.expects(:delivering_email).with(mail)
mail.deliver
end
test "you can register multiple interceptors to the mail object that both get passed the mail object before delivery" do
ActionMailer::Base.register_interceptors("BaseTest::MyInterceptor", MySecondInterceptor)
mail = BaseMailer.welcome
MyInterceptor.expects(:delivering_email).with(mail)
MySecondInterceptor.expects(:delivering_email).with(mail)
mail.deliver
end
test "being able to put proc's into the defaults hash and they get evaluated on mail sending" do
mail1 = ProcMailer.welcome
yesterday = 1.day.ago

@ -1,5 +1,7 @@
*Rails 3.1.0 (unreleased)*
* Implicit actions named not_implemented can be rendered [Santiago Pastorino]
* Wildcard route will always matching the optional format segment by default. For example if you have this route:
map '*pages' => 'pages#show'

@ -19,7 +19,7 @@ It consists of several modules:
* Action View, which handles view template lookup and rendering, and provides
view helpers that assist when building HTML forms, Atom feeds and more.
Template formats that Action View handles are ERb (embedded Ruby, typically
Template formats that Action View handles are ERB (embedded Ruby, typically
used to inline short Ruby snippets inside HTML), XML Builder and RJS
(dynamically generated JavaScript from Ruby code).
@ -57,7 +57,7 @@ A short rundown of some of the major features:
{Learn more}[link:classes/ActionController/Base.html]
* ERb templates (static content mixed with dynamic output from ruby)
* ERB templates (static content mixed with dynamic output from ruby)
<% for post in @posts %>
Title: <%= post.title %>

@ -17,8 +17,6 @@
s.require_path = 'lib'
s.requirements << 'none'
s.has_rdoc = true
s.add_dependency('activesupport', version)
s.add_dependency('activemodel', version)
s.add_dependency('rack-cache', '~> 1.0.0')
@ -28,5 +26,5 @@
s.add_dependency('rack-test', '~> 0.5.7')
s.add_dependency('rack-mount', '~> 0.7.1')
s.add_dependency('tzinfo', '~> 0.3.23')
s.add_dependency('erubis', '~> 2.6.6')
s.add_dependency('erubis', '~> 2.7.0')
end

@ -1,3 +1,4 @@
require 'erubis'
require 'active_support/configurable'
require 'active_support/descendants_tracker'
require 'active_support/core_ext/module/anonymous'
@ -18,6 +19,7 @@ class Base
include ActiveSupport::Configurable
extend ActiveSupport::DescendantsTracker
undef_method :not_implemented
class << self
attr_reader :abstract
alias_method :abstract?, :abstract

@ -29,7 +29,7 @@ module ClassMethods
#
# ==== Options
# * <tt>only</tt> - The callback should be run only for this action
# * <tt>except<tt> - The callback should be run for all actions except this action
# * <tt>except</tt> - The callback should be run for all actions except this action
def _normalize_callback_options(options)
if only = options[:only]
only = Array(only).map {|o| "action_name == '#{o}'"}.join(" || ")

@ -105,7 +105,7 @@ module ActionController
# == Renders
#
# Action Controller sends content to the user by using one of five rendering methods. The most versatile and common is the rendering
# of a template. Included in the Action Pack is the Action View, which enables rendering of ERb templates. It's automatically configured.
# of a template. Included in the Action Pack is the Action View, which enables rendering of ERB templates. It's automatically configured.
# The controller passes objects to the view by assigning instance variables:
#
# def show
@ -128,7 +128,7 @@ module ActionController
# end
# end
#
# Read more about writing ERb and Builder templates in ActionView::Base.
# Read more about writing ERB and Builder templates in ActionView::Base.
#
# == Redirects
#

@ -201,19 +201,23 @@ def to_a #:nodoc:
class_attribute :middleware_stack
self.middleware_stack = ActionController::MiddlewareStack.new
def self.inherited(base)
def self.inherited(base) #nodoc:
base.middleware_stack = self.middleware_stack.dup
super
end
# Adds given middleware class and its args to bottom of middleware_stack
def self.use(*args, &block)
middleware_stack.use(*args, &block)
end
# Alias for middleware_stack
def self.middleware
middleware_stack
end
# Makes the controller a rack endpoint that points to the action in
# the given env's action_dispatch.request.path_parameters key.
def self.call(env)
action(env['action_dispatch.request.path_parameters'][:action]).call(env)
end

@ -67,7 +67,7 @@ module HttpAuthentication
# class PostsController < ApplicationController
# REALM = "SuperSecret"
# USERS = {"dhh" => "secret", #plain text password
# "dap" => Digest:MD5::hexdigest(["dap",REALM,"secret"].join(":")) #ha1 digest password
# "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":")) #ha1 digest password
#
# before_filter :authenticate, :except => [:index]
#

@ -10,8 +10,8 @@ def send_action(method, *args)
end
end
def default_render
render
def default_render(*args)
render(*args)
end
def action_method?(action_name)

@ -1,8 +1,9 @@
require 'abstract_controller/collector'
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/object/inclusion'
module ActionController #:nodoc:
module MimeResponds #:nodoc:
module MimeResponds
extend ActiveSupport::Concern
included do
@ -189,7 +190,7 @@ def respond_to(*mimes, &block)
raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
if response = retrieve_response_from_mimes(mimes, &block)
response.call
response.call(nil)
end
end
@ -222,6 +223,9 @@ def respond_to(*mimes, &block)
# is quite simple (it just needs to respond to call), you can even give
# a proc to it.
#
# In order to use respond_with, first you need to declare the formats your
# controller responds to in the class level with a call to <tt>respond_to</tt>.
#
def respond_with(*resources, &block)
raise "In order to use respond_with, first you need to declare the formats your " <<
"controller responds to in the class level" if self.class.mimes_for_respond_to.empty?
@ -245,9 +249,9 @@ def collect_mimes_from_class_level #:nodoc:
config = self.class.mimes_for_respond_to[mime]
if config[:except]
!config[:except].include?(action)
!action.in?(config[:except])
elsif config[:only]
config[:only].include?(action)
action.in?(config[:only])
else
true
end
@ -257,9 +261,9 @@ def collect_mimes_from_class_level #:nodoc:
# Collects mimes and return the response for the negotiated format. Returns
# nil if :not_acceptable was sent to the client.
#
def retrieve_response_from_mimes(mimes=nil, &block)
def retrieve_response_from_mimes(mimes=nil, &block) #:nodoc:
mimes ||= collect_mimes_from_class_level
collector = Collector.new(mimes) { default_render }
collector = Collector.new(mimes) { |options| default_render(options || {}) }
block.call(collector) if block_given?
if format = request.negotiate_mime(collector.order)

@ -77,6 +77,37 @@ module ActionController #:nodoc:
#
# respond_with(@project, :manager, @task)
#
# === Custom options
#
# <code>respond_with</code> also allow you to pass options that are forwarded
# to the underlying render call. Those options are only applied success
# scenarios. For instance, you can do the following in the create method above:
#
# def create
# @project = Project.find(params[:project_id])
# @task = @project.comments.build(params[:task])
# flash[:notice] = 'Task was successfully created.' if @task.save
# respond_with(@project, @task, :status => 201)
# end
#
# This will return status 201 if the task was saved with success. If not,
# it will simply ignore the given options and return status 422 and the
# resource errors. To customize the failure scenario, you can pass a
# a block to <code>respond_with</code>:
#
# def create
# @project = Project.find(params[:project_id])
# @task = @project.comments.build(params[:task])
# respond_with(@project, @task, :status => 201) do |format|
# if @task.save
# flash[:notice] = 'Task was successfully created.'
# else
# format.html { render "some_special_template" }
# end
# end
# end
#
# Using <code>respond_with</code> with a block follows the same syntax as <code>respond_to</code>.
class Responder
attr_reader :controller, :request, :format, :resource, :resources, :options
@ -131,7 +162,11 @@ def to_html
# responds to :to_format and display it.
#
def to_format
default_render
if get? || !has_errors?
default_render
else
display_errors
end
rescue ActionView::MissingTemplate => e
api_behavior(e)
end
@ -155,8 +190,6 @@ def api_behavior(error)
if get?
display resource
elsif has_errors?
display resource.errors, :status => :unprocessable_entity
elsif post?
display resource, :status => :created, :location => api_location
elsif has_empty_resource_definition?
@ -185,7 +218,7 @@ def resource_location
# controller.
#
def default_render
@default_response.call
@default_response.call(options)
end
# Display is just a shortcut to render a resource with the current format.
@ -209,6 +242,10 @@ def display(resource, given_options={})
controller.render given_options.merge!(options).merge!(format => resource)
end
def display_errors
controller.render format => resource.errors, :status => :unprocessable_entity
end
# Check whether the resource has errors.
#
def has_errors?

@ -60,6 +60,7 @@ module ActionDispatch
autoload :Static
end
autoload :ClosedError, 'action_dispatch/middleware/closed_error'
autoload :MiddlewareStack, 'action_dispatch/middleware/stack'
autoload :Routing

@ -0,0 +1,7 @@
module ActionDispatch
class ClosedError < StandardError #:nodoc:
def initialize(kind)
super "Cannot modify #{kind} because it was closed. This means it was already streamed back to the client or converted to HTTP headers."
end
end
end

@ -83,7 +83,7 @@ class Cookies
# Raised when storing more than 4K of session data.
class CookieOverflow < StandardError; end
class CookieJar < Hash #:nodoc:
class CookieJar #:nodoc:
# This regular expression is used to split the levels of a domain.
# The top level domain can be any string without a period or
@ -115,13 +115,22 @@ def initialize(secret = nil, host = nil, secure = false)
@delete_cookies = {}
@host = host
@secure = secure
super()
@closed = false
@cookies = {}
end
attr_reader :closed
alias :closed? :closed
def close!; @closed = true end
# Returns the value of the cookie by +name+, or +nil+ if no such cookie exists.
def [](name)
super(name.to_s)
@cookies[name.to_s]
end
def update(other_hash)
@cookies.update other_hash
self
end
def handle_options(options) #:nodoc:
@ -145,6 +154,7 @@ def handle_options(options) #:nodoc:
# Sets the cookie named +name+. The second argument may be the very cookie
# value, or a hash of options as documented above.
def []=(key, options)
raise ClosedError, :cookies if closed?
if options.is_a?(Hash)
options.symbolize_keys!
value = options[:value]
@ -153,7 +163,7 @@ def []=(key, options)
options = { :value => value }
end
value = super(key.to_s, value)
value = @cookies[key.to_s] = value
handle_options(options)
@ -170,7 +180,7 @@ def delete(key, options = {})
handle_options(options)
value = super(key.to_s)
value = @cookies.delete(key.to_s)
@delete_cookies[key] = options
value
end
@ -225,6 +235,7 @@ def initialize(parent_jar, secret)
end
def []=(key, options)
raise ClosedError, :cookies if closed?
if options.is_a?(Hash)
options.symbolize_keys!
else
@ -263,6 +274,7 @@ def [](name)
end
def []=(key, options)
raise ClosedError, :cookies if closed?
if options.is_a?(Hash)
options.symbolize_keys!
options[:value] = @verifier.generate(options[:value])
@ -305,6 +317,7 @@ def initialize(app)
end
def call(env)
cookie_jar = nil
status, headers, body = @app.call(env)
if cookie_jar = env['action_dispatch.cookies']
@ -315,6 +328,9 @@ def call(env)
end
[status, headers, body]
ensure
cookie_jar = ActionDispatch::Request.new(env).cookie_jar unless cookie_jar
cookie_jar.close!
end
end
end

@ -43,9 +43,15 @@ class Flash
class FlashNow #:nodoc:
def initialize(flash)
@flash = flash
@closed = false
end
attr_reader :closed
alias :closed? :closed
def close!; @closed = true end
def []=(k, v)
raise ClosedError, :flash if closed?
@flash[k] = v
@flash.discard(k)
v
@ -66,27 +72,70 @@ def notice=(message)
end
end
class FlashHash < Hash
class FlashHash
include Enumerable
def initialize #:nodoc:
super
@used = Set.new
@used = Set.new
@closed = false
@flashes = {}
end
attr_reader :closed
alias :closed? :closed
def close!; @closed = true end
def []=(k, v) #:nodoc:
raise ClosedError, :flash if closed?
keep(k)
super
@flashes[k] = v
end
def [](k)
@flashes[k]
end
def update(h) #:nodoc:
h.keys.each { |k| keep(k) }
super
@flashes.update h
self
end
def keys
@flashes.keys
end
def key?(name)
@flashes.key? name
end
def delete(key)
@flashes.delete key
self
end
def to_hash
@flashes.dup
end
def empty?
@flashes.empty?
end
def clear
@flashes.clear
end
def each(&block)
@flashes.each(&block)
end
alias :merge! :update
def replace(h) #:nodoc:
@used = Set.new
super
@flashes.replace h
self
end
# Sets a flash that will not be available to the next action, only to the current.
@ -100,7 +149,7 @@ def replace(h) #:nodoc:
#
# Entries set via <tt>now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>.
def now
FlashNow.new(self)
@now ||= FlashNow.new(self)
end
# Keeps either the entire current flash or a specific flash entry available for the next action:
@ -184,8 +233,11 @@ def call(env)
session = env['rack.session'] || {}
flash_hash = env['action_dispatch.request.flash_hash']
if flash_hash && (!flash_hash.empty? || session.key?('flash'))
if flash_hash
if !flash_hash.empty? || session.key?('flash')
session["flash"] = flash_hash
end
flash_hash.close!
end
if session.key?('flash') && session['flash'].empty?

@ -1,6 +1,7 @@
require 'erb'
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/object/inclusion'
require 'active_support/inflector'
require 'action_dispatch/routing/redirection'
@ -1345,11 +1346,11 @@ def scope_action_options #:nodoc:
end
def resource_scope? #:nodoc:
[:resource, :resources].include?(@scope[:scope_level])
@scope[:scope_level].among?(:resource, :resources)
end
def resource_method_scope? #:nodoc:
[:collection, :member, :new].include?(@scope[:scope_level])
@scope[:scope_level].among?(:collection, :member, :new)
end
def with_exclusive_scope

@ -1,3 +1,5 @@
require 'active_support/core_ext/object/inclusion'
module ActionDispatch
module Assertions
# A small suite of assertions that test responses from \Rails applications.
@ -33,7 +35,7 @@ module ResponseAssertions
def assert_response(type, message = nil)
validate_request!
if [ :success, :missing, :redirect, :error ].include?(type) && @response.send("#{type}?")
if type.among?(:success, :missing, :redirect, :error) && @response.send("#{type}?")
assert_block("") { true } # to count the assertion
elsif type.is_a?(Fixnum) && @response.response_code == type
assert_block("") { true } # to count the assertion

@ -1,4 +1,5 @@
require 'action_controller/vendor/html-scanner'
require 'active_support/core_ext/object/inclusion'
#--
# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
@ -441,7 +442,7 @@ def assert_select_rjs(*args, &block)
if matches
assert_block("") { true } # to count the assertion
if block_given? && !([:remove, :show, :hide, :toggle].include? rjs_type)
if block_given? && !rjs_type.among?(:remove, :show, :hide, :toggle)
begin
@selected ||= nil
in_scope, @selected = @selected, matches

@ -1,6 +1,7 @@
require 'stringio'
require 'uri'
require 'active_support/core_ext/kernel/singleton_class'
require 'active_support/core_ext/object/inclusion'
require 'active_support/core_ext/object/try'
require 'rack/test'
require 'test/unit/assertions'
@ -26,31 +27,31 @@ module RequestHelpers
# object's <tt>@response</tt> instance variable will point to the same
# response object.
#
# You can also perform POST, PUT, DELETE, and HEAD requests with +post+,
# +put+, +delete+, and +head+.
# You can also perform POST, PUT, DELETE, and HEAD requests with +#post+,
# +#put+, +#delete+, and +#head+.
def get(path, parameters = nil, headers = nil)
process :get, path, parameters, headers
end
# Performs a POST request with the given parameters. See get() for more
# Performs a POST request with the given parameters. See +#get+ for more
# details.
def post(path, parameters = nil, headers = nil)
process :post, path, parameters, headers
end
# Performs a PUT request with the given parameters. See get() for more
# Performs a PUT request with the given parameters. See +#get+ for more
# details.
def put(path, parameters = nil, headers = nil)
process :put, path, parameters, headers
end
# Performs a DELETE request with the given parameters. See get() for
# Performs a DELETE request with the given parameters. See +#get+ for
# more details.
def delete(path, parameters = nil, headers = nil)
process :delete, path, parameters, headers
end
# Performs a HEAD request with the given parameters. See get() for more
# Performs a HEAD request with the given parameters. See +#get+ for more
# details.
def head(path, parameters = nil, headers = nil)
process :head, path, parameters, headers
@ -59,7 +60,7 @@ def head(path, parameters = nil, headers = nil)
# Performs an XMLHttpRequest request with the given parameters, mirroring
# a request from the Prototype library.
#
# The request_method is :get, :post, :put, :delete or :head; the
# The request_method is +:get+, +:post+, +:put+, +:delete+ or +:head+; the
# parameters are +nil+, a hash, or a url-encoded or multipart string;
# the headers are a hash. Keys are automatically upcased and prefixed
# with 'HTTP_' if not already.
@ -243,7 +244,8 @@ def _mock_session
end
# Performs the actual request.
def process(method, path, parameters = nil, rack_environment = nil)
def process(method, path, parameters = nil, env = nil)
env ||= {}
if path =~ %r{://}
location = URI.parse(path)
https! URI::HTTPS === location if location.scheme
@ -259,7 +261,7 @@ def process(method, path, parameters = nil, rack_environment = nil)
hostname, port = host.split(':')
env = {
default_env = {
:method => method,
:params => parameters,
@ -277,9 +279,7 @@ def process(method, path, parameters = nil, rack_environment = nil)
session = Rack::Test::Session.new(_mock_session)
(rack_environment || {}).each do |key, value|
env[key] = value
end
env.reverse_merge!(default_env)
# NOTE: rack-test v0.5 doesn't build a default uri correctly
# Make sure requested path is always a full uri
@ -321,7 +321,7 @@ def reset!
define_method(method) do |*args|
reset! unless integration_session
# reset the html_document variable, but only for new get/post calls
@html_document = nil unless %w(cookies assigns).include?(method)
@html_document = nil unless method.among?("cookies", "assigns")
integration_session.__send__(method, *args).tap do
copy_session_variables!
end
@ -384,7 +384,7 @@ def integration_session
end
end
# An test that spans multiple controllers and actions,
# An integration test spans multiple controllers and actions,
# tying them all together to ensure they work together as expected. It tests
# more completely than either unit or functional tests do, exercising the
# entire stack, from the dispatcher to the database.

@ -8,13 +8,13 @@
module ActionView #:nodoc:
# = Action View Base
#
# Action View templates can be written in three ways. If the template file has a <tt>.erb</tt> (or <tt>.rhtml</tt>) extension then it uses a mixture of ERb
# Action View templates can be written in three ways. If the template file has a <tt>.erb</tt> (or <tt>.rhtml</tt>) extension then it uses a mixture of ERB
# (included in Ruby) and HTML. If the template file has a <tt>.builder</tt> (or <tt>.rxml</tt>) extension then Jim Weirich's Builder::XmlMarkup library is used.
# If the template file has a <tt>.rjs</tt> extension then it will use ActionView::Helpers::PrototypeHelper::JavaScriptGenerator.
#
# == ERb
# == ERB
#
# You trigger ERb by using embeddings such as <% %>, <% -%>, and <%= %>. The <%= %> tag set is used when you want output. Consider the
# You trigger ERB by using embeddings such as <% %>, <% -%>, and <%= %>. The <%= %> tag set is used when you want output. Consider the
# following loop for names:
#
# <b>Names of all the people</b>
@ -23,7 +23,7 @@ module ActionView #:nodoc:
# <% end %>
#
# The loop is setup in regular embedding tags <% %> and the name is written using the output embedding tag <%= %>. Note that this
# is not just a usage suggestion. Regular output functions like print or puts won't work with ERb templates. So this would be wrong:
# is not just a usage suggestion. Regular output functions like print or puts won't work with ERB templates. So this would be wrong:
#
# <%# WRONG %>
# Hi, Mr. <% puts "Frodo" %>
@ -81,7 +81,7 @@ module ActionView #:nodoc:
#
# == Builder
#
# Builder templates are a more programmatic alternative to ERb. They are especially useful for generating XML content. An XmlMarkup object
# Builder templates are a more programmatic alternative to ERB. They are especially useful for generating XML content. An XmlMarkup object
# named +xml+ is automatically made available to templates with a <tt>.builder</tt> extension.
#
# Here are some basic examples:

@ -4,7 +4,7 @@ module ActionView
# = Action View Atom Feed Helpers
module Helpers #:nodoc:
module AtomFeedHelper
# Adds easy defaults to writing Atom feeds with the Builder template engine (this does not work on ERb or any other
# Adds easy defaults to writing Atom feeds with the Builder template engine (this does not work on ERB or any other
# template languages).
#
# Full usage example:

@ -14,7 +14,7 @@ module CaptureHelper
# variable. You can then use this variable anywhere in your templates or layout.
#
# ==== Examples
# The capture method can be used in ERb templates...
# The capture method can be used in ERB templates...
#
# <% @greeting = capture do %>
# Welcome to my shiny new web page! The date and time is

@ -17,10 +17,12 @@ module CsrfHelper
# Note that regular forms generate hidden fields, and that Ajax calls are whitelisted,
# so they do not use these tags.
def csrf_meta_tags
<<-METAS.strip_heredoc.chomp.html_safe if protect_against_forgery?
<meta name="csrf-param" content="#{Rack::Utils.escape_html(request_forgery_protection_token)}"/>
<meta name="csrf-token" content="#{Rack::Utils.escape_html(form_authenticity_token)}"/>
METAS
if protect_against_forgery?
[
tag('meta', :name => 'csrf-param', :content => request_forgery_protection_token),
tag('meta', :name => 'csrf-token', :content => form_authenticity_token)
].join("\n").html_safe
end
end
# For backwards compatibility.

@ -584,7 +584,7 @@ def update_page(&block)
# Works like update_page but wraps the generated JavaScript in a
# <tt>\<script></tt> tag. Use this to include generated JavaScript in an
# ERb template. See JavaScriptGenerator for more information.
# ERB template. See JavaScriptGenerator for more information.
#
# +html_options+ may be a hash of <tt>\<script></tt> attributes to be
# passed to ActionView::Helpers::JavaScriptHelper#javascript_tag.

@ -303,7 +303,7 @@ def simple_format(text, html_options={}, options={})
# # => "Welcome to my new blog at <a href=\"http://www.myblog.com/\" target=\"_blank\">http://www.myblog.com</a>.
# Please e-mail me at <a href=\"mailto:me@email.com\">me@email.com</a>."
def auto_link(text, *args, &block)#link = :all, html = {}, &block)
return ''.html_safe if text.blank?
return '' if text.blank?
options = args.size == 2 ? {} : args.extract_options! # this is necessary because the old auto_link API has a Hash as its last parameter
unless args.empty?
@ -507,7 +507,7 @@ def auto_link_urls(text, html_options = {}, options = {})
end
content_tag(:a, link_text, link_attributes.merge('href' => href), !!options[:sanitize]) + punctuation.reverse.join('')
end
end.html_safe
end
end
# Turns all email addresses into clickable links. If a block is given,

@ -81,9 +81,12 @@ def url_options
# # => /workshops
#
# <%= url_for(@workshop) %>
# # calls @workshop.to_s
# # calls @workshop.to_param which by default returns the id
# # => /workshops/5
#
# # to_param can be re-defined in a model to provide different URL names:
# # => /workshops/1-workshop-name
#
# <%= url_for("http://www.example.com") %>
# # => http://www.example.com
#
@ -183,7 +186,7 @@ def url_for(options = {})
# link_to "Profiles", :controller => "profiles"
# # => <a href="/profiles">Profiles</a>
#
# You can use a block as well if your link target is hard to fit into the name parameter. ERb example:
# You can use a block as well if your link target is hard to fit into the name parameter. ERB example:
#
# <%= link_to(@profile) do %>
# <strong><%= @profile.name %></strong> -- <span>Check it out!</span>

@ -55,7 +55,7 @@ def add_postamble(src)
class ERB
# Specify trim mode for the ERB compiler. Defaults to '-'.
# See ERb documentation for suitable values.
# See ERB documentation for suitable values.
class_attribute :erb_trim_mode
self.erb_trim_mode = '-'

@ -0,0 +1,90 @@
require 'abstract_unit'
module ActionDispatch
class FlashHashTest < ActiveSupport::TestCase
def setup
@hash = Flash::FlashHash.new
end
def test_set_get
@hash[:foo] = 'zomg'
assert_equal 'zomg', @hash[:foo]
end
def test_keys
assert_equal [], @hash.keys
@hash['foo'] = 'zomg'
assert_equal ['foo'], @hash.keys
@hash['bar'] = 'zomg'
assert_equal ['foo', 'bar'].sort, @hash.keys.sort
end
def test_update
@hash['foo'] = 'bar'
@hash.update('foo' => 'baz', 'hello' => 'world')
assert_equal 'baz', @hash['foo']
assert_equal 'world', @hash['hello']
end
def test_delete
@hash['foo'] = 'bar'
@hash.delete 'foo'
assert !@hash.key?('foo')
assert_nil @hash['foo']
end
def test_to_hash
@hash['foo'] = 'bar'
assert_equal({'foo' => 'bar'}, @hash.to_hash)
@hash.to_hash['zomg'] = 'aaron'
assert !@hash.key?('zomg')
assert_equal({'foo' => 'bar'}, @hash.to_hash)
end
def test_empty?
assert @hash.empty?
@hash['zomg'] = 'bears'
assert !@hash.empty?
@hash.clear
assert @hash.empty?
end
def test_each
@hash['hello'] = 'world'
@hash['foo'] = 'bar'
things = []
@hash.each do |k,v|
things << [k,v]
end
assert_equal([%w{ hello world }, %w{ foo bar }].sort, things.sort)
end
def test_replace
@hash['hello'] = 'world'
@hash.replace('omg' => 'aaron')
assert_equal({'omg' => 'aaron'}, @hash.to_hash)
end
def test_discard_no_args
@hash['hello'] = 'world'
@hash.discard
@hash.sweep
assert_equal({}, @hash.to_hash)
end
def test_discard_one_arg
@hash['hello'] = 'world'
@hash['omg'] = 'world'
@hash.discard 'hello'
@hash.sweep
assert_equal({'omg' => 'world'}, @hash.to_hash)
end
end
end

@ -174,13 +174,13 @@ def test_keep_and_discard_return_values
assert_equal(:foo_indeed, flash.discard(:foo)) # valid key passed
assert_nil flash.discard(:unknown) # non existant key passed
assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard()) # nothing passed
assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard(nil)) # nothing passed
assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard().to_hash) # nothing passed
assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard(nil).to_hash) # nothing passed
assert_equal(:foo_indeed, flash.keep(:foo)) # valid key passed
assert_nil flash.keep(:unknown) # non existant key passed
assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep()) # nothing passed
assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep(nil)) # nothing passed
assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep().to_hash) # nothing passed
assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep(nil).to_hash) # nothing passed
end
def test_redirect_to_with_alert
@ -214,11 +214,20 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest
SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33'
class TestController < ActionController::Base
def dont_set_flash
head :ok
end
def set_flash
flash["that"] = "hello"
head :ok
end
def set_flash_now
flash.now["that"] = "hello"
head :ok
end
def use_flash
render :inline => "flash: #{flash["that"]}"
end
@ -245,6 +254,47 @@ def test_just_using_flash_does_not_stream_a_cookie_back
end
end
def test_setting_flash_raises_after_stream_back_to_client
with_test_route_set do
env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new }
get '/set_flash', nil, env
assert_raise(ActionDispatch::ClosedError) {
@request.flash['alert'] = 'alert'
}
end
end
def test_setting_flash_raises_after_stream_back_to_client_even_with_an_empty_flash
with_test_route_set do
env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new }
get '/dont_set_flash', nil, env
assert_raise(ActionDispatch::ClosedError) {
@request.flash['alert'] = 'alert'
}
end
end
def test_setting_flash_now_raises_after_stream_back_to_client
with_test_route_set do
env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new }
get '/set_flash_now', nil, env
assert_raise(ActionDispatch::ClosedError) {
@request.flash.now['alert'] = 'alert'
}
end
end
def test_setting_flash_now_raises_after_stream_back_to_client_even_with_an_empty_flash
with_test_route_set do
env = { 'action_dispatch.request.flash_hash' => ActionDispatch::Flash::FlashHash.new }
get '/dont_set_flash', nil, env
assert_raise(ActionDispatch::ClosedError) {
@request.flash.now['alert'] = 'alert'
}
end
end
private
# Overwrite get to send SessionSecret in env hash

@ -521,4 +521,12 @@ def app
get '/foo'
assert_raise(NameError) { missing_path }
end
test "process reuse the env we pass as argument" do
env = { :SERVER_NAME => 'server', 'action_dispatch.custom' => 'custom' }
get '/foo', nil, env
assert_equal :get, env[:method]
assert_equal 'server', env[:SERVER_NAME]
assert_equal 'custom', env['action_dispatch.custom']
end
end

@ -1,6 +1,7 @@
require 'abstract_unit'
require 'controller/fake_models'
require 'active_support/core_ext/hash/conversions'
require 'active_support/core_ext/object/inclusion'
class StarStarMimeController < ActionController::Base
layout nil
@ -158,7 +159,7 @@ def rescue_action(e)
protected
def set_layout
if ["all_types_with_layout", "iphone_with_html_response_type"].include?(action_name)
if action_name.among?("all_types_with_layout", "iphone_with_html_response_type")
"respond_to/layouts/standard"
elsif action_name == "iphone_with_html_response_type_without_layout"
"respond_to/layouts/missing"
@ -558,6 +559,15 @@ def using_resource_with_status_and_location
respond_with(resource, :location => "http://test.host/", :status => :created)
end
def using_invalid_resource_with_template
respond_with(resource)
end
def using_options_with_template
@customer = resource
respond_with(@customer, :status => 123, :location => "http://test.host/")
end
def using_resource_with_responder
responder = proc { |c, r, o| c.render :text => "Resource name is #{r.first.name}" }
respond_with(resource, :responder => responder)
@ -953,6 +963,54 @@ def test_using_resource_with_status_and_location
assert_equal 201, @response.status
end
def test_using_resource_with_status_and_location_with_invalid_resource
errors = { :name => :invalid }
Customer.any_instance.stubs(:errors).returns(errors)
@request.accept = "text/xml"
post :using_resource_with_status_and_location
assert_equal errors.to_xml, @response.body
assert_equal 422, @response.status
assert_equal nil, @response.location
put :using_resource_with_status_and_location
assert_equal errors.to_xml, @response.body
assert_equal 422, @response.status
assert_equal nil, @response.location
end
def test_using_invalid_resource_with_template
errors = { :name => :invalid }
Customer.any_instance.stubs(:errors).returns(errors)
@request.accept = "text/xml"
post :using_invalid_resource_with_template
assert_equal errors.to_xml, @response.body
assert_equal 422, @response.status
assert_equal nil, @response.location
put :using_invalid_resource_with_template
assert_equal errors.to_xml, @response.body
assert_equal 422, @response.status
assert_equal nil, @response.location
end
def test_using_options_with_template
@request.accept = "text/xml"
post :using_options_with_template
assert_equal "<customer-name>david</customer-name>", @response.body
assert_equal 123, @response.status
assert_equal "http://test.host/", @response.location
put :using_options_with_template
assert_equal "<customer-name>david</customer-name>", @response.body
assert_equal 123, @response.status
assert_equal "http://test.host/", @response.location
end
def test_using_resource_with_responder
get :using_resource_with_responder
assert_equal "Resource name is david", @response.body

@ -3,8 +3,9 @@
module RenderImplicitAction
class SimpleController < ::ApplicationController
self.view_paths = [ActionView::FixtureResolver.new(
"render_implicit_action/simple/hello_world.html.erb" => "Hello world!",
"render_implicit_action/simple/hyphen-ated.html.erb" => "Hello hyphen-ated!"
"render_implicit_action/simple/hello_world.html.erb" => "Hello world!",
"render_implicit_action/simple/hyphen-ated.html.erb" => "Hello hyphen-ated!",
"render_implicit_action/simple/not_implemented.html.erb" => "Not Implemented"
)]
def hello_world() end
@ -25,9 +26,17 @@ class RenderImplicitActionTest < Rack::TestCase
assert_status 200
end
test "render an action called not_implemented" do
get "/render_implicit_action/simple/not_implemented"
assert_body "Not Implemented"
assert_status 200
end
test "action_method? returns true for implicit actions" do
assert SimpleController.new.action_method?(:hello_world)
assert SimpleController.new.action_method?(:"hyphen-ated")
assert SimpleController.new.action_method?(:not_implemented)
end
end
end

@ -172,13 +172,11 @@ def assert_not_blocked
class RequestForgeryProtectionControllerTest < ActionController::TestCase
include RequestForgeryProtectionTests
test 'should emit a csrf-token meta tag' do
test 'should emit a csrf-param meta tag and a csrf-token meta tag' do
ActiveSupport::SecureRandom.stubs(:base64).returns(@token + '<=?')
get :meta
assert_equal <<-METAS.strip_heredoc.chomp, @response.body
<meta name="csrf-param" content="authenticity_token"/>
<meta name="csrf-token" content="cf50faa3fe97702ca1ae&lt;=?"/>
METAS
assert_select 'meta[name=?][content=?]', 'csrf-param', 'authenticity_token'
assert_select 'meta[name=?][content=?]', 'csrf-token', 'cf50faa3fe97702ca1ae&lt;=?'
end
end

@ -1,6 +1,7 @@
require 'abstract_unit'
require 'active_support/core_ext/object/try'
require 'active_support/core_ext/object/with_options'
require 'active_support/core_ext/object/inclusion'
class ResourcesController < ActionController::Base
def index() render :nothing => true end
@ -1292,7 +1293,7 @@ def assert_named_route(expected, route, options)
def assert_resource_methods(expected, resource, action_method, method)
assert_equal expected.length, resource.send("#{action_method}_methods")[method].size, "#{resource.send("#{action_method}_methods")[method].inspect}"
expected.each do |action|
assert resource.send("#{action_method}_methods")[method].include?(action),
assert action.in?(resource.send("#{action_method}_methods")[method])
"#{method} not in #{action_method} methods: #{resource.send("#{action_method}_methods")[method].inspect}"
end
end
@ -1329,9 +1330,9 @@ def assert_whether_allowed(allowed, not_allowed, options, action, path, method)
options = options.merge(:action => action.to_s)
path_options = { :path => path, :method => method }
if Array(allowed).include?(action)
if action.in?(Array(allowed))
assert_recognizes options, path_options
elsif Array(not_allowed).include?(action)
elsif action.in?(Array(not_allowed))
assert_not_recognizes options, path_options
end
end

@ -495,3 +495,99 @@ def assert_not_cookie_header(expected)
end
end
end
class CookiesIntegrationTest < ActionDispatch::IntegrationTest
SessionKey = '_myapp_session'
SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33'
class TestController < ActionController::Base
def dont_set_cookies
head :ok
end
def set_cookies
cookies["that"] = "hello"
head :ok
end
end
def test_setting_cookies_raises_after_stream_back_to_client
with_test_route_set do
get '/set_cookies'
assert_raise(ActionDispatch::ClosedError) {
request.cookie_jar['alert'] = 'alert'
cookies['alert'] = 'alert'
}
end
end
def test_setting_cookies_raises_after_stream_back_to_client_even_without_cookies
with_test_route_set do
get '/dont_set_cookies'
assert_raise(ActionDispatch::ClosedError) {
request.cookie_jar['alert'] = 'alert'
}
end
end
def test_setting_permanent_cookies_raises_after_stream_back_to_client
with_test_route_set do
get '/set_cookies'
assert_raise(ActionDispatch::ClosedError) {
request.cookie_jar.permanent['alert'] = 'alert'
cookies['alert'] = 'alert'
}
end
end
def test_setting_permanent_cookies_raises_after_stream_back_to_client_even_without_cookies
with_test_route_set do
get '/dont_set_cookies'
assert_raise(ActionDispatch::ClosedError) {
request.cookie_jar.permanent['alert'] = 'alert'
}
end
end
def test_setting_signed_cookies_raises_after_stream_back_to_client
with_test_route_set do
get '/set_cookies'
assert_raise(ActionDispatch::ClosedError) {
request.cookie_jar.signed['alert'] = 'alert'
cookies['alert'] = 'alert'
}
end
end
def test_setting_signed_cookies_raises_after_stream_back_to_client_even_without_cookies
with_test_route_set do
get '/dont_set_cookies'
assert_raise(ActionDispatch::ClosedError) {
request.cookie_jar.signed['alert'] = 'alert'
}
end
end
private
# Overwrite get to send SessionSecret in env hash
def get(path, parameters = nil, env = {})
env["action_dispatch.secret_token"] ||= SessionSecret
super
end
def with_test_route_set
with_routing do |set|
set.draw do
match ':action', :to => CookiesIntegrationTest::TestController
end
@app = self.class.build_app(set) do |middleware|
middleware.use ActionDispatch::Cookies
middleware.delete "ActionDispatch::ShowExceptions"
end
yield
end
end
end

@ -1,6 +1,7 @@
require 'erb'
require 'abstract_unit'
require 'controller/fake_controllers'
require 'active_support/core_ext/object/inclusion'
class TestRoutingMapper < ActionDispatch::IntegrationTest
SprocketsApp = lambda { |env|
@ -495,7 +496,7 @@ def self.call(params, request)
resources :todos, :id => /\d+/
end
scope '/countries/:country', :constraints => lambda { |params, req| %[all France].include?(params[:country]) } do
scope '/countries/:country', :constraints => lambda { |params, req| params[:country].among?("all", "France") } do
match '/', :to => 'countries#index'
match '/cities', :to => 'countries#cities'
end

@ -0,0 +1 @@
<content>I should not be displayed</content>

@ -0,0 +1 @@
<customer-name><%= @customer.name %></customer-name>

@ -1,4 +1,5 @@
require 'abstract_unit'
require 'active_support/core_ext/object/inclusion'
class ErbUtilTest < Test::Unit::TestCase
include ERB::Util
@ -29,7 +30,7 @@ def test_html_escape_passes_html_escpe_unmodified
def test_rest_in_ascii
(0..127).to_a.map {|int| int.chr }.each do |chr|
next if %w(& " < >).include?(chr)
next if chr.in?('&"<>')
assert_equal chr, html_escape(chr)
end
end

@ -1,5 +1,6 @@
require 'abstract_unit'
require 'controller/fake_models'
require 'active_support/core_ext/object/inclusion'
class FormHelperTest < ActionView::TestCase
tests ActionView::Helpers::FormHelper
@ -1743,7 +1744,7 @@ def test_form_for_with_labelled_builder
def snowman(method = nil)
txt = %{<div style="margin:0;padding:0;display:inline">}
txt << %{<input name="utf8" type="hidden" value="&#x2713;" />}
if (method && !['get','post'].include?(method.to_s))
if method && !method.to_s.among?('get', 'post')
txt << %{<input name="_method" type="hidden" value="#{method}" />}
end
txt << %{</div>}

@ -1,5 +1,6 @@
require 'abstract_unit'
require 'tzinfo'
require 'active_support/core_ext/object/inclusion'
class Map < Hash
def category
@ -82,7 +83,7 @@ def test_collection_options_with_preselected_and_disabled_value
def test_collection_options_with_proc_for_disabled
assert_dom_equal(
"<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" disabled=\"disabled\">Babe went home</option>\n<option value=\"Cabe\" disabled=\"disabled\">Cabe went home</option>",
options_from_collection_for_select(dummy_posts, "author_name", "title", :disabled => lambda{|p| %w(Babe Cabe).include? p.author_name })
options_from_collection_for_select(dummy_posts, "author_name", "title", :disabled => lambda{|p| p.author_name.among?("Babe", "Cabe") })
)
end

@ -1,4 +1,5 @@
require 'abstract_unit'
require 'active_support/core_ext/object/inclusion'
class FormTagHelperTest < ActionView::TestCase
tests ActionView::Helpers::FormTagHelper
@ -13,7 +14,7 @@ def snowman(options = {})
txt = %{<div style="margin:0;padding:0;display:inline">}
txt << %{<input name="utf8" type="hidden" value="&#x2713;" />}
if (method && !['get','post'].include?(method.to_s))
if method && !method.to_s.among?('get','post')
txt << %{<input name="_method" type="hidden" value="#{method}" />}
end
txt << %{</div>}

@ -315,14 +315,20 @@ def generate_result(link_text, href = nil, escape = false)
end
end
def test_auto_link_should_be_html_safe
def test_auto_link_should_not_be_html_safe
email_raw = 'santiago@wyeworks.com'
link_raw = 'http://www.rubyonrails.org'
assert auto_link(nil).html_safe?
assert auto_link('').html_safe?
assert auto_link("#{link_raw} #{link_raw} #{link_raw}").html_safe?
assert auto_link("hello #{email_raw}").html_safe?
assert !auto_link(nil).html_safe?, 'should not be html safe'
assert !auto_link('').html_safe?, 'should not be html safe'
assert !auto_link("#{link_raw} #{link_raw} #{link_raw}").html_safe?, 'should not be html safe'
assert !auto_link("hello #{email_raw}").html_safe?, 'should not be html safe'
end
def test_auto_link_email_address
email_raw = 'aaron@tenderlovemaking.com'
email_result = %{<a href="mailto:#{email_raw}">#{email_raw}</a>}
assert !auto_link_email_addresses(email_result).html_safe?, 'should not be html safe'
end
def test_auto_link

@ -1,5 +1,14 @@
*Rails 3.1.0 (unreleased)*
* Add support for proc or lambda as an option for InclusionValidator,
ExclusionValidator, and FormatValidator [Prem Sichanugrist]
You can now supply Proc, lambda, or anything that respond to #call in those
validations, and it will be called with current record as an argument.
That given proc or lambda must returns an object which respond to #include? for
InclusionValidator and ExclusionValidator, and returns a regular expression
object for FormatValidator.
* Added ActiveModel::SecurePassword to encapsulate dead-simple password usage with BCrypt encryption and salting [DHH]
* ActiveModel::AttributeMethods allows attributes to be defined on demand [Alexander Uvarov]

@ -17,8 +17,6 @@
s.files = Dir['CHANGELOG', 'MIT-LICENSE', 'README.rdoc', 'lib/**/*']
s.require_path = 'lib'
s.has_rdoc = true
s.add_dependency('activesupport', version)
s.add_dependency('builder', '~> 3.0.0')
s.add_dependency('i18n', '~> 0.5.0')

@ -8,7 +8,7 @@ module ActiveModel
# Provides a way to track changes in your object in the same way as
# Active Record does.
#
# The requirements to implement ActiveModel::Dirty are to:
# The requirements for implementing ActiveModel::Dirty are:
#
# * <tt>include ActiveModel::Dirty</tt> in your object
# * Call <tt>define_attribute_methods</tt> passing each method you want to

@ -278,19 +278,18 @@ def full_messages
# When using inheritance in your models, it will check all the inherited
# models too, but only if the model itself hasn't been found. Say you have
# <tt>class Admin < User; end</tt> and you wanted the translation for
# the <tt>:blank</tt> error +message+ for the <tt>title</tt> +attribute+,
# the <tt>:blank</tt> error message for the <tt>title</tt> attribute,
# it looks for these translations:
#
# <ol>
# <li><tt>activemodel.errors.models.admin.attributes.title.blank</tt></li>
# <li><tt>activemodel.errors.models.admin.blank</tt></li>
# <li><tt>activemodel.errors.models.user.attributes.title.blank</tt></li>
# <li><tt>activemodel.errors.models.user.blank</tt></li>
# <li>any default you provided through the +options+ hash (in the activemodel.errors scope)</li>
# <li><tt>activemodel.errors.messages.blank</tt></li>
# <li><tt>errors.attributes.title.blank</tt></li>
# <li><tt>errors.messages.blank</tt></li>
# </ol>
# * <tt>activemodel.errors.models.admin.attributes.title.blank</tt>
# * <tt>activemodel.errors.models.admin.blank</tt>
# * <tt>activemodel.errors.models.user.attributes.title.blank</tt>
# * <tt>activemodel.errors.models.user.blank</tt>
# * any default you provided through the +options+ hash (in the <tt>activemodel.errors</tt> scope)
# * <tt>activemodel.errors.messages.blank</tt>
# * <tt>errors.attributes.title.blank</tt>
# * <tt>errors.messages.blank</tt>
#
def generate_message(attribute, type = :invalid, options = {})
type = options.delete(:message) if options[:message].is_a?(Symbol)

@ -1,18 +1,35 @@
require 'active_support/core_ext/range.rb'
module ActiveModel
# == Active Model Exclusion Validator
module Validations
class ExclusionValidator < EachValidator
ERROR_MESSAGE = "An object with the method #include? or a proc or lambda is required, " <<
"and must be supplied as the :in option of the configuration hash"
def check_validity!
raise ArgumentError, "An object with the method include? is required must be supplied as the " <<
":in option of the configuration hash" unless options[:in].respond_to?(:include?)
unless [:include?, :call].any? { |method| options[:in].respond_to?(method) }
raise ArgumentError, ERROR_MESSAGE
end
end
def validate_each(record, attribute, value)
if options[:in].include?(value)
delimiter = options[:in]
exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter
if exclusions.send(inclusion_method(exclusions), value)
record.errors.add(attribute, :exclusion, options.except(:in).merge!(:value => value))
end
end
private
# In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the
# range for equality, so it may be slow for large ranges. The new <tt>Range#cover?</tt>
# uses the previous logic of comparing a value with the range endpoints.
def inclusion_method(enumerable)
enumerable.is_a?(Range) ? :cover? : :include?
end
end
module HelperMethods
@ -22,10 +39,14 @@ module HelperMethods
# validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here"
# validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60"
# validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension %{value} is not allowed"
# validates_exclusion_of :password, :in => lambda { |p| [p.username, p.first_name] }, :message => "should not be the same as your username or first name"
# end
#
# Configuration options:
# * <tt>:in</tt> - An enumerable object of items that the value shouldn't be part of.
# This can be supplied as a proc or lambda which returns an enumerable. If the enumerable
# is a range the test is performed with <tt>Range#cover?</tt>
# (backported in Active Support for 1.8), otherwise with <tt>include?</tt>.
# * <tt>:message</tt> - Specifies a custom error message (default is: "is reserved").
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
# * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).

@ -4,10 +4,12 @@ module ActiveModel
module Validations
class FormatValidator < EachValidator
def validate_each(record, attribute, value)
if options[:with] && value.to_s !~ options[:with]
record.errors.add(attribute, :invalid, options.except(:with).merge!(:value => value))
elsif options[:without] && value.to_s =~ options[:without]
record.errors.add(attribute, :invalid, options.except(:without).merge!(:value => value))
if options[:with]
regexp = option_call(record, :with)
record_error(record, attribute, :with, value) if value.to_s !~ regexp
elsif options[:without]
regexp = option_call(record, :without)
record_error(record, attribute, :without, value) if value.to_s =~ regexp
end
end
@ -16,12 +18,25 @@ def check_validity!
raise ArgumentError, "Either :with or :without must be supplied (but not both)"
end
if options[:with] && !options[:with].is_a?(Regexp)
raise ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash"
end
check_options_validity(options, :with)
check_options_validity(options, :without)
end
if options[:without] && !options[:without].is_a?(Regexp)
raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash"
private
def option_call(record, name)
option = options[name]
option.respond_to?(:call) ? option.call(record) : option
end
def record_error(record, attribute, name, value)
record.errors.add(attribute, :invalid, options.except(name).merge!(:value => value))
end
def check_options_validity(options, name)
option = options[name]
if option && !option.is_a?(Regexp) && !option.respond_to?(:call)
raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}"
end
end
end
@ -40,17 +55,26 @@ module HelperMethods
# validates_format_of :email, :without => /NOSPAM/
# end
#
# You can also provide a proc or lambda which will determine the regular expression that will be used to validate the attribute
#
# class Person < ActiveRecord::Base
# # Admin can have number as a first letter in their screen name
# validates_format_of :screen_name, :with => lambda{ |person| person.admin? ? /\A[a-z0-9][a-z0-9_\-]*\Z/i : /\A[a-z][a-z0-9_\-]*\Z/i }
# end
#
# Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the string, <tt>^</tt> and <tt>$</tt> match the start/end of a line.
#
# You must pass either <tt>:with</tt> or <tt>:without</tt> as an option. In addition, both must be a regular expression,
# or else an exception will be raised.
# You must pass either <tt>:with</tt> or <tt>:without</tt> as an option. In addition, both must be a regular expression
# or a proc or lambda, or else an exception will be raised.
#
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "is invalid").
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
# * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
# * <tt>:with</tt> - Regular expression that if the attribute matches will result in a successful validation.
# This can be provided as a proc or lambda returning regular expression which will be called at runtime.
# * <tt>:without</tt> - Regular expression that if the attribute does not match will result in a successful validation.
# This can be provided as a proc or lambda returning regular expression which will be called at runtime.
# * <tt>:on</tt> - Specifies when this validation is active. Runs in all
# validation contexts by default (+nil+), other options are <tt>:create</tt>
# and <tt>:update</tt>.

@ -5,20 +5,30 @@ module ActiveModel
# == Active Model Inclusion Validator
module Validations
class InclusionValidator < EachValidator
ERROR_MESSAGE = "An object with the method #include? or a proc or lambda is required, " <<
"and must be supplied as the :in option of the configuration hash"
def check_validity!
raise ArgumentError, "An object with the method include? is required must be supplied as the " <<
":in option of the configuration hash" unless options[:in].respond_to?(:include?)
unless [:include?, :call].any?{ |method| options[:in].respond_to?(method) }
raise ArgumentError, ERROR_MESSAGE
end
end
def validate_each(record, attribute, value)
record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value)) unless options[:in].send(include?, value)
delimiter = options[:in]
exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter
unless exclusions.send(inclusion_method(exclusions), value)
record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value))
end
end
private
# In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the
# range for equality, so it may be slow for large ranges. The new <tt>Range#cover?</tt>
# uses the previous logic of comparing a value with the range endpoints.
def include?
options[:in].is_a?(Range) ? :cover? : :include?
def inclusion_method(enumerable)
enumerable.is_a?(Range) ? :cover? : :include?
end
end
@ -29,11 +39,13 @@ module HelperMethods
# validates_inclusion_of :gender, :in => %w( m f )
# validates_inclusion_of :age, :in => 0..99
# validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension %{value} is not included in the list"
# validates_inclusion_of :states, :in => lambda{ |person| STATES[person.country] }
# end
#
# Configuration options:
# * <tt>:in</tt> - An enumerable object of available items.
# If the enumerable is a range the test is performed with <tt>Range#cover?</tt>
# * <tt>:in</tt> - An enumerable object of available items. This can be
# supplied as a proc or lambda which returns an enumerable. If the enumerable
# is a range the test is performed with <tt>Range#cover?</tt>
# (backported in Active Support for 1.8), otherwise with <tt>include?</tt>.
# * <tt>:message</tt> - Specifies a custom error message (default is: "is not included in the list").
# * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).

@ -1,6 +1,7 @@
require 'active_support/core_ext/array/wrap'
require "active_support/core_ext/module/anonymous"
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/object/inclusion'
module ActiveModel #:nodoc:
@ -67,7 +68,7 @@ module ActiveModel #:nodoc:
#
# class TitleValidator < ActiveModel::EachValidator
# def validate_each(record, attribute, value)
# record.errors[attribute] << 'must be Mr. Mrs. or Dr.' unless ['Mr.', 'Mrs.', 'Dr.'].include?(value)
# record.errors[attribute] << 'must be Mr. Mrs. or Dr.' unless value.among?('Mr.', 'Mrs.', 'Dr.')
# end
# end
#

@ -1,5 +1,6 @@
require "cases/helper"
require 'logger'
require 'active_support/core_ext/object/inclusion'
class SanitizerTest < ActiveModel::TestCase
@ -9,7 +10,7 @@ class SanitizingAuthorizer
attr_accessor :logger
def deny?(key)
[ 'admin' ].include?(key)
key.in?(['admin'])
end
end

@ -42,4 +42,16 @@ def test_validates_exclusion_of_for_ruby_class
ensure
Person.reset_callbacks(:validate)
end
def test_validates_exclusion_of_with_lambda
Topic.validates_exclusion_of :title, :in => lambda{ |topic| topic.author_name == "sikachu" ? %w( monkey elephant ) : %w( abe wasabi ) }
p = Topic.new
p.title = "elephant"
p.author_name = "sikachu"
assert p.invalid?
p.title = "wasabi"
assert p.valid?
end
end

@ -98,6 +98,30 @@ def test_validates_format_of_when_not_isnt_a_regexp_should_raise_error
assert_raise(ArgumentError) { Topic.validates_format_of(:title, :without => "clearly not a regexp") }
end
def test_validates_format_of_with_lambda
Topic.validates_format_of :content, :with => lambda{ |topic| topic.title == "digit" ? /\A\d+\Z/ : /\A\S+\Z/ }
p = Topic.new
p.title = "digit"
p.content = "Pixies"
assert p.invalid?
p.content = "1234"
assert p.valid?
end
def test_validates_format_of_without_lambda
Topic.validates_format_of :content, :without => lambda{ |topic| topic.title == "characters" ? /\A\d+\Z/ : /\A\S+\Z/ }
p = Topic.new
p.title = "characters"
p.content = "1234"
assert p.invalid?
p.content = "Pixies"
assert p.valid?
end
def test_validates_format_of_for_ruby_class
Person.validates_format_of :karma, :with => /\A\d+\Z/

@ -74,4 +74,16 @@ def test_validates_inclusion_of_for_ruby_class
ensure
Person.reset_callbacks(:validate)
end
def test_validates_inclusion_of_with_lambda
Topic.validates_inclusion_of :title, :in => lambda{ |topic| topic.author_name == "sikachu" ? %w( monkey elephant ) : %w( abe wasabi ) }
p = Topic.new
p.title = "wasabi"
p.author_name = "sikachu"
assert p.invalid?
p.title = "elephant"
assert p.valid?
end
end

@ -1,5 +1,57 @@
*Rails 3.1.0 (unreleased)*
* Passing a proc (or other object that responds to #call) to scope is deprecated. If you need your
scope to be lazily evaluated, or takes parameters, please define it as a normal class method
instead. For example, change this:
class Post < ActiveRecord::Base
scope :unpublished, lambda { where('published_at > ?', Time.now) }
end
To this:
class Post < ActiveRecord::Base
def self.unpublished
where('published_at > ?', Time.now)
end
end
[Jon Leighton]
* Default scopes are now evaluated at the latest possible moment, to avoid problems where
scopes would be created which would implicitly contain the default scope, which would then
be impossible to get rid of via Model.unscoped.
Note that this means that if you are inspecting the internal structure of an
ActiveRecord::Relation, it will *not* contain the default scope, though the resulting
query will do. You can get a relation containing the default scope by calling
ActiveRecord#with_default_scope, though this is not part of the public API.
[Jon Leighton]
* Deprecated support for passing hashes and relations to 'default_scope'. Please create a class
method for your scope instead. For example, change this:
class Post < ActiveRecord::Base
default_scope where(:published => true)
end
To this:
class Post < ActiveRecord::Base
def self.default_scope
where(:published => true)
end
end
Rationale: It will make the implementation simpler because we can simply use inheritance to
handle inheritance scenarios, rather than trying to make up our own rules about what should
happen when you call default_scope multiple times or in subclasses.
[Jon Leighton]
* PostgreSQL adapter only supports PostgreSQL version 8.2 and higher.
* ConnectionManagement middleware is changed to clean up the connection pool
after the rack body has been flushed.

@ -17,7 +17,6 @@
s.files = Dir['CHANGELOG', 'README.rdoc', 'examples/**/*', 'lib/**/*']
s.require_path = 'lib'
s.has_rdoc = true
s.extra_rdoc_files = %w( README.rdoc )
s.rdoc_options.concat ['--main', 'README.rdoc']

@ -1,4 +1,5 @@
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/object/inclusion'
module ActiveRecord
module Associations
@ -163,7 +164,7 @@ def interpolate(sql, record = nil)
def creation_attributes
attributes = {}
if [:has_one, :has_many].include?(reflection.macro) && !options[:through]
if reflection.macro.among?(:has_one, :has_many) && !options[:through]
attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
if reflection.options[:as]

@ -1,3 +1,5 @@
require 'active_support/core_ext/object/inclusion'
module ActiveRecord::Associations::Builder
class BelongsTo < SingularAssociation #:nodoc:
self.macro = :belongs_to
@ -65,7 +67,7 @@ def add_touch_callbacks(reflection)
def configure_dependency
if options[:dependent]
unless [:destroy, :delete].include?(options[:dependent])
unless options[:dependent].among?(:destroy, :delete)
raise ArgumentError, "The :dependent option expects either :destroy or :delete (#{options[:dependent].inspect})"
end

@ -1,3 +1,5 @@
require 'active_support/core_ext/object/inclusion'
module ActiveRecord::Associations::Builder
class HasMany < CollectionAssociation #:nodoc:
self.macro = :has_many
@ -14,7 +16,7 @@ def build
def configure_dependency
if options[:dependent]
unless [:destroy, :delete_all, :nullify, :restrict].include?(options[:dependent])
unless options[:dependent].among?(:destroy, :delete_all, :nullify, :restrict)
raise ArgumentError, "The :dependent option expects either :destroy, :delete_all, " \
":nullify or :restrict (#{options[:dependent].inspect})"
end

@ -1,3 +1,5 @@
require 'active_support/core_ext/object/inclusion'
module ActiveRecord::Associations::Builder
class HasOne < SingularAssociation #:nodoc:
self.macro = :has_one
@ -27,7 +29,7 @@ def validate_options
def configure_dependency
if options[:dependent]
unless [:destroy, :delete, :nullify, :restrict].include?(options[:dependent])
unless options[:dependent].among?(:destroy, :delete, :nullify, :restrict)
raise ArgumentError, "The :dependent option expects either :destroy, :delete, " \
":nullify or :restrict (#{options[:dependent].inspect})"
end

@ -21,14 +21,7 @@ class CollectionAssociation < Association #:nodoc:
attr_reader :proxy
def initialize(owner, reflection)
# When scopes are created via method_missing on the proxy, they are stored so that
# any records fetched from the database are kept around for future use.
@scopes_cache = Hash.new do |hash, method|
hash[method] = { }
end
super
@proxy = CollectionProxy.new(self)
end
@ -74,7 +67,6 @@ def ids_writer(ids)
def reset
@loaded = false
@target = []
@scopes_cache.clear
end
def select(select = nil)
@ -327,10 +319,6 @@ def include?(record)
end
end
def cached_scope(method, args)
@scopes_cache[method][args] ||= scoped.readonly(nil).send(method, *args)
end
def load_target
if find_target?
targets = []

@ -82,8 +82,6 @@ def method_missing(method, *args, &block)
end
end
elsif @association.klass.scopes[method]
@association.cached_scope(method, args)
else
scoped.readonly(nil).send(method, *args, &block)
end

@ -1,3 +1,5 @@
require 'active_support/core_ext/object/inclusion'
module ActiveRecord
# = Active Record Belongs To Has One Association
module Associations
@ -50,7 +52,7 @@ def set_new_record(record)
end
def remove_target!(method)
if [:delete, :destroy].include?(method)
if method.among?(:delete, :destroy)
target.send(method)
else
nullify_owner_attributes(target)

@ -1,4 +1,5 @@
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/object/inclusion'
module ActiveRecord
module AttributeMethods
@ -58,7 +59,7 @@ def #{attr_name}=(original_time)
private
def create_time_zone_conversion_attribute?(name, column)
time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type)
time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && column.type.among?(:datetime, :timestamp)
end
end
end

@ -425,8 +425,8 @@ class Base
self.store_full_sti_class = true
# Stores the default scope for the class
class_attribute :default_scoping, :instance_writer => false
self.default_scoping = []
class_attribute :default_scopes, :instance_writer => false
self.default_scopes = []
# Returns a hash of all the attributes that have been specified for serialization as
# keys and their class restriction as values.
@ -870,7 +870,9 @@ def arel_engine
# Returns a scope for this class without taking into account the default_scope.
#
# class Post < ActiveRecord::Base
# default_scope :published => true
# def self.default_scope
# where :published => true
# end
# end
#
# Post.all # Fires "SELECT * FROM posts WHERE published = true"
@ -892,13 +894,8 @@ def unscoped #:nodoc:
block_given? ? relation.scoping { yield } : relation
end
def scoped_methods #:nodoc:
key = :"#{self}_scoped_methods"
Thread.current[key] = Thread.current[key].presence || self.default_scoping.dup
end
def before_remove_const #:nodoc:
reset_scoped_methods
self.current_scope = nil
end
# Specifies how the record is loaded by +Marshal+.
@ -1020,7 +1017,7 @@ def method_missing(method_id, *arguments, &block)
super unless all_attributes_exists?(attribute_names)
if match.finder?
options = arguments.extract_options!
relation = options.any? ? construct_finder_arel(options, current_scoped_methods) : scoped
relation = options.any? ? scoped(options) : scoped
relation.send :find_by_attributes, match, attribute_names, *arguments
elsif match.instantiator?
scoped.send :find_or_instantiator_by_attributes, match, attribute_names, *arguments, &block
@ -1109,43 +1106,47 @@ def all_attributes_exists?(attribute_names)
# end
#
# *Note*: the +:find+ scope also has effect on update and deletion methods, like +update_all+ and +delete_all+.
def with_scope(method_scoping = {}, action = :merge, &block)
method_scoping = method_scoping.method_scoping if method_scoping.respond_to?(:method_scoping)
def with_scope(scope = {}, action = :merge, &block)
# If another Active Record class has been passed in, get its current scope
scope = scope.current_scope if !scope.is_a?(Relation) && scope.respond_to?(:current_scope)
if method_scoping.is_a?(Hash)
previous_scope = self.current_scope
if scope.is_a?(Hash)
# Dup first and second level of hash (method and params).
method_scoping = method_scoping.dup
method_scoping.each do |method, params|
method_scoping[method] = params.dup unless params == true
scope = scope.dup
scope.each do |method, params|
scope[method] = params.dup unless params == true
end
method_scoping.assert_valid_keys([ :find, :create ])
relation = construct_finder_arel(method_scoping[:find] || {})
scope.assert_valid_keys([ :find, :create ])
relation = construct_finder_arel(scope[:find] || {})
relation.default_scoped = true unless action == :overwrite
if current_scoped_methods && current_scoped_methods.create_with_value && method_scoping[:create]
if previous_scope && previous_scope.create_with_value && scope[:create]
scope_for_create = if action == :merge
current_scoped_methods.create_with_value.merge(method_scoping[:create])
previous_scope.create_with_value.merge(scope[:create])
else
method_scoping[:create]
scope[:create]
end
relation = relation.create_with(scope_for_create)
else
scope_for_create = method_scoping[:create]
scope_for_create ||= current_scoped_methods.create_with_value if current_scoped_methods
scope_for_create = scope[:create]
scope_for_create ||= previous_scope.create_with_value if previous_scope
relation = relation.create_with(scope_for_create) if scope_for_create
end
method_scoping = relation
scope = relation
end
method_scoping = current_scoped_methods.merge(method_scoping) if current_scoped_methods && action == :merge
scope = previous_scope.merge(scope) if previous_scope && action == :merge
self.scoped_methods << method_scoping
self.current_scope = scope
begin
yield
ensure
self.scoped_methods.pop
self.current_scope = previous_scope
end
end
@ -1168,41 +1169,82 @@ def with_exclusive_scope(method_scoping = {}, &block)
with_scope(method_scoping, :overwrite, &block)
end
# Sets the default options for the model. The format of the
# <tt>options</tt> argument is the same as in find.
def current_scope #:nodoc:
Thread.current[:"#{self}_current_scope"]
end
def current_scope=(scope) #:nodoc:
Thread.current[:"#{self}_current_scope"] = scope
end
# Implement this method in your model to set a default scope for all operations on
# the model.
#
# class Person < ActiveRecord::Base
# def self.default_scope
# order('last_name, first_name')
# end
# end
#
# Person.all # => SELECT * FROM people ORDER BY last_name, first_name
#
# The <tt>default_scope</tt> is also applied while creating/building a record. It is not
# applied while updating a record.
#
# class Article < ActiveRecord::Base
# def self.default_scope
# where(:published => true)
# end
# end
#
# Article.new.published # => true
# Article.create.published # => true
#
# === Deprecation warning
#
# There is an alternative syntax as follows:
#
# class Person < ActiveRecord::Base
# default_scope order('last_name, first_name')
# end
#
# <tt>default_scope</tt> is also applied while creating/building a record. It is not
# applied while updating a record.
#
# class Article < ActiveRecord::Base
# default_scope where(:published => true)
# end
#
# Article.new.published # => true
# Article.create.published # => true
def default_scope(options = {})
reset_scoped_methods
default_scoping = self.default_scoping.dup
self.default_scoping = default_scoping << construct_finder_arel(options, default_scoping.pop)
# This is now deprecated and will be removed in Rails 3.2.
def default_scope(scope = {})
ActiveSupport::Deprecation.warn <<-WARN
Passing a hash or scope to default_scope is deprecated and will be removed in Rails 3.2. You should create a class method for your scope instead. For example, change this:
class Post < ActiveRecord::Base
default_scope where(:published => true)
end
To this:
class Post < ActiveRecord::Base
def self.default_scope
where(:published => true)
end
end
WARN
self.default_scopes = default_scopes.dup << scope
end
def current_scoped_methods #:nodoc:
method = scoped_methods.last
if method.respond_to?(:call)
relation.scoping { method.call }
else
method
def build_default_scope #:nodoc:
if method(:default_scope).owner != Base.singleton_class
# Use relation.scoping to ensure we ignore whatever the current value of
# self.current_scope may be.
relation.scoping { default_scope }
elsif default_scopes.any?
default_scopes.inject(relation) do |default_scope, scope|
if scope.is_a?(Hash)
default_scope.apply_finder_options(scope)
else
default_scope.merge(scope)
end
end
end
end
def reset_scoped_methods #:nodoc:
Thread.current[:"#{self}_scoped_methods"] = nil
end
# Returns the class type of the record using the current module as a prefix. So descendants of
# MyApp::Business::Account would appear as MyApp::Business::AccountSubclass.
def compute_type(type_name)
@ -1916,11 +1958,8 @@ def convert_number_column_value(value)
end
def populate_with_current_scope_attributes
if scope = self.class.send(:current_scoped_methods)
create_with = scope.scope_for_create
create_with.each { |att,value|
respond_to?("#{att}=") && send("#{att}=", value)
}
self.class.scoped.scope_for_create.each do |att,value|
respond_to?("#{att}=") && send("#{att}=", value)
end
end

@ -214,6 +214,24 @@ module ActiveRecord
# needs to be aware of it because an ordinary +save+ will raise such exception
# instead of quietly returning +false+.
#
# == Debugging callbacks
#
# The callback chain is accessible via the <tt>_*_callbacks</tt> method on an object. ActiveModel Callbacks support
# <tt>:before</tt>, <tt>:after</tt> and <tt>:around</tt> as values for the <tt>kind</tt> property. The <tt>kind</tt> property
# defines what part of the chain the callback runs in.
#
# To find all callbacks in the before_save callback chain:
#
# Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }
#
# Returns an array of callback objects that form the before_save chain.
#
# To further check if the before_save chain contains a proc defined as <tt>rest_when_dead</tt> use the <tt>filter</tt> property of the callback object:
#
# Topic._save_callbacks.select { |cb| cb.kind.eql?(:before) }.collect(&:filter).include?(:rest_when_dead)
#
# Returns true or false depending on whether the proc is contained in the before_save callback chain on a Topic model.
#
module Callbacks
extend ActiveSupport::Concern

@ -203,6 +203,10 @@ def rollback_to_savepoint
def release_savepoint
end
def case_sensitive_modifier(node)
node
end
def current_savepoint_name
"active_record_#{open_transactions}"
end

@ -528,6 +528,11 @@ def primary_key(table)
def case_sensitive_equality_operator
"= BINARY"
end
deprecate :case_sensitive_equality_operator
def case_sensitive_modifier(node)
Arel::Nodes::Bin.new(node)
end
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
where_sql
@ -587,8 +592,26 @@ def configure_connection
# Returns an array of record hashes with the column names as keys and
# column values as values.
def select(sql, name = nil)
execute(sql, name).each(:as => :hash)
def select(sql, name = nil, binds = [])
exec_query(sql, name, binds).to_a
end
def exec_query(sql, name = 'SQL', binds = [])
@connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
log(sql, name, binds) do
begin
result = @connection.query(sql)
rescue ActiveRecord::StatementInvalid => exception
if exception.message.split(":").first =~ /Packets out of order/
raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
else
raise
end
end
ActiveRecord::Result.new(result.fields, result.to_a)
end
end
def supports_views?

@ -196,6 +196,7 @@ def initialize(connection, logger, connection_options, config)
@connection_options, @config = connection_options, config
@quoted_column_names, @quoted_table_names = {}, {}
@statements = {}
@client_encoding = nil
connect
end
@ -330,6 +331,63 @@ def clear_cache!
@statements.clear
end
if "<3".respond_to?(:encode)
# Taken from here:
# https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb
# Author: TOMITA Masahiro <tommy@tmtm.org>
ENCODINGS = {
"armscii8" => nil,
"ascii" => Encoding::US_ASCII,
"big5" => Encoding::Big5,
"binary" => Encoding::ASCII_8BIT,
"cp1250" => Encoding::Windows_1250,
"cp1251" => Encoding::Windows_1251,
"cp1256" => Encoding::Windows_1256,
"cp1257" => Encoding::Windows_1257,
"cp850" => Encoding::CP850,
"cp852" => Encoding::CP852,
"cp866" => Encoding::IBM866,
"cp932" => Encoding::Windows_31J,
"dec8" => nil,
"eucjpms" => Encoding::EucJP_ms,
"euckr" => Encoding::EUC_KR,
"gb2312" => Encoding::EUC_CN,
"gbk" => Encoding::GBK,
"geostd8" => nil,
"greek" => Encoding::ISO_8859_7,
"hebrew" => Encoding::ISO_8859_8,
"hp8" => nil,
"keybcs2" => nil,
"koi8r" => Encoding::KOI8_R,
"koi8u" => Encoding::KOI8_U,
"latin1" => Encoding::ISO_8859_1,
"latin2" => Encoding::ISO_8859_2,
"latin5" => Encoding::ISO_8859_9,
"latin7" => Encoding::ISO_8859_13,
"macce" => Encoding::MacCentEuro,
"macroman" => Encoding::MacRoman,
"sjis" => Encoding::SHIFT_JIS,
"swe7" => nil,
"tis620" => Encoding::TIS_620,
"ucs2" => Encoding::UTF_16BE,
"ujis" => Encoding::EucJP_ms,
"utf8" => Encoding::UTF_8,
"utf8mb4" => Encoding::UTF_8,
}
else
ENCODINGS = Hash.new { |h,k| h[k] = k }
end
# Get the client encoding for this database
def client_encoding
return @client_encoding if @client_encoding
result = exec_query(
"SHOW VARIABLES WHERE Variable_name = 'character_set_client'",
'SCHEMA')
@client_encoding = ENCODINGS[result.rows.last.last]
end
def exec_query(sql, name = 'SQL', binds = [])
log(sql, name, binds) do
result = nil
@ -363,6 +421,10 @@ def exec_query(sql, name = 'SQL', binds = [])
end
end
def exec_insert(sql, name, binds)
exec_query(sql, name, binds)
end
def exec_without_stmt(sql, name = 'SQL') # :nodoc:
# Some queries, like SHOW CREATE TABLE don't work through the prepared
# statement API. For those queries, we need to use this method. :'(
@ -506,7 +568,7 @@ def collation
def tables(name = nil, database = nil) #:nodoc:
tables = []
result = execute(["SHOW TABLES", database].compact.join(' IN '), name)
result = execute(["SHOW TABLES", database].compact.join(' IN '), 'SCHEMA')
result.each { |field| tables << field[0] }
result.free
tables
@ -551,7 +613,7 @@ def indexes(table_name, name = nil)#:nodoc:
def columns(table_name, name = nil)#:nodoc:
sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
columns = []
result = execute(sql)
result = execute(sql, 'SCHEMA')
result.each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
result.free
columns
@ -638,7 +700,7 @@ def show_variable(name)
# Returns a table's primary key and belonging sequence.
def pk_and_sequence_for(table) #:nodoc:
keys = []
result = execute("describe #{quote_table_name(table)}")
result = execute("describe #{quote_table_name(table)}", 'SCHEMA')
result.each_hash do |h|
keys << h["Field"]if h["Key"] == "PRI"
end
@ -655,6 +717,11 @@ def primary_key(table)
def case_sensitive_equality_operator
"= BINARY"
end
deprecate :case_sensitive_equality_operator
def case_sensitive_modifier(node)
Arel::Nodes::Bin.new(node)
end
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
where_sql

@ -229,7 +229,12 @@ def initialize(connection, logger, connection_parameters, config)
@statements = {}
connect
@local_tz = execute('SHOW TIME ZONE').first["TimeZone"]
if postgresql_version < 80200
raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!"
end
@local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
end
def clear_cache!
@ -293,13 +298,13 @@ def supports_primary_key? #:nodoc:
# Enable standard-conforming strings if available.
def set_standard_conforming_strings
old, self.client_min_messages = client_min_messages, 'panic'
execute('SET standard_conforming_strings = on') rescue nil
execute('SET standard_conforming_strings = on', 'SCHEMA') rescue nil
ensure
self.client_min_messages = old
end
def supports_insert_with_returning?
postgresql_version >= 80200
true
end
def supports_ddl_transactions?
@ -310,10 +315,9 @@ def supports_savepoints?
true
end
# Returns the configured supported identifier length supported by PostgreSQL,
# or report the default of 63 on PostgreSQL 7.x.
# Returns the configured supported identifier length supported by PostgreSQL
def table_alias_length
@table_alias_length ||= (postgresql_version >= 80000 ? query('SHOW max_identifier_length')[0][0].to_i : 63)
@table_alias_length ||= query('SHOW max_identifier_length')[0][0].to_i
end
# QUOTING ==================================================
@ -404,7 +408,7 @@ def session_auth=(user)
# REFERENTIAL INTEGRITY ====================================
def supports_disable_referential_integrity?() #:nodoc:
postgresql_version >= 80100
true
end
def disable_referential_integrity #:nodoc:
@ -427,34 +431,16 @@ def select_rows(sql, name = nil)
end
# Executes an INSERT query and returns the new record's ID
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
# Extract the table from the insert sql. Yuck.
table = sql.split(" ", 4)[2].gsub('"', '')
_, table = extract_schema_and_table(sql.split(" ", 4)[2])
# Try an insert with 'returning id' if available (PG >= 8.2)
if supports_insert_with_returning?
pk, sequence_name = *pk_and_sequence_for(table) unless pk
if pk
id = select_value("#{sql} RETURNING #{quote_column_name(pk)}")
clear_query_cache
return id
end
end
pk ||= primary_key(table)
# Otherwise, insert then grab last_insert_id.
if insert_id = super
insert_id
if pk
select_value("#{sql} RETURNING #{quote_column_name(pk)}")
else
# If neither pk nor sequence name is given, look them up.
unless pk || sequence_name
pk, sequence_name = *pk_and_sequence_for(table)
end
# If a pk is given, fallback to default sequence name.
# Don't fetch last insert id for a table without a pk.
if pk && sequence_name ||= default_sequence_name(table, pk)
last_insert_id(sequence_name)
end
super
end
end
alias :create :insert
@ -554,6 +540,10 @@ def exec_query(sql, name = 'SQL', binds = [])
end
end
def exec_insert(sql, name, binds)
exec_query(sql, name, binds)
end
# Executes an UPDATE query and returns the number of affected tuples.
def update_sql(sql, name = nil)
super.cmd_tuples
@ -632,20 +622,12 @@ def create_database(name, options = {})
# Example:
# drop_database 'matt_development'
def drop_database(name) #:nodoc:
if postgresql_version >= 80200
execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
else
begin
execute "DROP DATABASE #{quote_table_name(name)}"
rescue ActiveRecord::StatementInvalid
@logger.warn "#{name} database doesn't exist." if @logger
end
end
execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
end
# Returns the list of all tables in the schema search path or a specified schema.
def tables(name = nil)
query(<<-SQL, name).map { |row| row[0] }
query(<<-SQL, 'SCHEMA').map { |row| row[0] }
SELECT tablename
FROM pg_tables
WHERE schemaname = ANY (current_schemas(false))
@ -653,7 +635,21 @@ def tables(name = nil)
end
def table_exists?(name)
name = name.to_s
schema, table = extract_schema_and_table(name.to_s)
binds = [[nil, table.gsub(/(^"|"$)/,'')]]
binds << [nil, schema] if schema
exec_query(<<-SQL, 'SCHEMA', binds).rows.first[0].to_i > 0
SELECT COUNT(*)
FROM pg_tables
WHERE tablename = $1
#{schema ? "AND schemaname = $2" : ''}
SQL
end
# Extracts the table and schema name from +name+
def extract_schema_and_table(name)
schema, table = name.split('.', 2)
unless table # A table was provided without a schema
@ -665,13 +661,7 @@ def table_exists?(name)
table = name
schema = nil
end
query(<<-SQL).first[0].to_i > 0
SELECT COUNT(*)
FROM pg_tables
WHERE tablename = '#{table.gsub(/(^"|"$)/,'')}'
#{schema ? "AND schemaname = '#{schema}'" : ''}
SQL
[schema, table]
end
# Returns the list of all indexes for a table.
@ -748,37 +738,47 @@ def schema_search_path
# Returns the current client message level.
def client_min_messages
query('SHOW client_min_messages')[0][0]
query('SHOW client_min_messages', 'SCHEMA')[0][0]
end
# Set the client message level.
def client_min_messages=(level)
execute("SET client_min_messages TO '#{level}'")
execute("SET client_min_messages TO '#{level}'", 'SCHEMA')
end
# Returns the sequence name for a table's primary key or some other specified key.
def default_sequence_name(table_name, pk = nil) #:nodoc:
default_pk, default_seq = pk_and_sequence_for(table_name)
default_seq || "#{table_name}_#{pk || default_pk || 'id'}_seq"
serial_sequence(table_name, pk || 'id').split('.').last
rescue ActiveRecord::StatementInvalid
"#{table_name}_#{pk || 'id'}_seq"
end
def serial_sequence(table, column)
result = exec_query(<<-eosql, 'SCHEMA', [[nil, table], [nil, column]])
SELECT pg_get_serial_sequence($1, $2)
eosql
result.rows.first.first
end
# Resets the sequence of a table's primary key to the maximum value.
def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
unless pk and sequence
default_pk, default_sequence = pk_and_sequence_for(table)
pk ||= default_pk
sequence ||= default_sequence
end
if pk
if sequence
quoted_sequence = quote_column_name(sequence)
select_value <<-end_sql, 'Reset sequence'
SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false)
end_sql
else
@logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger
end
if @logger && pk && !sequence
@logger.warn "#{table} has primary key #{pk} with no default sequence"
end
if pk && sequence
quoted_sequence = quote_column_name(sequence)
select_value <<-end_sql, 'Reset sequence'
SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false)
end_sql
end
end
@ -786,7 +786,7 @@ def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
def pk_and_sequence_for(table) #:nodoc:
# First try looking for a sequence with a dependency on the
# given table's primary key.
result = exec_query(<<-end_sql, 'PK and serial sequence').rows.first
result = exec_query(<<-end_sql, 'SCHEMA').rows.first
SELECT attr.attname, seq.relname
FROM pg_class seq,
pg_attribute attr,
@ -803,28 +803,6 @@ def pk_and_sequence_for(table) #:nodoc:
AND dep.refobjid = '#{quote_table_name(table)}'::regclass
end_sql
if result.nil? or result.empty?
# If that fails, try parsing the primary key's default value.
# Support the 7.x and 8.0 nextval('foo'::text) as well as
# the 8.1+ nextval('foo'::regclass).
result = query(<<-end_sql, 'PK and custom sequence')[0]
SELECT attr.attname,
CASE
WHEN split_part(def.adsrc, '''', 2) ~ '.' THEN
substr(split_part(def.adsrc, '''', 2),
strpos(split_part(def.adsrc, '''', 2), '.')+1)
ELSE split_part(def.adsrc, '''', 2)
END
FROM pg_class t
JOIN pg_attribute attr ON (t.oid = attrelid)
JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
WHERE t.oid = '#{quote_table_name(table)}'::regclass
AND cons.contype = 'p'
AND def.adsrc ~* 'nextval'
end_sql
end
# [primary_key, sequence]
[result.first, result.last]
rescue
@ -833,8 +811,21 @@ def pk_and_sequence_for(table) #:nodoc:
# Returns just a table's primary key
def primary_key(table)
pk_and_sequence = pk_and_sequence_for(table)
pk_and_sequence && pk_and_sequence.first
row = exec_query(<<-end_sql, 'SCHEMA', [[nil, table]]).rows.first
SELECT DISTINCT(attr.attname)
FROM pg_attribute attr,
pg_depend dep,
pg_namespace name,
pg_constraint cons
WHERE attr.attrelid = dep.refobjid
AND attr.attnum = dep.refobjsubid
AND attr.attrelid = cons.conrelid
AND attr.attnum = cons.conkey[1]
AND cons.contype = 'p'
AND dep.refobjid = $1::regclass
end_sql
row && row.first
end
# Renames a table.
@ -848,38 +839,14 @@ def add_column(table_name, column_name, type, options = {})
add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
add_column_options!(add_column_sql, options)
begin
execute add_column_sql
rescue ActiveRecord::StatementInvalid => e
raise e if postgresql_version > 80000
execute("ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}")
change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
end
execute add_column_sql
end
# Changes the column of a table.
def change_column(table_name, column_name, type, options = {})
quoted_table_name = quote_table_name(table_name)
begin
execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
rescue ActiveRecord::StatementInvalid => e
raise e if postgresql_version > 80000
# This is PostgreSQL 7.x, so we have to use a more arcane way of doing it.
begin
begin_db_transaction
tmp_column_name = "#{column_name}_ar_tmp"
add_column(table_name, tmp_column_name, type, options)
execute "UPDATE #{quoted_table_name} SET #{quote_column_name(tmp_column_name)} = CAST(#{quote_column_name(column_name)} AS #{type_to_sql(type, options[:limit], options[:precision], options[:scale])})"
remove_column(table_name, column_name)
rename_column(table_name, tmp_column_name, column_name)
commit_db_transaction
rescue
rollback_db_transaction
end
end
execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
@ -1031,9 +998,9 @@ def configure_connection
# If using Active Record's time zone support configure the connection to return
# TIMESTAMP WITH ZONE types in UTC.
if ActiveRecord::Base.default_timezone == :utc
execute("SET time zone 'UTC'")
execute("SET time zone 'UTC'", 'SCHEMA')
elsif @local_tz
execute("SET time zone '#{@local_tz}'")
execute("SET time zone '#{@local_tz}'", 'SCHEMA')
end
end
@ -1076,7 +1043,7 @@ def select_raw(sql, name = nil)
# - format_type includes the column size constraint, e.g. varchar(50)
# - ::regclass is a function that gives the id for a table name
def column_definitions(table_name) #:nodoc:
exec_query(<<-end_sql).rows
exec_query(<<-end_sql, 'SCHEMA').rows
SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
FROM pg_attribute a LEFT JOIN pg_attrdef d
ON a.attrelid = d.adrelid AND a.attnum = d.adnum

@ -173,6 +173,10 @@ def exec_query(sql, name = nil, binds = [])
end
end
def exec_insert(sql, name, binds)
exec_query(sql, name, binds)
end
def execute(sql, name = nil) #:nodoc:
log(sql, name) { @connection.execute(sql) }
end
@ -188,7 +192,8 @@ def delete_sql(sql, name = nil) #:nodoc:
end
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
super || @connection.last_insert_row_id
super
id_value || @connection.last_insert_row_id
end
alias :create :insert_sql

@ -173,10 +173,10 @@ class FixturesFileNotFound < StandardError; end
# traversed in the database to create the fixture hash and/or instance variables. This is expensive for
# large sets of fixtured data.
#
# = Dynamic fixtures with ERb
# = Dynamic fixtures with ERB
#
# Some times you don't care about the content of the fixtures as much as you care about the volume. In these cases, you can
# mix ERb in with your YAML or CSV fixtures to create a bunch of fixtures for load testing, like:
# mix ERB in with your YAML or CSV fixtures to create a bunch of fixtures for load testing, like:
#
# <% for i in 1..1000 %>
# fix_<%= i %>:
@ -186,7 +186,7 @@ class FixturesFileNotFound < StandardError; end
#
# This will create 1000 very simple YAML fixtures.
#
# Using ERb, you can also inject dynamic values into your fixtures with inserts like <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>.
# Using ERB, you can also inject dynamic values into your fixtures with inserts like <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>.
# This is however a feature to be used with some caution. The point of fixtures are that they're
# stable units of predictable sample data. If you feel that you need to inject dynamic values, then
# perhaps you should reexamine whether your application is properly testable. Hence, dynamic values

@ -50,7 +50,14 @@ def without
def get(klass, primary_key)
obj = repository[klass.symbolized_base_class][primary_key]
obj.is_a?(klass) ? obj : nil
if obj.is_a?(klass)
if ActiveRecord::Base.logger
ActiveRecord::Base.logger.debug "#{klass} with ID = #{primary_key} loaded from Identity Map"
end
obj
else
nil
end
end
def add(record)

@ -9,11 +9,6 @@ module ActiveRecord
module NamedScope
extend ActiveSupport::Concern
included do
class_attribute :scopes
self.scopes = {}
end
module ClassMethods
# Returns an anonymous \scope.
#
@ -35,7 +30,13 @@ def scoped(options = nil)
if options
scoped.apply_finder_options(options)
else
current_scoped_methods ? relation.merge(current_scoped_methods) : relation.clone
if current_scope
current_scope.clone
else
scope = relation.clone
scope.default_scoped = true
scope
end
end
end
@ -50,6 +51,14 @@ def scoped(options = nil)
# The above calls to <tt>scope</tt> define class methods Shirt.red and Shirt.dry_clean_only. Shirt.red,
# in effect, represents the query <tt>Shirt.where(:color => 'red')</tt>.
#
# Note that this is simply 'syntactic sugar' for defining an actual class method:
#
# class Shirt < ActiveRecord::Base
# def self.red
# where(:color => 'red')
# end
# end
#
# Unlike <tt>Shirt.find(...)</tt>, however, the object returned by Shirt.red is not an Array; it
# resembles the association object constructed by a <tt>has_many</tt> declaration. For instance,
# you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>, <tt>Shirt.red.where(:size => 'small')</tt>.
@ -73,14 +82,34 @@ def scoped(options = nil)
# then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
# only shirts.
#
# Named \scopes can also be procedural:
# If you need to pass parameters to a scope, define it as a normal method:
#
# class Shirt < ActiveRecord::Base
# scope :colored, lambda {|color| where(:color => color) }
# def self.colored(color)
# where(:color => color)
# end
# end
#
# In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
#
# Note that scopes defined with \scope will be evaluated when they are defined, rather than
# when they are used. For example, the following would be incorrect:
#
# class Post < ActiveRecord::Base
# scope :recent, where('published_at >= ?', Time.now - 1.week)
# end
#
# The example above would be 'frozen' to the <tt>Time.now</tt> value when the <tt>Post</tt>
# class was defined, and so the resultant SQL query would always be the same. The correct
# way to do this would be via a class method, which will re-evaluate the scope each time
# it is called:
#
# class Post < ActiveRecord::Base
# def self.recent
# where('published_at >= ?', Time.now - 1.week)
# end
# end
#
# Named \scopes can also have extensions, just as with <tt>has_many</tt> declarations:
#
# class Shirt < ActiveRecord::Base
@ -91,6 +120,18 @@ def scoped(options = nil)
# end
# end
#
# The above could also be written as a class method like so:
#
# class Shirt < ActiveRecord::Base
# def self.red
# where(:color => 'red').extending do
# def dom_id
# 'red_shirts'
# end
# end
# end
# end
#
# Scopes can also be used while creating/building a record.
#
# class Article < ActiveRecord::Base
@ -99,34 +140,68 @@ def scoped(options = nil)
#
# Article.published.new.published # => true
# Article.published.create.published # => true
#
# Class methods on your model are automatically available
# on scopes. Assuming the following setup:
#
# class Article < ActiveRecord::Base
# scope :published, where(:published => true)
# scope :featured, where(:featured => true)
#
# def self.latest_article
# order('published_at desc').first
# end
#
# def self.titles
# map(&:title)
# end
#
# end
#
# We are able to call the methods like this:
#
# Article.published.featured.latest_article
# Article.featured.titles
def scope(name, scope_options = {})
name = name.to_sym
valid_scope_name?(name)
extension = Module.new(&Proc.new) if block_given?
if !scope_options.is_a?(Relation) && scope_options.respond_to?(:call)
ActiveSupport::Deprecation.warn <<-WARN
Passing a proc (or other object that responds to #call) to scope is deprecated. If you need your scope to be lazily evaluated, or takes parameters, please define it as a normal class method instead. For example, change this:
class Post < ActiveRecord::Base
scope :unpublished, lambda { where('published_at > ?', Time.now) }
end
To this:
class Post < ActiveRecord::Base
def self.unpublished
where('published_at > ?', Time.now)
end
end
WARN
end
scope_proc = lambda do |*args|
options = scope_options.respond_to?(:call) ? scope_options.call(*args) : scope_options
options = scoped.apply_finder_options(options) if options.is_a?(Hash)
relation = if options.is_a?(Hash)
scoped.apply_finder_options(options)
elsif options
scoped.merge(options)
else
scoped
end
relation = scoped.merge(options)
extension ? relation.extending(extension) : relation
end
self.scopes = self.scopes.merge name => scope_proc
singleton_class.send(:redefine_method, name, &scopes[name])
singleton_class.send(:redefine_method, name, &scope_proc)
end
protected
def valid_scope_name?(name)
if !scopes[name] && respond_to?(name, true)
if respond_to?(name, true)
logger.warn "Creating scope :#{name}. " \
"Overwriting existing method #{self.name}.#{name}."
end

@ -403,12 +403,6 @@ def assign_nested_attributes_for_collection_association(association_name, attrib
unless reject_new_record?(association_name, attributes)
association.build(attributes.except(*UNASSIGNABLE_KEYS))
end
elsif existing_records.count == 0 #Existing record but not yet associated
existing_record = self.class.reflect_on_association(association_name).klass.find(attributes['id'])
if !call_reject_if(association_name, attributes)
association.send(:add_record_to_target_with_callbacks, existing_record) if !association.loaded?
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
end
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
unless association.loaded? || call_reject_if(association_name, attributes)
# Make sure we are operating on the actual object which is in the association's
@ -452,6 +446,7 @@ def reject_new_record?(association_name, attributes)
end
def call_reject_if(association_name, attributes)
return false if has_destroy_flag?(attributes)
case callback = self.nested_attributes_options[association_name][:reject_if]
when Symbol
method(callback).arity == 0 ? send(callback) : send(callback, attributes)

@ -1,3 +1,5 @@
require 'active_support/core_ext/object/inclusion'
db_namespace = namespace :db do
task :load_config => :rails_env do
require 'active_record'
@ -135,7 +137,7 @@ db_namespace = namespace :db do
end
def local_database?(config, &block)
if %w( 127.0.0.1 localhost ).include?(config['host']) || config['host'].blank?
if config['host'].among?("127.0.0.1", "localhost") || config['host'].blank?
yield
else
$stderr.puts "This task only modifies local databases. #{config['database']} is on a remote host."

@ -1,5 +1,6 @@
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/module/deprecation'
require 'active_support/core_ext/object/inclusion'
module ActiveRecord
# = Active Record Reflection
@ -163,7 +164,7 @@ def klass
def initialize(macro, name, options, active_record)
super
@collection = [:has_many, :has_and_belongs_to_many].include?(macro)
@collection = macro.among?(:has_many, :has_and_belongs_to_many)
end
# Returns a new, unsaved instance of the associated class. +options+ will

@ -6,7 +6,7 @@ class Relation
JoinOperation = Struct.new(:relation, :join_class, :on)
ASSOCIATION_METHODS = [:includes, :eager_load, :preload]
MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having, :bind]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from, :reorder]
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches
@ -15,14 +15,16 @@ class Relation
delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :to => :klass
attr_reader :table, :klass, :loaded
attr_accessor :extensions
attr_accessor :extensions, :default_scoped
alias :loaded? :loaded
alias :default_scoped? :default_scoped
def initialize(klass, table)
@klass, @table = klass, table
@implicit_readonly = nil
@loaded = false
@default_scoped = false
SINGLE_VALUE_METHODS.each {|v| instance_variable_set(:"@#{v}_value", nil)}
(ASSOCIATION_METHODS + MULTI_VALUE_METHODS).each {|v| instance_variable_set(:"@#{v}_values", [])}
@ -154,12 +156,7 @@ def many?
# Please check unscoped if you want to remove all previous scopes (including
# the default_scope) during the execution of a block.
def scoping
@klass.scoped_methods << self
begin
yield
ensure
@klass.scoped_methods.pop
end
@klass.send(:with_scope, self, :overwrite) { yield }
end
# Updates all records with details given if they match a set of conditions supplied, limits and order can
@ -194,6 +191,7 @@ def scoping
# # The same idea applies to limit and order
# Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(:author => 'David')
def update_all(updates, conditions = nil, options = {})
IdentityMap.repository[symbolized_base_class].clear if IdentityMap.enabled?
if conditions || options.present?
where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates)
else
@ -322,6 +320,7 @@ def destroy(id)
# If you need to destroy dependent associations or call your <tt>before_*</tt> or
# +after_destroy+ callbacks, use the +destroy_all+ method instead.
def delete_all(conditions = nil)
IdentityMap.repository[symbolized_base_class] = {} if IdentityMap.enabled?
if conditions
where(conditions).delete_all
else
@ -353,6 +352,7 @@ def delete_all(conditions = nil)
# # Delete multiple rows
# Todo.delete([2,3,4])
def delete(id_or_array)
IdentityMap.remove_by_id(self.symbolized_base_class, id_or_array) if IdentityMap.enabled?
where(primary_key => id_or_array).delete_all
end
@ -374,7 +374,7 @@ def to_sql
end
def where_values_hash
equalities = @where_values.grep(Arel::Nodes::Equality).find_all { |node|
equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node|
node.left.relation.name == table_name
}
@ -402,13 +402,20 @@ def inspect
to_a.inspect
end
def with_default_scope #:nodoc:
if default_scoped?
default_scope = @klass.send(:build_default_scope)
default_scope ? default_scope.merge(self) : self
else
self
end
end
protected
def method_missing(method, *args, &block)
if Array.method_defined?(method)
to_a.send(method, *args, &block)
elsif @klass.scopes[method]
merge(@klass.send(method, *args, &block))
elsif @klass.respond_to?(method)
scoping { @klass.send(method, *args, &block) }
elsif arel.respond_to?(method)

@ -309,6 +309,15 @@ def find_with_ids(*ids)
def find_one(id)
id = id.id if ActiveRecord::Base === id
if IdentityMap.enabled? && where_values.blank? &&
limit_value.blank? && order_values.blank? &&
includes_values.blank? && preload_values.blank? &&
readonly_value.nil? && joins_values.blank? &&
!@klass.locking_enabled? &&
record = IdentityMap.get(@klass, id)
return record
end
column = columns_hash[primary_key]
substitute = connection.substitute_for(column, @bind_values)
@ -343,8 +352,8 @@ def find_some(ids)
if result.size == expected_size
result
else
conditions = arel.wheres.map { |x| x.value }.join(', ')
conditions = " [WHERE #{conditions}]" if conditions.present?
conditions = arel.where_sql
conditions = " [#{conditions}]" if conditions
error = "Couldn't find all #{@klass.name.pluralize} with IDs "
error << "(#{ids.join(", ")})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"

@ -8,7 +8,8 @@ module QueryMethods
attr_accessor :includes_values, :eager_load_values, :preload_values,
:select_values, :group_values, :order_values, :joins_values,
:where_values, :having_values, :bind_values,
:limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value, :from_value
:limit_value, :offset_value, :lock_value, :readonly_value, :create_with_value,
:from_value, :reorder_value
def includes(*args)
args.reject! {|a| a.blank? }
@ -63,7 +64,11 @@ def order(*args)
end
def reorder(*args)
except(:order).order(args)
return self if args.blank?
relation = clone
relation.reorder_value = args.flatten
relation
end
def joins(*args)
@ -163,7 +168,7 @@ def reverse_order
end
def arel
@arel ||= build_arel
@arel ||= with_default_scope.build_arel
end
def build_arel
@ -180,7 +185,8 @@ def build_arel
arel.group(*@group_values.uniq.reject{|g| g.blank?}) unless @group_values.empty?
arel.order(*@order_values.uniq.reject{|o| o.blank?}) unless @order_values.empty?
order = @reorder_value ? @reorder_value : @order_values
arel.order(*order.uniq.reject{|o| o.blank?}) unless order.empty?
build_select(arel, @select_values.uniq)

@ -8,6 +8,8 @@ def merge(r)
merged_relation = clone
r = r.with_default_scope if r.default_scoped? && r.klass != klass
Relation::ASSOCIATION_METHODS.each do |method|
value = r.send(:"#{method}_values")
@ -70,6 +72,7 @@ def merge(r)
#
def except(*skips)
result = self.class.new(@klass, table)
result.default_scoped = default_scoped
((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) - skips).each do |method|
result.send(:"#{method}_values=", send(:"#{method}_values"))
@ -94,6 +97,7 @@ def except(*skips)
#
def only(*onlies)
result = self.class.new(@klass, table)
result.default_scoped = default_scoped
((Relation::ASSOCIATION_METHODS + Relation::MULTI_VALUE_METHODS) & onlies).each do |method|
result.send(:"#{method}_values=", send(:"#{method}_values"))

@ -1,9 +1,9 @@
module ActiveRecord
###
# This class encapsulates a Result returned from calling +exec+ on any
# This class encapsulates a Result returned from calling +exec_query+ on any
# database connection adapter. For example:
#
# x = ActiveRecord::Base.connection.exec('SELECT * FROM foo')
# x = ActiveRecord::Base.connection.exec_query('SELECT * FROM foo')
# x # => #<ActiveRecord::Result:0xdeadbeef>
class Result
include Enumerable

@ -270,6 +270,7 @@ def committed! #:nodoc:
def rolledback!(force_restore_state = false) #:nodoc:
run_callbacks :rollback
ensure
IdentityMap.remove(self) if IdentityMap.enabled?
restore_transaction_record_state(force_restore_state)
end

@ -14,6 +14,7 @@ def setup(klass)
def validate_each(record, attribute, value)
finder_class = find_finder_class_for(record)
table = finder_class.arel_table
coder = record.class.serialized_attributes[attribute.to_s]
@ -21,21 +22,15 @@ def validate_each(record, attribute, value)
value = coder.dump value
end
sql, params = mount_sql_and_params(finder_class, record.class.quoted_table_name, attribute, value)
relation = finder_class.unscoped.where(sql, *params)
relation = build_relation(finder_class, table, attribute, value)
relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.send(:id))) if record.persisted?
Array.wrap(options[:scope]).each do |scope_item|
scope_value = record.send(scope_item)
relation = relation.where(scope_item => scope_value)
relation = relation.and(table[scope_item].eq(scope_value))
end
if record.persisted?
# TODO : This should be in Arel
relation = relation.where("#{record.class.quoted_table_name}.#{record.class.primary_key} <> ?", record.send(:id))
end
if relation.exists?
if finder_class.unscoped.where(relation).exists?
record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value))
end
end
@ -57,27 +52,18 @@ def find_finder_class_for(record) #:nodoc:
class_hierarchy.detect { |klass| !klass.abstract_class? }
end
def mount_sql_and_params(klass, table_name, attribute, value) #:nodoc:
def build_relation(klass, table, attribute, value) #:nodoc:
column = klass.columns_hash[attribute.to_s]
value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s if column.text?
operator = if value.nil?
"IS ?"
elsif column.text?
value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s
"#{klass.connection.case_sensitive_equality_operator} ?"
if !options[:case_sensitive] && column.text?
relation = table[attribute].matches(value)
else
"= ?"
value = klass.connection.case_sensitive_modifier(value)
relation = table[attribute].eq(value)
end
sql_attribute = "#{table_name}.#{klass.connection.quote_column_name(attribute)}"
if value.nil? || (options[:case_sensitive] || !column.text?)
sql = "#{sql_attribute} #{operator}"
else
sql = "LOWER(#{sql_attribute}) = LOWER(?)"
end
[sql, [value]]
relation
end
end

@ -1,4 +1,5 @@
require 'rails/generators/active_record'
require 'active_support/core_ext/object/inclusion'
module ActiveRecord
module Generators
@ -13,7 +14,7 @@ def create_migration_file
def session_table_name
current_table_name = ActiveRecord::SessionStore::Session.table_name
if ["sessions", "session"].include?(current_table_name)
if current_table_name.among?("sessions", "session")
current_table_name = (ActiveRecord::Base.pluralize_table_names ? 'session'.pluralize : 'session')
end
current_table_name

@ -0,0 +1,69 @@
# encoding: utf-8
require "cases/helper"
module ActiveRecord
module ConnectionAdapters
class MysqlAdapterTest < ActiveRecord::TestCase
def setup
@conn = ActiveRecord::Base.connection
@conn.exec_query('drop table if exists ex')
@conn.exec_query(<<-eosql)
CREATE TABLE `ex` (
`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY,
`number` integer,
`data` varchar(255))
eosql
end
def test_client_encoding
if "<3".respond_to?(:encoding)
assert_equal Encoding::UTF_8, @conn.client_encoding
else
assert_equal 'utf8', @conn.client_encoding
end
end
def test_exec_insert_number
insert(@conn, 'number' => 10)
result = @conn.exec_query('SELECT number FROM ex WHERE number = 10')
assert_equal 1, result.rows.length
assert_equal 10, result.rows.last.last
end
def test_exec_insert_string
str = 'いただきます!'
insert(@conn, 'number' => 10, 'data' => str)
result = @conn.exec_query('SELECT number, data FROM ex WHERE number = 10')
value = result.rows.last.last
if "<3".respond_to?(:encoding)
# FIXME: this should probably be inside the mysql AR adapter?
value.force_encoding(@conn.client_encoding)
# The strings in this file are utf-8, so transcode to utf-8
value.encode!(Encoding::UTF_8)
end
assert_equal str, value
end
private
def insert(ctx, data)
binds = data.map { |name, value|
[ctx.columns('ex').find { |x| x.name == name }, value]
}
columns = binds.map(&:first).map(&:name)
sql = "INSERT INTO ex (#{columns.join(", ")})
VALUES (#{(['?'] * columns.length).join(', ')})"
ctx.exec_insert(sql, 'SQL', binds)
end
end
end
end

@ -1,3 +1,4 @@
# encoding: utf-8
require "cases/helper"
module ActiveRecord
@ -6,7 +7,52 @@ class PostgreSQLAdapterTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
@connection.exec_query('drop table if exists ex')
@connection.exec_query('create table ex(id serial primary key, data character varying(255))')
@connection.exec_query('create table ex(id serial primary key, number integer, data character varying(255))')
end
def test_serial_sequence
assert_equal 'public.accounts_id_seq',
@connection.serial_sequence('accounts', 'id')
assert_raises(ActiveRecord::StatementInvalid) do
@connection.serial_sequence('zomg', 'id')
end
end
def test_default_sequence_name
assert_equal 'accounts_id_seq',
@connection.default_sequence_name('accounts', 'id')
assert_equal 'accounts_id_seq',
@connection.default_sequence_name('accounts')
end
def test_default_sequence_name_bad_table
assert_equal 'zomg_id_seq',
@connection.default_sequence_name('zomg', 'id')
assert_equal 'zomg_id_seq',
@connection.default_sequence_name('zomg')
end
def test_exec_insert_number
insert(@connection, 'number' => 10)
result = @connection.exec_query('SELECT number FROM ex WHERE number = 10')
assert_equal 1, result.rows.length
assert_equal "10", result.rows.last.last
end
def test_exec_insert_string
str = 'いただきます!'
insert(@connection, 'number' => 10, 'data' => str)
result = @connection.exec_query('SELECT number, data FROM ex WHERE number = 10')
value = result.rows.last.last
assert_equal str, value
end
def test_table_alias_length
@ -63,6 +109,21 @@ def test_substitute_for
bind = @connection.substitute_for(nil, [nil])
assert_equal Arel.sql('$2'), bind
end
private
def insert(ctx, data)
binds = data.map { |name, value|
[ctx.columns('ex').find { |x| x.name == name }, value]
}
columns = binds.map(&:first).map(&:name)
bind_subs = columns.length.times.map { |x| "$#{x + 1}" }
sql = "INSERT INTO ex (#{columns.join(", ")})
VALUES (#{bind_subs.join(', ')})"
ctx.exec_insert(sql, 'SQL', binds)
end
end
end
end

@ -1,229 +0,0 @@
# encoding: utf-8
require "cases/helper"
require 'models/binary'
module ActiveRecord
module ConnectionAdapters
class SQLiteAdapterTest < ActiveRecord::TestCase
class DualEncoding < ActiveRecord::Base
end
def setup
@ctx = Base.sqlite3_connection :database => ':memory:',
:adapter => 'sqlite3',
:timeout => nil
@ctx.execute <<-eosql
CREATE TABLE items (
id integer PRIMARY KEY AUTOINCREMENT,
number integer
)
eosql
end
def test_quote_binary_column_escapes_it
DualEncoding.connection.execute(<<-eosql)
CREATE TABLE dual_encodings (
id integer PRIMARY KEY AUTOINCREMENT,
name string,
data binary
)
eosql
str = "\x80".force_encoding("ASCII-8BIT")
binary = DualEncoding.new :name => 'いただきます!', :data => str
binary.save!
assert_equal str, binary.data
end
def test_execute
@ctx.execute "INSERT INTO items (number) VALUES (10)"
records = @ctx.execute "SELECT * FROM items"
assert_equal 1, records.length
record = records.first
assert_equal 10, record['number']
assert_equal 1, record['id']
end
def test_quote_string
assert_equal "''", @ctx.quote_string("'")
end
def test_insert_sql
2.times do |i|
rv = @ctx.insert_sql "INSERT INTO items (number) VALUES (#{i})"
assert_equal(i + 1, rv)
end
records = @ctx.execute "SELECT * FROM items"
assert_equal 2, records.length
end
def test_insert_sql_logged
sql = "INSERT INTO items (number) VALUES (10)"
name = "foo"
assert_logged([[sql, name]]) do
@ctx.insert_sql sql, name
end
end
def test_insert_id_value_returned
sql = "INSERT INTO items (number) VALUES (10)"
idval = 'vuvuzela'
id = @ctx.insert_sql sql, nil, nil, idval
assert_equal idval, id
end
def test_select_rows
2.times do |i|
@ctx.create "INSERT INTO items (number) VALUES (#{i})"
end
rows = @ctx.select_rows 'select number, id from items'
assert_equal [[0, 1], [1, 2]], rows
end
def test_select_rows_logged
sql = "select * from items"
name = "foo"
assert_logged([[sql, name]]) do
@ctx.select_rows sql, name
end
end
def test_transaction
count_sql = 'select count(*) from items'
@ctx.begin_db_transaction
@ctx.create "INSERT INTO items (number) VALUES (10)"
assert_equal 1, @ctx.select_rows(count_sql).first.first
@ctx.rollback_db_transaction
assert_equal 0, @ctx.select_rows(count_sql).first.first
end
def test_tables
assert_equal %w{ items }, @ctx.tables
@ctx.execute <<-eosql
CREATE TABLE people (
id integer PRIMARY KEY AUTOINCREMENT,
number integer
)
eosql
assert_equal %w{ items people }.sort, @ctx.tables.sort
end
def test_tables_logs_name
name = "hello"
assert_logged [[name]] do
@ctx.tables(name)
assert_not_nil @ctx.logged.first.shift
end
end
def test_columns
columns = @ctx.columns('items').sort_by { |x| x.name }
assert_equal 2, columns.length
assert_equal %w{ id number }.sort, columns.map { |x| x.name }
assert_equal [nil, nil], columns.map { |x| x.default }
assert_equal [true, true], columns.map { |x| x.null }
end
def test_columns_with_default
@ctx.execute <<-eosql
CREATE TABLE columns_with_default (
id integer PRIMARY KEY AUTOINCREMENT,
number integer default 10
)
eosql
column = @ctx.columns('columns_with_default').find { |x|
x.name == 'number'
}
assert_equal 10, column.default
end
def test_columns_with_not_null
@ctx.execute <<-eosql
CREATE TABLE columns_with_default (
id integer PRIMARY KEY AUTOINCREMENT,
number integer not null
)
eosql
column = @ctx.columns('columns_with_default').find { |x|
x.name == 'number'
}
assert !column.null, "column should not be null"
end
def test_indexes_logs
intercept_logs_on @ctx
assert_difference('@ctx.logged.length') do
@ctx.indexes('items')
end
assert_match(/items/, @ctx.logged.last.first)
end
def test_no_indexes
assert_equal [], @ctx.indexes('items')
end
def test_index
@ctx.add_index 'items', 'id', :unique => true, :name => 'fun'
index = @ctx.indexes('items').find { |idx| idx.name == 'fun' }
assert_equal 'items', index.table
assert index.unique, 'index is unique'
assert_equal ['id'], index.columns
end
def test_non_unique_index
@ctx.add_index 'items', 'id', :name => 'fun'
index = @ctx.indexes('items').find { |idx| idx.name == 'fun' }
assert !index.unique, 'index is not unique'
end
def test_compound_index
@ctx.add_index 'items', %w{ id number }, :name => 'fun'
index = @ctx.indexes('items').find { |idx| idx.name == 'fun' }
assert_equal %w{ id number }.sort, index.columns.sort
end
def test_primary_key
assert_equal 'id', @ctx.primary_key('items')
@ctx.execute <<-eosql
CREATE TABLE foos (
internet integer PRIMARY KEY AUTOINCREMENT,
number integer not null
)
eosql
assert_equal 'internet', @ctx.primary_key('foos')
end
def test_no_primary_key
@ctx.execute 'CREATE TABLE failboat (number integer not null)'
assert_nil @ctx.primary_key('failboat')
end
private
def assert_logged logs
intercept_logs_on @ctx
yield
assert_equal logs, @ctx.logged
end
def intercept_logs_on ctx
@ctx.extend(Module.new {
attr_accessor :logged
def log sql, name
@logged << [sql, name]
yield
end
})
@ctx.logged = []
end
end
end
end

@ -1,12 +1,34 @@
# encoding: utf-8
require "cases/helper"
module ActiveRecord
module ConnectionAdapters
class SQLite3AdapterTest < ActiveRecord::TestCase
class DualEncoding < ActiveRecord::Base
end
def setup
@conn = Base.sqlite3_connection :database => ':memory:',
:adapter => 'sqlite3',
:timeout => 100
@conn.execute <<-eosql
CREATE TABLE items (
id integer PRIMARY KEY AUTOINCREMENT,
number integer
)
eosql
end
def test_exec_insert
column = @conn.columns('items').find { |col| col.name == 'number' }
vals = [[column, 10]]
@conn.exec_insert('insert into items (number) VALUES (?)', 'SQL', vals)
result = @conn.exec_query(
'select number from items where number = ?', 'SQL', vals)
assert_equal 1, result.rows.length
assert_equal 10, result.rows.first.first
end
def test_primary_key_returns_nil_for_no_pk
@ -102,6 +124,213 @@ def test_exec_query_typecasts_bind_vals
assert_equal [[1, 'foo']], result.rows
end
def test_quote_binary_column_escapes_it
return unless "<3".respond_to?(:encode)
DualEncoding.connection.execute(<<-eosql)
CREATE TABLE dual_encodings (
id integer PRIMARY KEY AUTOINCREMENT,
name string,
data binary
)
eosql
str = "\x80".force_encoding("ASCII-8BIT")
binary = DualEncoding.new :name => 'いただきます!', :data => str
binary.save!
assert_equal str, binary.data
end
def test_execute
@conn.execute "INSERT INTO items (number) VALUES (10)"
records = @conn.execute "SELECT * FROM items"
assert_equal 1, records.length
record = records.first
assert_equal 10, record['number']
assert_equal 1, record['id']
end
def test_quote_string
assert_equal "''", @conn.quote_string("'")
end
def test_insert_sql
2.times do |i|
rv = @conn.insert_sql "INSERT INTO items (number) VALUES (#{i})"
assert_equal(i + 1, rv)
end
records = @conn.execute "SELECT * FROM items"
assert_equal 2, records.length
end
def test_insert_sql_logged
sql = "INSERT INTO items (number) VALUES (10)"
name = "foo"
assert_logged([[sql, name, []]]) do
@conn.insert_sql sql, name
end
end
def test_insert_id_value_returned
sql = "INSERT INTO items (number) VALUES (10)"
idval = 'vuvuzela'
id = @conn.insert_sql sql, nil, nil, idval
assert_equal idval, id
end
def test_select_rows
2.times do |i|
@conn.create "INSERT INTO items (number) VALUES (#{i})"
end
rows = @conn.select_rows 'select number, id from items'
assert_equal [[0, 1], [1, 2]], rows
end
def test_select_rows_logged
sql = "select * from items"
name = "foo"
assert_logged([[sql, name, []]]) do
@conn.select_rows sql, name
end
end
def test_transaction
count_sql = 'select count(*) from items'
@conn.begin_db_transaction
@conn.create "INSERT INTO items (number) VALUES (10)"
assert_equal 1, @conn.select_rows(count_sql).first.first
@conn.rollback_db_transaction
assert_equal 0, @conn.select_rows(count_sql).first.first
end
def test_tables
assert_equal %w{ items }, @conn.tables
@conn.execute <<-eosql
CREATE TABLE people (
id integer PRIMARY KEY AUTOINCREMENT,
number integer
)
eosql
assert_equal %w{ items people }.sort, @conn.tables.sort
end
def test_tables_logs_name
name = "hello"
assert_logged [[name, []]] do
@conn.tables(name)
assert_not_nil @conn.logged.first.shift
end
end
def test_columns
columns = @conn.columns('items').sort_by { |x| x.name }
assert_equal 2, columns.length
assert_equal %w{ id number }.sort, columns.map { |x| x.name }
assert_equal [nil, nil], columns.map { |x| x.default }
assert_equal [true, true], columns.map { |x| x.null }
end
def test_columns_with_default
@conn.execute <<-eosql
CREATE TABLE columns_with_default (
id integer PRIMARY KEY AUTOINCREMENT,
number integer default 10
)
eosql
column = @conn.columns('columns_with_default').find { |x|
x.name == 'number'
}
assert_equal 10, column.default
end
def test_columns_with_not_null
@conn.execute <<-eosql
CREATE TABLE columns_with_default (
id integer PRIMARY KEY AUTOINCREMENT,
number integer not null
)
eosql
column = @conn.columns('columns_with_default').find { |x|
x.name == 'number'
}
assert !column.null, "column should not be null"
end
def test_indexes_logs
intercept_logs_on @conn
assert_difference('@conn.logged.length') do
@conn.indexes('items')
end
assert_match(/items/, @conn.logged.last.first)
end
def test_no_indexes
assert_equal [], @conn.indexes('items')
end
def test_index
@conn.add_index 'items', 'id', :unique => true, :name => 'fun'
index = @conn.indexes('items').find { |idx| idx.name == 'fun' }
assert_equal 'items', index.table
assert index.unique, 'index is unique'
assert_equal ['id'], index.columns
end
def test_non_unique_index
@conn.add_index 'items', 'id', :name => 'fun'
index = @conn.indexes('items').find { |idx| idx.name == 'fun' }
assert !index.unique, 'index is not unique'
end
def test_compound_index
@conn.add_index 'items', %w{ id number }, :name => 'fun'
index = @conn.indexes('items').find { |idx| idx.name == 'fun' }
assert_equal %w{ id number }.sort, index.columns.sort
end
def test_primary_key
assert_equal 'id', @conn.primary_key('items')
@conn.execute <<-eosql
CREATE TABLE foos (
internet integer PRIMARY KEY AUTOINCREMENT,
number integer not null
)
eosql
assert_equal 'internet', @conn.primary_key('foos')
end
def test_no_primary_key
@conn.execute 'CREATE TABLE failboat (number integer not null)'
assert_nil @conn.primary_key('failboat')
end
private
def assert_logged logs
intercept_logs_on @conn
yield
assert_equal logs, @conn.logged
end
def intercept_logs_on ctx
@conn.extend(Module.new {
attr_accessor :logged
def log sql, name, binds = []
@logged << [sql, name, binds]
yield
end
})
@conn.logged = []
end
end
end
end

@ -70,16 +70,16 @@ def test_create_from_association_should_respect_default_scope
# would be convenient), because this would cause that scope to be applied to any callbacks etc.
def test_build_and_create_should_not_happen_within_scope
car = cars(:honda)
original_scoped_methods = Bulb.scoped_methods
scoped_count = car.foo_bulbs.scoped.where_values.count
bulb = car.bulbs.build
assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize
bulb = car.foo_bulbs.build
assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
bulb = car.bulbs.create
assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize
bulb = car.foo_bulbs.create
assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
bulb = car.bulbs.create!
assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize
bulb = car.foo_bulbs.create!
assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
end
def test_no_sql_should_be_fired_if_association_already_loaded

@ -165,16 +165,16 @@ def test_successful_build_association
def test_build_and_create_should_not_happen_within_scope
pirate = pirates(:blackbeard)
original_scoped_methods = Bulb.scoped_methods.dup
scoped_count = pirate.association(:foo_bulb).scoped.where_values.count
bulb = pirate.build_bulb
assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize
bulb = pirate.build_foo_bulb
assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
bulb = pirate.create_bulb
assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize
bulb = pirate.create_foo_bulb
assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
bulb = pirate.create_bulb!
assert_equal original_scoped_methods, bulb.scoped_methods_after_initialize
bulb = pirate.create_foo_bulb!
assert_not_equal scoped_count, bulb.scope_after_initialize.where_values.count
end
def test_create_association

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