Merge branch 'master' into sprockets
This commit is contained in:
commit
ed24595647
@ -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 = '-'
|
||||
|
||||
|
90
actionpack/test/controller/flash_hash_test.rb
Normal file
90
actionpack/test/controller/flash_hash_test.rb
Normal file
@ -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<=?"/>
|
||||
METAS
|
||||
assert_select 'meta[name=?][content=?]', 'csrf-param', 'authenticity_token'
|
||||
assert_select 'meta[name=?][content=?]', 'csrf-token', 'cf50faa3fe97702ca1ae<=?'
|
||||
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
|
||||
|
1
actionpack/test/fixtures/respond_with/using_invalid_resource_with_template.xml.erb
vendored
Normal file
1
actionpack/test/fixtures/respond_with/using_invalid_resource_with_template.xml.erb
vendored
Normal file
@ -0,0 +1 @@
|
||||
<content>I should not be displayed</content>
|
1
actionpack/test/fixtures/respond_with/using_options_with_template.xml.erb
vendored
Normal file
1
actionpack/test/fixtures/respond_with/using_options_with_template.xml.erb
vendored
Normal file
@ -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="✓" />}
|
||||
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=\"<Abe>\"><Abe> 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="✓" />}
|
||||
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
|
||||
|
69
activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
Normal file
69
activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
Normal file
@ -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
Loading…
Reference in New Issue
Block a user