mirror of
https://github.com/github/rails.git
synced 2026-04-26 03:00:59 -04:00
Merge branch 'master' of git://github.com/rails/rails into old
This commit is contained in:
@@ -566,7 +566,7 @@ module ActionMailer #:nodoc:
|
||||
@template = initialize_template_class(body)
|
||||
layout = _pick_layout(layout, true) unless
|
||||
ActionController::Base.exempt_from_layout.include?(template.handler)
|
||||
@template._render_template_with_layout(template, layout, {})
|
||||
@template._render_template(template, layout, {})
|
||||
ensure
|
||||
@current_template_content_type = nil
|
||||
end
|
||||
@@ -592,7 +592,7 @@ module ActionMailer #:nodoc:
|
||||
!template || ActionController::Base.exempt_from_layout.include?(template.handler))
|
||||
|
||||
if template
|
||||
@template._render_template_with_layout(template, layout, opts)
|
||||
@template._render_template(template, layout, opts)
|
||||
elsif inline = opts[:inline]
|
||||
@template._render_inline(inline, layout, opts)
|
||||
end
|
||||
|
||||
@@ -573,12 +573,14 @@ class ActionMailerTest < Test::Unit::TestCase
|
||||
@info_contents, @debug_contents = "", ""
|
||||
end
|
||||
|
||||
def info(str)
|
||||
@info_contents << str
|
||||
def info(str = nil, &blk)
|
||||
@info_contents << str if str
|
||||
@info_contents << blk.call if block_given?
|
||||
end
|
||||
|
||||
def debug(str)
|
||||
@debug_contents << str
|
||||
def debug(str = nil, &blk)
|
||||
@debug_contents << str if str
|
||||
@debug_contents << blk.call if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
*Edge*
|
||||
|
||||
* Make sure javascript_include_tag/stylesheet_link_tag does not append ".js" or ".css" onto external urls. #1664 [Matthew Rudy Jacobs]
|
||||
|
||||
* Ruby 1.9: fix Content-Length for multibyte send_data streaming. #2661 [Sava Chankov]
|
||||
|
||||
* Ruby 1.9: ERB template encoding using a magic comment at the top of the file. [Jeremy Kemper]
|
||||
<%# encoding: utf-8 %>
|
||||
|
||||
|
||||
@@ -45,11 +45,9 @@ end
|
||||
OK = [200, {}, []]
|
||||
MetalPostController = lambda { OK }
|
||||
|
||||
if ActionController.const_defined?(:Http)
|
||||
class HttpPostController < ActionController::Http
|
||||
def index
|
||||
self.response_body = ''
|
||||
end
|
||||
class HttpPostController < ActionController::Metal
|
||||
def index
|
||||
self.response_body = ''
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ $:.push "rails/actionpack/lib"
|
||||
|
||||
require "action_controller"
|
||||
|
||||
class Kaigi < ActionController::Http
|
||||
class Kaigi < ActionController::Metal
|
||||
include AbstractController::Callbacks
|
||||
include ActionController::RackConvenience
|
||||
include ActionController::Renderer
|
||||
@@ -13,7 +13,7 @@ class Kaigi < ActionController::Http
|
||||
before_filter :set_name
|
||||
append_view_path "views"
|
||||
|
||||
def _action_view
|
||||
def view_context
|
||||
self
|
||||
end
|
||||
|
||||
@@ -23,7 +23,7 @@ class Kaigi < ActionController::Http
|
||||
|
||||
DEFAULT_LAYOUT = Object.new.tap {|l| def l.render(*) yield end }
|
||||
|
||||
def _render_template_from_controller(template, layout = DEFAULT_LAYOUT, options = {}, partial = false)
|
||||
def render_template(template, layout = DEFAULT_LAYOUT, options = {}, partial = false)
|
||||
ret = template.render(self, {})
|
||||
layout.render(self, {}) { ret }
|
||||
end
|
||||
|
||||
16
actionpack/lib/abstract_controller.rb
Normal file
16
actionpack/lib/abstract_controller.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
require "active_support/core_ext/module/attr_internal"
|
||||
require "active_support/core_ext/module/delegation"
|
||||
|
||||
module AbstractController
|
||||
autoload :Base, "abstract_controller/base"
|
||||
autoload :Benchmarker, "abstract_controller/benchmarker"
|
||||
autoload :Callbacks, "abstract_controller/callbacks"
|
||||
autoload :Helpers, "abstract_controller/helpers"
|
||||
autoload :Layouts, "abstract_controller/layouts"
|
||||
autoload :Logger, "abstract_controller/logger"
|
||||
autoload :Renderer, "abstract_controller/renderer"
|
||||
# === Exceptions
|
||||
autoload :ActionNotFound, "abstract_controller/exceptions"
|
||||
autoload :DoubleRenderError, "abstract_controller/exceptions"
|
||||
autoload :Error, "abstract_controller/exceptions"
|
||||
end
|
||||
@@ -30,7 +30,7 @@ module AbstractController
|
||||
# instance methods on that abstract class. Public instance methods of
|
||||
# a controller would normally be considered action methods, so we
|
||||
# are removing those methods on classes declared as abstract
|
||||
# (ActionController::Http and ActionController::Base are defined
|
||||
# (ActionController::Metal and ActionController::Base are defined
|
||||
# as abstract)
|
||||
def internal_methods
|
||||
controller = self
|
||||
@@ -1,4 +1,4 @@
|
||||
require "action_controller/abstract/logger"
|
||||
require "abstract_controller/logger"
|
||||
|
||||
module AbstractController
|
||||
module Renderer
|
||||
@@ -19,11 +19,11 @@ module AbstractController
|
||||
# The view class must have the following methods:
|
||||
# View.for_controller[controller] Create a new ActionView instance for a
|
||||
# controller
|
||||
# View#_render_partial_from_controller[options]
|
||||
# View#render_partial[options]
|
||||
# - responsible for setting options[:_template]
|
||||
# - Returns String with the rendered partial
|
||||
# options<Hash>:: see _render_partial in ActionView::Base
|
||||
# View#_render_template_from_controller[template, layout, options, partial]
|
||||
# View#render_template[template, layout, options, partial]
|
||||
# - Returns String with the rendered template
|
||||
# template<ActionView::Template>:: The template to render
|
||||
# layout<ActionView::Template>:: The layout to render around the template
|
||||
@@ -31,8 +31,8 @@ module AbstractController
|
||||
# partial<Boolean>:: Whether or not the template to render is a partial
|
||||
#
|
||||
# Override this method in a to change the default behavior.
|
||||
def _action_view
|
||||
@_action_view ||= ActionView::Base.for_controller(self)
|
||||
def view_context
|
||||
@_view_context ||= ActionView::Base.for_controller(self)
|
||||
end
|
||||
|
||||
# Mostly abstracts the fact that calling render twice is a DoubleRenderError.
|
||||
@@ -54,8 +54,8 @@ module AbstractController
|
||||
# :api: plugin
|
||||
def render_to_body(options = {})
|
||||
# TODO: Refactor so we can just use the normal template logic for this
|
||||
if options[:_partial_object]
|
||||
_action_view._render_partial_from_controller(options)
|
||||
if options.key?(:_partial_object)
|
||||
view_context.render_partial(options)
|
||||
else
|
||||
_determine_template(options)
|
||||
_render_template(options)
|
||||
@@ -77,7 +77,7 @@ module AbstractController
|
||||
# _layout<ActionView::Template>:: The layout to wrap the template in (optional)
|
||||
# _partial<TrueClass, FalseClass>:: Whether or not the template to be rendered is a partial
|
||||
def _render_template(options)
|
||||
_action_view._render_template_from_controller(options[:_template], options[:_layout], options, options[:_partial])
|
||||
view_context.render_template(options)
|
||||
end
|
||||
|
||||
# The list of view paths for this controller. See ActionView::ViewPathSet for
|
||||
@@ -1,62 +1,62 @@
|
||||
module ActionController
|
||||
autoload :Base, "action_controller/base/base"
|
||||
autoload :ConditionalGet, "action_controller/base/conditional_get"
|
||||
autoload :HideActions, "action_controller/base/hide_actions"
|
||||
autoload :Http, "action_controller/base/http"
|
||||
autoload :Layouts, "action_controller/base/layouts"
|
||||
autoload :RackConvenience, "action_controller/base/rack_convenience"
|
||||
autoload :Rails2Compatibility, "action_controller/base/compatibility"
|
||||
autoload :Redirector, "action_controller/base/redirector"
|
||||
autoload :Renderer, "action_controller/base/renderer"
|
||||
autoload :RenderOptions, "action_controller/base/render_options"
|
||||
autoload :Renderers, "action_controller/base/render_options"
|
||||
autoload :Rescue, "action_controller/base/rescuable"
|
||||
autoload :Testing, "action_controller/base/testing"
|
||||
autoload :UrlFor, "action_controller/base/url_for"
|
||||
autoload :Session, "action_controller/base/session"
|
||||
autoload :Helpers, "action_controller/base/helpers"
|
||||
autoload :Base, "action_controller/base"
|
||||
autoload :ConditionalGet, "action_controller/metal/conditional_get"
|
||||
autoload :HideActions, "action_controller/metal/hide_actions"
|
||||
autoload :Metal, "action_controller/metal"
|
||||
autoload :Layouts, "action_controller/metal/layouts"
|
||||
autoload :RackConvenience, "action_controller/metal/rack_convenience"
|
||||
autoload :Rails2Compatibility, "action_controller/metal/compatibility"
|
||||
autoload :Redirector, "action_controller/metal/redirector"
|
||||
autoload :Renderer, "action_controller/metal/renderer"
|
||||
autoload :RenderOptions, "action_controller/metal/render_options"
|
||||
autoload :Renderers, "action_controller/metal/render_options"
|
||||
autoload :Rescue, "action_controller/metal/rescuable"
|
||||
autoload :Testing, "action_controller/metal/testing"
|
||||
autoload :UrlFor, "action_controller/metal/url_for"
|
||||
autoload :Session, "action_controller/metal/session"
|
||||
autoload :Helpers, "action_controller/metal/helpers"
|
||||
|
||||
# Ported modules
|
||||
# require 'action_controller/routing'
|
||||
autoload :Caching, 'action_controller/caching'
|
||||
autoload :Dispatcher, 'action_controller/dispatch/dispatcher'
|
||||
autoload :Integration, 'action_controller/testing/integration'
|
||||
autoload :MimeResponds, 'action_controller/base/mime_responds'
|
||||
autoload :MimeResponds, 'action_controller/metal/mime_responds'
|
||||
autoload :PolymorphicRoutes, 'action_controller/routing/generation/polymorphic_routes'
|
||||
autoload :RecordIdentifier, 'action_controller/record_identifier'
|
||||
autoload :Resources, 'action_controller/routing/resources'
|
||||
autoload :SessionManagement, 'action_controller/base/session_management'
|
||||
autoload :SessionManagement, 'action_controller/metal/session_management'
|
||||
autoload :TestCase, 'action_controller/testing/test_case'
|
||||
autoload :TestProcess, 'action_controller/testing/process'
|
||||
autoload :UrlRewriter, 'action_controller/routing/generation/url_rewriter'
|
||||
autoload :UrlWriter, 'action_controller/routing/generation/url_rewriter'
|
||||
|
||||
autoload :Verification, 'action_controller/base/verification'
|
||||
autoload :Flash, 'action_controller/base/flash'
|
||||
autoload :RequestForgeryProtection, 'action_controller/base/request_forgery_protection'
|
||||
autoload :Streaming, 'action_controller/base/streaming'
|
||||
autoload :HttpAuthentication, 'action_controller/base/http_authentication'
|
||||
autoload :FilterParameterLogging, 'action_controller/base/filter_parameter_logging'
|
||||
autoload :Verification, 'action_controller/metal/verification'
|
||||
autoload :Flash, 'action_controller/metal/flash'
|
||||
autoload :RequestForgeryProtection, 'action_controller/metal/request_forgery_protection'
|
||||
autoload :Streaming, 'action_controller/metal/streaming'
|
||||
autoload :HttpAuthentication, 'action_controller/metal/http_authentication'
|
||||
autoload :FilterParameterLogging, 'action_controller/metal/filter_parameter_logging'
|
||||
autoload :Translation, 'action_controller/translation'
|
||||
autoload :Cookies, 'action_controller/base/cookies'
|
||||
autoload :Cookies, 'action_controller/metal/cookies'
|
||||
|
||||
autoload :ActionControllerError, 'action_controller/base/exceptions'
|
||||
autoload :SessionRestoreError, 'action_controller/base/exceptions'
|
||||
autoload :RenderError, 'action_controller/base/exceptions'
|
||||
autoload :RoutingError, 'action_controller/base/exceptions'
|
||||
autoload :MethodNotAllowed, 'action_controller/base/exceptions'
|
||||
autoload :NotImplemented, 'action_controller/base/exceptions'
|
||||
autoload :UnknownController, 'action_controller/base/exceptions'
|
||||
autoload :MissingFile, 'action_controller/base/exceptions'
|
||||
autoload :RenderError, 'action_controller/base/exceptions'
|
||||
autoload :SessionOverflowError, 'action_controller/base/exceptions'
|
||||
autoload :UnknownHttpMethod, 'action_controller/base/exceptions'
|
||||
autoload :ActionControllerError, 'action_controller/metal/exceptions'
|
||||
autoload :SessionRestoreError, 'action_controller/metal/exceptions'
|
||||
autoload :RenderError, 'action_controller/metal/exceptions'
|
||||
autoload :RoutingError, 'action_controller/metal/exceptions'
|
||||
autoload :MethodNotAllowed, 'action_controller/metal/exceptions'
|
||||
autoload :NotImplemented, 'action_controller/metal/exceptions'
|
||||
autoload :UnknownController, 'action_controller/metal/exceptions'
|
||||
autoload :MissingFile, 'action_controller/metal/exceptions'
|
||||
autoload :RenderError, 'action_controller/metal/exceptions'
|
||||
autoload :SessionOverflowError, 'action_controller/metal/exceptions'
|
||||
autoload :UnknownHttpMethod, 'action_controller/metal/exceptions'
|
||||
|
||||
autoload :Routing, 'action_controller/routing'
|
||||
end
|
||||
|
||||
autoload :HTML, 'action_controller/vendor/html-scanner'
|
||||
autoload :AbstractController, 'action_controller/abstract'
|
||||
autoload :AbstractController, 'abstract_controller'
|
||||
|
||||
autoload :Rack, 'action_dispatch'
|
||||
autoload :ActionDispatch, 'action_dispatch'
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
require "active_support/core_ext/module/attr_internal"
|
||||
require "active_support/core_ext/module/delegation"
|
||||
|
||||
module AbstractController
|
||||
autoload :Base, "action_controller/abstract/base"
|
||||
autoload :Benchmarker, "action_controller/abstract/benchmarker"
|
||||
autoload :Callbacks, "action_controller/abstract/callbacks"
|
||||
autoload :Helpers, "action_controller/abstract/helpers"
|
||||
autoload :Layouts, "action_controller/abstract/layouts"
|
||||
autoload :Logger, "action_controller/abstract/logger"
|
||||
autoload :Renderer, "action_controller/abstract/renderer"
|
||||
# === Exceptions
|
||||
autoload :ActionNotFound, "action_controller/abstract/exceptions"
|
||||
autoload :DoubleRenderError, "action_controller/abstract/exceptions"
|
||||
autoload :Error, "action_controller/abstract/exceptions"
|
||||
end
|
||||
@@ -1,5 +1,5 @@
|
||||
module ActionController
|
||||
class Base < Http
|
||||
class Base < Metal
|
||||
abstract!
|
||||
|
||||
include AbstractController::Benchmarker
|
||||
@@ -129,8 +129,7 @@ module ActionController #:nodoc:
|
||||
end
|
||||
|
||||
def content_for_layout(controller)
|
||||
# TODO: Remove this when new base is merged in
|
||||
template = controller.respond_to?(:template) ? controller.template : controller._action_view
|
||||
template = controller.view_context
|
||||
template.layout && template.instance_variable_get('@cached_content_for_layout')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
require 'action_controller/abstract'
|
||||
|
||||
module ActionController
|
||||
# ActionController::Http provides a way to get a valid Rack application from a controller.
|
||||
# ActionController::Metal provides a way to get a valid Rack application from a controller.
|
||||
#
|
||||
# In AbstractController, dispatching is triggered directly by calling #process on a new controller.
|
||||
# ActionController::Http provides an #action method that returns a valid Rack application for a
|
||||
# ActionController::Metal provides an #action method that returns a valid Rack application for a
|
||||
# given action. Other rack builders, such as Rack::Builder, Rack::URLMap, and the Rails router,
|
||||
# can dispatch directly to the action returned by FooController.action(:index).
|
||||
class Http < AbstractController::Base
|
||||
class Metal < AbstractController::Base
|
||||
abstract!
|
||||
|
||||
# :api: public
|
||||
@@ -72,11 +70,11 @@ module ActionController
|
||||
def call(name, env)
|
||||
@_env = env
|
||||
process(name)
|
||||
to_rack
|
||||
to_a
|
||||
end
|
||||
|
||||
# :api: private
|
||||
def to_rack
|
||||
def to_a
|
||||
[status, headers, response_body]
|
||||
end
|
||||
|
||||
@@ -72,7 +72,7 @@ module ActionController
|
||||
|
||||
# TODO: Remove this after we flip
|
||||
def template
|
||||
@template ||= _action_view
|
||||
@template ||= view_context
|
||||
end
|
||||
|
||||
def process_action(*)
|
||||
@@ -141,7 +141,7 @@ module ActionController
|
||||
end
|
||||
|
||||
def view_paths
|
||||
_action_view.view_paths
|
||||
view_context.view_paths
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -29,11 +29,7 @@ module ActionController
|
||||
response.last_modified = options[:last_modified] if options[:last_modified]
|
||||
|
||||
if options[:public]
|
||||
cache_control = response.headers["Cache-Control"].split(",").map {|k| k.strip }
|
||||
cache_control.delete("private")
|
||||
cache_control.delete("no-cache")
|
||||
cache_control << "public"
|
||||
response.headers["Cache-Control"] = cache_control.join(', ')
|
||||
response.cache_control[:public] = true
|
||||
end
|
||||
|
||||
if request.fresh?(response)
|
||||
@@ -107,21 +103,10 @@ module ActionController
|
||||
# This method will overwrite an existing Cache-Control header.
|
||||
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
|
||||
def expires_in(seconds, options = {}) #:doc:
|
||||
cache_control = response.headers["Cache-Control"].split(",").map {|k| k.strip }
|
||||
response.cache_control.merge!(:max_age => seconds, :public => options.delete(:public))
|
||||
options.delete(:private)
|
||||
|
||||
cache_control << "max-age=#{seconds}"
|
||||
cache_control.delete("no-cache")
|
||||
if options[:public]
|
||||
cache_control.delete("private")
|
||||
cache_control << "public"
|
||||
else
|
||||
cache_control << "private"
|
||||
end
|
||||
|
||||
# This allows for additional headers to be passed through like 'max-stale' => 5.hours
|
||||
cache_control += options.symbolize_keys.reject{|k,v| k == :public || k == :private }.map{ |k,v| v == true ? k.to_s : "#{k.to_s}=#{v.to_s}"}
|
||||
|
||||
response.headers["Cache-Control"] = cache_control.join(', ')
|
||||
response.cache_control[:extras] = options.map {|k,v| "#{k}=#{v}"}
|
||||
end
|
||||
|
||||
# Sets a HTTP 1.1 Cache-Control header of "no-cache" so no caching should occur by the browser or
|
||||
@@ -20,7 +20,7 @@ module ActionController
|
||||
end
|
||||
|
||||
# :api: private
|
||||
def to_rack
|
||||
def to_a
|
||||
@_response.prepare!
|
||||
@_response.to_a
|
||||
end
|
||||
@@ -13,7 +13,7 @@ module ActionController
|
||||
<<-RUBY_EVAL
|
||||
if options.key?(:#{r})
|
||||
_process_options(options)
|
||||
return _render_#{r}(options[:#{r}], options)
|
||||
return render_#{r}(options[:#{r}], options)
|
||||
end
|
||||
RUBY_EVAL
|
||||
end
|
||||
@@ -52,7 +52,7 @@ module ActionController
|
||||
extend RenderOption
|
||||
register_renderer :json
|
||||
|
||||
def _render_json(json, options)
|
||||
def render_json(json, options)
|
||||
json = ActiveSupport::JSON.encode(json) unless json.respond_to?(:to_str)
|
||||
json = "#{options[:callback]}(#{json})" unless options[:callback].blank?
|
||||
self.content_type ||= Mime::JSON
|
||||
@@ -64,9 +64,9 @@ module ActionController
|
||||
extend RenderOption
|
||||
register_renderer :js
|
||||
|
||||
def _render_js(js, options)
|
||||
def render_js(js, options)
|
||||
self.content_type ||= Mime::JS
|
||||
self.response_body = js
|
||||
self.response_body = js.respond_to?(:to_js) ? js.to_js : js
|
||||
end
|
||||
end
|
||||
|
||||
@@ -74,7 +74,7 @@ module ActionController
|
||||
extend RenderOption
|
||||
register_renderer :xml
|
||||
|
||||
def _render_xml(xml, options)
|
||||
def render_xml(xml, options)
|
||||
self.content_type ||= Mime::XML
|
||||
self.response_body = xml.respond_to?(:to_xml) ? xml.to_xml : xml
|
||||
end
|
||||
@@ -84,8 +84,8 @@ module ActionController
|
||||
extend RenderOption
|
||||
register_renderer :update
|
||||
|
||||
def _render_update(proc, options)
|
||||
generator = ActionView::Helpers::PrototypeHelper::JavaScriptGenerator.new(_action_view, &proc)
|
||||
def render_update(proc, options)
|
||||
generator = ActionView::Helpers::PrototypeHelper::JavaScriptGenerator.new(view_context, &proc)
|
||||
self.content_type = Mime::JS
|
||||
self.response_body = generator.to_s
|
||||
end
|
||||
@@ -57,7 +57,7 @@ module ActionController
|
||||
when true
|
||||
options[:_prefix] = _prefix
|
||||
when String
|
||||
options[:_prefix] = _prefix unless partial.index('/')
|
||||
options[:_prefix] = _prefix unless partial.include?(?/)
|
||||
options[:_template_name] = partial
|
||||
else
|
||||
options[:_partial_object] = true
|
||||
@@ -1,3 +1,5 @@
|
||||
require 'active_support/core_ext/string/bytesize'
|
||||
|
||||
module ActionController #:nodoc:
|
||||
# Methods for sending arbitrary data and for streaming files to the browser,
|
||||
# instead of rendering.
|
||||
@@ -142,7 +144,7 @@ module ActionController #:nodoc:
|
||||
# instead. See ActionController::Base#render for more information.
|
||||
def send_data(data, options = {}) #:doc:
|
||||
logger.info "Sending data #{options[:filename]}" if logger
|
||||
send_file_headers! options.merge(:length => data.size)
|
||||
send_file_headers! options.merge(:length => data.bytesize)
|
||||
@performed_render = false
|
||||
render :status => options[:status], :text => data
|
||||
end
|
||||
@@ -179,7 +181,7 @@ module ActionController #:nodoc:
|
||||
# after it displays the "open/save" dialog, which means that if you
|
||||
# hit "open" the file isn't there anymore when the application that
|
||||
# is called for handling the download is run, so let's workaround that
|
||||
headers['Cache-Control'] = 'private' if headers['Cache-Control'] == 'no-cache'
|
||||
response.cache_control[:public] ||= false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,84 +0,0 @@
|
||||
#--
|
||||
# Copyright (c) 2004-2009 David Heinemeier Hansson
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#++
|
||||
|
||||
activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
|
||||
$:.unshift activesupport_path if File.directory?(activesupport_path)
|
||||
require 'active_support'
|
||||
|
||||
require File.join(File.dirname(__FILE__), "action_pack")
|
||||
|
||||
module ActionController
|
||||
# TODO: Review explicit to see if they will automatically be handled by
|
||||
# the initilizer if they are really needed.
|
||||
def self.load_all!
|
||||
[Base, Request, Response, UrlRewriter, UrlWriter]
|
||||
[ActionDispatch::Http::Headers]
|
||||
end
|
||||
|
||||
autoload :Base, 'action_controller/base/base'
|
||||
autoload :Benchmarking, 'action_controller/base/chained/benchmarking'
|
||||
autoload :Caching, 'action_controller/caching'
|
||||
autoload :Cookies, 'action_controller/base/cookies'
|
||||
autoload :Dispatcher, 'action_controller/dispatch/dispatcher'
|
||||
autoload :Filters, 'action_controller/base/chained/filters'
|
||||
autoload :Flash, 'action_controller/base/chained/flash'
|
||||
autoload :Helpers, 'action_controller/base/helpers'
|
||||
autoload :HttpAuthentication, 'action_controller/base/http_authentication'
|
||||
autoload :Integration, 'action_controller/testing/integration'
|
||||
autoload :IntegrationTest, 'action_controller/testing/integration'
|
||||
autoload :Layout, 'action_controller/base/layout'
|
||||
autoload :MimeResponds, 'action_controller/base/mime_responds'
|
||||
autoload :PolymorphicRoutes, 'action_controller/routing/generation/polymorphic_routes'
|
||||
autoload :RecordIdentifier, 'action_controller/record_identifier'
|
||||
autoload :Redirector, 'action_controller/base/redirect'
|
||||
autoload :Renderer, 'action_controller/base/render'
|
||||
autoload :RequestForgeryProtection, 'action_controller/base/request_forgery_protection'
|
||||
autoload :Rescue, 'action_controller/base/rescue'
|
||||
autoload :Resources, 'action_controller/routing/resources'
|
||||
autoload :Responder, 'action_controller/base/responder'
|
||||
autoload :Routing, 'action_controller/routing'
|
||||
autoload :SessionManagement, 'action_controller/base/session_management'
|
||||
autoload :Streaming, 'action_controller/base/streaming'
|
||||
autoload :TestCase, 'action_controller/testing/test_case'
|
||||
autoload :TestProcess, 'action_controller/testing/process'
|
||||
autoload :Translation, 'action_controller/translation'
|
||||
autoload :UrlEncodedPairParser, 'action_controller/dispatch/url_encoded_pair_parser'
|
||||
autoload :UrlRewriter, 'action_controller/routing/generation/url_rewriter'
|
||||
autoload :UrlWriter, 'action_controller/routing/generation/url_rewriter'
|
||||
autoload :Verification, 'action_controller/base/verification'
|
||||
autoload :FilterParameterLogging, 'action_controller/base/filter_parameter_logging'
|
||||
|
||||
module Assertions
|
||||
autoload :DomAssertions, 'action_controller/testing/assertions/dom'
|
||||
autoload :ModelAssertions, 'action_controller/testing/assertions/model'
|
||||
autoload :ResponseAssertions, 'action_controller/testing/assertions/response'
|
||||
autoload :RoutingAssertions, 'action_controller/testing/assertions/routing'
|
||||
autoload :SelectorAssertions, 'action_controller/testing/assertions/selector'
|
||||
autoload :TagAssertions, 'action_controller/testing/assertions/tag'
|
||||
end
|
||||
end
|
||||
|
||||
autoload :HTML, 'action_controller/vendor/html-scanner'
|
||||
|
||||
require 'action_dispatch'
|
||||
require 'action_view'
|
||||
@@ -36,21 +36,6 @@ module ActionController
|
||||
JOIN = '_'.freeze
|
||||
NEW = 'new'.freeze
|
||||
|
||||
# Returns plural/singular for a record or class. Example:
|
||||
#
|
||||
# partial_path(post) # => "posts/post"
|
||||
# partial_path(Person) # => "people/person"
|
||||
# partial_path(Person, "admin/games") # => "admin/people/person"
|
||||
def partial_path(record_or_class, controller_path = nil)
|
||||
name = model_name_from_record_or_class(record_or_class)
|
||||
|
||||
if controller_path && controller_path.include?("/")
|
||||
"#{File.dirname(controller_path)}/#{name.partial_path}"
|
||||
else
|
||||
name.partial_path
|
||||
end
|
||||
end
|
||||
|
||||
# The DOM class convention is to use the singular form of an object or class. Examples:
|
||||
#
|
||||
# dom_class(post) # => "post"
|
||||
|
||||
@@ -52,7 +52,7 @@ module ActionController #:nodoc:
|
||||
class TestResponse < ActionDispatch::TestResponse
|
||||
def recycle!
|
||||
@status = 200
|
||||
@header = Rack::Utils::HeaderHash.new(DEFAULT_HEADERS)
|
||||
@header = Rack::Utils::HeaderHash.new
|
||||
@writer = lambda { |x| @body << x }
|
||||
@block = nil
|
||||
@length = 0
|
||||
|
||||
@@ -32,8 +32,8 @@ module ActionDispatch # :nodoc:
|
||||
# end
|
||||
# end
|
||||
class Response < Rack::Response
|
||||
DEFAULT_HEADERS = { "Cache-Control" => "no-cache" }
|
||||
attr_accessor :request
|
||||
attr_reader :cache_control
|
||||
|
||||
attr_writer :header
|
||||
alias_method :headers=, :header=
|
||||
@@ -42,21 +42,26 @@ module ActionDispatch # :nodoc:
|
||||
|
||||
def initialize
|
||||
super
|
||||
@header = Rack::Utils::HeaderHash.new(DEFAULT_HEADERS)
|
||||
@cache_control = {}
|
||||
@header = Rack::Utils::HeaderHash.new
|
||||
end
|
||||
|
||||
def status=(status)
|
||||
@status = status.to_i
|
||||
end
|
||||
|
||||
# The response code of the request
|
||||
def response_code
|
||||
status.to_s[0,3].to_i rescue 0
|
||||
@status
|
||||
end
|
||||
|
||||
# Returns a String to ensure compatibility with Net::HTTPResponse
|
||||
def code
|
||||
status.to_s.split(' ')[0]
|
||||
@status.to_s
|
||||
end
|
||||
|
||||
def message
|
||||
status.to_s.split(' ',2)[1] || StatusCodes::STATUS_CODES[response_code]
|
||||
StatusCodes::STATUS_CODES[@status]
|
||||
end
|
||||
alias_method :status_message, :message
|
||||
|
||||
@@ -142,10 +147,7 @@ module ActionDispatch # :nodoc:
|
||||
def prepare!
|
||||
assign_default_content_type_and_charset!
|
||||
handle_conditional_get!
|
||||
set_content_length!
|
||||
convert_content_type!
|
||||
convert_language!
|
||||
convert_cookies!
|
||||
self["Set-Cookie"] ||= ""
|
||||
end
|
||||
|
||||
def each(&callback)
|
||||
@@ -196,22 +198,24 @@ module ActionDispatch # :nodoc:
|
||||
|
||||
private
|
||||
def handle_conditional_get!
|
||||
if etag? || last_modified?
|
||||
if etag? || last_modified? || !cache_control.empty?
|
||||
set_conditional_cache_control!
|
||||
elsif nonempty_ok_response?
|
||||
self.etag = body
|
||||
|
||||
if request && request.etag_matches?(etag)
|
||||
self.status = '304 Not Modified'
|
||||
self.status = 304
|
||||
self.body = []
|
||||
end
|
||||
|
||||
set_conditional_cache_control!
|
||||
else
|
||||
headers["Cache-Control"] = "no-cache"
|
||||
end
|
||||
end
|
||||
|
||||
def nonempty_ok_response?
|
||||
ok = !status || status.to_s[0..2] == '200'
|
||||
ok = !@status || @status == 200
|
||||
ok && string_body?
|
||||
end
|
||||
|
||||
@@ -220,40 +224,20 @@ module ActionDispatch # :nodoc:
|
||||
end
|
||||
|
||||
def set_conditional_cache_control!
|
||||
if headers['Cache-Control'] == DEFAULT_HEADERS['Cache-Control']
|
||||
headers['Cache-Control'] = 'private, max-age=0, must-revalidate'
|
||||
if cache_control.empty?
|
||||
cache_control.merge!(:public => false, :max_age => 0, :must_revalidate => true)
|
||||
end
|
||||
end
|
||||
|
||||
def convert_content_type!
|
||||
headers['Content-Type'] ||= "text/html"
|
||||
headers['Content-Type'] += "; charset=" + headers.delete('charset') if headers['charset']
|
||||
end
|
||||
public_cache, max_age, must_revalidate, extras =
|
||||
cache_control.values_at(:public, :max_age, :must_revalidate, :extras)
|
||||
|
||||
# Don't set the Content-Length for block-based bodies as that would mean
|
||||
# reading it all into memory. Not nice for, say, a 2GB streaming file.
|
||||
def set_content_length!
|
||||
if status && status.to_s[0..2] == '204'
|
||||
headers.delete('Content-Length')
|
||||
elsif length = headers['Content-Length']
|
||||
headers['Content-Length'] = length.to_s
|
||||
elsif string_body? && (!status || status.to_s[0..2] != '304')
|
||||
headers["Content-Length"] = Rack::Utils.bytesize(body).to_s
|
||||
end
|
||||
end
|
||||
options = []
|
||||
options << "max-age=#{max_age}" if max_age
|
||||
options << (public_cache ? "public" : "private")
|
||||
options << "must-revalidate" if must_revalidate
|
||||
options.concat(extras) if extras
|
||||
|
||||
def convert_language!
|
||||
headers["Content-Language"] = headers.delete("language") if headers["language"]
|
||||
end
|
||||
|
||||
def convert_cookies!
|
||||
headers['Set-Cookie'] =
|
||||
if header = headers['Set-Cookie']
|
||||
header = header.split("\n") if header.respond_to?(:to_str)
|
||||
header.compact
|
||||
else
|
||||
[]
|
||||
end
|
||||
headers["Cache-Control"] = options.join(", ")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,11 +12,11 @@ module ActionView
|
||||
#
|
||||
# Context.for_controller[controller] Create a new ActionView instance for a
|
||||
# controller
|
||||
# Context#_render_partial_from_controller[options]
|
||||
# Context#render_partial[options]
|
||||
# - responsible for setting options[:_template]
|
||||
# - Returns String with the rendered partial
|
||||
# options<Hash>:: see _render_partial in ActionView::Base
|
||||
# Context#_render_template_from_controller[template, layout, options, partial]
|
||||
# Context#render_template[template, layout, options, partial]
|
||||
# - Returns String with the rendered template
|
||||
# template<ActionView::Template>:: The template to render
|
||||
# layout<ActionView::Template>:: The layout to render around the template
|
||||
|
||||
@@ -171,7 +171,7 @@ module ActionView
|
||||
end
|
||||
|
||||
# Computes the path to a javascript asset in the public javascripts directory.
|
||||
# If the +source+ filename has no extension, .js will be appended.
|
||||
# If the +source+ filename has no extension, .js will be appended (except for explicit URIs)
|
||||
# Full paths from the document root will be passed through.
|
||||
# Used internally by javascript_include_tag to build the script path.
|
||||
#
|
||||
@@ -179,7 +179,7 @@ module ActionView
|
||||
# javascript_path "xmlhr" # => /javascripts/xmlhr.js
|
||||
# javascript_path "dir/xmlhr.js" # => /javascripts/dir/xmlhr.js
|
||||
# javascript_path "/dir/xmlhr" # => /dir/xmlhr.js
|
||||
# javascript_path "http://www.railsapplication.com/js/xmlhr" # => http://www.railsapplication.com/js/xmlhr.js
|
||||
# javascript_path "http://www.railsapplication.com/js/xmlhr" # => http://www.railsapplication.com/js/xmlhr
|
||||
# javascript_path "http://www.railsapplication.com/js/xmlhr.js" # => http://www.railsapplication.com/js/xmlhr.js
|
||||
def javascript_path(source)
|
||||
compute_public_path(source, 'javascripts', 'js')
|
||||
@@ -337,7 +337,7 @@ module ActionView
|
||||
end
|
||||
|
||||
# Computes the path to a stylesheet asset in the public stylesheets directory.
|
||||
# If the +source+ filename has no extension, <tt>.css</tt> will be appended.
|
||||
# If the +source+ filename has no extension, <tt>.css</tt> will be appended (except for explicit URIs).
|
||||
# Full paths from the document root will be passed through.
|
||||
# Used internally by +stylesheet_link_tag+ to build the stylesheet path.
|
||||
#
|
||||
@@ -345,8 +345,8 @@ module ActionView
|
||||
# stylesheet_path "style" # => /stylesheets/style.css
|
||||
# stylesheet_path "dir/style.css" # => /stylesheets/dir/style.css
|
||||
# stylesheet_path "/dir/style.css" # => /dir/style.css
|
||||
# stylesheet_path "http://www.railsapplication.com/css/style" # => http://www.railsapplication.com/css/style.css
|
||||
# stylesheet_path "http://www.railsapplication.com/css/style.js" # => http://www.railsapplication.com/css/style.css
|
||||
# stylesheet_path "http://www.railsapplication.com/css/style" # => http://www.railsapplication.com/css/style
|
||||
# stylesheet_path "http://www.railsapplication.com/css/style.css" # => http://www.railsapplication.com/css/style.css
|
||||
def stylesheet_path(source)
|
||||
compute_public_path(source, 'stylesheets', 'css')
|
||||
end
|
||||
@@ -629,11 +629,11 @@ module ActionView
|
||||
has_request = @controller.respond_to?(:request)
|
||||
|
||||
source_ext = File.extname(source)[1..-1]
|
||||
if ext && (source_ext.blank? || (ext != source_ext && File.exist?(File.join(ASSETS_DIR, dir, "#{source}.#{ext}"))))
|
||||
if ext && !is_uri?(source) && (source_ext.blank? || (ext != source_ext && File.exist?(File.join(ASSETS_DIR, dir, "#{source}.#{ext}"))))
|
||||
source += ".#{ext}"
|
||||
end
|
||||
|
||||
unless source =~ %r{^[-a-z]+://}
|
||||
unless is_uri?(source)
|
||||
source = "/#{dir}/#{source}" unless source[0] == ?/
|
||||
|
||||
source = rewrite_asset_path(source)
|
||||
@@ -645,10 +645,10 @@ module ActionView
|
||||
end
|
||||
end
|
||||
|
||||
if include_host && source !~ %r{^[-a-z]+://}
|
||||
if include_host && !is_uri?(source)
|
||||
host = compute_asset_host(source)
|
||||
|
||||
if has_request && !host.blank? && host !~ %r{^[-a-z]+://}
|
||||
if has_request && !host.blank? && !is_uri?(host)
|
||||
host = "#{@controller.request.protocol}#{host}"
|
||||
end
|
||||
|
||||
@@ -658,6 +658,10 @@ module ActionView
|
||||
end
|
||||
end
|
||||
|
||||
def is_uri?(path)
|
||||
path =~ %r{^[-a-z]+://}
|
||||
end
|
||||
|
||||
# Pick an asset host for this source. Returns +nil+ if no host is set,
|
||||
# the host if no wildcard is set, the host interpolated with the
|
||||
# numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4),
|
||||
@@ -798,7 +802,7 @@ module ActionView
|
||||
end
|
||||
|
||||
def asset_file_path!(path)
|
||||
unless path =~ %r{^[-a-z]+://}
|
||||
unless is_uri?(path)
|
||||
absolute_path = asset_file_path(path)
|
||||
raise(Errno::ENOENT, "Asset file not found at '#{absolute_path}'" ) unless File.exist?(absolute_path)
|
||||
return absolute_path
|
||||
|
||||
@@ -932,6 +932,10 @@ module ActionView
|
||||
|
||||
attr_accessor :object_name, :object, :options
|
||||
|
||||
def self.model_name
|
||||
@model_name ||= Struct.new(:partial_path).new(name.demodulize.underscore.sub!(/_builder$/, ''))
|
||||
end
|
||||
|
||||
def initialize(object_name, object, template, options, proc)
|
||||
@nested_child_index = {}
|
||||
@object_name, @object, @template, @options, @proc = object_name, object, template, options, proc
|
||||
|
||||
@@ -172,131 +172,89 @@ module ActionView
|
||||
module Partials
|
||||
extend ActiveSupport::Memoizable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
|
||||
included do
|
||||
attr_accessor :_partial
|
||||
attr_accessor :_partial
|
||||
end
|
||||
|
||||
def _render_partial_from_controller(*args)
|
||||
module ClassMethods
|
||||
def _partial_names
|
||||
@_partial_names ||= ActiveSupport::ConcurrentHash.new
|
||||
end
|
||||
end
|
||||
|
||||
def render_partial(options)
|
||||
@assigns_added = false
|
||||
_render_partial(*args)
|
||||
_render_partial(options)
|
||||
end
|
||||
|
||||
def _render_partial(options = {}) #:nodoc:
|
||||
def _render_partial(options, &block) #:nodoc:
|
||||
options[:locals] ||= {}
|
||||
|
||||
case path = partial = options[:partial]
|
||||
when *_array_like_objects
|
||||
return _render_partial_collection(partial, options)
|
||||
else
|
||||
if partial.is_a?(ActionView::Helpers::FormBuilder)
|
||||
path = partial.class.to_s.demodulize.underscore.sub(/_builder$/, '')
|
||||
options[:locals].merge!(path.to_sym => partial)
|
||||
elsif !partial.is_a?(String)
|
||||
options[:object] = object = partial
|
||||
path = ActionController::RecordIdentifier.partial_path(object, controller_path)
|
||||
end
|
||||
_, _, prefix, object = parts = partial_parts(path, options)
|
||||
parts[1] = {:formats => parts[1]}
|
||||
template = find_by_parts(*parts)
|
||||
_render_partial_object(template, options, (object unless object == true))
|
||||
path = partial = options[:partial]
|
||||
|
||||
if partial.respond_to?(:to_ary)
|
||||
return _render_partial_collection(partial, options, &block)
|
||||
elsif !partial.is_a?(String)
|
||||
options[:object] = object = partial
|
||||
path = _partial_path(object)
|
||||
end
|
||||
|
||||
_render_partial_object(_pick_partial_template(path), options, &block)
|
||||
end
|
||||
|
||||
private
|
||||
def partial_parts(name, options)
|
||||
segments = name.split("/")
|
||||
parts = segments.pop.split(".")
|
||||
|
||||
case parts.size
|
||||
when 1
|
||||
parts
|
||||
when 2, 3
|
||||
extension = parts.delete_at(1).to_sym
|
||||
if formats.include?(extension)
|
||||
self.formats.replace [extension]
|
||||
def _partial_path(object)
|
||||
self.class._partial_names[[controller.class, object.class]] ||= begin
|
||||
name = object.class.model_name
|
||||
if controller_path && controller_path.include?("/")
|
||||
File.join(File.dirname(controller_path), name.partial_path)
|
||||
else
|
||||
name.partial_path
|
||||
end
|
||||
parts.pop if parts.size == 2
|
||||
end
|
||||
|
||||
path = parts.join(".")
|
||||
prefix = segments[0..-1].join("/")
|
||||
prefix = prefix.blank? ? controller_path : prefix
|
||||
parts = [path, formats, prefix]
|
||||
parts.push options[:object] || true
|
||||
end
|
||||
|
||||
def _render_partial_with_block(layout, block, options)
|
||||
@_proc_for_layout = block
|
||||
concat(_render_partial(options.merge(:partial => layout)))
|
||||
ensure
|
||||
@_proc_for_layout = nil
|
||||
end
|
||||
|
||||
def _render_partial_with_layout(layout, options)
|
||||
if layout
|
||||
prefix = controller && !layout.include?("/") ? controller.controller_path : nil
|
||||
layout = find_by_parts(layout, {:formats => formats}, prefix, true)
|
||||
end
|
||||
content = _render_partial(options)
|
||||
return _render_content_with_layout(content, layout, options[:locals])
|
||||
end
|
||||
|
||||
def _array_like_objects
|
||||
array_like = [Array]
|
||||
if defined?(ActiveRecord)
|
||||
array_like.push(ActiveRecord::Associations::AssociationCollection, ActiveRecord::NamedScope::Scope)
|
||||
end
|
||||
array_like
|
||||
end
|
||||
|
||||
def _render_partial_object(template, options, object = nil)
|
||||
if options.key?(:collection)
|
||||
_render_partial_collection(options.delete(:collection), options, template)
|
||||
else
|
||||
locals = (options[:locals] ||= {})
|
||||
object ||= locals[:object] || locals[template.variable_name]
|
||||
|
||||
_set_locals(object, locals, template, options)
|
||||
|
||||
options[:_template] = template
|
||||
|
||||
_render_template(template, locals)
|
||||
end
|
||||
end
|
||||
|
||||
def _set_locals(object, locals, template, options)
|
||||
def _render_partial_template(template, locals, object, options = {}, &block)
|
||||
options[:_template] = template
|
||||
locals[:object] = locals[template.variable_name] = object
|
||||
locals[options[:as]] = object if options[:as]
|
||||
|
||||
_render_single_template(template, locals, &block)
|
||||
end
|
||||
|
||||
def _render_partial_collection(collection, options = {}, passed_template = nil) #:nodoc:
|
||||
def _render_partial_object(template, options, &block)
|
||||
if options.key?(:collection)
|
||||
_render_partial_collection(options.delete(:collection), options, template, &block)
|
||||
else
|
||||
locals = (options[:locals] ||= {})
|
||||
object = options[:object] || locals[:object] || locals[template.variable_name]
|
||||
|
||||
_render_partial_template(template, locals, object, options, &block)
|
||||
end
|
||||
end
|
||||
|
||||
def _render_partial_collection(collection, options = {}, template = nil, &block) #:nodoc:
|
||||
return nil if collection.blank?
|
||||
|
||||
spacer = options[:spacer_template] ? _render_partial(:partial => options[:spacer_template]) : ''
|
||||
|
||||
locals = (options[:locals] ||= {})
|
||||
index, @_partial_path = 0, nil
|
||||
if options.key?(:spacer_template)
|
||||
spacer = _render_partial(:partial => options[:spacer_template])
|
||||
end
|
||||
|
||||
locals, index = options[:locals] || {}, 0
|
||||
|
||||
collection.map do |object|
|
||||
options[:_template] = template = passed_template || begin
|
||||
_partial_path =
|
||||
ActionController::RecordIdentifier.partial_path(object, controller_path)
|
||||
template = _pick_partial_template(_partial_path)
|
||||
end
|
||||
|
||||
_set_locals(object, locals, template, options)
|
||||
locals[template.counter_name] = index
|
||||
|
||||
tmp = template || _pick_partial_template(_partial_path(object))
|
||||
locals[tmp.counter_name] = index
|
||||
index += 1
|
||||
|
||||
_render_template(template, locals)
|
||||
|
||||
_render_partial_template(tmp, locals, object, options, &block)
|
||||
end.join(spacer)
|
||||
end
|
||||
|
||||
def _pick_partial_template(partial_path) #:nodoc:
|
||||
prefix = controller_path unless partial_path.include?('/')
|
||||
prefix = controller_path unless partial_path.include?(?/)
|
||||
find_by_parts(partial_path, {:formats => formats}, prefix, true)
|
||||
end
|
||||
memoize :_pick_partial_template
|
||||
end
|
||||
end
|
||||
|
||||
@@ -10,24 +10,25 @@ module ActionView
|
||||
#
|
||||
# If no options hash is passed or :update specified, the default is to render a partial and use the second parameter
|
||||
# as the locals hash.
|
||||
def render(options = {}, local_assigns = {}, &block) #:nodoc:
|
||||
local_assigns ||= {}
|
||||
|
||||
@exempt_from_layout = true
|
||||
|
||||
def render(options = {}, locals = {}, &block) #:nodoc:
|
||||
case options
|
||||
when String, NilClass
|
||||
_render_partial(:partial => options, :locals => locals || {})
|
||||
when Hash
|
||||
options[:locals] ||= {}
|
||||
layout = options[:layout]
|
||||
|
||||
return _render_partial_with_layout(layout, options) if options.key?(:partial)
|
||||
return _render_partial_with_block(layout, block, options) if block_given?
|
||||
|
||||
|
||||
if block_given?
|
||||
return concat(_render_partial(options.merge(:partial => layout), &block))
|
||||
elsif options.key?(:partial)
|
||||
layout = _pick_partial_template(layout) if layout
|
||||
return _render_content(_render_partial(options), layout, options[:locals])
|
||||
end
|
||||
|
||||
layout = find_by_parts(layout, {:formats => formats}) if layout
|
||||
|
||||
|
||||
if file = options[:file]
|
||||
template = find_by_parts(file, {:formats => formats})
|
||||
_render_template_with_layout(template, layout, :locals => options[:locals])
|
||||
_render_template(template, layout, :locals => options[:locals] || {})
|
||||
elsif inline = options[:inline]
|
||||
_render_inline(inline, layout, options)
|
||||
elsif text = options[:text]
|
||||
@@ -35,35 +36,33 @@ module ActionView
|
||||
end
|
||||
when :update
|
||||
update_page(&block)
|
||||
when String, NilClass
|
||||
_render_partial(:partial => options, :locals => local_assigns)
|
||||
end
|
||||
end
|
||||
|
||||
def _render_content_with_layout(content, layout, locals)
|
||||
|
||||
def _render_content(content, layout, locals)
|
||||
return content unless layout
|
||||
|
||||
|
||||
locals ||= {}
|
||||
|
||||
if controller && layout
|
||||
@_layout = layout.identifier
|
||||
logger.info("Rendering template within #{layout.identifier}") if logger
|
||||
end
|
||||
|
||||
|
||||
begin
|
||||
old_content, @_content_for[:layout] = @_content_for[:layout], content
|
||||
|
||||
@cached_content_for_layout = @_content_for[:layout]
|
||||
_render_template(layout, locals)
|
||||
_render_single_template(layout, locals)
|
||||
ensure
|
||||
@_content_for[:layout] = old_content
|
||||
end
|
||||
end
|
||||
|
||||
# You can think of a layout as a method that is called with a block. This method
|
||||
# returns the block that the layout is called with. If the user calls yield :some_name,
|
||||
# the block, by default, returns content_for(:some_name). If the user calls yield,
|
||||
# the default block returns content_for(:layout).
|
||||
# You can think of a layout as a method that is called with a block. _layout_for
|
||||
# returns the contents that are yielded to the layout. If the user calls yield
|
||||
# :some_name, the block, by default, returns content_for(:some_name). If the user
|
||||
# calls yield, the default block returns content_for(:layout).
|
||||
#
|
||||
# The user can override this default by passing a block to the layout.
|
||||
#
|
||||
@@ -92,15 +91,28 @@ module ActionView
|
||||
# In this case, the layout would receive the block passed into <tt>render :layout</tt>,
|
||||
# and the Struct specified in the layout would be passed into the block. The result
|
||||
# would be <html>Hello David</html>.
|
||||
def layout_proc(name)
|
||||
@_default_layout ||= proc { |*names| @_content_for[names.first || :layout] }
|
||||
!@_content_for.key?(name) && @_proc_for_layout || @_default_layout
|
||||
def _layout_for(names, &block)
|
||||
with_output_buffer do
|
||||
# This is due to the potentially ambiguous use of yield when
|
||||
# a block is passed in to a template *and* there is a content_for()
|
||||
# of the same name. Suggested solution: require explicit use of content_for
|
||||
# in these ambiguous cases.
|
||||
#
|
||||
# We would be able to continue supporting yield in all non-ambiguous
|
||||
# cases. Question: should we deprecate yield in favor of content_for
|
||||
# and reserve yield for cases where there is a yield into a real block?
|
||||
if @_content_for.key?(names.first) || !block_given?
|
||||
return @_content_for[names.first || :layout]
|
||||
else
|
||||
return yield(names)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def _render_template(template, local_assigns = {})
|
||||
def _render_single_template(template, locals = {}, &block)
|
||||
with_template(template) do
|
||||
template.render(self, local_assigns) do |*names|
|
||||
capture(*names, &layout_proc(names.first))
|
||||
template.render(self, locals) do |*names|
|
||||
_layout_for(names, &block)
|
||||
end
|
||||
end
|
||||
rescue Exception => e
|
||||
@@ -115,32 +127,42 @@ module ActionView
|
||||
def _render_inline(inline, layout, options)
|
||||
handler = Template.handler_class_for_extension(options[:type] || "erb")
|
||||
template = Template.new(options[:inline], "inline #{options[:inline].inspect}", handler, {})
|
||||
content = _render_template(template, options[:locals] || {})
|
||||
layout ? _render_content_with_layout(content, layout, options[:locals]) : content
|
||||
content = _render_single_template(template, options[:locals] || {})
|
||||
layout ? _render_content(content, layout, options[:locals]) : content
|
||||
end
|
||||
|
||||
def _render_text(text, layout, options)
|
||||
layout ? _render_content_with_layout(text, layout, options[:locals]) : text
|
||||
layout ? _render_content(text, layout, options[:locals]) : text
|
||||
end
|
||||
|
||||
def _render_template_from_controller(*args)
|
||||
# This is the API to render a ViewContext's template from a controller.
|
||||
#
|
||||
# Internal Options:
|
||||
# _template:: The Template object to render
|
||||
# _layout:: The layout, if any, to wrap the Template in
|
||||
# _partial:: true if the template is a partial
|
||||
def render_template(options)
|
||||
@assigns_added = nil
|
||||
_render_template_with_layout(*args)
|
||||
template, layout, partial = options.values_at(:_template, :_layout, :_partial)
|
||||
_render_template(template, layout, options, partial)
|
||||
end
|
||||
|
||||
def _render_template_with_layout(template, layout = nil, options = {}, partial = false)
|
||||
logger && logger.info("Rendering #{template.identifier}#{' (#{options[:status]})' if options[:status]}")
|
||||
def _render_template(template, layout = nil, options = {}, partial = nil)
|
||||
logger && logger.info do
|
||||
msg = "Rendering #{template.identifier}"
|
||||
msg << " (#{options[:status]})" if options[:status]
|
||||
msg
|
||||
end
|
||||
|
||||
locals = options[:locals] || {}
|
||||
|
||||
content = if partial
|
||||
object = partial unless partial == true
|
||||
_render_partial_object(template, options, object)
|
||||
_render_partial_object(template, options)
|
||||
else
|
||||
_render_template(template, locals)
|
||||
_render_single_template(template, locals)
|
||||
end
|
||||
|
||||
layout ? _render_content_with_layout(content, layout, locals) : content
|
||||
|
||||
_render_content(content, layout, locals)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -9,14 +9,14 @@ module ActionView
|
||||
end
|
||||
|
||||
attr_internal :rendered
|
||||
alias_method :_render_template_without_template_tracking, :_render_template
|
||||
def _render_template(template, local_assigns = {})
|
||||
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)
|
||||
_render_template_without_template_tracking(template, local_assigns, &block)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ require 'rubygems'
|
||||
require 'test/unit'
|
||||
require 'active_support'
|
||||
require 'active_support/test_case'
|
||||
require 'action_controller/abstract'
|
||||
require 'abstract_controller'
|
||||
require 'action_view'
|
||||
require 'action_view/base'
|
||||
require 'action_dispatch'
|
||||
|
||||
@@ -13,7 +13,6 @@ require 'test/unit'
|
||||
require 'active_support'
|
||||
|
||||
require 'active_support/test_case'
|
||||
require 'action_controller/abstract'
|
||||
require 'action_controller'
|
||||
require 'fixture_template'
|
||||
require 'action_controller/testing/process'
|
||||
@@ -78,7 +77,7 @@ module ActionController
|
||||
def assert_template(options = {}, message = nil)
|
||||
validate_request!
|
||||
|
||||
hax = @controller._action_view.instance_variable_get(:@_rendered)
|
||||
hax = @controller.view_context.instance_variable_get(:@_rendered)
|
||||
|
||||
case options
|
||||
when NilClass, String
|
||||
|
||||
@@ -17,9 +17,10 @@ class LoggingTest < ActionController::TestCase
|
||||
@level = Logger::DEBUG
|
||||
end
|
||||
|
||||
def method_missing(method, *args)
|
||||
def method_missing(method, *args, &blk)
|
||||
@logged ||= []
|
||||
@logged << args.first
|
||||
@logged << blk.call if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -54,24 +54,6 @@ class RecordIdentifierTest < Test::Unit::TestCase
|
||||
assert_equal "edit_#{@singular}_1", dom_id(@record, :edit)
|
||||
end
|
||||
|
||||
def test_partial_path
|
||||
expected = "#{@plural}/#{@singular}"
|
||||
assert_equal expected, partial_path(@record)
|
||||
assert_equal expected, partial_path(Comment)
|
||||
end
|
||||
|
||||
def test_partial_path_with_namespaced_controller_path
|
||||
expected = "admin/#{@plural}/#{@singular}"
|
||||
assert_equal expected, partial_path(@record, "admin/posts")
|
||||
assert_equal expected, partial_path(@klass, "admin/posts")
|
||||
end
|
||||
|
||||
def test_partial_path_with_not_namespaced_controller_path
|
||||
expected = "#{@plural}/#{@singular}"
|
||||
assert_equal expected, partial_path(@record, "posts")
|
||||
assert_equal expected, partial_path(@klass, "posts")
|
||||
end
|
||||
|
||||
def test_dom_class
|
||||
assert_equal @singular, dom_class(@record)
|
||||
end
|
||||
@@ -101,42 +83,3 @@ class RecordIdentifierTest < Test::Unit::TestCase
|
||||
RecordIdentifier.send(method, *args)
|
||||
end
|
||||
end
|
||||
|
||||
class NestedRecordIdentifierTest < RecordIdentifierTest
|
||||
def setup
|
||||
@klass = Comment::Nested
|
||||
@record = @klass.new
|
||||
@singular = 'comment_nested'
|
||||
@plural = 'comment_nesteds'
|
||||
end
|
||||
|
||||
def test_partial_path
|
||||
expected = "comment/nesteds/nested"
|
||||
assert_equal expected, partial_path(@record)
|
||||
assert_equal expected, partial_path(Comment::Nested)
|
||||
end
|
||||
|
||||
def test_partial_path_with_namespaced_controller_path
|
||||
expected = "admin/comment/nesteds/nested"
|
||||
assert_equal expected, partial_path(@record, "admin/posts")
|
||||
assert_equal expected, partial_path(@klass, "admin/posts")
|
||||
end
|
||||
|
||||
def test_partial_path_with_deeper_namespaced_controller_path
|
||||
expected = "deeper/admin/comment/nesteds/nested"
|
||||
assert_equal expected, partial_path(@record, "deeper/admin/posts")
|
||||
assert_equal expected, partial_path(@klass, "deeper/admin/posts")
|
||||
end
|
||||
|
||||
def test_partial_path_with_even_deeper_namespaced_controller_path
|
||||
expected = "even/more/deeper/admin/comment/nesteds/nested"
|
||||
assert_equal expected, partial_path(@record, "even/more/deeper/admin/posts")
|
||||
assert_equal expected, partial_path(@klass, "even/more/deeper/admin/posts")
|
||||
end
|
||||
|
||||
def test_partial_path_with_not_namespaced_controller_path
|
||||
expected = "comment/nesteds/nested"
|
||||
assert_equal expected, partial_path(@record, "posts")
|
||||
assert_equal expected, partial_path(@klass, "posts")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,8 +17,9 @@ class MockLogger
|
||||
@logged = []
|
||||
end
|
||||
|
||||
def method_missing(method, *args)
|
||||
def method_missing(method, *args, &blk)
|
||||
@logged << args.first
|
||||
@logged << blk.call if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1331,7 +1332,7 @@ class EtagRenderTest < ActionController::TestCase
|
||||
def test_render_200_should_set_etag
|
||||
get :render_hello_world_from_variable
|
||||
assert_equal etag_for("hello david"), @response.headers['ETag']
|
||||
assert_equal "private, max-age=0, must-revalidate", @response.headers['Cache-Control']
|
||||
assert_equal "max-age=0, private, must-revalidate", @response.headers['Cache-Control']
|
||||
end
|
||||
|
||||
def test_render_against_etag_request_should_304_when_match
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# encoding: utf-8
|
||||
require 'abstract_unit'
|
||||
|
||||
module TestFileUtils
|
||||
def file_name() File.basename(__FILE__) end
|
||||
def file_path() File.expand_path(__FILE__) end
|
||||
def file_data() File.open(file_path, 'rb') { |f| f.read } end
|
||||
def file_data() @data ||= File.open(file_path, 'rb') { |f| f.read } end
|
||||
end
|
||||
|
||||
class SendFileController < ActionController::Base
|
||||
@@ -22,6 +23,10 @@ class SendFileController < ActionController::Base
|
||||
def data
|
||||
send_data(file_data, options)
|
||||
end
|
||||
|
||||
def multibyte_text_data
|
||||
send_data("Кирилица\n祝您好運", options)
|
||||
end
|
||||
end
|
||||
|
||||
class SendFileTest < ActionController::TestCase
|
||||
@@ -55,6 +60,7 @@ class SendFileTest < ActionController::TestCase
|
||||
require 'stringio'
|
||||
output = StringIO.new
|
||||
output.binmode
|
||||
output.string.force_encoding(file_data.encoding) if output.string.respond_to?(:force_encoding)
|
||||
assert_nothing_raised { response.body_parts.each { |part| output << part.to_s } }
|
||||
assert_equal file_data, output.string
|
||||
end
|
||||
@@ -123,7 +129,7 @@ class SendFileTest < ActionController::TestCase
|
||||
# test overriding Cache-Control: no-cache header to fix IE open/save dialog
|
||||
@controller.headers = { 'Cache-Control' => 'no-cache' }
|
||||
@controller.send(:send_file_headers!, options)
|
||||
h = @controller.headers
|
||||
@controller.response.prepare!
|
||||
assert_equal 'private', h['Cache-Control']
|
||||
end
|
||||
|
||||
@@ -163,4 +169,11 @@ class SendFileTest < ActionController::TestCase
|
||||
assert_equal 200, @response.status
|
||||
end
|
||||
end
|
||||
|
||||
def test_send_data_content_length_header
|
||||
@controller.headers = {}
|
||||
@controller.options = { :type => :text, :filename => 'file_with_utf8_text' }
|
||||
process('multibyte_text_data')
|
||||
assert_equal '29', @controller.headers['Content-Length']
|
||||
end
|
||||
end
|
||||
|
||||
@@ -120,8 +120,6 @@ class MultipartParamsParsingTest < ActionController::IntegrationTest
|
||||
fixture = FIXTURE_PATH + "/mona_lisa.jpg"
|
||||
params = { :uploaded_data => fixture_file_upload(fixture, "image/jpg") }
|
||||
post '/read', params
|
||||
expected_length = 'File: '.length + File.size(fixture)
|
||||
assert_equal expected_length, response.content_length
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -13,10 +13,9 @@ class ResponseTest < ActiveSupport::TestCase
|
||||
assert_equal 200, status
|
||||
assert_equal({
|
||||
"Content-Type" => "text/html; charset=utf-8",
|
||||
"Cache-Control" => "private, max-age=0, must-revalidate",
|
||||
"Cache-Control" => "max-age=0, private, must-revalidate",
|
||||
"ETag" => '"65a8e27d8879283831b664bd8b7f0ad4"',
|
||||
"Set-Cookie" => "",
|
||||
"Content-Length" => "13"
|
||||
"Set-Cookie" => ""
|
||||
}, headers)
|
||||
|
||||
parts = []
|
||||
@@ -32,10 +31,9 @@ class ResponseTest < ActiveSupport::TestCase
|
||||
assert_equal 200, status
|
||||
assert_equal({
|
||||
"Content-Type" => "text/html; charset=utf-8",
|
||||
"Cache-Control" => "private, max-age=0, must-revalidate",
|
||||
"Cache-Control" => "max-age=0, private, must-revalidate",
|
||||
"ETag" => '"ebb5e89e8a94e9dd22abf5d915d112b2"',
|
||||
"Set-Cookie" => "",
|
||||
"Content-Length" => "8"
|
||||
"Set-Cookie" => ""
|
||||
}, headers)
|
||||
end
|
||||
|
||||
|
||||
@@ -34,7 +34,6 @@ module Dispatching
|
||||
assert_body "success"
|
||||
assert_status 200
|
||||
assert_content_type "text/html; charset=utf-8"
|
||||
assert_header "Content-Length", "7"
|
||||
end
|
||||
|
||||
# :api: plugin
|
||||
@@ -42,7 +41,6 @@ module Dispatching
|
||||
get "/dispatching/simple/modify_response_body"
|
||||
|
||||
assert_body "success"
|
||||
assert_header "Content-Length", "7" # setting the body manually sets the content length
|
||||
end
|
||||
|
||||
# :api: plugin
|
||||
@@ -50,7 +48,6 @@ module Dispatching
|
||||
get "/dispatching/simple/modify_response_body_twice"
|
||||
|
||||
assert_body "success!"
|
||||
assert_header "Content-Length", "8"
|
||||
end
|
||||
|
||||
test "controller path" do
|
||||
|
||||
@@ -83,7 +83,10 @@ class AssetTagHelperTest < ActionView::TestCase
|
||||
%(javascript_include_tag(:all)) => %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/version.1.0.js" type="text/javascript"></script>),
|
||||
%(javascript_include_tag(:all, :recursive => true)) => %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/robber.js" type="text/javascript"></script>\n<script src="/javascripts/subdir/subdir.js" type="text/javascript"></script>\n<script src="/javascripts/version.1.0.js" type="text/javascript"></script>),
|
||||
%(javascript_include_tag(:defaults, "bank")) => %(<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>),
|
||||
%(javascript_include_tag("bank", :defaults)) => %(<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>)
|
||||
%(javascript_include_tag("bank", :defaults)) => %(<script src="/javascripts/bank.js" type="text/javascript"></script>\n<script src="/javascripts/prototype.js" type="text/javascript"></script>\n<script src="/javascripts/effects.js" type="text/javascript"></script>\n<script src="/javascripts/dragdrop.js" type="text/javascript"></script>\n<script src="/javascripts/controls.js" type="text/javascript"></script>\n<script src="/javascripts/application.js" type="text/javascript"></script>),
|
||||
|
||||
%(javascript_include_tag("http://example.com/all")) => %(<script src="http://example.com/all" type="text/javascript"></script>),
|
||||
%(javascript_include_tag("http://example.com/all.js")) => %(<script src="http://example.com/all.js" type="text/javascript"></script>),
|
||||
}
|
||||
|
||||
StylePathToTag = {
|
||||
@@ -111,7 +114,8 @@ class AssetTagHelperTest < ActionView::TestCase
|
||||
%(stylesheet_link_tag(:all, :media => "all")) => %(<link href="/stylesheets/bank.css" media="all" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/robber.css" media="all" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/version.1.0.css" media="all" rel="stylesheet" type="text/css" />),
|
||||
%(stylesheet_link_tag("random.styles", "/elsewhere/file")) => %(<link href="/stylesheets/random.styles" media="screen" rel="stylesheet" type="text/css" />\n<link href="/elsewhere/file.css" media="screen" rel="stylesheet" type="text/css" />),
|
||||
|
||||
%(stylesheet_link_tag("http://www.example.com/styles/style")) => %(<link href="http://www.example.com/styles/style.css" media="screen" rel="stylesheet" type="text/css" />)
|
||||
%(stylesheet_link_tag("http://www.example.com/styles/style")) => %(<link href="http://www.example.com/styles/style" media="screen" rel="stylesheet" type="text/css" />),
|
||||
%(stylesheet_link_tag("http://www.example.com/styles/style.css")) => %(<link href="http://www.example.com/styles/style.css" media="screen" rel="stylesheet" type="text/css" />),
|
||||
}
|
||||
|
||||
ImagePathToTag = {
|
||||
|
||||
@@ -26,6 +26,7 @@ $:.unshift(activesupport_path) if File.directory?(activesupport_path)
|
||||
require 'active_support'
|
||||
|
||||
module ActiveModel
|
||||
autoload :AttributeMethods, 'active_model/attribute_methods'
|
||||
autoload :Conversion, 'active_model/conversion'
|
||||
autoload :DeprecatedErrorMethods, 'active_model/deprecated_error_methods'
|
||||
autoload :Errors, 'active_model/errors'
|
||||
|
||||
267
activemodel/lib/active_model/attribute_methods.rb
Normal file
267
activemodel/lib/active_model/attribute_methods.rb
Normal file
@@ -0,0 +1,267 @@
|
||||
module ActiveModel
|
||||
class MissingAttributeError < NoMethodError
|
||||
end
|
||||
|
||||
module AttributeMethods
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
# Declare and check for suffixed attribute methods.
|
||||
module ClassMethods
|
||||
# Defines an "attribute" method (like +inheritance_column+ or
|
||||
# +table_name+). A new (class) method will be created with the
|
||||
# given name. If a value is specified, the new method will
|
||||
# return that value (as a string). Otherwise, the given block
|
||||
# will be used to compute the value of the method.
|
||||
#
|
||||
# The original method will be aliased, with the new name being
|
||||
# prefixed with "original_". This allows the new method to
|
||||
# access the original value.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# class A < ActiveRecord::Base
|
||||
# define_attr_method :primary_key, "sysid"
|
||||
# define_attr_method( :inheritance_column ) do
|
||||
# original_inheritance_column + "_id"
|
||||
# end
|
||||
# end
|
||||
def define_attr_method(name, value=nil, &block)
|
||||
sing = metaclass
|
||||
sing.send :alias_method, "original_#{name}", name
|
||||
if block_given?
|
||||
sing.send :define_method, name, &block
|
||||
else
|
||||
# use eval instead of a block to work around a memory leak in dev
|
||||
# mode in fcgi
|
||||
sing.class_eval "def #{name}; #{value.to_s.inspect}; end"
|
||||
end
|
||||
end
|
||||
|
||||
# Declares a method available for all attributes with the given prefix.
|
||||
# Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
|
||||
#
|
||||
# #{prefix}#{attr}(*args, &block)
|
||||
#
|
||||
# to
|
||||
#
|
||||
# #{prefix}attribute(#{attr}, *args, &block)
|
||||
#
|
||||
# An <tt>#{prefix}attribute</tt> instance method must exist and accept at least
|
||||
# the +attr+ argument.
|
||||
#
|
||||
# For example:
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# attribute_method_prefix 'clear_'
|
||||
#
|
||||
# private
|
||||
# def clear_attribute(attr)
|
||||
# ...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# person = Person.find(1)
|
||||
# person.name # => 'Gem'
|
||||
# person.clear_name
|
||||
# person.name # => ''
|
||||
def attribute_method_prefix(*prefixes)
|
||||
attribute_method_matchers.concat(prefixes.map { |prefix| AttributeMethodMatcher.new :prefix => prefix })
|
||||
undefine_attribute_methods
|
||||
end
|
||||
|
||||
# Declares a method available for all attributes with the given suffix.
|
||||
# Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
|
||||
#
|
||||
# #{attr}#{suffix}(*args, &block)
|
||||
#
|
||||
# to
|
||||
#
|
||||
# attribute#{suffix}(#{attr}, *args, &block)
|
||||
#
|
||||
# An <tt>attribute#{suffix}</tt> instance method must exist and accept at least
|
||||
# the +attr+ argument.
|
||||
#
|
||||
# For example:
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# attribute_method_suffix '_short?'
|
||||
#
|
||||
# private
|
||||
# def attribute_short?(attr)
|
||||
# ...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# person = Person.find(1)
|
||||
# person.name # => 'Gem'
|
||||
# person.name_short? # => true
|
||||
def attribute_method_suffix(*suffixes)
|
||||
attribute_method_matchers.concat(suffixes.map { |suffix| AttributeMethodMatcher.new :suffix => suffix })
|
||||
undefine_attribute_methods
|
||||
end
|
||||
|
||||
# Declares a method available for all attributes with the given prefix
|
||||
# and suffix. Uses +method_missing+ and <tt>respond_to?</tt> to rewrite
|
||||
# the method.
|
||||
#
|
||||
# #{prefix}#{attr}#{suffix}(*args, &block)
|
||||
#
|
||||
# to
|
||||
#
|
||||
# #{prefix}attribute#{suffix}(#{attr}, *args, &block)
|
||||
#
|
||||
# An <tt>#{prefix}attribute#{suffix}</tt> instance method must exist and
|
||||
# accept at least the +attr+ argument.
|
||||
#
|
||||
# For example:
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!'
|
||||
#
|
||||
# private
|
||||
# def reset_attribute_to_default!(attr)
|
||||
# ...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# person = Person.find(1)
|
||||
# person.name # => 'Gem'
|
||||
# person.reset_name_to_default!
|
||||
# person.name # => 'Gemma'
|
||||
def attribute_method_affix(*affixes)
|
||||
attribute_method_matchers.concat(affixes.map { |affix| AttributeMethodMatcher.new :prefix => affix[:prefix], :suffix => affix[:suffix] })
|
||||
undefine_attribute_methods
|
||||
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}"
|
||||
|
||||
if respond_to?(generate_method)
|
||||
send(generate_method, name)
|
||||
else
|
||||
generated_attribute_methods.module_eval("def #{method_name}(*args); send(:#{method.prefix}attribute#{method.suffix}, '#{name}', *args); end", __FILE__, __LINE__)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def undefine_attribute_methods
|
||||
generated_attribute_methods.module_eval do
|
||||
instance_methods.each { |m| undef_method(m) }
|
||||
end
|
||||
@attribute_methods_generated = nil
|
||||
end
|
||||
|
||||
def generated_attribute_methods #:nodoc:
|
||||
@generated_attribute_methods ||= begin
|
||||
@attribute_methods_generated = true
|
||||
mod = Module.new
|
||||
include mod
|
||||
mod
|
||||
end
|
||||
end
|
||||
|
||||
def attribute_methods_generated?
|
||||
@attribute_methods_generated ? true : false
|
||||
end
|
||||
|
||||
protected
|
||||
def instance_method_already_implemented?(method_name)
|
||||
method_defined?(method_name)
|
||||
end
|
||||
|
||||
private
|
||||
class AttributeMethodMatcher
|
||||
attr_reader :prefix, :suffix
|
||||
|
||||
AttributeMethodMatch = Struct.new(:prefix, :base, :suffix)
|
||||
|
||||
def initialize(options = {})
|
||||
options.symbolize_keys!
|
||||
@prefix, @suffix = options[:prefix] || '', options[:suffix] || ''
|
||||
@regex = /^(#{Regexp.escape(@prefix)})(.+?)(#{Regexp.escape(@suffix)})$/
|
||||
end
|
||||
|
||||
def match(method_name)
|
||||
if matchdata = @regex.match(method_name)
|
||||
AttributeMethodMatch.new(matchdata[1], matchdata[2], matchdata[3])
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def attribute_method_matchers #:nodoc:
|
||||
@@attribute_method_matchers ||= []
|
||||
end
|
||||
end
|
||||
|
||||
# Allows access to the object attributes, which are held in the <tt>@attributes</tt> hash, as though they
|
||||
# were first-class methods. So a Person class with a name attribute can use Person#name and
|
||||
# Person#name= and never directly use the attributes hash -- except for multiple assigns with
|
||||
# ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
|
||||
# the completed attribute is not +nil+ or 0.
|
||||
#
|
||||
# It's also possible to instantiate related objects, so a Client class belonging to the clients
|
||||
# table with a +master_id+ foreign key can instantiate master through Client#master.
|
||||
def method_missing(method_id, *args, &block)
|
||||
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)
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
# A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
|
||||
# <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
|
||||
# which will all return +true+.
|
||||
alias :respond_to_without_attributes? :respond_to?
|
||||
def respond_to?(method, include_private_methods = false)
|
||||
if super
|
||||
return true
|
||||
elsif !include_private_methods && super(method, true)
|
||||
# If we're here then we haven't found among non-private methods
|
||||
# but found among all methods. Which means that given method is private.
|
||||
return false
|
||||
elsif match_attribute_method?(method.to_s)
|
||||
return true
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
protected
|
||||
def attribute_method?(attr_name)
|
||||
attributes.include?(attr_name)
|
||||
end
|
||||
|
||||
private
|
||||
# Returns a struct representing the matching attribute method.
|
||||
# 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)
|
||||
return match
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# prevent method_missing from calling private methods with #send
|
||||
def guard_private_attribute_method!(method_name, args)
|
||||
if self.class.private_method_defined?(method_name)
|
||||
raise NoMethodError.new("Attempt to call private method", method_name, args)
|
||||
end
|
||||
end
|
||||
|
||||
def missing_attribute(attr_name, stack)
|
||||
raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -68,7 +68,7 @@ module ActiveModel
|
||||
# Will add an error message to each of the attributes in +attributes+ that is empty.
|
||||
def add_on_empty(attributes, custom_message = nil)
|
||||
[attributes].flatten.each do |attribute|
|
||||
value = @base.send(attribute)
|
||||
value = @base.send(:read_attribute_for_validation, attribute)
|
||||
is_empty = value.respond_to?(:empty?) ? value.empty? : false
|
||||
add(attribute, :empty, :default => custom_message) unless !value.nil? && !is_empty
|
||||
end
|
||||
@@ -77,7 +77,7 @@ module ActiveModel
|
||||
# Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?).
|
||||
def add_on_blank(attributes, custom_message = nil)
|
||||
[attributes].flatten.each do |attribute|
|
||||
value = @base.send(attribute)
|
||||
value = @base.send(:read_attribute_for_validation, attribute)
|
||||
add(attribute, :blank, :default => custom_message) if value.blank?
|
||||
end
|
||||
end
|
||||
@@ -146,7 +146,7 @@ module ActiveModel
|
||||
defaults = defaults.compact.flatten << :"messages.#{message}"
|
||||
|
||||
key = defaults.shift
|
||||
value = @base.send(attribute)
|
||||
value = @base.send(:read_attribute_for_validation, attribute)
|
||||
|
||||
options = { :default => defaults,
|
||||
:model => @base.class.name.humanize,
|
||||
|
||||
@@ -5,12 +5,9 @@ module ActiveModel
|
||||
autoload :State, 'active_model/state_machine/state'
|
||||
autoload :StateTransition, 'active_model/state_machine/state_transition'
|
||||
|
||||
class InvalidTransition < Exception
|
||||
end
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def self.included(base)
|
||||
require 'active_model/state_machine/machine'
|
||||
base.extend ClassMethods
|
||||
class InvalidTransition < Exception
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
require 'active_model/state_machine/state_transition'
|
||||
|
||||
module ActiveModel
|
||||
module StateMachine
|
||||
class Event
|
||||
@@ -53,12 +51,12 @@ module ActiveModel
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
def transitions(trans_opts)
|
||||
Array(trans_opts[:from]).each do |s|
|
||||
@transitions << StateTransition.new(trans_opts.merge({:from => s.to_sym}))
|
||||
private
|
||||
def transitions(trans_opts)
|
||||
Array(trans_opts[:from]).each do |s|
|
||||
@transitions << StateTransition.new(trans_opts.merge({:from => s.to_sym}))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
require 'active_model/state_machine/state'
|
||||
require 'active_model/state_machine/event'
|
||||
|
||||
module ActiveModel
|
||||
module StateMachine
|
||||
class Machine
|
||||
@@ -57,22 +54,22 @@ module ActiveModel
|
||||
"@#{@name}_current_state"
|
||||
end
|
||||
|
||||
private
|
||||
def state(name, options = {})
|
||||
@states << (state_index[name] ||= State.new(name, :machine => self)).update(options)
|
||||
end
|
||||
private
|
||||
def state(name, options = {})
|
||||
@states << (state_index[name] ||= State.new(name, :machine => self)).update(options)
|
||||
end
|
||||
|
||||
def event(name, options = {}, &block)
|
||||
(@events[name] ||= Event.new(self, name)).update(options, &block)
|
||||
end
|
||||
def event(name, options = {}, &block)
|
||||
(@events[name] ||= Event.new(self, name)).update(options, &block)
|
||||
end
|
||||
|
||||
def event_fired_callback
|
||||
@event_fired_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_fired'
|
||||
end
|
||||
def event_fired_callback
|
||||
@event_fired_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_fired'
|
||||
end
|
||||
|
||||
def event_failed_callback
|
||||
@event_failed_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_failed'
|
||||
end
|
||||
def event_failed_callback
|
||||
@event_failed_callback ||= (@name == :default ? '' : "#{@name}_") + 'event_failed'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,7 +18,7 @@ module ActiveModel
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def execute(obj, *args)
|
||||
case @on_transition
|
||||
when Symbol, String
|
||||
|
||||
@@ -66,7 +66,7 @@ module ActiveModel
|
||||
# Declare the validation.
|
||||
send(validation_method(options[:on]), options) do |record|
|
||||
attrs.each do |attr|
|
||||
value = record.send(attr)
|
||||
value = record.send(:read_attribute_for_validation, attr)
|
||||
next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
|
||||
yield record, attr, value
|
||||
end
|
||||
@@ -95,6 +95,28 @@ module ActiveModel
|
||||
def invalid?
|
||||
!valid?
|
||||
end
|
||||
|
||||
protected
|
||||
# Hook method defining how an attribute value should be retieved. By default this is assumed
|
||||
# to be an instance named after the attribute. Override this method in subclasses should you
|
||||
# need to retrieve the value for a given attribute differently e.g.
|
||||
# class MyClass
|
||||
# include ActiveModel::Validations
|
||||
#
|
||||
# def initialize(data = {})
|
||||
# @data = data
|
||||
# end
|
||||
#
|
||||
# private
|
||||
#
|
||||
# def read_attribute_for_validation(key)
|
||||
# @data[key]
|
||||
# end
|
||||
# end
|
||||
#
|
||||
def read_attribute_for_validation(key)
|
||||
send(key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -54,4 +54,18 @@ class PresenceValidationTest < ActiveModel::TestCase
|
||||
assert p.valid?
|
||||
end
|
||||
end
|
||||
|
||||
def test_validates_presence_of_for_ruby_class_with_custom_reader
|
||||
repair_validations(Person) do
|
||||
CustomReader.validates_presence_of :karma
|
||||
|
||||
p = CustomReader.new
|
||||
assert p.invalid?
|
||||
|
||||
assert_equal ["can't be blank"], p.errors[:karma]
|
||||
|
||||
p[:karma] = "Cold"
|
||||
assert p.valid?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,6 +5,7 @@ require 'cases/tests_database'
|
||||
require 'models/topic'
|
||||
require 'models/reply'
|
||||
require 'models/developer'
|
||||
require 'models/custom_reader'
|
||||
|
||||
class ValidationsTest < ActiveModel::TestCase
|
||||
include ActiveModel::TestsDatabase
|
||||
@@ -97,6 +98,19 @@ class ValidationsTest < ActiveModel::TestCase
|
||||
assert_equal %w(gotcha gotcha), t.errors[:title]
|
||||
assert_equal %w(gotcha gotcha), t.errors[:content]
|
||||
end
|
||||
|
||||
def test_validates_each_custom_reader
|
||||
hits = 0
|
||||
CustomReader.validates_each(:title, :content, [:title, :content]) do |record, attr|
|
||||
record.errors.add attr, 'gotcha'
|
||||
hits += 1
|
||||
end
|
||||
t = CustomReader.new("title" => "valid", "content" => "whatever")
|
||||
assert !t.valid?
|
||||
assert_equal 4, hits
|
||||
assert_equal %w(gotcha gotcha), t.errors[:title]
|
||||
assert_equal %w(gotcha gotcha), t.errors[:content]
|
||||
end
|
||||
|
||||
def test_validate_block
|
||||
Topic.validate { |topic| topic.errors.add("title", "will never be valid") }
|
||||
|
||||
17
activemodel/test/models/custom_reader.rb
Normal file
17
activemodel/test/models/custom_reader.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
class CustomReader
|
||||
include ActiveModel::Validations
|
||||
|
||||
def initialize(data = {})
|
||||
@data = data
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
@data[key] = value
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def read_attribute_for_validation(key)
|
||||
@data[key]
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,9 @@
|
||||
*Edge*
|
||||
|
||||
* quoted_date converts time-like objects to ActiveRecord::Base.default_timezone before serialization. This allows you to use Time.now in find conditions and have it correctly be serialized as the current time in UTC when default_timezone == :utc. #2946 [Geoff Buesing]
|
||||
|
||||
* SQLite: drop support for 'dbfile' option in favor of 'database.' #2363 [Paul Hinze, Jeremy Kemper]
|
||||
|
||||
* Added :primary_key option to belongs_to associations. #765 [Szymon Nowak, Philip Hallstrom, Noel Rocha]
|
||||
# employees.company_name references companies.name
|
||||
Employee.belongs_to :company, :primary_key => 'name', :foreign_key => 'company_name'
|
||||
|
||||
@@ -65,6 +65,7 @@ module ActiveRecord
|
||||
autoload :SchemaDumper, 'active_record/schema_dumper'
|
||||
autoload :Serialization, 'active_record/serialization'
|
||||
autoload :SessionStore, 'active_record/session_store'
|
||||
autoload :StateMachine, 'active_record/state_machine'
|
||||
autoload :TestCase, 'active_record/test_case'
|
||||
autoload :Timestamp, 'active_record/timestamp'
|
||||
autoload :Transactions, 'active_record/transactions'
|
||||
|
||||
@@ -3,109 +3,13 @@ require 'active_support/core_ext/enumerable'
|
||||
module ActiveRecord
|
||||
module AttributeMethods #:nodoc:
|
||||
extend ActiveSupport::Concern
|
||||
include ActiveModel::AttributeMethods
|
||||
|
||||
# Declare and check for suffixed attribute methods.
|
||||
module ClassMethods
|
||||
# Declares a method available for all attributes with the given suffix.
|
||||
# Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method
|
||||
#
|
||||
# #{attr}#{suffix}(*args, &block)
|
||||
#
|
||||
# to
|
||||
#
|
||||
# attribute#{suffix}(#{attr}, *args, &block)
|
||||
#
|
||||
# An <tt>attribute#{suffix}</tt> instance method must exist and accept at least
|
||||
# the +attr+ argument.
|
||||
#
|
||||
# For example:
|
||||
#
|
||||
# class Person < ActiveRecord::Base
|
||||
# attribute_method_suffix '_changed?'
|
||||
#
|
||||
# private
|
||||
# def attribute_changed?(attr)
|
||||
# ...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# person = Person.find(1)
|
||||
# person.name_changed? # => false
|
||||
# person.name = 'Hubert'
|
||||
# person.name_changed? # => true
|
||||
def attribute_method_suffix(*suffixes)
|
||||
attribute_method_suffixes.concat(suffixes)
|
||||
rebuild_attribute_method_regexp
|
||||
undefine_attribute_methods
|
||||
end
|
||||
|
||||
# Defines an "attribute" method (like +inheritance_column+ or
|
||||
# +table_name+). A new (class) method will be created with the
|
||||
# given name. If a value is specified, the new method will
|
||||
# return that value (as a string). Otherwise, the given block
|
||||
# will be used to compute the value of the method.
|
||||
#
|
||||
# The original method will be aliased, with the new name being
|
||||
# prefixed with "original_". This allows the new method to
|
||||
# access the original value.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# class A < ActiveRecord::Base
|
||||
# define_attr_method :primary_key, "sysid"
|
||||
# define_attr_method( :inheritance_column ) do
|
||||
# original_inheritance_column + "_id"
|
||||
# end
|
||||
# end
|
||||
def define_attr_method(name, value=nil, &block)
|
||||
sing = metaclass
|
||||
sing.send :alias_method, "original_#{name}", name
|
||||
if block_given?
|
||||
sing.send :define_method, name, &block
|
||||
else
|
||||
# use eval instead of a block to work around a memory leak in dev
|
||||
# mode in fcgi
|
||||
sing.class_eval "def #{name}; #{value.to_s.inspect}; end"
|
||||
end
|
||||
end
|
||||
|
||||
# Returns MatchData if method_name is an attribute method.
|
||||
def match_attribute_method?(method_name)
|
||||
rebuild_attribute_method_regexp unless defined?(@@attribute_method_regexp) && @@attribute_method_regexp
|
||||
@@attribute_method_regexp.match(method_name)
|
||||
end
|
||||
|
||||
# Contains the names of the generated attribute methods.
|
||||
def generated_methods #:nodoc:
|
||||
@generated_methods ||= Set.new
|
||||
end
|
||||
|
||||
def generated_methods?
|
||||
!generated_methods.empty?
|
||||
end
|
||||
|
||||
# Generates all the attribute related methods for columns in the database
|
||||
# accessors, mutators and query methods.
|
||||
def define_attribute_methods
|
||||
return if generated_methods?
|
||||
columns_hash.keys.each do |name|
|
||||
attribute_method_suffixes.each do |suffix|
|
||||
method_name = "#{name}#{suffix}"
|
||||
unless instance_method_already_implemented?(method_name)
|
||||
generate_method = "define_attribute_method#{suffix}"
|
||||
if respond_to?(generate_method)
|
||||
send(generate_method, name)
|
||||
else
|
||||
evaluate_attribute_method("def #{method_name}(*args); send(:attribute#{suffix}, '#{name}', *args); end", method_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def undefine_attribute_methods
|
||||
generated_methods.each { |name| undef_method(name) }
|
||||
@generated_methods = nil
|
||||
super(columns_hash.keys)
|
||||
end
|
||||
|
||||
# Checks whether the method is defined in the model or any of its subclasses
|
||||
@@ -118,101 +22,30 @@ module ActiveRecord
|
||||
raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name)
|
||||
@_defined_class_methods.include?(method_name)
|
||||
end
|
||||
|
||||
private
|
||||
# Suffixes a, ?, c become regexp /(a|\?|c)$/
|
||||
def rebuild_attribute_method_regexp
|
||||
suffixes = attribute_method_suffixes.map { |s| Regexp.escape(s) }
|
||||
@@attribute_method_regexp = /(#{suffixes.join('|')})$/.freeze
|
||||
end
|
||||
|
||||
def attribute_method_suffixes
|
||||
@@attribute_method_suffixes ||= []
|
||||
end
|
||||
|
||||
# Evaluate the definition for an attribute related method
|
||||
def evaluate_attribute_method(method_definition, method_name)
|
||||
generated_methods << method_name.to_s
|
||||
|
||||
begin
|
||||
class_eval(method_definition, __FILE__, __LINE__)
|
||||
rescue SyntaxError => err
|
||||
generated_methods.delete(method_name.to_s)
|
||||
if logger
|
||||
logger.warn "Exception occurred during reader method compilation."
|
||||
logger.warn "Maybe #{method_name} is not a valid Ruby identifier?"
|
||||
logger.warn err.message
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Allows access to the object attributes, which are held in the <tt>@attributes</tt> hash, as though they
|
||||
# were first-class methods. So a Person class with a name attribute can use Person#name and
|
||||
# Person#name= and never directly use the attributes hash -- except for multiple assigns with
|
||||
# ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
|
||||
# the completed attribute is not +nil+ or 0.
|
||||
#
|
||||
# It's also possible to instantiate related objects, so a Client class belonging to the clients
|
||||
# table with a +master_id+ foreign key can instantiate master through Client#master.
|
||||
def method_missing(method_id, *args, &block)
|
||||
method_name = method_id.to_s
|
||||
|
||||
# If we haven't generated any methods yet, generate them, then
|
||||
# see if we've created the method we're looking for.
|
||||
if !self.class.generated_methods?
|
||||
if !self.class.attribute_methods_generated?
|
||||
self.class.define_attribute_methods
|
||||
method_name = method_id.to_s
|
||||
guard_private_attribute_method!(method_name, args)
|
||||
if self.class.generated_methods.include?(method_name)
|
||||
if self.class.generated_attribute_methods.instance_methods.include?(method_name)
|
||||
return self.send(method_id, *args, &block)
|
||||
end
|
||||
end
|
||||
|
||||
if md = self.class.match_attribute_method?(method_name)
|
||||
attribute_name, method_type = md.pre_match, md.to_s
|
||||
if attribute_name == 'id' || @attributes.include?(attribute_name)
|
||||
guard_private_attribute_method!(method_name, args)
|
||||
return __send__("attribute#{method_type}", attribute_name, *args, &block)
|
||||
end
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
# A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
|
||||
# <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
|
||||
# which will all return +true+.
|
||||
alias :respond_to_without_attributes? :respond_to?
|
||||
def respond_to?(method, include_private_methods = false)
|
||||
method_name = method.to_s
|
||||
if super
|
||||
return true
|
||||
elsif !include_private_methods && super(method, true)
|
||||
# If we're here than we haven't found among non-private methods
|
||||
# but found among all methods. Which means that given method is private.
|
||||
return false
|
||||
elsif !self.class.generated_methods?
|
||||
self.class.define_attribute_methods
|
||||
if self.class.generated_methods.include?(method_name)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
if md = self.class.match_attribute_method?(method_name)
|
||||
return true if md.pre_match == 'id' || @attributes.include?(md.pre_match)
|
||||
end
|
||||
def respond_to?(*args)
|
||||
self.class.define_attribute_methods
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
# prevent method_missing from calling private methods with #send
|
||||
def guard_private_attribute_method!(method_name, args)
|
||||
if self.class.private_method_defined?(method_name)
|
||||
raise NoMethodError.new("Attempt to call private method", method_name, args)
|
||||
end
|
||||
end
|
||||
|
||||
def missing_attribute(attr_name, stack)
|
||||
raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack
|
||||
protected
|
||||
def attribute_method?(attr_name)
|
||||
attr_name == 'id' || attributes.include?(attr_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,17 +3,17 @@ module ActiveRecord
|
||||
# Track unsaved attribute changes.
|
||||
#
|
||||
# A newly instantiated object is unchanged:
|
||||
# person = Person.find_by_name('uncle bob')
|
||||
# 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_was # => 'Uncle Bob'
|
||||
# person.name_change # => ['Uncle Bob', 'Bob']
|
||||
# person.name = 'Bill'
|
||||
# person.name_change # => ['uncle bob', 'Bill']
|
||||
# person.name_change # => ['Uncle Bob', 'Bill']
|
||||
#
|
||||
# Save the changes:
|
||||
# person.save
|
||||
@@ -26,21 +26,33 @@ module ActiveRecord
|
||||
# person.name_change # => nil
|
||||
#
|
||||
# Which attributes have changed?
|
||||
# person.name = 'bob'
|
||||
# person.name = 'Bob'
|
||||
# person.changed # => ['name']
|
||||
# person.changes # => { 'name' => ['Bill', 'bob'] }
|
||||
# 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 << 'by'
|
||||
# person.name_change # => ['uncle bob', 'uncle bobby']
|
||||
# person.name << 'y'
|
||||
# person.name_change # => ['Bill', 'Billy']
|
||||
module Dirty
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
DIRTY_SUFFIXES = ['_changed?', '_change', '_will_change!', '_was']
|
||||
DIRTY_AFFIXES = [
|
||||
{ :suffix => '_changed?' },
|
||||
{ :suffix => '_change' },
|
||||
{ :suffix => '_will_change!' },
|
||||
{ :suffix => '_was' },
|
||||
{ :prefix => 'reset_', :suffix => '!' }
|
||||
]
|
||||
|
||||
included do
|
||||
attribute_method_suffix *DIRTY_SUFFIXES
|
||||
attribute_method_affix *DIRTY_AFFIXES
|
||||
|
||||
alias_method_chain :save, :dirty
|
||||
alias_method_chain :save!, :dirty
|
||||
@@ -118,6 +130,11 @@ module ActiveRecord
|
||||
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)
|
||||
@@ -175,9 +192,9 @@ module ActiveRecord
|
||||
|
||||
def alias_attribute_with_dirty(new_name, old_name)
|
||||
alias_attribute_without_dirty(new_name, old_name)
|
||||
DIRTY_SUFFIXES.each do |suffix|
|
||||
DIRTY_AFFIXES.each do |affixes|
|
||||
module_eval <<-STR, __FILE__, __LINE__+1
|
||||
def #{new_name}#{suffix}; self.#{old_name}#{suffix}; end # def subject_changed?; self.title_changed?; end
|
||||
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
|
||||
|
||||
@@ -36,7 +36,7 @@ module ActiveRecord
|
||||
end
|
||||
|
||||
protected
|
||||
def define_attribute_method(attr_name)
|
||||
def define_method_attribute(attr_name)
|
||||
if self.serialized_attributes[attr_name]
|
||||
define_read_method_for_serialized_attribute(attr_name)
|
||||
else
|
||||
@@ -51,7 +51,7 @@ module ActiveRecord
|
||||
private
|
||||
# Define read method for serialized attribute.
|
||||
def define_read_method_for_serialized_attribute(attr_name)
|
||||
evaluate_attribute_method "def #{attr_name}; unserialize_attribute('#{attr_name}'); end", attr_name
|
||||
generated_attribute_methods.module_eval("def #{attr_name}; unserialize_attribute('#{attr_name}'); end", __FILE__, __LINE__)
|
||||
end
|
||||
|
||||
# Define an attribute reader method. Cope with nil column.
|
||||
@@ -66,7 +66,7 @@ module ActiveRecord
|
||||
if cache_attribute?(attr_name)
|
||||
access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
|
||||
end
|
||||
evaluate_attribute_method "def #{symbol}; #{access_code}; end", symbol
|
||||
generated_attribute_methods.module_eval("def #{symbol}; #{access_code}; end", __FILE__, __LINE__)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ module ActiveRecord
|
||||
protected
|
||||
# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
|
||||
# This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone.
|
||||
def define_attribute_method(attr_name)
|
||||
def define_method_attribute(attr_name)
|
||||
if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
|
||||
method_body = <<-EOV
|
||||
def #{attr_name}(reload = false)
|
||||
@@ -25,7 +25,7 @@ module ActiveRecord
|
||||
@attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
|
||||
end
|
||||
EOV
|
||||
evaluate_attribute_method method_body, attr_name
|
||||
generated_attribute_methods.module_eval(method_body, __FILE__, __LINE__)
|
||||
else
|
||||
super
|
||||
end
|
||||
@@ -33,7 +33,7 @@ module ActiveRecord
|
||||
|
||||
# Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
|
||||
# This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
|
||||
def define_attribute_method=(attr_name)
|
||||
def define_method_attribute=(attr_name)
|
||||
if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
|
||||
method_body = <<-EOV
|
||||
def #{attr_name}=(time)
|
||||
@@ -44,7 +44,7 @@ module ActiveRecord
|
||||
write_attribute(:#{attr_name}, time)
|
||||
end
|
||||
EOV
|
||||
evaluate_attribute_method method_body, "#{attr_name}="
|
||||
generated_attribute_methods.module_eval(method_body, __FILE__, __LINE__)
|
||||
else
|
||||
super
|
||||
end
|
||||
|
||||
@@ -9,8 +9,8 @@ module ActiveRecord
|
||||
|
||||
module ClassMethods
|
||||
protected
|
||||
def define_attribute_method=(attr_name)
|
||||
evaluate_attribute_method "def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", "#{attr_name}="
|
||||
def define_method_attribute=(attr_name)
|
||||
generated_attribute_methods.module_eval("def #{attr_name}=(new_value); write_attribute('#{attr_name}', new_value); end", __FILE__, __LINE__)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -148,11 +148,6 @@ module ActiveRecord #:nodoc:
|
||||
class DangerousAttributeError < ActiveRecordError
|
||||
end
|
||||
|
||||
# Raised when you've tried to access a column which wasn't loaded by your finder.
|
||||
# Typically this is because <tt>:select</tt> has been specified.
|
||||
class MissingAttributeError < NoMethodError
|
||||
end
|
||||
|
||||
# Raised when unknown attributes are supplied via mass assignment.
|
||||
class UnknownAttributeError < NoMethodError
|
||||
end
|
||||
|
||||
@@ -60,7 +60,12 @@ module ActiveRecord
|
||||
end
|
||||
|
||||
def quoted_date(value)
|
||||
value.to_s(:db)
|
||||
if value.acts_like?(:time)
|
||||
zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
|
||||
value.respond_to?(zone_conversion_method) ? value.send(zone_conversion_method) : value
|
||||
else
|
||||
value
|
||||
end.to_s(:db)
|
||||
end
|
||||
|
||||
def quoted_string_prefix
|
||||
|
||||
@@ -277,7 +277,6 @@ module ActiveRecord
|
||||
add_column_options!(column_sql, column_options) unless type.to_sym == :primary_key
|
||||
column_sql
|
||||
end
|
||||
alias to_s :to_sql
|
||||
|
||||
private
|
||||
|
||||
@@ -508,7 +507,7 @@ module ActiveRecord
|
||||
# concatenated together. This string can then be prepended and appended to
|
||||
# to generate the final SQL to create the table.
|
||||
def to_sql
|
||||
@columns * ', '
|
||||
@columns.map(&:to_sql) * ', '
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -27,7 +27,6 @@ module ActiveRecord
|
||||
|
||||
private
|
||||
def parse_sqlite_config!(config)
|
||||
config[:database] ||= config[:dbfile]
|
||||
# Require database.
|
||||
unless config[:database]
|
||||
raise ArgumentError, "No database file specified. Missing argument: database"
|
||||
|
||||
24
activerecord/lib/active_record/state_machine.rb
Normal file
24
activerecord/lib/active_record/state_machine.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
module ActiveRecord
|
||||
module StateMachine #:nodoc:
|
||||
extend ActiveSupport::Concern
|
||||
include ActiveModel::StateMachine
|
||||
|
||||
included do
|
||||
before_validation :set_initial_state
|
||||
validates_presence_of :state
|
||||
end
|
||||
|
||||
protected
|
||||
def write_state(state_machine, state)
|
||||
update_attributes! :state => state.to_s
|
||||
end
|
||||
|
||||
def read_state(state_machine)
|
||||
self.state.to_sym
|
||||
end
|
||||
|
||||
def set_initial_state
|
||||
self.state ||= self.class.state_machine.initial_state.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,40 +4,43 @@ require 'models/minimalistic'
|
||||
|
||||
class AttributeMethodsTest < ActiveRecord::TestCase
|
||||
fixtures :topics
|
||||
|
||||
def setup
|
||||
@old_suffixes = ActiveRecord::Base.send(:attribute_method_suffixes).dup
|
||||
@old_matchers = ActiveRecord::Base.send(:attribute_method_matchers).dup
|
||||
@target = Class.new(ActiveRecord::Base)
|
||||
@target.table_name = 'topics'
|
||||
end
|
||||
|
||||
def teardown
|
||||
ActiveRecord::Base.send(:attribute_method_suffixes).clear
|
||||
ActiveRecord::Base.attribute_method_suffix *@old_suffixes
|
||||
ActiveRecord::Base.send(:attribute_method_matchers).clear
|
||||
ActiveRecord::Base.send(:attribute_method_matchers).concat(@old_matchers)
|
||||
end
|
||||
|
||||
def test_match_attribute_method_query_returns_match_data
|
||||
assert_not_nil md = @target.match_attribute_method?('title=')
|
||||
assert_equal 'title', md.pre_match
|
||||
assert_equal ['='], md.captures
|
||||
|
||||
%w(_hello_world ist! _maybe?).each do |suffix|
|
||||
@target.class_eval "def attribute#{suffix}(*args) args end"
|
||||
@target.attribute_method_suffix suffix
|
||||
|
||||
assert_not_nil md = @target.match_attribute_method?("title#{suffix}")
|
||||
assert_equal 'title', md.pre_match
|
||||
assert_equal [suffix], md.captures
|
||||
end
|
||||
end
|
||||
|
||||
def test_declared_attribute_method_affects_respond_to_and_method_missing
|
||||
def test_undeclared_attribute_method_does_not_affect_respond_to_and_method_missing
|
||||
topic = @target.new(:title => 'Budget')
|
||||
assert topic.respond_to?('title')
|
||||
assert_equal 'Budget', topic.title
|
||||
assert !topic.respond_to?('title_hello_world')
|
||||
assert_raise(NoMethodError) { topic.title_hello_world }
|
||||
end
|
||||
|
||||
%w(_hello_world _it! _candidate= able?).each do |suffix|
|
||||
def test_declared_prefixed_attribute_method_affects_respond_to_and_method_missing
|
||||
topic = @target.new(:title => 'Budget')
|
||||
%w(default_ title_).each do |prefix|
|
||||
@target.class_eval "def #{prefix}attribute(*args) args end"
|
||||
@target.attribute_method_prefix prefix
|
||||
|
||||
meth = "#{prefix}title"
|
||||
assert topic.respond_to?(meth)
|
||||
assert_equal ['title'], topic.send(meth)
|
||||
assert_equal ['title', 'a'], topic.send(meth, 'a')
|
||||
assert_equal ['title', 1, 2, 3], topic.send(meth, 1, 2, 3)
|
||||
end
|
||||
end
|
||||
|
||||
def test_declared_suffixed_attribute_method_affects_respond_to_and_method_missing
|
||||
topic = @target.new(:title => 'Budget')
|
||||
%w(_default _title_default _it! _candidate= able?).each do |suffix|
|
||||
@target.class_eval "def attribute#{suffix}(*args) args end"
|
||||
@target.attribute_method_suffix suffix
|
||||
|
||||
@@ -49,6 +52,20 @@ class AttributeMethodsTest < ActiveRecord::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
def test_declared_affixed_attribute_method_affects_respond_to_and_method_missing
|
||||
topic = @target.new(:title => 'Budget')
|
||||
[['mark_', '_for_update'], ['reset_', '!'], ['default_', '_value?']].each do |prefix, suffix|
|
||||
@target.class_eval "def #{prefix}attribute#{suffix}(*args) args end"
|
||||
@target.attribute_method_affix({ :prefix => prefix, :suffix => suffix })
|
||||
|
||||
meth = "#{prefix}title#{suffix}"
|
||||
assert topic.respond_to?(meth)
|
||||
assert_equal ['title'], topic.send(meth)
|
||||
assert_equal ['title', 'a'], topic.send(meth, 'a')
|
||||
assert_equal ['title', 1, 2, 3], topic.send(meth, 1, 2, 3)
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_unserialize_attributes_for_frozen_records
|
||||
myobj = {:value1 => :value2}
|
||||
topic = Topic.create("content" => myobj)
|
||||
|
||||
@@ -464,6 +464,60 @@ class BasicsTest < ActiveRecord::TestCase
|
||||
end
|
||||
end
|
||||
|
||||
def test_preserving_time_objects_with_local_time_conversion_to_default_timezone_utc
|
||||
with_env_tz 'America/New_York' do
|
||||
with_active_record_default_timezone :utc do
|
||||
time = Time.local(2000)
|
||||
topic = Topic.create('written_on' => time)
|
||||
saved_time = Topic.find(topic.id).written_on
|
||||
assert_equal time, saved_time
|
||||
assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "EST"], time.to_a
|
||||
assert_equal [0, 0, 5, 1, 1, 2000, 6, 1, false, "UTC"], saved_time.to_a
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_utc
|
||||
with_env_tz 'America/New_York' do
|
||||
with_active_record_default_timezone :utc do
|
||||
Time.use_zone 'Central Time (US & Canada)' do
|
||||
time = Time.zone.local(2000)
|
||||
topic = Topic.create('written_on' => time)
|
||||
saved_time = Topic.find(topic.id).written_on
|
||||
assert_equal time, saved_time
|
||||
assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "CST"], time.to_a
|
||||
assert_equal [0, 0, 6, 1, 1, 2000, 6, 1, false, "UTC"], saved_time.to_a
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_preserving_time_objects_with_utc_time_conversion_to_default_timezone_local
|
||||
with_env_tz 'America/New_York' do
|
||||
time = Time.utc(2000)
|
||||
topic = Topic.create('written_on' => time)
|
||||
saved_time = Topic.find(topic.id).written_on
|
||||
assert_equal time, saved_time
|
||||
assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "UTC"], time.to_a
|
||||
assert_equal [0, 0, 19, 31, 12, 1999, 5, 365, false, "EST"], saved_time.to_a
|
||||
end
|
||||
end
|
||||
|
||||
def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_local
|
||||
with_env_tz 'America/New_York' do
|
||||
with_active_record_default_timezone :local do
|
||||
Time.use_zone 'Central Time (US & Canada)' do
|
||||
time = Time.zone.local(2000)
|
||||
topic = Topic.create('written_on' => time)
|
||||
saved_time = Topic.find(topic.id).written_on
|
||||
assert_equal time, saved_time
|
||||
assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "CST"], time.to_a
|
||||
assert_equal [0, 0, 1, 1, 1, 2000, 6, 1, false, "EST"], saved_time.to_a
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_custom_mutator
|
||||
topic = Topic.find(1)
|
||||
# This mutator is protected in the class definition
|
||||
@@ -2115,4 +2169,19 @@ class BasicsTest < ActiveRecord::TestCase
|
||||
def test_dup
|
||||
assert !Minimalistic.new.freeze.dup.frozen?
|
||||
end
|
||||
|
||||
protected
|
||||
def with_env_tz(new_tz = 'US/Eastern')
|
||||
old_tz, ENV['TZ'] = ENV['TZ'], new_tz
|
||||
yield
|
||||
ensure
|
||||
old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
|
||||
end
|
||||
|
||||
def with_active_record_default_timezone(zone)
|
||||
old_zone, ActiveRecord::Base.default_timezone = ActiveRecord::Base.default_timezone, zone
|
||||
yield
|
||||
ensure
|
||||
ActiveRecord::Base.default_timezone = old_zone
|
||||
end
|
||||
end
|
||||
|
||||
@@ -62,6 +62,16 @@ class DirtyTest < ActiveRecord::TestCase
|
||||
assert_equal parrot.name_change, parrot.title_change
|
||||
end
|
||||
|
||||
def test_reset_attribute!
|
||||
pirate = Pirate.create!(:catchphrase => 'Yar!')
|
||||
pirate.catchphrase = 'Ahoy!'
|
||||
|
||||
pirate.reset_catchphrase!
|
||||
assert_equal "Yar!", pirate.catchphrase
|
||||
assert_equal Hash.new, pirate.changes
|
||||
assert !pirate.catchphrase_changed?
|
||||
end
|
||||
|
||||
def test_nullable_number_not_marked_as_changed_if_new_value_is_blank
|
||||
pirate = Pirate.new
|
||||
|
||||
|
||||
@@ -251,7 +251,7 @@ class FinderTest < ActiveRecord::TestCase
|
||||
|
||||
def test_find_only_some_columns
|
||||
topic = Topic.find(1, :select => "author_name")
|
||||
assert_raise(ActiveRecord::MissingAttributeError) {topic.title}
|
||||
assert_raise(ActiveModel::MissingAttributeError) {topic.title}
|
||||
assert_equal "David", topic.author_name
|
||||
assert !topic.attribute_present?("title")
|
||||
#assert !topic.respond_to?("title")
|
||||
@@ -423,6 +423,42 @@ class FinderTest < ActiveRecord::TestCase
|
||||
assert_equal customers(:david), found_customer
|
||||
end
|
||||
|
||||
def test_condition_utc_time_interpolation_with_default_timezone_local
|
||||
with_env_tz 'America/New_York' do
|
||||
with_active_record_default_timezone :local do
|
||||
topic = Topic.first
|
||||
assert_equal topic, Topic.find(:first, :conditions => ['written_on = ?', topic.written_on.getutc])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_hash_condition_utc_time_interpolation_with_default_timezone_local
|
||||
with_env_tz 'America/New_York' do
|
||||
with_active_record_default_timezone :local do
|
||||
topic = Topic.first
|
||||
assert_equal topic, Topic.find(:first, :conditions => {:written_on => topic.written_on.getutc})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_condition_local_time_interpolation_with_default_timezone_utc
|
||||
with_env_tz 'America/New_York' do
|
||||
with_active_record_default_timezone :utc do
|
||||
topic = Topic.first
|
||||
assert_equal topic, Topic.find(:first, :conditions => ['written_on = ?', topic.written_on.getlocal])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_hash_condition_local_time_interpolation_with_default_timezone_utc
|
||||
with_env_tz 'America/New_York' do
|
||||
with_active_record_default_timezone :utc do
|
||||
topic = Topic.first
|
||||
assert_equal topic, Topic.find(:first, :conditions => {:written_on => topic.written_on.getlocal})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_bind_variables
|
||||
assert_kind_of Firm, Company.find(:first, :conditions => ["name = ?", "37signals"])
|
||||
assert_nil Company.find(:first, :conditions => ["name = ?", "37signals!"])
|
||||
@@ -1087,4 +1123,18 @@ class FinderTest < ActiveRecord::TestCase
|
||||
ActiveRecord::Base.send(:replace_bind_variables, statement, vars)
|
||||
end
|
||||
end
|
||||
|
||||
def with_env_tz(new_tz = 'US/Eastern')
|
||||
old_tz, ENV['TZ'] = ENV['TZ'], new_tz
|
||||
yield
|
||||
ensure
|
||||
old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
|
||||
end
|
||||
|
||||
def with_active_record_default_timezone(zone)
|
||||
old_zone, ActiveRecord::Base.default_timezone = ActiveRecord::Base.default_timezone, zone
|
||||
yield
|
||||
ensure
|
||||
ActiveRecord::Base.default_timezone = old_zone
|
||||
end
|
||||
end
|
||||
|
||||
42
activerecord/test/cases/state_machine_test.rb
Normal file
42
activerecord/test/cases/state_machine_test.rb
Normal file
@@ -0,0 +1,42 @@
|
||||
require 'cases/helper'
|
||||
require 'models/traffic_light'
|
||||
|
||||
class StateMachineTest < ActiveRecord::TestCase
|
||||
def setup
|
||||
@light = TrafficLight.create!
|
||||
end
|
||||
|
||||
test "states initial state" do
|
||||
assert @light.off?
|
||||
assert_equal :off, @light.current_state
|
||||
end
|
||||
|
||||
test "transition to a valid state" do
|
||||
@light.reset
|
||||
assert @light.red?
|
||||
assert_equal :red, @light.current_state
|
||||
|
||||
@light.green_on
|
||||
assert @light.green?
|
||||
assert_equal :green, @light.current_state
|
||||
end
|
||||
|
||||
test "transition does not persist state" do
|
||||
@light.reset
|
||||
assert_equal :red, @light.current_state
|
||||
@light.reload
|
||||
assert_equal "off", @light.state
|
||||
end
|
||||
|
||||
test "transition does persists state" do
|
||||
@light.reset!
|
||||
assert_equal :red, @light.current_state
|
||||
@light.reload
|
||||
assert_equal "red", @light.state
|
||||
end
|
||||
|
||||
test "transition to an invalid state" do
|
||||
assert_raise(ActiveModel::StateMachine::InvalidTransition) { @light.yellow_on }
|
||||
assert_equal :off, @light.current_state
|
||||
end
|
||||
end
|
||||
27
activerecord/test/models/traffic_light.rb
Normal file
27
activerecord/test/models/traffic_light.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
class TrafficLight < ActiveRecord::Base
|
||||
include ActiveRecord::StateMachine
|
||||
|
||||
state_machine do
|
||||
state :off
|
||||
|
||||
state :red
|
||||
state :green
|
||||
state :yellow
|
||||
|
||||
event :red_on do
|
||||
transitions :to => :red, :from => [:yellow]
|
||||
end
|
||||
|
||||
event :green_on do
|
||||
transitions :to => :green, :from => [:red]
|
||||
end
|
||||
|
||||
event :yellow_on do
|
||||
transitions :to => :yellow, :from => [:green]
|
||||
end
|
||||
|
||||
event :reset do
|
||||
transitions :to => :red, :from => [:off]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -448,6 +448,13 @@ ActiveRecord::Schema.define do
|
||||
t.integer :pet_id, :integer
|
||||
end
|
||||
|
||||
create_table :traffic_lights, :force => true do |t|
|
||||
t.string :location
|
||||
t.string :state
|
||||
t.datetime :created_at
|
||||
t.datetime :updated_at
|
||||
end
|
||||
|
||||
create_table :treasures, :force => true do |t|
|
||||
t.column :name, :string
|
||||
t.column :looter_id, :integer
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
unless '1.9'.respond_to?(:bytesize)
|
||||
class String
|
||||
alias :bytesize :size
|
||||
end
|
||||
end
|
||||
@@ -6,6 +6,7 @@
|
||||
=end
|
||||
|
||||
if RUBY_VERSION < '1.9'
|
||||
require 'active_support/core_ext/string/bytesize'
|
||||
|
||||
# KeyError is raised by String#% when the string contains a named placeholder
|
||||
# that is not contained in the given arguments hash. Ruby 1.9 includes and
|
||||
@@ -24,8 +25,6 @@ if RUBY_VERSION < '1.9'
|
||||
# the meaning of the msgids using "named argument" instead of %s/%d style.
|
||||
|
||||
class String
|
||||
# For older ruby versions, such as ruby-1.8.5
|
||||
alias :bytesize :size unless instance_methods.find {|m| m.to_s == 'bytesize'}
|
||||
alias :interpolate_without_ruby_19_syntax :% # :nodoc:
|
||||
|
||||
INTERPOLATION_PATTERN = Regexp.union(
|
||||
@@ -90,4 +89,4 @@ if RUBY_VERSION < '1.9'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -345,3 +345,10 @@ class TestGetTextString < Test::Unit::TestCase
|
||||
assert_raises(ArgumentError) { "%{name} %f" % [1.0, 2.0] }
|
||||
end
|
||||
end
|
||||
|
||||
class StringBytesizeTest < Test::Unit::TestCase
|
||||
def test_bytesize
|
||||
assert_respond_to 'foo', :bytesize
|
||||
assert_equal 3, 'foo'.bytesize
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,13 +3,6 @@ require 'action_controller'
|
||||
require 'fileutils'
|
||||
require 'optparse'
|
||||
|
||||
# TODO: Push Thin adapter upstream so we don't need worry about requiring it
|
||||
begin
|
||||
require_library_or_gem 'thin'
|
||||
rescue Exception
|
||||
# Thin not available
|
||||
end
|
||||
|
||||
options = {
|
||||
:Port => 3000,
|
||||
:Host => "0.0.0.0",
|
||||
|
||||
Reference in New Issue
Block a user