mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Merge branch 'master' of git://github.com/rails/rails
This commit is contained in:
commit
8692b11e81
42 changed files with 555 additions and 546 deletions
0
.gitmodules
vendored
Normal file
0
.gitmodules
vendored
Normal file
|
@ -1,118 +0,0 @@
|
|||
# Pass NEW=1 to run with the new Base
|
||||
ENV['RAILS_ENV'] ||= 'production'
|
||||
ENV['NO_RELOAD'] ||= '1'
|
||||
|
||||
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
|
||||
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../../activesupport/lib"
|
||||
require 'action_controller'
|
||||
require 'action_controller/new_base' if ENV['NEW']
|
||||
require 'action_view'
|
||||
require 'benchmark'
|
||||
|
||||
class Runner
|
||||
def initialize(app, output)
|
||||
@app, @output = app, output
|
||||
end
|
||||
|
||||
def puts(*)
|
||||
super if @output
|
||||
end
|
||||
|
||||
def call(env)
|
||||
env['n'].to_i.times { @app.call(env) }
|
||||
@app.call(env).tap { |response| report(env, response) }
|
||||
end
|
||||
|
||||
def report(env, response)
|
||||
return unless ENV["DEBUG"]
|
||||
out = env['rack.errors']
|
||||
out.puts response[0], response[1].to_yaml, '---'
|
||||
response[2].each { |part| out.puts part }
|
||||
out.puts '---'
|
||||
end
|
||||
|
||||
def self.puts(*)
|
||||
super if @output
|
||||
end
|
||||
|
||||
def self.run(app, n, label, output = true)
|
||||
@output = output
|
||||
puts label, '=' * label.size if label
|
||||
env = Rack::MockRequest.env_for("/").merge('n' => n, 'rack.input' => StringIO.new(''), 'rack.errors' => $stdout)
|
||||
t = Benchmark.realtime { new(app, output).call(env) }
|
||||
puts "%d ms / %d req = %.1f usec/req" % [10**3 * t, n, 10**6 * t / n]
|
||||
puts
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
N = (ENV['N'] || 1000).to_i
|
||||
|
||||
module ActionController::Rails2Compatibility
|
||||
instance_methods.each do |name|
|
||||
remove_method name
|
||||
end
|
||||
end
|
||||
|
||||
class BasePostController < ActionController::Base
|
||||
append_view_path "#{File.dirname(__FILE__)}/views"
|
||||
|
||||
def overhead
|
||||
self.response_body = ''
|
||||
end
|
||||
|
||||
def index
|
||||
render :text => ''
|
||||
end
|
||||
|
||||
def partial
|
||||
render :partial => "/partial"
|
||||
end
|
||||
|
||||
def many_partials
|
||||
render :partial => "/many_partials"
|
||||
end
|
||||
|
||||
def partial_collection
|
||||
render :partial => "/collection", :collection => [1,2,3,4,5,6,7,8,9,10]
|
||||
end
|
||||
|
||||
def show_template
|
||||
render :template => "template"
|
||||
end
|
||||
end
|
||||
|
||||
OK = [200, {}, []]
|
||||
MetalPostController = lambda { OK }
|
||||
|
||||
class HttpPostController < ActionController::Metal
|
||||
def index
|
||||
self.response_body = ''
|
||||
end
|
||||
end
|
||||
|
||||
unless ENV["PROFILE"]
|
||||
Runner.run(BasePostController.action(:overhead), N, 'overhead', false)
|
||||
Runner.run(BasePostController.action(:index), N, 'index', false)
|
||||
Runner.run(BasePostController.action(:partial), N, 'partial', false)
|
||||
Runner.run(BasePostController.action(:many_partials), N, 'many_partials', false)
|
||||
Runner.run(BasePostController.action(:partial_collection), N, 'collection', false)
|
||||
Runner.run(BasePostController.action(:show_template), N, 'template', false)
|
||||
|
||||
(ENV["M"] || 1).to_i.times do
|
||||
Runner.run(BasePostController.action(:overhead), N, 'overhead')
|
||||
Runner.run(BasePostController.action(:index), N, 'index')
|
||||
Runner.run(BasePostController.action(:partial), N, 'partial')
|
||||
Runner.run(BasePostController.action(:many_partials), N, 'many_partials')
|
||||
Runner.run(BasePostController.action(:partial_collection), N, 'collection')
|
||||
Runner.run(BasePostController.action(:show_template), N, 'template')
|
||||
end
|
||||
else
|
||||
Runner.run(BasePostController.action(:many_partials), N, 'many_partials')
|
||||
require "ruby-prof"
|
||||
RubyProf.start
|
||||
Runner.run(BasePostController.action(:many_partials), N, 'many_partials')
|
||||
result = RubyProf.stop
|
||||
printer = RubyProf::CallStackPrinter.new(result)
|
||||
printer.print(File.open("output.html", "w"))
|
||||
end
|
|
@ -1,50 +0,0 @@
|
|||
$:.push "rails/activesupport/lib"
|
||||
$:.push "rails/actionpack/lib"
|
||||
|
||||
require "action_controller"
|
||||
|
||||
class Kaigi < ActionController::Metal
|
||||
include AbstractController::Callbacks
|
||||
include ActionController::RackConvenience
|
||||
include ActionController::RenderingController
|
||||
include ActionController::Layouts
|
||||
include ActionView::Context
|
||||
|
||||
before_filter :set_name
|
||||
append_view_path "views"
|
||||
|
||||
def view_context
|
||||
self
|
||||
end
|
||||
|
||||
def controller
|
||||
self
|
||||
end
|
||||
|
||||
DEFAULT_LAYOUT = Object.new.tap {|l| def l.render(*) yield end }
|
||||
|
||||
def render_template(template, layout = DEFAULT_LAYOUT, options = {}, partial = false)
|
||||
ret = template.render(self, {})
|
||||
layout.render(self, {}) { ret }
|
||||
end
|
||||
|
||||
def index
|
||||
render :template => "template"
|
||||
end
|
||||
|
||||
def alt
|
||||
render :template => "template", :layout => "alt"
|
||||
end
|
||||
|
||||
private
|
||||
def set_name
|
||||
@name = params[:name]
|
||||
end
|
||||
end
|
||||
|
||||
app = Rack::Builder.new do
|
||||
map("/kaigi") { run Kaigi.action(:index) }
|
||||
map("/kaigi/alt") { run Kaigi.action(:alt) }
|
||||
end.to_app
|
||||
|
||||
Rack::Handler::Mongrel.run app, :Port => 3000
|
|
@ -1 +0,0 @@
|
|||
<%= collection %>
|
|
@ -1 +0,0 @@
|
|||
Hello
|
|
@ -1,10 +0,0 @@
|
|||
<%= render :partial => '/hello' %>
|
||||
<%= render :partial => '/hello' %>
|
||||
<%= render :partial => '/hello' %>
|
||||
<%= render :partial => '/hello' %>
|
||||
<%= render :partial => '/hello' %>
|
||||
<%= render :partial => '/hello' %>
|
||||
<%= render :partial => '/hello' %>
|
||||
<%= render :partial => '/hello' %>
|
||||
<%= render :partial => '/hello' %>
|
||||
<%= render :partial => '/hello' %>
|
|
@ -1,10 +0,0 @@
|
|||
<%= "Hello" %>
|
||||
<%= "Hello" %>
|
||||
<%= "Hello" %>
|
||||
<%= "Hello" %>
|
||||
<%= "Hello" %>
|
||||
<%= "Hello" %>
|
||||
<%= "Hello" %>
|
||||
<%= "Hello" %>
|
||||
<%= "Hello" %>
|
||||
<%= "Hello" %>
|
|
@ -1 +0,0 @@
|
|||
+ <%= yield %> +
|
|
@ -1 +0,0 @@
|
|||
Hello <%= yield %> Goodbye
|
|
@ -1 +0,0 @@
|
|||
Hello
|
|
@ -6,13 +6,51 @@ module AbstractController
|
|||
|
||||
included do
|
||||
extlib_inheritable_accessor(:_layout_conditions) { Hash.new }
|
||||
extlib_inheritable_accessor(:_action_has_layout) { Hash.new }
|
||||
_write_layout_method
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def inherited(klass)
|
||||
super
|
||||
klass._write_layout_method
|
||||
klass.class_eval do
|
||||
_write_layout_method
|
||||
@found_layouts = {}
|
||||
end
|
||||
end
|
||||
|
||||
def cache_layout(details)
|
||||
layout = @found_layouts
|
||||
values = details.values_at(:formats, :locale)
|
||||
|
||||
# Cache nil
|
||||
if layout.key?(values)
|
||||
return layout[values]
|
||||
else
|
||||
layout[values] = yield
|
||||
end
|
||||
end
|
||||
|
||||
# This module is mixed in if layout conditions are provided. This means
|
||||
# that if no layout conditions are used, this method is not used
|
||||
module LayoutConditions
|
||||
# Determines whether the current action has a layout by checking the
|
||||
# action name against the :only and :except conditions set on the
|
||||
# layout.
|
||||
#
|
||||
# ==== Returns
|
||||
# Boolean:: True if the action has a layout, false otherwise.
|
||||
def _action_has_layout?
|
||||
conditions = _layout_conditions
|
||||
|
||||
if only = conditions[:only]
|
||||
only.include?(action_name)
|
||||
elsif except = conditions[:except]
|
||||
!except.include?(action_name)
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Specify the layout to use for this class.
|
||||
|
@ -31,6 +69,8 @@ module AbstractController
|
|||
# :only<#to_s, Array[#to_s]>:: A list of actions to apply this layout to.
|
||||
# :except<#to_s, Array[#to_s]>:: Apply this layout to all actions but this one
|
||||
def layout(layout, conditions = {})
|
||||
include LayoutConditions unless conditions.empty?
|
||||
|
||||
conditions.each {|k, v| conditions[k] = Array(v).map {|a| a.to_s} }
|
||||
self._layout_conditions = conditions
|
||||
|
||||
|
@ -76,12 +116,14 @@ module AbstractController
|
|||
when nil
|
||||
self.class_eval <<-ruby_eval, __FILE__, __LINE__ + 1
|
||||
def _layout(details)
|
||||
self.class.cache_layout(details) do
|
||||
if view_paths.exists?("#{_implied_layout_name}", details, "layouts")
|
||||
"#{_implied_layout_name}"
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
ruby_eval
|
||||
end
|
||||
self.class_eval { private :_layout }
|
||||
|
@ -162,21 +204,8 @@ module AbstractController
|
|||
end
|
||||
end
|
||||
|
||||
# Determines whether the current action has a layout by checking the
|
||||
# action name against the :only and :except conditions set on the
|
||||
# layout.
|
||||
#
|
||||
# ==== Returns
|
||||
# Boolean:: True if the action has a layout, false otherwise.
|
||||
def _action_has_layout?
|
||||
conditions = _layout_conditions
|
||||
if only = conditions[:only]
|
||||
only.include?(action_name)
|
||||
elsif except = conditions[:except]
|
||||
!except.include?(action_name)
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,8 +25,9 @@ module ActionController
|
|||
cattr_accessor :relative_url_root
|
||||
self.relative_url_root = ENV['RAILS_RELATIVE_URL_ROOT']
|
||||
|
||||
cattr_accessor :default_charset
|
||||
self.default_charset = "utf-8"
|
||||
class << self
|
||||
delegate :default_charset=, :to => "ActionDispatch::Response"
|
||||
end
|
||||
|
||||
# cattr_reader :protected_instance_variables
|
||||
cattr_accessor :protected_instance_variables
|
||||
|
@ -101,11 +102,10 @@ module ActionController
|
|||
options[:template].sub!(/^\//, '')
|
||||
end
|
||||
|
||||
options[:text] = nil if options[:nothing] == true
|
||||
options[:text] = nil if options.delete(:nothing) == true
|
||||
options[:text] = " " if options.key?(:text) && options[:text].nil?
|
||||
|
||||
body = super
|
||||
body = [' '] if body.blank?
|
||||
body
|
||||
super || " "
|
||||
end
|
||||
|
||||
def _handle_method_missing
|
||||
|
|
|
@ -13,8 +13,10 @@ module ActionController
|
|||
# Overrides AbstractController::Base#action_method? to return false if the
|
||||
# action name is in the list of hidden actions.
|
||||
def action_method?(action_name)
|
||||
self.class.visible_action?(action_name) do
|
||||
!hidden_actions.include?(action_name) && super
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Sets all of the actions passed in as hidden actions.
|
||||
|
@ -25,6 +27,16 @@ module ActionController
|
|||
hidden_actions.merge(args.map! {|a| a.to_s })
|
||||
end
|
||||
|
||||
def inherited(klass)
|
||||
klass.instance_variable_set("@visible_actions", {})
|
||||
super
|
||||
end
|
||||
|
||||
def visible_action?(action_name)
|
||||
return @visible_actions[action_name] if @visible_actions.key?(action_name)
|
||||
@visible_actions[action_name] = yield
|
||||
end
|
||||
|
||||
# Overrides AbstractController::Base#action_methods to remove any methods
|
||||
# that are listed as hidden methods.
|
||||
def action_methods
|
||||
|
|
|
@ -145,7 +145,6 @@ module ActionController #:nodoc:
|
|||
def send_data(data, options = {}) #:doc:
|
||||
logger.info "Sending data #{options[:filename]}" if logger
|
||||
send_file_headers! options.merge(:length => data.bytesize)
|
||||
@performed_render = false
|
||||
render :status => options[:status], :text => data
|
||||
end
|
||||
|
||||
|
@ -175,6 +174,8 @@ module ActionController #:nodoc:
|
|||
'Content-Transfer-Encoding' => 'binary'
|
||||
)
|
||||
|
||||
response.sending_file = true
|
||||
|
||||
# Fix a problem with IE 6.0 on opening downloaded files:
|
||||
# If Cache-Control: no-cache is set (which Rails does by default),
|
||||
# IE removes the file it just downloaded from its cache immediately
|
||||
|
|
|
@ -4,15 +4,6 @@ module ActionController
|
|||
|
||||
include RackConvenience
|
||||
|
||||
def process_action(*)
|
||||
initialize_current_url
|
||||
super
|
||||
end
|
||||
|
||||
def initialize_current_url
|
||||
@url = UrlRewriter.new(request, params.clone)
|
||||
end
|
||||
|
||||
# Overwrite to implement a number of default options that all url_for-based methods will use. The default options should come in
|
||||
# the form of a hash, just like the one you would use for url_for directly. Example:
|
||||
#
|
||||
|
@ -40,6 +31,7 @@ module ActionController
|
|||
when String
|
||||
options
|
||||
when Hash
|
||||
@url ||= UrlRewriter.new(request, params)
|
||||
@url.rewrite(rewrite_options(options))
|
||||
else
|
||||
polymorphic_url(options)
|
||||
|
|
|
@ -52,7 +52,7 @@ module ActionController #:nodoc:
|
|||
class TestResponse < ActionDispatch::TestResponse
|
||||
def recycle!
|
||||
@status = 200
|
||||
@header = Rack::Utils::HeaderHash.new
|
||||
@header = {}
|
||||
@writer = lambda { |x| @body << x }
|
||||
@block = nil
|
||||
@length = 0
|
||||
|
|
|
@ -179,7 +179,6 @@ module ActionController
|
|||
if @controller
|
||||
@controller.request = @request
|
||||
@controller.params = {}
|
||||
@controller.send(:initialize_current_url)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -173,19 +173,15 @@ module ActionDispatch
|
|||
end
|
||||
end
|
||||
|
||||
# Expand raw_formats by converting Mime::ALL to the Mime::SET.
|
||||
#
|
||||
def formats
|
||||
if ActionController::Base.use_accept_header
|
||||
raw_formats.tap do |ret|
|
||||
if ret == ONLY_ALL
|
||||
ret.replace Mime::SET
|
||||
elsif all = ret.index(Mime::ALL)
|
||||
ret.delete_at(all) && ret.insert(all, *Mime::SET)
|
||||
end
|
||||
if param = parameters[:format]
|
||||
Array.wrap(Mime[param])
|
||||
else
|
||||
accepts.dup
|
||||
end
|
||||
else
|
||||
raw_formats + Mime::SET
|
||||
[format]
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -487,7 +483,7 @@ EOM
|
|||
# matches the order array.
|
||||
#
|
||||
def negotiate_mime(order)
|
||||
raw_formats.each do |priority|
|
||||
formats.each do |priority|
|
||||
if priority == Mime::ALL
|
||||
return order.first
|
||||
elsif order.include?(priority)
|
||||
|
@ -500,18 +496,6 @@ EOM
|
|||
|
||||
private
|
||||
|
||||
def raw_formats
|
||||
if ActionController::Base.use_accept_header
|
||||
if param = parameters[:format]
|
||||
Array.wrap(Mime[param])
|
||||
else
|
||||
accepts.dup
|
||||
end
|
||||
else
|
||||
[format]
|
||||
end
|
||||
end
|
||||
|
||||
def named_host?(host)
|
||||
!(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
|
||||
end
|
||||
|
|
|
@ -32,18 +32,35 @@ module ActionDispatch # :nodoc:
|
|||
# end
|
||||
# end
|
||||
class Response < Rack::Response
|
||||
attr_accessor :request
|
||||
attr_accessor :request, :blank
|
||||
attr_reader :cache_control
|
||||
|
||||
attr_writer :header
|
||||
attr_writer :header, :sending_file
|
||||
alias_method :headers=, :header=
|
||||
|
||||
delegate :default_charset, :to => 'ActionController::Base'
|
||||
|
||||
def initialize
|
||||
super
|
||||
@status = 200
|
||||
@header = {}
|
||||
@cache_control = {}
|
||||
@header = Rack::Utils::HeaderHash.new
|
||||
|
||||
@writer = lambda { |x| @body << x }
|
||||
@block = nil
|
||||
@length = 0
|
||||
|
||||
@body, @cookie = [], []
|
||||
@sending_file = false
|
||||
|
||||
yield self if block_given?
|
||||
end
|
||||
|
||||
def cache_control
|
||||
@cache_control ||= {}
|
||||
end
|
||||
|
||||
def write(str)
|
||||
s = str.to_s
|
||||
@writer.call s
|
||||
str
|
||||
end
|
||||
|
||||
def status=(status)
|
||||
|
@ -71,7 +88,10 @@ module ActionDispatch # :nodoc:
|
|||
str
|
||||
end
|
||||
|
||||
EMPTY = " "
|
||||
|
||||
def body=(body)
|
||||
@blank = true if body == EMPTY
|
||||
@body = body.respond_to?(:to_str) ? [body] : body
|
||||
end
|
||||
|
||||
|
@ -113,41 +133,39 @@ module ActionDispatch # :nodoc:
|
|||
end
|
||||
|
||||
def etag
|
||||
headers['ETag']
|
||||
@etag
|
||||
end
|
||||
|
||||
def etag?
|
||||
headers.include?('ETag')
|
||||
@etag
|
||||
end
|
||||
|
||||
def etag=(etag)
|
||||
if etag.blank?
|
||||
headers.delete('ETag')
|
||||
else
|
||||
headers['ETag'] = %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(etag))}")
|
||||
end
|
||||
key = ActiveSupport::Cache.expand_cache_key(etag)
|
||||
@etag = %("#{Digest::MD5.hexdigest(key)}")
|
||||
end
|
||||
|
||||
def sending_file?
|
||||
headers["Content-Transfer-Encoding"] == "binary"
|
||||
end
|
||||
CONTENT_TYPE = "Content-Type"
|
||||
|
||||
cattr_accessor(:default_charset) { "utf-8" }
|
||||
|
||||
def assign_default_content_type_and_charset!
|
||||
return if !headers["Content-Type"].blank?
|
||||
return if headers[CONTENT_TYPE].present?
|
||||
|
||||
@content_type ||= Mime::HTML
|
||||
@charset ||= default_charset
|
||||
@charset ||= self.class.default_charset
|
||||
|
||||
type = @content_type.to_s.dup
|
||||
type << "; charset=#{@charset}" unless sending_file?
|
||||
type << "; charset=#{@charset}" unless @sending_file
|
||||
|
||||
headers["Content-Type"] = type
|
||||
headers[CONTENT_TYPE] = type
|
||||
end
|
||||
|
||||
def prepare!
|
||||
assign_default_content_type_and_charset!
|
||||
handle_conditional_get!
|
||||
self["Set-Cookie"] ||= ""
|
||||
self["Set-Cookie"] = @cookie.join("\n")
|
||||
self["ETag"] = @etag if @etag
|
||||
end
|
||||
|
||||
def each(&callback)
|
||||
|
@ -168,23 +186,12 @@ module ActionDispatch # :nodoc:
|
|||
str
|
||||
end
|
||||
|
||||
def set_cookie(key, value)
|
||||
if value.has_key?(:http_only)
|
||||
ActiveSupport::Deprecation.warn(
|
||||
"The :http_only option in ActionController::Response#set_cookie " +
|
||||
"has been renamed. Please use :httponly instead.", caller)
|
||||
value[:httponly] ||= value.delete(:http_only)
|
||||
end
|
||||
|
||||
super(key, value)
|
||||
end
|
||||
|
||||
# Returns the response cookies, converted to a Hash of (name => value) pairs
|
||||
#
|
||||
# assert_equal 'AuthorOfNewPage', r.cookies['author']
|
||||
def cookies
|
||||
cookies = {}
|
||||
if header = headers['Set-Cookie']
|
||||
if header = @cookie
|
||||
header = header.split("\n") if header.respond_to?(:to_str)
|
||||
header.each do |cookie|
|
||||
if pair = cookie.split(';').first
|
||||
|
@ -196,12 +203,43 @@ module ActionDispatch # :nodoc:
|
|||
cookies
|
||||
end
|
||||
|
||||
def set_cookie(key, value)
|
||||
case value
|
||||
when Hash
|
||||
domain = "; domain=" + value[:domain] if value[:domain]
|
||||
path = "; path=" + value[:path] if value[:path]
|
||||
# According to RFC 2109, we need dashes here.
|
||||
# N.B.: cgi.rb uses spaces...
|
||||
expires = "; expires=" + value[:expires].clone.gmtime.
|
||||
strftime("%a, %d-%b-%Y %H:%M:%S GMT") if value[:expires]
|
||||
secure = "; secure" if value[:secure]
|
||||
httponly = "; HttpOnly" if value[:httponly]
|
||||
value = value[:value]
|
||||
end
|
||||
value = [value] unless Array === value
|
||||
cookie = Rack::Utils.escape(key) + "=" +
|
||||
value.map { |v| Rack::Utils.escape v }.join("&") +
|
||||
"#{domain}#{path}#{expires}#{secure}#{httponly}"
|
||||
|
||||
@cookie << cookie
|
||||
end
|
||||
|
||||
def delete_cookie(key, value={})
|
||||
@cookie.reject! { |cookie|
|
||||
cookie =~ /\A#{Rack::Utils.escape(key)}=/
|
||||
}
|
||||
|
||||
set_cookie(key,
|
||||
{:value => '', :path => nil, :domain => nil,
|
||||
:expires => Time.at(0) }.merge(value))
|
||||
end
|
||||
|
||||
private
|
||||
def handle_conditional_get!
|
||||
if etag? || last_modified? || !cache_control.empty?
|
||||
if etag? || last_modified? || !@cache_control.empty?
|
||||
set_conditional_cache_control!
|
||||
elsif nonempty_ok_response?
|
||||
self.etag = body
|
||||
self.etag = @body
|
||||
|
||||
if request && request.etag_matches?(etag)
|
||||
self.status = 304
|
||||
|
@ -215,29 +253,33 @@ module ActionDispatch # :nodoc:
|
|||
end
|
||||
|
||||
def nonempty_ok_response?
|
||||
ok = !@status || @status == 200
|
||||
ok && string_body?
|
||||
@status == 200 && string_body?
|
||||
end
|
||||
|
||||
def string_body?
|
||||
!body_parts.respond_to?(:call) && body_parts.any? && body_parts.all? { |part| part.is_a?(String) }
|
||||
!@blank && @body.respond_to?(:all?) && @body.all? { |part| part.is_a?(String) }
|
||||
end
|
||||
|
||||
DEFAULT_CACHE_CONTROL = "max-age=0, private, must-revalidate"
|
||||
|
||||
def set_conditional_cache_control!
|
||||
if cache_control.empty?
|
||||
cache_control.merge!(:public => false, :max_age => 0, :must_revalidate => true)
|
||||
end
|
||||
control = @cache_control
|
||||
|
||||
public_cache, max_age, must_revalidate, extras =
|
||||
cache_control.values_at(:public, :max_age, :must_revalidate, :extras)
|
||||
if control.empty?
|
||||
headers["Cache-Control"] = DEFAULT_CACHE_CONTROL
|
||||
else
|
||||
extras = control[:extras]
|
||||
max_age = control[:max_age]
|
||||
|
||||
options = []
|
||||
options << "max-age=#{max_age}" if max_age
|
||||
options << (public_cache ? "public" : "private")
|
||||
options << "must-revalidate" if must_revalidate
|
||||
options << (control[:public] ? "public" : "private")
|
||||
options << "must-revalidate" if control[:must_revalidate]
|
||||
options.concat(extras) if extras
|
||||
|
||||
headers["Cache-Control"] = options.join(", ")
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -184,6 +184,7 @@ module ActionView
|
|||
def initialize(view_context, options, block)
|
||||
partial = options[:partial]
|
||||
|
||||
@memo = {}
|
||||
@view = view_context
|
||||
@options = options
|
||||
@locals = options[:locals] || {}
|
||||
|
@ -207,9 +208,7 @@ module ActionView
|
|||
end
|
||||
|
||||
def render_collection
|
||||
# Even if no template is rendered, this will ensure that the MIME type
|
||||
# for the empty response is the same as the provided template
|
||||
@options[:_template] = default_template = find_template
|
||||
@options[:_template] = template = find_template
|
||||
|
||||
return nil if collection.blank?
|
||||
|
||||
|
@ -217,15 +216,46 @@ module ActionView
|
|||
spacer = find_template(@options[:spacer_template]).render(@view, @locals)
|
||||
end
|
||||
|
||||
segments = []
|
||||
|
||||
collection.each_with_index do |object, index|
|
||||
template = default_template || find_template(partial_path(object))
|
||||
@locals[template.counter_name] = index
|
||||
segments << render_template(template, object)
|
||||
result = template ? collection_with_template(template) : collection_without_template
|
||||
result.join(spacer)
|
||||
end
|
||||
|
||||
segments.join(spacer)
|
||||
def collection_with_template(template)
|
||||
options = @options
|
||||
|
||||
segments, locals, as = [], @locals, options[:as] || :object
|
||||
|
||||
variable_name = template.variable_name
|
||||
counter_name = template.counter_name
|
||||
locals[counter_name] = -1
|
||||
|
||||
collection.each do |object|
|
||||
locals[counter_name] += 1
|
||||
locals[variable_name] = object
|
||||
locals[as] = object if as
|
||||
|
||||
segments << template.render(@view, locals)
|
||||
end
|
||||
segments
|
||||
end
|
||||
|
||||
def collection_without_template
|
||||
options = @options
|
||||
|
||||
segments, locals, as = [], @locals, options[:as] || :object
|
||||
index, template = -1, nil
|
||||
|
||||
collection.each do |object|
|
||||
template = find_template(partial_path(object))
|
||||
locals[template.counter_name] = (index += 1)
|
||||
locals[template.variable_name] = object
|
||||
locals[as] = object if as
|
||||
|
||||
segments << template.render(@view, locals)
|
||||
end
|
||||
|
||||
@options[:_template] = template
|
||||
segments
|
||||
end
|
||||
|
||||
def render_template(template, object = @object)
|
||||
|
|
|
@ -41,9 +41,11 @@ module ActionView
|
|||
end
|
||||
|
||||
def handler_glob
|
||||
@handler_glob ||= begin
|
||||
e = TemplateHandlers.extensions.map{|h| ".#{h},"}.join
|
||||
"{#{e}}"
|
||||
end
|
||||
end
|
||||
|
||||
def formats_glob
|
||||
@formats_glob ||= begin
|
||||
|
@ -60,6 +62,10 @@ module ActionView
|
|||
|
||||
class FileSystemResolver < Resolver
|
||||
|
||||
def self.cached_glob
|
||||
@@cached_glob ||= {}
|
||||
end
|
||||
|
||||
def initialize(path, options = {})
|
||||
raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
|
||||
super(options)
|
||||
|
@ -105,6 +111,7 @@ module ActionView
|
|||
|
||||
# :api: plugin
|
||||
def details_to_glob(name, details, prefix, partial, root)
|
||||
self.class.cached_glob[[name, prefix, partial, details, root]] ||= begin
|
||||
path = ""
|
||||
path << "#{prefix}/" unless prefix.empty?
|
||||
path << (partial ? "_#{name}" : name)
|
||||
|
@ -120,6 +127,7 @@ module ActionView
|
|||
|
||||
"#{root}#{path}#{extensions}#{handler_glob}"
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: fix me
|
||||
# :api: plugin
|
||||
|
|
|
@ -9,14 +9,16 @@ module ActionView
|
|||
end
|
||||
|
||||
attr_internal :rendered
|
||||
alias_method :_render_template_without_template_tracking, :_render_single_template
|
||||
def _render_single_template(template, local_assigns, &block)
|
||||
if template.respond_to?(:identifier) && template.present?
|
||||
@_rendered[:partials][template] += 1 if template.partial?
|
||||
@_rendered[:template] ||= []
|
||||
@_rendered[:template] << template
|
||||
end
|
||||
_render_template_without_template_tracking(template, local_assigns, &block)
|
||||
|
||||
class Template
|
||||
alias_method :render_without_tracking, :render
|
||||
def render(view, locals, &blk)
|
||||
rendered = view.rendered
|
||||
rendered[:partials][self] += 1 if partial?
|
||||
rendered[:template] ||= []
|
||||
rendered[:template] << self
|
||||
render_without_tracking(view, locals, &blk)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -70,7 +72,6 @@ module ActionView
|
|||
@response = ActionController::TestResponse.new
|
||||
|
||||
@params = {}
|
||||
send(:initialize_current_url)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -536,7 +536,6 @@ class FragmentCachingTest < ActionController::TestCase
|
|||
@controller.params = @params
|
||||
@controller.request = @request
|
||||
@controller.response = @response
|
||||
@controller.send(:initialize_current_url)
|
||||
@controller.send(:initialize_template_class, @response)
|
||||
@controller.send(:assign_shortcuts, @request, @response)
|
||||
end
|
||||
|
|
|
@ -427,7 +427,7 @@ class RequestTest < ActiveSupport::TestCase
|
|||
|
||||
request = stub_request 'CONTENT_TYPE' => 'application/xml; charset=UTF-8'
|
||||
request.expects(:parameters).at_least_once.returns({})
|
||||
assert_equal with_set(Mime::XML, Mime::HTML), request.formats
|
||||
assert_equal with_set(Mime::XML, Mime::HTML, Mime::ALL), request.formats
|
||||
end
|
||||
|
||||
with_accept_header false do
|
||||
|
@ -460,7 +460,7 @@ protected
|
|||
end
|
||||
|
||||
def with_set(*args)
|
||||
args + Mime::SET
|
||||
args
|
||||
end
|
||||
|
||||
def with_accept_header(value)
|
||||
|
|
|
@ -1 +1 @@
|
|||
<%= customer.name %> <%= object.name %> <%= customer_with_var.name %>
|
||||
<%= customer.name %> <%= customer.name %> <%= customer_with_var.name %>
|
|
@ -1,5 +1,11 @@
|
|||
*Edge*
|
||||
|
||||
* Add validates_format_of :without => /regexp/ option. #430 [Elliot Winkler, Peer Allan]
|
||||
|
||||
Example :
|
||||
|
||||
validates_format_of :subdomain, :without => /www|admin|mail/
|
||||
|
||||
* Introduce validates_with to encapsulate attribute validations in a class. #2630 [Jeff Dean]
|
||||
|
||||
* Extracted from Active Record and Active Resource.
|
||||
|
|
|
@ -29,6 +29,7 @@ module ActiveModel
|
|||
autoload :AttributeMethods, 'active_model/attribute_methods'
|
||||
autoload :Conversion, 'active_model/conversion'
|
||||
autoload :DeprecatedErrorMethods, 'active_model/deprecated_error_methods'
|
||||
autoload :Dirty, 'active_model/dirty'
|
||||
autoload :Errors, 'active_model/errors'
|
||||
autoload :Name, 'active_model/naming'
|
||||
autoload :Naming, 'active_model/naming'
|
||||
|
|
|
@ -133,18 +133,31 @@ module ActiveModel
|
|||
undefine_attribute_methods
|
||||
end
|
||||
|
||||
def alias_attribute(new_name, old_name)
|
||||
attribute_method_matchers.each do |matcher|
|
||||
module_eval <<-STR, __FILE__, __LINE__+1
|
||||
def #{matcher.method_name(new_name)}(*args)
|
||||
send(:#{matcher.method_name(old_name)}, *args)
|
||||
end
|
||||
STR
|
||||
end
|
||||
end
|
||||
|
||||
def define_attribute_methods(attr_names)
|
||||
return if attribute_methods_generated?
|
||||
attr_names.each do |name|
|
||||
attribute_method_matchers.each do |method|
|
||||
method_name = "#{method.prefix}#{name}#{method.suffix}"
|
||||
unless instance_method_already_implemented?(method_name)
|
||||
generate_method = "define_method_#{method.prefix}attribute#{method.suffix}"
|
||||
attr_names.each do |attr_name|
|
||||
attribute_method_matchers.each do |matcher|
|
||||
unless instance_method_already_implemented?(matcher.method_name(attr_name))
|
||||
generate_method = "define_method_#{matcher.prefix}attribute#{matcher.suffix}"
|
||||
|
||||
if respond_to?(generate_method)
|
||||
send(generate_method, name)
|
||||
send(generate_method, attr_name)
|
||||
else
|
||||
generated_attribute_methods.module_eval("def #{method_name}(*args); send(:#{method.prefix}attribute#{method.suffix}, '#{name}', *args); end", __FILE__, __LINE__)
|
||||
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__+1
|
||||
def #{matcher.method_name(attr_name)}(*args)
|
||||
send(:#{matcher.method_missing_target}, '#{attr_name}', *args)
|
||||
end
|
||||
STR
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -180,7 +193,7 @@ module ActiveModel
|
|||
class AttributeMethodMatcher
|
||||
attr_reader :prefix, :suffix
|
||||
|
||||
AttributeMethodMatch = Struct.new(:prefix, :base, :suffix)
|
||||
AttributeMethodMatch = Struct.new(:target, :attr_name)
|
||||
|
||||
def initialize(options = {})
|
||||
options.symbolize_keys!
|
||||
|
@ -190,11 +203,19 @@ module ActiveModel
|
|||
|
||||
def match(method_name)
|
||||
if matchdata = @regex.match(method_name)
|
||||
AttributeMethodMatch.new(matchdata[1], matchdata[2], matchdata[3])
|
||||
AttributeMethodMatch.new(method_missing_target, matchdata[2])
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def method_name(attr_name)
|
||||
"#{prefix}#{attr_name}#{suffix}"
|
||||
end
|
||||
|
||||
def method_missing_target
|
||||
:"#{prefix}attribute#{suffix}"
|
||||
end
|
||||
end
|
||||
|
||||
def attribute_method_matchers #:nodoc:
|
||||
|
@ -214,7 +235,7 @@ module ActiveModel
|
|||
method_name = method_id.to_s
|
||||
if match = match_attribute_method?(method_name)
|
||||
guard_private_attribute_method!(method_name, args)
|
||||
return __send__("#{match.prefix}attribute#{match.suffix}", match.base, *args, &block)
|
||||
return __send__(match.target, match.attr_name, *args, &block)
|
||||
end
|
||||
super
|
||||
end
|
||||
|
@ -246,7 +267,7 @@ module ActiveModel
|
|||
# The struct's attributes are prefix, base and suffix.
|
||||
def match_attribute_method?(method_name)
|
||||
self.class.send(:attribute_method_matchers).each do |method|
|
||||
if (match = method.match(method_name)) && attribute_method?(match.base)
|
||||
if (match = method.match(method_name)) && attribute_method?(match.attr_name)
|
||||
return match
|
||||
end
|
||||
end
|
||||
|
|
112
activemodel/lib/active_model/dirty.rb
Normal file
112
activemodel/lib/active_model/dirty.rb
Normal file
|
@ -0,0 +1,112 @@
|
|||
module ActiveModel
|
||||
# Track unsaved attribute changes.
|
||||
#
|
||||
# A newly instantiated object is unchanged:
|
||||
# person = Person.find_by_name('Uncle Bob')
|
||||
# person.changed? # => false
|
||||
#
|
||||
# Change the name:
|
||||
# person.name = 'Bob'
|
||||
# person.changed? # => true
|
||||
# person.name_changed? # => true
|
||||
# person.name_was # => 'Uncle Bob'
|
||||
# person.name_change # => ['Uncle Bob', 'Bob']
|
||||
# person.name = 'Bill'
|
||||
# person.name_change # => ['Uncle Bob', 'Bill']
|
||||
#
|
||||
# Save the changes:
|
||||
# person.save
|
||||
# person.changed? # => false
|
||||
# person.name_changed? # => false
|
||||
#
|
||||
# Assigning the same value leaves the attribute unchanged:
|
||||
# person.name = 'Bill'
|
||||
# person.name_changed? # => false
|
||||
# person.name_change # => nil
|
||||
#
|
||||
# Which attributes have changed?
|
||||
# person.name = 'Bob'
|
||||
# person.changed # => ['name']
|
||||
# person.changes # => { 'name' => ['Bill', 'Bob'] }
|
||||
#
|
||||
# Resetting an attribute returns it to its original state:
|
||||
# person.reset_name! # => 'Bill'
|
||||
# person.changed? # => false
|
||||
# person.name_changed? # => false
|
||||
# person.name # => 'Bill'
|
||||
#
|
||||
# Before modifying an attribute in-place:
|
||||
# person.name_will_change!
|
||||
# person.name << 'y'
|
||||
# person.name_change # => ['Bill', 'Billy']
|
||||
module Dirty
|
||||
extend ActiveSupport::Concern
|
||||
include ActiveModel::AttributeMethods
|
||||
|
||||
included do
|
||||
attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
|
||||
attribute_method_affix :prefix => 'reset_', :suffix => '!'
|
||||
end
|
||||
|
||||
# Do any attributes have unsaved changes?
|
||||
# person.changed? # => false
|
||||
# person.name = 'bob'
|
||||
# person.changed? # => true
|
||||
def changed?
|
||||
!changed_attributes.empty?
|
||||
end
|
||||
|
||||
# List of attributes with unsaved changes.
|
||||
# person.changed # => []
|
||||
# person.name = 'bob'
|
||||
# person.changed # => ['name']
|
||||
def changed
|
||||
changed_attributes.keys
|
||||
end
|
||||
|
||||
# Map of changed attrs => [original value, new value].
|
||||
# person.changes # => {}
|
||||
# person.name = 'bob'
|
||||
# person.changes # => { 'name' => ['bill', 'bob'] }
|
||||
def changes
|
||||
changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
|
||||
end
|
||||
|
||||
private
|
||||
# Map of change <tt>attr => original value</tt>.
|
||||
def changed_attributes
|
||||
@changed_attributes ||= {}
|
||||
end
|
||||
|
||||
# Handle <tt>*_changed?</tt> for +method_missing+.
|
||||
def attribute_changed?(attr)
|
||||
changed_attributes.include?(attr)
|
||||
end
|
||||
|
||||
# Handle <tt>*_change</tt> for +method_missing+.
|
||||
def attribute_change(attr)
|
||||
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
|
||||
end
|
||||
|
||||
# Handle <tt>*_was</tt> for +method_missing+.
|
||||
def attribute_was(attr)
|
||||
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
|
||||
end
|
||||
|
||||
# Handle <tt>*_will_change!</tt> for +method_missing+.
|
||||
def attribute_will_change!(attr)
|
||||
begin
|
||||
value = __send__(attr)
|
||||
value = value.duplicable? ? value.clone : value
|
||||
rescue TypeError, NoMethodError
|
||||
end
|
||||
|
||||
changed_attributes[attr] = value
|
||||
end
|
||||
|
||||
# Handle <tt>reset_*!</tt> for +method_missing+.
|
||||
def reset_attribute!(attr)
|
||||
__send__("#{attr}=", changed_attributes[attr]) if attribute_changed?(attr)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,22 +1,30 @@
|
|||
module ActiveModel
|
||||
module Validations
|
||||
module ClassMethods
|
||||
# Validates whether the value of the specified attribute is of the correct form by matching it against the regular expression
|
||||
# provided.
|
||||
# Validates whether the value of the specified attribute is of the correct form, going by the regular expression provided.
|
||||
# You can require that the attribute matches the regular expression:
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create
|
||||
# end
|
||||
#
|
||||
# Alternatively, you can require that the specified attribute does _not_ match the regular expression:
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# validates_format_of :email, :without => /NOSPAM/
|
||||
# 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.
|
||||
#
|
||||
# A regular expression must be provided 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 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> - The regular expression used to validate the format with (note: must be supplied!).
|
||||
# * <tt>:with</tt> - Regular expression that if the attribute matches will result in a successful validation.
|
||||
# * <tt>:without</tt> - Regular expression that if the attribute does not match will result in a successful validation.
|
||||
# * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
|
||||
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
|
||||
# occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
|
||||
|
@ -25,14 +33,27 @@ module ActiveModel
|
|||
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
||||
# method, proc or string should return or evaluate to a true or false value.
|
||||
def validates_format_of(*attr_names)
|
||||
configuration = { :with => nil }
|
||||
configuration.update(attr_names.extract_options!)
|
||||
configuration = attr_names.extract_options!
|
||||
|
||||
raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp)
|
||||
unless configuration.include?(:with) ^ configuration.include?(:without) # ^ == xor, or "exclusive or"
|
||||
raise ArgumentError, "Either :with or :without must be supplied (but not both)"
|
||||
end
|
||||
|
||||
if configuration[:with] && !configuration[:with].is_a?(Regexp)
|
||||
raise ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash"
|
||||
end
|
||||
|
||||
if configuration[:without] && !configuration[:without].is_a?(Regexp)
|
||||
raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash"
|
||||
end
|
||||
|
||||
if configuration[:with]
|
||||
validates_each(attr_names, configuration) do |record, attr_name, value|
|
||||
unless value.to_s =~ configuration[:with]
|
||||
record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
|
||||
record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) if value.to_s !~ configuration[:with]
|
||||
end
|
||||
elsif configuration[:without]
|
||||
validates_each(attr_names, configuration) do |record, attr_name, value|
|
||||
record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) if value.to_s =~ configuration[:without]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -71,6 +71,35 @@ class PresenceValidationTest < ActiveModel::TestCase
|
|||
assert_equal ["can't be Invalid title"], t.errors[:title]
|
||||
end
|
||||
|
||||
def test_validate_format_with_not_option
|
||||
Topic.validates_format_of(:title, :without => /foo/, :message => "should not contain foo")
|
||||
t = Topic.new
|
||||
|
||||
t.title = "foobar"
|
||||
t.valid?
|
||||
assert_equal ["should not contain foo"], t.errors[:title]
|
||||
|
||||
t.title = "something else"
|
||||
t.valid?
|
||||
assert_equal [], t.errors[:title]
|
||||
end
|
||||
|
||||
def test_validate_format_of_without_any_regexp_should_raise_error
|
||||
assert_raise(ArgumentError) { Topic.validates_format_of(:title) }
|
||||
end
|
||||
|
||||
def test_validates_format_of_with_both_regexps_should_raise_error
|
||||
assert_raise(ArgumentError) { Topic.validates_format_of(:title, :with => /this/, :without => /that/) }
|
||||
end
|
||||
|
||||
def test_validates_format_of_when_with_isnt_a_regexp_should_raise_error
|
||||
assert_raise(ArgumentError) { Topic.validates_format_of(:title, :with => "clearly not a regexp") }
|
||||
end
|
||||
|
||||
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_custom_error_using_quotes
|
||||
repair_validations(Developer) do
|
||||
Developer.validates_format_of :name, :with => /^(A-Z*)$/, :message=> "format 'single' and \"double\" quotes"
|
||||
|
|
|
@ -2199,7 +2199,7 @@ during calendar reform. #7649, #7724 [fedot, Geoff Buesing]
|
|||
|
||||
* Escape database name in MySQL adapter when creating and dropping databases. #3409 [anna@wota.jp]
|
||||
|
||||
* Disambiguate table names for columns in validates_uniquness_of's WHERE clause. #3423 [alex.borovsky@gmail.com]
|
||||
* Disambiguate table names for columns in validates_uniqueness_of's WHERE clause. #3423 [alex.borovsky@gmail.com]
|
||||
|
||||
* .with_scope imposed create parameters now bypass attr_protected [Tobias Lütke]
|
||||
|
||||
|
@ -3714,7 +3714,7 @@ in effect. Added :readonly finder constraint. Calling an association collectio
|
|||
|
||||
* Escape database name in MySQL adapter when creating and dropping databases. #3409 [anna@wota.jp]
|
||||
|
||||
* Disambiguate table names for columns in validates_uniquness_of's WHERE clause. #3423 [alex.borovsky@gmail.com]
|
||||
* Disambiguate table names for columns in validates_uniqueness_of's WHERE clause. #3423 [alex.borovsky@gmail.com]
|
||||
|
||||
* .with_scope imposed create parameters now bypass attr_protected [Tobias Lütke]
|
||||
|
||||
|
|
|
@ -8,25 +8,11 @@ module ActiveRecord
|
|||
alias_method :new, :build
|
||||
|
||||
def create!(attrs = nil)
|
||||
ensure_owner_is_not_new
|
||||
|
||||
transaction do
|
||||
self << (object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association! } : @reflection.create_association!)
|
||||
object
|
||||
end
|
||||
create_record(attrs, true)
|
||||
end
|
||||
|
||||
def create(attrs = nil)
|
||||
ensure_owner_is_not_new
|
||||
|
||||
transaction do
|
||||
object = attrs ? @reflection.klass.send(:with_scope, :create => attrs) { @reflection.create_association } : @reflection.create_association
|
||||
raise_on_type_mismatch(object)
|
||||
add_record_to_target_with_callbacks(object) do |r|
|
||||
insert_record(object, false)
|
||||
end
|
||||
object
|
||||
end
|
||||
create_record(attrs, false)
|
||||
end
|
||||
|
||||
def destroy(*records)
|
||||
|
@ -46,6 +32,16 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
protected
|
||||
def create_record(attrs, force = true)
|
||||
ensure_owner_is_not_new
|
||||
|
||||
transaction do
|
||||
object = @reflection.klass.new(attrs)
|
||||
add_record_to_target_with_callbacks(object) {|r| insert_record(object, force) }
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
def target_reflection_has_associated_record?
|
||||
if @reflection.through_reflection.macro == :belongs_to && @owner[@reflection.through_reflection.primary_key_name].blank?
|
||||
false
|
||||
|
@ -69,9 +65,10 @@ module ActiveRecord
|
|||
return false unless record.save(validate)
|
||||
end
|
||||
end
|
||||
through_reflection = @reflection.through_reflection
|
||||
klass = through_reflection.klass
|
||||
@owner.send(@reflection.through_reflection.name).proxy_target << klass.send(:with_scope, :create => construct_join_attributes(record)) { through_reflection.create_association! }
|
||||
|
||||
through_association = @owner.send(@reflection.through_reflection.name)
|
||||
through_record = through_association.create!(construct_join_attributes(record))
|
||||
through_association.proxy_target << through_record
|
||||
end
|
||||
|
||||
# TODO - add dependent option support
|
||||
|
|
|
@ -1,59 +1,12 @@
|
|||
require 'active_support/core_ext/object/tap'
|
||||
|
||||
module ActiveRecord
|
||||
module AttributeMethods
|
||||
# Track unsaved attribute changes.
|
||||
#
|
||||
# A newly instantiated object is unchanged:
|
||||
# person = Person.find_by_name('Uncle Bob')
|
||||
# person.changed? # => false
|
||||
#
|
||||
# Change the name:
|
||||
# person.name = 'Bob'
|
||||
# person.changed? # => true
|
||||
# person.name_changed? # => true
|
||||
# person.name_was # => 'Uncle Bob'
|
||||
# person.name_change # => ['Uncle Bob', 'Bob']
|
||||
# person.name = 'Bill'
|
||||
# person.name_change # => ['Uncle Bob', 'Bill']
|
||||
#
|
||||
# Save the changes:
|
||||
# person.save
|
||||
# person.changed? # => false
|
||||
# person.name_changed? # => false
|
||||
#
|
||||
# Assigning the same value leaves the attribute unchanged:
|
||||
# person.name = 'Bill'
|
||||
# person.name_changed? # => false
|
||||
# person.name_change # => nil
|
||||
#
|
||||
# Which attributes have changed?
|
||||
# person.name = 'Bob'
|
||||
# person.changed # => ['name']
|
||||
# person.changes # => { 'name' => ['Bill', 'Bob'] }
|
||||
#
|
||||
# Resetting an attribute returns it to its original state:
|
||||
# person.reset_name! # => 'Bill'
|
||||
# person.changed? # => false
|
||||
# person.name_changed? # => false
|
||||
# person.name # => 'Bill'
|
||||
#
|
||||
# Before modifying an attribute in-place:
|
||||
# person.name_will_change!
|
||||
# person.name << 'y'
|
||||
# person.name_change # => ['Bill', 'Billy']
|
||||
module Dirty
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
DIRTY_AFFIXES = [
|
||||
{ :suffix => '_changed?' },
|
||||
{ :suffix => '_change' },
|
||||
{ :suffix => '_will_change!' },
|
||||
{ :suffix => '_was' },
|
||||
{ :prefix => 'reset_', :suffix => '!' }
|
||||
]
|
||||
include ActiveModel::Dirty
|
||||
|
||||
included do
|
||||
attribute_method_affix *DIRTY_AFFIXES
|
||||
|
||||
alias_method_chain :save, :dirty
|
||||
alias_method_chain :save!, :dirty
|
||||
alias_method_chain :update, :dirty
|
||||
|
@ -63,30 +16,6 @@ module ActiveRecord
|
|||
self.partial_updates = true
|
||||
end
|
||||
|
||||
# Do any attributes have unsaved changes?
|
||||
# person.changed? # => false
|
||||
# person.name = 'bob'
|
||||
# person.changed? # => true
|
||||
def changed?
|
||||
!changed_attributes.empty?
|
||||
end
|
||||
|
||||
# List of attributes with unsaved changes.
|
||||
# person.changed # => []
|
||||
# person.name = 'bob'
|
||||
# person.changed # => ['name']
|
||||
def changed
|
||||
changed_attributes.keys
|
||||
end
|
||||
|
||||
# Map of changed attrs => [original value, new value].
|
||||
# person.changes # => {}
|
||||
# person.name = 'bob'
|
||||
# person.changes # => { 'name' => ['bill', 'bob'] }
|
||||
def changes
|
||||
changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
|
||||
end
|
||||
|
||||
# Attempts to +save+ the record and clears changed attributes if successful.
|
||||
def save_with_dirty(*args) #:nodoc:
|
||||
if status = save_without_dirty(*args)
|
||||
|
@ -97,49 +26,15 @@ module ActiveRecord
|
|||
|
||||
# Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
|
||||
def save_with_dirty!(*args) #:nodoc:
|
||||
status = save_without_dirty!(*args)
|
||||
changed_attributes.clear
|
||||
status
|
||||
save_without_dirty!(*args).tap { changed_attributes.clear }
|
||||
end
|
||||
|
||||
# <tt>reload</tt> the record and clears changed attributes.
|
||||
def reload_with_dirty(*args) #:nodoc:
|
||||
record = reload_without_dirty(*args)
|
||||
changed_attributes.clear
|
||||
record
|
||||
reload_without_dirty(*args).tap { changed_attributes.clear }
|
||||
end
|
||||
|
||||
private
|
||||
# Map of change <tt>attr => original value</tt>.
|
||||
def changed_attributes
|
||||
@changed_attributes ||= {}
|
||||
end
|
||||
|
||||
# Handle <tt>*_changed?</tt> for +method_missing+.
|
||||
def attribute_changed?(attr)
|
||||
changed_attributes.include?(attr)
|
||||
end
|
||||
|
||||
# Handle <tt>*_change</tt> for +method_missing+.
|
||||
def attribute_change(attr)
|
||||
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
|
||||
end
|
||||
|
||||
# Handle <tt>*_was</tt> for +method_missing+.
|
||||
def attribute_was(attr)
|
||||
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
|
||||
end
|
||||
|
||||
# Handle <tt>reset_*!</tt> for +method_missing+.
|
||||
def reset_attribute!(attr)
|
||||
self[attr] = changed_attributes[attr] if attribute_changed?(attr)
|
||||
end
|
||||
|
||||
# Handle <tt>*_will_change!</tt> for +method_missing+.
|
||||
def attribute_will_change!(attr)
|
||||
changed_attributes[attr] = clone_attribute_value(:read_attribute, attr)
|
||||
end
|
||||
|
||||
# Wrap write_attribute to remember original attribute value.
|
||||
def write_attribute(attr, value)
|
||||
attr = attr.to_s
|
||||
|
@ -182,23 +77,6 @@ module ActiveRecord
|
|||
|
||||
old != value
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def self.extended(base)
|
||||
class << base
|
||||
alias_method_chain :alias_attribute, :dirty
|
||||
end
|
||||
end
|
||||
|
||||
def alias_attribute_with_dirty(new_name, old_name)
|
||||
alias_attribute_without_dirty(new_name, old_name)
|
||||
DIRTY_AFFIXES.each do |affixes|
|
||||
module_eval <<-STR, __FILE__, __LINE__+1
|
||||
def #{affixes[:prefix]}#{new_name}#{affixes[:suffix]}; self.#{affixes[:prefix]}#{old_name}#{affixes[:suffix]}; end # def reset_subject!; self.reset_title!; end
|
||||
STR
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -180,27 +180,41 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
|
|||
end
|
||||
|
||||
def test_associate_with_create_and_invalid_options
|
||||
peeps = companies(:first_firm).developers.count
|
||||
assert_nothing_raised { companies(:first_firm).developers.create(:name => '0') }
|
||||
assert_equal peeps, companies(:first_firm).developers.count
|
||||
firm = companies(:first_firm)
|
||||
assert_no_difference('firm.developers.count') { assert_nothing_raised { firm.developers.create(:name => '0') } }
|
||||
end
|
||||
|
||||
def test_associate_with_create_and_valid_options
|
||||
peeps = companies(:first_firm).developers.count
|
||||
assert_nothing_raised { companies(:first_firm).developers.create(:name => 'developer') }
|
||||
assert_equal peeps + 1, companies(:first_firm).developers.count
|
||||
firm = companies(:first_firm)
|
||||
assert_difference('firm.developers.count', 1) { firm.developers.create(:name => 'developer') }
|
||||
end
|
||||
|
||||
def test_associate_with_create_bang_and_invalid_options
|
||||
peeps = companies(:first_firm).developers.count
|
||||
assert_raises(ActiveRecord::RecordInvalid) { companies(:first_firm).developers.create!(:name => '0') }
|
||||
assert_equal peeps, companies(:first_firm).developers.count
|
||||
firm = companies(:first_firm)
|
||||
assert_no_difference('firm.developers.count') { assert_raises(ActiveRecord::RecordInvalid) { firm.developers.create!(:name => '0') } }
|
||||
end
|
||||
|
||||
def test_associate_with_create_bang_and_valid_options
|
||||
peeps = companies(:first_firm).developers.count
|
||||
assert_nothing_raised { companies(:first_firm).developers.create!(:name => 'developer') }
|
||||
assert_equal peeps + 1, companies(:first_firm).developers.count
|
||||
firm = companies(:first_firm)
|
||||
assert_difference('firm.developers.count', 1) { firm.developers.create!(:name => 'developer') }
|
||||
end
|
||||
|
||||
def test_push_with_invalid_record
|
||||
firm = companies(:first_firm)
|
||||
assert_raises(ActiveRecord::RecordInvalid) { firm.developers << Developer.new(:name => '0') }
|
||||
end
|
||||
|
||||
def test_push_with_invalid_join_record
|
||||
repair_validations(Contract) do
|
||||
Contract.validate {|r| r.errors[:base] << 'Invalid Contract' }
|
||||
|
||||
firm = companies(:first_firm)
|
||||
lifo = Developer.new(:name => 'lifo')
|
||||
assert_raises(ActiveRecord::RecordInvalid) { firm.developers << lifo }
|
||||
|
||||
lifo = Developer.create!(:name => 'lifo')
|
||||
assert_raises(ActiveRecord::RecordInvalid) { firm.developers << lifo }
|
||||
end
|
||||
end
|
||||
|
||||
def test_clear_associations
|
||||
|
|
|
@ -59,7 +59,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
|
|||
assert t2.save, "Should now save t2 as unique"
|
||||
end
|
||||
|
||||
def test_validates_uniquness_with_newline_chars
|
||||
def test_validates_uniqueness_with_newline_chars
|
||||
Topic.validates_uniqueness_of(:title, :case_sensitive => false)
|
||||
|
||||
t = Topic.new("title" => "new\nline")
|
||||
|
|
|
@ -62,19 +62,27 @@ module ActiveSupport
|
|||
end
|
||||
end
|
||||
|
||||
RAILS_CACHE_ID = ENV["RAILS_CACHE_ID"]
|
||||
RAILS_APP_VERION = ENV["RAILS_APP_VERION"]
|
||||
EXPANDED_CACHE = RAILS_CACHE_ID || RAILS_APP_VERION
|
||||
|
||||
def self.expand_cache_key(key, namespace = nil)
|
||||
expanded_cache_key = namespace ? "#{namespace}/" : ""
|
||||
|
||||
if ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]
|
||||
expanded_cache_key << "#{ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]}/"
|
||||
if EXPANDED_CACHE
|
||||
expanded_cache_key << "#{RAILS_CACHE_ID || RAILS_APP_VERION}/"
|
||||
end
|
||||
|
||||
expanded_cache_key << case
|
||||
when key.respond_to?(:cache_key)
|
||||
expanded_cache_key <<
|
||||
if key.respond_to?(:cache_key)
|
||||
key.cache_key
|
||||
when key.is_a?(Array)
|
||||
elsif key.is_a?(Array)
|
||||
if key.size > 1
|
||||
key.collect { |element| expand_cache_key(element) }.to_param
|
||||
when key
|
||||
else
|
||||
key.first.to_param
|
||||
end
|
||||
elsif key
|
||||
key.to_param
|
||||
end.to_s
|
||||
|
||||
|
|
|
@ -46,11 +46,12 @@ class Class
|
|||
end # end
|
||||
" unless options[:instance_writer] == false } # # instance writer above is generated unless options[:instance_writer] == false
|
||||
EOS
|
||||
self.send("#{sym}=", yield) if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
def cattr_accessor(*syms)
|
||||
def cattr_accessor(*syms, &blk)
|
||||
cattr_reader(*syms)
|
||||
cattr_writer(*syms)
|
||||
cattr_writer(*syms, &blk)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -26,13 +26,14 @@ class Class
|
|||
end
|
||||
end
|
||||
|
||||
def superclass_delegating_writer(*names)
|
||||
def superclass_delegating_writer(*names, &block)
|
||||
names.each do |name|
|
||||
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
||||
def self.#{name}=(value) # def self.property=(value)
|
||||
@#{name} = value # @property = value
|
||||
end # end
|
||||
EOS
|
||||
self.send("#{name}=", yield) if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -42,8 +43,8 @@ class Class
|
|||
# delegate to their superclass unless they have been given a
|
||||
# specific value. This stops the strange situation where values
|
||||
# set after class definition don't get applied to subclasses.
|
||||
def superclass_delegating_accessor(*names)
|
||||
def superclass_delegating_accessor(*names, &block)
|
||||
superclass_delegating_reader(*names)
|
||||
superclass_delegating_writer(*names)
|
||||
superclass_delegating_writer(*names, &block)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -121,8 +121,13 @@ class Module
|
|||
|
||||
module_eval(<<-EOS, file, line)
|
||||
def #{prefix}#{method}(*args, &block) # def customer_name(*args, &block)
|
||||
#{on_nil} if #{to}.nil?
|
||||
#{to}.__send__(#{method.inspect}, *args, &block) # client && client.__send__(:name, *args, &block)
|
||||
#{to}.__send__(#{method.inspect}, *args, &block) # client.__send__(:name, *args, &block)
|
||||
rescue NoMethodError # rescue NoMethodError
|
||||
if #{to}.nil? # if client.nil?
|
||||
#{on_nil}
|
||||
else # else
|
||||
raise # raise
|
||||
end # end
|
||||
end # end
|
||||
EOS
|
||||
end
|
||||
|
|
|
@ -32,7 +32,7 @@ end
|
|||
Somewhere = Struct.new(:street, :city)
|
||||
|
||||
Someone = Struct.new(:name, :place) do
|
||||
delegate :street, :city, :to => :place
|
||||
delegate :street, :city, :to_f, :to => :place
|
||||
delegate :state, :to => :@place
|
||||
delegate :upcase, :to => "place.city"
|
||||
end
|
||||
|
@ -44,6 +44,7 @@ end
|
|||
|
||||
Project = Struct.new(:description, :person) do
|
||||
delegate :name, :to => :person, :allow_nil => true
|
||||
delegate :to_f, :to => :description, :allow_nil => true
|
||||
end
|
||||
|
||||
class Name
|
||||
|
@ -145,6 +146,16 @@ class ModuleTest < Test::Unit::TestCase
|
|||
assert_raise(RuntimeError) { david.street }
|
||||
end
|
||||
|
||||
def test_delegation_to_method_that_exists_on_nil
|
||||
nil_person = Someone.new(nil)
|
||||
assert_equal 0.0, nil_person.to_f
|
||||
end
|
||||
|
||||
def test_delegation_to_method_that_exists_on_nil_when_allowing_nil
|
||||
nil_project = Project.new(nil)
|
||||
assert_equal 0.0, nil_project.to_f
|
||||
end
|
||||
|
||||
def test_parent
|
||||
assert_equal Yz::Zy, Yz::Zy::Cd.parent
|
||||
assert_equal Yz, Yz::Zy.parent
|
||||
|
|
|
@ -28,14 +28,14 @@ cd "#{root_dir}/activerecord" do
|
|||
puts
|
||||
puts "[CruiseControl] Building ActiveRecord with MySQL"
|
||||
puts
|
||||
build_results[:activerecord_mysql] = system 'rake test_mysql'
|
||||
build_results[:activerecord_mysql] = system 'rake mysql:rebuild_databases && rake test_mysql'
|
||||
end
|
||||
|
||||
cd "#{root_dir}/activerecord" do
|
||||
puts
|
||||
puts "[CruiseControl] Building ActiveRecord with PostgreSQL"
|
||||
puts
|
||||
build_results[:activerecord_postgresql8] = system 'rake test_postgresql'
|
||||
build_results[:activerecord_postgresql8] = system 'rake postgresql:rebuild_databases && rake test_postgresql'
|
||||
end
|
||||
|
||||
cd "#{root_dir}/activerecord" do
|
||||
|
|
Loading…
Reference in a new issue